e16c50d854
Deploy to LXC / deploy (push) Successful in 21s
- feature_requests table with title, description, status, votes, createdBy - Submit new requests with title and description - Upvote button sorted by most votes - Status dropdown: open, planned, in-progress, done, declined - Color-coded status badges - Delete button per request - Sidebar nav item with lightbulb icon Run db:push on the server to create the new table. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
130 lines
6.0 KiB
Svelte
130 lines
6.0 KiB
Svelte
<script lang="ts">
|
|
import { enhance } from '$app/forms';
|
|
import { timeAgo } from '$lib/utils/date.js';
|
|
|
|
let { data, form } = $props();
|
|
let showNewForm = $state(false);
|
|
|
|
const statusColors: Record<string, string> = {
|
|
open: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
|
|
planned: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
|
|
'in-progress': 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
|
|
done: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
|
|
declined: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400'
|
|
};
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Feature Requests - My Collection</title>
|
|
</svelte:head>
|
|
|
|
<div class="mx-auto max-w-3xl">
|
|
<div class="mb-6 flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Feature Requests</h1>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Suggest and vote on new features.</p>
|
|
</div>
|
|
<button type="button" onclick={() => (showNewForm = !showNewForm)}
|
|
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
|
|
{showNewForm ? 'Cancel' : 'New Request'}
|
|
</button>
|
|
</div>
|
|
|
|
{#if form?.error}
|
|
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-300">{form.error}</div>
|
|
{/if}
|
|
|
|
{#if showNewForm}
|
|
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
|
<form method="POST" action="?/create" use:enhance={() => {
|
|
return async ({ update, result }) => {
|
|
await update();
|
|
if (result.type === 'success') showNewForm = false;
|
|
};
|
|
}} class="space-y-3">
|
|
<div>
|
|
<label for="title" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Title *</label>
|
|
<input type="text" id="title" name="title" required
|
|
placeholder="Short description of the feature..."
|
|
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" />
|
|
</div>
|
|
<div>
|
|
<label for="description" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Description</label>
|
|
<textarea id="description" name="description" rows="3"
|
|
placeholder="Why is this needed? How should it work?"
|
|
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400"></textarea>
|
|
</div>
|
|
<button type="submit" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">Submit Request</button>
|
|
</form>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if data.requests.length === 0 && !showNewForm}
|
|
<div class="rounded-lg border border-gray-200 bg-white p-12 text-center dark:border-gray-700 dark:bg-gray-800">
|
|
<p class="text-gray-500 dark:text-gray-400">No feature requests yet. Be the first to suggest one!</p>
|
|
</div>
|
|
{:else}
|
|
<div class="space-y-3">
|
|
{#each data.requests as req}
|
|
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
|
|
<div class="flex items-start gap-3">
|
|
<!-- Vote button -->
|
|
<form method="POST" action="?/vote" use:enhance class="flex-shrink-0">
|
|
<input type="hidden" name="id" value={req.id} />
|
|
<button type="submit" class="flex flex-col items-center rounded-md border border-gray-200 px-2 py-1 text-gray-500 hover:border-blue-300 hover:text-blue-600 dark:border-gray-600 dark:text-gray-400 dark:hover:border-blue-500 dark:hover:text-blue-400" title="Upvote">
|
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
|
|
</svg>
|
|
<span class="text-xs font-medium">{req.votes}</span>
|
|
</button>
|
|
</form>
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
<h3 class="font-medium text-gray-900 dark:text-white">{req.title}</h3>
|
|
<span class="rounded-full px-2 py-0.5 text-xs font-medium {statusColors[req.status] ?? statusColors.open}">
|
|
{req.status}
|
|
</span>
|
|
</div>
|
|
{#if req.description}
|
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">{req.description}</p>
|
|
{/if}
|
|
<div class="mt-2 flex items-center gap-3 text-xs text-gray-400 dark:text-gray-500">
|
|
{#if req.createdBy}
|
|
<span>{req.createdBy}</span>
|
|
{/if}
|
|
<span>{timeAgo(req.createdAt)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Admin actions -->
|
|
<div class="flex items-center gap-1">
|
|
<select onchange={(e) => {
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = '?/updateStatus';
|
|
form.innerHTML = `<input type="hidden" name="id" value="${req.id}" /><input type="hidden" name="status" value="${(e.target as HTMLSelectElement).value}" />`;
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}} class="rounded border border-gray-200 px-1 py-0.5 text-xs text-gray-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400">
|
|
{#each ['open', 'planned', 'in-progress', 'done', 'declined'] as s}
|
|
<option value={s} selected={s === req.status}>{s}</option>
|
|
{/each}
|
|
</select>
|
|
<form method="POST" action="?/delete" use:enhance>
|
|
<input type="hidden" name="id" value={req.id} />
|
|
<button type="submit" class="rounded p-1 text-gray-400 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-400" title="Delete">
|
|
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|