From cf70656b4799993738cd6015a097714da6f55a44 Mon Sep 17 00:00:00 2001 From: Koni vKxni Date: Sun, 18 Dec 2022 20:36:57 +0100 Subject: [PATCH] init --- .formatter.exs | 4 + .gitignore | 34 ++++ README.md | 183 ++++++++++++++++++ assets/css/app.css | 3 + assets/js/app.js | 44 +++++ assets/tailwind.config.js | 10 + assets/vendor/topbar.js | 157 +++++++++++++++ config/config.exs | 48 +++++ config/dev.exs | 66 +++++++ config/prod.exs | 49 +++++ config/runtime.exs | 52 +++++ config/test.exs | 14 ++ lib/koniphx.ex | 9 + lib/koniphx/application.ex | 34 ++++ lib/koniphx_web.ex | 110 +++++++++++ .../controllers/dashboard_controller.ex | 11 ++ .../controllers/page_controller.ex | 7 + lib/koniphx_web/endpoint.ex | 49 +++++ lib/koniphx_web/gettext.ex | 24 +++ lib/koniphx_web/router.ex | 39 ++++ lib/koniphx_web/telemetry.ex | 48 +++++ .../templates/dashboard/dashboard.html.heex | 6 + .../templates/layout/app.html.heex | 5 + .../templates/layout/live.html.heex | 11 ++ .../templates/layout/root.html.heex | 15 ++ .../templates/page/index.html.heex | 18 ++ lib/koniphx_web/views/dashboard_view.ex | 3 + lib/koniphx_web/views/error_helpers.ex | 47 +++++ lib/koniphx_web/views/error_view.ex | 16 ++ lib/koniphx_web/views/layout_view.ex | 7 + lib/koniphx_web/views/page_view.ex | 3 + mix.exs | 64 ++++++ mix.lock | 28 +++ priv/gettext/en/LC_MESSAGES/errors.po | 11 ++ priv/gettext/errors.pot | 10 + priv/static/robots.txt | 5 + .../controllers/page_controller_test.exs | 8 + test/koniphx_web/views/error_view_test.exs | 14 ++ test/koniphx_web/views/layout_view_test.exs | 8 + test/koniphx_web/views/page_view_test.exs | 3 + test/support/channel_case.ex | 34 ++++ test/support/conn_case.ex | 37 ++++ test/test_helper.exs | 1 + 43 files changed, 1349 insertions(+) create mode 100644 .formatter.exs create mode 100644 .gitignore create mode 100644 README.md create mode 100644 assets/css/app.css create mode 100644 assets/js/app.js create mode 100644 assets/tailwind.config.js create mode 100644 assets/vendor/topbar.js create mode 100644 config/config.exs create mode 100644 config/dev.exs create mode 100644 config/prod.exs create mode 100644 config/runtime.exs create mode 100644 config/test.exs create mode 100644 lib/koniphx.ex create mode 100644 lib/koniphx/application.ex create mode 100644 lib/koniphx_web.ex create mode 100644 lib/koniphx_web/controllers/dashboard_controller.ex create mode 100644 lib/koniphx_web/controllers/page_controller.ex create mode 100644 lib/koniphx_web/endpoint.ex create mode 100644 lib/koniphx_web/gettext.ex create mode 100644 lib/koniphx_web/router.ex create mode 100644 lib/koniphx_web/telemetry.ex create mode 100644 lib/koniphx_web/templates/dashboard/dashboard.html.heex create mode 100644 lib/koniphx_web/templates/layout/app.html.heex create mode 100644 lib/koniphx_web/templates/layout/live.html.heex create mode 100644 lib/koniphx_web/templates/layout/root.html.heex create mode 100644 lib/koniphx_web/templates/page/index.html.heex create mode 100644 lib/koniphx_web/views/dashboard_view.ex create mode 100644 lib/koniphx_web/views/error_helpers.ex create mode 100644 lib/koniphx_web/views/error_view.ex create mode 100644 lib/koniphx_web/views/layout_view.ex create mode 100644 lib/koniphx_web/views/page_view.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 priv/gettext/en/LC_MESSAGES/errors.po create mode 100644 priv/gettext/errors.pot create mode 100644 priv/static/robots.txt create mode 100644 test/koniphx_web/controllers/page_controller_test.exs create mode 100644 test/koniphx_web/views/error_view_test.exs create mode 100644 test/koniphx_web/views/layout_view_test.exs create mode 100644 test/koniphx_web/views/page_view_test.exs create mode 100644 test/support/channel_case.ex create mode 100644 test/support/conn_case.ex create mode 100644 test/test_helper.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..4761678 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +[ + import_deps: [:phoenix], + inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cee2225 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# 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 + +# Ignore package tarball (built via "mix hex.build"). +koniphx-*.tar + +# Ignore assets that are produced by build tools. +/priv/static/assets/ + +# Ignore digested assets cache. +/priv/static/cache_manifest.json + +# In case you use Node.js/npm, you want to ignore these. +npm-debug.log +/assets/node_modules/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5dbaca5 --- /dev/null +++ b/README.md @@ -0,0 +1,183 @@ +# Koniphx + +A small playground for myself to "play" around with [Phoenix](http://www.phoenixframework.org/) and [TailwindCSS](https://tailwindcss.com/). + +--- + +### Adding a new page + +1. Create a route for it (`lib/koniphx_web/router.ex`) + +```elixir + scope "/", KoniphxWeb do + pipe_through :browser + + get "/newpage", NewPageController, :index + end +``` + +We also specified a controller, so lets create that. + +2. Create a controller (`lib/koniphx_web/controllers/new_page_controller.ex`) + +```elixir +defmodule KoniphxWeb.NewPageController do + use KoniphxWeb, :controller + + def index(conn, _params) do + render(conn, "newpage.html") + end +end +``` + +3. Create a view (`lib/koniphx_web/views/new_page_view.ex`) + +```elixir +defmodule KoniphxWeb.NewPageView do + use KoniphxWeb, :view +end +``` + +4. Create a template (`lib/koniphx_web/templates/new_page/newpage.html.eex`) + +```html +

Welcome to New Page!

+``` + +Remember, you first have to create a directory for the template to live in :) + +--- + +### Call a custom controller + +If you want to send data to your controller, you can do it like this: + +```html +
+ +
+ + +
+
+ +
+
+``` + +This will send a `POST` request to the route `/route_here` with the data `name` and `_csrf_token` (which is needed for security reasons). + +Make sure to add the `post` method in the router + +```elixir +post "/route_here", ARouteController, :request_function +``` + +and then use the provided `request_function` in the controller + +```elixir +def request_function(conn, params) do + if conn.params["name"] == "something" do + # do something + else + # do something else +end +``` + +You could use `phx-click` in [LiveView](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html) to send / handle data requests (generally easier than the above) because of server-rendered HTML.. + +--- + +#### Designing with TailwindCSS + +If you want to add Tailwind too, follow the steps: + +1.) Add to your mix.exs file + +```elixir +defp deps do + [ + {:tailwind, "~> 0.1", runtime: Mix.env() == :dev}, + ] +end +```` + +2.) Run `mix deps.get` +This will install the new package + +3.) Run `mix tailwind.install` + +4.) Add tailwind to `config/config.exs` + +```elixir +config :tailwind, + version: "3.0.24", + default: [ + args: ~w( + --config=tailwind.config.js + --input=css/app.css + --output=../priv/static/assets/app.css + ), + cd: Path.expand("../assets", __DIR__) + ] +``` + +5.) Add tailwind to the watcher (`config/dev.exs`) + +```elixir + tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} +``` + +6.) Create a `tailwind.config.js` file in the `assets` directory + +```javascript +module.exports = { + mode: "jit", + content: ["./js/**/*.js", "../lib/*_web/**/*.*ex"], + plugins: [require("@tailwindcss/typography")], +}; +``` + +7.) Add the following to your `app.css` file + +```css +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +and delete `phoenix.css` and all the other stuff from app.css + +8.) Start coding! + +```html +
+
+

Welcome, centered text!

+
+
+``` diff --git a/assets/css/app.css b/assets/css/app.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/assets/css/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/assets/js/app.js b/assets/js/app.js new file mode 100644 index 0000000..bf203ba --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1,44 @@ +// We import the CSS which is extracted to its own file by esbuild. +// Remove this line if you add a your own CSS build pipeline (e.g postcss). + +// 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, {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()) +window.addEventListener("phx:page-loading-stop", info => topbar.hide()) + +// connect if there are any LiveViews on the page +liveSocket.connect() + +// expose liveSocket on window for web console debug logs and latency simulation: +// >> liveSocket.enableDebug() +// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session +// >> liveSocket.disableLatencySim() +window.liveSocket = liveSocket + diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js new file mode 100644 index 0000000..17ac6fe --- /dev/null +++ b/assets/tailwind.config.js @@ -0,0 +1,10 @@ +module.exports = { + mode: 'jit', + content: [ + './js/**/*.js', + '../lib/*_web/**/*.*ex' + ], + plugins: [ + require('@tailwindcss/typography') + ], + } \ No newline at end of file diff --git a/assets/vendor/topbar.js b/assets/vendor/topbar.js new file mode 100644 index 0000000..1f62209 --- /dev/null +++ b/assets/vendor/topbar.js @@ -0,0 +1,157 @@ +/** + * @license MIT + * topbar 1.0.0, 2021-01-06 + * 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, + progressTimerId, + fadeTimerId, + currentProgress, + showing, + 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 () { + if (showing) return; + 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 () { + if (!showing) return; + showing = false; + if (progressTimerId != null) { + window.cancelAnimationFrame(progressTimerId); + progressTimerId = null; + } + (function loop() { + if (topbar.progress("+.1") >= 1) { + canvas.style.opacity -= 0.05; + if (canvas.style.opacity <= 0.05) { + canvas.style.display = "none"; + fadeTimerId = null; + return; + } + } + fadeTimerId = window.requestAnimationFrame(loop); + })(); + }, + }; + + if (typeof module === "object" && typeof module.exports === "object") { + module.exports = topbar; + } else if (typeof define === "function" && define.amd) { + define(function () { + return topbar; + }); + } else { + this.topbar = topbar; + } +}.call(this, window, document)); diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..6056e60 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,48 @@ +# 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 + +# Configures the endpoint +config :koniphx, KoniphxWeb.Endpoint, + url: [host: "localhost"], + render_errors: [view: KoniphxWeb.ErrorView, accepts: ~w(html json), layout: false], + pubsub_server: Koniphx.PubSub, + live_view: [signing_salt: "ivk5BqmR"] + +config :tailwind, + version: "3.0.24", + default: [ + args: ~w( + --config=tailwind.config.js + --input=css/app.css + --output=../priv/static/assets/app.css + ), + cd: Path.expand("../assets", __DIR__) + ] + +# Configure esbuild (the version is required) +config :esbuild, + version: "0.13.5", + default: [ + 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__)} + ] + +# Configures Elixir's Logger +config :logger, :console, + format: "$time $metadata[$level] $message\n", + metadata: [:request_id] + +# Use Jason for JSON parsing in Phoenix +config :phoenix, :json_library, Jason + +# Import environment specific config. This must remain at the bottom +# of this file so it overrides the configuration defined above. +import_config "#{config_env()}.exs" diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 0000000..dd608b6 --- /dev/null +++ b/config/dev.exs @@ -0,0 +1,66 @@ +import Config + +# 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 use it +# with esbuild to bundle .js and .css sources. +config :koniphx, KoniphxWeb.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: {127, 0, 0, 1}, port: 4000], + check_origin: false, + code_reloader: true, + debug_errors: true, + secret_key_base: "jxEh1mmolMn74WhIIKKuFqXIRkm9x4dSPxwVLcEl8UHl1+IZn6ylWvEjTaJYPhhP", + watchers: [ + # Start the esbuild watcher by calling Esbuild.install_and_run(:default, args) + esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}, + tailwind: {Tailwind, :install_and_run, [:default, ~w(--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 +# +# Note that this task requires Erlang/OTP 20 or later. +# 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 :koniphx, KoniphxWeb.Endpoint, + live_reload: [ + patterns: [ + ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", + ~r"priv/gettext/.*(po)$", + ~r"lib/koniphx_web/(live|views)/.*(ex)$", + ~r"lib/koniphx_web/templates/.*(eex)$" + ] + ] + +# 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 diff --git a/config/prod.exs b/config/prod.exs new file mode 100644 index 0000000..04db1dd --- /dev/null +++ b/config/prod.exs @@ -0,0 +1,49 @@ +import Config + +# For production, don't forget to configure the url host +# to something meaningful, Phoenix uses this information +# when generating URLs. +# +# Note we also include the path to a cache manifest +# containing the digested version of static files. This +# manifest is generated by the `mix phx.digest` task, +# which you should run after static files are built and +# before starting your production server. +config :koniphx, KoniphxWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" + +# Do not print debug messages in production +config :logger, level: :info + +# ## SSL Support +# +# To get SSL working, you will need to add the `https` key +# to the previous section and set your `:url` port to 443: +# +# config :koniphx, KoniphxWeb.Endpoint, +# ..., +# url: [host: "example.com", port: 443], +# 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 endpoint, ensuring +# no data is ever sent via http, always redirecting to https: +# +# config :koniphx, KoniphxWeb.Endpoint, +# force_ssl: [hsts: true] +# +# Check `Plug.SSL` for all available options in `force_ssl`. diff --git a/config/runtime.exs b/config/runtime.exs new file mode 100644 index 0000000..9386da9 --- /dev/null +++ b/config/runtime.exs @@ -0,0 +1,52 @@ +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. + +# Start the phoenix server if environment is set and running in a release +if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do + config :koniphx, KoniphxWeb.Endpoint, server: true +end + +if config_env() == :prod do + # 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 :koniphx, KoniphxWeb.Endpoint, + url: [host: host, port: 443], + 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/plug_cowboy/Plug.Cowboy.html + # 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 + + # ## Using releases + # + # If you are doing OTP releases, you need to instruct Phoenix + # to start each relevant endpoint: + # + # config :koniphx, KoniphxWeb.Endpoint, server: true + # + # Then you can assemble a release by calling `mix release`. + # See `mix help release` for more information. +end diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 0000000..e50c5af --- /dev/null +++ b/config/test.exs @@ -0,0 +1,14 @@ +import Config + +# We don't run a server during test. If one is required, +# you can enable the server option below. +config :koniphx, KoniphxWeb.Endpoint, + http: [ip: {127, 0, 0, 1}, port: 4002], + secret_key_base: "XiKJQvWlgR7pJed+76+gqcjvw8J3YW4UxCORGmC0FO9T6jb7OrLOBoBdoakR8P/5", + server: false + +# Print only warnings and errors during test +config :logger, level: :warn + +# Initialize plugs at runtime for faster test compilation +config :phoenix, :plug_init_mode, :runtime diff --git a/lib/koniphx.ex b/lib/koniphx.ex new file mode 100644 index 0000000..d1ad04e --- /dev/null +++ b/lib/koniphx.ex @@ -0,0 +1,9 @@ +defmodule Koniphx do + @moduledoc """ + Koniphx keeps the contexts that define your domain + and business logic. + + Contexts are also responsible for managing your data, regardless + if it comes from the database, an external API or others. + """ +end diff --git a/lib/koniphx/application.ex b/lib/koniphx/application.ex new file mode 100644 index 0000000..09fa7b1 --- /dev/null +++ b/lib/koniphx/application.ex @@ -0,0 +1,34 @@ +defmodule Koniphx.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 = [ + # Start the Telemetry supervisor + KoniphxWeb.Telemetry, + # Start the PubSub system + {Phoenix.PubSub, name: Koniphx.PubSub}, + # Start the Endpoint (http/https) + KoniphxWeb.Endpoint + # Start a worker by calling: Koniphx.Worker.start_link(arg) + # {Koniphx.Worker, arg} + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: Koniphx.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 + KoniphxWeb.Endpoint.config_change(changed, removed) + :ok + end +end diff --git a/lib/koniphx_web.ex b/lib/koniphx_web.ex new file mode 100644 index 0000000..fc30318 --- /dev/null +++ b/lib/koniphx_web.ex @@ -0,0 +1,110 @@ +defmodule KoniphxWeb do + @moduledoc """ + The entrypoint for defining your web interface, such + as controllers, views, channels and so on. + + This can be used in your application as: + + use KoniphxWeb, :controller + use KoniphxWeb, :view + + The definitions below will be executed for every view, + controller, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. Instead, define any helper function in modules + and import those modules here. + """ + + def controller do + quote do + use Phoenix.Controller, namespace: KoniphxWeb + + import Plug.Conn + import KoniphxWeb.Gettext + alias KoniphxWeb.Router.Helpers, as: Routes + end + end + + def view do + quote do + use Phoenix.View, + root: "lib/koniphx_web/templates", + namespace: KoniphxWeb + + # Import convenience functions from controllers + import Phoenix.Controller, + only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] + + # Include shared imports and aliases for views + unquote(view_helpers()) + end + end + + def live_view do + quote do + use Phoenix.LiveView, + layout: {KoniphxWeb.LayoutView, "live.html"} + + unquote(view_helpers()) + end + end + + def live_component do + quote do + use Phoenix.LiveComponent + + unquote(view_helpers()) + end + end + + def component do + quote do + use Phoenix.Component + + unquote(view_helpers()) + end + end + + def router do + quote do + use Phoenix.Router + + import Plug.Conn + import Phoenix.Controller + import Phoenix.LiveView.Router + end + end + + def channel do + quote do + use Phoenix.Channel + import KoniphxWeb.Gettext + end + end + + defp view_helpers do + quote do + # Use all HTML functionality (forms, tags, etc) + use Phoenix.HTML + + # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc) + import Phoenix.LiveView.Helpers + + # Import basic rendering functionality (render, render_layout, etc) + import Phoenix.View + + import KoniphxWeb.ErrorHelpers + import KoniphxWeb.Gettext + alias KoniphxWeb.Router.Helpers, as: Routes + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end diff --git a/lib/koniphx_web/controllers/dashboard_controller.ex b/lib/koniphx_web/controllers/dashboard_controller.ex new file mode 100644 index 0000000..7b82685 --- /dev/null +++ b/lib/koniphx_web/controllers/dashboard_controller.ex @@ -0,0 +1,11 @@ +defmodule KoniphxWeb.DashboardController do + use KoniphxWeb, :controller + + def index(conn, _params) do + render(conn, "dashboard.html") + end + + def create(conn, %{"name" => name}) do + render(conn, "dashboard.html", name: name) + end +end diff --git a/lib/koniphx_web/controllers/page_controller.ex b/lib/koniphx_web/controllers/page_controller.ex new file mode 100644 index 0000000..b1d2e71 --- /dev/null +++ b/lib/koniphx_web/controllers/page_controller.ex @@ -0,0 +1,7 @@ +defmodule KoniphxWeb.PageController do + use KoniphxWeb, :controller + + def index(conn, _params) do + render(conn, "index.html") + end +end diff --git a/lib/koniphx_web/endpoint.ex b/lib/koniphx_web/endpoint.ex new file mode 100644 index 0000000..768df4d --- /dev/null +++ b/lib/koniphx_web/endpoint.ex @@ -0,0 +1,49 @@ +defmodule KoniphxWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :koniphx + + # The session will be stored in the cookie and signed, + # this means its contents can be read but not tampered with. + # Set :encryption_salt if you would also like to encrypt it. + @session_options [ + store: :cookie, + key: "_koniphx_key", + signing_salt: "BCUFfJaY" + ] + + socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] + + # Serve at "/" the static files from "priv/static" directory. + # + # You should set gzip to true if you are running phx.digest + # when deploying your static files in production. + plug Plug.Static, + at: "/", + from: :koniphx, + gzip: false, + only: ~w(assets fonts images favicon.ico robots.txt) + + # Code reloading can be explicitly enabled under the + # :code_reloader configuration of your endpoint. + if code_reloading? do + socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket + plug Phoenix.LiveReloader + plug Phoenix.CodeReloader + end + + plug Phoenix.LiveDashboard.RequestLogger, + param_key: "request_logger", + cookie_key: "request_logger" + + plug Plug.RequestId + plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] + + plug Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Phoenix.json_library() + + plug Plug.MethodOverride + plug Plug.Head + plug Plug.Session, @session_options + plug KoniphxWeb.Router +end diff --git a/lib/koniphx_web/gettext.ex b/lib/koniphx_web/gettext.ex new file mode 100644 index 0000000..69ee5a9 --- /dev/null +++ b/lib/koniphx_web/gettext.ex @@ -0,0 +1,24 @@ +defmodule KoniphxWeb.Gettext do + @moduledoc """ + A module providing Internationalization with a gettext-based API. + + By using [Gettext](https://hexdocs.pm/gettext), + your module gains a set of macros for translations, for example: + + import KoniphxWeb.Gettext + + # Simple translation + gettext("Here is the string to translate") + + # Plural translation + ngettext("Here is the string to translate", + "Here are the strings to translate", + 3) + + # Domain-based translation + dgettext("errors", "Here is the error message to translate") + + See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. + """ + use Gettext, otp_app: :koniphx +end diff --git a/lib/koniphx_web/router.ex b/lib/koniphx_web/router.ex new file mode 100644 index 0000000..959a5e8 --- /dev/null +++ b/lib/koniphx_web/router.ex @@ -0,0 +1,39 @@ +defmodule KoniphxWeb.Router do + use KoniphxWeb, :router + + pipeline :browser do + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_live_flash + plug :put_root_layout, {KoniphxWeb.LayoutView, :root} + plug :protect_from_forgery + plug :put_secure_browser_headers + end + + pipeline :api do + plug :accepts, ["json"] + end + + scope "/", KoniphxWeb do + pipe_through :browser + + get "/", PageController, :index + + # dashboard with custom name + get "/dashboard", DashboardController, :index + post "/dashboard", DashboardController, :create + end + + # Other scopes may use custom stacks. + # scope "/api", KoniphxWeb do + # pipe_through :api + # end + + # Enables LiveDashboard only for development + # + # If you want to use the LiveDashboard in production, you should put + # it behind authentication and allow only admins to access it. + # If your application does not have an admins-only section yet, + # you can use Plug.BasicAuth to set up some basic authentication + # as long as you are also using SSL (which you should anyway). +end diff --git a/lib/koniphx_web/telemetry.ex b/lib/koniphx_web/telemetry.ex new file mode 100644 index 0000000..7597f05 --- /dev/null +++ b/lib/koniphx_web/telemetry.ex @@ -0,0 +1,48 @@ +defmodule KoniphxWeb.Telemetry do + use Supervisor + import Telemetry.Metrics + + def start_link(arg) do + Supervisor.start_link(__MODULE__, arg, name: __MODULE__) + end + + @impl true + def init(_arg) do + children = [ + # Telemetry poller will execute the given period measurements + # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics + {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} + # Add reporters as children of your supervision tree. + # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} + ] + + Supervisor.init(children, strategy: :one_for_one) + end + + def metrics do + [ + # Phoenix Metrics + summary("phoenix.endpoint.stop.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.stop.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + + # VM Metrics + summary("vm.memory.total", unit: {:byte, :kilobyte}), + summary("vm.total_run_queue_lengths.total"), + summary("vm.total_run_queue_lengths.cpu"), + summary("vm.total_run_queue_lengths.io") + ] + end + + defp periodic_measurements do + [ + # A module, function and arguments to be invoked periodically. + # This function must call :telemetry.execute/3 and a metric must be added above. + # {KoniphxWeb, :count_users, []} + ] + end +end diff --git a/lib/koniphx_web/templates/dashboard/dashboard.html.heex b/lib/koniphx_web/templates/dashboard/dashboard.html.heex new file mode 100644 index 0000000..d9e1e87 --- /dev/null +++ b/lib/koniphx_web/templates/dashboard/dashboard.html.heex @@ -0,0 +1,6 @@ +
+
+ +

Welcome, <%= @name %>

+
+
diff --git a/lib/koniphx_web/templates/layout/app.html.heex b/lib/koniphx_web/templates/layout/app.html.heex new file mode 100644 index 0000000..169aed9 --- /dev/null +++ b/lib/koniphx_web/templates/layout/app.html.heex @@ -0,0 +1,5 @@ +
+ + + <%= @inner_content %> +
diff --git a/lib/koniphx_web/templates/layout/live.html.heex b/lib/koniphx_web/templates/layout/live.html.heex new file mode 100644 index 0000000..a29d604 --- /dev/null +++ b/lib/koniphx_web/templates/layout/live.html.heex @@ -0,0 +1,11 @@ +
+ + + + + <%= @inner_content %> +
diff --git a/lib/koniphx_web/templates/layout/root.html.heex b/lib/koniphx_web/templates/layout/root.html.heex new file mode 100644 index 0000000..d4a9e7e --- /dev/null +++ b/lib/koniphx_web/templates/layout/root.html.heex @@ -0,0 +1,15 @@ + + + + + + + <%= csrf_meta_tag() %> + <%= live_title_tag assigns[:page_title] || "Koniphx" %> + + + + + <%= @inner_content %> + + diff --git a/lib/koniphx_web/templates/page/index.html.heex b/lib/koniphx_web/templates/page/index.html.heex new file mode 100644 index 0000000..0591faa --- /dev/null +++ b/lib/koniphx_web/templates/page/index.html.heex @@ -0,0 +1,18 @@ + + +
+ +
+ + +
+
+ +
+
\ No newline at end of file diff --git a/lib/koniphx_web/views/dashboard_view.ex b/lib/koniphx_web/views/dashboard_view.ex new file mode 100644 index 0000000..e0316c4 --- /dev/null +++ b/lib/koniphx_web/views/dashboard_view.ex @@ -0,0 +1,3 @@ +defmodule KoniphxWeb.DashboardView do + use KoniphxWeb, :view +end diff --git a/lib/koniphx_web/views/error_helpers.ex b/lib/koniphx_web/views/error_helpers.ex new file mode 100644 index 0000000..54687fc --- /dev/null +++ b/lib/koniphx_web/views/error_helpers.ex @@ -0,0 +1,47 @@ +defmodule KoniphxWeb.ErrorHelpers do + @moduledoc """ + Conveniences for translating and building error messages. + """ + + use Phoenix.HTML + + @doc """ + Generates tag for inlined form input errors. + """ + def error_tag(form, field) do + Enum.map(Keyword.get_values(form.errors, field), fn error -> + content_tag(:span, translate_error(error), + class: "invalid-feedback", + phx_feedback_for: input_name(form, field) + ) + end) + 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 "is invalid" in the "errors" domain + # dgettext("errors", "is invalid") + # + # # Translate the number of files with plural rules + # dngettext("errors", "1 file", "%{count} files", count) + # + # Because the error messages we show in our forms and APIs + # are defined inside Ecto, we need to translate them dynamically. + # This requires us to call the Gettext module passing our gettext + # backend as first argument. + # + # Note we use the "errors" domain, which means translations + # should be written to the errors.po file. The :count option is + # set by Ecto and indicates we should also apply plural rules. + if count = opts[:count] do + Gettext.dngettext(KoniphxWeb.Gettext, "errors", msg, msg, count, opts) + else + Gettext.dgettext(KoniphxWeb.Gettext, "errors", msg, opts) + end + end +end diff --git a/lib/koniphx_web/views/error_view.ex b/lib/koniphx_web/views/error_view.ex new file mode 100644 index 0000000..84284ea --- /dev/null +++ b/lib/koniphx_web/views/error_view.ex @@ -0,0 +1,16 @@ +defmodule KoniphxWeb.ErrorView do + use KoniphxWeb, :view + + # If you want to customize a particular status code + # for a certain format, you may uncomment below. + # def render("500.html", _assigns) do + # "Internal Server Error" + # end + + # By default, Phoenix returns the status message from + # the template name. For example, "404.html" becomes + # "Not Found". + def template_not_found(template, _assigns) do + Phoenix.Controller.status_message_from_template(template) + end +end diff --git a/lib/koniphx_web/views/layout_view.ex b/lib/koniphx_web/views/layout_view.ex new file mode 100644 index 0000000..3530823 --- /dev/null +++ b/lib/koniphx_web/views/layout_view.ex @@ -0,0 +1,7 @@ +defmodule KoniphxWeb.LayoutView do + use KoniphxWeb, :view + + # Phoenix LiveDashboard is available only in development by default, + # so we instruct Elixir to not warn if the dashboard route is missing. + @compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}} +end diff --git a/lib/koniphx_web/views/page_view.ex b/lib/koniphx_web/views/page_view.ex new file mode 100644 index 0000000..f8df8ea --- /dev/null +++ b/lib/koniphx_web/views/page_view.ex @@ -0,0 +1,3 @@ +defmodule KoniphxWeb.PageView do + use KoniphxWeb, :view +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..185c250 --- /dev/null +++ b/mix.exs @@ -0,0 +1,64 @@ +defmodule Koniphx.MixProject do + use Mix.Project + + def project do + [ + app: :koniphx, + version: "0.1.0", + elixir: "~> 1.12", + elixirc_paths: elixirc_paths(Mix.env()), + compilers: Mix.compilers(), + start_permanent: Mix.env() == :prod, + aliases: aliases(), + deps: deps() + ] + end + + # Configuration for the OTP application. + # + # Type `mix help compile.app` for more information. + def application do + [ + mod: {Koniphx.Application, []}, + extra_applications: [:logger, :runtime_tools] + ] + end + + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + # Specifies your project dependencies. + # + # Type `mix help deps` for examples and options. + defp deps do + [ + {:phoenix, "~> 1.6.3"}, + {:phoenix_html, "~> 3.0"}, + {:phoenix_live_reload, "~> 1.2", only: :dev}, + {:phoenix_live_view, "~> 0.17.5"}, + {:floki, ">= 0.30.0", only: :test}, + {:phoenix_live_dashboard, "~> 0.6"}, + {:esbuild, "~> 0.3", runtime: Mix.env() == :dev}, + {:telemetry_metrics, "~> 0.6"}, + {:telemetry_poller, "~> 1.0"}, + {:gettext, "~> 0.18"}, + {:jason, "~> 1.2"}, + {:plug_cowboy, "~> 2.5"}, + {:tailwind, "~> 0.1", runtime: Mix.env() == :dev}, + ] + end + + # Aliases are shortcuts or tasks specific to the current project. + # For example, to install project dependencies and perform other setup tasks, run: + # + # $ mix setup + # + # See the documentation for `Mix` for more info on aliases. + defp aliases do + [ + setup: ["deps.get"], + "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"] + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..126f316 --- /dev/null +++ b/mix.lock @@ -0,0 +1,28 @@ +%{ + "castore": {:hex, :castore, "0.1.20", "62a0126cbb7cb3e259257827b9190f88316eb7aa3fdac01fd6f2dfd64e7f46e9", [:mix], [], "hexpm", "a020b7650529c986c454a4035b6b13a328e288466986307bea3aadb4c95ac98a"}, + "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, + "esbuild": {:hex, :esbuild, "0.6.0", "9ba6ead054abd43cb3d7b14946a0cdd1493698ccd8e054e0e5d6286d7f0f509c", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "30f9a05d4a5bab0d3e37398f312f80864e1ee1a081ca09149d06d474318fd040"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"}, + "gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"}, + "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, + "phoenix": {:hex, :phoenix, "1.6.15", "0a1d96bbc10747fd83525370d691953cdb6f3ccbac61aa01b4acb012474b047d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d70ab9fbf6b394755ea88b644d34d79d8b146e490973151f248cacd122d20672"}, + "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.5", "1495bb014be12c9a9252eca04b9af54246f6b5c1e4cd1f30210cd00ec540cf8e", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.7", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "ef4fa50dd78364409039c99cf6f98ab5209b4c5f8796c17f4db118324f0db852"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.12", "74f4c0ad02d7deac2d04f50b52827a5efdc5c6e7fac5cede145f5f0e4183aedc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "af6dd5e0aac16ff43571f527a8e0616d62cb80b10eb87aac82170243e50d99c8"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"}, + "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, + "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "tailwind": {:hex, :tailwind, "0.1.9", "25ba09d42f7bfabe170eb67683a76d6ec2061952dc9bd263a52a99ba3d24bd4d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "9213f87709c458aaec313bb5f2df2b4d2cedc2b630e4ae821bf3c54c47a56d0b"}, + "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, +} diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po new file mode 100644 index 0000000..cdec3a1 --- /dev/null +++ b/priv/gettext/en/LC_MESSAGES/errors.po @@ -0,0 +1,11 @@ +## `msgid`s in this file come from POT (.pot) files. +## +## Do not add, change, or remove `msgid`s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use `mix gettext.extract --merge` or `mix gettext.merge` +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: en\n" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot new file mode 100644 index 0000000..d6f47fa --- /dev/null +++ b/priv/gettext/errors.pot @@ -0,0 +1,10 @@ +## This is a PO Template file. +## +## `msgid`s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run `mix gettext.extract` to bring this file up to +## date. Leave `msgstr`s empty as changing them here has no +## effect: edit them in PO (`.po`) files instead. + diff --git a/priv/static/robots.txt b/priv/static/robots.txt new file mode 100644 index 0000000..26e06b5 --- /dev/null +++ b/priv/static/robots.txt @@ -0,0 +1,5 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/test/koniphx_web/controllers/page_controller_test.exs b/test/koniphx_web/controllers/page_controller_test.exs new file mode 100644 index 0000000..30264a9 --- /dev/null +++ b/test/koniphx_web/controllers/page_controller_test.exs @@ -0,0 +1,8 @@ +defmodule KoniphxWeb.PageControllerTest do + use KoniphxWeb.ConnCase + + test "GET /", %{conn: conn} do + conn = get(conn, "/") + assert html_response(conn, 200) =~ "Welcome to Phoenix!" + end +end diff --git a/test/koniphx_web/views/error_view_test.exs b/test/koniphx_web/views/error_view_test.exs new file mode 100644 index 0000000..2642030 --- /dev/null +++ b/test/koniphx_web/views/error_view_test.exs @@ -0,0 +1,14 @@ +defmodule KoniphxWeb.ErrorViewTest do + use KoniphxWeb.ConnCase, async: true + + # Bring render/3 and render_to_string/3 for testing custom views + import Phoenix.View + + test "renders 404.html" do + assert render_to_string(KoniphxWeb.ErrorView, "404.html", []) == "Not Found" + end + + test "renders 500.html" do + assert render_to_string(KoniphxWeb.ErrorView, "500.html", []) == "Internal Server Error" + end +end diff --git a/test/koniphx_web/views/layout_view_test.exs b/test/koniphx_web/views/layout_view_test.exs new file mode 100644 index 0000000..32710fd --- /dev/null +++ b/test/koniphx_web/views/layout_view_test.exs @@ -0,0 +1,8 @@ +defmodule KoniphxWeb.LayoutViewTest do + use KoniphxWeb.ConnCase, async: true + + # When testing helpers, you may want to import Phoenix.HTML and + # use functions such as safe_to_string() to convert the helper + # result into an HTML string. + # import Phoenix.HTML +end diff --git a/test/koniphx_web/views/page_view_test.exs b/test/koniphx_web/views/page_view_test.exs new file mode 100644 index 0000000..9b30495 --- /dev/null +++ b/test/koniphx_web/views/page_view_test.exs @@ -0,0 +1,3 @@ +defmodule KoniphxWeb.PageViewTest do + use KoniphxWeb.ConnCase, async: true +end diff --git a/test/support/channel_case.ex b/test/support/channel_case.ex new file mode 100644 index 0000000..07e853c --- /dev/null +++ b/test/support/channel_case.ex @@ -0,0 +1,34 @@ +defmodule KoniphxWeb.ChannelCase do + @moduledoc """ + This module defines the test case to be used by + channel tests. + + Such tests rely on `Phoenix.ChannelTest` and also + import other functionality to make it easier + to build common data structures and query the data layer. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `use KoniphxWeb.ChannelCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + using do + quote do + # Import conveniences for testing with channels + import Phoenix.ChannelTest + import KoniphxWeb.ChannelCase + + # The default endpoint for testing + @endpoint KoniphxWeb.Endpoint + end + end + + setup _tags do + :ok + end +end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex new file mode 100644 index 0000000..7aeb36b --- /dev/null +++ b/test/support/conn_case.ex @@ -0,0 +1,37 @@ +defmodule KoniphxWeb.ConnCase do + @moduledoc """ + This module defines the test case to be used by + tests that require setting up a connection. + + Such tests rely on `Phoenix.ConnTest` and also + import other functionality to make it easier + to build common data structures and query the data layer. + + Finally, if the test case interacts with the database, + we enable the SQL sandbox, so changes done to the database + are reverted at the end of every test. If you are using + PostgreSQL, you can even run database tests asynchronously + by setting `use KoniphxWeb.ConnCase, async: true`, although + this option is not recommended for other databases. + """ + + use ExUnit.CaseTemplate + + using do + quote do + # Import conveniences for testing with connections + import Plug.Conn + import Phoenix.ConnTest + import KoniphxWeb.ConnCase + + alias KoniphxWeb.Router.Helpers, as: Routes + + # The default endpoint for testing + @endpoint KoniphxWeb.Endpoint + end + end + + setup _tags do + {:ok, conn: Phoenix.ConnTest.build_conn()} + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()