This commit is contained in:
phaichayon
2026-06-11 12:46:57 +07:00
parent 1993d88306
commit 7a390cf0df
720 changed files with 100919 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
export function AreaGraphSkeleton() {
return (
<Card>
<CardHeader>
<div className='flex items-center gap-2'>
<Skeleton className='h-6 w-[140px]' />
<Skeleton className='h-5 w-[60px] rounded-full' />
</div>
<Skeleton className='h-4 w-[250px]' />
</CardHeader>
<CardContent>
<div className='relative aspect-auto h-[280px] w-full'>
<div className='from-primary/5 to-primary/20 absolute inset-0 rounded-lg bg-linear-to-t' />
<Skeleton className='absolute right-0 bottom-0 left-0 h-[1px]' />
<Skeleton className='absolute top-0 bottom-0 left-0 w-[1px]' />
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,116 @@
'use client';
import { Area, AreaChart, CartesianGrid, XAxis } from 'recharts';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent
} from '@/components/ui/chart';
import { Badge } from '@/components/ui/badge';
import { Icons } from '@/components/icons';
import React from 'react';
const chartData = [
{ month: 'January', desktop: 342, mobile: 245 },
{ month: 'February', desktop: 876, mobile: 654 },
{ month: 'March', desktop: 512, mobile: 387 },
{ month: 'April', desktop: 629, mobile: 521 },
{ month: 'May', desktop: 458, mobile: 412 },
{ month: 'June', desktop: 781, mobile: 598 },
{ month: 'July', desktop: 394, mobile: 312 },
{ month: 'August', desktop: 925, mobile: 743 },
{ month: 'September', desktop: 647, mobile: 489 },
{ month: 'October', desktop: 532, mobile: 476 },
{ month: 'November', desktop: 803, mobile: 687 },
{ month: 'December', desktop: 271, mobile: 198 }
];
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'var(--chart-1)'
},
mobile: {
label: 'Mobile',
color: 'var(--chart-2)'
}
} satisfies ChartConfig;
export function AreaGraph() {
return (
<Card>
<CardHeader>
<CardTitle>
Dotted Area Chart
<Badge variant='outline'>
<Icons.trendingUp />
-5.2%
</Badge>
</CardTitle>
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<AreaChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} strokeDasharray='3 3' />
<XAxis
dataKey='month'
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<defs>
<DottedBackgroundPattern config={chartConfig} />
</defs>
<Area
dataKey='mobile'
type='natural'
fill='url(#dotted-background-pattern-mobile)'
fillOpacity={0.4}
stroke='var(--color-mobile)'
stackId='a'
strokeWidth={0.8}
/>
<Area
dataKey='desktop'
type='natural'
fill='url(#dotted-background-pattern-desktop)'
fillOpacity={0.4}
stroke='var(--color-desktop)'
stackId='a'
strokeWidth={0.8}
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}
const DottedBackgroundPattern = ({ config }: { config: ChartConfig }) => {
const items = Object.fromEntries(
Object.entries(config).map(([key, value]) => [key, value.color])
);
return (
<>
{Object.entries(items).map(([key, value]) => (
<pattern
key={key}
id={`dotted-background-pattern-${key}`}
x='0'
y='0'
width='7'
height='7'
patternUnits='userSpaceOnUse'
>
<circle cx='5' cy='5' r='1.5' fill={value} opacity={0.5}></circle>
</pattern>
))}
</>
);
};

View File

