Skip to main content

max / makenotwork

11.4 KB · 225 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 a TXT record at your domain registrar.</p>
158 <div class="dns-row">
159 <span class="dns-row-label">Record:</span>
160 <code id="dns-record" class="dns-row-value">_mnw-verify.{{ cd.domain }}</code>
161 <button type="button" class="btn-secondary dns-row-copy"
162 onclick="navigator.clipboard.writeText(document.getElementById('dns-record').textContent).then(() => { this.textContent='Copied'; setTimeout(() => this.textContent='Copy', 1500) })">Copy</button>
163 </div>
164 <div class="dns-row">
165 <span class="dns-row-label">Value:</span>
166 <code id="dns-value" class="dns-row-value">{{ cd.verification_token }}</code>
167 <button type="button" class="btn-secondary dns-row-copy"
168 onclick="navigator.clipboard.writeText(document.getElementById('dns-value').textContent).then(() => { this.textContent='Copied'; setTimeout(() => this.textContent='Copy', 1500) })">Copy</button>
169 </div>
170 <p class="callout-hint">DNS changes can take up to 24 hours to propagate. <a href="/docs/guide/custom-domains">Setup guide</a></p>
171 </div>
172 <div class="profile-domain-actions">
173 <button class="btn-primary"
174 hx-post="/api/domains/verify"
175 hx-vals='{"domain_id": "{{ cd.id }}"}'
176 hx-target="#domain-verify-result"
177 hx-swap="innerHTML"
178 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>
179 <button class="btn-danger"
180 hx-delete="/api/domains/{{ cd.id }}"
181 hx-confirm="Remove this domain?"
182 hx-target="closest .form-section"
183 hx-swap="outerHTML"
184 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>
185 </div>
186 <div id="domain-verify-result" class="profile-domain-result"></div>
187 {% else %}
188 <p class="profile-domain-lead">
189 Visitors to <strong>{{ cd.domain }}</strong> will see your profile, projects, and items.
190 </p>
191 <button class="btn-danger"
192 hx-delete="/api/domains/{{ cd.id }}"
193 hx-confirm="Remove this domain? Your domain will stop serving your content."
194 hx-target="closest .form-section"
195 hx-swap="outerHTML"
196 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>
197 {% endif %}
198 {% when None %}
199 <form hx-post="/api/domains"
200 hx-target="#domain-add-result"
201 hx-swap="innerHTML"
202 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(); } }">
203 <div class="field-row">
204 <div class="form-group is-grow">
205 <label for="custom-domain">Domain</label>
206 <input type="text" id="custom-domain" name="domain" placeholder="mysite.com" required>
207 </div>
208 <button class="btn-primary" type="submit">Add Domain</button>
209 </div>
210 </form>
211 <div id="domain-add-result" class="profile-domain-add-result"></div>
212 {% endmatch %}
213 </details>
214 {% endif %}
215
216 <details class="form-section">
217 <summary><h2 class="subsection-title">Personal Feed</h2></summary>
218 <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>
219 <div class="profile-feed-row">
220 <input type="text" id="feed-url" value="{{ feed_url }}" readonly class="profile-feed-input">
221 <button class="btn-secondary nowrap" type="button"
222 onclick="navigator.clipboard.writeText(document.getElementById('feed-url').value).then(() => { this.textContent='Copied!'; setTimeout(() => this.textContent='Copy URL', 2000) })">Copy URL</button>
223 </div>
224 </details>
225