Skip to content

Commit dd6ca05

Browse files
committed
Update styling
1 parent 5434ec0 commit dd6ca05

File tree

5 files changed

+270
-15
lines changed

5 files changed

+270
-15
lines changed

src/handlers/admin.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ async fn handle_forms_admin(
156156
.filter(|s| !s.is_empty())
157157
.map(String::from),
158158
instagram_sources: Vec::new(),
159+
archived: false,
159160
created_at: now.clone(),
160161
updated_at: now,
161162
};
@@ -231,6 +232,7 @@ async fn handle_forms_admin(
231232
existing.google_sheet_url
232233
},
233234
instagram_sources: existing.instagram_sources,
235+
archived: existing.archived,
234236
created_at: existing.created_at,
235237
updated_at: now_iso(),
236238
};
@@ -239,6 +241,42 @@ async fn handle_forms_admin(
239241
Response::ok("Updated")
240242
}
241243

244+
(Method::Post, p) if p.ends_with("/archive") && p.starts_with("/admin/forms/") => {
245+
let slug = p
246+
.strip_prefix("/admin/forms/")
247+
.and_then(|s| s.strip_suffix("/archive"))
248+
.unwrap_or("");
249+
if slug.is_empty() {
250+
return Response::error("Slug required", 400);
251+
}
252+
let mut form = match get_form(&kv, slug).await? {
253+
Some(f) => f,
254+
None => return Response::error("Form not found", 404),
255+
};
256+
form.archived = true;
257+
form.updated_at = now_iso();
258+
save_form(&kv, &form).await?;
259+
Response::empty()
260+
}
261+
262+
(Method::Post, p) if p.ends_with("/unarchive") && p.starts_with("/admin/forms/") => {
263+
let slug = p
264+
.strip_prefix("/admin/forms/")
265+
.and_then(|s| s.strip_suffix("/unarchive"))
266+
.unwrap_or("");
267+
if slug.is_empty() {
268+
return Response::error("Slug required", 400);
269+
}
270+
let mut form = match get_form(&kv, slug).await? {
271+
Some(f) => f,
272+
None => return Response::error("Form not found", 404),
273+
};
274+
form.archived = false;
275+
form.updated_at = now_iso();
276+
save_form(&kv, &form).await?;
277+
Response::empty()
278+
}
279+
242280
(Method::Delete, p) if p.starts_with("/admin/forms/") => {
243281
let slug = p.strip_prefix("/admin/forms/").unwrap_or("");
244282
if slug.is_empty() {
@@ -285,6 +323,7 @@ async fn handle_calendars_admin(
285323
instagram_sources: Vec::new(),
286324
style: CalendarStyle::default(),
287325
allowed_origins: Vec::new(),
326+
archived: false,
288327
created_at: now.clone(),
289328
updated_at: now,
290329
};
@@ -298,6 +337,11 @@ async fn handle_calendars_admin(
298337
<td>{date}</td>
299338
<td>
300339
<a href="{base_url}/admin/calendars/{id}" class="btn btn-sm">Edit</a>
340+
<button class="btn btn-sm btn-secondary"
341+
hx-post="{base_url}/admin/calendars/{id}/archive"
342+
hx-confirm="Archive this calendar? It will become read-only."
343+
hx-target="closest tr"
344+
hx-swap="outerHTML">Archive</button>
301345
</td>
302346
</tr>"#,
303347
base_url = base_url,
@@ -351,6 +395,28 @@ async fn handle_calendars_admin(
351395
Response::empty()
352396
}
353397

398+
(Method::Post, [id, "archive"]) => {
399+
let mut calendar = match get_calendar(&kv, id).await? {
400+
Some(c) => c,
401+
None => return Response::error("Calendar not found", 404),
402+
};
403+
calendar.archived = true;
404+
calendar.updated_at = now_iso();
405+
save_calendar(&kv, &calendar).await?;
406+
Response::empty()
407+
}
408+
409+
(Method::Post, [id, "unarchive"]) => {
410+
let mut calendar = match get_calendar(&kv, id).await? {
411+
Some(c) => c,
412+
None => return Response::error("Calendar not found", 404),
413+
};
414+
calendar.archived = false;
415+
calendar.updated_at = now_iso();
416+
save_calendar(&kv, &calendar).await?;
417+
Response::empty()
418+
}
419+
354420
(Method::Get, [id, "events"]) => {
355421
let calendar = match get_calendar(&kv, id).await? {
356422
Some(c) => c,

src/templates/admin.rs

Lines changed: 151 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,16 @@ pub fn admin_dashboard_html(
1414
response_counts: &HashMap<String, i64>,
1515
base_url: &str,
1616
) -> String {
17-
// Forms section
18-
let form_rows: String = forms
17+
// Split forms into active and archived
18+
let active_forms: Vec<_> = forms.iter().filter(|f| !f.archived).collect();
19+
let archived_forms: Vec<_> = forms.iter().filter(|f| f.archived).collect();
20+
21+
// Split calendars into active and archived
22+
let active_calendars: Vec<_> = calendars.iter().filter(|c| !c.archived).collect();
23+
let archived_calendars: Vec<_> = calendars.iter().filter(|c| c.archived).collect();
24+
25+
// Active forms section
26+
let form_rows: String = active_forms
1927
.iter()
2028
.map(|f| {
2129
let count = response_counts.get(&f.slug).unwrap_or(&0);
@@ -28,6 +36,11 @@ pub fn admin_dashboard_html(
2836
<button onclick=\"copyLink('/f/{slug}', this)\" class=\"btn btn-sm\">Copy Link</button>
2937
<a href=\"{base_url}/admin/forms/{slug}/responses\" class=\"btn btn-sm\">Responses</a>
3038
<a href=\"{base_url}/admin/forms/{slug}\" class=\"btn btn-sm\">Edit</a>
39+
<button class=\"btn btn-sm btn-secondary\"
40+
hx-post=\"{base_url}/admin/forms/{slug}/archive\"
41+
hx-confirm=\"Archive this form? It will become read-only.\"
42+
hx-target=\"closest tr\"
43+
hx-swap=\"outerHTML\">Archive</button>
3144
</td>
3245
</tr>",
3346
base_url = base_url,
@@ -38,8 +51,35 @@ pub fn admin_dashboard_html(
3851
})
3952
.collect();
4053

41-
// Calendars section
42-
let calendar_rows: String = calendars
54+
// Archived forms section
55+
let archived_form_rows: String = archived_forms
56+
.iter()
57+
.map(|f| {
58+
let count = response_counts.get(&f.slug).unwrap_or(&0);
59+
format!(
60+
"<tr>
61+
<td>{name} <span style=\"color:#666;font-size:0.85em;\">(archived)</span></td>
62+
<td><code>/f/{slug}</code></td>
63+
<td>{count}</td>
64+
<td>
65+
<a href=\"{base_url}/admin/forms/{slug}/responses\" class=\"btn btn-sm\">Responses</a>
66+
<a href=\"{base_url}/admin/forms/{slug}\" class=\"btn btn-sm\">View</a>
67+
<button class=\"btn btn-sm\"
68+
hx-post=\"{base_url}/admin/forms/{slug}/unarchive\"
69+
hx-target=\"closest tr\"
70+
hx-swap=\"outerHTML\">Unarchive</button>
71+
</td>
72+
</tr>",
73+
base_url = base_url,
74+
name = html_escape(&f.name),
75+
slug = html_escape(&f.slug),
76+
count = count
77+
)
78+
})
79+
.collect();
80+
81+
// Active calendars section
82+
let calendar_rows: String = active_calendars
4383
.iter()
4484
.map(|cal| {
4585
format!(
@@ -50,11 +90,11 @@ pub fn admin_dashboard_html(
5090
<td>{updated}</td>
5191
<td>
5292
<a href=\"{base_url}/admin/calendars/{id}\" class=\"btn btn-sm\">Edit</a>
53-
<button class=\"btn btn-sm btn-danger\"
54-
hx-delete=\"{base_url}/admin/calendars/{id}\"
55-
hx-confirm=\"Delete this calendar?\"
93+
<button class=\"btn btn-sm btn-secondary\"
94+
hx-post=\"{base_url}/admin/calendars/{id}/archive\"
95+
hx-confirm=\"Archive this calendar? It will become read-only.\"
5696
hx-target=\"closest tr\"
57-
hx-swap=\"outerHTML\">Delete</button>
97+
hx-swap=\"outerHTML\">Archive</button>
5898
</td>
5999
</tr>",
60100
base_url = base_url,
@@ -67,6 +107,82 @@ pub fn admin_dashboard_html(
67107
})
68108
.collect();
69109

110+
// Archived calendars section
111+
let archived_calendar_rows: String = archived_calendars
112+
.iter()
113+
.map(|cal| {
114+
format!(
115+
"<tr>
116+
<td>{name} <span style=\"color:#666;font-size:0.85em;\">(archived)</span></td>
117+
<td>{booking_count} booking links</td>
118+
<td>{view_count} view links</td>
119+
<td>{updated}</td>
120+
<td>
121+
<a href=\"{base_url}/admin/calendars/{id}\" class=\"btn btn-sm\">View</a>
122+
<button class=\"btn btn-sm\"
123+
hx-post=\"{base_url}/admin/calendars/{id}/unarchive\"
124+
hx-target=\"closest tr\"
125+
hx-swap=\"outerHTML\">Unarchive</button>
126+
</td>
127+
</tr>",
128+
base_url = base_url,
129+
id = html_escape(&cal.id),
130+
name = html_escape(&cal.name),
131+
booking_count = cal.booking_links.len(),
132+
view_count = cal.view_links.len(),
133+
updated = html_escape(&cal.updated_at.split('T').next().unwrap_or("")),
134+
)
135+
})
136+
.collect();
137+
138+
// Build archived sections HTML
139+
let archived_forms_section = if archived_forms.is_empty() {
140+
String::new()
141+
} else {
142+
format!(
143+
"<details style=\"margin-top: 1rem;\">
144+
<summary style=\"cursor: pointer; color: #666;\">Archived Forms ({count})</summary>
145+
<table style=\"margin-top: 0.5rem;\">
146+
<thead>
147+
<tr>
148+
<th>Name</th>
149+
<th>URL</th>
150+
<th>Responses</th>
151+
<th>Actions</th>
152+
</tr>
153+
</thead>
154+
<tbody>{rows}</tbody>
155+
</table>
156+
</details>",
157+
count = archived_forms.len(),
158+
rows = archived_form_rows
159+
)
160+
};
161+
162+
let archived_calendars_section = if archived_calendars.is_empty() {
163+
String::new()
164+
} else {
165+
format!(
166+
"<details style=\"margin-top: 1rem;\">
167+
<summary style=\"cursor: pointer; color: #666;\">Archived Calendars ({count})</summary>
168+
<table style=\"margin-top: 0.5rem;\">
169+
<thead>
170+
<tr>
171+
<th>Name</th>
172+
<th>Booking Links</th>
173+
<th>View Links</th>
174+
<th>Updated</th>
175+
<th>Actions</th>
176+
</tr>
177+
</thead>
178+
<tbody>{rows}</tbody>
179+
</table>
180+
</details>",
181+
count = archived_calendars.len(),
182+
rows = archived_calendar_rows
183+
)
184+
};
185+
70186
let content = format!(
71187
"<h1 style=\"display: flex; align-items: center; gap: 0.5rem;\">
72188
<img src=\"/logo.svg\" alt=\"\" style=\"width: 32px; height: 32px;\">
@@ -90,6 +206,7 @@ pub fn admin_dashboard_html(
90206
{form_rows}
91207
</tbody>
92208
</table>
209+
{archived_forms_section}
93210
94211
<h2 style=\"margin-top: 2rem;\">Calendars</h2>
95212
<p style=\"margin: 1rem 0;\">
@@ -111,6 +228,7 @@ pub fn admin_dashboard_html(
111228
{calendar_rows}
112229
</tbody>
113230
</table>
231+
{archived_calendars_section}
114232
<script>
115233
function copyLink(path, btn) {{
116234
const url = window.location.origin + path;
@@ -136,6 +254,8 @@ pub fn admin_dashboard_html(
136254
} else {
137255
calendar_rows
138256
},
257+
archived_forms_section = archived_forms_section,
258+
archived_calendars_section = archived_calendars_section,
139259
hash = HASH,
140260
);
141261

@@ -273,6 +393,7 @@ pub fn admin_calendar_html(calendar: &CalendarConfig, base_url: &str) -> String
273393
274394
<p><a href=\"{base_url}/admin\">&larr; Back to Dashboard</a></p>
275395
<h1>{name}</h1>
396+
{archived_notice}
276397
277398
<div class=\"tabs\">
278399
<button class=\"tab active\" onclick=\"showTab('settings')\">Settings</button>
@@ -327,12 +448,21 @@ pub fn admin_calendar_html(calendar: &CalendarConfig, base_url: &str) -> String
327448
<h2>Feed Links (iCal)</h2>
328449
<p style=\"margin-bottom: 1rem; color: #666;\">Subscribe to this calendar from other apps.</p>
329450
<button class=\"btn\" hx-post=\"{base_url}/admin/calendars/{id}/feed\"
330-
hx-target=\"{hash}feed-links tbody\" hx-swap=\"beforeend\">+ Add Feed Link</button>
451+
hx-target=\"{hash}feed-links tbody\" hx-swap=\"beforeend\"{readonly_disabled}>+ Add Feed Link</button>
331452
<table id=\"feed-links\" style=\"margin-top: 1rem;\">
332453
<thead><tr><th>Name</th><th>URL</th><th>Status</th><th>Actions</th></tr></thead>
333454
<tbody>{feed_links_html}</tbody>
334455
</table>
335456
</div>
457+
458+
<div class=\"card\" style=\"border-color: #dc3545;\">
459+
<h2 style=\"color: #dc3545;\">Danger Zone</h2>
460+
<p style=\"margin-bottom: 1rem; color: #666;\">Permanently delete this calendar and all its data.</p>
461+
<button class=\"btn btn-danger\"
462+
hx-delete=\"{base_url}/admin/calendars/{id}\"
463+
hx-confirm=\"Are you sure you want to permanently delete this calendar? This action cannot be undone.\"
464+
hx-on::after-request=\"if(event.detail.successful) window.location.href='/admin'\">Delete Calendar</button>
465+
</div>
336466
</div>
337467
338468
<div id=\"tab-events\" class=\"tab-content\">
@@ -395,10 +525,21 @@ pub fn admin_calendar_html(calendar: &CalendarConfig, base_url: &str) -> String
395525
view_links_html = view_links_html,
396526
feed_links_html = feed_links_html,
397527
instagram_sources_html = instagram_sources_html,
528+
archived_notice = if calendar.archived {
529+
"<div class=\"card\" style=\"background: #fff3cd; border-color: #ffc107; margin-bottom: 1rem;\">
530+
<p style=\"margin: 0; color: #856404;\"><strong>This calendar is archived.</strong> It is read-only. Unarchive from the dashboard to make changes.</p>
531+
</div>"
532+
} else { "" },
533+
readonly_disabled = if calendar.archived { " disabled" } else { "" },
398534
hash = HASH,
399535
);
400536

401-
base_html(&format!("Edit: {}", calendar.name), &content, &calendar.style)
537+
let title = if calendar.archived {
538+
format!("{} (Archived)", calendar.name)
539+
} else {
540+
format!("Edit: {}", calendar.name)
541+
};
542+
base_html(&title, &content, &calendar.style)
402543
}
403544

404545
pub fn admin_events_html(calendar: &CalendarConfig, events: &[CalendarEvent], base_url: &str) -> String {

src/templates/base.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ pub fn base_html(title: &str, content: &str, style: &CalendarStyle) -> String {
184184
cursor: pointer;
185185
text-decoration: none;
186186
font-size: 0.875rem;
187+
font-family: inherit;
188+
line-height: 1.2;
189+
vertical-align: middle;
190+
box-sizing: border-box;
187191
}}
188192
.btn:hover {{ opacity: 0.9; }}
189193
.btn-secondary {{
@@ -367,6 +371,10 @@ pub fn base_html_with_css(title: &str, content: &str, style: &CalendarStyle, css
367371
cursor: pointer;
368372
text-decoration: none;
369373
font-size: 0.875rem;
374+
font-family: inherit;
375+
line-height: 1.2;
376+
vertical-align: middle;
377+
box-sizing: border-box;
370378
}}
371379
.btn:hover {{ opacity: 0.9; }}
372380
.btn-secondary {{

0 commit comments

Comments
 (0)