@@ -0,0 +1,29 @@
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
export function BarGraphSkeleton() {
return (
<Card>
<CardHeader>
<div className='flex items-center gap-2'>
<Skeleton className='h-6 w-[160px]' />
<Skeleton className='h-5 w-[60px] rounded-full' />
</div>
<Skeleton className='h-4 w-[150px]' />
</CardHeader>
<CardContent>
<div className='flex aspect-auto h-[280px] w-full items-end justify-around gap-2 pt-8'>
{Array.from({ length: 12 }).map((_, i) => (
<Skeleton
key={i}
className='w-full rounded-t-sm'
style={{
height: `${Math.max(20, Math.random() * 100)}%`
}}
/>
))}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,144 @@
'use client';
import { Bar, BarChart, XAxis } from 'recharts';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent
} from '@/components/ui/chart';
import { Badge } from '@/components/ui/badge';
import { Icons } from '@/components/icons';
const chartData = [
{ month: 'January', desktop: 186, mobile: 80 },
{ month: 'February', desktop: 305, mobile: 200 },
{ month: 'March', desktop: 237, mobile: 120 },
{ month: 'April', desktop: 73, mobile: 190 },
{ month: 'May', desktop: 209, mobile: 130 },
{ month: 'June', desktop: 214, mobile: 140 }
];
const chartConfig = {
desktop: {
label: 'Desktop',
color: 'var(--chart-1)'
},
mobile: {
label: 'Mobile',
color: 'var(--chart-2)'
}
} satisfies ChartConfig;
export function BarGraph() {
return (
<Card>
<CardHeader>
<CardTitle>
Bar Chart - Multiple
<Badge variant='outline'>
<Icons.trendingDown />
-5.2%
</Badge>
</CardTitle>
<CardDescription>January - June 2025</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig}>
<BarChart accessibilityLayer data={chartData}>
<rect
x='0'
y='0'
width='100%'
height='85%'
fill='url(#default-multiple-pattern-dots)'
/>
<defs>
<DottedBackgroundPattern />
</defs>
<XAxis
dataKey='month'
tickLine={false}
tickMargin={10}
axisLine={false}
tickFormatter={(value) => value.slice(0, 3)}
/>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator='dashed' hideLabel />}
/>
<Bar
dataKey='desktop'
color='var(--chart-1)'
fill='var(--color-desktop)'
shape={<CustomHatchedBar isHatched={false} />}
radius={4}
/>
<Bar
dataKey='mobile'
fill='var(--color-mobile)'
shape={<CustomHatchedBar />}
radius={4}
/>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
);
}
const CustomHatchedBar = (
props: React.SVGProps<SVGRectElement> & {
dataKey?: string;
isHatched?: boolean;
}
) => {
const { fill, x, y, width, height, dataKey } = props;
const isHatched = props.isHatched ?? true;
return (
<>
<rect
rx={4}
x={x}
y={y}
width={width}
height={height}
stroke='none'
fill={isHatched ? `url(#hatched-bar-pattern-${dataKey})` : fill}
/>
<defs>
<pattern
key={dataKey}
id={`hatched-bar-pattern-${dataKey}`}
x='0'
y='0'
width='5'
height='5'
patternUnits='userSpaceOnUse'
patternTransform='rotate(-45)'
>
<rect width='10' height='10' opacity={0.5} fill={fill}></rect>
<rect width='1' height='10' fill={fill}></rect>
</pattern>
</defs>
</>
);
};
const DottedBackgroundPattern = () => {
return (
<pattern
id='default-multiple-pattern-dots'
x='0'
y='0'
width='10'
height='10'
patternUnits='userSpaceOnUse'
>
<circle className='dark:text-muted/40 text-muted' cx='2' cy='2' r='1' fill='currentColor' />
</pattern>
);
};

View File

