mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 09:14:11 +05:30
Initial commit
This commit is contained in:
353
src/app/(admin)/admin/announcements/announcement-form.tsx
Normal file
353
src/app/(admin)/admin/announcements/announcement-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user