Merge branch 'master' of github.com:wyongk/juip-proxy-react

This commit is contained in:
2025-06-07 11:30:14 +08:00
8 changed files with 418 additions and 327 deletions

View File

@@ -1,19 +0,0 @@
'use server'
import {PageRecord} from '@/lib/api'
import {Announcement} from '@/lib/models'
import {callByUser} from './base'
export async function listAnnouncements(props: {
page: number
size: number
title?: string
type?: number
status?: number
create_after?: Date
create_before?: Date
update_after?: Date
update_before?: Date
}) {
return await callByUser<PageRecord<Announcement>>('/api/announcement/list', props)
}

43
src/actions/dashboard.ts Normal file
View File

@@ -0,0 +1,43 @@
'use server'
import {callByUser} from './base'
export async function listInitialization(props: {
page: number
size: number
title?: string
type?: number
status?: number
create_after?: Date
create_before?: Date
update_after?: Date
update_before?: Date
short_term_package?: number
}) {
return await callByUser<{
short_term: string
short_term_monthly: string
long_term: string
long_term_monthly: string
list: {
id: number
title: string
created_at: Date
}[]
}>('/api/announcement/list', props)
}
type listAccountReq = {
resource_no: number
create_after: Date
create_before: Date
}
type listAccountResp = {
time: Date
count: number
}[]
export async function listAccount(props: listAccountReq) {
return await callByUser<listAccountResp>('/api/account/list', props)
}

View File

@@ -1,35 +1,33 @@
'use client'
import { ChartConfig, ChartContainer } from "@/components/ui/chart"
import { Area, AreaChart, Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from "recharts"
import { addDays, format } from "date-fns"
const data = Array(100).fill(0).map((_, i) => {
let time = new Date()
time = addDays(time, i)
return {
time: format(time, `MM/dd`),
count: Math.floor(Math.random() * 100) + 1,
}
})
import {ChartConfig, ChartContainer} from '@/components/ui/chart'
import {Area, AreaChart, Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis} from 'recharts'
import {addDays, format} from 'date-fns'
import {listAccount} from '@/actions/dashboard'
import {ExtraReq, ExtraResp} from '@/lib/api'
const config = {
count: {
label: `套餐使用量`,
color: `var(--color-primary)`,
}
},
} satisfies ChartConfig
export default function DashboardChart() {
type DashboardChartProps = {
data: ExtraResp<typeof listAccount>
}
async function DashboardChart(props: DashboardChartProps) {
return (
<ChartContainer config={config} className={`w-full h-full`}>
<AreaChart data={data} margin={{top: 0, right: 20, left: 0, bottom: 0}}>
<ChartContainer config={config} className="w-full h-full">
<AreaChart data={props.data} margin={{top: 0, right: 20, left: 0, bottom: 0}}>
<CartesianGrid vertical={false}/>
<XAxis dataKey={`time`} tickLine={false} />
<XAxis dataKey="time" tickLine={false}/>
<XAxis dataKey="time" tickLine={false}/>
<YAxis tickLine={false}/>
<Tooltip animationDuration={100}/>
<Area dataKey={`count`} radius={20} className="fill-[var(--color-primary)]"/>
<Area dataKey="count" radius={20} className="fill-[var(--color-primary)]"/>
<Area dataKey="count" radius={20} className="fill-[var(--color-primary)]"/>
</AreaChart>
</ChartContainer>
)
}
}
export default DashboardChart

View File