@@ -0,0 +1,138 @@
import PageContainer from '@/components/layout/page-container';
import { Button } from '@/components/ui/button';
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
CardAction
} from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { AreaGraph } from './area-graph';
import { BarGraph } from './bar-graph';
import { PieGraph } from './pie-graph';
import { RecentSales } from './recent-sales';
import { Icons } from '@/components/icons';
import { Badge } from '@/components/ui/badge';
export default function OverViewPage() {
return (
<PageContainer>
<div className='flex flex-1 flex-col space-y-2'>
<div className='flex items-center justify-between space-y-2'>
<h2 className='text-2xl font-bold tracking-tight'>Hi, Welcome back 👋</h2>
<div className='hidden items-center space-x-2 md:flex'>
<Button>Download</Button>
</div>
</div>
<Tabs defaultValue='overview' className='space-y-4'>
<TabsList>
<TabsTrigger value='overview'>Overview</TabsTrigger>
<TabsTrigger value='analytics' disabled>
Analytics
</TabsTrigger>
</TabsList>
<TabsContent value='overview' className='space-y-4'>
<div className='*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4'>
<Card className='@container/card'>
<CardHeader>
<CardDescription>Total Revenue</CardDescription>
<CardTitle className='text-2xl font-semibold tabular-nums @[250px]/card:text-3xl'>
$1,250.00
</CardTitle>
<CardAction>
<Badge variant='outline'>
<Icons.trendingUp />
+12.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className='flex-col items-start gap-1.5 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Trending up this month <Icons.trendingUp className='size-4' />
</div>
<div className='text-muted-foreground'>Visitors for the last 6 months</div>
</CardFooter>
</Card>
<Card className='@container/card'>
<CardHeader>
<CardDescription>New Customers</CardDescription>
<CardTitle className='text-2xl font-semibold tabular-nums @[250px]/card:text-3xl'>
1,234
</CardTitle>
<CardAction>
<Badge variant='outline'>
<Icons.trendingDown />
-20%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className='flex-col items-start gap-1.5 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Down 20% this period <Icons.trendingDown className='size-4' />
</div>
<div className='text-muted-foreground'>Acquisition needs attention</div>
</CardFooter>
</Card>
<Card className='@container/card'>
<CardHeader>
<CardDescription>Active Accounts</CardDescription>
<CardTitle className='text-2xl font-semibold tabular-nums @[250px]/card:text-3xl'>
45,678
</CardTitle>
<CardAction>
<Badge variant='outline'>
<Icons.trendingUp />
+12.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className='flex-col items-start gap-1.5 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Strong user retention <Icons.trendingUp className='size-4' />
</div>
<div className='text-muted-foreground'>Engagement exceed targets</div>
</CardFooter>
</Card>
<Card className='@container/card'>
<CardHeader>
<CardDescription>Growth Rate</CardDescription>
<CardTitle className='text-2xl font-semibold tabular-nums @[250px]/card:text-3xl'>
4.5%
</CardTitle>
<CardAction>
<Badge variant='outline'>
<Icons.trendingUp />
+4.5%
</Badge>
</CardAction>
</CardHeader>
<CardFooter className='flex-col items-start gap-1.5 text-sm'>
<div className='line-clamp-1 flex gap-2 font-medium'>
Steady performance increase <Icons.trendingUp className='size-4' />
</div>
<div className='text-muted-foreground'>Meets growth projections</div>
</CardFooter>
</Card>
</div>
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>
<div className='col-span-4'>
<BarGraph />
</div>
<Card className='col-span-4 md:col-span-3'>
<RecentSales />
</Card>
<div className='col-span-4'>
<AreaGraph />
</div>
<div className='col-span-4 md:col-span-3'>
<PieGraph />
</div>
</div>
</TabsContent>
</Tabs>
</div>
</PageContainer>
);
}

View File

