Files
buildfor_life_repair/src/routes/(app)/feature-requests/+page.svelte
T
grabowski e16c50d854
Deploy to LXC / deploy (push) Successful in 21s
Add feature requests page with voting and status tracking
- 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>
2026-04-10 10:03:56 +07:00

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>