phoenix integration begin
ci / build (push) Has started running
Details
ci / build (push) Has started running
Details
This commit is contained in:
parent
5e83742341
commit
8b8de3b072
|
@ -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
|
|
@ -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"]
|
||||
]
|
|
@ -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/
|
||||
|
|
@ -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
|
|
@ -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 */
|
|
@ -0,0 +1,4 @@
|
|||
/* This file is for your main application CSS */
|
||||
// @import "./phoenix.css";
|
||||
@import "bulma";
|
||||
|
|
@ -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
|
||||
|
|
@ -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})
|
||||
})
|
||||
]
|
||||
}
|
|
@ -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));
|
|
@ -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"
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
defmodule Bright.Mailer do
|
||||
use Swoosh.Mailer, otp_app: :bright
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
defmodule Bright.Repo do
|
||||
use Ecto.Repo,
|
||||
otp_app: :bright,
|
||||
adapter: Ecto.Adapters.Postgres
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
<.navbar></.navbar>
|
||||
|
||||
<main class="section">
|
||||
<.flash_group flash={@flash} />
|
||||
<%= @inner_content %>
|
||||
</main>
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
<section>
|
||||
<h2>Hello World, from Phoenix!~</h2>
|
||||
</section>
|
|
@ -0,0 +1,3 @@
|
|||
<section>
|
||||
<.greet messenger={@messenger} />
|
||||
</section>
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
defmodule BrightWeb.OrderHTML do
|
||||
use BrightWeb, :html
|
||||
embed_templates "order_html/*"
|
||||
end
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
<.flash_group flash={@flash} />
|
||||
|
||||
<.navbar />
|
||||
|
||||
<main class="section">
|
||||
<h2 class="title is-2">About</h2>
|
||||
</main>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
|||
defmodule BrightWeb.PageJSON do
|
||||
def home(_assigns) do
|
||||
%{message: "this is some JSON"}
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
defmodule BrightWeb.PatronHTML do
|
||||
use BrightWeb, :html
|
||||
embed_templates "patron_html/*"
|
||||
end
|
|
@ -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 %>
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
|
@ -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
Loading…
Reference in New Issue