@@ -0,0 +1,19 @@
import { Skeleton } from '@/components/ui/skeleton';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
export function PieGraphSkeleton() {
return (
<Card className='flex h-full flex-col'>
<CardHeader className='items-center pb-0'>
<div className='flex items-center gap-2'>
<Skeleton className='h-6 w-[100px]' />
<Skeleton className='h-5 w-[60px] rounded-full' />
</div>
<Skeleton className='h-4 w-[150px]' />
</CardHeader>
<CardContent className='flex flex-1 items-center justify-center pb-0'>
<Skeleton className='h-[250px] w-[250px] rounded-full' />
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,91 @@
'use client';
import { LabelList, Pie, PieChart } from 'recharts';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent
} from '@/components/ui/chart';
import { Badge } from '@/components/ui/badge';
import { Icons } from '@/components/icons';
const chartData = [
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' }
];
const chartConfig = {
visitors: {
label: 'Visitors'
},
chrome: {
label: 'Chrome',
color: 'var(--chart-1)'
},
safari: {
label: 'Safari',
color: 'var(--chart-2)'
},
firefox: {
label: 'Firefox',
color: 'var(--chart-3)'
},
edge: {
label: 'Edge',
color: 'var(--chart-4)'
},
other: {
label: 'Other',
color: 'var(--chart-5)'
}
} satisfies ChartConfig;
export function PieGraph() {
return (
<Card className='flex h-full flex-col'>
<CardHeader className='items-center pb-0'>
<CardTitle>
Pie Chart
<Badge variant='outline'>
<Icons.trendingUp />
+5.2%
</Badge>
</CardTitle>
<CardDescription>January - June 2024</CardDescription>
</CardHeader>
<CardContent className='flex flex-1 items-center justify-center pb-0'>
<ChartContainer
config={chartConfig}
className='[&_.recharts-text]:fill-background mx-auto aspect-square max-h-[300px] min-h-[250px]'
>
<PieChart>
<ChartTooltip content={<ChartTooltipContent nameKey='visitors' hideLabel />} />
<Pie
data={chartData}
innerRadius={30}
dataKey='visitors'
radius={10}
cornerRadius={8}
paddingAngle={4}
>
<LabelList
dataKey='visitors'
stroke='none'
fontSize={12}
fontWeight={500}
fill='currentColor'
formatter={(value: number) => value.toString()}
/>
</Pie>
</PieChart>
</ChartContainer>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,27 @@
import { Skeleton } from '@/components/ui/skeleton';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
export function RecentSalesSkeleton() {
return (
<Card className='h-full'>
<CardHeader>
<Skeleton className='h-6 w-[140px]' /> {/* CardTitle */}
<Skeleton className='h-4 w-[180px]' /> {/* CardDescription */}
</CardHeader>
<CardContent>
<div className='space-y-8'>
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className='flex items-center'>
<Skeleton className='h-9 w-9 rounded-full' /> {/* Avatar */}
<div className='ml-4 space-y-1'>
<Skeleton className='h-4 w-[120px]' /> {/* Name */}
<Skeleton className='h-4 w-[160px]' /> {/* Email */}
</div>
<Skeleton className='ml-auto h-4 w-[80px]' /> {/* Amount */}
</div>
))}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,68 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Card, CardHeader, CardContent, CardTitle, CardDescription } from '@/components/ui/card';
const salesData = [
{
name: 'Olivia Martin',
email: 'olivia.martin@email.com',
avatar: 'https://api.slingacademy.com/public/sample-users/1.png',
fallback: 'OM',
amount: '+$1,999.00'
},
{
name: 'Jackson Lee',
email: 'jackson.lee@email.com',
avatar: 'https://api.slingacademy.com/public/sample-users/2.png',
fallback: 'JL',
amount: '+$39.00'
},
{
name: 'Isabella Nguyen',
email: 'isabella.nguyen@email.com',
avatar: 'https://api.slingacademy.com/public/sample-users/3.png',
fallback: 'IN',
amount: '+$299.00'
},
{
name: 'William Kim',
email: 'will@email.com',
avatar: 'https://api.slingacademy.com/public/sample-users/4.png',
fallback: 'WK',
amount: '+$99.00'
},
{
name: 'Sofia Davis',
email: 'sofia.davis@email.com',
avatar: 'https://api.slingacademy.com/public/sample-users/5.png',
fallback: 'SD',
amount: '+$39.00'
}
];
export function RecentSales() {
return (
<Card className='h-full'>
<CardHeader>
<CardTitle>Recent Sales</CardTitle>
<CardDescription>You made 265 sales this month.</CardDescription>
</CardHeader>
<CardContent>
<div className='space-y-8'>
{salesData.map((sale, index) => (
<div key={index} className='flex items-center'>
<Avatar className='h-9 w-9'>
<AvatarImage src={sale.avatar} alt='Avatar' />
<AvatarFallback>{sale.fallback}</AvatarFallback>
</Avatar>
<div className='ml-4 space-y-1'>
<p className='text-sm leading-none font-medium'>{sale.name}</p>
<p className='text-muted-foreground text-sm'>{sale.email}</p>
</div>
<div className='ml-auto font-medium'>{sale.amount}</div>
</div>
))}
</div>
</CardContent>
</Card>
);
}