feat: feature requests board with voting

- [FEATURES] tab next to PINNED in channel header
- Create, vote/unvote, filter by status (open/planned/done/rejected)
- Sort by vote count (most voted first)
- Status selector for moderation
- DB: feature_requests + feature_request_votes tables
- Backend: full CRUD + vote endpoints under /servers/{id}/feature-requests
This commit is contained in:
2026-07-02 13:11:52 -04:00
parent 3002626ac4
commit 5ca5e018ca
7 changed files with 525 additions and 1 deletions
+56
View File
@@ -0,0 +1,56 @@
import { create } from "zustand";
import api from "../lib/api.ts";
export interface FeatureRequest {
id: string;
server_id: string;
channel_id: string;
author_id: string;
title: string;
description: string;
status: "open" | "planned" | "done" | "rejected";
vote_count: number;
voters: string[];
created_at: string;
updated_at: string;
}
interface FeatureRequestStore {
requestsByServer: Record<string, FeatureRequest[]>;
fetchRequests: (serverId: string) => Promise<void>;
createRequest: (serverId: string, title: string, description: string, channelId?: string) => Promise<void>;
vote: (serverId: string, requestId: string) => Promise<void>;
unvote: (serverId: string, requestId: string) => Promise<void>;
updateStatus: (serverId: string, requestId: string, status: string) => Promise<void>;
}
export const useFeatureRequestStore = create<FeatureRequestStore>((set, get) => ({
requestsByServer: {},
fetchRequests: async (serverId: string) => {
const data = await api.get<FeatureRequest[]>(`/servers/${serverId}/feature-requests`);
set((s) => ({
requestsByServer: { ...s.requestsByServer, [serverId]: data },
}));
},
createRequest: async (serverId, title, description, channelId) => {
await api.post(`/servers/${serverId}/feature-requests`, { title, description, channel_id: channelId });
await get().fetchRequests(serverId);
},
vote: async (serverId, requestId) => {
await api.post(`/servers/${serverId}/feature-requests/${requestId}/vote`);
await get().fetchRequests(serverId);
},
unvote: async (serverId, requestId) => {
await api.delete(`/servers/${serverId}/feature-requests/${requestId}/vote`);
await get().fetchRequests(serverId);
},
updateStatus: async (serverId, requestId, status) => {
await api.patch(`/servers/${serverId}/feature-requests/${requestId}/status`, { status });
await get().fetchRequests(serverId);
},
}));