phoenix integration begin
ci / build (push) Has started running Details

This commit is contained in:
CJ_Clippy 2025-01-03 06:45:35 -08:00
parent 5e83742341
commit 8b8de3b072
205 changed files with 8818 additions and 0 deletions

View File

@ -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

View File

@ -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"]
]

37
services/bright/.gitignore vendored Normal file
View File

@ -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/

18
services/bright/README.md Normal file
View File

@ -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

View File

@ -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 */

View File

@ -0,0 +1,4 @@
/* This file is for your main application CSS */
// @import "./phoenix.css";
@import "bulma";

View File

@ -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

View File

@ -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:
//
// <div class="phx-click-loading:animate-ping">
//
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})
})
]
}

165
services/bright/assets/vendor/topbar.js vendored Normal file
View File

@ -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));

View File

@ -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"

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
defmodule Bright.Mailer do
use Swoosh.Mailer, otp_app: :bright
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
defmodule Bright.Repo do
use Ecto.Repo,
otp_app: :bright,
adapter: Ecto.Adapters.Postgres
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.
</.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.
</.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"""
<div
id={@id}
phx-mounted={@show && show_modal(@id)}
phx-remove={hide_modal(@id)}
data-cancel={JS.exec(@on_cancel, "phx-remove")}
class="relative z-50 hidden"
>
<div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" />
<div
class="fixed inset-0 overflow-y-auto"
aria-labelledby={"#{@id}-title"}
aria-describedby={"#{@id}-description"}
role="dialog"
aria-modal="true"
tabindex="0"
>
<div class="flex min-h-full items-center justify-center">
<div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
<.focus_wrap
id={"#{@id}-container"}
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
phx-key="escape"
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
>
<div class="absolute top-6 right-5">
<button
phx-click={JS.exec("data-cancel", to: "##{@id}")}
type="button"
class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
aria-label={gettext("close")}
>
<.icon name="hero-x-mark-solid" class="h-5 w-5" />
</button>
</div>
<div id={"#{@id}-content"}>
<%= render_slot(@inner_block) %>
</div>
</.focus_wrap>
</div>
</div>
</div>
</div>
"""
end
@doc """
Renders flash notices using Bulma.
## Examples
<.flash kind={:info} flash={@flash} />
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
"""
attr :id, :string, doc: "the optional id of flash container"
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
attr :title, :string, default: nil
attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
slot :inner_block, doc: "the optional inner block that renders the flash message"
def flash(assigns) do
assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
~H"""
<div
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
id={@id}
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
role="alert"
class={[
"notification",
@kind == :info && "is-success",
@kind == :error && "is-danger"
]}
{@rest}
>
<button type="button" class="delete" aria-label={gettext("close")}></button>
<p :if={@title} class="has-text-weight-bold">
<%= @title %>
</p>
<p><%= msg %></p>
</div>
"""
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"""
<div id={@id}>
<.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>
<.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" />
</.flash>
</div>
"""
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</.button>
</:actions>
</.simple_form>
"""
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}>
<div>
<%= render_slot(@inner_block, f) %>
<div :for={action <- @actions} class="mt-2">
<%= render_slot(action, f) %>
</div>
</div>
</.form>
"""
end
@doc """
Renders a button.
## Examples
<.button>Send!</.button>
<.button phx-click="go" class="ml-2">Send!</.button>
"""
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"""
<button
type={@type}
class={[
"button",
"is-primary",
"is-rounded",
"py-2", "px-3",
@class
]}
{@rest}
>
<%= render_slot(@inner_block) %>
</button>
"""
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 `<select>` tag
* `type="checkbox"` is used exclusively to render boolean values
* For live file uploads, see `Phoenix.Component.live_file_input/1`
See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
for more information. Unsupported types, such as hidden and radio,
are best written directly in your templates.
## Examples
<.input field={@form[:email]} type="email" />
<.input name="my-input" errors={["oh no!"]} />
"""
attr :id, :any, default: nil
attr :name, :any
attr :label, :string, default: nil
attr :value, :any
attr :type, :string,
default: "text",
values: ~w(checkbox color date datetime-local email file month number password
range search select tel text textarea time url week)
attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
attr :errors, :list, default: []
attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
attr :rest, :global,
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
multiple pattern placeholder readonly required rows size step)
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
assigns
|> assign(field: nil, id: assigns.id || field.id)
|> assign(:errors, Enum.map(errors, &translate_error(&1)))
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|> assign_new(:value, fn -> field.value end)
|> input()
end
def input(%{type: "checkbox"} = assigns) do
assigns =
assign_new(assigns, :checked, fn ->
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
end)
~H"""
<div class="field-label is-normal">
<label class="label">
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
<input
type="checkbox"
id={@id}
name={@name}
value="true"
checked={@checked}
class="input"
{@rest}
/>
<%= @label %>
</label>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
def input(%{type: "select"} = assigns) do
~H"""
<div class="field mb-5">
<.label for={@id}>{@label}</.label>
<div class="control">
<div class={[
"select",
@multiple && "is-multiple"
]}>
<select
id={@id}
name={@name}
multiple={@multiple}
{@rest}
>
<option :if={@prompt} value="">{@prompt}</option>
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
</select>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
</div>
</div>
"""
end
def input(%{type: "textarea"} = assigns) do
~H"""
<div>
<.label for={@id}><%= @label %></.label>
<textarea
id={@id}
name={@name}
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 min-h-[6rem]",
@errors == [] && "",
@errors != [] && "is-danger"
]}
{@rest}
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
# All other inputs text, datetime-local, url, password, etc. are handled here...
def input(assigns) do
~H"""
<div class="control mb-5">
<.label for={@id}><%= @label %></.label>
<input
type={@type}
name={@name}
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"input",
@errors == [] && "",
@errors != [] && "is-danger"
]}
{@rest}
/>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
@doc """
Renders a label.
"""
attr :for, :string, default: nil
slot :inner_block, required: true
def label(assigns) do
~H"""
<label for={@for} class="label">
<%= render_slot(@inner_block) %>
</label>
"""
end
@doc """
Generates a generic error message.
"""
slot :inner_block, required: true
def error(assigns) do
~H"""
<p class="help is-danger">
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
<%= render_slot(@inner_block) %>
</p>
"""
end
@doc """
Renders a header with title.
"""
attr :class, :string, default: nil
slot :inner_block, required: true
slot :subtitle
slot :actions
def header(assigns) do
~H"""
<header class={[@actions != [] && "columns is-vcentered", @class]}>
<div class="column is-narrow">
<h2 class="title is-2">
<%= render_slot(@inner_block) %>
</h2>
<p :if={@subtitle != []} class="subtitle is-5">
<%= render_slot(@subtitle) %>
</p>
</div>
<div class="column is-narrow">
<%= render_slot(@actions) %>
</div>
</header>
"""
end
@doc ~S"""
Renders a table with generic styling.
## Examples
<.table id="users" rows={@users}>
<:col :let={user} label="id"><%= user.id %></:col>
<:col :let={user} label="username"><%= user.username %></:col>
</.table>
"""
attr :id, :string, required: true
attr :rows, :list, required: true
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
attr :row_item, :any,
default: &Function.identity/1,
doc: "the function for mapping each row before calling the :col and :action slots"
slot :col, required: true do
attr :label, :string
end
slot :action, doc: "the slot for showing user actions in the last table column"
def table(assigns) do
assigns =
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
end
~H"""
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
<table class="table is-striped is-hoverable is-fullwidth">
<thead class="text-sm text-left leading-6 text-zinc-500">
<tr>
<th :for={col <- @col} class="font-normal"><%= col[:label] %></th>
<th :if={@action != []} class="">
<span class="sr-only"><%= gettext("Actions") %></span>
</th>
</tr>
</thead>
<tbody
id={@id}
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
>
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
<td
:for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)}
class={["", @row_click && "hover:cursor-pointer"]}
>
<div class="">
<span class="" />
<span class={["", i == 0 && ""]}>
<%= render_slot(col, @row_item.(row)) %>
</span>
</div>
</td>
<td :if={@action != []} class="">
<div class="">
<span class="" />
<span
:for={action <- @action}
class=""
>
<%= render_slot(action, @row_item.(row)) %>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
"""
end
@doc """
Renders a data list.
## Examples
<.list>
<:item title="Title"><%= @post.title %></:item>
<:item title="Views"><%= @post.views %></:item>
</.list>
"""
slot :item, required: true do
attr :title, :string, required: true
end
def list(assigns) do
~H"""
<div class="mt-6 mb-6">
<dl>
<div :for={item <- @item} class="columns is-mobile is-vcentered py-4">
<dt class="column is-one-quarter"><%= item.title %></dt>
<dd class="column"><%= render_slot(item) %></dd>
</div>
</dl>
</div>
"""
end
@doc """
Renders a back navigation link.
## Examples
<.back navigate={~p"/posts"}>Back to posts</.back>
"""
attr :navigate, :any, required: true
slot :inner_block, required: true
def back(assigns) do
~H"""
<div class="mt-16">
<.link
navigate={@navigate}
class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
<.icon name="hero-arrow-left-solid" class="h-3 w-3" />
<%= render_slot(@inner_block) %>
</.link>
</div>
"""
end
@doc """
Renders a [Heroicon](https://heroicons.com).
Heroicons come in three styles outline, solid, and mini.
By default, the outline style is used, but solid and mini may
be applied by using the `-solid` and `-mini` suffix.
You can customize the size and colors of the icons by setting
width, height, and background color classes.
Icons are extracted from the `deps/heroicons` directory and bundled within
your compiled app.css by the plugin in your `assets/tailwind.config.js`.
## Examples
<.icon name="hero-x-mark-solid" />
<.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
"""
attr :name, :string, required: true
attr :class, :string, default: nil
def icon(%{name: "hero-" <> _} = assigns) do
~H"""
<span class={[@name, @class]} />
"""
end
## JS Commands
def show(js \\ %JS{}, selector) do
JS.show(js,
to: selector,
time: 300,
transition:
{"transition-all transform ease-out duration-300",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
"opacity-100 translate-y-0 sm:scale-100"}
)
end
def hide(js \\ %JS{}, selector) do
JS.hide(js,
to: selector,
time: 200,
transition:
{"transition-all transform ease-in duration-200",
"opacity-100 translate-y-0 sm:scale-100",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
)
end
def show_modal(js \\ %JS{}, id) when is_binary(id) do
js
|> JS.show(to: "##{id}")
|> JS.show(
to: "##{id}-bg",
time: 300,
transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
)
|> show("##{id}-container")
|> JS.add_class("overflow-hidden", to: "body")
|> JS.focus_first(to: "##{id}-content")
end
def hide_modal(js \\ %JS{}, id) do
js
|> JS.hide(
to: "##{id}-bg",
transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
)
|> hide("##{id}-container")
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|> JS.remove_class("overflow-hidden", to: "body")
|> JS.pop_focus()
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
# # Translate the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# However the error messages in our forms and APIs are generated
# dynamically, so we need to translate them by calling Gettext
# with our gettext backend as first argument. Translations are
# available in the errors.po file (as we use the "errors" domain).
if count = opts[:count] do
Gettext.dngettext(BrightWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(BrightWeb.Gettext, "errors", msg, opts)
end
end
@doc """
Translates the errors for a field from a keyword list of errors.
"""
def translate_errors(errors, field) when is_list(errors) do
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
end
end

View File

@ -0,0 +1,14 @@
defmodule BrightWeb.Layouts do
@moduledoc """
This module holds different layouts used by your application.
See the `layouts` directory for all templates available.
The "root" layout is a skeleton rendered as part of the
application router. The "app" layout is set as the default
layout on both `use BrightWeb, :controller` and
`use BrightWeb, :live_view`.
"""
use BrightWeb, :html
embed_templates "layouts/*"
end

View File

@ -0,0 +1,6 @@
<.navbar></.navbar>
<main class="section">
<.flash_group flash={@flash} />
<%= @inner_content %>
</main>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en" class="[scrollbar-gutter:stable]">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title suffix=" · Phoenix Framework">
<%= assigns[:page_title] || "Bright" %>
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
</script>
</head>
<body class="">
<%= @inner_content %>
</body>
</html>

View File

@ -0,0 +1,114 @@
defmodule BrightWeb.NavigationComponents do
@moduledoc """
Components for user navigation
"""
use Phoenix.Component
use Phoenix.VerifiedRoutes,
endpoint: BrightWeb.Endpoint,
router: BrightWeb.Router,
statics: BrightWeb.static_paths()
alias Phoenix.LiveView.JS
@doc """
Renders a Bulma navbar component.
## Examples
<.navbar brand="MyApp">
<:start>
<a class="navbar-item" href="/">Home</a>
<a class="navbar-item" href="/about">About</a>
</:start>
<:end>
<a class="navbar-item" href="/login">Login</a>
<a class="navbar-item" href="/signup">Sign Up</a>
</:end>
</.navbar>
"""
attr :rest, :global, doc: "any additional attributes for the navbar element"
slot :start_slot, doc: "slot for navbar items aligned to the start"
slot :end_slot, doc: "slot for navbar items aligned to the end"
def navbar(assigns) do
~H"""
<nav class="navbar" role="navigation" aria-label="main navigation" {@rest}>
<div class="navbar-brand">
<.link
href={~p"/"}
class="navbar-item"
>
<h1 class="title">🔞💦 Futureporn.net</h1>
</.link>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" phx-click={JS.toggle(to: ".navbar-menu")}>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
<.link
href={~p"/streams"}
class="navbar-item">
Streams Archive
</.link>
<.link
href={~p"/vtubers"}
class="navbar-item">
Vtubers
</.link>
<.link
href={~p"/about"}
class="navbar-item">
About
</.link>
<.link
href={~p"/tags"}
class="navbar-item">
Tags
</.link>
<.link
href={~p"/patrons"}
class="navbar-item">
Patrons
</.link>
<.link
href={~p"/api"}
class="navbar-item">
API
</.link>
</div>
<div class="navbar-end">
<.link
href={~p"/status"}
class="navbar-item">
Status
</.link>
<.link
href={~p"/profile"}
class="navbar-item">
Profile
</.link>
</div>
</div>
</nav>
"""
end
end

View File

@ -0,0 +1,32 @@
defmodule BrightWeb.CartController do
use BrightWeb, :controller
alias Bright.ShoppingCart
def show(conn, _params) do
render(conn, :show, changeset: ShoppingCart.change_cart(conn.assigns.cart))
# render(conn, :show)
end
def update(conn, %{"cart" => cart_params}) do
case ShoppingCart.update_cart(conn.assigns.cart, cart_params) do
{:ok, _cart} ->
redirect(conn, to: ~p"/cart")
{:error, _changeset} ->
conn
|> put_flash(:error, "There was an error updating your cart")
|> redirect(to: ~p"/cart")
end
end
end
# def show(conn, %{"id" => id}) do
# product = Catalog.get_product!(id)
# render(conn, :show, product: product)
# end
# def show(conn, %{"id" => id}) do
# stream =
# id
# |> Streams.get_stream!()
# |> Streams.inc_page_views()
# render(conn, :show, stream: stream)
# end

View File

@ -0,0 +1,10 @@
defmodule BrightWeb.CartHTML do
use BrightWeb, :html
# this alias is for the html.heex templates
alias Bright.ShoppingCart
embed_templates "cart_html/*"
def currency_to_str(%Decimal{} = val), do: "$#{Decimal.round(val, 2)}"
end

View File

@ -0,0 +1,26 @@
<.header>
My Cart
<:subtitle :if={@cart.items == []}>Your cart is empty</:subtitle>
<:actions>
<.link href={~p"/orders"} method="post">
<.button>Complete order</.button>
</.link>
</:actions>
</.header>
<div :if={@cart.items !== []}>
<.simple_form :let={f} for={@changeset} action={~p"/cart"}>
<.inputs_for :let={%{data: item} = item_form} field={f[:items]}>
<.input field={item_form[:quantity]} type="number" label={item.product.title} />
{currency_to_str(ShoppingCart.total_item_price(item))}
</.inputs_for>
<:actions>
<.button>Update cart</.button>
</:actions>
</.simple_form>
<b>Total</b>: {currency_to_str(ShoppingCart.total_cart_price(@cart))}
</div>
<.back navigate={~p"/products"}>Back to products</.back>

View File

@ -0,0 +1,22 @@
defmodule BrightWeb.CartItemController do
use BrightWeb, :controller
alias Bright.ShoppingCart
def create(conn, %{"product_id" => product_id}) do
case ShoppingCart.add_item_to_cart(conn.assigns.cart, product_id) do
{:ok, _item} ->
conn
|> put_flash(:info, "Item added to your cart")
|> redirect(to: ~p"/cart")
{:error, _changeset} ->
conn
|> put_flash(:eerror, "There was an error adding the item to your cart")
|> redirect(to: ~p"/cart")
end
end
def delete(conn, %{"id" => product_id}) do
{:ok, _cart} = ShoppingCart.remove_item_from_cart(conn.assigns.cart, product_id)
redirect(conn, to: ~p"/cart")
end
end

View File

@ -0,0 +1,25 @@
defmodule BrightWeb.ChangesetJSON do
@doc """
Renders changeset errors.
"""
def error(%{changeset: changeset}) do
# When encoded, the changeset returns its errors
# as a JSON object. So we just pass it forward.
%{errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)}
end
defp translate_error({msg, opts}) do
# You can make use of gettext to translate error messages by
# uncommenting and adjusting the following code:
# if count = opts[:count] do
# Gettext.dngettext(BrightWeb.Gettext, "errors", msg, msg, count, opts)
# else
# Gettext.dgettext(BrightWeb.Gettext, "errors", msg, opts)
# end
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
end)
end
end

