From 94cec33b46be065e2927ed4525c192146ce46ea5 Mon Sep 17 00:00:00 2001 From: trisua Date: Sun, 20 Jul 2025 02:49:01 -0400 Subject: [PATCH] Initial --- app/public/app.js | 134 +++++++ app/public/style.css | 562 ++++++++++++++++++++++++++++++ app/templates_src/components.lisp | 40 +++ app/templates_src/edit.lisp | 122 +++++++ app/templates_src/error.lisp | 9 + app/templates_src/index.lisp | 87 +++++ app/templates_src/root.lisp | 28 ++ app/templates_src/view.lisp | 29 ++ 8 files changed, 1011 insertions(+) create mode 100644 app/public/app.js create mode 100644 app/public/style.css create mode 100644 app/templates_src/components.lisp create mode 100644 app/templates_src/edit.lisp create mode 100644 app/templates_src/error.lisp create mode 100644 app/templates_src/index.lisp create mode 100644 app/templates_src/root.lisp create mode 100644 app/templates_src/view.lisp diff --git a/app/public/app.js b/app/public/app.js new file mode 100644 index 0000000..cac6874 --- /dev/null +++ b/app/public/app.js @@ -0,0 +1,134 @@ +// theme preference +function media_theme_pref() { + document.documentElement.removeAttribute("class"); + + if ( + window.matchMedia("(prefers-color-scheme: dark)").matches && + (!window.localStorage.getItem("attobin:theme") || + window.localStorage.getItem("attobin:theme") === "Auto") + ) { + document.documentElement.classList.add("dark"); + + document.getElementById("switch_light").classList.add("hidden"); + document.getElementById("switch_dark").classList.remove("hidden"); + } else if ( + window.matchMedia("(prefers-color-scheme: light)").matches && + (!window.localStorage.getItem("attobin:theme") || + window.localStorage.getItem("attobin:theme") === "Auto") + ) { + document.documentElement.classList.remove("dark"); + + document.getElementById("switch_light").classList.remove("hidden"); + document.getElementById("switch_dark").classList.add("hidden"); + } else if (window.localStorage.getItem("attobin:theme")) { + /* restore theme */ + const current = window.localStorage.getItem("attobin:theme"); + document.documentElement.className = current.toLowerCase(); + + if (current === "Light") { + document.getElementById("switch_light").classList.remove("hidden"); + document.getElementById("switch_dark").classList.add("hidden"); + } else { + document.getElementById("switch_light").classList.add("hidden"); + document.getElementById("switch_dark").classList.remove("hidden"); + } + } +} + +globalThis.set_theme = (theme) => { + window.localStorage.setItem("attobin:theme", theme); + document.documentElement.className = theme; + media_theme_pref(); +}; + +media_theme_pref(); + +// messages +function get_cookie(key) { + return (document.cookie.split(`${key}=`)[1] || "").split(";")[0]; +} + +function check_message() { + const element = document.getElementById("messages"); + + const message = get_cookie("Atto-Message"); + const message_good = get_cookie("Atto-Message-Good") === "true"; + + if (message) { + element.style.marginBottom = "1rem"; + element.style.paddingLeft = "1rem"; + element.innerHTML += `
  • ${message.replaceAll('"', "")}
  • `; + } + + // clear cookies + for (cookie of document.cookie.split(";")) { + // biome-ignore lint/suspicious/noDocumentCookie: cookie store is barely supported + document.cookie = `${cookie.split("=")[0]}=; expires=${new Date(0).toUTCString()}; path=/`; + } +} + +check_message(); + +// editor +globalThis.init_editor = () => { + globalThis.editor = CodeMirror(document.getElementById("editor_tab"), { + value: (document.getElementById("editor_content") || { innerHTML: "" }) + .innerHTML, + mode: "markdown", + lineWrapping: true, + lineNumbers: false, + autoCloseBrackets: true, + autofocus: true, + viewportMargin: Number.POSITIVE_INFINITY, + inputStyle: "contenteditable", + highlightFormatting: false, + fencedCodeBlockHighlighting: false, + xml: false, + smartIndent: true, + indentUnit: 4, + placeholder: "", + extraKeys: { + Home: "goLineLeft", + End: "goLineRight", + Enter: (cm) => { + cm.replaceSelection("\n"); + }, + }, + }); + + window.addEventListener("beforeunload", (e) => { + e.preventDefault(); + return null; + }); +}; + +globalThis.tab_editor = () => { + document.getElementById("editor_tab").classList.remove("hidden"); + document.getElementById("preview_tab").classList.add("hidden"); + + document.getElementById("editor_tab_button").classList.remove("camo"); + document.getElementById("preview_tab_button").classList.add("camo"); +}; + +globalThis.tab_preview = async () => { + // render + const res = await ( + await fetch("/api/v1/render", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + content: globalThis.editor.getValue(), + }), + }) + ).text(); + + document.getElementById("preview_tab").innerHTML = res; + hljs.highlightAll(); + + // ... + document.getElementById("editor_tab").classList.add("hidden"); + document.getElementById("preview_tab").classList.remove("hidden"); + + document.getElementById("editor_tab_button").classList.add("camo"); + document.getElementById("preview_tab_button").classList.remove("camo"); +}; diff --git a/app/public/style.css b/app/public/style.css new file mode 100644 index 0000000..c058fa0 --- /dev/null +++ b/app/public/style.css @@ -0,0 +1,562 @@ +:root { + color-scheme: light dark; + + --color-super-lowered: oklch(87.1% 0.006 286.286); + --color-lowered: oklch(96.7% 0.001 286.375); + --color-surface: oklch(92.9% 0.013 255.508); + --color-raised: oklch(98.4% 0.003 247.858); + --color-super-raised: oklch(96.8% 0.007 247.896); + --color-text: hsl(0, 0%, 5%); + + --color-link: #2949b2; + --color-shadow: rgba(0, 0, 0, 0.08); + --color-red: hsl(0, 84%, 40%); + --color-green: hsl(100, 84%, 20%); + --color-yellow: hsl(41, 63%, 75%); + --color-purple: hsl(284, 84%, 20%); + --color-primary: oklch(67.3% 0.182 276.935); + + --shadow-x-offset: 0; + --shadow-y-offset: 0.125rem; + --shadow-size: var(--pad-1); + + --pad-1: 0.2rem; + --pad-2: 0.35rem; + --pad-3: 0.5rem; + --pad-4: 1rem; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-size: 16px; +} + +.dark, +.dark * { + --color-super-lowered: var(--color-super-raised); + --color-lowered: var(--color-raised); + --color-surface: oklch(21% 0.006 285.885); + --color-raised: oklch(27.4% 0.006 286.033); + --color-super-raised: oklch(37% 0.013 285.805); + --color-text: hsl(0, 0%, 95%); + + --color-link: #93c5fd; + --color-red: hsl(0, 94%, 82%); + --color-green: hsl(100, 94%, 82%); + --color-yellow: hsl(41, 63%, 65%); + --color-purple: hsl(284, 94%, 82%); +} + +html, +body { + line-height: 1.5; + letter-spacing: 0.15px; + font-family: + "Inter", "Poppins", "Roboto", ui-sans-serif, system-ui, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; + color: var(--color-text); + background: var(--color-surface); + overflow: auto auto; + height: 100dvh; + scroll-behavior: smooth; + overflow-x: hidden; +} + +main { + width: 80ch; + margin: var(--pad-4) auto; + padding: var(--pad-3) var(--pad-4); +} + +article { + margin: var(--pad-2) 0; + height: calc(100dvh - var(--pad-4) * 2); +} + +.tab { + flex: 1 0 auto; + overflow: auto; +} + +.tabs .tab { + height: 100%; +} + +.fadein { + animation: fadein ease-in-out 1 0.5s forwards running; +} + +@media screen and (max-width: 900px) { + main, + article, + nav, + header, + footer { + width: 100%; + } + + article { + margin-top: 0; + } + + main { + padding: 0; + } + + .flex-collapse-rev { + flex-direction: column-reverse !important; + } +} + +.content_container { + margin: var(--pad-2) auto; + width: 100%; +} + +@media screen and (min-width: 500px) { + .content_container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .content_container { + max-width: 720px; + } +} + +@media (min-width: 900px) { + .content_container { + max-width: 960px; + } + + @media (min-width: 1200px) { + article { + padding: 0; + } + + .content_container { + max-width: 1100px; + } + } +} + +video { + max-width: 100%; + border-radius: var(--radius); +} + +/* card */ +.card { + padding: var(--pad-4); + background: var(--color-raised); + color: var(--color-text); +} + +/* button */ +.button { + display: flex; + justify-content: center; + align-items: center; + gap: var(--gap-2); + padding: var(--pad-2) var(--pad-4); + cursor: pointer; + background: var(--color-raised); + color: var(--color-text); + outline: none; + border: none; + width: max-content; + height: max-content; + transition: background 0.15s; + text-decoration: none !important; + user-select: none; +} + +.button:hover { + background: var(--color-super-raised); +} + +.button.camo { + background: transparent; + color: inherit; +} + +.bar .button.camo:hover { + color: var(--color-link); +} + +/* input */ +input { + padding: var(--pad-2) var(--pad-4); + background: var(--color-raised); + color: var(--color-text); + outline: none; + border: none; + width: max-content; + transition: background 0.15s; +} + +input:focus { + outline: solid 2px var(--color-primary); + box-shadow: 0 0 0 4px oklch(87% 0.065 274.039 / 25%); + background: var(--color-super-raised); +} + +/* typo */ +p { + margin-bottom: var(--pad-4); +} + +p:last-child { + margin-bottom: 0; +} + +.post_right:not(.repost) { + max-width: calc(100% - 52px); +} + +.rhs { + width: 100% !important; +} + +.name { + max-width: 250px; + overflow: hidden; + /* overflow-wrap: break-word; */ + overflow-wrap: anywhere; + text-overflow: ellipsis; +} + +@media screen and (min-width: 901px) { + .name.shorter { + max-width: 200px; + } + + .name.lg\:long { + max-width: unset; + } + + .rhs { + width: calc(100% - 23rem) !important; + } +} + +ul, +ol { + margin-left: var(--pad-4); +} + +pre { + padding: var(--pad-2) var(--pad-4); + border-left: solid 5px var(--color-primary); + background: var(--color-surface); +} + +code { + padding: 0; +} + +pre, +code { + font-family: "Jetbrains Mono", "Fire Code", monospace; + width: 100%; + max-width: 100%; + overflow: auto; + border-radius: var(--radius); + font-size: 0.8rem !important; + color: inherit; +} + +code * { + font-size: 0.8rem !important; +} + +svg.icon { + stroke: currentColor; + width: 18px; + height: 1em; +} + +svg.icon.filled { + fill: currentColor; +} + +button svg { + pointer-events: none; +} + +hr { + border-top: solid 1px var(--color-super-lowered) !important; + border-left: 0; + border-bottom: 0; + border-right: 0; +} + +hr.margin { + margin: var(--pad-4) 0; +} + +p, +li, +span, +code { + max-width: 100%; + overflow-wrap: normal; + text-wrap: stable; + word-wrap: break-word; +} + +h1 { + font-size: 2rem; +} + +h2 { + font-size: 1.75rem; +} + +h3 { + font-size: 1.5rem; +} + +h4 { + font-size: 1.25rem; +} + +h5 { + font-size: var(--pad-4); +} + +h6 { + font-size: var(--pad-3); +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + font-weight: 700; + width: -moz-max-content; + position: relative; + max-width: 100%; +} + +h1 { + text-align: center; + margin-bottom: 2rem; + width: 100%; +} + +a { + text-decoration: none; + color: var(--color-link); +} + +a.flush { + color: inherit; +} + +a:hover { + text-decoration: underline; +} + +img { + display: inline; + max-width: 100%; + vertical-align: middle; +} + +blockquote { + padding-left: 1rem; + border-left: solid 5px var(--color-super-lowered); + font-style: italic; +} + +/* codemirror/hljs */ +.CodeMirror { + color: var(--color-text) !important; +} + +.CodeMirror { + background: transparent !important; + font-family: inherit !important; + height: 10rem !important; + min-height: 100%; + max-height: 100%; + cursor: text; +} + +.CodeMirror-cursor { + border-color: rgb(0, 0, 0) !important; +} + +.CodeMirror-cursor:is(.dark *) { + border-color: rgb(255, 255, 255) !important; +} + +.CodeMirror-cursor { + height: 22px !important; +} + +[role="presentation"]::-moz-selection, +[role="presentation"] *::-moz-selection { + background-color: rgb(191, 219, 254) !important; +} + +[role="presentation"]::selection, +[role="presentation"] *::selection, +.CodeMirror-selected { + background-color: rgb(191, 219, 254) !important; +} + +[role="presentation"]:is(.dark *)::-moz-selection, +[role="presentation"] *:is(.dark *)::-moz-selection { + background-color: rgb(64, 64, 64) !important; +} + +[role="presentation"]:is(.dark *)::selection, +[role="presentation"] *:is(.dark *)::selection, +.CodeMirror-selected:is(.dark *) { + background-color: rgb(64, 64, 64) !important; +} + +.cm-header { + color: inherit !important; +} + +.cm-variable-2, +.cm-quote, +.cm-keyword, +.cm-string, +.cm-atom, +.hljs-string { + color: rgb(63, 98, 18) !important; +} + +.cm-variable-2:is(.dark *), +.cm-quote:is(.dark *), +.cm-keyword:is(.dark *), +.cm-string:is(.dark *), +.cm-atom:is(.dark *), +.hljs-string:is(.dark *) { + color: rgb(217, 249, 157) !important; +} + +.cm-comment, +.hljs-keyword { + color: rgb(153 27 27) !important; +} + +.cm-comment:is(.dark *), +.hljs-keyword:is(.dark *) { + color: rgb(254, 202, 202) !important; +} + +.cm-link { + color: var(--color-link) !important; +} + +.cm-url, +.cm-property, +.cm-qualifier, +.hljs-title { + color: rgb(29, 78, 216) !important; +} + +.cm-url:is(.dark *), +.cm-property:is(.dark *), +.cm-qualifier:is(.dark *), +.hljs-title:is(.dark *) { + color: rgb(191, 219, 254) !important; +} + +.cm-variable-3, +.cm-tag, +.cm-def, +.cm-attribute, +.cm-number, +.hljs-type { + color: rgb(91, 33, 182) !important; +} + +.cm-variable-3:is(.dark *), +.cm-tag:is(.dark *), +.cm-def:is(.dark *), +.cm-attribute:is(.dark *), +.cm-number:is(.dark *), +.hljs-type:is(.dark *) { + color: rgb(221, 214, 254) !important; +} + +.hljs-built_in { + color: var(--color-purple) !important; +} + +.hljs-variable { + color: var(--color-link) !important; +} + +.hljs-number { + color: var(--color-green) !important; +} + +.CodeMirror-scroll { + height: 100% !important; +} + +.CodeMirror-line { + padding-left: 0 !important; +} + +.CodeMirror-focused .CodeMirror-placeholder { + opacity: 50%; +} + +.hljs { + background: transparent !important; + color: inherit !important; + padding: 0 !important; +} + +/* extra */ +@keyframes fadein { + from { + opacity: 0%; + } + + to { + opacity: 100%; + } +} + +.items-end { + align-items: flex-end; +} + +/* table */ +table { + width: 100%; + table-layout: auto; + margin: var(--pad-4) 0; + border-collapse: separate; + border-spacing: 0; + border: solid 1px var(--color-super-raised); +} + +table td, +table th { + padding: var(--pad-2) var(--pad-4); +} + +table tr:not(thead *):nth-child(odd) { + background: var(--color-super-raised); +} + +table thead th { + text-align: left; +} diff --git a/app/templates_src/components.lisp b/app/templates_src/components.lisp new file mode 100644 index 0000000..2381f24 --- /dev/null +++ b/app/templates_src/components.lisp @@ -0,0 +1,40 @@ +(text "{% macro footer() -%}") +(footer + ("class" "flex flex-col items-center gap-2") + (hr ("style" "min-width: 20rem; margin-top: calc(var(--pad-4) * 4)")) + (div + ("class" "w-full flex justify-between") + (div ("style" "width: 50px")) + (div + ("class" "flex flex-col gap-2 items-center") + (div + ("class" "flex gap-2 flex-wrap") + (a + ("href" "/") + (text "new")) + + (a + ("href" "/what") + (text "what")) + + (a + ("href" "https://trisua.com/t/attobin") + (text "source"))) + + (span ("style" "font-size: 14px") ("class" "fade") (text "{{ name }}"))) + + ; theme switches + (button + ("class" "button camo fade") + ("id" "switch_light") + ("title" "Switch theme") + ("onclick" "set_theme('Dark')") + (text "")) + + (button + ("class" "button camo fade hidden") + ("id" "switch_dark") + ("title" "Switch theme") + ("onclick" "set_theme('Light')") + (text "")))) +(text "{%- endmacro %}") diff --git a/app/templates_src/edit.lisp b/app/templates_src/edit.lisp new file mode 100644 index 0000000..3704b5c --- /dev/null +++ b/app/templates_src/edit.lisp @@ -0,0 +1,122 @@ +(text "{% extends \"root.lisp\" %} {% block head %}") +(title + (text "{{ entry.slug }}")) +(text "{% endblock %} {% block body %}") +(div + ("class" "flex items-center bar") + (button + ("class" "button tab_button") + ("id" "editor_tab_button") + ("onclick" "tab_editor()") + (text "Edit")) + (button + ("class" "button camo tab_button") + ("id" "preview_tab_button") + ("onclick" "tab_preview()") + (text "Preview"))) +(div + ("class" "card tab tabs") + (div + ("id" "editor_tab") + ("class" "tab fadein")) + (div + ("id" "preview_tab") + ("class" "tab fadein hidden"))) +(form + ("class" "w-full flex flex-col gap-2") + ("style" "margin-top: var(--pad-2)") + ("onsubmit" "edit_entry(event)") + (div + ("class" "flex gap-2") + (input + ("class" "w-full") + ("type" "text") + ("minlength" "2") + ("name" "edit_code") + ("required" "") + ("placeholder" "Enter edit code")) + (input + ("class" "w-full") + ("type" "text") + ("minlength" "2") + ("name" "new_edit_code") + ("placeholder" "New edit code")) + (input + ("class" "w-full") + ("type" "text") + ("minlength" "2") + ("name" "new_slug") + ("placeholder" "New url"))) + (div + ("class" "w-full flex justify-between gap-2") + (div + ("class" "flex gap-2") + (button + ("class" "button green") + (text "Save")) + (a + ("href" "/{{ entry.slug }}") + ("class" "button") + (text "Back"))) + + (button + ("class" "button red") + ("ui_ident" "delete") + (text "Delete")))) +(text "{{ components::footer() }}") + +; editor +(script ("src" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.js")) +(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/markdown/markdown.js")) +(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/display/placeholder.js")) +(link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.css")) +(script ("src" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js")) +(link ("rel" "stylesheet") ("href" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css")) + +(script ("id" "editor_content") ("type" "text/markdown") (text "{{ entry.content|remove_script_tags|safe }}")) + +(script + (text "setTimeout(() => { + globalThis.init_editor(); + }, 150); + + globalThis.edit_entry = (e) => { + e.preventDefault(); + const rm = e.submitter.getAttribute(\"ui_ident\") === \"delete\"; + + if (rm && !confirm(\"Are you sure you want to do this?\")) { + return; + } + + fetch(\"/api/v1/entries/{{ entry.slug }}\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + content: globalThis.editor.getValue(), + edit_code: e.target.edit_code.value, + new_slug: e.target.new_slug.value || undefined, + new_edit_code: e.target.new_edit_code.value || undefined, + \"delete\": rm, + }), + }) + .then(res => res.json()) + .then((res) => { + if (res.ok) { + if (!rm) { + document.cookie = `Atto-Message=\"Entry updated\"; path=/`; + document.cookie = \"Atto-Message-Good=true; path=/\"; + window.location.href = `/${res.payload}`; + } else { + document.cookie = `Atto-Message=\"Entry deleted\"; path=/`; + document.cookie = \"Atto-Message-Good=true; path=/\"; + window.location.href = \"/\"; + } + } else { + document.cookie = `Atto-Message=\"${res.message}\"; path=/`; + check_message(); + } + }) + }")) +(text "{% endblock %}") diff --git a/app/templates_src/error.lisp b/app/templates_src/error.lisp new file mode 100644 index 0000000..1a02116 --- /dev/null +++ b/app/templates_src/error.lisp @@ -0,0 +1,9 @@ +(text "{% extends \"root.lisp\" %} {% block head %}") +(title + (text "Error - {{ name }}")) +(text "{% endblock %} {% block body %}") +(div + ("class" "card") + (p (text "{{ error }}"))) +(text "{{ components::footer() }}") +(text "{% endblock %}") diff --git a/app/templates_src/index.lisp b/app/templates_src/index.lisp new file mode 100644 index 0000000..6948f83 --- /dev/null +++ b/app/templates_src/index.lisp @@ -0,0 +1,87 @@ +(text "{% extends \"root.lisp\" %} {% block head %}") +(title + (text "{{ name }}")) +(text "{% endblock %} {% block body %}") +(div + ("class" "flex items-center bar") + (button + ("class" "button tab_button") + ("id" "editor_tab_button") + ("onclick" "tab_editor()") + (text "Edit")) + (button + ("class" "button camo tab_button") + ("id" "preview_tab_button") + ("onclick" "tab_preview()") + (text "Preview"))) +(div + ("class" "card tab tabs") + (div + ("id" "editor_tab") + ("class" "tab fadein")) + (div + ("id" "preview_tab") + ("class" "tab fadein hidden"))) +(form + ("class" "w-full flex justify-between gap-2 flex-collapse-rev") + ("style" "margin-top: var(--pad-2)") + ("onsubmit" "create_entry(event)") + (button + ("class" "button") + (text "Go")) + (div + ("class" "flex gap-2") + (input + ("class" "w-full") + ("type" "text") + ("minlength" "2") + ("name" "edit_code") + ("placeholder" "Custom edit code")) + (input + ("class" "w-full") + ("type" "text") + ("minlength" "2") + ("maxlength" "32") + ("name" "slug") + ("placeholder" "Custom url")))) +(text "{{ components::footer() }}") + +; editor +(script ("src" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.js")) +(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/markdown/markdown.js")) +(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/display/placeholder.js")) +(link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.css")) +(script ("src" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js")) +(link ("rel" "stylesheet") ("href" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css")) + +(script + (text "setTimeout(() => { + globalThis.init_editor(); + }, 150); + + globalThis.create_entry = (e) => { + e.preventDefault(); + fetch(\"/api/v1/entries\", { + method: \"POST\", + headers: { + \"Content-Type\": \"application/json\", + }, + body: JSON.stringify({ + content: globalThis.editor.getValue(), + slug: e.target.slug.value || undefined, + edit_code: e.target.edit_code.value || undefined, + }), + }) + .then(res => res.json()) + .then((res) => { + if (res.ok) { + document.cookie = `Atto-Message=\"Entry created! Your edit code: ${res.payload[1]}.\"; path=/`; + document.cookie = \"Atto-Message-Good=true; path=/\"; + window.location.href = `/${res.payload[0]}`; + } else { + document.cookie = `Atto-Message=\"${res.message}\"; path=/`; + check_message(); + } + }) + }")) +(text "{% endblock %}") diff --git a/app/templates_src/root.lisp b/app/templates_src/root.lisp new file mode 100644 index 0000000..48d5143 --- /dev/null +++ b/app/templates_src/root.lisp @@ -0,0 +1,28 @@ +(text "{%- import \"components.lisp\" as components -%}") +(text "") +(html + ("lang" "en") + (head + (meta ("charset" "UTF-8")) + (meta ("name" "viewport") ("content" "width=device-width, initial-scale=1.0")) + (meta ("http-equiv" "X-UA-Compatible") ("content" "ie=edge")) + + (link ("rel" "icon") ("href" "/public/favicon.svg")) + (link ("rel" "stylesheet") ("href" "{{ tetratto }}/css/utility.css")) + (link ("rel" "stylesheet") ("href" "/public/style.css")) + + (meta ("name" "theme-color") ("content" "#fbc27f")) + (meta ("name" "description") ("content" "{{ name }}")) + (meta ("property" "og:type") ("content" "website")) + (meta ("property" "og:site_name") ("content" "{{ name }}")) + + (script ("src" "/public/app.js") ("defer")) + + (text "{% block head %}{% endblock %}")) + + (body + (article + ("class" "content_container flex flex-col") + ("id" "page") + (ul ("id" "messages")) + (text "{% block body %}{% endblock %}")))) diff --git a/app/templates_src/view.lisp b/app/templates_src/view.lisp new file mode 100644 index 0000000..c4b1e5f --- /dev/null +++ b/app/templates_src/view.lisp @@ -0,0 +1,29 @@ +(text "{% extends \"root.lisp\" %} {% block head %}") +(title + (text "{{ entry.slug }}")) +(text "{% endblock %} {% block body %}") +(div + ("class" "flex flex-col gap-2") + (div + ("class" "card") + ("style" "min-height: 15rem") + (text "{{ entry.content|markdown|safe }}")) + (div + ("class" "w-full flex justify-between gap-2") + (a + ("class" "button") + ("href" "/{{ entry.slug }}/edit") + (text "Edit")) + + (div + ("class" "flex flex-col gap-1 items-end fade") + (span (text "Pub: {{ entry.created / 1000|int|date(format=\"%Y-%m-%d %H:%M\", timezone=\"Etc/UTC\") }} UTC")) + (span (text "Edit: {{ entry.edited / 1000|int|date(format=\"%Y-%m-%d %H:%M\", timezone=\"Etc/UTC\") }} UTC")) + (span (text "Views: {{ views }}"))))) + +(link ("rel" "stylesheet") ("href" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css")) +(script ("src" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js")) +(script (text "hljs.highlightAll();")) + +(text "{{ components::footer() }}") +(text "{% endblock %}")