password_manager/src/components/EntryList.svelte

235 lines
5.4 KiB
Svelte

<script>
import { getEntries, searchEntries } from '../lib/storage/db.js'
import { search as searchStore } from '../lib/stores/search.svelte.js'
let entries = $state([])
let loading = $state(true)
let error = $state('')
let resultCount = $state(0)
let dragging = $state(false)
let { onSelect, onAdd } = $props()
async function loadEntries() {
loading = true
error = ''
try {
const query = searchStore.debouncedQuery.trim()
const groupId = searchStore.activeGroupId
if (query) {
// Search with optional group filter
const options = groupId !== 'all' ? { groupId } : {}
entries = await searchEntries(query, options)
} else if (groupId !== 'all') {
// Filter by group only
entries = await getEntries({ groupId })
} else {
// Show all
entries = await getEntries()
}
resultCount = entries.length
} catch (e) {
error = 'Failed to load entries: ' + e.message
}
loading = false
}
// Reload when debounced search query, active group filter, or refresh trigger changes
$effect(() => {
searchStore.debouncedQuery
searchStore.activeGroupId
searchStore.refreshTrigger
loadEntries()
})
</script>
<div class="entry-list">
{#if loading}
<div class="loading">Loading entries...</div>
{:else if error}
<div class="error-banner">{error}</div>
{:else if entries.length === 0}
<div class="empty-state">
<p class="empty-icon">{searchStore.query ? '🔍' : '🔑'}</p>
<p class="empty-text">{searchStore.query ? 'No results found' : 'No entries yet'}</p>
<p class="empty-hint">
{searchStore.query
? 'Try a different search term'
: 'Add your first login credential to get started'}
</p>
{#if !searchStore.query}
<button class="btn btn-primary mt-3" onclick={onAdd}>+ New Entry</button>
{/if}
</div>
{:else}
<div class="results-info">
<span class="text-sm text-muted">
{resultCount} entr{resultCount === 1 ? 'y' : 'ies'}
{#if searchStore.query}
matching "<strong>{searchStore.query}</strong>"
{/if}
</span>
</div>
<table class="entries-table">
<thead>
<tr>
<th>Title</th>
<th>Username</th>
<th>URL</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{#each entries as entry (entry.id)}
<tr
draggable={true}
onclick={() => onSelect(entry.id)}
ondragstart={(e) => { dragging = true; e.dataTransfer.setData('text/plain', entry.id); e.dataTransfer.effectAllowed = 'move'; }}
ondragend={() => { dragging = false; }}
class="entry-row {dragging ? 'dragging' : ''}"
>
<td>
<span class="drag-handle" aria-hidden="true"></span>
<span class="entry-title">{entry.title}</span>
</td>
<td>
<span class="entry-username">{entry.username || '—'}</span>
</td>
<td>
<span class="entry-url truncate">{entry.url || '—'}</span>
</td>
<td>
<span class="entry-notes truncate">{entry.notes || '—'}</span>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<style>
.loading, .empty-state {
text-align: center;
padding: 3rem 1rem;
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;
}
.empty-icon {
font-size: 3rem;
margin-bottom: 0.5rem;
}
.empty-text {
font-size: 1.1rem;
font-weight: 500;
color: var(--color-text);
}
.empty-hint {
font-size: 0.85rem;
color: var(--color-text-muted);
}
.results-info {
padding: 8px 0;
margin-bottom: 8px;
}
.entries-table {
width: 100%;
border-collapse: collapse;
}
.entries-table th {
text-align: left;
padding: 8px 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
border-bottom: 1px solid var(--color-border);
}
.entry-row {
cursor: grab;
transition: background-color 150ms, opacity 150ms;
}
.entry-row:active {
cursor: grabbing;
}
.entry-row:hover {
background: var(--color-surface-hover);
}
.entry-row.dragging {
opacity: 0.35;
}
.entry-row td {
padding: 10px 12px;
font-size: 0.875rem;
border-bottom: 1px solid var(--color-border);
}
.drag-handle {
color: var(--color-text-muted);
opacity: 0.3;
margin-right: 6px;
font-size: 0.9rem;
user-select: none;
transition: opacity 150ms;
}
.entry-row:hover .drag-handle {
opacity: 0.7;
}
.entry-title {
font-weight: 500;
}
.entry-username {
color: var(--color-text-muted);
}
.entry-url {
color: var(--color-text-muted);
max-width: 200px;
}
.entry-notes {
color: var(--color-text-muted);
max-width: 200px;
}
@media (max-width: 768px) {
.entries-table th:nth-child(4),
.entry-row td:nth-child(4) {
display: none;
}
}
@media (max-width: 600px) {
.entries-table th:nth-child(3),
.entry-row td:nth-child(3) {
display: none;
}
}
</style>