From 0ac69ee8405bed70da4cfebe0f3ccd92de7b849f Mon Sep 17 00:00:00 2001 From: smueller Date: Wed, 22 Jul 2026 15:34:20 +0200 Subject: [PATCH] Refactor giveaway job processing and update environment documentation - Enhanced the handling of the `giveawayCreate` job in the bot's job processing for improved consistency. - Clarified the usage of `BOT_TOKEN` in the `.env.example` file for both the bot and WebUI. - Integrated the `bullmq` dependency into the WebUI package to bolster job management capabilities. --- .../app/api/guilds/[guildId]/stats/route.ts | 46 +++++++ .../items/[suggestionId]/action/route.ts | 34 +++++ .../[guildId]/suggestions/items/route.ts | 24 ++++ .../api/guilds/[guildId]/suggestions/route.ts | 46 +++++++ .../dashboard/[guildId]/birthdays/loading.tsx | 13 ++ .../dashboard/[guildId]/birthdays/page.tsx | 22 ++++ .../dashboard/[guildId]/starboard/loading.tsx | 13 ++ .../dashboard/[guildId]/starboard/page.tsx | 22 ++++ .../app/dashboard/[guildId]/stats/loading.tsx | 13 ++ .../app/dashboard/[guildId]/stats/page.tsx | 22 ++++ .../dashboard/[guildId]/tempvoice/loading.tsx | 13 ++ .../dashboard/[guildId]/tempvoice/page.tsx | 22 ++++ .../src/components/modules/birthdays-form.tsx | 78 ++++++++++++ .../src/components/modules/starboard-form.tsx | 118 ++++++++++++++++++ .../src/components/modules/stats-form.tsx | 89 +++++++++++++ .../src/components/modules/tempvoice-form.tsx | 79 ++++++++++++ 16 files changed, 654 insertions(+) create mode 100644 apps/webui/src/app/api/guilds/[guildId]/stats/route.ts create mode 100644 apps/webui/src/app/api/guilds/[guildId]/suggestions/items/[suggestionId]/action/route.ts create mode 100644 apps/webui/src/app/api/guilds/[guildId]/suggestions/items/route.ts create mode 100644 apps/webui/src/app/api/guilds/[guildId]/suggestions/route.ts create mode 100644 apps/webui/src/app/dashboard/[guildId]/birthdays/loading.tsx create mode 100644 apps/webui/src/app/dashboard/[guildId]/birthdays/page.tsx create mode 100644 apps/webui/src/app/dashboard/[guildId]/starboard/loading.tsx create mode 100644 apps/webui/src/app/dashboard/[guildId]/starboard/page.tsx create mode 100644 apps/webui/src/app/dashboard/[guildId]/stats/loading.tsx create mode 100644 apps/webui/src/app/dashboard/[guildId]/stats/page.tsx create mode 100644 apps/webui/src/app/dashboard/[guildId]/tempvoice/loading.tsx create mode 100644 apps/webui/src/app/dashboard/[guildId]/tempvoice/page.tsx create mode 100644 apps/webui/src/components/modules/birthdays-form.tsx create mode 100644 apps/webui/src/components/modules/starboard-form.tsx create mode 100644 apps/webui/src/components/modules/stats-form.tsx create mode 100644 apps/webui/src/components/modules/tempvoice-form.tsx diff --git a/apps/webui/src/app/api/guilds/[guildId]/stats/route.ts b/apps/webui/src/app/api/guilds/[guildId]/stats/route.ts new file mode 100644 index 0000000..1aac9fb --- /dev/null +++ b/apps/webui/src/app/api/guilds/[guildId]/stats/route.ts @@ -0,0 +1,46 @@ +import { StatsConfigDashboardPatchSchema } from '@nexumi/shared'; +import { type NextRequest, NextResponse } from 'next/server'; +import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth'; +import { writeDashboardAudit } from '@/lib/audit'; +import { getStatsDashboard, updateStatsDashboard } from '@/lib/module-configs/stats'; +import { prisma } from '@/lib/prisma'; + +interface RouteParams { + params: Promise<{ guildId: string }>; +} + +export async function GET(_request: NextRequest, { params }: RouteParams) { + const { guildId } = await params; + try { + await requireGuildAccess(guildId); + const config = await getStatsDashboard(guildId); + return NextResponse.json(config); + } catch (error) { + return toApiErrorResponse(error); + } +} + +export async function PATCH(request: NextRequest, { params }: RouteParams) { + const { guildId } = await params; + try { + const session = await requireGuildAccess(guildId); + const body = await request.json(); + const patch = StatsConfigDashboardPatchSchema.parse(body); + + const before = await getStatsDashboard(guildId); + const after = await updateStatsDashboard(guildId, patch); + + await writeDashboardAudit(prisma, { + guildId, + actorUserId: session.user.id, + action: 'stats.update', + path: `/dashboard/${guildId}/stats`, + before, + after + }); + + return NextResponse.json(after); + } catch (error) { + return toApiErrorResponse(error); + } +} diff --git a/apps/webui/src/app/api/guilds/[guildId]/suggestions/items/[suggestionId]/action/route.ts b/apps/webui/src/app/api/guilds/[guildId]/suggestions/items/[suggestionId]/action/route.ts new file mode 100644 index 0000000..63cbcff --- /dev/null +++ b/apps/webui/src/app/api/guilds/[guildId]/suggestions/items/[suggestionId]/action/route.ts @@ -0,0 +1,34 @@ +import { SuggestionActionSchema } from '@nexumi/shared'; +import { type NextRequest, NextResponse } from 'next/server'; +import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth'; +import { writeDashboardAudit } from '@/lib/audit'; +import { applySuggestionAction } from '@/lib/module-configs/suggestions'; +import { prisma } from '@/lib/prisma'; + +interface RouteParams { + params: Promise<{ guildId: string; suggestionId: string }>; +} + +export async function POST(request: NextRequest, { params }: RouteParams) { + const { guildId, suggestionId } = await params; + try { + const session = await requireGuildAccess(guildId); + const body = await request.json(); + const action = SuggestionActionSchema.parse(body); + + const after = await applySuggestionAction(guildId, suggestionId, action, session.user.id); + + await writeDashboardAudit(prisma, { + guildId, + actorUserId: session.user.id, + action: `suggestions.${action.status}`, + path: `/dashboard/${guildId}/suggestions`, + before: null, + after + }); + + return NextResponse.json(after); + } catch (error) { + return toApiErrorResponse(error); + } +} diff --git a/apps/webui/src/app/api/guilds/[guildId]/suggestions/items/route.ts b/apps/webui/src/app/api/guilds/[guildId]/suggestions/items/route.ts new file mode 100644 index 0000000..7f351f1 --- /dev/null +++ b/apps/webui/src/app/api/guilds/[guildId]/suggestions/items/route.ts @@ -0,0 +1,24 @@ +import { SuggestionListQuerySchema } from '@nexumi/shared'; +import { type NextRequest, NextResponse } from 'next/server'; +import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth'; +import { listSuggestions } from '@/lib/module-configs/suggestions'; + +interface RouteParams { + params: Promise<{ guildId: string }>; +} + +export async function GET(request: NextRequest, { params }: RouteParams) { + const { guildId } = await params; + try { + await requireGuildAccess(guildId); + const searchParams = request.nextUrl.searchParams; + const query = SuggestionListQuerySchema.parse({ + status: searchParams.get('status') ?? undefined, + limit: searchParams.get('limit') ?? undefined + }); + const suggestions = await listSuggestions(guildId, query); + return NextResponse.json({ suggestions }); + } catch (error) { + return toApiErrorResponse(error); + } +} diff --git a/apps/webui/src/app/api/guilds/[guildId]/suggestions/route.ts b/apps/webui/src/app/api/guilds/[guildId]/suggestions/route.ts new file mode 100644 index 0000000..302aac2 --- /dev/null +++ b/apps/webui/src/app/api/guilds/[guildId]/suggestions/route.ts @@ -0,0 +1,46 @@ +import { SuggestionConfigDashboardPatchSchema } from '@nexumi/shared'; +import { type NextRequest, NextResponse } from 'next/server'; +import { requireGuildAccess, toApiErrorResponse } from '@/lib/auth'; +import { writeDashboardAudit } from '@/lib/audit'; +import { getSuggestionConfigDashboard, updateSuggestionConfigDashboard } from '@/lib/module-configs/suggestions'; +import { prisma } from '@/lib/prisma'; + +interface RouteParams { + params: Promise<{ guildId: string }>; +} + +export async function GET(_request: NextRequest, { params }: RouteParams) { + const { guildId } = await params; + try { + await requireGuildAccess(guildId); + const config = await getSuggestionConfigDashboard(guildId); + return NextResponse.json(config); + } catch (error) { + return toApiErrorResponse(error); + } +} + +export async function PATCH(request: NextRequest, { params }: RouteParams) { + const { guildId } = await params; + try { + const session = await requireGuildAccess(guildId); + const body = await request.json(); + const patch = SuggestionConfigDashboardPatchSchema.parse(body); + + const before = await getSuggestionConfigDashboard(guildId); + const after = await updateSuggestionConfigDashboard(guildId, patch); + + await writeDashboardAudit(prisma, { + guildId, + actorUserId: session.user.id, + action: 'suggestions.update', + path: `/dashboard/${guildId}/suggestions`, + before, + after + }); + + return NextResponse.json(after); + } catch (error) { + return toApiErrorResponse(error); + } +} diff --git a/apps/webui/src/app/dashboard/[guildId]/birthdays/loading.tsx b/apps/webui/src/app/dashboard/[guildId]/birthdays/loading.tsx new file mode 100644 index 0000000..b6eda57 --- /dev/null +++ b/apps/webui/src/app/dashboard/[guildId]/birthdays/loading.tsx @@ -0,0 +1,13 @@ +import { Skeleton } from '@/components/ui/skeleton'; + +export default function BirthdaysLoading() { + return ( +
+
+ + +
+ +
+ ); +} diff --git a/apps/webui/src/app/dashboard/[guildId]/birthdays/page.tsx b/apps/webui/src/app/dashboard/[guildId]/birthdays/page.tsx new file mode 100644 index 0000000..f3803c5 --- /dev/null +++ b/apps/webui/src/app/dashboard/[guildId]/birthdays/page.tsx @@ -0,0 +1,22 @@ +import { BirthdaysForm } from '@/components/modules/birthdays-form'; +import { getLocale, t } from '@/lib/i18n'; +import { getBirthdayDashboard } from '@/lib/module-configs/birthdays'; + +interface BirthdaysPageProps { + params: Promise<{ guildId: string }>; +} + +export default async function BirthdaysPage({ params }: BirthdaysPageProps) { + const { guildId } = await params; + const [locale, config] = await Promise.all([getLocale(), getBirthdayDashboard(guildId)]); + + return ( +
+
+

{t(locale, 'modules.birthdays.label')}

+

{t(locale, 'modules.birthdays.description')}

+
+ +
+ ); +} diff --git a/apps/webui/src/app/dashboard/[guildId]/starboard/loading.tsx b/apps/webui/src/app/dashboard/[guildId]/starboard/loading.tsx new file mode 100644 index 0000000..23df99d --- /dev/null +++ b/apps/webui/src/app/dashboard/[guildId]/starboard/loading.tsx @@ -0,0 +1,13 @@ +import { Skeleton } from '@/components/ui/skeleton'; + +export default function StarboardLoading() { + return ( +
+
+ + +
+ +
+ ); +} diff --git a/apps/webui/src/app/dashboard/[guildId]/starboard/page.tsx b/apps/webui/src/app/dashboard/[guildId]/starboard/page.tsx new file mode 100644 index 0000000..14c2f53 --- /dev/null +++ b/apps/webui/src/app/dashboard/[guildId]/starboard/page.tsx @@ -0,0 +1,22 @@ +import { StarboardForm } from '@/components/modules/starboard-form'; +import { getLocale, t } from '@/lib/i18n'; +import { getStarboardDashboard } from '@/lib/module-configs/starboard'; + +interface StarboardPageProps { + params: Promise<{ guildId: string }>; +} + +export default async function StarboardPage({ params }: StarboardPageProps) { + const { guildId } = await params; + const [locale, config] = await Promise.all([getLocale(), getStarboardDashboard(guildId)]); + + return ( +
+
+

{t(locale, 'modules.starboard.label')}

+

{t(locale, 'modules.starboard.description')}

+
+ +
+ ); +} diff --git a/apps/webui/src/app/dashboard/[guildId]/stats/loading.tsx b/apps/webui/src/app/dashboard/[guildId]/stats/loading.tsx new file mode 100644 index 0000000..1d411a4 --- /dev/null +++ b/apps/webui/src/app/dashboard/[guildId]/stats/loading.tsx @@ -0,0 +1,13 @@ +import { Skeleton } from '@/components/ui/skeleton'; + +export default function StatsLoading() { + return ( +
+
+ + +
+ +
+ ); +} diff --git a/apps/webui/src/app/dashboard/[guildId]/stats/page.tsx b/apps/webui/src/app/dashboard/[guildId]/stats/page.tsx new file mode 100644 index 0000000..8fd6afa --- /dev/null +++ b/apps/webui/src/app/dashboard/[guildId]/stats/page.tsx @@ -0,0 +1,22 @@ +import { StatsForm } from '@/components/modules/stats-form'; +import { getLocale, t } from '@/lib/i18n'; +import { getStatsDashboard } from '@/lib/module-configs/stats'; + +interface StatsPageProps { + params: Promise<{ guildId: string }>; +} + +export default async function StatsPage({ params }: StatsPageProps) { + const { guildId } = await params; + const [locale, config] = await Promise.all([getLocale(), getStatsDashboard(guildId)]); + + return ( +
+
+

{t(locale, 'modules.stats.label')}

+

{t(locale, 'modules.stats.description')}

+
+ +
+ ); +} diff --git a/apps/webui/src/app/dashboard/[guildId]/tempvoice/loading.tsx b/apps/webui/src/app/dashboard/[guildId]/tempvoice/loading.tsx new file mode 100644 index 0000000..5a30f8f --- /dev/null +++ b/apps/webui/src/app/dashboard/[guildId]/tempvoice/loading.tsx @@ -0,0 +1,13 @@ +import { Skeleton } from '@/components/ui/skeleton'; + +export default function TempVoiceLoading() { + return ( +
+
+ + +
+ +
+ ); +} diff --git a/apps/webui/src/app/dashboard/[guildId]/tempvoice/page.tsx b/apps/webui/src/app/dashboard/[guildId]/tempvoice/page.tsx new file mode 100644 index 0000000..101d934 --- /dev/null +++ b/apps/webui/src/app/dashboard/[guildId]/tempvoice/page.tsx @@ -0,0 +1,22 @@ +import { TempVoiceForm } from '@/components/modules/tempvoice-form'; +import { getLocale, t } from '@/lib/i18n'; +import { getTempVoiceDashboard } from '@/lib/module-configs/tempvoice'; + +interface TempVoicePageProps { + params: Promise<{ guildId: string }>; +} + +export default async function TempVoicePage({ params }: TempVoicePageProps) { + const { guildId } = await params; + const [locale, config] = await Promise.all([getLocale(), getTempVoiceDashboard(guildId)]); + + return ( +
+
+

{t(locale, 'modules.tempvoice.label')}

+

{t(locale, 'modules.tempvoice.description')}

+
+ +
+ ); +} diff --git a/apps/webui/src/components/modules/birthdays-form.tsx b/apps/webui/src/components/modules/birthdays-form.tsx new file mode 100644 index 0000000..363beb0 --- /dev/null +++ b/apps/webui/src/components/modules/birthdays-form.tsx @@ -0,0 +1,78 @@ +'use client'; + +import type { BirthdayConfigDashboard } from '@nexumi/shared'; +import { useId } from 'react'; +import { useTranslations } from '@/components/locale-provider'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { Textarea } from '@/components/ui/textarea'; +import type { SettingsSaveResult } from '@/components/settings/settings-form'; +import { SettingsForm } from '@/components/settings/settings-form'; + +async function saveBirthdays(guildId: string, value: BirthdayConfigDashboard): Promise { + try { + const response = await fetch(`/api/guilds/${guildId}/birthdays`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(value) + }); + if (!response.ok) { + const body = (await response.json().catch(() => null)) as { error?: string } | null; + return { ok: false, error: body?.error ?? `Request failed with status ${response.status}` }; + } + return { ok: true }; + } catch { + return { ok: false, error: 'Network error' }; + } +} + +interface BirthdaysFormProps { + guildId: string; + initialValue: BirthdayConfigDashboard; +} + +export function BirthdaysForm({ guildId, initialValue }: BirthdaysFormProps) { + const t = useTranslations(); + const channelId = useId(); + const roleId = useId(); + const timezoneId = useId(); + + return ( + initialValue={initialValue} onSave={(value) => saveBirthdays(guildId, value)}> + {({ value, setValue }) => ( + + + {t('modulePages.birthdays.title')} + {t('modulePages.birthdays.description')} + + +
+ + setValue((prev) => ({ ...prev, enabled }))} /> +
+
+
+ + setValue((prev) => ({ ...prev, channelId: event.target.value.trim() }))} placeholder="123456789012345678" /> +
+
+ + setValue((prev) => ({ ...prev, roleId: event.target.value.trim() }))} placeholder="123456789012345678" /> +
+
+ + setValue((prev) => ({ ...prev, timezone: event.target.value }))} placeholder="Europe/Berlin" /> +
+
+
+ +