password_manager/src/components/EntryForm.svelte

227 lines
6.0 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script>
import { addEntry, updateEntry, getEntryById, getGroups } from '../lib/storage/db.js'
import { encrypt, decrypt } from '../lib/crypto/crypto.js'
import { createEntry, updateEntry as updateEntryModel, validateEntry } from '../lib/models/schema.js'
import { generatePassword } from '../lib/crypto/crypto.js'
import { app } from '../lib/stores/app.svelte.js'
import PasswordGenerator from './PasswordGenerator.svelte'
import { autofocus } from '../lib/autofocus.js'
let { entryId, onSave, onCancel } = $props()
let title = $state('')
let username = $state('')
let password = $state('')
let url = $state('')
let notes = $state('')
let groupId = $state('')
let passwordVisible = $state(false)
let groups = $state([])
let loading = $state(true)
let error = $state('')
let saving = $state(false)
let isEdit = $state(false)
let formErrors = $state([])
async function loadForm() {
loading = true
try {
groups = await getGroups()
if (entryId) {
isEdit = true
const entry = await getEntryById(entryId)
if (entry) {
title = entry.title
username = entry.username
password = await decrypt(entry.encryptedPassword, app.encryptionKey)
url = entry.url || ''
notes = entry.notes || ''
groupId = entry.groupId || ''
} else {
error = 'Entry not found'
}
}
} catch (e) {
error = 'Failed to load form: ' + e.message
}
loading = false
}
loadForm()
async function handleSubmit() {
formErrors = []
error = ''
saving = true
try {
const validation = validateEntry({ title, username, encryptedPassword: password })
if (!validation.valid) {
formErrors = validation.errors
saving = false
return
}
const encryptedPassword = await encrypt(password, app.encryptionKey)
if (isEdit) {
const existing = await getEntryById(entryId)
const updated = updateEntryModel(existing, {
title,
username,
encryptedPassword,
url,
notes,
groupId,
})
await updateEntry(updated)
} else {
const entry = createEntry({
title,
username,
encryptedPassword,
url,
notes,
groupId,
})
await addEntry(entry)
}
onSave()
} catch (e) {
error = 'Failed to save: ' + e.message
}
saving = false
}
</script>
<div class="entry-form">
{#if loading}
<div class="loading">Loading...</div>
{:else}
{#if error}
<div class="error-banner">{error}</div>
{/if}
<form class="form-card" onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
{#if formErrors.length > 0}
<div class="validation-errors">
{#each formErrors as err}
<div class="validation-error">{err}</div>
{/each}
</div>
{/if}
<div class="form-group">
<label for="title">Title *</label>
<input id="title" type="text" bind:value={title} placeholder="e.g. GitHub, Gmail" use:autofocus={!isEdit} />
</div>
<div class="form-group">
<label for="username">Username / Email</label>
<input id="username" type="text" bind:value={username} placeholder="username or email" />
</div>
<div class="form-group">
<label for="password">Password *</label>
<div class="password-input-group">
<input
id="password"
type={passwordVisible ? 'text' : 'password'}
bind:value={password}
placeholder="Password"
/>
<button type="button" class="btn btn-ghost btn-sm" onclick={() => passwordVisible = !passwordVisible} title="Toggle visibility">
{passwordVisible ? '🙈' : '👁'}
</button>
<button type="button" class="btn btn-ghost btn-sm" onclick={() => password = generatePassword({ length: 16 })} title="Generate password">
🎲
</button>
</div>
</div>
<div class="form-group">
<label for="url">URL</label>
<input id="url" type="url" bind:value={url} placeholder="https://example.com" />
</div>
<div class="form-group">
<label for="group">Group</label>
<select id="group" bind:value={groupId}>
<option value="">No group</option>
{#each groups as group}
<option value={group.id}>{group.name}</option>
{/each}
</select>
</div>
<div class="form-group">
<label for="notes">Notes</label>
<textarea id="notes" bind:value={notes} placeholder="Any additional notes..."></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" disabled={saving}>
{saving ? 'Saving...' : (isEdit ? '💾 Update' : ' Create')}
</button>
<button type="button" class="btn btn-ghost" onclick={onCancel}>Cancel</button>
</div>
</form>
{/if}
</div>
<style>
.loading {
text-align: center;
padding: 3rem;
color: var(--color-text-muted);
}
.error-banner {
padding: 12px 16px;
background: rgba(229, 72, 77, 0.15);
border: 1px solid rgba(229, 72, 77, 0.4);
border-radius: var(--radius-md);
color: var(--color-danger);
font-size: 0.85rem;
margin-bottom: 16px;
}
.form-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 24px;
max-width: 500px;
}
.validation-errors {
margin-bottom: 16px;
padding: 12px;
background: rgba(251, 191, 36, 0.1);
border: 1px solid rgba(251, 191, 36, 0.3);
border-radius: var(--radius-md);
}
.validation-error {
font-size: 0.85rem;
color: var(--color-warning);
}
.password-input-group {
display: flex;
gap: 8px;
}
.password-input-group input {
flex: 1;
}
.form-actions {
display: flex;
gap: 8px;
margin-top: 20px;
}
</style>