@@ -1,121 +1,155 @@
'use client'
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
import DashboardChart from "./chart"
import {Tabs, TabsList, TabsTrigger, TabsContent} from '@/components/ui/tabs'
import DashboardChart from './chart'
import Image from 'next/image'
import soon from '../_assets/coming-soon.svg'
import DatePicker from "@/components/date-picker"
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { Form, FormField } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import DatePicker from '@/components/date-picker'
import {Card, CardHeader, CardTitle, CardContent} from '@/components/ui/card'
import {Form, FormField} from '@/components/ui/form'
import {Input} from '@/components/ui/input'
import {zodResolver} from '@hookform/resolvers/zod'
import {useForm} from 'react-hook-form'
import zod from 'zod'
import {merge} from '@/lib/utils'
import mask from '../_assets/Mask group.webp'
import {Button} from '@/components/ui/button'
import {useStatus} from '@/lib/states'
import {useState} from 'react'
import {useSearchParams} from 'next/navigation'
import {log} from 'console'
import {listAccount} from '@/actions/dashboard'
import {ExtraReq, ExtraResp} from '@/lib/api'
import {toast} from 'sonner'
import {addDays, format} from 'date-fns'
export default function Charts() {
const dateStr = '2025-03-05'
const dateStrA = '2024-03-05'
const date = new Date(dateStr)
const dateA = new Date(dateStrA)
// const [submittedData, setSubmittedData] = useState<ExtraReq<typeof listAccount>>()
const [submittedData, setSubmittedData] = useState<ExtraResp<typeof listAccount>>([
{time: new Date(), count: 80},
{time: date, count: 100},
{time: dateA, count: 50},
// {time: `2023-10-03`, count: 80},
// {time: `2023-10-04`, count: 200},
// {time: `2023-10-05`, count: 150},
])
const data = [
{ time: `2023-10-01`, count: 100 },
{ time: `2023-10-02`, count: 50 },
{ time: `2023-10-03`, count: 80 },
{ time: `2023-10-04`, count: 200 },
{ time: `2023-10-05`, count: 150 },
{time: `2023-10-01`, count: 100},
{time: `2023-10-02`, count: 50},
{time: `2023-10-03`, count: 80},
{time: `2023-10-04`, count: 200},
{time: `2023-10-05`, count: 150},
]
const formSchema = zod.object({
name: zod.string(),
age: zod.string(),
})
const formSchema = zod.object({
resource_no: zod.number().min(11, '请输入正确的套餐编号').max(11, '请输入正确的套餐编号').optional(),
create_after: zod.date().optional(),
create_before: zod.date().optional(),
})
type FormValues = zod.infer<typeof formSchema>
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
age: '',
},
})
const handler = form.handleSubmit(
async (value) => {
const res = {
resource_no: value.resource_no ?? 0,
create_after: value.create_after ?? new Date(),
create_before: value.create_before ?? new Date(),
}
const resp = await listAccount(res)
if (!resp.success) {
toast.error('接口请求失败:' + resp.message)
return
}
resp.data.map((_, i) => {
let time = new Date()
time = addDays(time, i)
return {
time: format(time, `MM/dd`),
count: Math.floor(Math.random() * 100) + 1,
}
})
setSubmittedData(resp.data)
},
)
return (
<Card className={`col-start-1 row-start-3 col-span-3 row-span-2`}>
<CardContent className={`overflow-hidden`}>
<Tabs defaultValue={`dynamic`} className="h-full gap-4">
<Card className="col-start-1 row-start-3 col-span-3 row-span-2">
<CardContent className="overflow-hidden">
<Tabs defaultValue="dynamic" className="h-full gap-4">
<TabsList>
<TabsTrigger value={`dynamic`} className={`data-[state=active]:text-primary`}>
<TabsTrigger value="dynamic" className="data-[state=active]:text-primary">
{/* <Image src={mask} alt={`Mask group`} width={35} height={35} priority /> */}
IP </TabsTrigger>
<TabsTrigger value={`static`} className={`data-[state=active]:text-primary`}> IP </TabsTrigger>
IP
</TabsTrigger>
<TabsTrigger value="static" className="data-[state=active]:text-primary"> IP </TabsTrigger>
</TabsList>
<Form
// className="space-y-6"
form={form}
className={merge(
`grid grid-cols-3 gap-4 items-start`,
)}
<Form<FormValues>
className={merge(`grid grid-cols-3 gap-4 items-start`)}
handler={handler}
form={form}
>
<FormField
name="username"
label={<span className={`w-full flex justify-end`}></span>}
<FormField
name="resource_no"
label={<span className="w-full flex justify-end"></span>}
className={`grid grid-cols-[70px_1fr] grid-rows-[auto_auto] `}
classNames={{
message: `col-start-2`,
}}
>
{({ field }) => (
<Input {...field} className={`w-52`}/>
{({field}) => (
<Input {...field} className="w-52"/>
)}
</FormField>
<div className={`flex items-center `} >
<FormField
name={`create_after`}
label={<span className={`w-full flex justify-end`}></span>}
<FormField
name="create_after"
label={<span className="w-full flex justify-end"></span>}
className={`grid grid-cols-[100px_1fr] `}
classNames={{
message: `col-start-2`,
}}
>
{({ field }) => (
{({field}) => (
<DatePicker
{...field}
className={`w-36`}
placeholder={`开始时间`}
format={`yyyy-MM-dd`}
className="w-36"
placeholder="开始时间"
format="yyyy-MM-dd"
/>
)}
</FormField>
<span className={`px-1`}>-</span>
<FormField name={`create_before`}>
{({ field }) => (
<span className="px-1">-</span>
<FormField name="create_before">
{({field}) => (
<DatePicker
{...field}
className={`w-36`}
placeholder={`结束时间`}
format={`yyyy-MM-dd`}
className="w-36"
placeholder="结束时间"
format="yyyy-MM-dd"
/>
)}
</FormField>
<Button className={'h-9 w-20'} type="submit">
<Button className="h-9 w-20" type="submit">
<span></span>
</Button>
</div>
</div>
</Form>
<TabsContent value={`dynamic`} className={`overflow-hidden`}>
<DashboardChart />
<TabsContent value="dynamic" className="overflow-hidden">
{submittedData && <DashboardChart data={submittedData}/>}
</TabsContent>
<TabsContent value={`static`} className={`flex flex-col items-center justify-center gap-2`}>
<Image alt={`coming soon`} src={soon} />
<TabsContent value="static" className="flex flex-col items-center justify-center gap-2">
<Image alt="coming soon" src={soon}/>
<p></p>
</TabsContent>
</Tabs>
</CardContent>
</Card>
)
}
}

View File

@@ -1,76 +0,0 @@
'use client'
import Image from 'next/image'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import soon from '../_assets/coming-soon.svg'
import mask from '../_assets/Mask group.webp'
export default function Pins() {
return <>
{/* 短效 */}
<Card className={`col-start-1 row-start-2 py-4`}>
<CardHeader>
<CardTitle><Image src={mask} alt={`Mask group`} width={35} height={35} priority /> </CardTitle>
</CardHeader>
<CardContent className={`flex-auto flex flex-col gap-2`}>
<div className={`flex-1 flex items-center justify-between`}>
<h4></h4>
<p className={`flex flex-col items-end`}>
<span className={`text-sm text-weak`}></span>
<span className={`text-lg`}>todo</span>
</p>
</div>
<div className={`border-b`}></div>
<div className={`flex-1 flex items-center justify-between`}>
<h4></h4>
<p className={`flex flex-col items-end`}>
<span className={`text-sm text-weak`}></span>
<span className={`text-lg`}>todo</span>
</p>
</div>
</CardContent>
</Card>
{/* 长效 */}
<Card className={`col-start-2 row-start-2`}>
<CardHeader>
<CardTitle><Image src={mask} alt={`Mask group`} width={35} height={35} priority /> </CardTitle>
</CardHeader>
<CardContent className={`flex-auto flex flex-col gap-2`}>
{/* <Image alt={`coming soon`} src={soon} />
<p>敬请期待</p> */}
<div className={`flex-1 flex items-center justify-between`}>
<h4></h4>
<p className={`flex flex-col items-end`}>
<span className={`text-sm text-weak`}></span>
<span className={`text-lg`}>todo</span>
</p>
</div>
<div className={`border-b`}></div>
<div className={`flex-1 flex items-center justify-between`}>
<h4></h4>
<p className={`flex flex-col items-end`}>
<span className={`text-sm text-weak`}></span>
<span className={`text-lg`}>todo</span>
</p>
</div>
</CardContent>
</Card>
{/* 固定 */}
<Card className={`col-start-3 row-start-2 py-4`}>
<CardHeader className={`px-4`}>
<CardTitle>IP套餐</CardTitle>
</CardHeader>
<CardContent className={`flex-auto flex flex-col gap-2 items-center justify-center`}>
<Image alt={`coming soon`} src={soon} />
<p></p>
</CardContent>
</Card>
</>
}

View File

@@ -0,0 +1,91 @@
import Image from 'next/image'
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'
import {getProfile} from '@/actions/auth'
import {format} from 'date-fns'
import {CheckCircleIcon, CircleAlertIcon} from 'lucide-react'
import {Button, buttonVariants} from '@/components/ui/button'
import RechargeModal from '@/components/composites/recharge'
import {merge} from '@/lib/utils'
import Link from 'next/link'
import actionBill from '../_assets/action-bill.webp'
import actionBuy from '../_assets/action-buy.webp'
import actionLogout from '../_assets/action-logout.webp'
async function UserCenter() {
const resp = await getProfile()
if (!resp.success) {
return (
<div className="col-start-4 row-start-1 row-span-2 flex justify-center items-center">
</div>
)
}
const profile = resp.data
return (
<Card className="col-start-4 row-start-1 row-span-2">
<CardContent className="flex-auto flex flex-col justify-between">
<div className="flex flex-col gap-1">
<p>{profile.phone}</p>
<p className="text-sm text-weak">{`最后登录:${format(profile.last_login, 'yyyy-MM-dd HH:mm')}`}</p>
</div>
<div className={merge(
`flex justify-between p-2 rounded-md`,
profile.id_token ? `bg-done-muted` : `bg-warn-muted`,
)}
>
{profile.id_token
? (
<>
<div className="flex gap-2 items-center">
<CheckCircleIcon size={20} className="text-done"/>
<span></span>
</div>
<div className="flex flex-col items-end">
<span className="text-sm">{profile.name}</span>
<span className="text-xs text-weak">{profile.id_no}</span>
</div>
</>
)
: (
<>
<span className="flex gap-2 items-center">
<CircleAlertIcon className="text-warn"/>
<span></span>
</span>
<Button className="h-9"></Button>
</>
)}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-sm text-weak"></h4>
<div className="flex justify-between items-baseline">
<p className="text-xl text-accent">
{profile.balance}
</p>
<RechargeModal/>
</div>
</div>
<div className="flex flex-col gap-3">
<h4 className="text-sm text-weak"></h4>
<div className="flex justify-around gap-2">
<Link href="/admin/bills" className={merge(buttonVariants({theme: `ghost`}), `flex flex-col gap-2 py-2 px-3 h-auto`)}>
<Image alt="bill icon" src={actionBill} height={48}/>
<span className="text-sm text-weak"></span>
</Link>
<Link href="/admin/purchase" className={merge(buttonVariants({theme: `ghost`}), `flex flex-col gap-2 py-2 px-3 h-auto`)}>
<Image alt="buy icon" src={actionBuy} height={48}/>
<span className="text-sm text-weak"></span>
</Link>
<Link href="/admin/profile" className={merge(buttonVariants({theme: `ghost`}), `flex flex-col gap-2 py-2 px-3 h-auto`)}>
<Image alt="logout icon" src={actionLogout} height={48}/>
<span className="text-sm text-weak"></span>
</Link>
</div>
</div>
</CardContent>
</Card>
)
}
export default UserCenter

View File

@@ -1,6 +1,5 @@
import { ReactNode } from 'react'
import { Metadata } from 'next'
import {ReactNode} from 'react'
import {Metadata} from 'next'
export async function generateMetadata(): Promise<Metadata> {
return {
@@ -14,4 +13,4 @@ export type BillsLayoutProps = {
export default async function BillsLayout(props: BillsLayoutProps) {
return props.children
}
}

View File

@@ -1,170 +1,191 @@
import Page from '@/components/page'
import Image from 'next/image'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { getProfile } from '@/actions/auth'
import { format } from 'date-fns'
import { CheckCircleIcon, CircleAlertIcon } from 'lucide-react'
import { Button, buttonVariants } from '@/components/ui/button'
import RechargeModal from '@/components/composites/recharge'
import { merge } from '@/lib/utils'
import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'
import {format} from 'date-fns'
import {merge} from '@/lib/utils'
import banner from './_assets/banner.webp'
import actionBill from './_assets/action-bill.webp'
import actionBuy from './_assets/action-buy.webp'
import actionLogout from './_assets/action-logout.webp'
import Link from 'next/link'
import { listAnnouncements } from '@/actions/announcement'
import {listInitialization} from '@/actions/dashboard'
import Charts from './_client/charts'
import Pins from './_client/pins'
import UserCenter from './_client/userCenter'
import soon from './_assets/coming-soon.svg'
import mask from './_assets/Mask group.webp'
export type DashboardPageProps = {}
export default function DashboardPage(props: DashboardPageProps) {
return (
<Page className={merge(
`flex-auto grid`,
`grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_300px]`,
`grid-rows-[150px_200px_minmax(150px,1fr)_minmax(150px,1fr)]`,
)}>
{/* banner */}
<section className={`col-start-1 row-start-1 col-span-3 relative rounded-lg overflow-hidden`}>
<Image src={banner} alt={`banner image`} className={`w-full h-full inset-0 absolute object-cover`} />
<div className={`flex flex-col absolute inset-0 justify-center px-8 gap-1`}>
<h3 className={`text-2xl text-primary font-medium`}>IP资源</h3>
<p className={`text-primary font-medium`}>//IP代理</p>
</div>
</section>
{/* 磁贴集 */}
<Pins />
{/* 图表 */}
<Charts />
{/* 信息 */}
<UserCenter />
{/* 通知 */}
<Announcements />
</Page>
)
}
async function UserCenter() {
const resp = await getProfile()
if (!resp.success) {
return (
<div className={`col-start-4 row-start-1 row-span-2 flex justify-center items-center`}>
</div>
)
}
const profile = resp.data
return (
<Card className={`col-start-4 row-start-1 row-span-2`}>
<CardContent className={`flex-auto flex flex-col justify-between`}>
<div className={`flex flex-col gap-1`}>
<p>{profile.phone}</p>
<p className={`text-sm text-weak`}>{`最后登录:${format(profile.last_login, 'yyyy-MM-dd HH:mm')}`}</p>
</div>
<div className={merge(
`flex justify-between p-2 rounded-md`,
profile.id_token ? `bg-done-muted` : `bg-warn-muted`,
)}>
{profile.id_token
? <>
<div className={`flex gap-2 items-center`}>
<CheckCircleIcon size={20} className={`text-done`} />
<span></span>
</div>
<div className={`flex flex-col items-end`}>
<span className={`text-sm`}>{profile.name}</span>
<span className={`text-xs text-weak`}>{profile.id_no}</span>
</div>
</>
: <>
<span className={`flex gap-2 items-center`}>
<CircleAlertIcon className={`text-warn`} />
<span></span>
</span>
<Button className={`h-9`}></Button>
</>
}
</div>
<div className={`flex flex-col gap-1`}>
<h4 className={`text-sm text-weak`}></h4>
<div className={`flex justify-between items-baseline`}>
<p className={`text-xl text-accent`}>{profile.balance}</p>
<RechargeModal />
</div>
</div>
<div className={`flex flex-col gap-3`}>
<h4 className={`text-sm text-weak`}></h4>
<div className={`flex justify-around gap-2`}>
<Link href="/admin/bills" className={merge(buttonVariants({ variant: `ghost` }), `flex flex-col gap-2 py-2 px-3 h-auto`)}>
<Image alt={`bill icon`} src={actionBill} height={48} />
<span className={`text-sm text-weak`}></span>
</Link>
<Link href="/admin/purchase" className={merge(buttonVariants({ variant: `ghost` }), `flex flex-col gap-2 py-2 px-3 h-auto`)}>
<Image alt={`buy icon`} src={actionBuy} height={48} />
<span className={`text-sm text-weak`}></span>
</Link>
<Link href="/admin/profile" className={merge(buttonVariants({ variant: `ghost` }), `flex flex-col gap-2 py-2 px-3 h-auto`)}>
<Image alt={`logout icon`} src={actionLogout} height={48} />
<span className={`text-sm text-weak`}></span>
</Link>
</div>
</div>
</CardContent>
</Card>
)
}
async function Announcements() {
const resp = await listAnnouncements({
export default async function DashboardPage(props: DashboardPageProps) {
const resp = await listInitialization({
page: 1,
size: 5,
})
if (!resp.success) {
return (
<div className={`col-start-4 row-start-3 row-span-2 flex justify-center items-center`}>
<div className="col-start-4 row-start-3 row-span-2 flex justify-center items-center">
</div>
)
}
const announcements = resp.data.list
const initData = resp.data
return (
<Card className={`col-start-4 row-start-3 row-span-2`}>
<Page className={merge(
`flex-auto grid`,
`grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_300px]`,
`grid-rows-[150px_200px_minmax(150px,1fr)_minmax(150px,1fr)]`,
)}
>
{/* banner */}
<section className="col-start-1 row-start-1 col-span-3 relative rounded-lg overflow-hidden">
<Image src={banner} alt="banner image" className="w-full h-full inset-0 absolute object-cover"/>
<div className="flex flex-col absolute inset-0 justify-center px-8 gap-1">
<h3 className="text-2xl text-primary font-medium">IP资源</h3>
<p className="text-primary font-medium">//IP代理</p>
</div>
</section>
{/* 磁贴集 */}
{initData && (
<Pins
short_term={String(initData.short_term)}
short_term_monthly={String(initData.short_term_monthly)}
long_term={String(initData.long_term)}
long_term_monthly={String(initData.long_term_monthly)}
/>
)}
{/* 图表 */}
<Charts/>
{/* 信息 */}
<UserCenter/>
{/* 通知 */}
{initData && (
<Announcements list={initData.list}/>
)}
</Page>
)
}
type DashboardChartProps = {
short_term: string
short_term_monthly: string
long_term: string
long_term_monthly: string
}
function Pins(props: DashboardChartProps) {
return (
<>
{/* 短效 */}
<Card className="col-start-1 row-start-2 py-4">
<CardHeader>
<CardTitle>
<Image src={mask} alt="Mask group" width={35} height={35} priority/>
{' '}
</CardTitle>
</CardHeader>
<CardContent className="flex-auto flex flex-col gap-2">
<div className="flex-1 flex items-center justify-between">
<h4></h4>
<p className="flex flex-col items-end">
<span className="text-sm text-weak"></span>
<span className="text-lg">{props.short_term ? props.short_term : '1'}</span>
</p>
</div>
<div className="border-b"></div>
<div className="flex-1 flex items-center justify-between">
<h4></h4>
<p className="flex flex-col items-end">
<span className="text-sm text-weak"></span>
<span className="text-lg">{props.short_term_monthly}</span>
</p>
</div>
</CardContent>
</Card>
{/* 长效 */}
<Card className="col-start-2 row-start-2">
<CardHeader>
<CardTitle>
<Image src={mask} alt="Mask group" width={35} height={35} priority/>
{' '}
</CardTitle>
</CardHeader>
<CardContent className="flex-auto flex flex-col gap-2">
{/* <Image alt={`coming soon`} src={soon} />
<p>敬请期待</p> */}
<div className="flex-1 flex items-center justify-between">
<h4></h4>
<p className="flex flex-col items-end">
<span className="text-sm text-weak"></span>
<span className="text-lg">{props.long_term}</span>
</p>
</div>
<div className="border-b"></div>
<div className="flex-1 flex items-center justify-between">
<h4></h4>
<p className="flex flex-col items-end">
<span className="text-sm text-weak"></span>
<span className="text-lg">{props.long_term_monthly}</span>
</p>
</div>
</CardContent>
</Card>
{/* 固定 */}
<Card className="col-start-3 row-start-2 py-4">
<CardHeader className="px-4">
<CardTitle>IP套餐</CardTitle>
</CardHeader>
<CardContent className="flex-auto flex flex-col gap-2 items-center justify-center">
<Image alt="coming soon" src={soon}/>
<p></p>
</CardContent>
</Card>
</>
)
}
type Props = {
list: {
id: number
title: string
created_at: Date
} []
}
function Announcements(props: Props) {
return (
<Card className="col-start-4 row-start-3 row-span-2">
<CardHeader>
<div className={`flex justify-between gap-2`}>
<div className="flex justify-between gap-2">
<CardTitle></CardTitle>
<span className={`text-sm text-weak`}></span>
<span className="text-sm text-weak"></span>
</div>
</CardHeader>
<CardContent className={`flex-auto p-0`}>
{announcements.length === 0
<CardContent className="flex-auto p-0">
{!props.list.length
? (
<div className={`flex flex-col items-center justify-center gap-2 h-full`}>
{/* <Image alt={`coming soon`} src={soon} />
<p>暂无公告</p> */}
</div>
)
: announcements.map(item => (
<div key={item.id} className={merge(
`transition-colors duration-150 ease-in-out`,
`flex flex-col gap-1 px-4 py-2`,
`hover:bg-muted cursor-pointer`,
)}>
<h4>{item.title}</h4>
<p className={`text-sm text-weak`}>{format(item.created_at, 'yyyy-MM-dd HH:mm')}</p>
</div>
))
}
<div className="flex flex-col items-center justify-center gap-2 h-full">
<Image alt="coming soon" src={soon}/>
<p></p>
</div>
)
: props.list.map(item => (
<div
key={item.id}
className={merge(
`transition-colors duration-150 ease-in-out`,
`flex flex-col gap-1 px-4 py-2`,
`hover:bg-muted cursor-pointer`,
)}
>
<h4>{item.title}</h4>
<p className="text-sm text-weak">{format(item.created_at, 'yyyy-MM-dd HH:mm')}</p>
</div>
))}
</CardContent>
</Card>
)