Initial commit

This commit is contained in:
JetSprow
2026-04-29 05:12:39 +10:00
commit 27dbca9cbf
379 changed files with 43486 additions and 0 deletions

View File

@@ -0,0 +1,353 @@
"use client";
import { useState } from "react";
import type {
AnnouncementAudience,
AnnouncementDisplayType,
} from "@prisma/client";
import { toast } from "sonner";
import {
createAnnouncement,
updateAnnouncement,
} from "@/actions/admin/announcements";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { getErrorMessage } from "@/lib/errors";
interface AnnouncementOptionUser {
id: string;
email: string;
}
interface AnnouncementFormData {
id: string;
title: string;
body: string;
audience: AnnouncementAudience;
displayType: AnnouncementDisplayType;
targetUserId: string | null;
dismissible: boolean;
sendNotification: boolean;
startAt: Date | string | null;
endAt: Date | string | null;
}
function toDateTimeLocalValue(value: Date | string | null) {
if (!value) {
return "";
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return "";
}
const localTime = new Date(date.getTime() - date.getTimezoneOffset() * 60_000);
return localTime.toISOString().slice(0, 16);
}
export function AnnouncementForm({
users,
announcement,
triggerLabel,
triggerVariant = "outline",
}: {
users: AnnouncementOptionUser[];
announcement: AnnouncementFormData;
triggerLabel?: string;
triggerVariant?: "default" | "outline" | "ghost";
}) {
const [open, setOpen] = useState(false);
const [audience, setAudience] = useState<AnnouncementAudience>(announcement.audience);
async function handleSubmit(formData: FormData) {
try {
await updateAnnouncement(announcement.id, formData);
toast.success("公告已更新");
setOpen(false);
} catch (error) {
toast.error(getErrorMessage(error, "更新公告失败"));
}
}
return (
<Dialog
open={open}
onOpenChange={(nextOpen) => {
if (nextOpen) {
setAudience(announcement.audience);
}
setOpen(nextOpen);
}}
>
<DialogTrigger render={<Button variant={triggerVariant} size="sm" />}>
{triggerLabel ?? "编辑"}
</DialogTrigger>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<form action={handleSubmit} className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`title-${announcement.id}`}></Label>
<Input id={`title-${announcement.id}`} name="title" defaultValue={announcement.title} required />
</div>
<div className="space-y-2">
<Label htmlFor={`audience-${announcement.id}`}></Label>
<select
id={`audience-${announcement.id}`}
name="audience"
defaultValue={announcement.audience}
onChange={(event) => setAudience(event.target.value as AnnouncementAudience)}
className="h-10 w-full px-3 text-sm outline-none"
>
<option value="PUBLIC">/</option>
<option value="USERS"></option>
<option value="ADMINS"></option>
<option value="SPECIFIC_USER"></option>
</select>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`displayType-${announcement.id}`}></Label>
<select
id={`displayType-${announcement.id}`}
name="displayType"
defaultValue={announcement.displayType}
className="h-10 w-full px-3 text-sm outline-none"
>
<option value="INLINE"></option>
<option value="BIG"></option>
<option value="POPUP"></option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor={`dismissible-${announcement.id}`}></Label>
<select
id={`dismissible-${announcement.id}`}
name="dismissible"
defaultValue={announcement.dismissible ? "true" : "false"}
className="h-10 w-full px-3 text-sm outline-none"
>
<option value="true"></option>
<option value="false"></option>
</select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor={`targetUserId-${announcement.id}`}></Label>
<select
id={`targetUserId-${announcement.id}`}
name="targetUserId"
defaultValue={announcement.targetUserId ?? ""}
disabled={audience !== "SPECIFIC_USER"}
className="h-10 w-full px-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-60"
>
<option value=""></option>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.email}
</option>
))}
</select>
</div>
<div className="space-y-2">
<Label htmlFor={`body-${announcement.id}`}></Label>
<Textarea
id={`body-${announcement.id}`}
name="body"
rows={5}
defaultValue={announcement.body}
required
/>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`startAt-${announcement.id}`}></Label>
<Input
id={`startAt-${announcement.id}`}
name="startAt"
type="datetime-local"
defaultValue={toDateTimeLocalValue(announcement.startAt)}
/>
</div>
<div className="space-y-2">
<Label htmlFor={`endAt-${announcement.id}`}></Label>
<Input
id={`endAt-${announcement.id}`}
name="endAt"
type="datetime-local"
defaultValue={toDateTimeLocalValue(announcement.endAt)}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor={`sendNotification-${announcement.id}`}></Label>
<select
id={`sendNotification-${announcement.id}`}
name="sendNotification"
defaultValue={announcement.sendNotification ? "true" : "false"}
className="h-10 w-full px-3 text-sm outline-none"
>
<option value="true"></option>
<option value="false"></option>
</select>
</div>
<Button type="submit" className="w-full">
</Button>
</form>
</DialogContent>
</Dialog>
);
}
export function CreateAnnouncementButton({
users,
}: {
users: AnnouncementOptionUser[];
}) {
const [open, setOpen] = useState(false);
const [audience, setAudience] = useState<AnnouncementAudience>("USERS");
async function handleSubmit(formData: FormData) {
try {
await createAnnouncement(formData);
toast.success("公告已发布");
setOpen(false);
setAudience("USERS");
} catch (error) {
toast.error(getErrorMessage(error, "发布公告失败"));
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button />}></DialogTrigger>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<form action={handleSubmit} className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="create-announcement-title"></Label>
<Input id="create-announcement-title" name="title" required />
</div>
<div className="space-y-2">
<Label htmlFor="create-announcement-audience"></Label>
<select
id="create-announcement-audience"
name="audience"
defaultValue="USERS"
onChange={(event) => setAudience(event.target.value as AnnouncementAudience)}
className="h-10 w-full px-3 text-sm outline-none"
>
<option value="PUBLIC">/</option>
<option value="USERS"></option>
<option value="ADMINS"></option>
<option value="SPECIFIC_USER"></option>
</select>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="create-announcement-displayType"></Label>
<select
id="create-announcement-displayType"
name="displayType"
defaultValue="INLINE"
className="h-10 w-full px-3 text-sm outline-none"
>
<option value="INLINE"></option>
<option value="BIG"></option>
<option value="POPUP"></option>
</select>
</div>
<div className="space-y-2">
<Label htmlFor="create-announcement-dismissible"></Label>
<select
id="create-announcement-dismissible"
name="dismissible"
defaultValue="true"
className="h-10 w-full px-3 text-sm outline-none"
>
<option value="true"></option>
<option value="false"></option>
</select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="create-announcement-targetUserId"></Label>
<select
id="create-announcement-targetUserId"
name="targetUserId"
defaultValue=""
disabled={audience !== "SPECIFIC_USER"}
className="h-10 w-full px-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-60"
>
<option value=""></option>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.email}
</option>
))}
</select>
</div>
<div className="space-y-2">
<Label htmlFor="create-announcement-body"></Label>
<Textarea id="create-announcement-body" name="body" rows={5} required />
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="create-announcement-startAt"></Label>
<Input id="create-announcement-startAt" name="startAt" type="datetime-local" />
</div>
<div className="space-y-2">
<Label htmlFor="create-announcement-endAt"></Label>
<Input id="create-announcement-endAt" name="endAt" type="datetime-local" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="create-announcement-sendNotification"></Label>
<select
id="create-announcement-sendNotification"
name="sendNotification"
defaultValue="true"
className="h-10 w-full px-3 text-sm outline-none"
>
<option value="true"></option>
<option value="false"></option>
</select>
</div>
<Button type="submit" className="w-full">
</Button>
</form>
</DialogContent>
</Dialog>
);
}