(text "{% extends \"root.html\" %} {% block head %}") (text "{% if journal -%}") (title (text "{{ journal.title }}")) (text "{% else %}") (title (text "Journals - {{ config.name }}")) (text "{%- endif %}") (link ("rel" "stylesheet") ("data-turbo-temporary" "true") ("href" "/css/chats.css?v=tetratto-{{ random_cache_breaker }}")) (text "{% if view_mode and journal and is_editor -%} {% if note -%}") ; redirect to note (meta ("http-equiv" "refresh") ("content" "0; url=/@{{ user.username }}/{{ journal.title }}/{{ note.title }}")) (text "{% else %}") ; redirect to journal homepage (meta ("http-equiv" "refresh") ("content" "0; url=/@{{ user.username }}/{{ journal.title }}")) (text "{%- endif %} {%- endif %}") (text "{% if view_mode and journal -%}") ; add journal css (link ("rel" "stylesheet") ("data-turbo-temporary" "true") ("href" "/api/v1/journals/{{ journal.id }}/journal.css?v=tetratto-{{ random_cache_breaker }}")) (text "{%- endif %}") (text "{% endblock %} {% block body %} {{ macros::nav(selected=\"journals\") }}") (text "{% if not view_mode -%}") (nav ("class" "chats_nav") (button ("class" "flex gap-2 items-center active") ("onclick" "toggle_sidebars(event)") (text "{{ icon \"panel-left\" }} {% if community -%}") (b ("class" "name shorter") (text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}")) (text "{% else %}") (b (text "{{ text \"journals:label.my_journals\" }}")) (text "{%- endif %}"))) (text "{%- endif %}") (div ("class" "flex") ; journals/notes listing (text "{% if not view_mode -%}") ; this isn't shown if we're in view mode (div ("class" "sidebar flex flex-col gap-2 justify-between") ("id" "notes_list") (div ("class" "flex flex-col gap-2 w-full") (button ("class" "lowered justify-start w-full") ("onclick" "create_journal()") (icon (text "plus")) (str (text "journals:action.create_journal"))) (text "{% for journal in journals %}") (text "{{ components::journal_listing(journal=journal, notes=notes, selected_note=selected_note, selected_journal=selected_journal, view_mode=view_mode) }}") (text "{% endfor %}"))) (text "{%- endif %}") ; editor (div ("class" "w-full padded_section") ("id" "editor") ("style" "padding: var(--pad-4)") (main ("class" "flex flex-col gap-2") ; the journal/note header is always shown (text "{% if journal -%}") (div ("class" "mobile_nav w-full flex items-center justify-between gap-2") (div ("class" "flex gap-2 items-center") (a ("class" "flex items-center") ("href" "/api/v1/auth/user/find/{{ journal.owner }}") (text "{{ components::avatar(username=journal.owner, selector_type=\"id\", size=\"18px\") }}")) (a ("class" "flush") ("href" "{% if view_mode -%} /@{{ owner.username }}/{{ journal.title }} {%- else -%} /journals/{{ journal.id }}/0 {%- endif %}") (b (text "{{ journal.title }}"))) (text "{% if note -%}") (span (text "/")) (b (text "{{ note.title }}")) (text "{%- endif %}")) (text "{% if user and user.id == journal.owner and (not note or note.title != \"journal.css\") -%}") (div ("class" "pillmenu") (a ("class" "{% if not view_mode -%}active{%- endif %}") ("href" "/journals/{{ journal.id }}/{% if note -%} {{ note.id }} {%- else -%} 0 {%- endif %}") ("data-turbo" "false") (icon (text "pencil"))) (a ("class" "{% if view_mode -%}active{%- endif %}") ("href" "/@{{ user.username }}/{{ journal.title }}{% if note -%} /{{ note.title }} {%- endif %}") (icon (text "eye")))) (text "{%- endif %}")) (text "{%- endif %}") ; we're going to put some help panes in here if something is 0 ; ...people got confused on the chats page because they somehow couldn't figure out how to open the sidebar (text "{% if selected_journal == 0 -%}") ; no journal selected (div ("class" "card w-full flex flex-col gap-2") (h3 (str (text "journals:label.welcome"))) (span (str (text "journals:label.select_a_journal"))) (span ("class" "mobile") (str (text "journals:label.mobile_click_my_journals"))) (button ("onclick" "create_journal()") (icon (text "plus")) (str (text "journals:action.create_journal")))) (text "{% elif selected_note == 0 -%}") ; journal selected, but no note is selected (text "{% if not view_mode -%}") ; we're the journal owner and we're not in view mode (div ("class" "card w-full flex flex-col gap-2") (h3 (text "{{ journal.title }}")) (span (str (text "journals:label.select_a_note"))) (button ("onclick" "create_note()") (icon (text "plus")) (str (text "journals:action.create_note")))) ; we'll also let users edit the journal's settings here i guess (details ("class" "w-full") (summary ("class" "button lowered w-full justify-start") (icon (text "settings")) (str (text "general:action.manage"))) (div ("class" "card flex flex-col gap-2 lowered") (div ("class" "card-nest") (div ("class" "card small") (b (text "Privacy"))) (div ("class" "card") (select ("onchange" "change_journal_privacy(event)") (option ("value" "Private") ("selected" "{% if journal.privacy == 'Private' -%}true{% else %}false{%- endif %}") (text "Private")) (option ("value" "Public") ("selected" "{% if journal.privacy == 'Public' -%}true{% else %}false{%- endif %}") (text "Public"))))) (div ("class" "card-nest") (div ("class" "card small") (label ("for" "title") (b (str (text "communities:label.title"))))) (form ("class" "card flex flex-col gap-2") ("onsubmit" "change_journal_title(event)") (div ("class" "flex flex-col gap-1") (input ("type" "text") ("name" "title") ("id" "title") ("placeholder" "title") ("required" "") ("minlength" "2"))) (button ("class" "primary") (text "{{ icon \"check\" }}") (span (text "{{ text \"general:action.save\" }}"))))))) (text "{% else %}") ; we're in view mode; just show journal listing and notes as journal homepage (div ("class" "card flex flex-col gap-2") (text "{{ components::journal_listing(journal=journal, notes=notes, selected_note=selected_note, selected_journal=selected_journal, view_mode=view_mode, owner=owner) }}")) (text "{%- endif %}") (text "{% else %}") ; journal AND note selected (text "{% if not view_mode -%}") ; not view mode; show editor ; import codemirror (script ("src" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.js") ("data-turbo-temporary" "true")) (script ("src" "https://unpkg.com/codemirror@5.39.2/addon/display/placeholder.js") ("data-turbo-temporary" "true")) (link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.css") ("data-turbo-temporary" "true")) (text "{% if note.title == \"journal.css\" -%}") ; css editor (script ("src" "https://unpkg.com/codemirror@5.39.2/addon/edit/closebrackets.js") ("data-turbo-temporary" "true")) (script ("src" "https://unpkg.com/codemirror@5.39.2/addon/hint/css-hint.js") ("data-turbo-temporary" "true")) (script ("src" "https://unpkg.com/codemirror@5.39.2/addon/hint/show-hint.js") ("data-turbo-temporary" "true")) (script ("src" "https://unpkg.com/codemirror@5.39.2/addon/lint/css-lint.js") ("data-turbo-temporary" "true")) (script ("src" "https://unpkg.com/codemirror@5.39.2/mode/css/css.js") ("data-turbo-temporary" "true")) (link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/addon/hint/show-hint.css") ("data-turbo-temporary" "true")) (link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/addon/lint/lint.css") ("data-turbo-temporary" "true")) (style (text ".CodeMirror { font-family: monospace !important; font-size: 16px; border: solid 1px var(--color-super-lowered); border-radius: var(--radius); } .CodeMirror-line { padding-left: 5px !important; }")) (text "{% else %}") ; markdown editor (script ("src" "https://unpkg.com/codemirror@5.39.2/mode/markdown/markdown.js") ("data-turbo-temporary" "true")) (text "{%- endif %}") ; tab bar (text "{% if note.title != \"journal.css\" -%}") (div ("class" "pillmenu") (a ("href" "#/editor") ("data-tab-button" "editor") ("data-turbo" "false") ("class" "active") (str (text "journals:label.editor"))) (a ("href" "#/preview") ("data-tab-button" "preview") ("data-turbo" "false") (str (text "journals:label.preview_pane")))) (text "{%- endif %}") ; tabs (div ("data-tab" "editor") ("class" "flex flex-col gap-2 card") ("style" "animation: fadein ease-in-out 1 0.5s forwards running") ("id" "editor_tab")) (div ("data-tab" "preview") ("class" "flex flex-col gap-2 card hidden") ("style" "animation: fadein ease-in-out 1 0.5s forwards running") ("id" "preview_tab")) (button ("onclick" "change_note_content('{{ note.id }}')") (icon (text "check")) (str (text "general:action.save"))) ; init codemirror (script ("id" "editor_content") ("type" "text/markdown") (text "{{ note.content|remove_script_tags|safe }}")) (script (text "setTimeout(() => { if (!document.getElementById(\"preview_tab\").shadowRoot) { document.getElementById(\"preview_tab\").attachShadow({ mode: \"open\" }); } globalThis.editor = CodeMirror(document.getElementById(\"editor_tab\"), { value: document.getElementById(\"editor_content\").innerHTML, mode: \"{% if note.title == 'journal.css' -%} css {%- else -%} markdown {%- endif %}\", lineWrapping: true, lineNumbers: \"{{ note.title }}\" === \"journal.css\", autoCloseBrackets: true, autofocus: true, viewportMargin: Number.POSITIVE_INFINITY, inputStyle: \"contenteditable\", highlightFormatting: false, fencedCodeBlockHighlighting: false, xml: false, smartIndent: true, indentUnit: 4, placeholder: `# {{ note.title }}`, extraKeys: { Home: \"goLineLeft\", End: \"goLineRight\", Enter: (cm) => { cm.replaceSelection(\"\\n\"); }, }, }); editor.on(\"keydown\", (cm, e) => { if (e.key.length > 1) { // ignore all keys that aren't a letter return; } CodeMirror.showHint(cm, CodeMirror.hint.css); }); document.querySelector(\"[data-tab-button=editor]\").addEventListener(\"click\", async (e) => { e.preventDefault(); trigger(\"atto::hooks::tabs:switch\", [\"editor\"]); }); document.querySelector(\"[data-tab-button=preview]\").addEventListener(\"click\", async (e) => { e.preventDefault(); const res = await ( await fetch(\"/api/v1/notes/preview\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ content: globalThis.editor.getValue(), }), }) ).text(); const preview_token = window.crypto.randomUUID(); document.getElementById(\"preview_tab\").shadowRoot.innerHTML = `${res}`; trigger(\"atto::hooks::tabs:switch\", [\"preview\"]); }); }, 150);")) (text "{% else %}") ; we're just viewing this note (div ("class" "flex flex-col gap-2 card") (text "{{ note.content|markdown|safe }}")) (div ("class" "flex w-full justify-between gap-2") (span (text "Last updated: ") (span ("class" "date") (text "{{ note.edited }}"))) (text "{% if user and user.id == owner.id -%}") (button ("class" "small") ("onclick" "{% if journal.privacy == \"Public\" -%} trigger('atto::copy_text', ['{{ config.host }}/@{{ owner.username }}/{{ journal.title }}/{{ note.title }}']) {%- else -%} prompt_make_public(); trigger('atto::copy_text', ['{{ config.host }}/@{{ owner.username }}/{{ journal.title }}/{{ note.title }}']) {%- endif %}") (icon (text "share")) (str (text "general:label.share"))) (script (text "globalThis.prompt_make_public = async () => { if ( !(await trigger(\"atto::confirm\", [ \"Would you like to make this journal public? This is required for others to view this note.\", ])) ) { return; } change_journal_privacy({ target: { selectedOptions: [{ value: \"Public\" }] }, preventDefault: () => {} }); }")) (text "{%- endif %}")) (text "{%- endif %}") (text "{%- endif %}"))) (style (text "nav::after { width: 100%; left: 0; }")) (script (text "window.JOURNAL_PROPS = { selected_journal: \"{{ selected_journal }}\", selected_note: \"{{ selected_note }}\", }; // journals/notes globalThis.create_journal = async () => { const title = await trigger(\"atto::prompt\", [\"Journal title:\"]); if (!title) { return; } fetch(\"/api/v1/journals\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ title, }), }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); if (res.ok) { setTimeout(() => { window.location.href = `/journals/${res.payload}/0`; }, 100); } }); } globalThis.create_note = async () => { const title = await trigger(\"atto::prompt\", [\"Note title:\"]); if (!title) { return; } fetch(\"/api/v1/notes\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ title, content: title === \"journal.css\" ? `/* ${title} */\\n` : `# ${title}`, journal: \"{{ selected_journal }}\", }), }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); if (res.ok) { setTimeout(() => { window.location.href = `/journals/{{ selected_journal }}/${res.payload}`; }, 100); } }); } globalThis.delete_journal = async (id) => { if ( !(await trigger(\"atto::confirm\", [ \"Are you sure you would like to do this?\", ])) ) { return; } fetch(`/api/v1/journals/${id}`, { method: \"DELETE\", }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); if (res.ok) { setTimeout(() => { window.location.href = \"/journals\"; }, 100); } }); } globalThis.delete_note = async (id) => { if ( !(await trigger(\"atto::confirm\", [ \"Are you sure you would like to do this?\", ])) ) { return; } fetch(`/api/v1/notes/${id}`, { method: \"DELETE\", }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); if (res.ok) { setTimeout(() => { window.location.href = \"/journals/{{ selected_journal }}/0\"; }, 100); } }); } globalThis.change_journal_title = async (e) => { e.preventDefault(); fetch(\"/api/v1/journals/{{ selected_journal }}/title\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ title: e.target.title.value, }), }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); if (res.ok) { e.reset(); } }); } globalThis.change_journal_privacy = async (e) => { e.preventDefault(); const selected = e.target.selectedOptions[0]; fetch(\"/api/v1/journals/{% if journal -%} {{ journal.id }} {%- else -%} {{ selected_journal }} {%- endif %}/privacy\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ privacy: selected.value, }), }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); }); } globalThis.change_note_title = async (id) => { const title = await trigger(\"atto::prompt\", [\"New note title:\"]); if (!title) { return; } fetch(`/api/v1/notes/${id}/title`, { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ title, }), }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); if (res.ok) { e.reset(); } }); } globalThis.change_note_content = async (id) => { fetch(`/api/v1/notes/${id}/content`, { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ content: globalThis.editor.getValue(), }), }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); }); } // sidebars window.SIDEBARS_OPEN = false; if (new URLSearchParams(window.location.search).get(\"nav\") === \"true\") { window.SIDEBARS_OPEN = true; } if ( window.SIDEBARS_OPEN && !document.body.classList.contains(\"sidebars_shown\") ) { toggle_sidebars(); window.SIDEBARS_OPEN = true; } for (const anchor of document.querySelectorAll(\"[data-turbo=false]\")) { anchor.href += `?nav=${window.SIDEBARS_OPEN}`; } function toggle_sidebars() { window.SIDEBARS_OPEN = !window.SIDEBARS_OPEN; for (const anchor of document.querySelectorAll( \"[data-turbo=false]\", )) { anchor.href = anchor.href.replace( `?nav=${!window.SIDEBARS_OPEN}`, `?nav=${window.SIDEBARS_OPEN}`, ); } const notes_list = document.getElementById(\"notes_list\"); if (document.body.classList.contains(\"sidebars_shown\")) { // hide document.body.classList.remove(\"sidebars_shown\"); notes_list.style.left = \"-200%\"; } else { // show document.body.classList.add(\"sidebars_shown\"); notes_list.style.left = \"0\"; } }"))) (text "{% endblock %}")