From 24562d22b7e23dbf51297b221dd5a3799b4119e6 Mon Sep 17 00:00:00 2001 From: Tiago Moraes Date: Thu, 8 Feb 2024 11:28:10 -0300 Subject: [PATCH] Update all the things! (#74) * update actions/checkout to v4 * elixir 1.14.5 and erlang 25.3.2.8 * fix `Passing an atom to "for" in the form component is deprecated.` warnings * remove deprecated phoenix compiler https://github.com/phoenixframework/phoenix/blob/66e9aaa84aa4ba1c5c7713d039ed7fba937293e8/CHANGELOG.md * remove unused and unneeded deps * align with phx_new 1.6.16 surface.init 0.9.4 * align with phx_new 1.7.7 surface.init 0.11.1 --- .formatter.exs | 7 +- .github/workflows/ci.yml | 27 +- .gitignore | 35 +- .tool-versions | 2 + README.md | 5 +- assets/css/app.css | 163 ++--- assets/js/app.js | 39 +- assets/package.json | 5 - assets/tailwind.config.js | 71 ++ assets/vendor/topbar.js | 44 +- config/config.exs | 62 +- config/dev.exs | 34 +- config/prod.exs | 66 +- config/runtime.exs | 82 +++ config/test.exs | 11 +- lib/surface_site/application.ex | 5 +- lib/surface_site_web.ex | 103 ++- .../components/core_components.ex | 635 ++++++++++++++++++ lib/surface_site_web/components/layouts.ex | 13 + .../components/layouts/app.sface | 4 + .../components/layouts/root.sface | 66 ++ .../controllers/blog_controller.ex | 7 +- lib/surface_site_web/controllers/blog_html.ex | 63 ++ .../controllers/blog_html/index.sface | 57 ++ .../controllers/blog_html/show.sface | 56 ++ .../controllers/error_html.ex | 19 + .../error_json.ex} | 11 +- lib/surface_site_web/endpoint.ex | 24 +- .../live/builtin_components/field_info.ex | 2 +- .../live/builtin_components/form_info.ex | 2 +- lib/surface_site_web/live/contexts.ex | 2 +- .../live/events/dialog_example.ex | 12 +- lib/surface_site_web/live/sidebar_menu.ex | 7 +- lib/surface_site_web/live/slots.ex | 2 +- .../live/slots/typed_slots_example.ex | 4 +- lib/surface_site_web/router.ex | 2 +- lib/surface_site_web/views/blog_view.ex | 175 ----- lib/surface_site_web/views/layout_view.ex | 92 --- mix.exs | 33 +- mix.lock | 45 +- phoenix_static_buildpack.config | 1 - priv/gettext/en/LC_MESSAGES/errors.po | 97 --- priv/gettext/errors.pot | 95 --- priv/static/robots.txt | 2 +- test/support/conn_case.ex | 16 +- .../controllers/blog_controller_test.exs | 28 + .../controllers/error_html_test.exs | 15 + .../controllers/error_json_test.exs | 12 + test/surface_site_web/live/app_test.exs | 19 +- .../views/error_view_test.exs | 15 - 50 files changed, 1560 insertions(+), 834 deletions(-) create mode 100644 .tool-versions delete mode 100644 assets/package.json create mode 100644 assets/tailwind.config.js create mode 100644 config/runtime.exs create mode 100644 lib/surface_site_web/components/core_components.ex create mode 100644 lib/surface_site_web/components/layouts.ex create mode 100644 lib/surface_site_web/components/layouts/app.sface create mode 100644 lib/surface_site_web/components/layouts/root.sface create mode 100644 lib/surface_site_web/controllers/blog_html.ex create mode 100644 lib/surface_site_web/controllers/blog_html/index.sface create mode 100644 lib/surface_site_web/controllers/blog_html/show.sface create mode 100644 lib/surface_site_web/controllers/error_html.ex rename lib/surface_site_web/{views/error_view.ex => controllers/error_json.ex} (59%) delete mode 100644 lib/surface_site_web/views/blog_view.ex delete mode 100644 lib/surface_site_web/views/layout_view.ex delete mode 100644 phoenix_static_buildpack.config delete mode 100644 priv/gettext/en/LC_MESSAGES/errors.po delete mode 100644 priv/gettext/errors.pot create mode 100644 test/surface_site_web/controllers/blog_controller_test.exs create mode 100644 test/surface_site_web/controllers/error_html_test.exs create mode 100644 test/surface_site_web/controllers/error_json_test.exs delete mode 100644 test/surface_site_web/views/error_view_test.exs diff --git a/.formatter.exs b/.formatter.exs index d165150..ff8ae9c 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,6 +1,5 @@ [ - import_deps: [:phoenix, :surface, :ecto], - inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], - surface_inputs: ["{lib,test}/**/*.{ex,exs,sface}"], - subdirectories: [] + import_deps: [:phoenix, :surface], + plugins: [Phoenix.LiveView.HTMLFormatter, Surface.Formatter.Plugin], + inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "{lib,test}/**/*.sface"] ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac8221a..822e2c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,31 +14,32 @@ jobs: fail-fast: false matrix: include: - - elixir: 1.12.0 - otp: 24.0 - - elixir: 1.13.4 - otp: 24.0 - - elixir: 1.13.4 - otp: 25.0 - check_formatted: true - warnings_as_errors: true - - elixir: 1.14.0 - otp: 25.0 + - elixir: '1.14.5' + otp: '25.3.2.8' env: MIX_ENV: test steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} + - name: Deps and _build cache + uses: actions/cache@v4 + id: deps-cache + with: + path: | + deps + _build + key: ${{ runner.os }}-${{ runner.arch }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }} - name: Install Dependencies + if: steps.deps-cache.outputs.cache-hit != 'true' run: | mix local.hex --force mix local.rebar --force mix deps.get --only test + - run: mix deps.compile + if: steps.deps-cache.outputs.cache-hit != 'true' - run: mix compile --warnings-as-errors - if: matrix.warnings_as_errors - run: mix format --check-formatted - if: matrix.check_formatted - run: mix test diff --git a/.gitignore b/.gitignore index 5745e81..751c063 100644 --- a/.gitignore +++ b/.gitignore @@ -19,31 +19,24 @@ 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"). surface_site-*.tar -# If NPM crashes, it generates a log, let's ignore it too. -npm-debug.log +# Ignore assets that are produced by build tools. +/priv/static/assets/ -# The directory NPM downloads your dependencies sources to. -/assets/node_modules/ +# Ignore digested assets cache. +/priv/static/cache_manifest.json -# Since we are building assets from assets/, -# we ignore priv/static. You may want to comment -# this depending on your deployment strategy. -# /priv/static/ +# In case you use Node.js/npm, you want to ignore these. +npm-debug.log +/assets/node_modules/ -# Ignore assets that are produced by build tools. -/priv/static/assets/ +# Ignore generated js hook files for components +assets/js/_hooks/ -# Files matching config/*.secret.exs pattern contain sensitive -# data and you should not commit them into version control. -# -# Alternatively, you may comment the line below and commit the -# secrets files as long as you replace their contents by environment -# variables. -/config/*.secret.exs - -# Ignore auto-generated hook files -/assets/js/_hooks/ -/assets/css/_components.css +# Ignore generated CSS file for components +assets/css/_components.css diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..bde48fc --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +erlang 25.3.2.8 +elixir 1.14.5-otp-25 diff --git a/README.md b/README.md index ae94252..e0b9952 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # Surface Site + Documentation website for the [Surface](https://surface-ui.org/) project. ## Development To start your Phoenix server: - * Run `mix setup` - * Start Phoenix endpoint with `mix phx.server` +* Run `mix setup` +* Start Phoenix endpoint with `mix phx.server` Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. diff --git a/assets/css/app.css b/assets/css/app.css index 23b4421..cf52003 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -1,3 +1,13 @@ +/* Import scoped CSS rules for components +@import "./_components.css"; + +TODO: Tailwind +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; + +This file is for your main application CSS */ + body.mobile-sidebar-open { overflow: hidden; position: fixed; @@ -30,6 +40,7 @@ code { margin-top: .2em; margin-bottom: .3em; } + .menu-list li a { padding: .4em .75em; } @@ -40,7 +51,7 @@ code { position: absolute; right: 0; top: 0; - background: hsla(0,0%,4%,.86); + background: hsla(0, 0%, 4%, .86); position: fixed; z-index: 40; } @@ -64,7 +75,7 @@ code { .mobile-sidebar .sidebar-content { background-color: #f5f5f5; - box-shadow: 5px 0 13px 3px hsla(0,0%,4%,.1); + box-shadow: 5px 0 13px 3px hsla(0, 0%, 4%, .1); width: 260px; z-index: 41; } @@ -121,7 +132,7 @@ code { .dark.card { border-radius: 10px; - box-shadow: 0 2px 3px rgba(10,10,10,.1), 0 0 0 1px rgba(10,10,10,.1); + box-shadow: 0 2px 3px rgba(10, 10, 10, .1), 0 0 0 1px rgba(10, 10, 10, .1); } .dark.card > .card-content { @@ -203,7 +214,7 @@ code { color: #7a7a7a; } -#ComponentInfo_Markdown div.Example pre > code{ +#ComponentInfo_Markdown div.Example pre > code { line-height: 1.6; } @@ -239,11 +250,12 @@ code { /* SurfaceSiteWeb.Components.ComponentAPI */ -.ComponentAPI table{ +.ComponentAPI table { font-size: .9rem; } -.ComponentAPI .table td, .table th { +.ComponentAPI .table td, +.table th { line-height: 1.5; } @@ -256,7 +268,7 @@ code { border-top-right-radius: 5px; border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; - color: rgba(0,0,0,.7); + color: rgba(0, 0, 0, .7); margin: 2.25rem 0; } @@ -416,31 +428,29 @@ pre > code[class*="language-"] .token.attr-value .token.punctuation.string { .markdown pre.line-numbers, pre[class*="language-"].line-numbers { - position: relative; - padding-left: 3.8em; - counter-reset: linenumber; + position: relative; + padding-left: 3.8em; + counter-reset: linenumber; } pre[class*="language-"].line-numbers > code { - position: relative; - white-space: inherit; + position: relative; + white-space: inherit; } .line-numbers .line-numbers-rows { - position: absolute; - pointer-events: none; - top: 0; - font-size: 100%; - left: -3.8em; - width: 3em; /* works for line-numbers below 1000 lines */ - letter-spacing: -1px; - border-right: 1px solid #999; - - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - + position: absolute; + pointer-events: none; + top: 0; + font-size: 100%; + left: -3.8em; + width: 3em; /* works for line-numbers below 1000 lines */ + letter-spacing: -1px; + border-right: 1px solid #999; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .line-numbers-rows > span { @@ -459,76 +469,75 @@ pre[class*="language-"].line-numbers > code { /* Prism's line-highlight plugin */ pre[data-line] { - position: relative; - padding: 1em 0 1em 3em; + position: relative; + padding: 1em 0 1em 3em; } .line-highlight { - position: absolute; - left: 0; - right: 0; - padding: inherit 0; + position: absolute; + left: 0; + right: 0; + padding: inherit 0; /* Same as .prism’s padding-top */ - /* margin-top: 1em; */ - margin-top: 0.85em; - - background: hsla(24, 20%, 50%,.08); - background: linear-gradient(to right, hsla(24, 20%, 50%,.1) 70%, hsla(24, 20%, 50%,0)); - - pointer-events: none; - - line-height: inherit; - white-space: pre; + /* margin-top: 1em; */ + margin-top: 0.85em; + background: hsla(24, 20%, 50%, .08); + background: linear-gradient(to right, hsla(24, 20%, 50%, .1) 70%, hsla(24, 20%, 50%, 0)); + pointer-events: none; + line-height: inherit; + white-space: pre; } @media print { - .line-highlight { - /* + .line-highlight { + /* * This will prevent browsers from replacing the background color with white. * It's necessary because the element is layered on top of the displayed code. */ - -webkit-print-color-adjust: exact; - color-adjust: exact; - } -} - - .line-highlight:before, - .line-highlight[data-end]:after { - content: attr(data-start); - position: absolute; - top: .4em; - left: .6em; - min-width: 1em; - padding: 0 .5em; - background-color: hsla(24, 20%, 50%,.4); - color: hsl(24, 20%, 95%); - font: bold 65%/1.5 sans-serif; - text-align: center; - vertical-align: .3em; - border-radius: 999px; - text-shadow: none; - box-shadow: 0 1px white; - } - - .line-highlight[data-end]:after { - content: attr(data-end); - top: auto; - bottom: .4em; - } + -webkit-print-color-adjust: exact; + color-adjust: exact; + } +} + +.line-highlight:before, +.line-highlight[data-end]:after { + content: attr(data-start); + position: absolute; + top: .4em; + left: .6em; + min-width: 1em; + padding: 0 .5em; + background-color: hsla(24, 20%, 50%, .4); + color: hsl(24, 20%, 95%); + font: bold 65%/1.5 sans-serif; + text-align: center; + vertical-align: .3em; + border-radius: 999px; + text-shadow: none; + box-shadow: 0 1px white; +} + +.line-highlight[data-end]:after { + content: attr(data-end); + top: auto; + bottom: .4em; +} .line-numbers .line-highlight:before, .line-numbers .line-highlight:after { - content: none; + content: none; } pre[id].linkable-line-numbers span.line-numbers-rows { - pointer-events: all; + pointer-events: all; } + pre[id].linkable-line-numbers span.line-numbers-rows > span:before { - cursor: pointer; + cursor: pointer; } + pre[id].linkable-line-numbers span.line-numbers-rows > span:hover:before { - background-color: rgba(128, 128, 128, .2); + background-color: rgba(128, 128, 128, .2); } pre.language-mermaid { @@ -568,7 +577,7 @@ div#nprogress .bar .peg { /* Customize vsc-dark-plus theme */ -.token.slotable-name.class-name{ +.token.slotable-name.class-name { color: #9cdcfe; } diff --git a/assets/js/app.js b/assets/js/app.js index 98b1a5f..04ba908 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -6,22 +6,45 @@ import "../css/app.css"; window.Prism = window.Prism || {}; window.Prism.manual = true; +// 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" import Prism from "../vendor/prism.js"; import "../vendor/font-awesome.js"; import Hooks from "./_hooks" + +let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content"); +let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks}) + // Uncomment when we start using mermaid // import mermaid from "../vendor/mermaid" // mermaid.initialize({startOnLoad: false}); -// Don't load the topbar for catalogue examples/praygrounds +// Don't load the topbar for catalogue examples/playgrounds if (!window.frameElement) { topbar.config({barColors: {0: "hsl(0, 0%, 86%)"}}) - window.addEventListener("phx:page-loading-start", info => topbar.show()) - window.addEventListener("phx:page-loading-stop", info => topbar.hide()) + window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) + window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) } Hooks["Highlight"] = { @@ -52,7 +75,11 @@ Hooks["SectionHeading"] = { } } -let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content"); -let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}}) +// connect if there are any LiveViews on the page liveSocket.connect() -// window.liveSocket = liveSocket + +// 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/package.json b/assets/package.json deleted file mode 100644 index 79c82d4..0000000 --- a/assets/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "scripts": { - "deploy": "cd .. && mix assets.deploy && rm -f _build/esbuild" - } -} diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js new file mode 100644 index 0000000..b779942 --- /dev/null +++ b/assets/tailwind.config.js @@ -0,0 +1,71 @@ +// TODO: Tailwind +// // 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/*_web.ex", +// "../lib/*_web/**/*.*ex", +// "../lib/*_web/**/*.sface", +// "../priv/catalogue/**/*.{ex,sface}" +// ], +// theme: { +// extend: { +// colors: { +// brand: "#FD4F00", +// } +// }, +// }, +// plugins: [ +// require("@tailwindcss/forms"), +// // Allows prefixing tailwind classes with LiveView classes to add rules +// // only when LiveView classes are applied, for example: +// // +// //
+// // +// plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])), +// 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, "./vendor/heroicons/optimized") +// let values = {} +// let icons = [ +// ["", "/24/outline"], +// ["-solid", "/24/solid"], +// ["-mini", "/20/solid"] +// ] +// icons.forEach(([suffix, dir]) => { +// fs.readdirSync(path.join(iconsDir, dir)).map(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, "") +// 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": theme("spacing.5"), +// "height": theme("spacing.5") +// } +// } +// }, {values}) +// }) +// ] +// } diff --git a/assets/vendor/topbar.js b/assets/vendor/topbar.js index ff7fbb6..4195727 100644 --- a/assets/vendor/topbar.js +++ b/assets/vendor/topbar.js @@ -1,7 +1,7 @@ /** * @license MIT - * topbar 1.0.0, 2021-01-06 - * http://buunguyen.github.io/topbar + * topbar 2.0.0, 2023-02-04 + * https://buunguyen.github.io/topbar * Copyright (c) 2021 Buu Nguyen */ (function (window, document) { @@ -35,10 +35,11 @@ })(); var canvas, - progressTimerId, - fadeTimerId, 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); @@ -95,21 +96,26 @@ for (var key in opts) if (options.hasOwnProperty(key)) options[key] = opts[key]; }, - show: function () { + show: function (delay) { 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) - ); - })(); + 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) { @@ -125,6 +131,8 @@ return currentProgress; }, hide: function () { + clearTimeout(delayTimerId); + delayTimerId = null; if (!showing) return; showing = false; if (progressTimerId != null) { diff --git a/config/config.exs b/config/config.exs index 45c34cf..49b7b0c 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,5 +1,5 @@ # This file is responsible for configuring your application -# and its dependencies with the aid of the Mix.Config module. +# and its dependencies with the aid of the Config module. # # This configuration file is loaded before any dependency and # is restricted to this project. @@ -7,25 +7,6 @@ # General application configuration import Config -# Configures the endpoint -config :surface_site, SurfaceSiteWeb.Endpoint, - url: [host: "localhost"], - secret_key_base: "qdZaVqRjkIyGWZ50fXKWgziVNqwZTtBLxQiTxBHJpMGXJvDljm+oAEwq+4r+2R4y", - render_errors: [view: SurfaceSiteWeb.ErrorView, accepts: ~w(json)], - pubsub_server: SurfaceSite.PubSub, - check_origin: true, - live_view: [ - signing_salt: "eEXUz84u6QjVdwt4cgT1rDU12+C9NuDn" - ] - -# 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 - # Configures Surface components config :surface, :components, [ {Surface.Components.Markdown, @@ -43,15 +24,50 @@ config :surface, :components, [ {SurfaceSiteWeb.Contexts.Example01.Field, propagate_context_to_slots: true} ] +# Configures the endpoint +config :surface_site, SurfaceSiteWeb.Endpoint, + url: [host: "localhost"], + render_errors: [ + formats: [html: SurfaceSiteWeb.ErrorHTML, json: SurfaceSiteWeb.ErrorJSON], + layout: false + ], + pubsub_server: SurfaceSite.PubSub, + check_origin: true, + live_view: [ + signing_salt: "eEXUz84u6QjVdwt4cgT1rDU12+C9NuDn" + ] + # Configure esbuild (the version is required) config :esbuild, - version: "0.14.10", + version: "0.17.11", default: [ - args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets), + 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__)} ] +# TODO: Tailwind +# Configure tailwind (the version is required) +# config :tailwind, +# version: "3.3.2", +# default: [ +# 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 "#{Mix.env()}.exs" +import_config "#{config_env()}.exs" diff --git a/config/dev.exs b/config/dev.exs index be3e290..3db7c95 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -4,22 +4,20 @@ import Config # debugging and code reloading. # # The watchers configuration can be used to run external -# watchers to your application. For example, we use it -# with webpack to recompile .js and .css sources. +# watchers to your application. For example, we can use it +# to bundle .js and .css sources. config :surface_site, SurfaceSiteWeb.Endpoint, - http: [port: 4000], - debug_errors: true, - code_reloader: true, + # 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: "qdZaVqRjkIyGWZ50fXKWgziVNqwZTtBLxQiTxBHJpMGXJvDljm+oAEwq+4r+2R4y", watchers: [ esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]} - ], - reloadable_compilers: [:phoenix, :elixir, :surface], - live_reload: [ - patterns: [ - ~r{lib/surface_site_web/(live|components)/.*(ex|js)$}, - ~r{priv/posts/*/.*(md)$} - ] + # TODO: Tailwind + # tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} ] # ## SSL Support @@ -30,7 +28,6 @@ config :surface_site, SurfaceSiteWeb.Endpoint, # # 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: @@ -46,6 +43,17 @@ config :surface_site, SurfaceSiteWeb.Endpoint, # configured to run both http and https servers on # different ports. +# Watch static and templates for browser reloading. +config :surface_site, SurfaceSiteWeb.Endpoint, + reloadable_compilers: [:elixir, :app, :surface], + live_reload: [ + patterns: [ + ~r{priv/posts/*/.*(md)$}, + ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", + ~r"lib/surface_site_web/(controllers|live|components)/.*(ex|heex|sface|js)$" + ] + ] + # Do not include metadata nor timestamps in development logs config :logger, :console, format: "[$level] $message\n" diff --git a/config/prod.exs b/config/prod.exs index da26abf..19ade0b 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -1,76 +1,16 @@ 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, +# 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 :surface_site, SurfaceSiteWeb.Endpoint, - http: [ - port: System.get_env("PORT") || 4000, - transport_options: [socket_opts: [:inet6]] - ], - url: [scheme: "https", host: "surface-ui.org", port: 443], force_ssl: [hsts: true, rewrite_on: [:x_forwarded_proto]], - secret_key_base: Map.fetch!(System.get_env(), "SECRET_KEY_BASE"), 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 :surface_site, SurfaceSiteWeb.Endpoint, -# ... -# url: [host: "example.com", port: 443], -# https: [ -# :inet6, -# 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 :surface_site, SurfaceSiteWeb.Endpoint, -# force_ssl: [hsts: true] -# -# Check `Plug.SSL` for all available options in `force_ssl`. - -# ## Using releases (distillery) -# -# If you are doing OTP releases, you need to instruct Phoenix -# to start the server for all endpoints: -# -# config :phoenix, :serve_endpoints, true -# -# Alternatively, you can configure exactly which server to -# start per endpoint: -# -# config :surface_site, SurfaceSiteWeb.Endpoint, server: true -# -# Note you can't rely on `System.get_env/1` when using releases. -# See the releases documentation accordingly. - -# Finally import the config/prod.secret.exs which should be versioned -# separately. -# import_config "prod.secret.exs" +# Runtime production configuration, including reading +# of environment variables, is done on config/runtime.exs. diff --git a/config/runtime.exs b/config/runtime.exs new file mode 100644 index 0000000..2ea1020 --- /dev/null +++ b/config/runtime.exs @@ -0,0 +1,82 @@ +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/surface_site 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 :surface_site, SurfaceSiteWeb.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") || "surface-ui.org" + port = String.to_integer(System.get_env("PORT") || "4000") + + config :surface_site, SurfaceSiteWeb.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/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 + + # ## SSL Support + # + # To get SSL working, you will need to add the `https` key + # to your endpoint configuration: + # + # config :surface_site, SurfaceSiteWeb.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 endpoint, ensuring + # no data is ever sent via http, always redirecting to https: + # + # config :surface_site, SurfaceSiteWeb.Endpoint, + # force_ssl: [hsts: true] + # + # Check `Plug.SSL` for all available options in `force_ssl`. +end diff --git a/config/test.exs b/config/test.exs index 04e2141..82cd81d 100644 --- a/config/test.exs +++ b/config/test.exs @@ -3,8 +3,15 @@ import Config # We don't run a server during test. If one is required, # you can enable the server option below. config :surface_site, SurfaceSiteWeb.Endpoint, - http: [port: 4002], + http: [ip: {127, 0, 0, 1}, port: 4002], + secret_key_base: "qick86rrt0cZHQpbf7BLKywNWLsT9FUWpg/f//xOmOw6ZD/rVBUd+f0KMzEw0jI+", server: false +# In test we don't send emails. +config :surface_site, SurfaceSite.Mailer, adapter: Swoosh.Adapters.Test + # Print only warnings and errors during test -config :logger, level: :warn +config :logger, level: :warning + +# Initialize plugs at runtime for faster test compilation +config :phoenix, :plug_init_mode, :runtime diff --git a/lib/surface_site/application.ex b/lib/surface_site/application.ex index 870fc96..c5505a0 100644 --- a/lib/surface_site/application.ex +++ b/lib/surface_site/application.ex @@ -5,10 +5,12 @@ defmodule SurfaceSite.Application do use Application + @impl true def start(_type, _args) do - # List all child processes to be supervised children = [ + # Start the PubSub system {Phoenix.PubSub, name: SurfaceSite.PubSub}, + # Start the Endpoint (http/https) SurfaceSiteWeb.Endpoint ] @@ -20,6 +22,7 @@ defmodule SurfaceSite.Application do # Tell Phoenix to update the endpoint configuration # whenever the application is updated. + @impl true def config_change(changed, _new, removed) do SurfaceSiteWeb.Endpoint.config_change(changed, removed) :ok diff --git a/lib/surface_site_web.ex b/lib/surface_site_web.ex index 64de852..2a8500c 100644 --- a/lib/surface_site_web.ex +++ b/lib/surface_site_web.ex @@ -1,61 +1,105 @@ defmodule SurfaceSiteWeb do @moduledoc """ The entrypoint for defining your web interface, such - as controllers, views, channels and so on. + as controllers, components, channels, and so on. This can be used in your application as: use SurfaceSiteWeb, :controller - use SurfaceSiteWeb, :view + use SurfaceSiteWeb, :html - The definitions below will be executed for every view, - controller, etc, so keep them short and clean, focused + 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 any helper function in modules - and import those modules here. + 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, namespace: SurfaceSiteWeb + use Phoenix.Controller, + formats: [:html, :json], + layouts: [html: SurfaceSiteWeb.Layouts] import Plug.Conn - import Phoenix.LiveView.Controller, only: [live_render: 3] - alias SurfaceSiteWeb.Router.Helpers, as: Routes + + unquote(verified_routes()) end end - def view do + def live_view do quote do - use Phoenix.View, - root: "lib/surface_site_web/templates", - namespace: SurfaceSiteWeb + use Phoenix.LiveView, + layout: {SurfaceSiteWeb.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_flash: 1, get_flash: 2, view_module: 1] - import Phoenix.LiveView.Helpers + import Phoenix.Controller, + only: [get_csrf_token: 0, view_module: 1, view_template: 1] - alias SurfaceSiteWeb.Router.Helpers, as: Routes + # Include general helpers for rendering HTML + unquote(html_helpers()) import Surface - - use Phoenix.HTML end end - def router do + defp html_helpers do quote do - use Phoenix.Router - import Plug.Conn - import Phoenix.Controller - import Phoenix.LiveView.Router + # HTML escaping functionality + import Phoenix.HTML + # Core UI components and translation + import SurfaceSiteWeb.CoreComponents + + # Shortcut for generating JS commands + alias Phoenix.LiveView.JS + + # Routes generation with the ~p sigil + unquote(verified_routes()) end end - def channel do + def verified_routes do quote do - use Phoenix.Channel + use Phoenix.VerifiedRoutes, + endpoint: SurfaceSiteWeb.Endpoint, + router: SurfaceSiteWeb.Router, + statics: SurfaceSiteWeb.static_paths() end end @@ -65,4 +109,13 @@ defmodule SurfaceSiteWeb do defmacro __using__(which) when is_atom(which) do apply(__MODULE__, which, []) end + + def surface_live_view do + quote do + use Surface.LiveView, + layout: {SurfaceSiteWeb.Layouts, :app} + + unquote(html_helpers()) + end + end end diff --git a/lib/surface_site_web/components/core_components.ex b/lib/surface_site_web/components/core_components.ex new file mode 100644 index 0000000..5220267 --- /dev/null +++ b/lib/surface_site_web/components/core_components.ex @@ -0,0 +1,635 @@ +defmodule SurfaceSiteWeb.CoreComponents do + @moduledoc """ + Provides core UI components. + + At the first glance, this module may seem daunting, but its goal is + to provide some core building blocks in your application, such as modals, + tables, and forms. The components are mostly markup and 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 + + @doc """ + Renders a modal. + + ## Examples + + <.modal id="confirm-modal"> + This is a modal. + + + JS commands may be passed to the `:on_cancel` to configure + the closing/cancel event, for example: + + <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}> + This is another modal. + + + """ + attr :id, :string, required: true + attr :show, :boolean, default: false + attr :on_cancel, JS, default: %JS{} + slot :inner_block, required: true + + def modal(assigns) do + ~H""" + + """ + end + + def input(%{type: "select"} = assigns) do + ~H""" +
+ <.label for={@id}><%= @label %> + + <.error :for={msg <- @errors}><%= msg %> +
+ """ + end + + def input(%{type: "textarea"} = assigns) do + ~H""" +
+ <.label for={@id}><%= @label %> + + <.error :for={msg <- @errors}><%= msg %> +
+ """ + end + + # All other inputs text, datetime-local, url, password, etc. are handled here... + def input(assigns) do + ~H""" +
+ <.label for={@id}><%= @label %> + + <.error :for={msg <- @errors}><%= msg %> +
+ """ + end + + @doc """ + Renders a label. + """ + attr :for, :string, default: nil + slot :inner_block, required: true + + def label(assigns) do + ~H""" + + """ + end + + @doc """ + Generates a generic error message. + """ + slot :inner_block, required: true + + def error(assigns) do + ~H""" +

+ <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" /> + <%= render_slot(@inner_block) %> +

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

+ <%= render_slot(@inner_block) %> +

+

+ <%= render_slot(@subtitle) %> +

+
+
<%= render_slot(@actions) %>
+
+ """ + end + + @doc ~S""" + Renders a table with generic styling. + + ## Examples + + <.table id="users" rows={@users}> + <:col :let={user} label="id"><%= user.id %> + <:col :let={user} label="username"><%= user.username %> + + """ + 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""" +
+ + + + + + + + + + + + + +
<%= col[:label] %>Actions
+
+ + + <%= render_slot(col, @row_item.(row)) %> + +
+
+
+ + + <%= render_slot(action, @row_item.(row)) %> + +
+
+
+ """ + end + + @doc """ + Renders a data list. + + ## Examples + + <.list> + <:item title="Title"><%= @post.title %> + <:item title="Views"><%= @post.views %> + + """ + slot :item, required: true do + attr :title, :string, required: true + end + + def list(assigns) do + ~H""" +
+
+
+
<%= item.title %>
+
<%= render_slot(item) %>
+
+
+
+ """ + end + + @doc """ + Renders a back navigation link. + + ## Examples + + <.back navigate={~p"/posts"}>Back to posts + """ + attr :navigate, :any, required: true + slot :inner_block, required: true + + def back(assigns) do + ~H""" +
+ <.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) %> + +
+ """ + 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 your `assets/vendor/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""" + + """ + end + + ## JS Commands + + def show(js \\ %JS{}, selector) do + JS.show(js, + to: selector, + 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", + 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 +end diff --git a/lib/surface_site_web/components/layouts.ex b/lib/surface_site_web/components/layouts.ex new file mode 100644 index 0000000..1429649 --- /dev/null +++ b/lib/surface_site_web/components/layouts.ex @@ -0,0 +1,13 @@ +defmodule SurfaceSiteWeb.Layouts do + use SurfaceSiteWeb, :html + + alias Surface.Components.LiveRedirect + + embed_templates "layouts/*" + embed_sface("layouts/root.sface") + # embed_sface("layouts/app.sface") + + defp surface_version() do + Application.spec(:surface, :vsn) + end +end diff --git a/lib/surface_site_web/components/layouts/app.sface b/lib/surface_site_web/components/layouts/app.sface new file mode 100644 index 0000000..f07c194 --- /dev/null +++ b/lib/surface_site_web/components/layouts/app.sface @@ -0,0 +1,4 @@ +
+ <.flash_group flash={@flash} /> + {@inner_content} +
diff --git a/lib/surface_site_web/components/layouts/root.sface b/lib/surface_site_web/components/layouts/root.sface new file mode 100644 index 0000000..a1c75e2 --- /dev/null +++ b/lib/surface_site_web/components/layouts/root.sface @@ -0,0 +1,66 @@ + + + + + + + <.live_title> + {assigns[:page_title] || "Surface"} + + + + + + + + + +
+
+
+

+ Surface UI +

+

+ A server-side rendering component library for Phoenix +

+ + + +
+
+
+ {@inner_content} + + + diff --git a/lib/surface_site_web/controllers/blog_controller.ex b/lib/surface_site_web/controllers/blog_controller.ex index 8cbba3e..764a830 100644 --- a/lib/surface_site_web/controllers/blog_controller.ex +++ b/lib/surface_site_web/controllers/blog_controller.ex @@ -4,7 +4,8 @@ defmodule SurfaceSiteWeb.BlogController do alias SurfaceSite.Blog def index(conn, %{"tag" => tag}) do - render(conn, "index.html", + render(conn, :index, + layout: false, posts: Blog.get_posts_by_tag!(tag), recent_posts: Blog.recent_posts(5), tags: Blog.all_tags() @@ -12,7 +13,8 @@ defmodule SurfaceSiteWeb.BlogController do end def index(conn, _params) do - render(conn, "index.html", + render(conn, :index, + layout: false, posts: Blog.visible_posts(), recent_posts: Blog.recent_posts(5), tags: Blog.all_tags() @@ -21,6 +23,7 @@ defmodule SurfaceSiteWeb.BlogController do def show(conn, %{"id" => id}) do render(conn, "show.html", + layout: false, post: Blog.get_post_by_id!(id), recent_posts: Blog.recent_posts(5), tags: Blog.all_tags() diff --git a/lib/surface_site_web/controllers/blog_html.ex b/lib/surface_site_web/controllers/blog_html.ex new file mode 100644 index 0000000..7070354 --- /dev/null +++ b/lib/surface_site_web/controllers/blog_html.ex @@ -0,0 +1,63 @@ +defmodule SurfaceSiteWeb.BlogHTML do + use SurfaceSiteWeb, :html + + alias Surface.Components.{Link, LiveRedirect} + + embed_sface("blog_html/index.sface") + embed_sface("blog_html/show.sface") + + def sidebar(assigns) do + ~F""" + {!-- + --} + + + """ + end +end diff --git a/lib/surface_site_web/controllers/blog_html/index.sface b/lib/surface_site_web/controllers/blog_html/index.sface new file mode 100644 index 0000000..1c358ab --- /dev/null +++ b/lib/surface_site_web/controllers/blog_html/index.sface @@ -0,0 +1,57 @@ +
+
+
+
+
+ +
+
+
+ {#for post <- @posts} +
+ +

{post.title}

+ +

+ by {post.author} ・ +

+

+ + + + + + {#for tag <- post.tags} + + {tag} + + {/for} +

+ {post.description} + +
+
+ {#else} +
+ No post found. +
+ {/for} +
+
+ <.sidebar tags={@tags} , recent_posts={@recent_posts} /> +
+
+
+
+
+
+
diff --git a/lib/surface_site_web/controllers/blog_html/show.sface b/lib/surface_site_web/controllers/blog_html/show.sface new file mode 100644 index 0000000..5f469f4 --- /dev/null +++ b/lib/surface_site_web/controllers/blog_html/show.sface @@ -0,0 +1,56 @@ + +
+
+
+
+
+ +
+
+
+
+

{@post.title}

+

+ by {@post.author} ・ +

+

+ + + + + + {#for tag <- @post.tags} + {tag} + {/for} +

+ {raw(@post.body)} +
+
+
+ <.sidebar tags={@tags} , recent_posts={@recent_posts} /> +
+
+ +
+
+
+
+
diff --git a/lib/surface_site_web/controllers/error_html.ex b/lib/surface_site_web/controllers/error_html.ex new file mode 100644 index 0000000..43ee392 --- /dev/null +++ b/lib/surface_site_web/controllers/error_html.ex @@ -0,0 +1,19 @@ +defmodule SurfaceSiteWeb.ErrorHTML do + use SurfaceSiteWeb, :html + + # If you want to customize your error pages, + # uncomment the embed_templates/1 call below + # and add pages to the error directory: + # + # * lib/surface_site_web/controllers/error_html/404.html.heex + # * lib/surface_site_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 diff --git a/lib/surface_site_web/views/error_view.ex b/lib/surface_site_web/controllers/error_json.ex similarity index 59% rename from lib/surface_site_web/views/error_view.ex rename to lib/surface_site_web/controllers/error_json.ex index a5418a9..ead183e 100644 --- a/lib/surface_site_web/views/error_view.ex +++ b/lib/surface_site_web/controllers/error_json.ex @@ -1,8 +1,7 @@ -defmodule SurfaceSiteWeb.ErrorView do - use SurfaceSiteWeb, :view - - # If you want to customize a particular status code - # for a certain format, you may uncomment below. +defmodule SurfaceSiteWeb.ErrorJSON do + # 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 @@ -10,7 +9,7 @@ defmodule SurfaceSiteWeb.ErrorView do # By default, Phoenix returns the status message from # the template name. For example, "404.json" becomes # "Not Found". - def template_not_found(template, _assigns) do + def render(template, _assigns) do %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} end end diff --git a/lib/surface_site_web/endpoint.ex b/lib/surface_site_web/endpoint.ex index 1245259..ecaa7f2 100644 --- a/lib/surface_site_web/endpoint.ex +++ b/lib/surface_site_web/endpoint.ex @@ -1,7 +1,17 @@ defmodule SurfaceSiteWeb.Endpoint do use Phoenix.Endpoint, otp_app: :surface_site - socket "/live", Phoenix.LiveView.Socket + # 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: "_surface_site_key", + signing_salt: "AOliij/f", + same_site: "Lax" + ] + + socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] # Serve at "/" the static files from "priv/static" directory. # @@ -11,7 +21,7 @@ defmodule SurfaceSiteWeb.Endpoint do at: "/", from: :surface_site, gzip: false, - only: ~w(assets fonts images favicon.ico robots.txt) + only: SurfaceSiteWeb.static_paths() # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. @@ -31,14 +41,6 @@ defmodule SurfaceSiteWeb.Endpoint do plug Plug.MethodOverride plug Plug.Head - - # 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. - plug Plug.Session, - store: :cookie, - key: "_surface_site_key", - signing_salt: "AOliij/f" - + plug Plug.Session, @session_options plug SurfaceSiteWeb.Router end diff --git a/lib/surface_site_web/live/builtin_components/field_info.ex b/lib/surface_site_web/live/builtin_components/field_info.ex index 5163f36..ecb6bd4 100644 --- a/lib/surface_site_web/live/builtin_components/field_info.ex +++ b/lib/surface_site_web/live/builtin_components/field_info.ex @@ -13,7 +13,7 @@ defmodule SurfaceSiteWeb.BuiltinComponents.FieldInfo do <:examples> <#Example> -
+
diff --git a/lib/surface_site_web/live/builtin_components/form_info.ex b/lib/surface_site_web/live/builtin_components/form_info.ex index 7297b05..12a23c2 100644 --- a/lib/surface_site_web/live/builtin_components/form_info.ex +++ b/lib/surface_site_web/live/builtin_components/form_info.ex @@ -13,7 +13,7 @@ defmodule SurfaceSiteWeb.BuiltinComponents.FormInfo do <:examples> <#Example> - +