diff options
Diffstat (limited to 'web/templates/settings.html')
| -rw-r--r-- | web/templates/settings.html | 372 |
1 files changed, 114 insertions, 258 deletions
diff --git a/web/templates/settings.html b/web/templates/settings.html index 4df4cb0..ca1d268 100644 --- a/web/templates/settings.html +++ b/web/templates/settings.html @@ -5,228 +5,84 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="csrf-token" content="{{.CSRFToken}}"> <title>Settings - Task Dashboard</title> - <script src="https://unpkg.com/htmx.org@1.9.10"></script> + <link rel="icon" type="image/svg+xml" href="/static/favicon.svg"> + <link rel="stylesheet" href="/static/css/output.css"> + <script src="/static/js/htmx.min.js"></script> <style> - :root { - --bg-primary: #1a1a2e; - --bg-secondary: #16213e; - --bg-tertiary: #0f3460; - --text-primary: #eee; - --text-secondary: #888; - --accent: #e94560; - --success: #28a745; - --danger: #dc3545; - --border: #333; - } - body { - font-family: system-ui, sans-serif; - background: var(--bg-primary); - color: var(--text-primary); - margin: 0; - padding: 20px; - min-height: 100vh; - } - .container { max-width: 900px; margin: 0 auto; } - h1 { color: var(--accent); margin-bottom: 8px; } - h2 { color: var(--text-primary); font-size: 1.1em; margin: 24px 0 16px; border-bottom: 1px solid var(--border); padding-bottom: 8px; } - .subtitle { color: var(--text-secondary); margin-bottom: 24px; } - .back-link { color: var(--accent); text-decoration: none; display: inline-block; margin-bottom: 16px; } - .back-link:hover { text-decoration: underline; } - - .card { - background: var(--bg-secondary); - border-radius: 8px; - padding: 20px; - margin-bottom: 16px; - } - .card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - } - .card-title { - font-size: 1.1em; - font-weight: 600; - display: flex; - align-items: center; - gap: 8px; - } - .source-badge { - display: inline-block; - padding: 2px 8px; - border-radius: 10px; - font-size: 0.7em; - font-weight: normal; - } - .source-trello { background: #0079bf; } - .source-todoist { background: #e44332; } - .source-gcal { background: #9b59b6; } - .source-gtasks { background: #f39c12; } - - .btn { - background: var(--bg-tertiary); - color: var(--text-primary); - border: 1px solid var(--border); - padding: 8px 16px; - border-radius: 4px; - cursor: pointer; - font-size: 0.9em; - } - .btn:hover { background: #1a4a80; } - .btn:disabled { opacity: 0.5; cursor: not-allowed; } - .btn-danger { border-color: var(--danger); } - .btn-danger:hover { background: var(--danger); } - .btn-sm { padding: 4px 8px; font-size: 0.8em; } - - .items-list { display: grid; gap: 8px; } - .item-row { - display: flex; - align-items: center; - gap: 12px; - padding: 10px 12px; - background: var(--bg-tertiary); - border-radius: 4px; - } - .item-row:hover { background: #1a4a80; } - - .toggle-switch { - position: relative; - width: 44px; - height: 24px; - flex-shrink: 0; - } - .toggle-switch input { opacity: 0; width: 0; height: 0; } - .toggle-slider { - position: absolute; - cursor: pointer; - top: 0; left: 0; right: 0; bottom: 0; - background-color: #555; - transition: .3s; - border-radius: 24px; - } - .toggle-slider:before { - position: absolute; - content: ""; - height: 18px; - width: 18px; - left: 3px; - bottom: 3px; - background-color: white; - transition: .3s; - border-radius: 50%; - } - input:checked + .toggle-slider { background-color: var(--success); } - input:checked + .toggle-slider:before { transform: translateX(20px); } - - .item-name { flex: 1; } - .item-desc { color: var(--text-secondary); font-size: 0.85em; } - .item-id { - font-family: monospace; - font-size: 0.75em; - color: var(--text-secondary); - max-width: 200px; - overflow: hidden; - text-overflow: ellipsis; - } - - .empty-state { - color: var(--text-secondary); - padding: 20px; - text-align: center; - } - - .add-form { - display: flex; - gap: 8px; - margin-top: 12px; - padding-top: 12px; - border-top: 1px solid var(--border); - } - .add-form input { - flex: 1; - background: var(--bg-tertiary); - border: 1px solid var(--border); - color: var(--text-primary); - padding: 8px 12px; - border-radius: 4px; - } - .add-form input:focus { outline: none; border-color: var(--accent); } - .htmx-indicator { display: none; } .htmx-request .htmx-indicator { display: inline; } </style> </head> -<body hx-headers='{"X-CSRF-Token": "{{.CSRFToken}}"}'> - - <div class="container"> - <a href="/" class="back-link">← Back to Dashboard</a> - <h1>Settings</h1> - <p class="subtitle">Configure feature toggles and data sources.</p> +<body class="bg-slate-950 text-slate-200 min-h-screen p-4 sm:p-8" hx-headers='{"X-CSRF-Token": "{{.CSRFToken}}"}'> + <div class="max-w-4xl mx-auto"> + <a href="/" class="inline-block mb-6 text-slate-400 hover:text-white transition-colors">← Back to Dashboard</a> + <h1 class="text-4xl font-light text-white mb-2 tracking-tight">Settings</h1> + <p class="text-slate-400 mb-10">Configure feature toggles and data sources.</p> <!-- Feature Toggles Section --> - <div class="card"> - <div class="card-header"> - <div class="card-title">Feature Toggles</div> - </div> - <div class="items-list" id="toggles-list"> + <section class="mb-12"> + <h2 class="text-xl font-medium text-white mb-6 pb-2 border-b border-white/10">Feature Toggles</h2> + <div class="grid gap-4" id="toggles-list"> {{if .Toggles}} {{range .Toggles}} - <div class="item-row" id="toggle-{{.Name}}"> - <label class="toggle-switch"> - <input type="checkbox" - {{if .Enabled}}checked{{end}} - hx-post="/settings/features/toggle" - hx-vals='{"name": "{{.Name}}", "enabled": "{{if .Enabled}}false{{else}}true{{end}}"}' - hx-swap="none" - hx-on::after-request="this.checked = !this.checked; if(event.detail.successful) this.checked = !this.checked;"> - <span class="toggle-slider"></span> - </label> - <div class="item-name"> - <strong>{{.Name}}</strong> - {{if .Description}}<div class="item-desc">{{.Description}}</div>{{end}} + <div class="card flex items-center gap-4" id="toggle-{{.Name}}"> + <div class="flex-1"> + <strong class="text-white">{{.Name}}</strong> + {{if .Description}}<div class="text-sm text-slate-400">{{.Description}}</div>{{end}} + </div> + <div class="flex items-center gap-6"> + <label class="relative inline-flex items-center cursor-pointer"> + <input type="checkbox" value="" class="sr-only peer" + {{if .Enabled}}checked{{end}} + hx-post="/settings/features/toggle" + hx-vals='{"name": "{{.Name}}", "enabled": "{{if .Enabled}}false{{else}}true{{end}}"}' + hx-swap="none" + hx-on::after-request="this.checked = !this.checked; if(event.detail.successful) this.checked = !this.checked;"> + <div class="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div> + </label> + <button class="text-xs text-red-400 hover:text-red-300 transition-colors" + hx-delete="/settings/features/{{.Name}}" + hx-target="#toggle-{{.Name}}" + hx-swap="outerHTML" + hx-confirm="Delete feature toggle '{{.Name}}'?"> + Delete + </button> </div> - <button class="btn btn-danger btn-sm" - hx-delete="/settings/features/{{.Name}}" - hx-target="#toggle-{{.Name}}" - hx-swap="outerHTML" - hx-confirm="Delete feature toggle '{{.Name}}'?"> - Delete - </button> </div> {{end}} {{else}} - <div class="empty-state">No feature toggles configured.</div> + <div class="card text-center text-slate-500 py-10">No feature toggles configured.</div> {{end}} </div> - <form class="add-form" hx-post="/settings/features" hx-target="#toggles-list" hx-swap="beforeend"> - <input type="text" name="name" placeholder="Feature name (snake_case)" required pattern="[a-z_]+"> - <input type="text" name="description" placeholder="Description (optional)"> - <button type="submit" class="btn">Add Toggle</button> + <form class="mt-4 flex flex-wrap gap-3 p-4 bg-slate-900/40 rounded-xl border border-white/5" hx-post="/settings/features" hx-target="#toggles-list" hx-swap="beforeend"> + <input type="text" name="name" placeholder="Feature name (snake_case)" required pattern="[a-z_]+" + class="flex-1 bg-slate-950 border border-white/10 rounded-lg px-4 py-2 text-sm focus:ring-1 focus:ring-blue-500 outline-none"> + <input type="text" name="description" placeholder="Description (optional)" + class="flex-[2] bg-slate-950 border border-white/10 rounded-lg px-4 py-2 text-sm focus:ring-1 focus:ring-blue-500 outline-none"> + <button type="submit" class="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded-lg text-sm font-medium transition-colors">Add Toggle</button> </form> - </div> + </section> <!-- Passkeys Section --> {{if .WebAuthnEnabled}} - <div class="card" id="passkeys-card"> - <div class="card-header"> - <div class="card-title">Passkeys</div> + <section class="mb-12" id="passkeys-card-section"> + <h2 class="text-xl font-medium text-white mb-6 pb-2 border-b border-white/10">Passkeys</h2> + <div id="passkeys-list" hx-get="/settings/passkeys" hx-trigger="load" hx-swap="innerHTML" class="grid gap-4"> + <div class="card text-center text-slate-500 py-10">Loading passkeys...</div> </div> - <div id="passkeys-list" hx-get="/settings/passkeys" hx-trigger="load" hx-swap="innerHTML"> - <div class="empty-state">Loading passkeys...</div> + <div class="mt-4 flex flex-wrap gap-3 p-4 bg-slate-900/40 rounded-xl border border-white/5" id="passkey-register-form"> + <input type="text" id="passkey-name" placeholder="Passkey name (e.g., MacBook Touch ID)" maxlength="100" + class="flex-1 bg-slate-950 border border-white/10 rounded-lg px-4 py-2 text-sm focus:ring-1 focus:ring-blue-500 outline-none"> + <button type="button" class="bg-slate-700 hover:bg-slate-600 text-white px-6 py-2 rounded-lg text-sm font-medium transition-colors" id="register-passkey-btn">Register New Passkey</button> </div> - <div class="add-form" id="passkey-register-form"> - <input type="text" id="passkey-name" placeholder="Passkey name (e.g., MacBook Touch ID)" maxlength="100"> - <button type="button" class="btn" id="register-passkey-btn">Register New Passkey</button> - </div> - <p id="passkey-status" style="color: var(--text-secondary); font-size: 0.85em; margin-top: 8px; display: none;"></p> - </div> - {{end}} + <p id="passkey-status" class="mt-3 text-sm text-slate-400 hidden"></p> + </section> + <script> (function() { - if (!document.getElementById('passkeys-card') || !window.PublicKeyCredential) { - var card = document.getElementById('passkeys-card'); - if (card) card.style.display = 'none'; + if (!window.PublicKeyCredential) { + var section = document.getElementById('passkeys-card-section'); + if (section) section.style.display = 'none'; return; } @@ -238,7 +94,7 @@ btn.disabled = true; statusEl.style.display = 'block'; - statusEl.style.color = 'var(--text-secondary)'; + statusEl.className = 'mt-3 text-sm text-slate-400'; statusEl.textContent = 'Starting registration...'; try { @@ -279,13 +135,12 @@ }); if (!finishResp.ok) throw new Error('Registration failed'); - statusEl.style.color = 'var(--success)'; + statusEl.className = 'mt-3 text-sm text-green-400'; statusEl.textContent = 'Passkey registered successfully!'; nameInput.value = ''; - // Reload passkeys list htmx.trigger('#passkeys-list', 'load'); } catch (e) { - statusEl.style.color = 'var(--danger)'; + statusEl.className = 'mt-3 text-sm text-red-400'; if (e.name === 'NotAllowedError') { statusEl.textContent = 'Registration was cancelled.'; } else { @@ -310,66 +165,67 @@ } })(); </script> + {{end}} - <!-- Source Configuration Section --> - <h2>Data Sources</h2> - <div style="display: flex; gap: 8px; align-items: center;"> - <form hx-post="/settings/sync" hx-swap="outerHTML" hx-target="#sources-container" hx-indicator=".sync-indicator"> - <button type="submit" class="btn"> - <span class="sync-indicator htmx-indicator">Syncing...</span> - Sync Available Sources - </button> - </form> - <button class="btn btn-danger" - hx-post="/settings/clear-cache" - hx-target="#sync-log" - hx-swap="outerHTML" - hx-confirm="Clear all cached data? Next page load will fetch fresh data from all sources."> - Clear Cache - </button> - </div> - - {{template "sync-log" .SyncLog}} - - <div id="sources-container" style="margin-top: 16px;"> - {{range .Sources}} - <div class="card"> - <div class="card-header"> - <div class="card-title"> - <span class="source-badge source-{{.}}">{{.}}</span> - {{if eq . "trello"}}Trello Boards{{end}} - {{if eq . "todoist"}}Todoist Projects{{end}} - {{if eq . "gcal"}}Google Calendars{{end}} - {{if eq . "gtasks"}}Google Task Lists{{end}} - </div> + <!-- Data Sources Section --> + <section class="mb-12"> + <div class="flex flex-wrap items-center justify-between gap-4 mb-6 pb-2 border-b border-white/10"> + <h2 class="text-xl font-medium text-white">Data Sources</h2> + <div class="flex gap-3"> + <form hx-post="/settings/sync" hx-swap="outerHTML" hx-target="#sources-container" hx-indicator=".sync-indicator"> + <button type="submit" class="text-sm bg-slate-800 hover:bg-slate-700 text-white px-4 py-2 rounded-lg transition-colors"> + <span class="sync-indicator htmx-indicator mr-2">Syncing...</span> + Sync Available Sources + </button> + </form> + <button class="text-sm border border-red-500/30 text-red-400 hover:bg-red-500/10 px-4 py-2 rounded-lg transition-colors" + hx-post="/settings/clear-cache" + hx-target="#sync-log" + hx-swap="outerHTML" + hx-confirm="Clear all cached data? Next page load will fetch fresh data from all sources."> + Clear Cache + </button> </div> - <div class="items-list"> - {{$configs := index $.Configs .}} - {{if $configs}} - {{range $configs}} - <div class="item-row"> - <label class="toggle-switch"> - <input type="checkbox" - {{if .Enabled}}checked{{end}} - hx-post="/settings/toggle" - hx-vals='{"source": "{{.Source}}", "item_type": "{{.ItemType}}", "item_id": "{{.ItemID}}", "enabled": "{{if .Enabled}}false{{else}}true{{end}}"}' - hx-swap="none" - hx-on::after-request="this.checked = !this.checked; if(event.detail.successful) this.checked = !this.checked;"> - <span class="toggle-slider"></span> - </label> - <span class="item-name">{{.ItemName}}</span> - <span class="item-id" title="{{.ItemID}}">{{.ItemID}}</span> - </div> + </div> + + {{template "sync-log" .SyncLog}} + + <div id="sources-container" class="grid gap-8 mt-8"> + {{range .Sources}} + <div class="space-y-4"> + <h3 class="text-sm font-semibold uppercase tracking-wider text-slate-500 flex items-center gap-2"> + {{if eq . "trello"}}Trello Boards{{else if eq . "todoist"}}Todoist Projects{{else if eq . "gcal"}}Google Calendars{{else if eq . "gtasks"}}Google Task Lists{{else}}{{.}}{{end}} + </h3> + <div class="grid gap-3 sm:grid-cols-2"> + {{$configs := index $.Configs .}} + {{if $configs}} + {{range $configs}} + <div class="card flex items-center gap-4"> + <label class="relative inline-flex items-center cursor-pointer"> + <input type="checkbox" value="" class="sr-only peer" + {{if .Enabled}}checked{{end}} + hx-post="/settings/toggle" + hx-vals='{"source": "{{.Source}}", "item_type": "{{.ItemType}}", "item_id": "{{.ItemID}}", "enabled": "{{if .Enabled}}false{{else}}true{{end}}"}' + hx-swap="none" + hx-on::after-request="this.checked = !this.checked; if(event.detail.successful) this.checked = !this.checked;"> + <div class="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div> + </label> + <div class="flex-1 min-w-0"> + <div class="text-white truncate" title="{{.ItemName}}">{{.ItemName}}</div> + <div class="text-[10px] text-slate-500 font-mono truncate" title="{{.ItemID}}">{{.ItemID}}</div> + </div> + </div> + {{end}} + {{else}} + <div class="card col-span-full text-center text-slate-500 py-6 text-sm italic"> + No items found. Click "Sync Available Sources" to fetch. + </div> {{end}} - {{else}} - <div class="empty-state"> - No items configured. Click "Sync Available Sources" to fetch. - </div> - {{end}} + </div> </div> + {{end}} </div> - {{end}} - </div> + </section> </div> </body> </html> |