View File

@ -0,0 +1,24 @@
defmodule BrightWeb.ErrorHTML do
@moduledoc """
This module is invoked by your endpoint in case of errors on HTML requests.
See config/config.exs.
"""
use BrightWeb, :html
# If you want to customize your error pages,
# uncomment the embed_templates/1 call below
# and add pages to the error directory:
#
# * lib/bright_web/controllers/error_html/404.html.heex
# * lib/bright_web/controllers/error_html/500.html.heex
#
# embed_templates "error_html/*"
# The default is to render a plain text page based on
# the template name. For example, "404.html" becomes
# "Not Found".
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end

View File

@ -0,0 +1,21 @@
defmodule BrightWeb.ErrorJSON do
@moduledoc """
This module is invoked by your endpoint in case of errors on JSON requests.
See config/config.exs.
"""
# If you want to customize a particular status code,
# you may add your own clauses, such as:
#
# def render("500.json", _assigns) do
# %{errors: %{detail: "Internal Server Error"}}
# end
# By default, Phoenix returns the status message from
# the template name. For example, "404.json" becomes
# "Not Found".
def render(template, _assigns) do
%{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
end
end

View File

@ -0,0 +1,24 @@
defmodule BrightWeb.FallbackController do
@moduledoc """
Translates controller action results into valid `Plug.Conn` responses.
See `Phoenix.Controller.action_fallback/1` for more details.
"""
use BrightWeb, :controller
# This clause handles errors returned by Ecto's insert/update/delete.
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(json: BrightWeb.ChangesetJSON)
|> render(:error, changeset: changeset)
end
# This clause is an example of how to handle resources that cannot be found.
def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> put_view(html: BrightWeb.ErrorHTML, json: BrightWeb.ErrorJSON)
|> render(:"404")
end
end

View File

@ -0,0 +1,17 @@
defmodule BrightWeb.HelloController do
use BrightWeb, :controller
# plug :put_view, html: HelloWeb.PageHTML, json: HelloWeb.PageJSON
def index(conn, _params) do
render(conn, :index)
end
def show(conn, %{"messenger" => messenger}) do
conn
|> assign(:messenger, messenger)
|> assign(:receiver, "Dweezil")
|> render(:show)
end
end

View File

@ -0,0 +1,13 @@
defmodule BrightWeb.HelloHTML do
use BrightWeb, :html
embed_templates "hello_html/*"
attr :messenger, :string, required: true
def greet(assigns) do
~H"""
<h2>Hello World, from {@messenger}!</h2>
"""
end
end

View File

@ -0,0 +1,3 @@
<section>
<h2>Hello World, from Phoenix!~</h2>
</section>

View File

@ -0,0 +1,3 @@
<section>
<.greet messenger={@messenger} />
</section>

View File

@ -0,0 +1,21 @@
defmodule BrightWeb.OrderController do
use BrightWeb, :controller
alias Bright.Orders
def create(conn, _) do
case Orders.complete_order(conn.assigns.cart) do
{:ok, order} ->
conn
|> put_flash(:info, "Order created successfully.")
|> redirect(to: ~p"/orders/#{order}")
{:error, _reason} ->
conn
|> put_flash(:error, "There was an error processing your order")
|> redirect(to: ~p"/cart")
end
end
def show(conn, %{"id" => id}) do
order = Orders.get_order!(conn.assigns.current_uuid, id)
render(conn, :show, order: order)
end
end

View File

@ -0,0 +1,4 @@
defmodule BrightWeb.OrderHTML do
use BrightWeb, :html
embed_templates "order_html/*"
end

View File

@ -0,0 +1,20 @@
<.header>
Thank you for your order!
<:subtitle>
<strong>User uuid: </strong>{@order.user_uuid}
</:subtitle>
</.header>
<.table id="items" rows={@order.line_items}>
<:col :let={item} label="Title">{item.product.title}</:col>
<:col :let={item} label="Quantity">{item.quantity}</:col>
<:col :let={item} label="Price">
{BrightWeb.CartHTML.currency_to_str(item.price)}
</:col>
</.table>
<strong>Total price:</strong>
{BrightWeb.CartHTML.currency_to_str(@order.total_price)}
<.back navigate={~p"/products"}>Back to products</.back>

View File

@ -0,0 +1,30 @@
defmodule BrightWeb.PageController do
use BrightWeb, :controller
def home(conn, _params) do
# The home page is often custom made,
# so skip the default app layout.
# render(conn, :home, layout: false)
# send_resp(conn, 201, "")
conn
|> put_flash(:info, "You are beautiful!")
|> put_flash(:error, "Test error!")
|> put_status(202)
|> render(:home, layout: false)
# redirect(conn, to: ~p"/redirect_test")
# redirect(conn, external: "https://elixir-lang.org/")
end
def about(conn, _params) do
render(conn, :about, layout: false)
end
def api(conn, _params) do
render(conn, :api, layout: false)
end
def redirect_test(conn, _params) do
render(conn, :home, layout: false)
end
end

View File

@ -0,0 +1,10 @@
defmodule BrightWeb.PageHTML do
@moduledoc """
This module contains pages rendered by PageController.
See the `page_html` directory for all templates available.
"""
use BrightWeb, :html
embed_templates "page_html/*"
end

View File

@ -0,0 +1,7 @@
<.flash_group flash={@flash} />
<.navbar />
<main class="section">
<h2 class="title is-2">About</h2>
</main>

View File

@ -0,0 +1,16 @@
<.flash_group flash={@flash} />
<.navbar />
<main class="section">
<h2 class="title is-2">API</h2>
<p class="subtitle">For Developers and Power Users</p>
</main>
<section class="section">
<p>@todo</p>
</section>

View File

@ -0,0 +1,43 @@
<.navbar />
<main class="section">
<.flash_group flash={@flash} />
<div class="mx-auto max-w-xl lg:mx-0">
<p class="title is-2">
The Galaxy's Best VTuber Hentai Site
</p>
<p class="subtitle">
For adults only (NSFW)
</p>
<div class="section">
<h2 class="is-2 title">Latest VODs</h2>
<%= for number <- 1..10 do %>
<tr>
<td><%= number %></td>
<td><%= number * number %></td>
</tr>
<% end %>
</div>
<div class="mt-10">
<button class="button is-rounded" phx-click="inc_temperature">+</button>
</div>
<a class="button is-info" href={~p"/hello/CJ"}>Hello {@current_uuid}</a>
<a class="button is-info" href={~p"/products"}>products</a>
<a class="button is-info" href={~p"/cart"}>cart</a>
<a class="button is-primary" href={~p"/archive"}>Archive</a>
<.back navigate={~p"/posts"}>Back</.back>
</div>
</main>

View File

@ -0,0 +1,5 @@
defmodule BrightWeb.PageJSON do
def home(_assigns) do
%{message: "this is some JSON"}
end
end

View File

@ -0,0 +1,10 @@
defmodule BrightWeb.PatronController do
use BrightWeb, :controller
alias Bright.Patrons
def index(conn, _params) do
patrons = Patrons.list_patrons()
render(conn, :index, patrons: patrons)
end
end

View File

@ -0,0 +1,4 @@
defmodule BrightWeb.PatronHTML do
use BrightWeb, :html
embed_templates "patron_html/*"
end

View File

@ -0,0 +1,14 @@
<.header>
Listing Patrons
</.header>
<.table id="patrons" rows={@patrons}>
<:col :let={patron} label="Name">{patron.name}</:col>
</.table>
<%#
<% for patron <- @patrons do
<div class="card">
</div>
end %>

View File

@ -0,0 +1,62 @@
defmodule BrightWeb.PlatformController do
use BrightWeb, :controller
alias Bright.Platforms
alias Bright.Platforms.Platform
def index(conn, _params) do
platforms = Platforms.list_platforms()
render(conn, :index, platforms: platforms)
end
def new(conn, _params) do
changeset = Platforms.change_platform(%Platform{})
render(conn, :new, changeset: changeset)
end
def create(conn, %{"platform" => platform_params}) do
case Platforms.create_platform(platform_params) do
{:ok, platform} ->
conn
|> put_flash(:info, "Platform created successfully.")
|> redirect(to: ~p"/platforms/#{platform}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
platform = Platforms.get_platform!(id)
render(conn, :show, platform: platform)
end
def edit(conn, %{"id" => id}) do
platform = Platforms.get_platform!(id)
changeset = Platforms.change_platform(platform)
render(conn, :edit, platform: platform, changeset: changeset)
end
def update(conn, %{"id" => id, "platform" => platform_params}) do
platform = Platforms.get_platform!(id)
case Platforms.update_platform(platform, platform_params) do
{:ok, platform} ->
conn
|> put_flash(:info, "Platform updated successfully.")
|> redirect(to: ~p"/platforms/#{platform}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit, platform: platform, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
platform = Platforms.get_platform!(id)
{:ok, _platform} = Platforms.delete_platform(platform)
conn
|> put_flash(:info, "Platform deleted successfully.")
|> redirect(to: ~p"/platforms")
end
end

View File

@ -0,0 +1,13 @@
defmodule BrightWeb.PlatformHTML do
use BrightWeb, :html
embed_templates "platform_html/*"
@doc """
Renders a platform form.
"""
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
def platform_form(assigns)
end

View File

@ -0,0 +1,8 @@
<.header>
Edit Platform {@platform.id}
<:subtitle>Use this form to manage platform records in the database.</:subtitle>
</.header>
<.platform_form changeset={@changeset} action={~p"/platforms/#{@platform}"} />
<.back navigate={~p"/platforms"}>Back to platforms</.back>

View File

@ -0,0 +1,25 @@
<.header>
Listing Platforms
<:actions>
<.link href={~p"/platforms/new"}>
<.button>New Platform</.button>
</.link>
</:actions>
</.header>
<.table id="platforms" rows={@platforms} row_click={&JS.navigate(~p"/platforms/#{&1}")}>
<:col :let={platform} label="Name">{platform.name}</:col>
<:col :let={platform} label="Url">{platform.url}</:col>
<:col :let={platform} label="Icon">{raw(platform.icon)}</:col>
<:action :let={platform}>
<div class="sr-only">
<.link navigate={~p"/platforms/#{platform}"}>Show</.link>
</div>
<.link navigate={~p"/platforms/#{platform}/edit"}>Edit</.link>
</:action>
<:action :let={platform}>
<.link href={~p"/platforms/#{platform}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>

View File

@ -0,0 +1,8 @@
<.header>
New Platform
<:subtitle>Use this form to manage platform records in the database.</:subtitle>
</.header>
<.platform_form changeset={@changeset} action={~p"/platforms"} />
<.back navigate={~p"/platforms"}>Back to platforms</.back>

View File

@ -0,0 +1,11 @@
<.simple_form :let={f} for={@changeset} action={@action}>
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={f[:name]} type="text" label="Name" />
<.input field={f[:url]} type="text" label="Url" />
<.input field={f[:icon]} type="text" label="Icon" />
<:actions>
<.button>Save Platform</.button>
</:actions>
</.simple_form>

View File

@ -0,0 +1,17 @@
<.header>
Platform {@platform.id}
<:subtitle>This is a platform record from the database.</:subtitle>
<:actions>
<.link href={~p"/platforms/#{@platform}/edit"}>
<.button>Edit platform</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Name">{@platform.name}</:item>
<:item title="Url">{@platform.url}</:item>
<:item title="Icon">{raw(@platform.icon)}</:item>
</.list>
<.back navigate={~p"/platforms"}>Back to platforms</.back>

View File

@ -0,0 +1,62 @@
defmodule BrightWeb.ProductController do
use BrightWeb, :controller
alias Bright.Catalog
alias Bright.Catalog.Product
def index(conn, _params) do
products = Catalog.list_products()
render(conn, :index, products: products)
end
def new(conn, _params) do
changeset = Catalog.change_product(%Product{})
render(conn, :new, changeset: changeset)
end
def create(conn, %{"product" => product_params}) do
case Catalog.create_product(product_params) do
{:ok, product} ->
conn
|> put_flash(:info, "Product created successfully.")
|> redirect(to: ~p"/products/#{product}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
product = Catalog.get_product!(id)
render(conn, :show, product: product)
end
def edit(conn, %{"id" => id}) do
product = Catalog.get_product!(id)
changeset = Catalog.change_product(product)
render(conn, :edit, product: product, changeset: changeset)
end
def update(conn, %{"id" => id, "product" => product_params}) do
product = Catalog.get_product!(id)
case Catalog.update_product(product, product_params) do
{:ok, product} ->
conn
|> put_flash(:info, "Product updated successfully.")
|> redirect(to: ~p"/products/#{product}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit, product: product, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
product = Catalog.get_product!(id)
{:ok, _product} = Catalog.delete_product(product)
conn
|> put_flash(:info, "Product deleted successfully.")
|> redirect(to: ~p"/products")
end
end

View File

@ -0,0 +1,23 @@
defmodule BrightWeb.ProductHTML do
use BrightWeb, :html
embed_templates "product_html/*"
@doc """
Renders a product form.
"""
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
def product_form(assigns)
def category_opts(changeset) do
existing_ids =
changeset
|> Ecto.Changeset.get_change(:categories, [])
|> Enum.map(& &1.data.id)
for cat <- Bright.Catalog.list_categories(),
do: [key: cat.title, value: cat.id, selected: cat.id in existing_ids]
end
end

View File

@ -0,0 +1,8 @@
<.header>
Edit Product {@product.id}
<:subtitle>Use this form to manage product records in the database.</:subtitle>
</.header>
<.product_form changeset={@changeset} action={~p"/products/#{@product}"} />
<.back navigate={~p"/products"}>Back to products</.back>

View File

@ -0,0 +1,26 @@
<.header>
Listing Products
<:actions>
<.link href={~p"/products/new"}>
<.button>New Product</.button>
</.link>
</:actions>
</.header>
<.table id="products" rows={@products} row_click={&JS.navigate(~p"/products/#{&1}")}>
<:col :let={product} label="Title">{product.title}</:col>
<:col :let={product} label="Description">{product.description}</:col>
<:col :let={product} label="Price">{product.price}</:col>
<:col :let={product} label="Views">{product.views}</:col>
<:action :let={product}>
<div class="sr-only">
<.link navigate={~p"/products/#{product}"}>Show</.link>
</div>
<.link navigate={~p"/products/#{product}/edit"}>Edit</.link>
</:action>
<:action :let={product}>
<.link href={~p"/products/#{product}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>

View File

@ -0,0 +1,8 @@
<.header>
New Product
<:subtitle>Use this form to manage product records in the database.</:subtitle>
</.header>
<.product_form changeset={@changeset} action={~p"/products"} />
<.back navigate={~p"/products"}>Back to products</.back>

View File

@ -0,0 +1,12 @@
<.simple_form :let={f} for={@changeset} action={@action}>
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={f[:title]} type="text" label="Title" />
<.input field={f[:description]} type="text" label="Description" />
<.input field={f[:price]} type="number" label="Price" step="any" />
<.input field={f[:category_ids]} type="select" multiple={true} options={category_opts(@changeset)} />
<:actions>
<.button>Save Product</.button>
</:actions>
</.simple_form>

View File

@ -0,0 +1,26 @@
<.header>
Product {@product.id}
<:subtitle>This is a product record from the database.</:subtitle>
<:actions>
<.link href={~p"/products/#{@product}/edit"}>
<.button>Edit product</.button>
</.link>
<.link href={~p"/cart_items?product_id=#{@product.id}"} method="post">
<.button>Add to cart</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Title">{@product.title}</:item>
<:item title="Description">{@product.description}</:item>
<:item title="Price">{@product.price}</:item>
<:item title="Views">{@product.views}</:item>
<:item title="Categories">
<ul>
<li :for={cat <- @product.categories}>{cat.title}</li>
</ul>
</:item>
</.list>
<.back navigate={~p"/products"}>Back to products</.back>

View File

@ -0,0 +1,66 @@
defmodule BrightWeb.StreamController do
use BrightWeb, :controller
alias Bright.Streams
alias Bright.Streams.Stream
def index(conn, _params) do
streams = Streams.list_streams()
render(conn, :index, streams: streams)
end
def new(conn, _params) do
changeset = Streams.change_stream(%Stream{})
render(conn, :new, changeset: changeset)
end
def create(conn, %{"stream" => stream_params}) do
case Streams.create_stream(stream_params) do
{:ok, stream} ->
conn
|> put_flash(:info, "Stream created successfully.")
|> redirect(to: ~p"/streams/#{stream}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
stream =
id
|> Streams.get_stream!()
|> Streams.inc_page_views()
render(conn, :show, stream: stream)
end
def edit(conn, %{"id" => id}) do
stream = Streams.get_stream!(id)
changeset = Streams.change_stream(stream)
render(conn, :edit, stream: stream, changeset: changeset)
end
def update(conn, %{"id" => id, "stream" => stream_params}) do
stream = Streams.get_stream!(id)
case Streams.update_stream(stream, stream_params) do
{:ok, stream} ->
conn
|> put_flash(:info, "Stream updated successfully.")
|> redirect(to: ~p"/streams/#{stream}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit, stream: stream, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
stream = Streams.get_stream!(id)
{:ok, _stream} = Streams.delete_stream(stream)
conn
|> put_flash(:info, "Stream deleted successfully.")
|> redirect(to: ~p"/streams")
end
end

View File

@ -0,0 +1,58 @@
defmodule BrightWeb.StreamHTML do
use BrightWeb, :html
embed_templates "stream_html/*"
@doc """
Renders a stream form.
"""
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
def stream_form(assigns)
def tag_opts(changeset) do
existing_ids =
changeset
|> Ecto.Changeset.get_change(:tags, [])
|> Enum.map(& &1.data.id)
for tag <- Bright.Tags.list_tags(),
do: [key: tag.name, value: tag.id, selected: tag.id in existing_ids]
end
def vod_opts(changeset) do
existing_ids =
changeset
|> Ecto.Changeset.get_change(:vods, [])
|> Enum.map(& &1.data.id)
for vod <- Bright.Streams.list_vods(),
do: [key: vod.id, value: vod.id, selected: vod.id in existing_ids]
end
def vtuber_opts(changeset) do
existing_ids =
changeset
|> Ecto.Changeset.get_change(:vtubers, [])
|> Enum.map(& &1.data.id)
for vtuber <- Bright.Vtubers.list_vtubers(),
do: [key: vtuber.display_name, value: vtuber.id, selected: vtuber.id in existing_ids]
end
def platform_opts(changeset) do
existing_ids =
changeset
|> Ecto.Changeset.get_change(:vtubers, [])
|> Enum.map(& &1.data.id)
for platform <- Bright.Platforms.list_platforms(),
do: [key: platform.name, value: platform.id, selected: platform.id in existing_ids]
end
end

View File

@ -0,0 +1,8 @@
<.header>
Edit Stream {@stream.id}
<:subtitle>Use this form to manage stream records in the database.</:subtitle>
</.header>
<.stream_form changeset={@changeset} action={~p"/streams/#{@stream}"} />
<.back navigate={~p"/streams"}>Back to streams</.back>

View File

@ -0,0 +1,54 @@
<.header>
Listing Streams
<:actions>
<.link href={~p"/streams/new"}>
<.button>New Stream</.button>
</.link>
</:actions>
</.header>
<.table id="streams" rows={@streams} row_click={&JS.navigate(~p"/streams/#{&1}")}>
<:col :let={stream} label="ID">{stream.id}</:col>
<:col :let={stream} label="Title">{stream.title}</:col>
<:col :let={stream} label="Date">{stream.date}</:col>
<:col :let={stream} label="Platforms">
<div class="columns is-1">
<%= for platform <- stream.platforms do %>
<div class="column is-mobile is-narrow">
{raw(platform.icon)}
</div>
<% end %>
</div>
</:col>
<:col :let={stream} label="Vtubers">
<div class="columns is-1">
<%= for vtuber <- stream.vtubers do %>
<div class="column is-mobile is-narrow">
<figure class="image is-24x24">
<img src={vtuber.image} alt={vtuber.display_name} class="is-rounded" />
</figure>
</div>
<% end %>
</div>
</:col>
<:col :let={stream} label="VODs">
<div class="columns is-1">
<%= for vod <- stream.vods do %>
<div class="column is-mobile is-narrow">
<.link href={~p"/vods/#{vod.id}"}>#{vod.id}</.link>
</div>
<% end %>
</div>
</:col>
<:action :let={stream}>
<div class="sr-only">
<.link navigate={~p"/streams/#{stream}"}>Show</.link>
</div>
<.link navigate={~p"/streams/#{stream}/edit"}>Edit</.link>
</:action>
<:action :let={stream}>
<.link href={~p"/streams/#{stream}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>

View File

@ -0,0 +1,8 @@
<.header>
New Stream
<:subtitle>Use this form to manage stream records in the database.</:subtitle>
</.header>
<.stream_form changeset={@changeset} action={~p"/streams"} />
<.back navigate={~p"/streams"}>Back to streams</.back>

View File

@ -0,0 +1,36 @@
<.header>
Stream {@stream.id}
<:subtitle>This is a stream record from the database.</:subtitle>
<:actions>
<.link href={~p"/streams/#{@stream}/edit"}>
<.button>Edit stream</.button>
</.link>
</:actions>
</.header>
<.list>
<:item title="Title">{@stream.title}</:item>
<:item title="Notes">{@stream.notes}</:item>
<:item title="Date">{@stream.date}</:item>
<:item title="Views">{@stream.views}</:item>
<:item title="Tags">
<ul>
<li :for={tag <- @stream.tags}>{tag.name}</li>
</ul>
</:item>
<:item title="Vods">
<ul>
<li :for={vod <- @stream.vods}><.link href={~p"/vods/#{vod.id}"}>{vod.s3_key}</.link></li>
</ul>
</:item>
<:item title="Vtubers">
<ul>
<li :for={vtuber <- @stream.vtubers}>{vtuber.display_name}</li>
</ul>
</:item>
</.list>
<.back navigate={~p"/streams"}>Back to streams</.back>

View File

@ -0,0 +1,17 @@
<.simple_form :let={f} for={@changeset} action={@action}>
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={f[:title]} type="text" label="Title" />
<.input field={f[:notes]} type="text" label="Notes" />
<.input field={f[:date]} type="datetime-local" label="Date" />
<.input field={f[:tag_ids]} label="Tags" type="select" multiple={true} options={tag_opts(@changeset)} />
<.input field={f[:vod_ids]} label="Vods" type="select" multiple={true} options={vod_opts(@changeset)} />
<.input field={f[:platform_ids]} label="Platforms" type="select" multiple={true} options={platform_opts(@changeset)} />
<.input field={f[:vtuber_ids]} label="Vtubers" type="select" multiple={true} options={vtuber_opts(@changeset)} />
<:actions>
<.button>Save Stream</.button>
</:actions>
</.simple_form>

View File

@ -0,0 +1,62 @@
defmodule BrightWeb.TagController do
use BrightWeb, :controller
alias Bright.Tags
alias Bright.Tags.Tag
def index(conn, _params) do
tags = Tags.list_tags()
render(conn, :index, tags: tags)
end
def new(conn, _params) do
changeset = Tags.change_tag(%Tag{})
render(conn, :new, changeset: changeset)
end
def create(conn, %{"tag" => tag_params}) do
case Tags.create_tag(tag_params) do
{:ok, tag} ->
conn
|> put_flash(:info, "Tag created successfully.")
|> redirect(to: ~p"/tags/#{tag}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
tag = Tags.get_tag!(id)
render(conn, :show, tag: tag)
end
def edit(conn, %{"id" => id}) do
tag = Tags.get_tag!(id)
changeset = Tags.change_tag(tag)
render(conn, :edit, tag: tag, changeset: changeset)
end
def update(conn, %{"id" => id, "tag" => tag_params}) do
tag = Tags.get_tag!(id)
case Tags.update_tag(tag, tag_params) do
{:ok, tag} ->
conn
|> put_flash(:info, "Tag updated successfully.")
|> redirect(to: ~p"/tags/#{tag}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :edit, tag: tag, changeset: changeset)
end
end
def delete(conn, %{"id" => id}) do
tag = Tags.get_tag!(id)
{:ok, _tag} = Tags.delete_tag(tag)
conn
|> put_flash(:info, "Tag deleted successfully.")
|> redirect(to: ~p"/tags")
end
end

View File

@ -0,0 +1,13 @@
defmodule BrightWeb.TagHTML do
use BrightWeb, :html
embed_templates "tag_html/*"
@doc """
Renders a tag form.
"""
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
def tag_form(assigns)
end

View File

@ -0,0 +1,8 @@
<.header>
Edit Tag {@tag.id}
<:subtitle>Use this form to manage tag records in the database.</:subtitle>
</.header>
<.tag_form changeset={@changeset} action={~p"/tags/#{@tag}"} />
<.back navigate={~p"/tags"}>Back to tags</.back>

View File

@ -0,0 +1,23 @@
<.header>
Listing Tags
<:actions>
<.link href={~p"/tags/new"}>
<.button>New Tag</.button>
</.link>
</:actions>
</.header>
<.table id="tags" rows={@tags} row_click={&JS.navigate(~p"/tags/#{&1}")}>
<:col :let={tag} label="Name">{tag.name}</:col>
<:action :let={tag}>
<div class="sr-only">
<.link navigate={~p"/tags/#{tag}"}>Show</.link>
</div>
<.link navigate={~p"/tags/#{tag}/edit"}>Edit</.link>
</:action>
<:action :let={tag}>
<.link href={~p"/tags/#{tag}"} method="delete" data-confirm="Are you sure?">
Delete
</.link>
</:action>
</.table>

Some files were not shown because too many files have changed in this diff Show More