mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-31 05:07:09 +00:00
621 lines
32 KiB
HTML
621 lines
32 KiB
HTML
{{ define "settings" }}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
{{ template "head" . }}
|
|
{{ template "meta" .Meta }}
|
|
</head>
|
|
<body>
|
|
{{ template "nav" . }}
|
|
|
|
<main class="container mx-auto px-4 py-8">
|
|
<div class="max-w-5xl mx-auto">
|
|
<h1 class="text-3xl font-bold mb-6">Settings</h1>
|
|
|
|
<!-- Mobile tab bar (below lg) -->
|
|
<div class="flex gap-2 overflow-x-auto pb-2 lg:hidden mb-6">
|
|
<button class="btn btn-sm btn-ghost settings-tab-mobile" data-tab="identity">
|
|
{{ icon "fingerprint" "size-4" }} Identity
|
|
</button>
|
|
<button class="btn btn-sm btn-ghost settings-tab-mobile" data-tab="devices">
|
|
{{ icon "terminal" "size-4" }} Devices
|
|
</button>
|
|
<button class="btn btn-sm btn-ghost settings-tab-mobile" data-tab="storage">
|
|
{{ icon "hard-drive" "size-4" }} Storage
|
|
</button>
|
|
<button class="btn btn-sm btn-ghost settings-tab-mobile" data-tab="advanced">
|
|
{{ icon "shield-check" "size-4" }} Advanced
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex gap-8">
|
|
<!-- Sidebar (lg and above) -->
|
|
<aside class="hidden lg:block w-56 shrink-0">
|
|
<ul class="menu bg-base-200 rounded-box w-full">
|
|
<li data-tab="identity"><a href="#identity">{{ icon "fingerprint" "size-4" }} Identity</a></li>
|
|
<li data-tab="devices"><a href="#devices">{{ icon "terminal" "size-4" }} Devices</a></li>
|
|
<li data-tab="storage"><a href="#storage">{{ icon "hard-drive" "size-4" }} Storage</a></li>
|
|
<li data-tab="advanced"><a href="#advanced">{{ icon "shield-check" "size-4" }} Advanced</a></li>
|
|
</ul>
|
|
</aside>
|
|
|
|
<!-- Tab content -->
|
|
<div class="flex-1 min-w-0">
|
|
|
|
<!-- IDENTITY TAB -->
|
|
<div id="tab-identity" class="settings-panel space-y-6">
|
|
<section class="card bg-base-100 shadow-sm p-6 space-y-4">
|
|
<h2 class="text-xl font-semibold">Identity</h2>
|
|
<div class="grid gap-3">
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-sm font-medium text-base-content/70">Handle</span>
|
|
<span>{{ .Profile.Handle }}</span>
|
|
</div>
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-sm font-medium text-base-content/70">DID</span>
|
|
<code class="cmd">{{ .Profile.DID }}</code>
|
|
</div>
|
|
<div class="flex flex-col gap-1">
|
|
<span class="text-sm font-medium text-base-content/70">PDS</span>
|
|
<span>{{ .Profile.PDSEndpoint }}</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<!-- DEVICES TAB -->
|
|
<div id="tab-devices" class="settings-panel hidden space-y-6">
|
|
<section class="card bg-base-100 shadow-sm p-6 space-y-6">
|
|
<div>
|
|
<h2 class="text-xl font-semibold">Authorized Devices</h2>
|
|
<p class="text-base-content/70 mt-1">Devices authorized via <code class="cmd">docker-credential-atcr</code> credential helper.</p>
|
|
</div>
|
|
|
|
<!-- Setup Instructions -->
|
|
<div class="bg-base-200 rounded-lg p-4 space-y-4">
|
|
<h3 class="font-semibold">First Time Setup</h3>
|
|
<ol class="list-decimal list-inside space-y-4 text-sm">
|
|
<li>Install credential helper:
|
|
<pre class="mt-2 p-3 bg-base-300 rounded-lg overflow-x-auto"><code>curl -fsSL {{ .SiteURL }}/static/install.sh | bash</code></pre>
|
|
</li>
|
|
<li>Configure Docker to use the helper. Add to <code class="cmd">~/.docker/config.json</code>:
|
|
<pre class="mt-2 p-3 bg-base-300 rounded-lg overflow-x-auto"><code>{
|
|
"credHelpers": {
|
|
"{{ .RegistryURL }}": "atcr"
|
|
}
|
|
}</code></pre>
|
|
</li>
|
|
<li>Run any Docker command:
|
|
<div class="mt-2">{{ template "docker-command" (print "docker pull " .RegistryURL "/" .Profile.Handle "/myimage") }}</div>
|
|
</li>
|
|
<li>Browser will open for authorization - click Approve</li>
|
|
<li>Done! Device is automatically authorized</li>
|
|
</ol>
|
|
|
|
<div class="pt-3 border-t border-base-300 text-sm">
|
|
<strong>Fallback:</strong> Use <a href="https://bsky.app/settings/app-passwords" target="_blank" class="link link-primary">app password</a> with <code class="cmd">docker login {{ .RegistryURL }}</code> for quick start (no device tracking)
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Devices List -->
|
|
<div class="space-y-3">
|
|
<h3 class="font-semibold">Your Authorized Devices</h3>
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-zebra">
|
|
<thead>
|
|
<tr>
|
|
<th>Device Name</th>
|
|
<th>IP Address</th>
|
|
<th>Created</th>
|
|
<th>Last Used</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="devices-table"
|
|
hx-get="/api/devices"
|
|
hx-trigger="tab:devices from:body once, every 30s[isTabActive('devices')], devicesChanged from:body"
|
|
hx-swap="innerHTML">
|
|
<tr><td colspan="5" class="text-center">{{ icon "loader-2" "size-4 animate-spin inline-block" }} Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<!-- STORAGE TAB -->
|
|
<div id="tab-storage" class="settings-panel hidden space-y-6">
|
|
<!-- Default Hold Section -->
|
|
<section class="card bg-base-100 shadow-sm p-6 space-y-4">
|
|
<h2 class="text-xl font-semibold">Default Hold</h2>
|
|
<p class="text-base-content/70">Select where your container images will be stored.</p>
|
|
|
|
<form hx-post="/api/profile/default-hold"
|
|
hx-target="#hold-status"
|
|
hx-swap="innerHTML"
|
|
id="hold-form"
|
|
class="space-y-4">
|
|
|
|
<fieldset class="fieldset">
|
|
<legend class="sr-only">Storage hold selection</legend>
|
|
<label class="label" for="default-hold">
|
|
<span class="label-text">Storage Hold</span>
|
|
</label>
|
|
<select id="default-hold" name="hold_did" class="select select-bordered w-full" autocomplete="off">
|
|
<option value="{{ .AppViewDefaultHoldDID }}"{{ if or (eq .CurrentHoldDID "") (eq .CurrentHoldDID .AppViewDefaultHoldDID) }} selected{{ end }}>AppView Default ({{ .AppViewDefaultHoldDisplay }}{{ if .AppViewDefaultRegion }}, {{ .AppViewDefaultRegion }}{{ end }})</option>
|
|
|
|
{{ if .ShowCurrentHold }}
|
|
<option value="{{ .CurrentHoldDID }}" selected>Current ({{ .CurrentHoldDisplay }})</option>
|
|
{{ end }}
|
|
|
|
{{ if .OwnedHolds }}
|
|
<optgroup label="Your Holds">
|
|
{{ range .OwnedHolds }}
|
|
<option value="{{ .DID }}" {{ if eq $.CurrentHoldDID .DID }}selected{{ end }}>
|
|
{{ .DisplayName }}{{ if .Region }} ({{ .Region }}){{ end }}
|
|
</option>
|
|
{{ end }}
|
|
</optgroup>
|
|
{{ end }}
|
|
|
|
{{ if .CrewHolds }}
|
|
<optgroup label="Crew Member">
|
|
{{ range .CrewHolds }}
|
|
<option value="{{ .DID }}" {{ if eq $.CurrentHoldDID .DID }}selected{{ end }}>
|
|
{{ .DisplayName }}{{ if .Region }} ({{ .Region }}){{ end }}
|
|
</option>
|
|
{{ end }}
|
|
</optgroup>
|
|
{{ end }}
|
|
|
|
{{ if .EligibleHolds }}
|
|
<optgroup label="Open Registration">
|
|
{{ range .EligibleHolds }}
|
|
<option value="{{ .DID }}" {{ if eq $.CurrentHoldDID .DID }}selected{{ end }}>
|
|
{{ .DisplayName }}{{ if .Region }} ({{ .Region }}){{ end }}
|
|
</option>
|
|
{{ end }}
|
|
</optgroup>
|
|
{{ end }}
|
|
|
|
</select>
|
|
<p class="text-sm text-base-content/60 mt-1">Your images will be stored on the selected hold</p>
|
|
</fieldset>
|
|
|
|
<button type="submit" class="btn btn-primary">Save</button>
|
|
</form>
|
|
|
|
<div id="hold-status"></div>
|
|
|
|
<!-- Hold details panel (shows when hold selected) -->
|
|
<div id="hold-details" class="hidden mt-4 p-4 bg-base-200 rounded-lg">
|
|
<h3 class="font-semibold mb-3">Hold Details</h3>
|
|
<dl class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm">
|
|
<dt class="text-base-content/70">DID:</dt>
|
|
<dd id="hold-did" class="font-mono"></dd>
|
|
<dt class="text-base-content/70">Region:</dt>
|
|
<dd id="hold-region"></dd>
|
|
<dt class="text-base-content/70">Your Access:</dt>
|
|
<dd id="hold-access"></dd>
|
|
</dl>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Storage Usage Section -->
|
|
<section class="card bg-base-100 shadow-sm p-6 space-y-4">
|
|
<h2 class="text-xl font-semibold">Stowage</h2>
|
|
<p class="text-base-content/70">Estimated storage usage on your default hold.</p>
|
|
<div id="storage-stats" hx-get="/api/storage" hx-trigger="tab:storage from:body once" hx-swap="innerHTML">
|
|
<p class="flex items-center gap-2">{{ icon "loader-2" "size-4 animate-spin" }} Loading...</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Subscription Section -->
|
|
<div id="subscription-wrapper" hx-get="/api/subscription" hx-trigger="tab:storage from:body once" hx-swap="innerHTML">
|
|
<section id="subscription-section" class="card bg-base-100 shadow-sm p-6 space-y-4">
|
|
<h2 class="text-xl font-semibold">Subscription</h2>
|
|
<p class="text-base-content/70">Manage your storage tier and billing.</p>
|
|
<p class="flex items-center gap-2">{{ icon "loader-2" "size-4 animate-spin" }} Loading subscription info...</p>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ADVANCED TAB -->
|
|
<div id="tab-advanced" class="settings-panel hidden space-y-6">
|
|
<!-- Data Privacy Section -->
|
|
<section class="card bg-base-100 shadow-sm p-6 space-y-4">
|
|
<h2 class="text-xl font-semibold">Data Privacy</h2>
|
|
<p class="text-base-content/70">Download a copy of all data we store about you.</p>
|
|
|
|
<div>
|
|
<a href="/api/export-data" class="btn btn-secondary gap-2" download>
|
|
{{ icon "download" "size-4" }}
|
|
Export All My Data
|
|
</a>
|
|
</div>
|
|
|
|
<p class="text-sm text-base-content/60">
|
|
This includes your authorized devices, sessions, and hold memberships.
|
|
Data stored on your PDS is already under your control.
|
|
See our <a href="/privacy" class="link link-primary">Privacy Policy</a> for details.
|
|
</p>
|
|
</section>
|
|
|
|
<!-- Danger Zone Section -->
|
|
<section class="border-2 border-error rounded-lg p-6 space-y-4">
|
|
<h2 class="text-xl font-semibold text-error flex items-center gap-2">
|
|
{{ icon "alert-triangle" "size-5" }}
|
|
Danger Zone
|
|
</h2>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<h3 class="font-semibold">Delete {{ .ClientShortName }} Data</h3>
|
|
<p class="text-base-content/70 mt-1">Remove your data from {{ .ClientShortName }}. This action cannot be undone.</p>
|
|
</div>
|
|
|
|
<div class="alert bg-base-200">
|
|
{{ icon "info" "size-5 shrink-0" }}
|
|
<span><strong>This does not delete your ATProto (Bluesky, Blacksky, Tangled) account.</strong><br>Only {{ .ClientShortName }}-specific data (authorized devices, hold memberships, settings) will be removed.</span>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<label class="flex items-start gap-3 cursor-pointer">
|
|
<input type="checkbox" id="delete-pds-records" class="checkbox checkbox-sm mt-0.5">
|
|
<span class="text-sm">Also delete all <code class="cmd">io.atcr.*</code> records from my ATProto PDS</span>
|
|
</label>
|
|
<p class="text-xs text-base-content/60 ml-7">
|
|
This removes {{ .ClientShortName }} records (manifests, tags, stars, profile) stored in your PDS.
|
|
Other records in your account are not impacted.
|
|
</p>
|
|
</div>
|
|
|
|
<button type="button" id="delete-account-btn" class="btn btn-error btn-lg gap-2">
|
|
{{ icon "trash-2" "size-5" }}
|
|
Delete My {{ .ClientShortName }} Data
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
// Hold data from server (for details panel)
|
|
const holdData = {{ .HoldDataJSON }};
|
|
|
|
// Tab switching
|
|
(function() {
|
|
var validTabs = ['identity', 'devices', 'storage', 'advanced'];
|
|
|
|
function switchSettingsTab(tabId) {
|
|
// Hide all panels
|
|
document.querySelectorAll('.settings-panel').forEach(function(p) {
|
|
p.classList.add('hidden');
|
|
});
|
|
// Show selected panel
|
|
var panel = document.getElementById('tab-' + tabId);
|
|
if (panel) panel.classList.remove('hidden');
|
|
|
|
// Sidebar: toggle menu-active on <li> elements
|
|
document.querySelectorAll('.menu li[data-tab]').forEach(function(li) {
|
|
if (li.dataset.tab === tabId) {
|
|
li.classList.add('menu-active');
|
|
} else {
|
|
li.classList.remove('menu-active');
|
|
}
|
|
});
|
|
|
|
// Mobile: toggle btn-secondary/btn-ghost on buttons
|
|
document.querySelectorAll('.settings-tab-mobile').forEach(function(btn) {
|
|
if (btn.dataset.tab === tabId) {
|
|
btn.classList.remove('btn-ghost');
|
|
btn.classList.add('btn-secondary');
|
|
} else {
|
|
btn.classList.remove('btn-secondary');
|
|
btn.classList.add('btn-ghost');
|
|
}
|
|
});
|
|
|
|
// Update URL hash without adding history entry
|
|
history.replaceState(null, '', '#' + tabId);
|
|
// Dispatch custom event for HTMX lazy loading
|
|
document.body.dispatchEvent(new CustomEvent('tab:' + tabId));
|
|
}
|
|
|
|
// Helper for HTMX conditional polling
|
|
window.isTabActive = function(tabId) {
|
|
var panel = document.getElementById('tab-' + tabId);
|
|
return panel && !panel.classList.contains('hidden');
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Read initial tab from hash
|
|
var hash = window.location.hash.replace('#', '') || 'identity';
|
|
if (validTabs.indexOf(hash) === -1) hash = 'identity';
|
|
|
|
// Mobile tab click handlers
|
|
document.querySelectorAll('.settings-tab-mobile').forEach(function(btn) {
|
|
btn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
switchSettingsTab(this.dataset.tab);
|
|
});
|
|
});
|
|
|
|
// Sidebar tab click handlers
|
|
document.querySelectorAll('.menu li[data-tab] a').forEach(function(link) {
|
|
link.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
switchSettingsTab(this.parentElement.dataset.tab);
|
|
});
|
|
});
|
|
|
|
// Activate initial tab (use requestAnimationFrame to ensure HTMX has initialized)
|
|
requestAnimationFrame(function() {
|
|
switchSettingsTab(hash);
|
|
});
|
|
});
|
|
|
|
// Handle browser back/forward
|
|
window.addEventListener('hashchange', function() {
|
|
var hash = window.location.hash.replace('#', '') || 'identity';
|
|
if (validTabs.indexOf(hash) !== -1) {
|
|
switchSettingsTab(hash);
|
|
}
|
|
});
|
|
})();
|
|
|
|
// Hold Selection and Details Display
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const holdSelect = document.getElementById('default-hold');
|
|
const holdDetails = document.getElementById('hold-details');
|
|
const holdForm = document.getElementById('hold-form');
|
|
|
|
if (holdSelect) {
|
|
holdSelect.addEventListener('change', function() {
|
|
const selectedDID = this.value;
|
|
|
|
if (!selectedDID || !holdData[selectedDID]) {
|
|
holdDetails.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const hold = holdData[selectedDID];
|
|
|
|
document.getElementById('hold-did').textContent = hold.did;
|
|
document.getElementById('hold-region').textContent = hold.region || 'Unknown';
|
|
|
|
// Set access level with badge
|
|
const accessEl = document.getElementById('hold-access');
|
|
const accessLabel = {
|
|
'owner': 'Owner (Full Control)',
|
|
'crew': 'Crew Member',
|
|
'eligible': 'Open Registration',
|
|
'public': 'Public Access'
|
|
}[hold.membership] || hold.membership;
|
|
|
|
const badgeColor = {
|
|
'owner': 'badge-primary',
|
|
'crew': 'badge-secondary',
|
|
'eligible': 'badge-accent',
|
|
'public': 'badge-ghost'
|
|
}[hold.membership] || '';
|
|
|
|
accessEl.innerHTML = '<span class="badge badge-sm ' + badgeColor + '">' + accessLabel + '</span>';
|
|
|
|
// Show permissions for crew members
|
|
if (hold.membership === 'crew' && hold.permissions && hold.permissions.length > 0) {
|
|
accessEl.innerHTML += '<br><span class="text-xs text-base-content/60">Permissions: ' + hold.permissions.join(', ') + '</span>';
|
|
}
|
|
|
|
holdDetails.style.display = 'block';
|
|
});
|
|
|
|
// Re-fetch stowage and subscription when hold selection changes
|
|
holdSelect.addEventListener('change', function() {
|
|
var params = this.value ? '?hold_did=' + encodeURIComponent(this.value) : '';
|
|
htmx.ajax('GET', '/api/storage' + params, '#storage-stats');
|
|
htmx.ajax('GET', '/api/subscription' + params, {target: '#subscription-wrapper', swap: 'innerHTML'});
|
|
});
|
|
|
|
// Trigger on page load if a hold is already selected
|
|
if (holdSelect.value) {
|
|
holdSelect.dispatchEvent(new Event('change'));
|
|
}
|
|
}
|
|
|
|
// HTMX success handler - no icon reinitialization needed with SVG sprites
|
|
if (holdForm) {
|
|
holdForm.addEventListener('htmx:afterSwap', function(event) {
|
|
// SVG sprites don't need reinitialization
|
|
});
|
|
}
|
|
});
|
|
|
|
// Account Deletion JavaScript
|
|
(function() {
|
|
const deleteBtn = document.getElementById('delete-account-btn');
|
|
if (!deleteBtn) return;
|
|
|
|
deleteBtn.addEventListener('click', function() {
|
|
showDeleteConfirmationModal();
|
|
});
|
|
|
|
function showDeleteConfirmationModal() {
|
|
// Create modal using DaisyUI structure
|
|
const modal = document.createElement('div');
|
|
modal.className = 'modal modal-open';
|
|
modal.innerHTML = `
|
|
<div class="modal-box max-w-lg">
|
|
<h2 class="text-xl font-bold flex items-center gap-2 text-error">
|
|
<svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#alert-triangle"></use></svg>
|
|
Delete {{ .ClientShortName }} Data
|
|
</h2>
|
|
|
|
<div class="py-4 space-y-4">
|
|
<div class="alert alert-success">
|
|
<svg class="icon size-5" aria-hidden="true"><use href="/icons.svg#check-circle"></use></svg>
|
|
<span>Your ATProto account will <strong>NOT</strong> be affected.</span>
|
|
</div>
|
|
|
|
<p class="text-base-content/80">
|
|
This action <strong>cannot be undone</strong>. This will permanently delete:
|
|
</p>
|
|
|
|
<ul class="list-disc list-inside text-sm space-y-1 text-base-content/70">
|
|
<li>Your {{ .ClientShortName }} account and all settings</li>
|
|
<li>All authorized devices</li>
|
|
<li>Your data from all holds you're a member of</li>
|
|
${document.getElementById('delete-pds-records').checked ?
|
|
'<li>All io.atcr.* records from your ATProto PDS</li>' : ''}
|
|
</ul>
|
|
|
|
<div class="space-y-2">
|
|
<label for="confirm-delete-input" class="text-sm">Type <strong class="font-mono">DELETE {{ .Profile.Handle }}</strong> to confirm:</label>
|
|
<input type="text" id="confirm-delete-input" class="input input-bordered w-full font-mono" placeholder="DELETE {{ .Profile.Handle }}" autocomplete="off">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button type="button" class="btn" id="cancel-delete">Cancel</button>
|
|
<button type="button" class="btn btn-error gap-2" id="confirm-delete" disabled>
|
|
<svg class="icon size-4" aria-hidden="true"><use href="/icons.svg#trash-2"></use></svg>
|
|
Delete My {{ .ClientShortName }} Data
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-backdrop bg-black/50" id="modal-backdrop"></div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
|
|
// Focus the input
|
|
const confirmInput = document.getElementById('confirm-delete-input');
|
|
const confirmBtn = document.getElementById('confirm-delete');
|
|
const cancelBtn = document.getElementById('cancel-delete');
|
|
|
|
setTimeout(() => confirmInput.focus(), 100);
|
|
|
|
// Expected confirmation string
|
|
const expectedConfirmation = 'DELETE {{ .Profile.Handle }}';
|
|
|
|
// Enable button only when full confirmation is typed
|
|
confirmInput.addEventListener('input', function() {
|
|
confirmBtn.disabled = this.value !== expectedConfirmation;
|
|
});
|
|
|
|
// Handle enter key
|
|
confirmInput.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter' && this.value === expectedConfirmation) {
|
|
performAccountDeletion();
|
|
}
|
|
});
|
|
|
|
// Cancel button
|
|
cancelBtn.addEventListener('click', function() {
|
|
modal.remove();
|
|
});
|
|
|
|
// Click outside to close (on backdrop)
|
|
document.getElementById('modal-backdrop').addEventListener('click', function() {
|
|
modal.remove();
|
|
});
|
|
|
|
// Escape key to close
|
|
document.addEventListener('keydown', function escHandler(e) {
|
|
if (e.key === 'Escape') {
|
|
modal.remove();
|
|
document.removeEventListener('keydown', escHandler);
|
|
}
|
|
});
|
|
|
|
// Confirm delete
|
|
confirmBtn.addEventListener('click', performAccountDeletion);
|
|
|
|
async function performAccountDeletion() {
|
|
const deletePDS = document.getElementById('delete-pds-records').checked;
|
|
|
|
// Show loading state
|
|
confirmBtn.disabled = true;
|
|
confirmBtn.innerHTML = '<svg class="icon size-4 animate-spin" aria-hidden="true"><use href="/icons.svg#loader-2"></use></svg> Deleting...';
|
|
cancelBtn.disabled = true;
|
|
|
|
try {
|
|
const response = await fetch('/api/account', {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
delete_pds_records: deletePDS,
|
|
confirmation: expectedConfirmation
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok && result.success) {
|
|
// Show success and redirect
|
|
modal.querySelector('.modal-box').innerHTML = `
|
|
<h2 class="text-xl font-bold flex items-center gap-2 text-success">
|
|
<svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#check-circle"></use></svg>
|
|
Account Deleted
|
|
</h2>
|
|
<div class="py-4 space-y-2">
|
|
<p>Your account has been successfully deleted.</p>
|
|
<p class="text-base-content/70">Redirecting to home page...</p>
|
|
</div>
|
|
`;
|
|
setTimeout(() => {
|
|
window.location.href = '/?deleted=true';
|
|
}, 2000);
|
|
} else {
|
|
// Show error
|
|
const errors = result.errors || ['An unknown error occurred'];
|
|
modal.querySelector('.modal-box').innerHTML = `
|
|
<h2 class="text-xl font-bold flex items-center gap-2 text-error">
|
|
<svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#x-circle"></use></svg>
|
|
Deletion Failed
|
|
</h2>
|
|
<div class="py-4 space-y-4">
|
|
<p>There were errors during account deletion:</p>
|
|
<ul class="list-disc list-inside text-sm space-y-1 text-error">
|
|
${errors.map(e => '<li>' + escapeHtml(e) + '</li>').join('')}
|
|
</ul>
|
|
</div>
|
|
<div class="modal-action">
|
|
<button type="button" class="btn" onclick="this.closest('.modal').remove()">Close</button>
|
|
</div>
|
|
`;
|
|
}
|
|
} catch (err) {
|
|
console.error('Delete account error:', err);
|
|
modal.querySelector('.modal-box').innerHTML = `
|
|
<h2 class="text-xl font-bold flex items-center gap-2 text-error">
|
|
<svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#x-circle"></use></svg>
|
|
Error
|
|
</h2>
|
|
<div class="py-4">
|
|
<p>Failed to delete account: ${escapeHtml(err.message)}</p>
|
|
</div>
|
|
<div class="modal-action">
|
|
<button type="button" class="btn" onclick="this.closest('.modal').remove()">Close</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
{{ template "footer" . }}
|
|
</body>
|
|
</html>
|
|
{{ end }}
|