refactor(web): Modal component; admin + discover modals migrated (W2)

This commit is contained in:
2026-05-08 05:37:58 -04:00
parent 92813ba1bb
commit 970752a153
7 changed files with 543 additions and 512 deletions
+150 -195
View File
@@ -17,6 +17,7 @@
type CreateUserInput
} from '$lib/api/admin';
import { errCode } from '$lib/api/errors';
import Modal from '$lib/components/Modal.svelte';
const client = useQueryClient();
@@ -372,204 +373,158 @@
</div>
{/if}
{#if showCreateModal}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-center justify-center"
style="background: rgba(0,0,0,0.5);"
onclick={() => { showCreateModal = false; }}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby="create-user-title"
class="w-full max-w-md rounded-xl border border-border bg-surface p-5 shadow-lg"
onclick={(e) => e.stopPropagation()}
tabindex="-1"
>
<h3 id="create-user-title" class="font-display text-lg font-medium text-text-primary">New user</h3>
<form onsubmit={onSubmitCreate} class="mt-4 space-y-3">
<div>
<label for="cu-username" class="block text-sm text-text-secondary">Username</label>
<input
id="cu-username"
type="text"
required
minlength="3"
maxlength="32"
bind:value={createForm.username}
class="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label for="cu-display" class="block text-sm text-text-secondary">
Display name <span class="text-text-muted">(optional)</span>
</label>
<input
id="cu-display"
type="text"
maxlength="64"
bind:value={createForm.display_name}
class="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label for="cu-password" class="block text-sm text-text-secondary">Password</label>
<input
id="cu-password"
type="password"
required
minlength="8"
bind:value={createForm.password}
class="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label for="cu-confirm" class="block text-sm text-text-secondary">Confirm password</label>
<input
id="cu-confirm"
type="password"
required
minlength="8"
bind:value={createForm.confirmPassword}
class="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<label class="flex items-center gap-2 text-sm text-text-secondary">
<input type="checkbox" bind:checked={createForm.is_admin} />
Make this user an admin
</label>
<div class="flex justify-end gap-2 pt-2">
<button
type="button"
disabled={saving}
class="rounded-md border border-border px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary"
onclick={() => { showCreateModal = false; }}
>
Cancel
</button>
<button
type="submit"
disabled={saving}
class="inline-flex items-center gap-1 rounded-md bg-action-primary px-3 py-1.5 text-sm text-action-fg disabled:cursor-not-allowed disabled:opacity-50"
>
{saving ? 'Creating…' : 'Create user'}
</button>
</div>
</form>
<Modal
title="New user"
open={showCreateModal}
onClose={() => { showCreateModal = false; }}
>
<form onsubmit={onSubmitCreate} class="space-y-3">
<div>
<label for="cu-username" class="block text-sm text-text-secondary">Username</label>
<input
id="cu-username"
type="text"
required
minlength="3"
maxlength="32"
bind:value={createForm.username}
class="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
</div>
{/if}
<div>
<label for="cu-display" class="block text-sm text-text-secondary">
Display name <span class="text-text-muted">(optional)</span>
</label>
<input
id="cu-display"
type="text"
maxlength="64"
bind:value={createForm.display_name}
class="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label for="cu-password" class="block text-sm text-text-secondary">Password</label>
<input
id="cu-password"
type="password"
required
minlength="8"
bind:value={createForm.password}
class="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label for="cu-confirm" class="block text-sm text-text-secondary">Confirm password</label>
<input
id="cu-confirm"
type="password"
required
minlength="8"
bind:value={createForm.confirmPassword}
class="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<label class="flex items-center gap-2 text-sm text-text-secondary">
<input type="checkbox" bind:checked={createForm.is_admin} />
Make this user an admin
</label>
<div class="flex justify-end gap-2 pt-2">
<button
type="button"
disabled={saving}
class="rounded-md border border-border px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary"
onclick={() => { showCreateModal = false; }}
>
Cancel
</button>
<button
type="submit"
disabled={saving}
class="inline-flex items-center gap-1 rounded-md bg-action-primary px-3 py-1.5 text-sm text-action-fg disabled:cursor-not-allowed disabled:opacity-50"
>
{saving ? 'Creating…' : 'Create user'}
</button>
</div>
</form>
</Modal>
{#if resetPasswordTarget}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-center justify-center"
style="background: rgba(0,0,0,0.5);"
onclick={() => { resetPasswordTarget = null; resetPasswordValue = ''; resetPasswordConfirm = ''; }}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby="reset-password-title"
class="w-full max-w-md rounded-xl border border-border bg-surface p-5 shadow-lg"
onclick={(e) => e.stopPropagation()}
tabindex="-1"
>
<h3 id="reset-password-title" class="font-display text-lg font-medium text-text-primary">
Reset password for {resetPasswordTarget.username}
</h3>
<p class="mt-1 text-sm text-text-secondary">
The user will need to log in with the new password.
</p>
<form onsubmit={onSubmitResetPassword} class="mt-4 space-y-3">
<div>
<label for="rp-password" class="block text-sm text-text-secondary">New password</label>
<input
id="rp-password"
type="password"
required
minlength="8"
bind:value={resetPasswordValue}
class="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label for="rp-confirm" class="block text-sm text-text-secondary">Confirm</label>
<input
id="rp-confirm"
type="password"
required
minlength="8"
bind:value={resetPasswordConfirm}
class="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div class="flex justify-end gap-2 pt-2">
<button
type="button"
disabled={saving}
class="rounded-md border border-border px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary"
onclick={() => { resetPasswordTarget = null; resetPasswordValue = ''; resetPasswordConfirm = ''; }}
>
Cancel
</button>
<button
type="submit"
disabled={saving}
class="inline-flex items-center gap-1 rounded-md bg-action-primary px-3 py-1.5 text-sm text-action-fg disabled:cursor-not-allowed disabled:opacity-50"
>
{saving ? 'Resetting…' : 'Set password'}
</button>
</div>
</form>
<Modal
title={resetPasswordTarget ? `Reset password for ${resetPasswordTarget.username}` : ''}
open={resetPasswordTarget !== null}
onClose={() => { resetPasswordTarget = null; resetPasswordValue = ''; resetPasswordConfirm = ''; }}
>
<p class="-mt-2 mb-3 text-sm text-text-secondary">
The user will need to log in with the new password.
</p>
<form onsubmit={onSubmitResetPassword} class="space-y-3">
<div>
<label for="rp-password" class="block text-sm text-text-secondary">New password</label>
<input
id="rp-password"
type="password"
required
minlength="8"
bind:value={resetPasswordValue}
class="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
</div>
{/if}
<div>
<label for="rp-confirm" class="block text-sm text-text-secondary">Confirm</label>
<input
id="rp-confirm"
type="password"
required
minlength="8"
bind:value={resetPasswordConfirm}
class="mt-1 w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div class="flex justify-end gap-2 pt-2">
<button
type="button"
disabled={saving}
class="rounded-md border border-border px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary"
onclick={() => { resetPasswordTarget = null; resetPasswordValue = ''; resetPasswordConfirm = ''; }}
>
Cancel
</button>
<button
type="submit"
disabled={saving}
class="inline-flex items-center gap-1 rounded-md bg-action-primary px-3 py-1.5 text-sm text-action-fg disabled:cursor-not-allowed disabled:opacity-50"
>
{saving ? 'Resetting…' : 'Set password'}
</button>
</div>
</form>
</Modal>
{#if confirmDeleteTarget}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-center justify-center"
style="background: rgba(0,0,0,0.5);"
onclick={() => { confirmDeleteTarget = null; }}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby="delete-user-title"
class="w-full max-w-md rounded-xl border border-border bg-surface p-5 shadow-lg"
onclick={(e) => e.stopPropagation()}
tabindex="-1"
<Modal
title={confirmDeleteTarget ? `Delete ${confirmDeleteTarget.username}?` : ''}
open={confirmDeleteTarget !== null}
onClose={() => { confirmDeleteTarget = null; }}
>
<p class="-mt-2 text-sm text-text-secondary">
This permanently deletes the account and cascades through plays, likes,
sessions, and quarantine. The action cannot be undone.
</p>
<div class="mt-5 flex justify-end gap-2">
<button
type="button"
disabled={saving}
class="rounded-md border border-border px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary"
onclick={() => { confirmDeleteTarget = null; }}
>
<h3 id="delete-user-title" class="font-display text-lg font-medium text-text-primary">
Delete {confirmDeleteTarget.username}?
</h3>
<p class="mt-2 text-sm text-text-secondary">
This permanently deletes the account and cascades through plays, likes,
sessions, and quarantine. The action cannot be undone.
</p>
<div class="mt-5 flex justify-end gap-2">
<button
type="button"
disabled={saving}
class="rounded-md border border-border px-3 py-1.5 text-sm text-text-secondary hover:text-text-primary"
onclick={() => { confirmDeleteTarget = null; }}
>
Cancel
</button>
<button
type="button"
disabled={saving}
class="inline-flex items-center gap-1 rounded-md bg-action-destructive px-3 py-1.5 text-sm text-action-fg disabled:cursor-not-allowed disabled:opacity-50"
onclick={onConfirmDelete}
>
{saving ? 'Deleting…' : 'Delete'}
</button>
</div>
</div>
Cancel
</button>
<button
type="button"
disabled={saving}
class="inline-flex items-center gap-1 rounded-md bg-action-destructive px-3 py-1.5 text-sm text-action-fg disabled:cursor-not-allowed disabled:opacity-50"
onclick={onConfirmDelete}
>
{saving ? 'Deleting…' : 'Delete'}
</button>
</div>
{/if}
</Modal>