diff --git a/services/bright/.dockerignore b/services/bright/.dockerignore
new file mode 100644
index 0000000..61a7393
--- /dev/null
+++ b/services/bright/.dockerignore
@@ -0,0 +1,45 @@
+# This file excludes paths from the Docker build context.
+#
+# By default, Docker's build context includes all files (and folders) in the
+# current directory. Even if a file isn't copied into the container it is still sent to
+# the Docker daemon.
+#
+# There are multiple reasons to exclude files from the build context:
+#
+# 1. Prevent nested folders from being copied into the container (ex: exclude
+# /assets/node_modules when copying /assets)
+# 2. Reduce the size of the build context and improve build time (ex. /build, /deps, /doc)
+# 3. Avoid sending files containing sensitive information
+#
+# More information on using .dockerignore is available here:
+# https://docs.docker.com/engine/reference/builder/#dockerignore-file
+
+.dockerignore
+
+# Ignore git, but keep git HEAD and refs to access current commit hash if needed:
+#
+# $ cat .git/HEAD | awk '{print ".git/"$2}' | xargs cat
+# d0b8727759e1e0e7aa3d41707d12376e373d5ecc
+.git
+!.git/HEAD
+!.git/refs
+
+# Common development/test artifacts
+/cover/
+/doc/
+/test/
+/tmp/
+.elixir_ls
+
+# Mix artifacts
+/_build/
+/deps/
+*.ez
+
+# Generated on crash by the VM
+erl_crash.dump
+
+# Static artifacts - These should be fetched and built inside the Docker image
+/assets/node_modules/
+/priv/static/assets/
+/priv/static/cache_manifest.json
diff --git a/services/bright/.formatter.exs b/services/bright/.formatter.exs
new file mode 100644
index 0000000..ef8840c
--- /dev/null
+++ b/services/bright/.formatter.exs
@@ -0,0 +1,6 @@
+[
+ import_deps: [:ecto, :ecto_sql, :phoenix],
+ subdirectories: ["priv/*/migrations"],
+ plugins: [Phoenix.LiveView.HTMLFormatter],
+ inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"]
+]
diff --git a/services/bright/.gitignore b/services/bright/.gitignore
new file mode 100644
index 0000000..0c9c530
--- /dev/null
+++ b/services/bright/.gitignore
@@ -0,0 +1,37 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where 3rd-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Temporary files, for example, from tests.
+/tmp/
+
+# Ignore package tarball (built via "mix hex.build").
+bright-*.tar
+
+# Ignore assets that are produced by build tools.
+/priv/static/assets/
+
+# Ignore digested assets cache.
+/priv/static/cache_manifest.json
+
+# In case you use Node.js/npm, you want to ignore these.
+npm-debug.log
+/assets/node_modules/
+
diff --git a/services/bright/README.md b/services/bright/README.md
new file mode 100644
index 0000000..2f472de
--- /dev/null
+++ b/services/bright/README.md
@@ -0,0 +1,18 @@
+# Bright
+
+To start your Phoenix server:
+
+ * Run `mix setup` to install and setup dependencies
+ * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
+
+Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
+
+Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
+
+## Learn more
+
+ * Official website: https://www.phoenixframework.org/
+ * Guides: https://hexdocs.pm/phoenix/overview.html
+ * Docs: https://hexdocs.pm/phoenix
+ * Forum: https://elixirforum.com/c/phoenix-forum
+ * Source: https://github.com/phoenixframework/phoenix
diff --git a/services/bright/assets/css/app.css b/services/bright/assets/css/app.css
new file mode 100644
index 0000000..433a06e
--- /dev/null
+++ b/services/bright/assets/css/app.css
@@ -0,0 +1,7 @@
+/* @import "tailwindcss/base";
+@import "tailwindcss/components";
+@import "tailwindcss/utilities"; */
+
+@import "./bulma.min.css";
+
+/* This file is for your main application CSS */
diff --git a/services/bright/assets/css/app.scss b/services/bright/assets/css/app.scss
new file mode 100644
index 0000000..4e8cb6b
--- /dev/null
+++ b/services/bright/assets/css/app.scss
@@ -0,0 +1,4 @@
+/* This file is for your main application CSS */
+// @import "./phoenix.css";
+@import "bulma";
+
diff --git a/services/bright/assets/js/app.js b/services/bright/assets/js/app.js
new file mode 100644
index 0000000..d5e278a
--- /dev/null
+++ b/services/bright/assets/js/app.js
@@ -0,0 +1,44 @@
+// If you want to use Phoenix channels, run `mix help phx.gen.channel`
+// to get started and then uncomment the line below.
+// import "./user_socket.js"
+
+// You can include dependencies in two ways.
+//
+// The simplest option is to put them in assets/vendor and
+// import them using relative paths:
+//
+// import "../vendor/some-package.js"
+//
+// Alternatively, you can `npm install some-package --prefix assets` and import
+// them using a path starting with the package name:
+//
+// import "some-package"
+//
+
+// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
+import "phoenix_html"
+// Establish Phoenix Socket and LiveView configuration.
+import {Socket} from "phoenix"
+import {LiveSocket} from "phoenix_live_view"
+import topbar from "../vendor/topbar"
+
+let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
+let liveSocket = new LiveSocket("/live", Socket, {
+ longPollFallbackMs: 2500,
+ params: {_csrf_token: csrfToken}
+})
+
+// Show progress bar on live navigation and form submits
+topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
+window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
+window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
+
+// connect if there are any LiveViews on the page
+liveSocket.connect()
+
+// expose liveSocket on window for web console debug logs and latency simulation:
+// >> liveSocket.enableDebug()
+// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
+// >> liveSocket.disableLatencySim()
+window.liveSocket = liveSocket
+
diff --git a/services/bright/assets/tailwind.config.js b/services/bright/assets/tailwind.config.js
new file mode 100644
index 0000000..df37d6d
--- /dev/null
+++ b/services/bright/assets/tailwind.config.js
@@ -0,0 +1,74 @@
+// See the Tailwind configuration guide for advanced usage
+// https://tailwindcss.com/docs/configuration
+
+const plugin = require("tailwindcss/plugin")
+const fs = require("fs")
+const path = require("path")
+
+module.exports = {
+ content: [
+ "./js/**/*.js",
+ "../lib/bright_web.ex",
+ "../lib/bright_web/**/*.*ex"
+ ],
+ theme: {
+ extend: {
+ colors: {
+ brand: "#FD4F00",
+ }
+ },
+ },
+ plugins: [
+ require("@tailwindcss/forms"),
+ // Allows prefixing tailwind classes with LiveView classes to add rules
+ // only when LiveView classes are applied, for example:
+ //
+ //
+ //
+ plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])),
+ plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])),
+ plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])),
+
+ // Embeds Heroicons (https://heroicons.com) into your app.css bundle
+ // See your `CoreComponents.icon/1` for more information.
+ //
+ plugin(function({matchComponents, theme}) {
+ let iconsDir = path.join(__dirname, "../deps/heroicons/optimized")
+ let values = {}
+ let icons = [
+ ["", "/24/outline"],
+ ["-solid", "/24/solid"],
+ ["-mini", "/20/solid"],
+ ["-micro", "/16/solid"]
+ ]
+ icons.forEach(([suffix, dir]) => {
+ fs.readdirSync(path.join(iconsDir, dir)).forEach(file => {
+ let name = path.basename(file, ".svg") + suffix
+ values[name] = {name, fullPath: path.join(iconsDir, dir, file)}
+ })
+ })
+ matchComponents({
+ "hero": ({name, fullPath}) => {
+ let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "")
+ let size = theme("spacing.6")
+ if (name.endsWith("-mini")) {
+ size = theme("spacing.5")
+ } else if (name.endsWith("-micro")) {
+ size = theme("spacing.4")
+ }
+ return {
+ [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
+ "-webkit-mask": `var(--hero-${name})`,
+ "mask": `var(--hero-${name})`,
+ "mask-repeat": "no-repeat",
+ "background-color": "currentColor",
+ "vertical-align": "middle",
+ "display": "inline-block",
+ "width": size,
+ "height": size
+ }
+ }
+ }, {values})
+ })
+ ]
+}
diff --git a/services/bright/assets/vendor/topbar.js b/services/bright/assets/vendor/topbar.js
new file mode 100644
index 0000000..4195727
--- /dev/null
+++ b/services/bright/assets/vendor/topbar.js
@@ -0,0 +1,165 @@
+/**
+ * @license MIT
+ * topbar 2.0.0, 2023-02-04
+ * https://buunguyen.github.io/topbar
+ * Copyright (c) 2021 Buu Nguyen
+ */
+(function (window, document) {
+ "use strict";
+
+ // https://gist.github.com/paulirish/1579671
+ (function () {
+ var lastTime = 0;
+ var vendors = ["ms", "moz", "webkit", "o"];
+ for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+ window.requestAnimationFrame =
+ window[vendors[x] + "RequestAnimationFrame"];
+ window.cancelAnimationFrame =
+ window[vendors[x] + "CancelAnimationFrame"] ||
+ window[vendors[x] + "CancelRequestAnimationFrame"];
+ }
+ if (!window.requestAnimationFrame)
+ window.requestAnimationFrame = function (callback, element) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = window.setTimeout(function () {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+ if (!window.cancelAnimationFrame)
+ window.cancelAnimationFrame = function (id) {
+ clearTimeout(id);
+ };
+ })();
+
+ var canvas,
+ currentProgress,
+ showing,
+ progressTimerId = null,
+ fadeTimerId = null,
+ delayTimerId = null,
+ addEvent = function (elem, type, handler) {
+ if (elem.addEventListener) elem.addEventListener(type, handler, false);
+ else if (elem.attachEvent) elem.attachEvent("on" + type, handler);
+ else elem["on" + type] = handler;
+ },
+ options = {
+ autoRun: true,
+ barThickness: 3,
+ barColors: {
+ 0: "rgba(26, 188, 156, .9)",
+ ".25": "rgba(52, 152, 219, .9)",
+ ".50": "rgba(241, 196, 15, .9)",
+ ".75": "rgba(230, 126, 34, .9)",
+ "1.0": "rgba(211, 84, 0, .9)",
+ },
+ shadowBlur: 10,
+ shadowColor: "rgba(0, 0, 0, .6)",
+ className: null,
+ },
+ repaint = function () {
+ canvas.width = window.innerWidth;
+ canvas.height = options.barThickness * 5; // need space for shadow
+
+ var ctx = canvas.getContext("2d");
+ ctx.shadowBlur = options.shadowBlur;
+ ctx.shadowColor = options.shadowColor;
+
+ var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
+ for (var stop in options.barColors)
+ lineGradient.addColorStop(stop, options.barColors[stop]);
+ ctx.lineWidth = options.barThickness;
+ ctx.beginPath();
+ ctx.moveTo(0, options.barThickness / 2);
+ ctx.lineTo(
+ Math.ceil(currentProgress * canvas.width),
+ options.barThickness / 2
+ );
+ ctx.strokeStyle = lineGradient;
+ ctx.stroke();
+ },
+ createCanvas = function () {
+ canvas = document.createElement("canvas");
+ var style = canvas.style;
+ style.position = "fixed";
+ style.top = style.left = style.right = style.margin = style.padding = 0;
+ style.zIndex = 100001;
+ style.display = "none";
+ if (options.className) canvas.classList.add(options.className);
+ document.body.appendChild(canvas);
+ addEvent(window, "resize", repaint);
+ },
+ topbar = {
+ config: function (opts) {
+ for (var key in opts)
+ if (options.hasOwnProperty(key)) options[key] = opts[key];
+ },
+ show: function (delay) {
+ if (showing) return;
+ if (delay) {
+ if (delayTimerId) return;
+ delayTimerId = setTimeout(() => topbar.show(), delay);
+ } else {
+ showing = true;
+ if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId);
+ if (!canvas) createCanvas();
+ canvas.style.opacity = 1;
+ canvas.style.display = "block";
+ topbar.progress(0);
+ if (options.autoRun) {
+ (function loop() {
+ progressTimerId = window.requestAnimationFrame(loop);
+ topbar.progress(
+ "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2)
+ );
+ })();
+ }
+ }
+ },
+ progress: function (to) {
+ if (typeof to === "undefined") return currentProgress;
+ if (typeof to === "string") {
+ to =
+ (to.indexOf("+") >= 0 || to.indexOf("-") >= 0
+ ? currentProgress
+ : 0) + parseFloat(to);
+ }
+ currentProgress = to > 1 ? 1 : to;
+ repaint();
+ return currentProgress;
+ },
+ hide: function () {
+ clearTimeout(delayTimerId);
+ delayTimerId = null;
+ if (!showing) return;
+ showing = false;
+ if (progressTimerId != null) {
+ window.cancelAnimationFrame(progressTimerId);
+ progressTimerId = null;
+ }
+ (function loop() {
+ if (topbar.progress("+.1") >= 1) {
+ canvas.style.opacity -= 0.05;
+ if (canvas.style.opacity <= 0.05) {
+ canvas.style.display = "none";
+ fadeTimerId = null;
+ return;
+ }
+ }
+ fadeTimerId = window.requestAnimationFrame(loop);
+ })();
+ },
+ };
+
+ if (typeof module === "object" && typeof module.exports === "object") {
+ module.exports = topbar;
+ } else if (typeof define === "function" && define.amd) {
+ define(function () {
+ return topbar;
+ });
+ } else {
+ this.topbar = topbar;
+ }
+}.call(this, window, document));
diff --git a/services/bright/config/config.exs b/services/bright/config/config.exs
new file mode 100644
index 0000000..f1d068c
--- /dev/null
+++ b/services/bright/config/config.exs
@@ -0,0 +1,79 @@
+# This file is responsible for configuring your application
+# and its dependencies with the aid of the Config module.
+#
+# This configuration file is loaded before any dependency and
+# is restricted to this project.
+
+# General application configuration
+import Config
+
+config :bright,
+ ecto_repos: [Bright.Repo],
+ generators: [timestamp_type: :utc_datetime]
+
+# Configures the endpoint
+config :bright, BrightWeb.Endpoint,
+ url: [host: "localhost"],
+ adapter: Bandit.PhoenixAdapter,
+ render_errors: [
+ formats: [html: BrightWeb.ErrorHTML, json: BrightWeb.ErrorJSON],
+ layout: false
+ ],
+ pubsub_server: Bright.PubSub,
+ live_view: [signing_salt: "JGNufzrG"]
+
+
+
+
+
+# Configures the mailer
+#
+# By default it uses the "Local" adapter which stores the emails
+# locally. You can see the emails in your browser, at "/dev/mailbox".
+#
+# For production it's recommended to configure a different adapter
+# at the `config/runtime.exs`.
+config :bright, Bright.Mailer, adapter: Swoosh.Adapters.Local
+
+# Configure esbuild (the version is required)
+config :esbuild,
+ version: "0.17.11",
+ bright: [
+ args:
+ ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
+ cd: Path.expand("../assets", __DIR__),
+ env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
+ ]
+
+# Configure dart_sass, used for bulma
+ config :dart_sass,
+ version: "1.61.0",
+ default: [
+ args: ~w(--load-path=../deps/bulma css:../priv/static/assets),
+ cd: Path.expand("../assets", __DIR__)
+ ]
+
+
+# # Configure tailwind (the version is required)
+# config :tailwind,
+# version: "3.4.3",
+# bright: [
+# args: ~w(
+# --config=tailwind.config.js
+# --input=css/app.css
+# --output=../priv/static/assets/app.css
+# ),
+# cd: Path.expand("../assets", __DIR__)
+# ]
+
+# Configures Elixir's Logger
+config :logger, :console,
+ format: "$time $metadata[$level] $message\n",
+ metadata: [:request_id]
+
+# Use Jason for JSON parsing in Phoenix
+config :phoenix, :json_library, Jason
+
+# Import environment specific config. This must remain at the bottom
+# of this file so it overrides the configuration defined above.
+import_config "#{config_env()}.exs"
diff --git a/services/bright/config/dev.exs b/services/bright/config/dev.exs
new file mode 100644
index 0000000..5585617
--- /dev/null
+++ b/services/bright/config/dev.exs
@@ -0,0 +1,87 @@
+import Config
+
+# Configure the database
+config :bright, Bright.Repo,
+ url: "#{System.get_env("DATABASE_URL")}",
+ stacktrace: true,
+ show_sensitive_data_on_connection_error: true,
+ pool_size: 10
+
+# For development, we disable any cache and enable
+# debugging and code reloading.
+#
+# The watchers configuration can be used to run external
+# watchers to your application. For example, we can use it
+# to bundle .js and .css sources.
+config :bright, BrightWeb.Endpoint,
+ # Binding to loopback ipv4 address prevents access from other machines.
+ # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
+ http: [ip: {0, 0, 0, 0}, port: "#{System.get_env("PORT")}"],
+ check_origin: false,
+ code_reloader: true,
+ debug_errors: true,
+ secret_key_base: "#{System.get_env("SECRET_KEY_BASE")}",
+ watchers: [
+ esbuild: {Esbuild, :install_and_run, [:bright, ~w(--sourcemap=inline --watch)]},
+ # tailwind: {Tailwind, :install_and_run, [:bright, ~w(--watch)]},
+ sass: {
+ DartSass,
+ :install_and_run,
+ [:default, ~w(--embed-source-map --source-map-urls=absolute --watch)]
+ }
+ ]
+
+# ## SSL Support
+#
+# In order to use HTTPS in development, a self-signed
+# certificate can be generated by running the following
+# Mix task:
+#
+# mix phx.gen.cert
+#
+# Run `mix help phx.gen.cert` for more information.
+#
+# The `http:` config above can be replaced with:
+#
+# https: [
+# port: 4001,
+# cipher_suite: :strong,
+# keyfile: "priv/cert/selfsigned_key.pem",
+# certfile: "priv/cert/selfsigned.pem"
+# ],
+#
+# If desired, both `http:` and `https:` keys can be
+# configured to run both http and https servers on
+# different ports.
+
+# Watch static and templates for browser reloading.
+config :bright, BrightWeb.Endpoint,
+ live_reload: [
+ patterns: [
+ ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"priv/gettext/.*(po)$",
+ ~r"lib/bright_web/(controllers|live|components)/.*(ex|heex)$"
+ ]
+ ]
+
+# Enable dev routes for dashboard and mailbox
+config :bright, dev_routes: true
+
+# Do not include metadata nor timestamps in development logs
+config :logger, :console, format: "[$level] $message\n"
+
+# Set a higher stacktrace during development. Avoid configuring such
+# in production as building large stacktraces may be expensive.
+config :phoenix, :stacktrace_depth, 20
+
+# Initialize plugs at runtime for faster development compilation
+config :phoenix, :plug_init_mode, :runtime
+
+config :phoenix_live_view,
+ # Include HEEx debug annotations as HTML comments in rendered markup
+ debug_heex_annotations: true,
+ # Enable helpful, but potentially expensive runtime checks
+ enable_expensive_runtime_checks: true
+
+# Disable swoosh api client as it is only required for production adapters.
+config :swoosh, :api_client, false
diff --git a/services/bright/config/prod.exs b/services/bright/config/prod.exs
new file mode 100644
index 0000000..1e8548c
--- /dev/null
+++ b/services/bright/config/prod.exs
@@ -0,0 +1,20 @@
+import Config
+
+# Note we also include the path to a cache manifest
+# containing the digested version of static files. This
+# manifest is generated by the `mix assets.deploy` task,
+# which you should run after static files are built and
+# before starting your production server.
+config :bright, BrightWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json"
+
+# Configures Swoosh API Client
+config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: Bright.Finch
+
+# Disable Swoosh Local Memory Storage
+config :swoosh, local: false
+
+# Do not print debug messages in production
+config :logger, level: :info
+
+# Runtime production configuration, including reading
+# of environment variables, is done on config/runtime.exs.
diff --git a/services/bright/config/runtime.exs b/services/bright/config/runtime.exs
new file mode 100644
index 0000000..427306b
--- /dev/null
+++ b/services/bright/config/runtime.exs
@@ -0,0 +1,117 @@
+import Config
+
+# config/runtime.exs is executed for all environments, including
+# during releases. It is executed after compilation and before the
+# system starts, so it is typically used to load production configuration
+# and secrets from environment variables or elsewhere. Do not define
+# any compile-time configuration in here, as it won't be applied.
+# The block below contains prod specific runtime configuration.
+
+# ## Using releases
+#
+# If you use `mix release`, you need to explicitly enable the server
+# by passing the PHX_SERVER=true when you start it:
+#
+# PHX_SERVER=true bin/bright start
+#
+# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
+# script that automatically sets the env var above.
+if System.get_env("PHX_SERVER") do
+ config :bright, BrightWeb.Endpoint, server: true
+end
+
+if config_env() == :prod do
+ database_url =
+ System.get_env("DATABASE_URL") ||
+ raise """
+ environment variable DATABASE_URL is missing.
+ For example: ecto://USER:PASS@HOST/DATABASE
+ """
+
+ maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
+
+ config :bright, Bright.Repo,
+ # ssl: true,
+ url: database_url,
+ pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
+ socket_options: maybe_ipv6
+
+ # The secret key base is used to sign/encrypt cookies and other secrets.
+ # A default value is used in config/dev.exs and config/test.exs but you
+ # want to use a different value for prod and you most likely don't want
+ # to check this value into version control, so we use an environment
+ # variable instead.
+ secret_key_base =
+ System.get_env("SECRET_KEY_BASE") ||
+ raise """
+ environment variable SECRET_KEY_BASE is missing.
+ You can generate one by calling: mix phx.gen.secret
+ """
+
+ host = System.get_env("PHX_HOST") || "example.com"
+ port = String.to_integer(System.get_env("PORT") || "4000")
+
+ config :bright, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
+
+ config :bright, BrightWeb.Endpoint,
+ url: [host: host, port: 443, scheme: "https"],
+ http: [
+ # Enable IPv6 and bind on all interfaces.
+ # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
+ # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
+ # for details about using IPv6 vs IPv4 and loopback vs public addresses.
+ ip: {0, 0, 0, 0, 0, 0, 0, 0},
+ port: port
+ ],
+ secret_key_base: secret_key_base
+
+ # ## SSL Support
+ #
+ # To get SSL working, you will need to add the `https` key
+ # to your endpoint configuration:
+ #
+ # config :bright, BrightWeb.Endpoint,
+ # https: [
+ # ...,
+ # port: 443,
+ # cipher_suite: :strong,
+ # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
+ # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
+ # ]
+ #
+ # The `cipher_suite` is set to `:strong` to support only the
+ # latest and more secure SSL ciphers. This means old browsers
+ # and clients may not be supported. You can set it to
+ # `:compatible` for wider support.
+ #
+ # `:keyfile` and `:certfile` expect an absolute path to the key
+ # and cert in disk or a relative path inside priv, for example
+ # "priv/ssl/server.key". For all supported SSL configuration
+ # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
+ #
+ # We also recommend setting `force_ssl` in your config/prod.exs,
+ # ensuring no data is ever sent via http, always redirecting to https:
+ #
+ # config :bright, BrightWeb.Endpoint,
+ # force_ssl: [hsts: true]
+ #
+ # Check `Plug.SSL` for all available options in `force_ssl`.
+
+ # ## Configuring the mailer
+ #
+ # In production you need to configure the mailer to use a different adapter.
+ # Also, you may need to configure the Swoosh API client of your choice if you
+ # are not using SMTP. Here is an example of the configuration:
+ #
+ # config :bright, Bright.Mailer,
+ # adapter: Swoosh.Adapters.Mailgun,
+ # api_key: System.get_env("MAILGUN_API_KEY"),
+ # domain: System.get_env("MAILGUN_DOMAIN")
+ #
+ # For this example you need include a HTTP client required by Swoosh API client.
+ # Swoosh supports Hackney and Finch out of the box:
+ #
+ # config :swoosh, :api_client, Swoosh.ApiClient.Hackney
+ #
+ # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
+end
diff --git a/services/bright/config/test.exs b/services/bright/config/test.exs
new file mode 100644
index 0000000..8935c25
--- /dev/null
+++ b/services/bright/config/test.exs
@@ -0,0 +1,35 @@
+import Config
+
+# Configure the database
+#
+# The MIX_TEST_PARTITION environment variable can be used
+# to provide built-in test partitioning in CI environment.
+# Run `mix help test` for more information.
+config :bright, Bright.Repo,
+ url: "#{System.get_env("DATABASE_URL")}",
+ database: "bright_test#{System.get_env("MIX_TEST_PARTITION")}",
+ pool: Ecto.Adapters.SQL.Sandbox,
+ pool_size: System.schedulers_online() * 2
+
+# We don't run a server during test. If one is required,
+# you can enable the server option below.
+config :bright, BrightWeb.Endpoint,
+ http: [ip: {127, 0, 0, 1}, port: 4002],
+ secret_key_base: "#{System.get_env("SECRET_KEY_BASE")}",
+ server: false
+
+# In test we don't send emails
+config :bright, Bright.Mailer, adapter: Swoosh.Adapters.Test
+
+# Disable swoosh api client as it is only required for production adapters
+config :swoosh, :api_client, false
+
+# Print only warnings and errors during test
+config :logger, level: :warning
+
+# Initialize plugs at runtime for faster test compilation
+config :phoenix, :plug_init_mode, :runtime
+
+# Enable helpful, but potentially expensive runtime checks
+config :phoenix_live_view,
+ enable_expensive_runtime_checks: true
diff --git a/services/bright/lib/bright.ex b/services/bright/lib/bright.ex
new file mode 100644
index 0000000..55d586e
--- /dev/null
+++ b/services/bright/lib/bright.ex
@@ -0,0 +1,9 @@
+defmodule Bright do
+ @moduledoc """
+ Bright keeps the contexts that define your domain
+ and business logic.
+
+ Contexts are also responsible for managing your data, regardless
+ if it comes from the database, an external API or others.
+ """
+end
diff --git a/services/bright/lib/bright/application.ex b/services/bright/lib/bright/application.ex
new file mode 100644
index 0000000..cae4975
--- /dev/null
+++ b/services/bright/lib/bright/application.ex
@@ -0,0 +1,36 @@
+defmodule Bright.Application do
+ # See https://hexdocs.pm/elixir/Application.html
+ # for more information on OTP Applications
+ @moduledoc false
+
+ use Application
+
+ @impl true
+ def start(_type, _args) do
+ children = [
+ BrightWeb.Telemetry,
+ Bright.Repo,
+ {DNSCluster, query: Application.get_env(:bright, :dns_cluster_query) || :ignore},
+ {Phoenix.PubSub, name: Bright.PubSub},
+ # Start the Finch HTTP client for sending emails
+ {Finch, name: Bright.Finch},
+ # Start a worker by calling: Bright.Worker.start_link(arg)
+ # {Bright.Worker, arg},
+ # Start to serve requests, typically the last entry
+ BrightWeb.Endpoint
+ ]
+
+ # See https://hexdocs.pm/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: Bright.Supervisor]
+ Supervisor.start_link(children, opts)
+ end
+
+ # Tell Phoenix to update the endpoint configuration
+ # whenever the application is updated.
+ @impl true
+ def config_change(changed, _new, removed) do
+ BrightWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+end
diff --git a/services/bright/lib/bright/blog.ex b/services/bright/lib/bright/blog.ex
new file mode 100644
index 0000000..2e891fb
--- /dev/null
+++ b/services/bright/lib/bright/blog.ex
@@ -0,0 +1,104 @@
+defmodule Bright.Blog do
+ @moduledoc """
+ The Blog context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Bright.Repo
+
+ alias Bright.Blog.Post
+
+ @doc """
+ Returns the list of posts.
+
+ ## Examples
+
+ iex> list_posts()
+ [%Post{}, ...]
+
+ """
+ def list_posts do
+ Repo.all(Post)
+ end
+
+ @doc """
+ Gets a single post.
+
+ Raises `Ecto.NoResultsError` if the Post does not exist.
+
+ ## Examples
+
+ iex> get_post!(123)
+ %Post{}
+
+ iex> get_post!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_post!(id), do: Repo.get!(Post, id)
+
+ @doc """
+ Creates a post.
+
+ ## Examples
+
+ iex> create_post(%{field: value})
+ {:ok, %Post{}}
+
+ iex> create_post(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_post(attrs \\ %{}) do
+ %Post{}
+ |> Post.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a post.
+
+ ## Examples
+
+ iex> update_post(post, %{field: new_value})
+ {:ok, %Post{}}
+
+ iex> update_post(post, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_post(%Post{} = post, attrs) do
+ post
+ |> Post.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a post.
+
+ ## Examples
+
+ iex> delete_post(post)
+ {:ok, %Post{}}
+
+ iex> delete_post(post)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_post(%Post{} = post) do
+ Repo.delete(post)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking post changes.
+
+ ## Examples
+
+ iex> change_post(post)
+ %Ecto.Changeset{data: %Post{}}
+
+ """
+ def change_post(%Post{} = post, attrs \\ %{}) do
+ Post.changeset(post, attrs)
+ end
+end
diff --git a/services/bright/lib/bright/blog/post.ex b/services/bright/lib/bright/blog/post.ex
new file mode 100644
index 0000000..e5bc353
--- /dev/null
+++ b/services/bright/lib/bright/blog/post.ex
@@ -0,0 +1,18 @@
+defmodule Bright.Blog.Post do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "posts" do
+ field :title, :string
+ field :body, :string
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(post, attrs) do
+ post
+ |> cast(attrs, [:title, :body])
+ |> validate_required([:title, :body])
+ end
+end
diff --git a/services/bright/lib/bright/catalog.ex b/services/bright/lib/bright/catalog.ex
new file mode 100644
index 0000000..5ced820
--- /dev/null
+++ b/services/bright/lib/bright/catalog.ex
@@ -0,0 +1,214 @@
+defmodule Bright.Catalog do
+ @moduledoc """
+ The Catalog context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Bright.Repo
+
+ alias Bright.Catalog.Product
+ alias Bright.Catalog.Category
+
+ @doc """
+ Returns the list of products.
+
+ ## Examples
+
+ iex> list_products()
+ [%Product{}, ...]
+
+ """
+ def list_products do
+ Repo.all(Product)
+ end
+
+ @doc """
+ Gets a single product.
+
+ Raises `Ecto.NoResultsError` if the Product does not exist.
+
+ ## Examples
+
+ iex> get_product!(123)
+ %Product{}
+
+ iex> get_product!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_product!(id) do
+ Product
+ |> Repo.get!(id)
+ |> Repo.preload(:categories)
+ end
+
+ @doc """
+ Creates a product.
+
+ ## Examples
+
+ iex> create_product(%{field: value})
+ {:ok, %Product{}}
+
+ iex> create_product(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_product(attrs \\ %{}) do
+ %Product{}
+ |> change_product(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a product.
+
+ ## Examples
+
+ iex> update_product(product, %{field: new_value})
+ {:ok, %Product{}}
+
+ iex> update_product(product, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_product(%Product{} = product, attrs) do
+ product
+ |> change_product(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a product.
+
+ ## Examples
+
+ iex> delete_product(product)
+ {:ok, %Product{}}
+
+ iex> delete_product(product)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_product(%Product{} = product) do
+ Repo.delete(product)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking product changes.
+
+ ## Examples
+
+ iex> change_product(product)
+ %Ecto.Changeset{data: %Product{}}
+
+ """
+ def change_product(%Product{} = product, attrs \\ %{}) do
+ categories = list_categories_by_id(attrs["category_ids"])
+ product
+ |> Repo.preload(:categories)
+ |> Product.changeset(attrs)
+ |> Ecto.Changeset.put_assoc(:categories, categories)
+ end
+
+
+ @doc """
+ Returns the list of categories.
+
+ ## Examples
+
+ iex> list_categories()
+ [%Category{}, ...]
+
+ """
+ def list_categories do
+ Repo.all(Category)
+ end
+
+
+ @doc """
+ Gets a single category.
+
+ Raises `Ecto.NoResultsError` if the Category does not exist.
+
+ ## Examples
+
+ iex> get_category!(123)
+ %Category{}
+
+ iex> get_category!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_category!(id), do: Repo.get!(Category, id)
+
+ @doc """
+ Creates a category.
+
+ ## Examples
+
+ iex> create_category(%{field: value})
+ {:ok, %Category{}}
+
+ iex> create_category(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_category(attrs \\ %{}) do
+ %Category{}
+ |> Category.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a category.
+
+ ## Examples
+
+ iex> update_category(category, %{field: new_value})
+ {:ok, %Category{}}
+
+ iex> update_category(category, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_category(%Category{} = category, attrs) do
+ category
+ |> Category.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a category.
+
+ ## Examples
+
+ iex> delete_category(category)
+ {:ok, %Category{}}
+
+ iex> delete_category(category)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_category(%Category{} = category) do
+ Repo.delete(category)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking category changes.
+
+ ## Examples
+
+ iex> change_category(category)
+ %Ecto.Changeset{data: %Category{}}
+
+ """
+ def change_category(%Category{} = category, attrs \\ %{}) do
+ Category.changeset(category, attrs)
+ end
+
+ def list_categories_by_id(nil), do: []
+ def list_categories_by_id(category_ids) do
+ Repo.all(from c in Category, where: c.id in ^category_ids)
+ end
+end
diff --git a/services/bright/lib/bright/catalog/category.ex b/services/bright/lib/bright/catalog/category.ex
new file mode 100644
index 0000000..50ac10a
--- /dev/null
+++ b/services/bright/lib/bright/catalog/category.ex
@@ -0,0 +1,18 @@
+defmodule Bright.Catalog.Category do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "categories" do
+ field :title, :string
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(category, attrs) do
+ category
+ |> cast(attrs, [:title])
+ |> validate_required([:title])
+ |> unique_constraint(:title)
+ end
+end
diff --git a/services/bright/lib/bright/catalog/product.ex b/services/bright/lib/bright/catalog/product.ex
new file mode 100644
index 0000000..cee7218
--- /dev/null
+++ b/services/bright/lib/bright/catalog/product.ex
@@ -0,0 +1,23 @@
+defmodule Bright.Catalog.Product do
+ use Ecto.Schema
+ import Ecto.Changeset
+ alias Bright.Catalog.Category
+
+ schema "products" do
+ field :description, :string
+ field :title, :string
+ field :price, :decimal
+ field :views, :integer
+
+ many_to_many :categories, Category, join_through: "product_categories", on_replace: :delete
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(product, attrs) do
+ product
+ |> cast(attrs, [:title, :description, :price, :views])
+ |> validate_required([:title, :description, :price])
+ end
+end
diff --git a/services/bright/lib/bright/mailer.ex b/services/bright/lib/bright/mailer.ex
new file mode 100644
index 0000000..1c82853
--- /dev/null
+++ b/services/bright/lib/bright/mailer.ex
@@ -0,0 +1,3 @@
+defmodule Bright.Mailer do
+ use Swoosh.Mailer, otp_app: :bright
+end
diff --git a/services/bright/lib/bright/orders.ex b/services/bright/lib/bright/orders.ex
new file mode 100644
index 0000000..458506b
--- /dev/null
+++ b/services/bright/lib/bright/orders.ex
@@ -0,0 +1,231 @@
+defmodule Bright.Orders do
+ @moduledoc """
+ The Orders context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Bright.Repo
+
+ alias Bright.Orders.{Order,LineItem}
+ alias Bright.ShoppingCart
+
+ @doc """
+ Returns the list of orders.
+
+ ## Examples
+
+ iex> list_orders()
+ [%Order{}, ...]
+
+ """
+ def list_orders do
+ Repo.all(Order)
+ end
+
+ @doc """
+ Gets a single order.
+
+ Raises `Ecto.NoResultsError` if the Order does not exist.
+
+ ## Examples
+
+ iex> get_order!(123)
+ %Order{}
+
+ iex> get_order!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ # def get_order!(id), do: Repo.get!(Order, id)
+ def get_order!(user_uuid, id) do
+ Order
+ |> Repo.get_by!(id: id, user_uuid: user_uuid)
+ |> Repo.preload([line_items: [:product]])
+ end
+
+ @doc """
+ Creates a order.
+
+ ## Examples
+
+ iex> create_order(%{field: value})
+ {:ok, %Order{}}
+
+ iex> create_order(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_order(attrs \\ %{}) do
+ %Order{}
+ |> Order.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a order.
+
+ ## Examples
+
+ iex> update_order(order, %{field: new_value})
+ {:ok, %Order{}}
+
+ iex> update_order(order, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_order(%Order{} = order, attrs) do
+ order
+ |> Order.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a order.
+
+ ## Examples
+
+ iex> delete_order(order)
+ {:ok, %Order{}}
+
+ iex> delete_order(order)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_order(%Order{} = order) do
+ Repo.delete(order)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking order changes.
+
+ ## Examples
+
+ iex> change_order(order)
+ %Ecto.Changeset{data: %Order{}}
+
+ """
+ def change_order(%Order{} = order, attrs \\ %{}) do
+ Order.changeset(order, attrs)
+ end
+
+ alias Bright.Orders.LineItem
+
+ @doc """
+ Returns the list of order_line_items.
+
+ ## Examples
+
+ iex> list_order_line_items()
+ [%LineItem{}, ...]
+
+ """
+ def list_order_line_items do
+ Repo.all(LineItem)
+ end
+
+ @doc """
+ Gets a single line_item.
+
+ Raises `Ecto.NoResultsError` if the Line item does not exist.
+
+ ## Examples
+
+ iex> get_line_item!(123)
+ %LineItem{}
+
+ iex> get_line_item!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_line_item!(id), do: Repo.get!(LineItem, id)
+
+ @doc """
+ Creates a line_item.
+
+ ## Examples
+
+ iex> create_line_item(%{field: value})
+ {:ok, %LineItem{}}
+
+ iex> create_line_item(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_line_item(attrs \\ %{}) do
+ %LineItem{}
+ |> LineItem.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a line_item.
+
+ ## Examples
+
+ iex> update_line_item(line_item, %{field: new_value})
+ {:ok, %LineItem{}}
+
+ iex> update_line_item(line_item, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_line_item(%LineItem{} = line_item, attrs) do
+ line_item
+ |> LineItem.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a line_item.
+
+ ## Examples
+
+ iex> delete_line_item(line_item)
+ {:ok, %LineItem{}}
+
+ iex> delete_line_item(line_item)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_line_item(%LineItem{} = line_item) do
+ Repo.delete(line_item)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking line_item changes.
+
+ ## Examples
+
+ iex> change_line_item(line_item)
+ %Ecto.Changeset{data: %LineItem{}}
+
+ """
+ def change_line_item(%LineItem{} = line_item, attrs \\ %{}) do
+ LineItem.changeset(line_item, attrs)
+ end
+
+ def complete_order(%ShoppingCart.Cart{} = cart) do
+ line_items =
+ Enum.map(cart.items, fn item ->
+ %{product_id: item.product_id, price: item.product.price, quantity: item.quantity}
+ end)
+
+ order =
+ Ecto.Changeset.change(%Order{},
+ user_uuid: cart.user_uuid,
+ total_price: ShoppingCart.total_cart_price(cart),
+ line_items: line_items
+ )
+
+ Ecto.Multi.new()
+ |> Ecto.Multi.insert(:order, order)
+ |> Ecto.Multi.run(:prune_cart, fn _repo, _changes ->
+ ShoppingCart.prune_cart_items(cart)
+ end)
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{order: order}} -> {:ok, order}
+ {:error, name, value, _changes_so_far} -> {:error, {name, value}}
+ end
+ end
+end
diff --git a/services/bright/lib/bright/orders/line_item.ex b/services/bright/lib/bright/orders/line_item.ex
new file mode 100644
index 0000000..a94009f
--- /dev/null
+++ b/services/bright/lib/bright/orders/line_item.ex
@@ -0,0 +1,21 @@
+defmodule Bright.Orders.LineItem do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "order_line_items" do
+ field :price, :decimal
+ field :quantity, :integer
+
+ belongs_to :order, Bright.Orders.Order
+ belongs_to :product, Bright.Catalog.Product
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(line_item, attrs) do
+ line_item
+ |> cast(attrs, [:price, :quantity])
+ |> validate_required([:price, :quantity])
+ end
+end
diff --git a/services/bright/lib/bright/orders/order.ex b/services/bright/lib/bright/orders/order.ex
new file mode 100644
index 0000000..44c5ba7
--- /dev/null
+++ b/services/bright/lib/bright/orders/order.ex
@@ -0,0 +1,21 @@
+defmodule Bright.Orders.Order do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "orders" do
+ field :user_uuid, Ecto.UUID
+ field :total_price, :decimal
+
+ has_many :line_items, Bright.Orders.LineItem
+ has_many :products, through: [:line_items, :product]
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(order, attrs) do
+ order
+ |> cast(attrs, [:user_uuid, :total_price])
+ |> validate_required([:user_uuid, :total_price])
+ end
+end
diff --git a/services/bright/lib/bright/patrons.ex b/services/bright/lib/bright/patrons.ex
new file mode 100644
index 0000000..a7f4aeb
--- /dev/null
+++ b/services/bright/lib/bright/patrons.ex
@@ -0,0 +1,104 @@
+defmodule Bright.Patrons do
+ @moduledoc """
+ The Patrons context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Bright.Repo
+
+ alias Bright.Patrons.Patron
+
+ @doc """
+ Returns the list of patrons.
+
+ ## Examples
+
+ iex> list_patrons()
+ [%Patron{}, ...]
+
+ """
+ def list_patrons do
+ Repo.all(Patron)
+ end
+
+ @doc """
+ Gets a single patron.
+
+ Raises `Ecto.NoResultsError` if the Patron does not exist.
+
+ ## Examples
+
+ iex> get_patron!(123)
+ %Patron{}
+
+ iex> get_patron!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_patron!(id), do: Repo.get!(Patron, id)
+
+ @doc """
+ Creates a patron.
+
+ ## Examples
+
+ iex> create_patron(%{field: value})
+ {:ok, %Patron{}}
+
+ iex> create_patron(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_patron(attrs \\ %{}) do
+ %Patron{}
+ |> Patron.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a patron.
+
+ ## Examples
+
+ iex> update_patron(patron, %{field: new_value})
+ {:ok, %Patron{}}
+
+ iex> update_patron(patron, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_patron(%Patron{} = patron, attrs) do
+ patron
+ |> Patron.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a patron.
+
+ ## Examples
+
+ iex> delete_patron(patron)
+ {:ok, %Patron{}}
+
+ iex> delete_patron(patron)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_patron(%Patron{} = patron) do
+ Repo.delete(patron)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking patron changes.
+
+ ## Examples
+
+ iex> change_patron(patron)
+ %Ecto.Changeset{data: %Patron{}}
+
+ """
+ def change_patron(%Patron{} = patron, attrs \\ %{}) do
+ Patron.changeset(patron, attrs)
+ end
+end
diff --git a/services/bright/lib/bright/patrons/patron.ex b/services/bright/lib/bright/patrons/patron.ex
new file mode 100644
index 0000000..98a714a
--- /dev/null
+++ b/services/bright/lib/bright/patrons/patron.ex
@@ -0,0 +1,19 @@
+defmodule Bright.Patrons.Patron do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "patrons" do
+ field :name, :string
+ field :public, :boolean, default: false
+ field :lifetime_support_cents, :integer
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(patron, attrs) do
+ patron
+ |> cast(attrs, [:name, :lifetime_support_cents, :public])
+ |> validate_required([:name, :lifetime_support_cents, :public])
+ end
+end
diff --git a/services/bright/lib/bright/platforms.ex b/services/bright/lib/bright/platforms.ex
new file mode 100644
index 0000000..0b99102
--- /dev/null
+++ b/services/bright/lib/bright/platforms.ex
@@ -0,0 +1,104 @@
+defmodule Bright.Platforms do
+ @moduledoc """
+ The Platforms context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Bright.Repo
+
+ alias Bright.Platforms.Platform
+
+ @doc """
+ Returns the list of platforms.
+
+ ## Examples
+
+ iex> list_platforms()
+ [%Platform{}, ...]
+
+ """
+ def list_platforms do
+ Repo.all(Platform)
+ end
+
+ @doc """
+ Gets a single platform.
+
+ Raises `Ecto.NoResultsError` if the Platform does not exist.
+
+ ## Examples
+
+ iex> get_platform!(123)
+ %Platform{}
+
+ iex> get_platform!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_platform!(id), do: Repo.get!(Platform, id)
+
+ @doc """
+ Creates a platform.
+
+ ## Examples
+
+ iex> create_platform(%{field: value})
+ {:ok, %Platform{}}
+
+ iex> create_platform(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_platform(attrs \\ %{}) do
+ %Platform{}
+ |> Platform.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a platform.
+
+ ## Examples
+
+ iex> update_platform(platform, %{field: new_value})
+ {:ok, %Platform{}}
+
+ iex> update_platform(platform, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_platform(%Platform{} = platform, attrs) do
+ platform
+ |> Platform.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a platform.
+
+ ## Examples
+
+ iex> delete_platform(platform)
+ {:ok, %Platform{}}
+
+ iex> delete_platform(platform)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_platform(%Platform{} = platform) do
+ Repo.delete(platform)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking platform changes.
+
+ ## Examples
+
+ iex> change_platform(platform)
+ %Ecto.Changeset{data: %Platform{}}
+
+ """
+ def change_platform(%Platform{} = platform, attrs \\ %{}) do
+ Platform.changeset(platform, attrs)
+ end
+end
diff --git a/services/bright/lib/bright/platforms/platform.ex b/services/bright/lib/bright/platforms/platform.ex
new file mode 100644
index 0000000..dcc915f
--- /dev/null
+++ b/services/bright/lib/bright/platforms/platform.ex
@@ -0,0 +1,30 @@
+defmodule Bright.Platforms.Platform do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "platforms" do
+ field :name, :string
+ field :url, :string
+ field :icon, :string
+
+ many_to_many :streams, Bright.Streams.Stream, join_through: "streams_platforms"
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(platform, attrs) do
+ platform
+ |> cast(attrs, [:name, :url, :icon])
+ |> validate_required([:name, :url, :icon])
+ end
+end
+
+
+
+defimpl Phoenix.HTML.Safe, for: Bright.Platforms.Platform do
+ def to_iodata(platform) do
+ # platform.icon
+ platform.name
+ end
+end
diff --git a/services/bright/lib/bright/release.ex b/services/bright/lib/bright/release.ex
new file mode 100644
index 0000000..f27fcaa
--- /dev/null
+++ b/services/bright/lib/bright/release.ex
@@ -0,0 +1,28 @@
+defmodule Bright.Release do
+ @moduledoc """
+ Used for executing DB release tasks when run in production without Mix
+ installed.
+ """
+ @app :bright
+
+ def migrate do
+ load_app()
+
+ for repo <- repos() do
+ {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
+ end
+ end
+
+ def rollback(repo, version) do
+ load_app()
+ {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
+ end
+
+ defp repos do
+ Application.fetch_env!(@app, :ecto_repos)
+ end
+
+ defp load_app do
+ Application.load(@app)
+ end
+end
diff --git a/services/bright/lib/bright/repo.ex b/services/bright/lib/bright/repo.ex
new file mode 100644
index 0000000..54b41a2
--- /dev/null
+++ b/services/bright/lib/bright/repo.ex
@@ -0,0 +1,5 @@
+defmodule Bright.Repo do
+ use Ecto.Repo,
+ otp_app: :bright,
+ adapter: Ecto.Adapters.Postgres
+end
diff --git a/services/bright/lib/bright/shopping_cart.ex b/services/bright/lib/bright/shopping_cart.ex
new file mode 100644
index 0000000..d2ecdb1
--- /dev/null
+++ b/services/bright/lib/bright/shopping_cart.ex
@@ -0,0 +1,272 @@
+defmodule Bright.ShoppingCart do
+ @moduledoc """
+ The ShoppingCart context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Bright.Repo
+ alias Bright.Catalog
+ alias Bright.ShoppingCart.{Cart, CartItem}
+
+ @doc """
+ Returns the list of carts.
+
+ ## Examples
+
+ iex> list_carts()
+ [%Cart{}, ...]
+
+ """
+ def list_carts do
+ Repo.all(Cart)
+ end
+
+ @doc """
+ Gets a single cart.
+
+ Raises `Ecto.NoResultsError` if the Cart does not exist.
+
+ ## Examples
+
+ iex> get_cart!(123)
+ %Cart{}
+
+ iex> get_cart!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_cart!(id), do: Repo.get!(Cart, id)
+
+ @doc """
+ Creates a cart.
+
+ ## Examples
+
+ iex> create_cart(%{field: value})
+ {:ok, %Cart{}}
+
+ iex> create_cart(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_cart(user_uuid) do
+ %Cart{user_uuid: user_uuid}
+ |> Cart.changeset(%{})
+ |> Repo.insert()
+ |> case do
+ {:ok, cart} -> {:ok, reload_cart(cart)}
+ {:error, changeset} -> {:error, changeset}
+ end
+ end
+
+ def reload_cart(%Cart{} = cart), do: get_cart_by_user_uuid(cart.user_uuid)
+
+ def add_item_to_cart(%Cart{} = cart, product_id) do
+ product = Catalog.get_product!(product_id)
+ %CartItem{quantity: 1, price_when_carted: product.price}
+ |> CartItem.changeset(%{})
+ |> Ecto.Changeset.put_assoc(:cart, cart)
+ |> Ecto.Changeset.put_assoc(:product, product)
+ |> Repo.insert(
+ on_conflict: [inc: [quantity: 1]],
+ conflict_target: [:cart_id, :product_id]
+ )
+ end
+
+ def remove_item_from_cart(%Cart{} = cart, product_id) do
+ {1, _} =
+ Repo.delete_all(
+ from(i in CartItem,
+ where: i.cart_id == ^cart.id,
+ where: i.product_id == ^product_id
+ )
+ )
+
+ {:ok, reload_cart(cart)}
+ end
+
+ @doc """
+ Updates a cart.
+
+ ## Examples
+
+ iex> update_cart(cart, %{field: new_value})
+ {:ok, %Cart{}}
+
+ iex> update_cart(cart, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_cart(%Cart{} = cart, attrs) do
+ changeset =
+ cart
+ |> Cart.changeset(attrs)
+ |> Ecto.Changeset.cast_assoc(:items, with: &CartItem.changeset/2)
+
+ Ecto.Multi.new()
+ |> Ecto.Multi.update(:cart, changeset)
+ |> Ecto.Multi.delete_all(:discarded_items, fn %{cart: cart} ->
+ from(i in CartItem, where: i.cart_id == ^cart.id and i.quantity == 0)
+ end)
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{cart: cart}} -> {:ok, cart}
+ {:error, :cart, changeset, _changes_so_far} -> {:error, changeset}
+ end
+ end
+
+ @doc """
+ Deletes a cart.
+
+ ## Examples
+
+ iex> delete_cart(cart)
+ {:ok, %Cart{}}
+
+ iex> delete_cart(cart)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_cart(%Cart{} = cart) do
+ Repo.delete(cart)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking cart changes.
+
+ ## Examples
+
+ iex> change_cart(cart)
+ %Ecto.Changeset{data: %Cart{}}
+
+ """
+ def change_cart(%Cart{} = cart, attrs \\ %{}) do
+ Cart.changeset(cart, attrs)
+ end
+
+ alias Bright.ShoppingCart.CartItem
+
+ @doc """
+ Returns the list of cart_items.
+
+ ## Examples
+
+ iex> list_cart_items()
+ [%CartItem{}, ...]
+
+ """
+ def list_cart_items do
+ Repo.all(CartItem)
+ end
+
+ @doc """
+ Gets a single cart_item.
+
+ Raises `Ecto.NoResultsError` if the Cart item does not exist.
+
+ ## Examples
+
+ iex> get_cart_item!(123)
+ %CartItem{}
+
+ iex> get_cart_item!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_cart_item!(id), do: Repo.get!(CartItem, id)
+
+ @doc """
+ Creates a cart_item.
+
+ ## Examples
+
+ iex> create_cart_item(%{field: value})
+ {:ok, %CartItem{}}
+
+ iex> create_cart_item(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_cart_item(attrs \\ %{}) do
+ %CartItem{}
+ |> CartItem.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a cart_item.
+
+ ## Examples
+
+ iex> update_cart_item(cart_item, %{field: new_value})
+ {:ok, %CartItem{}}
+
+ iex> update_cart_item(cart_item, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_cart_item(%CartItem{} = cart_item, attrs) do
+ cart_item
+ |> CartItem.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a cart_item.
+
+ ## Examples
+
+ iex> delete_cart_item(cart_item)
+ {:ok, %CartItem{}}
+
+ iex> delete_cart_item(cart_item)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_cart_item(%CartItem{} = cart_item) do
+ Repo.delete(cart_item)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking cart_item changes.
+
+ ## Examples
+
+ iex> change_cart_item(cart_item)
+ %Ecto.Changeset{data: %CartItem{}}
+
+ """
+ def change_cart_item(%CartItem{} = cart_item, attrs \\ %{}) do
+ CartItem.changeset(cart_item, attrs)
+ end
+
+ def get_cart_by_user_uuid(user_uuid) do
+ Repo.one(
+ from(c in Cart,
+ where: c.user_uuid == ^user_uuid,
+ left_join: i in assoc(c, :items),
+ left_join: p in assoc(i, :product),
+ order_by: [asc: i.inserted_at],
+ preload: [items: {i, product: p}]
+ )
+ )
+ end
+
+ def total_item_price(%CartItem{} = item) do
+ Decimal.mult(item.product.price, item.quantity)
+ end
+
+ def total_cart_price(%Cart{} = cart) do
+ Enum.reduce(cart.items, 0, fn item, acc ->
+ item
+ |> total_item_price()
+ |> Decimal.add(acc)
+ end)
+ end
+
+ def prune_cart_items(%Cart{} = cart) do
+ {_, _} = Repo.delete_all(from(i in CartItem, where: i.cart_id == ^cart.id))
+ {:ok, reload_cart(cart)}
+ end
+
+end
diff --git a/services/bright/lib/bright/shopping_cart/cart.ex b/services/bright/lib/bright/shopping_cart/cart.ex
new file mode 100644
index 0000000..61bb811
--- /dev/null
+++ b/services/bright/lib/bright/shopping_cart/cart.ex
@@ -0,0 +1,19 @@
+defmodule Bright.ShoppingCart.Cart do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "carts" do
+ field :user_uuid, Ecto.UUID
+ has_many :items, Bright.ShoppingCart.CartItem
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(cart, attrs) do
+ cart
+ |> cast(attrs, [:user_uuid])
+ |> validate_required([:user_uuid])
+ |> unique_constraint(:user_uuid)
+ end
+end
diff --git a/services/bright/lib/bright/shopping_cart/cart_item.ex b/services/bright/lib/bright/shopping_cart/cart_item.ex
new file mode 100644
index 0000000..2e49d54
--- /dev/null
+++ b/services/bright/lib/bright/shopping_cart/cart_item.ex
@@ -0,0 +1,21 @@
+defmodule Bright.ShoppingCart.CartItem do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "cart_items" do
+ field :price_when_carted, :decimal
+ field :quantity, :integer
+ belongs_to :cart, Bright.ShoppingCart.Cart
+ belongs_to :product, Bright.Catalog.Product
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(cart_item, attrs) do
+ cart_item
+ |> cast(attrs, [:price_when_carted, :quantity])
+ |> validate_required([:price_when_carted, :quantity])
+ |> validate_number(:quantity, greater_than_or_equal_to: 0, less_than: 100)
+ end
+end
diff --git a/services/bright/lib/bright/streams.ex b/services/bright/lib/bright/streams.ex
new file mode 100644
index 0000000..b38503b
--- /dev/null
+++ b/services/bright/lib/bright/streams.ex
@@ -0,0 +1,247 @@
+defmodule Bright.Streams do
+ @moduledoc """
+ The Streams context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Bright.Repo
+
+ alias Bright.Streams.{Stream,Vod}
+ alias Bright.Vtubers.Vtuber
+ alias Bright.Tags.Tag
+ alias Bright.Platforms.Platform
+
+ @doc """
+ Returns the list of streams.
+
+ ## Examples
+
+ iex> list_streams()
+ [%Stream{}, ...]
+
+ """
+ def list_streams do
+ Stream
+ |> Repo.all()
+ |> Repo.preload([:tags, :vods, :vtubers, :platforms])
+ end
+
+ @doc """
+ Gets a single stream.
+
+ Raises `Ecto.NoResultsError` if the Stream does not exist.
+
+ ## Examples
+
+ iex> get_stream!(123)
+ %Stream{}
+
+ iex> get_stream!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_stream!(id) do
+ Stream
+ |> Repo.get!(id)
+ |> Repo.preload([:tags, :vods, :vtubers, :platforms])
+ end
+
+ @doc """
+ Creates a stream.
+
+ ## Examples
+
+ iex> create_stream(%{field: value})
+ {:ok, %Stream{}}
+
+ iex> create_stream(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_stream(attrs \\ %{}) do
+ %Stream{}
+ # |> Stream.changeset(attrs)
+ |> change_stream(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a stream.
+
+ ## Examples
+
+ iex> update_stream(stream, %{field: new_value})
+ {:ok, %Stream{}}
+
+ iex> update_stream(stream, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_stream(%Stream{} = stream, attrs) do
+ stream
+ |> change_stream(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a stream.
+
+ ## Examples
+
+ iex> delete_stream(stream)
+ {:ok, %Stream{}}
+
+ iex> delete_stream(stream)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_stream(%Stream{} = stream) do
+ Repo.delete(stream)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking stream changes.
+
+ ## Examples
+
+ iex> change_stream(stream)
+ %Ecto.Changeset{data: %Stream{}}
+
+ """
+ def change_stream(%Stream{} = stream, attrs \\ %{}) do
+ tags = list_tags_by_id(attrs["tag_ids"])
+ vods = list_vods_by_id(attrs["vod_ids"])
+ vtubers = list_vtubers_by_id(attrs["vtuber_ids"])
+ platforms = list_platforms_by_id(attrs["platform_ids"])
+ stream
+ |> Repo.preload([:tags, :vods, :vtubers])
+ |> Stream.changeset(attrs)
+ |> Ecto.Changeset.put_assoc(:tags, tags)
+ |> Ecto.Changeset.put_assoc(:vods, vods)
+ |> Ecto.Changeset.put_assoc(:vtubers, vtubers)
+ |> Ecto.Changeset.put_assoc(:platforms, platforms)
+ end
+
+ def inc_page_views(%Stream{} = stream) do
+ {1, [%Stream{views: views}]} =
+ from(s in Stream, where: s.id == ^stream.id, select: [:views])
+ |> Repo.update_all(inc: [views: 1])
+ put_in(stream.views, views)
+ end
+
+ def list_tags_by_id(nil), do: []
+ def list_tags_by_id(tag_ids) do
+ Repo.all(from t in Tag, where: t.id in ^tag_ids)
+ end
+
+ def list_vods_by_id(nil), do: []
+ def list_vods_by_id(vod_ids) do
+ Repo.all(from v in Vod, where: v.id in ^vod_ids)
+ end
+
+ def list_vtubers_by_id(nil), do: []
+ def list_vtubers_by_id(vtuber_ids) do
+ Repo.all(from v in Vtuber, where: v.id in ^vtuber_ids)
+ end
+
+ def list_platforms_by_id(nil), do: []
+ def list_platforms_by_id(platform_ids) do
+ Repo.all(from p in Platform, where: p.id in ^platform_ids)
+ end
+
+ alias Bright.Streams.Vod
+
+ @doc """
+ Returns the list of vods.
+
+ ## Examples
+
+ iex> list_vods()
+ [%Vod{}, ...]
+
+ """
+ def list_vods do
+ Repo.all(Vod)
+ end
+
+ @doc """
+ Gets a single vod.
+
+ Raises `Ecto.NoResultsError` if the Vod does not exist.
+
+ ## Examples
+
+ iex> get_vod!(123)
+ %Vod{}
+
+ iex> get_vod!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_vod!(id), do: Repo.get!(Vod, id)
+
+ @doc """
+ Creates a vod.
+
+ ## Examples
+
+ iex> create_vod(%{field: value})
+ {:ok, %Vod{}}
+
+ iex> create_vod(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_vod(attrs \\ %{}) do
+ %Vod{}
+ |> Vod.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a vod.
+
+ ## Examples
+
+ iex> update_vod(vod, %{field: new_value})
+ {:ok, %Vod{}}
+
+ iex> update_vod(vod, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_vod(%Vod{} = vod, attrs) do
+ vod
+ |> Vod.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a vod.
+
+ ## Examples
+
+ iex> delete_vod(vod)
+ {:ok, %Vod{}}
+
+ iex> delete_vod(vod)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_vod(%Vod{} = vod) do
+ Repo.delete(vod)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking vod changes.
+
+ ## Examples
+
+ iex> change_vod(vod)
+ %Ecto.Changeset{data: %Vod{}}
+
+ """
+ def change_vod(%Vod{} = vod, attrs \\ %{}) do
+ Vod.changeset(vod, attrs)
+ end
+end
diff --git a/services/bright/lib/bright/streams/stream.ex b/services/bright/lib/bright/streams/stream.ex
new file mode 100644
index 0000000..5395456
--- /dev/null
+++ b/services/bright/lib/bright/streams/stream.ex
@@ -0,0 +1,30 @@
+defmodule Bright.Streams.Stream do
+ use Ecto.Schema
+ import Ecto.Changeset
+ alias Bright.Tags.Tag
+ alias Bright.Vtubers.Vtuber
+ alias Bright.Platforms.Platform
+
+ schema "streams" do
+ field :date, :utc_datetime
+ field :title, :string
+ field :notes, :string
+ field :views, :integer
+
+
+ many_to_many :tags, Tag, join_through: "streams_tags", on_replace: :delete
+ many_to_many :vtubers, Vtuber, join_through: "streams_vtubers", on_replace: :delete
+ many_to_many :platforms, Platform, join_through: "streams_platforms", on_replace: :delete
+
+ has_many :vods, Bright.Streams.Vod, on_replace: :delete
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(stream, attrs) do
+ stream
+ |> cast(attrs, [:title, :notes, :date])
+ |> validate_required([:title, :date])
+ end
+end
diff --git a/services/bright/lib/bright/streams/vod.ex b/services/bright/lib/bright/streams/vod.ex
new file mode 100644
index 0000000..9222f66
--- /dev/null
+++ b/services/bright/lib/bright/streams/vod.ex
@@ -0,0 +1,29 @@
+defmodule Bright.Streams.Vod do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "vods" do
+ field :s3_cdn_url, :string
+ field :s3_upload_id, :string
+ field :s3_key, :string
+ field :s3_bucket, :string
+ field :mux_asset_id, :string
+ field :mux_playback_id, :string
+ field :ipfs_cid, :string
+ field :torrent, :string
+ field :notes, :string
+
+ belongs_to :stream, Bright.Streams.Stream
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(vod, attrs) do
+ vod
+ |> cast(attrs, [:s3_cdn_url, :s3_upload_id, :s3_key, :s3_bucket, :mux_asset_id, :mux_playback_id, :ipfs_cid, :torrent, :stream_id])
+ |> validate_required([:stream_id])
+ end
+
+
+end
diff --git a/services/bright/lib/bright/tags.ex b/services/bright/lib/bright/tags.ex
new file mode 100644
index 0000000..de364a4
--- /dev/null
+++ b/services/bright/lib/bright/tags.ex
@@ -0,0 +1,104 @@
+defmodule Bright.Tags do
+ @moduledoc """
+ The Tags context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Bright.Repo
+
+ alias Bright.Tags.Tag
+
+ @doc """
+ Returns the list of tags.
+
+ ## Examples
+
+ iex> list_tags()
+ [%Tag{}, ...]
+
+ """
+ def list_tags do
+ Repo.all(Tag)
+ end
+
+ @doc """
+ Gets a single tag.
+
+ Raises `Ecto.NoResultsError` if the Tag does not exist.
+
+ ## Examples
+
+ iex> get_tag!(123)
+ %Tag{}
+
+ iex> get_tag!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_tag!(id), do: Repo.get!(Tag, id)
+
+ @doc """
+ Creates a tag.
+
+ ## Examples
+
+ iex> create_tag(%{field: value})
+ {:ok, %Tag{}}
+
+ iex> create_tag(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_tag(attrs \\ %{}) do
+ %Tag{}
+ |> Tag.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a tag.
+
+ ## Examples
+
+ iex> update_tag(tag, %{field: new_value})
+ {:ok, %Tag{}}
+
+ iex> update_tag(tag, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_tag(%Tag{} = tag, attrs) do
+ tag
+ |> Tag.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a tag.
+
+ ## Examples
+
+ iex> delete_tag(tag)
+ {:ok, %Tag{}}
+
+ iex> delete_tag(tag)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_tag(%Tag{} = tag) do
+ Repo.delete(tag)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking tag changes.
+
+ ## Examples
+
+ iex> change_tag(tag)
+ %Ecto.Changeset{data: %Tag{}}
+
+ """
+ def change_tag(%Tag{} = tag, attrs \\ %{}) do
+ Tag.changeset(tag, attrs)
+ end
+end
diff --git a/services/bright/lib/bright/tags/stream_tag.ex b/services/bright/lib/bright/tags/stream_tag.ex
new file mode 100644
index 0000000..a047f34
--- /dev/null
+++ b/services/bright/lib/bright/tags/stream_tag.ex
@@ -0,0 +1,19 @@
+defmodule Bright.Tags.StreamTag do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "streams_tags" do
+
+ field :stream_id, :id
+ field :tag_id, :id
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(stream_tag, attrs) do
+ stream_tag
+ |> cast(attrs, [])
+ |> validate_required([])
+ end
+end
diff --git a/services/bright/lib/bright/tags/tag.ex b/services/bright/lib/bright/tags/tag.ex
new file mode 100644
index 0000000..a06429d
--- /dev/null
+++ b/services/bright/lib/bright/tags/tag.ex
@@ -0,0 +1,18 @@
+defmodule Bright.Tags.Tag do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "tags" do
+ field :name, :string
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(tag, attrs) do
+ tag
+ |> cast(attrs, [:name])
+ |> validate_required([:name])
+ |> unique_constraint(:name)
+ end
+end
diff --git a/services/bright/lib/bright/urls.ex b/services/bright/lib/bright/urls.ex
new file mode 100644
index 0000000..eb68b53
--- /dev/null
+++ b/services/bright/lib/bright/urls.ex
@@ -0,0 +1,104 @@
+defmodule Bright.Urls do
+ @moduledoc """
+ The Urls context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Bright.Repo
+
+ alias Bright.Urls.Url
+
+ @doc """
+ Returns the list of urls.
+
+ ## Examples
+
+ iex> list_urls()
+ [%Url{}, ...]
+
+ """
+ def list_urls do
+ Repo.all(Url)
+ end
+
+ @doc """
+ Gets a single url.
+
+ Raises `Ecto.NoResultsError` if the Url does not exist.
+
+ ## Examples
+
+ iex> get_url!(123)
+ %Url{}
+
+ iex> get_url!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_url!(id), do: Repo.get!(Url, id)
+
+ @doc """
+ Creates a url.
+
+ ## Examples
+
+ iex> create_url(%{field: value})
+ {:ok, %Url{}}
+
+ iex> create_url(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_url(attrs \\ %{}) do
+ %Url{}
+ |> Url.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a url.
+
+ ## Examples
+
+ iex> update_url(url, %{field: new_value})
+ {:ok, %Url{}}
+
+ iex> update_url(url, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_url(%Url{} = url, attrs) do
+ url
+ |> Url.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a url.
+
+ ## Examples
+
+ iex> delete_url(url)
+ {:ok, %Url{}}
+
+ iex> delete_url(url)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_url(%Url{} = url) do
+ Repo.delete(url)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking url changes.
+
+ ## Examples
+
+ iex> change_url(url)
+ %Ecto.Changeset{data: %Url{}}
+
+ """
+ def change_url(%Url{} = url, attrs \\ %{}) do
+ Url.changeset(url, attrs)
+ end
+end
diff --git a/services/bright/lib/bright/urls/url.ex b/services/bright/lib/bright/urls/url.ex
new file mode 100644
index 0000000..22b3e14
--- /dev/null
+++ b/services/bright/lib/bright/urls/url.ex
@@ -0,0 +1,18 @@
+defmodule Bright.Urls.Url do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "urls" do
+ field :link, :string
+ field :title, :string
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(url, attrs) do
+ url
+ |> cast(attrs, [:link, :title])
+ |> validate_required([:link, :title])
+ end
+end
diff --git a/services/bright/lib/bright/user.ex b/services/bright/lib/bright/user.ex
new file mode 100644
index 0000000..15d1e9e
--- /dev/null
+++ b/services/bright/lib/bright/user.ex
@@ -0,0 +1,20 @@
+defmodule Bright.User do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "users" do
+ field :name, :string
+ field :email, :string
+ field :bio, :string
+ field :number_of_pets, :integer
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(user, attrs) do
+ user
+ |> cast(attrs, [:name, :email, :bio, :number_of_pets])
+ |> validate_required([:name, :email, :bio, :number_of_pets])
+ end
+end
diff --git a/services/bright/lib/bright/vtubers.ex b/services/bright/lib/bright/vtubers.ex
new file mode 100644
index 0000000..2202b8f
--- /dev/null
+++ b/services/bright/lib/bright/vtubers.ex
@@ -0,0 +1,105 @@
+defmodule Bright.Vtubers do
+ @moduledoc """
+ The Vtubers context.
+ """
+
+ import Ecto.Query, warn: false
+ alias Bright.Repo
+
+ alias Bright.Vtubers.Vtuber
+
+ @doc """
+ Returns the list of vtubers.
+
+ ## Examples
+
+ iex> list_vtubers()
+ [%Vtuber{}, ...]
+
+ """
+ def list_vtubers do
+ Repo.all(Vtuber)
+ end
+
+ @doc """
+ Gets a single vtuber.
+
+ Raises `Ecto.NoResultsError` if the Vtuber does not exist.
+
+ ## Examples
+
+ iex> get_vtuber!(123)
+ %Vtuber{}
+
+ iex> get_vtuber!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_vtuber!(id), do: Repo.get!(Vtuber, id)
+
+ @doc """
+ Creates a vtuber.
+
+ ## Examples
+
+ iex> create_vtuber(%{field: value})
+ {:ok, %Vtuber{}}
+
+ iex> create_vtuber(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_vtuber(attrs \\ %{}) do
+ %Vtuber{}
+ |> Vtuber.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a vtuber.
+
+ ## Examples
+
+ iex> update_vtuber(vtuber, %{field: new_value})
+ {:ok, %Vtuber{}}
+
+ iex> update_vtuber(vtuber, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_vtuber(%Vtuber{} = vtuber, attrs) do
+ vtuber
+ |> Vtuber.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a vtuber.
+
+ ## Examples
+
+ iex> delete_vtuber(vtuber)
+ {:ok, %Vtuber{}}
+
+ iex> delete_vtuber(vtuber)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_vtuber(%Vtuber{} = vtuber) do
+ Repo.delete(vtuber)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking vtuber changes.
+
+ ## Examples
+
+ iex> change_vtuber(vtuber)
+ %Ecto.Changeset{data: %Vtuber{}}
+
+ """
+ def change_vtuber(%Vtuber{} = vtuber, attrs \\ %{}) do
+ Vtuber.changeset(vtuber, attrs)
+ end
+
+end
diff --git a/services/bright/lib/bright/vtubers/vtuber.ex b/services/bright/lib/bright/vtubers/vtuber.ex
new file mode 100644
index 0000000..644b853
--- /dev/null
+++ b/services/bright/lib/bright/vtubers/vtuber.ex
@@ -0,0 +1,42 @@
+defmodule Bright.Vtubers.Vtuber do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "vtubers" do
+ field :image, :string
+ field :slug, :string
+ field :display_name, :string
+ field :chaturbate, :string
+ field :twitter, :string
+ field :patreon, :string
+ field :twitch, :string
+ field :tiktok, :string
+ field :onlyfans, :string
+ field :youtube, :string
+ field :linktree, :string
+ field :carrd, :string
+ field :fansly, :string
+ field :pornhub, :string
+ field :discord, :string
+ field :reddit, :string
+ field :throne, :string
+ field :instagram, :string
+ field :facebook, :string
+ field :merch, :string
+ field :description_1, :string
+ field :description_2, :string
+ field :theme_color, :string
+ field :fansly_id, :string
+ field :chaturbate_id, :string
+ field :twitter_id, :string
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(vtuber, attrs) do
+ vtuber
+ |> cast(attrs, [:slug, :display_name, :chaturbate, :twitter, :patreon, :twitch, :tiktok, :onlyfans, :youtube, :linktree, :carrd, :fansly, :pornhub, :discord, :reddit, :throne, :instagram, :facebook, :merch, :description_1, :description_2, :image, :theme_color, :fansly_id, :chaturbate_id, :twitter_id])
+ |> validate_required([:slug, :display_name, :image, :theme_color])
+ end
+end
diff --git a/services/bright/lib/bright_web.ex b/services/bright/lib/bright_web.ex
new file mode 100644
index 0000000..f3e83d4
--- /dev/null
+++ b/services/bright/lib/bright_web.ex
@@ -0,0 +1,114 @@
+defmodule BrightWeb do
+ @moduledoc """
+ The entrypoint for defining your web interface, such
+ as controllers, components, channels, and so on.
+
+ This can be used in your application as:
+
+ use BrightWeb, :controller
+ use BrightWeb, :html
+
+ The definitions below will be executed for every controller,
+ component, etc, so keep them short and clean, focused
+ on imports, uses and aliases.
+
+ Do NOT define functions inside the quoted expressions
+ below. Instead, define additional modules and import
+ those modules here.
+ """
+
+ def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
+
+ def router do
+ quote do
+ use Phoenix.Router, helpers: false
+
+ # Import common connection and controller functions to use in pipelines
+ import Plug.Conn
+ import Phoenix.Controller
+ import Phoenix.LiveView.Router
+ end
+ end
+
+ def channel do
+ quote do
+ use Phoenix.Channel
+ end
+ end
+
+ def controller do
+ quote do
+ use Phoenix.Controller,
+ formats: [:html, :json],
+ layouts: [html: BrightWeb.Layouts]
+
+ import Plug.Conn
+ import BrightWeb.Gettext
+
+ unquote(verified_routes())
+ end
+ end
+
+ def live_view do
+ quote do
+ use Phoenix.LiveView,
+ layout: {BrightWeb.Layouts, :app}
+
+ unquote(html_helpers())
+ end
+ end
+
+ def live_component do
+ quote do
+ use Phoenix.LiveComponent
+
+ unquote(html_helpers())
+ end
+ end
+
+ def html do
+ quote do
+ use Phoenix.Component
+
+ # Import convenience functions from controllers
+ import Phoenix.Controller,
+ only: [get_csrf_token: 0, view_module: 1, view_template: 1]
+
+ # Include general helpers for rendering HTML
+ unquote(html_helpers())
+ end
+ end
+
+ defp html_helpers do
+ quote do
+ # HTML escaping functionality
+ import Phoenix.HTML
+ # Core UI components and translation
+ import BrightWeb.CoreComponents
+ import BrightWeb.Gettext
+ import BrightWeb.NavigationComponents
+
+ # Shortcut for generating JS commands
+ alias Phoenix.LiveView.JS
+
+ # Routes generation with the ~p sigil
+ unquote(verified_routes())
+ end
+ end
+
+ def verified_routes do
+ quote do
+ use Phoenix.VerifiedRoutes,
+ endpoint: BrightWeb.Endpoint,
+ router: BrightWeb.Router,
+ statics: BrightWeb.static_paths()
+ end
+ end
+
+ @doc """
+ When used, dispatch to the appropriate controller/live_view/etc.
+ """
+ defmacro __using__(which) when is_atom(which) do
+ apply(__MODULE__, which, [])
+ end
+end
diff --git a/services/bright/lib/bright_web/components/core_components.ex b/services/bright/lib/bright_web/components/core_components.ex
new file mode 100644
index 0000000..b047d8a
--- /dev/null
+++ b/services/bright/lib/bright_web/components/core_components.ex
@@ -0,0 +1,686 @@
+defmodule BrightWeb.CoreComponents do
+ @moduledoc """
+ Provides core UI components.
+
+ At first glance, this module may seem daunting, but its goal is to provide
+ core building blocks for your application, such as modals, tables, and
+ forms. The components consist mostly of markup and are well-documented
+ with doc strings and declarative assigns. You may customize and style
+ them in any way you want, based on your application growth and needs.
+
+ The default components use Tailwind CSS, a utility-first CSS framework.
+ See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
+ how to customize them or feel free to swap in another framework altogether.
+
+ Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
+ """
+ use Phoenix.Component
+
+ alias Phoenix.LiveView.JS
+ import BrightWeb.Gettext
+
+ @doc """
+ Renders a modal.
+
+ ## Examples
+
+ <.modal id="confirm-modal">
+ This is a modal.
+
+
+ JS commands may be passed to the `:on_cancel` to configure
+ the closing/cancel event, for example:
+
+ <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
+ This is another modal.
+
+
+ """
+ attr :id, :string, required: true
+ attr :show, :boolean, default: false
+ attr :on_cancel, JS, default: %JS{}
+ slot :inner_block, required: true
+
+ def modal(assigns) do
+ ~H"""
+
+ """
+ end
+
+
+ @doc """
+ Shows the flash group with standard titles and content.
+
+ ## Examples
+
+ <.flash_group flash={@flash} />
+ """
+ attr :flash, :map, required: true, doc: "the map of flash messages"
+ attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
+
+ def flash_group(assigns) do
+ ~H"""
+
+ <.flash kind={:info} title={gettext("Success!")} flash={@flash} />
+ <.flash kind={:error} title={gettext("Error!")} flash={@flash} />
+ <.flash
+ id="client-error"
+ kind={:error}
+ title={gettext("We can't find the internet")}
+ phx-disconnected={show(".phx-client-error #client-error")}
+ phx-connected={hide("#client-error")}
+ hidden
+ >
+ <%= gettext("Attempting to reconnect") %>
+ <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
+
+
+ <.flash
+ id="server-error"
+ kind={:error}
+ title={gettext("Something went wrong!")}
+ phx-disconnected={show(".phx-server-error #server-error")}
+ phx-connected={hide("#server-error")}
+ hidden
+ >
+ <%= gettext("Hang in there while we get back on track") %>
+ <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
+
+
+ """
+ end
+
+ @doc """
+ Renders a simple form.
+
+ ## Examples
+
+ <.simple_form for={@form} phx-change="validate" phx-submit="save">
+ <.input field={@form[:email]} label="Email"/>
+ <.input field={@form[:username]} label="Username" />
+ <:actions>
+ <.button>Save
+
+
+ """
+ attr :for, :any, required: true, doc: "the data structure for the form"
+ attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
+
+ attr :rest, :global,
+ include: ~w(autocomplete name rel action enctype method novalidate target multipart),
+ doc: "the arbitrary HTML attributes to apply to the form tag"
+
+ slot :inner_block, required: true
+ slot :actions, doc: "the slot for form actions, such as a submit button"
+
+ def simple_form(assigns) do
+ ~H"""
+ <.form :let={f} for={@for} as={@as} {@rest}>
+
+ <%= render_slot(@inner_block, f) %>
+
+ <%= render_slot(action, f) %>
+
+
+
+ """
+ end
+
+@doc """
+Renders a button.
+
+## Examples
+
+ <.button>Send!
+ <.button phx-click="go" class="ml-2">Send!
+"""
+attr :type, :string, default: nil
+attr :class, :string, default: nil
+attr :rest, :global, include: ~w(disabled form name value)
+
+slot :inner_block, required: true
+
+def button(assigns) do
+ ~H"""
+
+ """
+end
+
+
+ @doc """
+ Renders an input with label and error messages.
+
+ A `Phoenix.HTML.FormField` may be passed as argument,
+ which is used to retrieve the input name, id, and values.
+ Otherwise all attributes may be passed explicitly.
+
+ ## Types
+
+ This function accepts all HTML input types, considering that:
+
+ * You may also set `type="select"` to render a `