Skip to main content

max / makenotwork

13.1 KB · 245 lines History Blame Raw
1 <div class="tab-docs"><a href="/docs/profile">Docs: Profile &rarr;</a></div>
2
3 <div class="form-section">
4 <h2 class="subsection-title">Links</h2>
5 <p class="muted mb-4">Add links to your public profile.</p>
6
7 <div id="links-list">
8 {% for link in custom_links %}
9 <div class="link-row" data-id="{{ link.id }}">
10 <div class="link-order-buttons">
11 <button type="button" class="order-btn" onclick="moveLink(this, -1)" title="Move up">&#9650;</button>
12 <button type="button" class="order-btn" onclick="moveLink(this, 1)" title="Move down">&#9660;</button>
13 </div>
14 <input type="text" value="{{ link.title }}" placeholder="Title" name="title" disabled>
15 <input type="url" value="{{ link.url }}" placeholder="https://..." name="url" disabled>
16 <button class="btn-secondary link-edit-btn" onclick="editLink(this)">Edit</button>
17 <button class="btn-primary link-save-btn hidden" onclick="saveLink(this)">Save</button>
18 <button class="btn-secondary link-cancel-btn hidden" onclick="cancelLink(this)">Cancel</button>
19 <button class="btn-danger link-remove-btn"
20 hx-delete="/api/links/{{ link.id }}"
21 hx-target="closest .link-row"
22 hx-swap="outerHTML"
23 hx-confirm="Remove this link?">Remove</button>
24 </div>
25 {% endfor %}
26 </div>
27
28 <script>
29 function moveLink(btn, direction) {
30 const row = btn.closest('.link-row');
31 const list = document.getElementById('links-list');
32 const rows = Array.from(list.querySelectorAll('.link-row'));
33 const index = rows.indexOf(row);
34
35 if (direction === -1 && index > 0) {
36 list.insertBefore(row, rows[index - 1]);
37 } else if (direction === 1 && index < rows.length - 1) {
38 list.insertBefore(rows[index + 1], row);
39 } else {
40 return; // Already at boundary
41 }
42
43 // Send new order to server
44 const linkIds = Array.from(list.querySelectorAll('.link-row')).map(r => r.dataset.id);
45 fetch('/api/links/reorder', {
46 method: 'PUT',
47 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
48 body: JSON.stringify({ link_ids: linkIds })
49 }).catch(function() {});
50 }
51
52 function editLink(btn) {
53 var row = btn.closest('.link-row');
54 row.querySelectorAll('input').forEach(function(i) { i.disabled = false; });
55 row.querySelector('.link-edit-btn').classList.add('hidden');
56 row.querySelector('.link-remove-btn').classList.add('hidden');
57 row.querySelector('.link-save-btn').classList.remove('hidden');
58 row.querySelector('.link-cancel-btn').classList.remove('hidden');
59 row.dataset.origTitle = row.querySelector('[name=title]').value;
60 row.dataset.origUrl = row.querySelector('[name=url]').value;
61 }
62
63 function cancelLink(btn) {
64 var row = btn.closest('.link-row');
65 row.querySelector('[name=title]').value = row.dataset.origTitle;
66 row.querySelector('[name=url]').value = row.dataset.origUrl;
67 row.querySelectorAll('input').forEach(function(i) { i.disabled = true; });
68 row.querySelector('.link-edit-btn').classList.remove('hidden');
69 row.querySelector('.link-remove-btn').classList.remove('hidden');
70 row.querySelector('.link-save-btn').classList.add('hidden');
71 row.querySelector('.link-cancel-btn').classList.add('hidden');
72 }
73
74 function saveLink(btn) {
75 var row = btn.closest('.link-row');
76 var id = row.dataset.id;
77 fetch('/api/links/' + id, {
78 method: 'PUT',
79 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
80 body: JSON.stringify({
81 title: row.querySelector('[name=title]').value,
82 url: row.querySelector('[name=url]').value
83 })
84 }).then(function(res) {
85 if (!res.ok) throw new Error('Failed to save');
86 cancelLink(btn);
87 }).catch(function() {
88 var saveBtn = row.querySelector('.link-save-btn');
89 if (saveBtn) saveBtn.textContent = 'Save failed: retry';
90 });
91 }
92 </script>
93
94 <form hx-post="/api/links"
95 hx-target="#links-list"
96 hx-swap="beforeend"
97 hx-on::after-request="if(event.detail.successful) this.reset()">
98 <div class="field-row">
99 <div class="form-group is-grow">
100 <label for="link-title">Title</label>
101 <input type="text" id="link-title" name="title" placeholder="My Website" required>
102 </div>
103 <div class="form-group is-grow-2">
104 <label for="link-url">URL</label>
105 <input type="url" id="link-url" name="url" placeholder="https://..." required>
106 </div>
107 <button class="btn-secondary" type="submit">Add Link</button>
108 </div>
109 </form>
110 </div>
111
112 <div class="form-section">
113 <h2 class="subsection-title">Profile</h2>
114 <p class="muted mb-4">Your public page: <a href="/u/{{ user.username }}">makenot.work/u/{{ user.username }}</a></p>
115
116 <form hx-put="/api/users/me"
117 hx-target="#profile-save-status"
118 hx-swap="innerHTML"
119 hx-indicator="#profile-spinner">
120 <div class="form-group">
121 <label for="display-name">Display Name</label>
122 <input type="text" id="display-name" name="display_name" value="{{ user.display_name_or_username() }}">
123 <div class="hint">Defaults to your username if left empty</div>
124 </div>
125
126 <div class="form-group">
127 <label for="bio">Bio</label>
128 <textarea id="bio" name="bio">{{ user.bio.as_deref().unwrap_or("") }}</textarea>
129 </div>
130
131 <button class="btn-primary" type="submit">
132 Save Changes
133 <span id="profile-spinner" class="htmx-indicator"> ...</span>
134 </button>
135 <span id="profile-save-status"></span>
136 </form>
137 </div>
138
139 {% if can_create_projects %}
140 <details class="form-section">
141 <summary><h2 class="subsection-title">Custom Domain</h2></summary>
142 <p class="muted mb-4">Point your own domain at your makenot.work profile.</p>
143
144 {% match custom_domain %}
145 {% when Some with (cd) %}
146 <div class="profile-domain-header">
147 <strong>{{ cd.domain }}</strong>
148 {% if cd.verified %}
149 <span class="profile-domain-status is-verified">Verified</span>
150 {% else %}
151 <span class="profile-domain-status is-pending">Pending verification</span>
152 {% endif %}
153 </div>
154
155 {% if !cd.verified %}
156 <div class="callout callout--warning">
157 <p class="m-0 mb-3"><strong>DNS setup required:</strong> add two records at your domain registrar, then click Check DNS.</p>
158
159 <p class="m-0 mb-2"><strong>1. Point your domain</strong> (routes visitors to your page)</p>
160 <div class="dns-row">
161 <span class="dns-row-label">CNAME:</span>
162 <code id="dns-route-host" class="dns-row-value">{{ cd.domain }}</code>
163 <button type="button" class="btn-secondary dns-row-copy"
164 onclick="navigator.clipboard.writeText(document.getElementById('dns-route-host').textContent).then(() => { this.textContent='Copied'; setTimeout(() => this.textContent='Copy', 1500) })">Copy</button>
165 </div>
166 <div class="dns-row">
167 <span class="dns-row-label">Value:</span>
168 <code id="dns-route-value" class="dns-row-value">connect.makenot.work</code>
169 <button type="button" class="btn-secondary dns-row-copy"
170 onclick="navigator.clipboard.writeText(document.getElementById('dns-route-value').textContent).then(() => { this.textContent='Copied'; setTimeout(() => this.textContent='Copy', 1500) })">Copy</button>
171 </div>
172 <p class="callout-hint mb-3">On Cloudflare, set this record to <strong>DNS only</strong> (grey cloud), not Proxied. For an apex/root domain, use CNAME flattening or ALIAS.</p>
173
174 <p class="m-0 mb-2"><strong>2. Verify ownership</strong></p>
175 <div class="dns-row">
176 <span class="dns-row-label">TXT:</span>
177 <code id="dns-record" class="dns-row-value">_mnw-verify.{{ cd.domain }}</code>
178 <button type="button" class="btn-secondary dns-row-copy"
179 onclick="navigator.clipboard.writeText(document.getElementById('dns-record').textContent).then(() => { this.textContent='Copied'; setTimeout(() => this.textContent='Copy', 1500) })">Copy</button>
180 </div>
181 <div class="dns-row">
182 <span class="dns-row-label">Value:</span>
183 <code id="dns-value" class="dns-row-value">{{ cd.verification_token }}</code>
184 <button type="button" class="btn-secondary dns-row-copy"
185 onclick="navigator.clipboard.writeText(document.getElementById('dns-value').textContent).then(() => { this.textContent='Copied'; setTimeout(() => this.textContent='Copy', 1500) })">Copy</button>
186 </div>
187 <p class="callout-hint">DNS changes can take up to 24 hours to propagate. <a href="/docs/guide/custom-domains">Setup guide</a></p>
188 </div>
189 <div class="profile-domain-actions">
190 <button class="btn-primary"
191 hx-post="/api/domains/verify"
192 hx-vals='{"domain_id": "{{ cd.id }}"}'
193 hx-target="#domain-verify-result"
194 hx-swap="innerHTML"
195 hx-on::after-request="if(event.detail.successful && event.detail.xhr.responseText.indexOf('verified successfully')!==-1) { var t=document.getElementById('settings-body'); if(t) htmx.ajax('GET','/dashboard/tabs/profile',{target:t,swap:'innerHTML'}); else { var b=document.getElementById('tab-profile'); if(b) b.click(); } }">Check DNS</button>
196 <button class="btn-danger"
197 hx-delete="/api/domains/{{ cd.id }}"
198 hx-confirm="Remove this domain?"
199 hx-target="closest .form-section"
200 hx-swap="outerHTML"
201 hx-on::after-request="if(event.detail.successful) { var t=document.getElementById('settings-body'); if(t) htmx.ajax('GET','/dashboard/tabs/profile',{target:t,swap:'innerHTML'}); else { var b=document.getElementById('tab-profile'); if(b) b.click(); } }">Remove</button>
202 </div>
203 <div id="domain-verify-result" class="profile-domain-result"></div>
204 {% else %}
205 <p class="profile-domain-lead">
206 Visitors to <strong>{{ cd.domain }}</strong> will see your profile, projects, and items.
207 </p>
208 <button class="btn-danger"
209 hx-delete="/api/domains/{{ cd.id }}"
210 hx-confirm="Remove this domain? Your domain will stop serving your content."
211 hx-target="closest .form-section"
212 hx-swap="outerHTML"
213 hx-on::after-request="if(event.detail.successful) { var t=document.getElementById('settings-body'); if(t) htmx.ajax('GET','/dashboard/tabs/profile',{target:t,swap:'innerHTML'}); else { var b=document.getElementById('tab-profile'); if(b) b.click(); } }">Remove Domain</button>
214 {% endif %}
215 {% when None %}
216 <form hx-post="/api/domains"
217 hx-target="#domain-add-result"
218 hx-swap="innerHTML"
219 hx-on::after-request="if(event.detail.successful) { var t=document.getElementById('settings-body'); if(t) htmx.ajax('GET','/dashboard/tabs/profile',{target:t,swap:'innerHTML'}); else { var b=document.getElementById('tab-profile'); if(b) b.click(); } }">
220 <div class="field-row">
221 <div class="form-group is-grow">
222 <label for="custom-domain">Domain</label>
223 <input type="text" id="custom-domain" name="domain" placeholder="mysite.com" required>
224 </div>
225 <button class="btn-primary" type="submit">Add Domain</button>
226 </div>
227 </form>
228 <div id="domain-add-result" class="profile-domain-add-result"></div>
229 {% endmatch %}
230 </details>
231 {% endif %}
232
233 <details class="form-section">
234 <summary><h2 class="subsection-title">Personal Feed</h2></summary>
235 <p class="muted profile-feed-lead">An RSS feed of new content from creators and projects you follow. Add this URL to your RSS reader.</p>
236 <div class="profile-feed-row" id="feed-url-row">
237 <input type="text" id="feed-url" value="{{ feed_url }}" readonly class="profile-feed-input">
238 <button class="btn-secondary nowrap" type="button"
239 onclick="navigator.clipboard.writeText(document.getElementById('feed-url').value).then(() => { this.textContent='Copied!'; setTimeout(() => this.textContent='Copy URL', 2000) })">Copy URL</button>
240 <button class="btn-secondary nowrap" type="button"
241 hx-post="/dashboard/feed/regenerate" hx-target="#feed-url-row" hx-swap="outerHTML">Regenerate</button>
242 </div>
243 <p class="muted profile-feed-lead">Regenerating issues a new URL and immediately stops the old one from working — use it if your feed link leaks.</p>
244 </details>
245