(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 }}")) (style (text "html, body { overflow: hidden auto !important; } .sidebar { position: sticky; top: 42px; } @media screen and (max-width: 900px) { .sidebar { position: absolute; top: unset; } body.sidebars_shown { overflow: hidden !important; } }")) (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 flex-col 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\") }}")) (text "{% if (view_mode and owner) or not view_mode -%}") (a ("class" "flush") ("href" "{% if view_mode -%} /@{{ owner.username }}/{{ journal.title }} {%- else -%} /journals/{{ journal.id }}/0 {%- endif %}") (b (text "{{ journal.title }}"))) (text "{%- endif %}") (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\" }}"))))))) ; users should also be able to manage the journal's sub directories here (details ("class" "w-full") (summary ("class" "button lowered w-full justify-start") (icon (text "folders")) (str (text "journals:label.directories"))) (div ("class" "card flex flex-col gap-2 lowered") (text "{{ components::directories_editor(dirs=journal.dirs) }}"))) (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 "{% if tag|length > 0 -%}") (a ("href" "?") ("class" "notification chip w-content") (text "{{ tag }}")) (text "{%- endif %}") (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"))) (a ("href" "#/tags") ("data-tab-button" "tags") ("data-turbo" "false") ("class" "hidden") (str (text "journals:action.edit_tags")))) (text "{%- endif %}") ; tabs (text "{{ components::note_tags(note=note) }}") (div ("data-tab" "editor") (div ("class" "flex flex-col gap-2 card") ("style" "animation: fadein ease-in-out 1 0.5s forwards running") ("id" "editor_tab")) (button ("onclick" "change_note_content('{{ note.id }}')") (icon (text "check")) (str (text "general:action.save")))) (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")) (div ("data-tab" "tags") ("class" "flex flex-col gap-2 card hidden") ("style" "animation: fadein ease-in-out 1 0.5s forwards running") (form ("onsubmit" "save_tags(event)") ("class" "flex flex-col gap-1") (label ("for" "tags") (str (text "journals:action.tags")) (textarea ("type" "text") ("name" "tags") ("id" "tags") ("placeholder" "tags") ("required" "") ("minlength" "2") ("maxlength" "128") (text "{% for tag in note.tags -%} {{ tag }}, {% endfor %}")) (span ("class" "fade") (text "Tags should be separated by a comma."))) (button (icon (text "check")) (str (text "general:action.save")))) (script (text "globalThis.save_tags = (e) => { event.preventDefault(); fetch(\"/api/v1/notes/{{ selected_note }}/tags\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ tags: e.target.tags.value.split(\",\").map(t => t.trim()).filter(t => t), }), }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); }); }"))) ; init codemirror (script ("id" "editor_content") ("type" "text/markdown") (text "{{ note.content|remove_script_tags|safe }}")) (script (text "setTimeout(async () => { if (!document.getElementById(\"preview_tab\").shadowRoot) { document.getElementById(\"preview_tab\").attachShadow({ mode: \"open\" }); } document.getElementById(\"editor_tab\").innerHTML = \"\"; 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") (div ("class" "flex flex-col gap-2") (span (text "Last updated: ") (span ("class" "date") (text "{{ note.edited }}"))) (text "{{ components::note_tags(note=note) }}")) (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, ]); }); } globalThis.create_directory = async (parent) => { const name = await trigger(\"atto::prompt\", [\"Directory name:\"]); if (!name) { return; } fetch(\"/api/v1/journals/{{ selected_journal }}/dirs\", { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ name, parent, }), }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); if (res.ok) { window.location.reload(); } }); } globalThis.delete_directory = async (id) => { if ( !(await trigger(\"atto::confirm\", [ \"Are you sure you would like to do this? This will delete all notes within this directory.\", ])) ) { return; } fetch(\"/api/v1/journals/{{ selected_journal }}/dirs\", { method: \"DELETE\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ id, }), }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); if (res.ok) { fetch(`/api/v1/notes/{{ selected_journal }}/dir/${id}`, { method: \"DELETE\", }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); }); } }); } globalThis.move_note_dir = async (id, dir) => { fetch(`/api/v1/notes/${id}/dir`, { method: \"POST\", headers: { \"Content-Type\": \"application/json\", }, body: JSON.stringify({ dir, }), }) .then((res) => res.json()) .then((res) => { trigger(\"atto::toast\", [ res.ok ? \"success\" : \"error\", res.message, ]); if (res.ok) { window.location.reload(); } }); } // 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 "{% if journal -%}") ; note mover (dialog ("id" "note_mover_dialog") (div ("class" "inner flex flex-col gap-2") (p (text "Select a directory to move this note into:")) (text "{{ components::note_mover_dirs(dirs=journal.dirs) }}") (div ("class" "flex justify-between") (div) (div ("class" "flex gap-2") (button ("class" "bold red lowered") ("onclick" "document.getElementById('note_mover_dialog').close()") ("type" "button") (text "{{ icon \"x\" }} {{ text \"dialog:action.close\" }}")))))) (text "{%- endif %}") (text "{% endblock %}")