diff --git a/.mise.toml b/.mise.toml index 11357cc..18d0485 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,3 +1,4 @@ [tools] elixir = "1.18.2-otp-27" erlang = "27.2.3" +node = "22.14.0" diff --git a/assets/js/editor.js b/assets/js/editor.js new file mode 100644 index 0000000..8d31dd3 --- /dev/null +++ b/assets/js/editor.js @@ -0,0 +1,5 @@ +import Trix from 'trix' + +document.addEventListener('trix-before-initialize', () => { + console.log('trix-before-initialize') +}) diff --git a/assets/package-lock.json b/assets/package-lock.json new file mode 100644 index 0000000..e595100 --- /dev/null +++ b/assets/package-lock.json @@ -0,0 +1,109 @@ +{ + "name": "assets", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@tailwindcss/typography": "^0.5.16", + "trix": "^2.1.12" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "license": "MIT", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dompurify": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tailwindcss": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.8.tgz", + "integrity": "sha512-Me7N5CKR+D2A1xdWA5t5+kjjT7bwnxZOE6/yDI/ixJdJokszsn2n++mdU5yJwrsTpqFX2B9ZNMBJDwcqk9C9lw==", + "license": "MIT", + "peer": true + }, + "node_modules/trix": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/trix/-/trix-2.1.12.tgz", + "integrity": "sha512-0hQvJdy257XuzRdCzSQ/QvcqyTp+8ixMxVLWxSbWvEzD2kgKFlcrMjgWZbtVkJENaod+jm2sBTOWAZVNWK+DMA==", + "license": "MIT", + "dependencies": { + "dompurify": "^3.2.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + } + } +} diff --git a/assets/package.json b/assets/package.json new file mode 100644 index 0000000..e1d90c4 --- /dev/null +++ b/assets/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "@tailwindcss/typography": "^0.5.16", + "trix": "^2.1.12" + } +} diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index 39dbd54..b3c077f 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -20,6 +20,7 @@ module.exports = { }, plugins: [ require("@tailwindcss/forms"), + require("@tailwindcss/typography"), // Allows prefixing tailwind classes with LiveView classes to add rules // only when LiveView classes are applied, for example: // diff --git a/config/config.exs b/config/config.exs index 260235d..5ef9690 100644 --- a/config/config.exs +++ b/config/config.exs @@ -27,7 +27,8 @@ config :cms, config :esbuild, version: "0.17.11", cms: [ - args: ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), + args: + ~w(js/app.js js/editor.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), cd: Path.expand("../assets", __DIR__), env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} ] diff --git a/lib/cms_web/components/core_components.ex b/lib/cms_web/components/core_components.ex index 0b07bfa..dff2960 100644 --- a/lib/cms_web/components/core_components.ex +++ b/lib/cms_web/components/core_components.ex @@ -4,6 +4,28 @@ defmodule CMSWeb.CoreComponents do """ use Phoenix.Component + attr :class, :string, default: nil + + attr :global, :global, + include: ~w[navigate patch href replace method csrf_token download hreflang referrerpolicy rel target type] + + slot :inner_block + + def a(assigns) do + ~H""" + <.link class={["hover:underline", @class]} {@global}>{render_slot(@inner_block)}</.link> + """ + end + + attr :field, Phoenix.HTML.FormField, required: true + attr :global, :global, include: ~w[required placeholder type] + + def input(assigns) do + ~H""" + <input id={@field.id} name={@field.name} value={@field.value} {@global} /> + """ + end + @doc """ Renders a [Heroicon](https://heroicons.com). diff --git a/lib/cms_web/components/layouts/app.html.heex b/lib/cms_web/components/layouts/app.html.heex index 223cdd9..76d81a8 100644 --- a/lib/cms_web/components/layouts/app.html.heex +++ b/lib/cms_web/components/layouts/app.html.heex @@ -1,30 +1,32 @@ <div :if={@admin?} - class="flex flex-row justify-between py-1 px-3 md:mb-2 border-b border-slate-100" + class="sticky top-0 bg-white z-50 flex flex-row justify-between py-1 px-3 md:mb-2 border-b border-slate-100" > <section class="flex flex-row gap-x-2"> <div class="pr-2 border-r border-slate-100"> - <.link navigate={~p"/admin"} class="font-bold">admin mode</.link> + <.a navigate={~p"/admin"} class="font-bold">admin mode</.a> </div> <nav> <ul class="flex flex-row"> - <.link href={~p"/admin/posts/new"} class="hover:underline">new post</.link> + <.a href={~p"/admin/posts/new"}>new post</.a> </ul> </nav> </section> <section class="flex flex-row"> - <.link href={~p"/admin/session/destroy?return_to=#{@current_path}"} class="hover:underline"> + <.a href={~p"/admin/session/destroy?return_to=#{@current_path}"}> sign out - </.link> + </.a> </section> </div> <div class="flex flex-col md:flex-row mx-auto max-w-4xl"> <section class="flex flex-col p-2 gap-y-1 border-slate-100 border-b md:border-b-0"> - <.link href={~p"/"} class="font-bold hover:underline">sloanelybutsurely.com</.link> + <.a href={~p"/"} class="font-bold">sloanelybutsurely.com</.a> <nav> <ul> - <li><.link href={~p"/posts"} class="hover:underline">writing</.link></li> + <li> + <.a href={~p"/posts"}>writing</.a> + </li> </ul> </nav> </section> @@ -36,5 +38,5 @@ :if={not @admin?} class="fixed right-0 bottom-0 p-2 text-transparent underline hover:text-current" > - <.link href={~p"/sign-in?return_to=#{@current_path}"}>sign in</.link> + <.a href={~p"/sign-in?return_to=#{@current_path}"}>sign in</.a> </div> diff --git a/lib/cms_web/controllers/post_html/show.html.heex b/lib/cms_web/controllers/post_html/show.html.heex index d037a53..d84ab8f 100644 --- a/lib/cms_web/controllers/post_html/show.html.heex +++ b/lib/cms_web/controllers/post_html/show.html.heex @@ -1,5 +1,7 @@ -<header class="flex flex-row justify-between"> - <h1>{@post.title}</h1> - <.link :if={@admin?} href={~p"/admin/posts/#{@post}"}>edit</.link> -</header> -<p>{@post.contents}</p> +<article> + <header class="flex flex-row justify-between"> + <h1 :if={@post.title} class="font-bold text-xl mb-3">{@post.title}</h1> + <.link :if={@admin?} href={~p"/admin/posts/#{@post}"}>edit</.link> + </header> + <section class="prose">{raw(@post.contents)}</section> +</article> diff --git a/lib/cms_web/live/admin_login_live.ex b/lib/cms_web/live/admin_login_live.ex index ef229cd..a8f9e1c 100644 --- a/lib/cms_web/live/admin_login_live.ex +++ b/lib/cms_web/live/admin_login_live.ex @@ -4,7 +4,12 @@ defmodule CMSWeb.AdminLoginLive do @impl true def mount(params, _session, socket) do - socket = assign(socket, :form, to_form(%{"password" => "", "return_to" => params["return_to"]})) + socket = + assign( + socket, + form: to_form(%{"password" => "", "return_to" => params["return_to"]}), + return_to: params["return_to"] + ) {:ok, socket, layout: false} end @@ -30,10 +35,17 @@ defmodule CMSWeb.AdminLoginLive do /> <div class="flex flex-col items-end"> <button type="submit" class="font-bold hover:underline">sign in</button> - <.link href={~p"/"} class="hover:underline">cancel</.link> + <.a href={cancel_href(@return_to)}> + cancel + </.a> </div> </.form> </main> """ end + + defp cancel_href("/admin"), do: ~p"/" + defp cancel_href("/admin/" <> _), do: ~p"/" + defp cancel_href(nil), do: ~p"/" + defp cancel_href(return_to), do: return_to end diff --git a/lib/cms_web/live/post_live.ex b/lib/cms_web/live/post_live.ex index 10a5514..b4f53f3 100644 --- a/lib/cms_web/live/post_live.ex +++ b/lib/cms_web/live/post_live.ex @@ -52,11 +52,18 @@ defmodule CMSWeb.PostLive do @impl true def render(assigns) do ~H""" - <.form for={@form} class="flex flex-col" phx-submit="save_post"> - <input type="text" id={@form[:title].id} name={@form[:title].name} value={@form[:title].value} /> - <textarea id={@form[:contents].id} name={@form[:contents].name}>{@form[:contents].value} </textarea> + <.form for={@form} class="flex flex-col gap-y-2" phx-submit="save_post"> + <.input type="hidden" field={@form[:contents]} /> + <.input class="border-gray-400 rounded" field={@form[:title]} /> + <div id="editor" phx-update="ignore"> + <trix-editor input={@form[:contents].id} class="prose prose-gray max-w-full"></trix-editor> + </div> + <button type="submit" class="self-end">save</button> </.form> + <link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css" /> + <script defer phx-track-static type="text/javascript" src={~p"/assets/editor.js"}> + </script> """ end end