add notification and chore list codes

This commit is contained in:
oiov
2025-06-11 20:08:32 +08:00
parent 87009bfd05
commit cbdccaa60e
19 changed files with 2783 additions and 44 deletions
+2
View File
@@ -1,5 +1,6 @@
import { NavMobile } from "@/components/layout/mobile-nav";
import { NavBar } from "@/components/layout/navbar";
import { Notification } from "@/components/layout/notification";
import { SiteFooter } from "@/components/layout/site-footer";
interface MarketingLayoutProps {
@@ -11,6 +12,7 @@ export default function MarketingLayout({ children }: MarketingLayoutProps) {
<div className="flex min-h-screen flex-col dark:bg-black">
<NavMobile />
<NavBar scroll={true} />
<Notification />
<main className="flex-1 bg-[radial-gradient(circle_500px_at_50%_300px,#a1fffc36,#ffffff)] dark:bg-[radial-gradient(circle_500px_at_50%_300px,#a1fffc36,#000)]">
{children}
</main>
+2 -1
View File
@@ -58,7 +58,7 @@ export default function AppConfigs({}: {}) {
}
return (
<Card>
<Card className="bg-neutral-50 dark:bg-neutral-900">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-lg font-bold">{t("App Configs")}</CardTitle>
</CardHeader>
@@ -111,6 +111,7 @@ export default function AppConfigs({}: {}) {
<div className="flex w-full items-start gap-2">
<Textarea
className="h-16 max-h-32 min-h-9 resize-y"
placeholder="Support HTML format, such as <div>info</div>"
rows={5}
defaultValue={configs.system_notification}
value={notification}
+1 -1
View File
@@ -291,7 +291,7 @@ export default function DomainList({ user, action }: DomainListProps) {
</TableCell>
<TableCell className="col-span-1 flex items-center gap-1">
<Button
className="h-7 px-1 text-xs hover:bg-slate-100 dark:hover:text-primary-foreground"
className="h-7 px-1 text-xs hover:bg-slate-100 dark:hover:text-primary-foreground sm:px-1.5"
size="sm"
variant={"outline"}
onClick={() => {
+1 -1
View File
@@ -203,7 +203,7 @@ export default function PlanList({ user, action }: PlanListProps) {
</TableCell>
<TableCell className="col-span-1 flex items-center gap-1">
<Button
className="h-7 px-1 text-xs hover:bg-slate-100 dark:hover:text-primary-foreground"
className="h-7 px-1 text-xs hover:bg-slate-100 dark:hover:text-primary-foreground sm:px-1.5"
size="sm"
variant={"outline"}
onClick={() => {
+2
View File
@@ -8,6 +8,7 @@ import {
MobileSheetSidebar,
} from "@/components/layout/dashboard-sidebar";
import { ModeToggle } from "@/components/layout/mode-toggle";
import { Notification } from "@/components/layout/notification";
import { UserAccountNav } from "@/components/layout/user-account-nav";
import MaxWidthWrapper from "@/components/shared/max-width-wrapper";
@@ -32,6 +33,7 @@ export default async function Dashboard({ children }: ProtectedLayoutProps) {
<DashboardSidebar links={filteredLinks} />
<div className="flex flex-1 flex-col">
<Notification />
<header className="sticky top-0 z-50 flex h-14 bg-background px-4 lg:h-[60px] xl:px-8">
<MaxWidthWrapper className="flex max-w-7xl items-center gap-x-3 px-0">
<MobileSheetSidebar links={filteredLinks} />
+12 -5
View File
@@ -11,11 +11,18 @@ export async function GET(req: NextRequest) {
const user = checkUserStatus(await getCurrentUser());
if (user instanceof Response) return user;
const configs = await getMultipleConfigs([
"enable_user_registration",
"enable_subdomain_apply",
"system_notification",
]);
const url = new URL(req.url);
const keys = url.searchParams.getAll("key") || [];
if (keys.length === 0) {
return Response.json("key is required", { status: 400 });
}
const configs = await getMultipleConfigs(keys);
// "enable_user_registration",
// "enable_subdomain_apply",
// "system_notification",
return Response.json(configs, { status: 200 });
} catch (error) {
+2
View File
@@ -1,6 +1,8 @@
import { env } from "@/env.mjs";
import { getConfigValue } from "@/lib/dto/system-config";
export const dynamic = "force-dynamic";
export async function GET(req: Request) {
try {
const registration = await getConfigValue<boolean>(
+121 -21
View File
@@ -148,8 +148,11 @@ export function PlanForm({
<div className="rounded-t-lg bg-muted px-4 py-2 text-lg font-semibold">
{type === "add" ? t("Create Plan") : t("Edit Plan")}
</div>
<form className="p-4" onSubmit={onSubmit}>
<div className="items-center justify-start gap-4 md:flex">
<form className="space-y-3 p-4" onSubmit={onSubmit}>
<div className="relative grid-cols-1 gap-2 rounded-md border bg-neutral-50 px-3 pb-3 pt-8 dark:bg-neutral-900 md:grid md:grid-cols-2">
<h2 className="absolute left-2 top-2 text-xs font-semibold text-neutral-400">
{t("Base")}
</h2>
<FormSectionColumns title={t("Plan Name")} required>
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="Plan-Name">
@@ -169,14 +172,32 @@ export function PlanForm({
</p>
) : (
<p className="pb-0.5 text-[13px] text-muted-foreground">
{t("Required")}. Plan name must be unique
{t("Required")}. {t("Plan name must be unique")}
</p>
)}
</div>
</FormSectionColumns>
<FormSectionColumns title={t("Active")}>
<Label className="sr-only" htmlFor="active">
{t("Active")}:
</Label>
<Switch
id="active"
className="mb-3"
{...register("isActive")}
defaultChecked={initData?.isActive ?? true}
onCheckedChange={(value) => setValue("isActive", value)}
/>
<p className="pb-1 text-[13px] text-muted-foreground">
{t("Only active plans can be used")}
</p>
</FormSectionColumns>
</div>
<div className="items-center justify-start gap-4 md:flex">
<div className="relative grid-cols-1 gap-2 rounded-md border bg-neutral-50 px-3 pb-3 pt-8 dark:bg-neutral-900 md:grid md:grid-cols-2">
<h2 className="absolute left-2 top-2 text-xs font-semibold text-neutral-400">
{t("Shorten Service")}
</h2>
{/* Short Limit - slNewLinks */}
<FormSectionColumns title={t("Short Limit")} required>
<div className="flex w-full items-center gap-2">
@@ -203,8 +224,96 @@ export function PlanForm({
)}
</div>
</FormSectionColumns>
{/* Short Limit - slAnalyticsRetention*/}
<FormSectionColumns title={t("View Period")}>
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="View-Period">
{t("View Period")}
</Label>
<Input
id="short-limit"
className="flex-1 shadow-inner"
size={32}
type="number"
{...register("slAnalyticsRetention", { valueAsNumber: true })}
/>
</div>
<div className="flex flex-col justify-between p-1">
{errors?.slAnalyticsRetention ? (
<p className="pb-0.5 text-[13px] text-red-600">
{errors.slAnalyticsRetention.message}
</p>
) : (
<p className="pb-0.5 text-[13px] text-muted-foreground">
{t(
"Time range for viewing short link visitor statistics data (days)",
)}
.
</p>
)}
</div>
</FormSectionColumns>
{/* Short Limit - slTrackedClicks*/}
<FormSectionColumns title={t("Tracked Limit")}>
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="Click-Limit">
{t("Tracked Limit")}
</Label>
<Input
id="short-limit"
className="flex-1 shadow-inner"
size={32}
type="number"
{...register("slTrackedClicks", { valueAsNumber: true })}
/>
</div>
<div className="flex flex-col justify-between p-1">
{errors?.slTrackedClicks ? (
<p className="pb-0.5 text-[13px] text-red-600">
{errors.slTrackedClicks.message}
</p>
) : (
<p className="pb-0.5 text-[13px] text-muted-foreground">
{t("Monthly limit of tracked clicks (times)")}.
</p>
)}
</div>
</FormSectionColumns>
{/* Short Limit - slDomains */}
<FormSectionColumns title={t("Domain Limit")}>
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="Domain-Limit">
{t("Domain Limit")}
</Label>
<Input
id="short-limit"
className="flex-1 shadow-inner"
size={32}
type="number"
disabled
{...register("slDomains", { valueAsNumber: true })}
/>
</div>
<div className="flex flex-col justify-between p-1">
{errors?.slDomains ? (
<p className="pb-0.5 text-[13px] text-red-600">
{errors.slDomains.message}
</p>
) : (
<p className="pb-0.5 text-[13px] text-muted-foreground">
{t("Limit on the number of allowed domains")}.
</p>
)}
</div>
</FormSectionColumns>
</div>
<div className="relative grid-cols-1 gap-2 rounded-md border bg-neutral-50 px-3 pb-3 pt-8 dark:bg-neutral-900 md:grid md:grid-cols-2">
<h2 className="absolute left-2 top-2 text-xs font-semibold text-neutral-400">
{t("Email Service")}
</h2>
{/* Email Limit - emEmailAddresses */}
<FormSectionColumns title={t("Email Limit")} required>
<FormSectionColumns title={t("Email Limit")}>
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="Email-Limit">
{t("Email Limit")}
@@ -229,11 +338,8 @@ export function PlanForm({
)}
</div>
</FormSectionColumns>
</div>
<div className="items-center justify-start gap-4 md:flex">
{/* "Send Limit" - emSendEmails */}
<FormSectionColumns title={t("Send Limit")} required>
<FormSectionColumns title={t("Send Limit")}>
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="Send-Limit">
{t("Send Limit")}
@@ -258,6 +364,12 @@ export function PlanForm({
)}
</div>
</FormSectionColumns>
</div>
<div className="relative grid-cols-1 gap-2 rounded-md border bg-neutral-50 px-3 pb-3 pt-8 dark:bg-neutral-900 md:grid md:grid-cols-2">
<h2 className="absolute left-2 top-2 text-xs font-semibold text-neutral-400">
{t("Subdomain Service")}
</h2>
{/* Record Limit - rcNewRecords */}
<FormSectionColumns title={t("Record Limit")} required>
<div className="flex w-full items-center gap-2">
@@ -286,18 +398,6 @@ export function PlanForm({
</FormSectionColumns>
</div>
<FormSectionColumns title={t("Active")} required>
<Label className="sr-only" htmlFor="active">
{t("Active")}:
</Label>
<Switch
id="active"
{...register("isActive")}
defaultChecked={initData?.isActive ?? true}
onCheckedChange={(value) => setValue("isActive", value)}
/>
</FormSectionColumns>
{/* Action buttons */}
<div className="mt-3 flex justify-end gap-3">
{type === "edit" && initData?.name !== "free" && (
+1 -1
View File
@@ -102,7 +102,7 @@ export function RecordForm({
);
const { data: configs } = useSWR<Record<string, any>>(
"/api/configs",
"/api/configs?key=enable_subdomain_apply",
fetcher,
);
+56
View File
@@ -0,0 +1,56 @@
"use client";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import useSWR from "swr";
import { fetcher } from "@/lib/utils";
import { Icons } from "../shared/icons";
import { Button } from "../ui/button";
export function Notification() {
const [isVisible, setIsVisible] = useState(true);
const { data, isLoading, error } = useSWR<Record<string, any>>(
"/api/configs?key=system_notification",
fetcher,
);
const handleClose = () => {
setIsVisible(false);
};
if (error || isLoading || !data || !data.system_notification) return null;
return (
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{
duration: 0.3,
ease: "easeInOut",
}}
className="relative flex max-h-24 w-full items-center justify-center bg-muted text-sm text-primary"
>
<div
className="max-w-3xl flex-1 px-8 py-2.5 text-center"
dangerouslySetInnerHTML={{ __html: data.system_notification }}
/>
<Button
onClick={handleClose}
variant={"ghost"}
size={"icon"}
className="absolute right-1.5 top-[18px] flex size-6 -translate-y-1/2 items-center justify-center"
>
<Icons.close className="size-4 text-primary" />
</Button>
</motion.div>
)}
</AnimatePresence>
);
}
+2 -2
View File
@@ -65,7 +65,7 @@ export function UserAccountNav() {
<p className="font-medium">{user.name || "Anonymous"}</p>
<Link href={"/pricing"} target="_blank">
<Badge className="text-xs font-semibold" variant="default">
{user.team.toUpperCase()}
{user.team}
</Badge>
</Link>
</div>
@@ -155,7 +155,7 @@ export function UserAccountNav() {
<p className="font-medium">{user.name || "Anonymous"}</p>
<Link href={"/pricing"} target="_blank">
<Badge className="text-xs font-semibold" variant="default">
{user.team.toUpperCase()}
{user.team}
</Badge>
</Link>
</div>
-2
View File
@@ -1,8 +1,6 @@
import { User } from "@prisma/client";
import { AvatarProps } from "@radix-ui/react-avatar";
import { generateGradientClasses } from "@/lib/enums";
import { cn } from "@/lib/utils";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
interface UserAvatarProps extends AvatarProps {
+8 -1
View File
@@ -156,7 +156,14 @@
"Monthly limit of emails sent": "Monthly limit of emails sent",
"Monthly limit of email addresses created": "Monthly limit of email addresses created",
"Reject": "Reject",
"Rejected": "Rejected"
"Rejected": "Rejected",
"View Period": "View Period",
"Time range for viewing short link visitor statistics data (days)": "Time range for viewing short link visitor statistics data (days)",
"Tracked Limit": "Tracked Limit",
"Monthly limit of tracked clicks (times)": "Monthly limit of tracked clicks (times)",
"Limit on the number of allowed domains": "Limit on the number of allowed domains",
"Only active plans can be used": "Only active plans can be used",
"Plan name must be unique": "Plan name must be unique"
},
"Components": {
"Dashboard": "Dashboard",
+14 -7
View File
@@ -142,21 +142,28 @@
"Edit URL": "编辑短链",
"Plan Name": "计划名称",
"Quota Settings": "配额设置",
"Short Limit": "短链限制",
"Record Limit": "子域名限制",
"Email Limit": "邮箱限制",
"Send Limit": "发件限制",
"Short Limit": "创建短链数量",
"Record Limit": "创建子域名数量",
"Email Limit": "创建邮箱数量",
"Send Limit": "发件数量",
"Domain Limit": "域名限制",
"Add Plan": "添加计划",
"No Plans": "暂无计划",
"Create Plan": "创建计划",
"Edit Plan": "编辑计划",
"Monthly limit of short links created": "每月创建短链数量限制",
"Monthly limit of subdomains created": "每月创建子域名数量限制",
"Monthly limit of short links created": "每月创建短链数量限制",
"Monthly limit of subdomains created": "每月创建子域名数量限制",
"Monthly limit of emails sent": "每月发送邮件数量限制",
"Monthly limit of email addresses created": "每月接收邮件数量限制",
"Reject": "拒绝",
"Rejected": "已拒绝"
"Rejected": "已拒绝",
"View Period": "时间范围",
"Time range for viewing short link visitor statistics data (days)": "查看短链访问量统计数据的时间范围(天)",
"Tracked Limit": "跟踪统计限制",
"Monthly limit of tracked clicks (times)": "每月最大跟踪点击量限制(次)",
"Limit on the number of allowed domains": "允许的域名数量限制",
"Only active plans can be used": "只有启用的计划才能生效",
"Plan name must be unique": "计划名称必须唯一"
},
"Components": {
"Dashboard": "用户面板",
+101 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long