decentralized and customizable links page on top of atproto

improve htmx and styles

+11 -2
src/main.py
··· 3 3 4 4 from aiohttp.client import ClientSession 5 5 from flask import Flask, g, session, redirect, render_template, request, url_for 6 - from flask_htmx import HTMX 6 + from flask_htmx import HTMX, make_response as htmx_reponse 7 7 from typing import Any 8 8 9 9 from .atproto import ( ··· 167 167 kv.set(user.did, json.dumps(record)) 168 168 169 169 if htmx: 170 - return render_template("_editor_profile.html", profile=record) 170 + return htmx_reponse( 171 + render_template("_editor_profile.html", profile=record), 172 + reswap="outerHTML", 173 + ) 171 174 172 175 return redirect(url_for("page_editor"), 303) 173 176 ··· 212 215 if success: 213 216 kv = KV(app, app.logger, "links_from_did") 214 217 kv.set(user.did, json.dumps(record)) 218 + 219 + if htmx: 220 + return htmx_reponse( 221 + render_template("_editor_links.html", links=record["links"]), 222 + reswap="outerHTML", 223 + ) 215 224 216 225 return redirect(url_for("page_editor"), 303) 217 226
+28 -9
src/static/style.css
··· 5 5 --color-background-secondary: #eee; 6 6 --color-border: #bbb; 7 7 --color-border-secondary: #ddd; 8 + --color-success: #080; 8 9 } 9 10 10 11 @media (prefers-color-scheme: dark) { ··· 12 13 --color-background-secondary: #333; 13 14 --color-border: #555; 14 15 --color-border-secondary: #333; 16 + --color-success: #af2; 15 17 } 16 18 } 17 19 ··· 69 71 padding: 0.25em; 70 72 } 71 73 72 - input[type="submit"]:last-child { 74 + button.submit:last-of-type, 75 + input[type="submit"]:last-of-type { 73 76 margin-top: 1em; 74 77 } 75 78 76 - label:has(input) { 79 + editor-label { 77 80 display: block; 78 - margin-top: 0.25em; 79 81 } 80 82 81 - label:has(input):has(span) input { 82 - display: block; 83 + editor-label + editor-label { 84 + margin-top: 0.25em; 83 85 } 84 86 85 - label:has(input):has(span) span { 87 + editor-label .label { 86 88 font-size: 14px; 87 89 font-weight: 700; 88 90 text-transform: uppercase; ··· 166 168 } 167 169 168 170 .htmx-indicator { 169 - background: yellowgreen; 170 - height: 2px; 171 + --height: 2px; 172 + height: var(--height); 173 + margin: 2px 0; 174 + opacity: 0; 175 + visibility: none; 171 176 } 172 177 173 - .htmx-request .htmx-indicator { 178 + .htmx-request .progress { 179 + animation: progress-bar 0.5s linear; 180 + animation-fill-mode: both; 181 + background: var(--color-success); 182 + border-radius: calc(var(--height) / 2); 174 183 display: block; 184 + height: var(--height); 185 + } 186 + 187 + @keyframes progress-bar { 188 + 0% { 189 + width: 25%; 190 + } 191 + 100% { 192 + width: 100%; 193 + } 175 194 } 176 195 177 196 form[hx-post] {
+81
src/templates/_editor_links.html
··· 1 + <div id="editor-links-container" x-data="{ links: {{ links }}, linksChanged: false }"> 2 + <div> 3 + <h2 style="display: inline-block;">links</h2> 4 + <template x-if="linksChanged"> 5 + <span class="alert">You have unsaved changes!</span> 6 + </template> 7 + </div> 8 + 9 + <form 10 + method="post" 11 + action="/editor/links" 12 + hx-post="/editor/links" 13 + hx-target="#editor-links-container" 14 + @change="linksChanged = true" 15 + > 16 + <input type="submit" value="save links" /> 17 + {% include "_htmx_indicator.html" %} 18 + 19 + <div x-sort x-sort:config="{ handle: '[x-sort\\:handle]' }"> 20 + <template x-for="(link, index) in links"> 21 + <link-editor-item x-data="{ editing: !link.href }" x-sort:item="link.href"> 22 + <link-editor-header> 23 + <div class="static link-item" :style="'color: ' + link.backgroundColor"> 24 + <span class="link-item-title" x-text="link.title"></span> 25 + <span class="link-item-detail" x-show="link.subtitle" x-text="link.subtitle"></span> 26 + </div> 27 + <link-editor-drag-handle x-sort:handle>⠿</link-editor-drag-handle> 28 + </link-editor-header> 29 + <div x-show="!editing"> 30 + <link-editor-buttons> 31 + <button type="button" @click="editing = true">edit</button> 32 + <button type="button" @click="if (confirm('delete ' + link.title + '?')) links.splice(index, 1)">delete</button> 33 + </link-editor-buttons> 34 + </div> 35 + <div x-show="editing"> 36 + <editor-label> 37 + <label> 38 + <span class="label">URL</span> 39 + <input type="text" name="link-href" x-model="link.href" required /> 40 + </label> 41 + </editor-label> 42 + <editor-label> 43 + <label> 44 + <span class="label">Title</span> 45 + <input type="text" name="link-title" x-model="link.title" required /> 46 + </label> 47 + </editor-label> 48 + <editor-label> 49 + <label> 50 + <span class="label">Subtitle</span> 51 + <input type="text" name="link-subtitle" x-model="link.subtitle" /> 52 + </label> 53 + </editor-label> 54 + <editor-label> 55 + <label> 56 + <span class="label">Background color</span> 57 + <input type="color" name="link-background-color" x-model="link.backgroundColor" required /> 58 + </label> 59 + </editor-label> 60 + <button type="button" class="submit" @click="editing = false">close</button> 61 + </div> 62 + </link-editor-item> 63 + </template> 64 + </div> 65 + 66 + <template x-if="links.length === 0"> 67 + <p> 68 + Nothing here! Add your first link, then the second, then... 69 + You can always delete and sort them. 70 + </p> 71 + </template> 72 + 73 + <button type="button" @click="links.push({ backgroundColor: '#A1D87E' })" style="display: block; margin-top: 1em;"> 74 + add link 75 + </button> 76 + 77 + <input type="submit" value="save links" /> 78 + {% include "_htmx_indicator.html" %} 79 + </form> 80 + <!-- #editor-links-container --> 81 + </div>
+16 -14
src/templates/_editor_profile.html
··· 1 - <form action="/editor/profile" method="post" hx-post="/editor/profile" hx-swap="outerHTML"> 2 - <label> 3 - <span>Display name</span> 4 - <input type="text" name="displayName" value="{{ profile.displayName }}" required /> 5 - </label> 6 - <label> 7 - <span>Description</span> 8 - <input type="text" name="description" value="{{ profile.description }}" /> 9 - </label> 1 + <form method="post" action="/editor/profile" hx-post="/editor/profile"> 2 + <editor-label> 3 + <label> 4 + <span class="label">Display name</span> 5 + <input type="text" name="displayName" value="{{ profile.displayName }}" required /> 6 + </label> 7 + </editor-label> 8 + <editor-label> 9 + <label> 10 + <span class="label">Description</span> 11 + <input type="text" name="description" value="{{ profile.description }}" /> 12 + </label> 13 + </editor-label> 10 14 {% if profile_from_bluesky %} 11 15 <p> 12 16 <span class="faded caption">Profile was fetched from Bluesky. On save it will use an independent, ligo.at only copy.</span> 13 17 </p> 14 18 {% endif %} 15 - <label> 16 - <input type="submit" value="save profile"> 17 - </label> 18 - <div class="htmx-indicator"></div> 19 + <input type="submit" value="save profile"> 20 + {% include "_htmx_indicator.html" %} 21 + <!-- /editor/profile --> 19 22 </form> 20 - <!-- /editor/profile -->
+3
src/templates/_htmx_indicator.html
··· 1 + <div class="htmx-indicator"> 2 + <span class="progress"></span> 3 + </div>
+2 -64
src/templates/editor.html
··· 15 15 <script defer src="{{ url_for('static', filename='alpine.3.15.0.min.js') }}"></script> 16 16 </head> 17 17 <body> 18 - <div class="wrapper editor" x-data="{ links: {{ links }}, linksChanged: false }"> 18 + <div class="wrapper editor"> 19 19 <header> 20 20 <h1>ligo.at</h1> 21 21 <span class="tagline">edit your profile & links</span> ··· 36 36 <h2>profile</h2> 37 37 {% include "_editor_profile.html" %} 38 38 39 - <div> 40 - <h2 style="display: inline-block;">links</h2> 41 - <template x-if="linksChanged"> 42 - <span class="alert">You have unsaved changes!</span> 43 - </template> 44 - </div> 45 - 46 39 <noscript> 47 40 JavaScript is needed for a better experience configuring the links. 48 41 </noscript> 49 42 50 - <form action="/editor/links" method="post" @change="linksChanged = true"> 51 - <input type="submit" value="save links" /> 52 - 53 - <div x-sort x-sort:config="{ handle: '[x-sort\\:handle]' }"> 54 - <template x-for="(link, index) in links"> 55 - <link-editor-item x-data="{ editing: !link.href }" x-sort:item="link.href"> 56 - <link-editor-header> 57 - <div class="static link-item" :style="'color: ' + link.backgroundColor"> 58 - <span class="link-item-title" x-text="link.title"></span> 59 - <span class="link-item-detail" x-show="link.subtitle" x-text="link.subtitle"></span> 60 - </div> 61 - <link-editor-drag-handle x-sort:handle>⠿</link-editor-drag-handle> 62 - </link-editor-header> 63 - <div x-show="!editing"> 64 - <link-editor-buttons> 65 - <button type="button" @click="editing = true">edit</button> 66 - <button type="button" @click="if (confirm('delete ' + link.title + '?')) links.splice(index, 1)">delete</button> 67 - </link-editor-buttons> 68 - </div> 69 - <div x-show="editing"> 70 - <label> 71 - <span>URL</span> 72 - <input type="text" name="link-href" x-model="link.href" required /> 73 - </label> 74 - <label> 75 - <span>Title</span> 76 - <input type="text" name="link-title" x-model="link.title" required /> 77 - </label> 78 - <label> 79 - <span>Subtitle</span> 80 - <input type="text" name="link-subtitle" x-model="link.subtitle" /> 81 - </label> 82 - <label> 83 - <span>Background color</span> 84 - <input type="color" name="link-background-color" x-model="link.backgroundColor" required /> 85 - </label> 86 - <button type="button" @click="editing = false" style="margin-top: 1em;">close</button> 87 - </div> 88 - </link-editor-item> 89 - </template> 90 - </div> 91 - 92 - <template x-if="links.length === 0"> 93 - <p> 94 - Nothing here! Add your first link, then the second, then... 95 - You can always delete and sort them. 96 - </p> 97 - </template> 98 - 99 - <button type="button" @click="links.push({ backgroundColor: '#A1D87E' })" style="display: block; margin-top: 1em;"> 100 - add link 101 - </button> 102 - 103 - <input type="submit" value="save links" /> 104 - </form> 105 - <!-- /editor/links --> 43 + {% include "_editor_links.html" %} 106 44 107 45 <footer> 108 46 <p>