feat: add admin panel, stats API, and url support across frontend
- Add adminApi with full CRUD for users, companies, listings, oil-prices - Add statsApi for fetching latest market price aggregates - Add AdminTable component and /admin page for site management - Add StatsPrice, UpdateUserRequest, UpdateOilPriceRequest types - Add url field support in listing create/edit forms - Update state SVG data for all 6 New England states - Update county page to display richer listing info (phone, url, bio%) - Misc layout and style updates across vendor and public routes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
100
src/lib/components/AdminTable.svelte
Executable file
100
src/lib/components/AdminTable.svelte
Executable file
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
export let data: any[] = [];
|
||||
export let columns: { key: string; label: string; type?: 'text' | 'number' | 'boolean' | 'date'; editable?: boolean }[] = [];
|
||||
export let onSave: (item: any) => Promise<void>;
|
||||
export let onDelete: (item: any) => Promise<void>;
|
||||
|
||||
let editingId: number | null = null;
|
||||
let editValues: any = {};
|
||||
let isSaving = false;
|
||||
let isDeleting = false;
|
||||
|
||||
function startEdit(item: any) {
|
||||
editingId = item.id;
|
||||
editValues = { ...item };
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!editingId) return;
|
||||
isSaving = true;
|
||||
try {
|
||||
await onSave(editValues);
|
||||
editingId = null;
|
||||
editValues = {};
|
||||
} catch (e) {
|
||||
alert('Failed to save: ' + e);
|
||||
} finally {
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
editingId = null;
|
||||
editValues = {};
|
||||
}
|
||||
|
||||
async function handleDelete(item: any) {
|
||||
if (confirm('Are you sure you want to delete this item?')) {
|
||||
isDeleting = true;
|
||||
try {
|
||||
await onDelete(item);
|
||||
} catch (e) {
|
||||
alert('Failed to delete: ' + e);
|
||||
} finally {
|
||||
isDeleting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-xs md:table-sm table-pin-rows">
|
||||
<thead>
|
||||
<tr>
|
||||
{#each columns as col}
|
||||
<th>{col.label}</th>
|
||||
{/each}
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data as item (item.id)}
|
||||
{@const isEditing = editingId === item.id}
|
||||
<tr class="hover">
|
||||
{#each columns as col}
|
||||
<td>
|
||||
{#if isEditing && col.editable}
|
||||
{#if col.type === 'boolean'}
|
||||
<input type="checkbox" class="checkbox checkbox-sm" bind:checked={editValues[col.key]} />
|
||||
{:else if col.type === 'number'}
|
||||
<input type="number" class="input input-bordered input-sm w-full" bind:value={editValues[col.key]} />
|
||||
{:else}
|
||||
<input type="text" class="input input-bordered input-sm w-full" bind:value={editValues[col.key]} />
|
||||
{/if}
|
||||
{:else}
|
||||
{#if col.type === 'boolean'}
|
||||
<input type="checkbox" class="checkbox checkbox-xs" checked={item[col.key]} disabled />
|
||||
{:else}
|
||||
<div class="truncate max-w-[200px]" title={item[col.key]}>
|
||||
{item[col.key] ?? ''}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
<td class="flex gap-2">
|
||||
{#if isEditing}
|
||||
<button class="btn btn-xs btn-success" on:click={save} disabled={isSaving}>
|
||||
{#if isSaving}Saving...{:else}Save{/if}
|
||||
</button>
|
||||
<button class="btn btn-xs btn-ghost" on:click={cancel} disabled={isSaving}>Cancel</button>
|
||||
{:else}
|
||||
<button class="btn btn-xs btn-primary" on:click={() => startEdit(item)}>Edit</button>
|
||||
<button class="btn btn-xs btn-error" on:click={() => handleDelete(item)} disabled={isDeleting}>Delete</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
Reference in New Issue
Block a user