Skip to content

Instantly share code, notes, and snippets.

@shricodev
Created March 30, 2026 16:07
Show Gist options
  • Select an option

  • Save shricodev/6b734610378fbbed6e524f137618d15f to your computer and use it in GitHub Desktop.

Select an option

Save shricodev/6b734610378fbbed6e524f137618d15f to your computer and use it in GitHub Desktop.
Admin Dashboard - Kombai 1
From bca15e18aa4cd161bcd5d0bd29a2d3157414e002 Mon Sep 17 00:00:00 2001
From: Shrijal Acharya <shrijal.acharya@gmail.com>
Date: Sat, 28 Mar 2026 16:39:10 +0545
Subject: [PATCH 1/2] test kombai 1
---
bun.lock | 7 +
env.example.txt | 101 --------
package.json | 1 +
src/app/dashboard/overview/layout.tsx | 222 +++++++++--------
src/components/icons.tsx | 6 +
src/components/ui/carousel.tsx | 230 ++++++++++++++++++
.../overview/components/area-graph.tsx | 207 +++++++++-------
.../overview/components/bar-graph.tsx | 184 ++++++--------
.../overview/components/customer-reviews.tsx | 174 +++++++++++++
.../overview/components/pie-graph.tsx | 149 ++++++------
.../overview/components/recent-sales.tsx | 174 +++++++++----
src/styles/globals.css | 17 ++
12 files changed, 940 insertions(+), 532 deletions(-)
delete mode 100644 env.example.txt
create mode 100644 src/components/ui/carousel.tsx
create mode 100644 src/features/overview/components/customer-reviews.tsx
diff --git a/bun.lock b/bun.lock
index 9a96765..61acd0d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -50,6 +50,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
+ "embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"kbar": "^0.1.0-beta.48",
"match-sorter": "^8.2.0",
@@ -881,6 +882,12 @@
"electron-to-chromium": ["electron-to-chromium@1.5.244", "", {}, "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw=="],
+ "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],
+
+ "embla-carousel-react": ["embla-carousel-react@8.6.0", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA=="],
+
+ "embla-carousel-reactive-utils": ["embla-carousel-reactive-utils@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="],
+
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
diff --git a/env.example.txt b/env.example.txt
deleted file mode 100644
index 2263d06..0000000
--- a/env.example.txt
+++ /dev/null
@@ -1,101 +0,0 @@
-# =================================================================
-# Authentication Configuration (Clerk)
-# =================================================================
-# IMPORTANT: This template supports Clerk's keyless mode!
-# You can start using the app immediately without any configuration.
-# When you're ready to claim your application, simply click the Clerk
-# popup at the bottom of the screen to get your API keys.
-
-# Required: Clerk API Keys (Leave empty for keyless mode)
-
-NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
-CLERK_SECRET_KEY=
-
-
-# Authentication Redirect URLs
-# These control where users are directed after authentication actions
-
-NEXT_PUBLIC_CLERK_SIGN_IN_URL="/auth/sign-in"
-NEXT_PUBLIC_CLERK_SIGN_UP_URL="/auth/sign-up"
-NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL="/dashboard/overview"
-NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL="/dashboard/overview"
-
-
-# =================================================================
-# Clerk Organizations Configuration
-# =================================================================
-# To enable Organizations in your Clerk Dashboard:
-# 1. Go to https://dashboard.clerk.com
-# 2. Navigate to Organizations Settings
-# 3. Enable "Organizations"
-# 4. Configure default roles (admin, member, viewer) if needed
-# 5. Set membership limits (default: 5 members on Free plan)
-#
-# Development Plan Features:
-# - Up to 50 Monthly Active Organizations (MAOs) in development
-# - Up to 5 members per organization on Free plan
-# - Unlimited members on Pro plan
-#
-# For more information, see: https://clerk.com/docs/organizations/overview
-
-
-# =================================================================
-# Clerk Webhooks (Optional - for production billing)
-# =================================================================
-# If you're using Clerk Billing or webhooks, add your webhook secret here.
-# You can find this in Clerk Dashboard > Webhooks
-
-WEBHOOK_SECRET= # Example: whsec_...
-
-
-# =================================================================
-# Build Configuration
-# =================================================================
-# Set to 'true' when deploying with Docker or self-hosting on a VPS.
-# This enables Next.js standalone output mode for smaller deployments.
-
-BUILD_STANDALONE= # Example: "true"
-
-
-# =================================================================
-# Error Tracking Configuration (Sentry)
-# =================================================================
-# To set up Sentry error tracking:
-# 1. Create an account at https://sentry.io
-# 2. Create a new project for Next.js
-# 3. Follow the setup instructions below
-
-# Step 1: Sentry DSN (Required)
-# Found at: Settings > Projects > [Your Project] > Client Keys (DSN)
-
-NEXT_PUBLIC_SENTRY_DSN= #Example: https://****@****.ingest.sentry.io/****
-
-
-# Step 2: Organization & Project Details
-# Found at: Settings > Organization > General Settings
-
-NEXT_PUBLIC_SENTRY_ORG= # Example: acme-corp
-NEXT_PUBLIC_SENTRY_PROJECT= # Example: nextjs-dashboard
-
-
-# Step 3: Sentry Auth Token
-
-# Sentry can automatically provide readable stack traces for errors using source maps, requiring a Sentry auth token.
-# More info: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#step-4-add-readable-stack-traces-with-source-maps-optional
-
-SENTRY_AUTH_TOKEN= #Example: sntrys_************************************
-
-
-# Step 4: Environment Control (Optional)
-# Set to 'true' to disable Sentry in development
-
-NEXT_PUBLIC_SENTRY_DISABLED= "false"
-
-
-# =================================================================
-# Important Notes:
-# =================================================================
-# 1. Rename this file to '.env' for local development
-# 2. Never commit the actual '.env' file to version control
-# 3. Make sure to replace all placeholder values with real ones
-# 4. Keep your secret keys private and never share them
diff --git a/package.json b/package.json
index ebce2ed..b3f6d90 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
+ "embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"kbar": "^0.1.0-beta.48",
"match-sorter": "^8.2.0",
diff --git a/src/app/dashboard/overview/layout.tsx b/src/app/dashboard/overview/layout.tsx
index 0d6393d..0db614c 100644
--- a/src/app/dashboard/overview/layout.tsx
+++ b/src/app/dashboard/overview/layout.tsx
@@ -1,15 +1,61 @@
import PageContainer from '@/components/layout/page-container';
-import { Badge } from '@/components/ui/badge';
-import {
- Card,
- CardHeader,
- CardTitle,
- CardDescription,
- CardAction,
- CardFooter
-} from '@/components/ui/card';
import { Icons } from '@/components/icons';
+import { CustomerReviews } from '@/features/overview/components/customer-reviews';
import React from 'react';
+import { cn } from '@/lib/utils';
+
+interface MetricCardProps {
+ label: string;
+ value: string;
+ trend: string;
+ trendUp: boolean;
+ icon: React.ReactNode;
+ trendPeriod?: string;
+}
+
+function MetricCard({
+ label,
+ value,
+ trend,
+ trendUp,
+ icon,
+ trendPeriod = '30 days'
+}: MetricCardProps) {
+ return (
+ <div className='bg-card border-border flex items-center gap-5 rounded-2xl border p-6 shadow-sm transition-shadow hover:shadow-md'>
+ {/* Icon circle */}
+ <div className='bg-(--overview-accent-muted) flex h-20 w-20 shrink-0 items-center justify-center rounded-full'>
+ {icon}
+ </div>
+
+ {/* Content */}
+ <div className='min-w-0'>
+ <p className='text-foreground text-4xl font-bold tabular-nums leading-tight'>{value}</p>
+ <p className='text-muted-foreground mt-1 text-sm'>{label}</p>
+ <div className='mt-2 flex items-center gap-1'>
+ {trendUp ? (
+ <Icons.trendingUp
+ className={cn(
+ 'h-4 w-4',
+ trendUp ? 'text-(--overview-accent)' : 'text-(--overview-red)'
+ )}
+ />
+ ) : (
+ <Icons.trendingDown className='text-(--overview-red) h-4 w-4' />
+ )}
+ <span
+ className={cn(
+ 'text-xs font-medium',
+ trendUp ? 'text-(--overview-accent)' : 'text-(--overview-red)'
+ )}
+ >
+ {trend} ({trendPeriod})
+ </span>
+ </div>
+ </div>
+ </div>
+ );
+}
export default function OverViewLayout({
sales,
@@ -24,102 +70,76 @@ export default function OverViewLayout({
}) {
return (
<PageContainer>
- <div className='flex flex-1 flex-col space-y-2'>
- <div className='flex items-center justify-between'>
- <h2 className='text-2xl font-bold tracking-tight'>Hi, Welcome back 👋</h2>
+ <div className='flex flex-1 flex-col gap-6'>
+ {/* Page heading */}
+ <div>
+ <h1 className='text-foreground text-2xl font-semibold'>Dashboard</h1>
+ <p className='text-muted-foreground mt-1 text-sm'>Hi, Welcome back to the Admin!</p>
</div>
- <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 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs md:grid-cols-2 lg: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>
+ {/* Metric cards */}
+ <div className='grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4'>
+ <MetricCard
+ label='Total Revenue'
+ value='$1,250'
+ trend='+12.5%'
+ trendUp={true}
+ icon={
+ <Icons.billing
+ className='text-(--overview-accent) h-9 w-9'
+ aria-label='Total Revenue'
+ />
+ }
+ />
+ <MetricCard
+ label='New Users'
+ value='1,234'
+ trend='-20%'
+ trendUp={false}
+ icon={
+ <Icons.userPlus className='text-(--overview-accent) h-9 w-9' aria-label='New Users' />
+ }
+ />
+ <MetricCard
+ label='Active Accounts'
+ value='45,678'
+ trend='+12.5%'
+ trendUp={true}
+ icon={
+ <Icons.teams
+ className='text-(--overview-accent) h-9 w-9'
+ aria-label='Active Accounts'
+ />
+ }
+ />
+ <MetricCard
+ label='Growth Rate'
+ value='4.5%'
+ trend='+4.5%'
+ trendUp={true}
+ icon={
+ <Icons.trendingUp
+ className='text-(--overview-accent) h-9 w-9'
+ aria-label='Growth Rate'
+ />
+ }
+ />
</div>
- <div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>
- <div className='col-span-4'>{bar_stats}</div>
- <div className='col-span-4 md:col-span-3'>
- {/* sales arallel routes */}
- {sales}
- </div>
- <div className='col-span-4'>{area_stats}</div>
- <div className='col-span-4 min-h-0 md:col-span-3'>{pie_stats}</div>
+
+ {/* Charts row 1: Pie charts + Activity overview */}
+ <div className='grid grid-cols-1 gap-4 lg:grid-cols-2'>
+ <div>{pie_stats}</div>
+ <div>{bar_stats}</div>
</div>
+
+ {/* Charts row 2: Revenue comparison (wider) + User distribution */}
+ <div className='grid grid-cols-1 gap-4 lg:grid-cols-7'>
+ <div className='lg:col-span-5'>{area_stats}</div>
+ <div className='lg:col-span-2'>{sales}</div>
+ </div>
+
+ {/* Customer feedback section */}
+ <CustomerReviews />
</div>
</PageContainer>
);
diff --git a/src/components/icons.tsx b/src/components/icons.tsx
index 14be643..7f72618 100644
--- a/src/components/icons.tsx
+++ b/src/components/icons.tsx
@@ -31,6 +31,7 @@ import {
IconDeviceLaptop,
IconDots,
IconDotsVertical,
+ IconDownload,
IconEdit,
IconExternalLink,
IconEyeOff,
@@ -73,6 +74,7 @@ import {
IconSparkles,
IconStack2,
IconStar,
+ IconStarFilled,
IconSun,
IconTrash,
IconTrendingDown,
@@ -83,6 +85,7 @@ import {
IconUser,
IconUserCircle,
IconUserEdit,
+ IconUserPlus,
IconUserX,
IconUsers,
IconVideo,
@@ -164,10 +167,12 @@ export const Icons = {
add: IconPlus,
edit: IconEdit,
upload: IconUpload,
+ download: IconDownload,
share: IconShare,
login: IconLogin,
logout: IconLogout,
gripVertical: IconGripVertical,
+ userPlus: IconUserPlus,
// Shapes / Indicators
circle: IconCircle,
@@ -189,6 +194,7 @@ export const Icons = {
product: IconBox,
pro: IconCrown,
exclusive: IconStar,
+ starFilled: IconStarFilled,
sparkles: IconSparkles,
badgeCheck: IconRosetteDiscountCheck,
lock: IconLock,
diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..632bf44
--- /dev/null
+++ b/src/components/ui/carousel.tsx
@@ -0,0 +1,230 @@
+'use client';
+
+import * as React from 'react';
+import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react';
+import { IconArrowLeft, IconArrowRight } from '@tabler/icons-react';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+
+type CarouselApi = UseEmblaCarouselType[1];
+type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
+type CarouselOptions = UseCarouselParameters[0];
+type CarouselPlugin = UseCarouselParameters[1];
+
+type CarouselProps = {
+ opts?: CarouselOptions;
+ plugins?: CarouselPlugin;
+ orientation?: 'horizontal' | 'vertical';
+ setApi?: (api: CarouselApi) => void;
+};
+
+type CarouselContextProps = {
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
+ api: ReturnType<typeof useEmblaCarousel>[1];
+ scrollPrev: () => void;
+ scrollNext: () => void;
+ canScrollPrev: boolean;
+ canScrollNext: boolean;
+} & CarouselProps;
+
+const CarouselContext = React.createContext<CarouselContextProps | null>(null);
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext);
+
+ if (!context) {
+ throw new Error('useCarousel must be used within a <Carousel />');
+ }
+
+ return context;
+}
+
+function Carousel({
+ orientation = 'horizontal',
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+}: React.ComponentProps<'div'> & CarouselProps) {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === 'horizontal' ? 'x' : 'y'
+ },
+ plugins
+ );
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) return;
+ setCanScrollPrev(api.canScrollPrev());
+ setCanScrollNext(api.canScrollNext());
+ }, []);
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev();
+ }, [api]);
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext();
+ }, [api]);
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
+ if (event.key === 'ArrowLeft') {
+ event.preventDefault();
+ scrollPrev();
+ } else if (event.key === 'ArrowRight') {
+ event.preventDefault();
+ scrollNext();
+ }
+ },
+ [scrollPrev, scrollNext]
+ );
+
+ React.useEffect(() => {
+ if (!api || !setApi) return;
+ setApi(api);
+ }, [api, setApi]);
+
+ React.useEffect(() => {
+ if (!api) return;
+ onSelect(api);
+ api.on('reInit', onSelect);
+ api.on('select', onSelect);
+
+ return () => {
+ api?.off('select', onSelect);
+ };
+ }, [api, onSelect]);
+
+ return (
+ <CarouselContext.Provider
+ value={{
+ carouselRef,
+ api: api,
+ opts,
+ orientation: orientation || (opts?.axis === 'y' ? 'vertical' : 'horizontal'),
+ scrollPrev,
+ scrollNext,
+ canScrollPrev,
+ canScrollNext
+ }}
+ >
+ <div
+ onKeyDownCapture={handleKeyDown}
+ className={cn('relative', className)}
+ role='region'
+ aria-roledescription='carousel'
+ data-slot='carousel'
+ {...props}
+ >
+ {children}
+ </div>
+ </CarouselContext.Provider>
+ );
+}
+
+function CarouselContent({ className, ...props }: React.ComponentProps<'div'>) {
+ const { carouselRef, orientation } = useCarousel();
+
+ return (
+ <div ref={carouselRef} className='overflow-hidden' data-slot='carousel-content'>
+ <div
+ className={cn('flex', orientation === 'horizontal' ? '-ml-4' : '-mt-4 flex-col', className)}
+ {...props}
+ />
+ </div>
+ );
+}
+
+function CarouselItem({ className, ...props }: React.ComponentProps<'div'>) {
+ const { orientation } = useCarousel();
+
+ return (
+ <div
+ role='group'
+ aria-roledescription='slide'
+ data-slot='carousel-item'
+ className={cn(
+ 'min-w-0 shrink-0 grow-0 basis-full',
+ orientation === 'horizontal' ? 'pl-4' : 'pt-4',
+ className
+ )}
+ {...props}
+ />
+ );
+}
+
+function CarouselPrevious({
+ className,
+ variant = 'outline',
+ size = 'icon',
+ ...props
+}: React.ComponentProps<typeof Button>) {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
+
+ return (
+ <Button
+ data-slot='carousel-previous'
+ variant={variant}
+ size={size}
+ className={cn(
+ 'absolute size-8 rounded-full',
+ orientation === 'horizontal'
+ ? 'top-1/2 -left-12 -translate-y-1/2'
+ : '-top-12 left-1/2 -translate-x-1/2 rotate-90',
+ className
+ )}
+ disabled={!canScrollPrev}
+ onClick={scrollPrev}
+ {...props}
+ >
+ <IconArrowLeft />
+ <span className='sr-only'>Previous slide</span>
+ </Button>
+ );
+}
+
+function CarouselNext({
+ className,
+ variant = 'outline',
+ size = 'icon',
+ ...props
+}: React.ComponentProps<typeof Button>) {
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
+
+ return (
+ <Button
+ data-slot='carousel-next'
+ variant={variant}
+ size={size}
+ className={cn(
+ 'absolute size-8 rounded-full',
+ orientation === 'horizontal'
+ ? 'top-1/2 -right-12 -translate-y-1/2'
+ : '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
+ className
+ )}
+ disabled={!canScrollNext}
+ onClick={scrollNext}
+ {...props}
+ >
+ <IconArrowRight />
+ <span className='sr-only'>Next slide</span>
+ </Button>
+ );
+}
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext
+};
diff --git a/src/features/overview/components/area-graph.tsx b/src/features/overview/components/area-graph.tsx
index ea75fd8..c1cf716 100644
--- a/src/features/overview/components/area-graph.tsx
+++ b/src/features/overview/components/area-graph.tsx
@@ -1,116 +1,149 @@
'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 { Line, LineChart, CartesianGrid, XAxis, YAxis, Tooltip, ReferenceDot } from 'recharts';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { ChartConfig, ChartContainer } from '@/components/ui/chart';
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 }
+ { month: 'Jan', year2023: 15000, year2024: 20000 },
+ { month: 'Feb', year2023: 25000, year2024: 30000 },
+ { month: 'Mar', year2023: 18000, year2024: 22000 },
+ { month: 'Apr', year2023: 28000, year2024: 32000 },
+ { month: 'May', year2023: 22000, year2024: 28000 },
+ { month: 'Jun', year2023: 38753, year2024: 25000 },
+ { month: 'Jul', year2023: 32000, year2024: 38000 },
+ { month: 'Aug', year2023: 20000, year2024: 28000 },
+ { month: 'Sep', year2023: 26000, year2024: 22000 },
+ { month: 'Oct', year2023: 12657, year2024: 29000 },
+ { month: 'Nov', year2023: 35000, year2024: 32000 },
+ { month: 'Dec', year2023: 30000, year2024: 38000 }
];
const chartConfig = {
- desktop: {
- label: 'Desktop',
- color: 'var(--chart-1)'
+ year2023: {
+ label: '2023',
+ color: 'var(--overview-blue)'
},
- mobile: {
- label: 'Mobile',
- color: 'var(--chart-2)'
+ year2024: {
+ label: '2024',
+ color: 'var(--overview-red)'
}
} satisfies ChartConfig;
+function formatValue(value: number): string {
+ if (value >= 1000) return `$${(value / 1000).toFixed(0)}k`;
+ return `$${value}`;
+}
+
+interface CustomTooltipProps {
+ active?: boolean;
+ payload?: Array<{ value: number; color: string; name: string }>;
+ label?: string;
+}
+
+function CustomTooltip({ active, payload, label }: CustomTooltipProps) {
+ if (active && payload && payload.length) {
+ return (
+ <div className='bg-background border-border rounded-xl border px-3 py-2 shadow-md'>
+ <p className='text-muted-foreground mb-1 text-xs'>{label}</p>
+ {payload.map((entry) => (
+ <p key={entry.name} className='text-sm font-semibold' style={{ color: entry.color }}>
+ ${entry.value.toLocaleString()}
+ </p>
+ ))}
+ </div>
+ );
+ }
+ return null;
+}
+
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>
+ <Card className='h-full'>
+ <CardHeader className='pb-2'>
+ <div className='flex items-center justify-between'>
+ <CardTitle className='text-lg font-bold'>Revenue Comparison</CardTitle>
+ <div className='flex items-center gap-4'>
+ <div className='flex items-center gap-1.5'>
+ <span className='bg-(--overview-blue) h-3 w-3 rounded-full' />
+ <span className='text-muted-foreground text-sm'>2023</span>
+ </div>
+ <div className='flex items-center gap-1.5'>
+ <span className='bg-(--overview-red) h-3 w-3 rounded-full' />
+ <span className='text-muted-foreground text-sm'>2024</span>
+ </div>
+ </div>
+ </div>
</CardHeader>
<CardContent>
- <ChartContainer config={chartConfig}>
- <AreaChart accessibilityLayer data={chartData}>
- <CartesianGrid vertical={false} strokeDasharray='3 3' />
+ <ChartContainer config={chartConfig} className='h-[280px] w-full'>
+ <LineChart data={chartData} margin={{ top: 16, right: 10, left: 0, bottom: 0 }}>
+ <CartesianGrid vertical={false} stroke='var(--border)' strokeOpacity={0.5} />
<XAxis
dataKey='month'
tickLine={false}
axisLine={false}
tickMargin={8}
- tickFormatter={(value) => value.slice(0, 3)}
+ tick={{ fill: 'var(--muted-foreground)', fontSize: 12 }}
+ />
+ <YAxis
+ tickLine={false}
+ axisLine={false}
+ tickMargin={8}
+ tickFormatter={formatValue}
+ tick={{ fill: 'var(--muted-foreground)', fontSize: 12 }}
+ width={45}
+ />
+ <Tooltip content={<CustomTooltip />} />
+ <Line
+ dataKey='year2023'
+ type='monotone'
+ stroke='var(--overview-blue)'
+ strokeWidth={2.5}
+ dot={{ r: 3.5, fill: 'var(--overview-blue)', strokeWidth: 0 }}
+ activeDot={{ r: 5, fill: 'var(--overview-blue)', stroke: '#fff', strokeWidth: 2 }}
+ />
+ <Line
+ dataKey='year2024'
+ type='monotone'
+ stroke='var(--overview-red)'
+ strokeWidth={2.5}
+ dot={{ r: 3.5, fill: 'var(--overview-red)', strokeWidth: 0 }}
+ activeDot={{ r: 5, fill: 'var(--overview-red)', stroke: '#fff', strokeWidth: 2 }}
/>
- <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}
+ {/* Highlighted reference points */}
+ <ReferenceDot
+ x='Jun'
+ y={38753}
+ r={6}
+ fill='var(--overview-blue)'
+ stroke='#fff'
+ strokeWidth={2}
+ label={{
+ value: '$38,753',
+ position: 'top',
+ fontSize: 11,
+ fill: 'var(--overview-blue)'
+ }}
/>
- <Area
- dataKey='desktop'
- type='natural'
- fill='url(#dotted-background-pattern-desktop)'
- fillOpacity={0.4}
- stroke='var(--color-desktop)'
- stackId='a'
- strokeWidth={0.8}
+ <ReferenceDot
+ x='Oct'
+ y={12657}
+ r={6}
+ fill='var(--overview-red)'
+ stroke='#fff'
+ strokeWidth={2}
+ label={{
+ value: '$12,657',
+ position: 'bottom',
+ fontSize: 11,
+ fill: 'var(--overview-red)'
+ }}
/>
- </AreaChart>
+ </LineChart>
</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>
- ))}
- </>
- );
-};
diff --git a/src/features/overview/components/bar-graph.tsx b/src/features/overview/components/bar-graph.tsx
index b961c73..1e6221c 100644
--- a/src/features/overview/components/bar-graph.tsx
+++ b/src/features/overview/components/bar-graph.tsx
@@ -1,144 +1,100 @@
'use client';
-import { Bar, BarChart, XAxis } from 'recharts';
-
+import { Area, AreaChart, CartesianGrid, XAxis, YAxis, Tooltip } 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 { ChartConfig, ChartContainer } from '@/components/ui/chart';
+import { Button } from '@/components/ui/button';
import { Icons } from '@/components/icons';
+import React from 'react';
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 }
+ { day: 'Sun', activity: 280 },
+ { day: 'Mon', activity: 456 },
+ { day: 'Tue', activity: 380 },
+ { day: 'Wed', activity: 510 },
+ { day: 'Thu', activity: 440 },
+ { day: 'Fri', activity: 380 },
+ { day: 'Sat', activity: 295 }
];
const chartConfig = {
- desktop: {
- label: 'Desktop',
- color: 'var(--chart-1)'
- },
- mobile: {
- label: 'Mobile',
- color: 'var(--chart-2)'
+ activity: {
+ label: 'Activity',
+ color: 'var(--overview-blue)'
}
} satisfies ChartConfig;
+interface CustomTooltipProps {
+ active?: boolean;
+ payload?: Array<{ value: number }>;
+ label?: string;
+}
+
+function CustomTooltip({ active, payload, label }: CustomTooltipProps) {
+ if (active && payload && payload.length) {
+ return (
+ <div className='bg-background border-border rounded-xl border px-3 py-2 shadow-md'>
+ <p className='text-foreground text-sm font-semibold'>{payload[0].value} Events</p>
+ <p className='text-muted-foreground text-xs'>{label}</p>
+ </div>
+ );
+ }
+ return null;
+}
+
export function BarGraph() {
return (
- <Card>
+ <Card className='h-full'>
<CardHeader>
- <CardTitle>
- Bar Chart - Multiple
- <Badge variant='outline'>
- <Icons.trendingDown />
- -5.2%
- </Badge>
- </CardTitle>
- <CardDescription>January - June 2025</CardDescription>
+ <div className='flex items-start justify-between gap-2'>
+ <div>
+ <CardTitle className='text-lg font-bold'>Activity Overview</CardTitle>
+ <CardDescription className='mt-1'>Daily user activity across the week</CardDescription>
+ </div>
+ <Button
+ variant='outline'
+ size='sm'
+ className='shrink-0 border-(--overview-blue) text-(--overview-blue) hover:bg-(--overview-blue)/10 hover:text-(--overview-blue) gap-1.5'
+ >
+ <Icons.download className='h-4 w-4' />
+ Save Report
+ </Button>
+ </div>
</CardHeader>
<CardContent>
- <ChartContainer config={chartConfig}>
- <BarChart accessibilityLayer data={chartData}>
- <rect
- x='0'
- y='0'
- width='100%'
- height='85%'
- fill='url(#default-multiple-pattern-dots)'
- />
+ <ChartContainer config={chartConfig} className='h-[220px] w-full'>
+ <AreaChart data={chartData} margin={{ top: 10, right: 10, left: -10, bottom: 0 }}>
<defs>
- <DottedBackgroundPattern />
+ <linearGradient id='activityGradient' x1='0' y1='0' x2='0' y2='1'>
+ <stop offset='5%' stopColor='var(--overview-blue)' stopOpacity={0.3} />
+ <stop offset='95%' stopColor='var(--overview-blue)' stopOpacity={0.02} />
+ </linearGradient>
</defs>
+ <CartesianGrid vertical={false} stroke='var(--border)' strokeOpacity={0.5} />
<XAxis
- dataKey='month'
+ dataKey='day'
tickLine={false}
- tickMargin={10}
axisLine={false}
- tickFormatter={(value) => value.slice(0, 3)}
- />
- <ChartTooltip
- cursor={false}
- content={<ChartTooltipContent indicator='dashed' hideLabel />}
+ tickMargin={8}
+ tick={{ fill: 'var(--muted-foreground)', fontSize: 13 }}
/>
- <Bar
- dataKey='desktop'
- color='var(--chart-1)'
- fill='var(--color-desktop)'
- shape={<CustomHatchedBar isHatched={false} />}
- radius={4}
+ <YAxis hide />
+ <Tooltip
+ cursor={{ stroke: 'var(--overview-blue)', strokeWidth: 1, strokeDasharray: '4 4' }}
+ content={<CustomTooltip />}
/>
- <Bar
- dataKey='mobile'
- fill='var(--color-mobile)'
- shape={<CustomHatchedBar />}
- radius={4}
+ <Area
+ dataKey='activity'
+ type='monotone'
+ fill='url(#activityGradient)'
+ stroke='var(--overview-blue)'
+ strokeWidth={2.5}
+ dot={{ r: 4, fill: 'var(--overview-blue)', strokeWidth: 0 }}
+ activeDot={{ r: 6, fill: 'var(--overview-blue)', stroke: '#ffffff', strokeWidth: 3 }}
/>
- </BarChart>
+ </AreaChart>
</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>
- );
-};
diff --git a/src/features/overview/components/customer-reviews.tsx b/src/features/overview/components/customer-reviews.tsx
new file mode 100644
index 0000000..18aecfd
--- /dev/null
+++ b/src/features/overview/components/customer-reviews.tsx
@@ -0,0 +1,174 @@
+'use client';
+
+import React from 'react';
+import {
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselNext,
+ CarouselPrevious
+} from '@/components/ui/carousel';
+import { Icons } from '@/components/icons';
+import { cn } from '@/lib/utils';
+
+interface Review {
+ id: number;
+ name: string;
+ avatar: string;
+ timeAgo: string;
+ text: string;
+ rating: number;
+ productImage: string;
+}
+
+const reviews: Review[] = [
+ {
+ id: 1,
+ name: 'James Wilson',
+ avatar: 'https://i.pravatar.cc/54?u=james-wilson',
+ timeAgo: '2 days ago',
+ text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text.",
+ rating: 4.5,
+ productImage: 'https://picsum.photos/223/223?random=10'
+ },
+ {
+ id: 2,
+ name: 'Sofia Chen',
+ avatar: 'https://i.pravatar.cc/54?u=sofia-chen',
+ timeAgo: '2 days ago',
+ text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text.",
+ rating: 4.0,
+ productImage: 'https://picsum.photos/223/223?random=11'
+ },
+ {
+ id: 3,
+ name: 'Anand Sharma',
+ avatar: 'https://i.pravatar.cc/54?u=anand-sharma',
+ timeAgo: '2 days ago',
+ text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text.",
+ rating: 4.5,
+ productImage: 'https://picsum.photos/223/223?random=12'
+ },
+ {
+ id: 4,
+ name: 'Maria Garcia',
+ avatar: 'https://i.pravatar.cc/54?u=maria-garcia',
+ timeAgo: '3 days ago',
+ text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text.",
+ rating: 5.0,
+ productImage: 'https://picsum.photos/223/223?random=13'
+ },
+ {
+ id: 5,
+ name: 'David Park',
+ avatar: 'https://i.pravatar.cc/54?u=david-park',
+ timeAgo: '4 days ago',
+ text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text.",
+ rating: 3.5,
+ productImage: 'https://picsum.photos/223/223?random=14'
+ }
+];
+
+function StarRating({ rating }: { rating: number }) {
+ const fullStars = Math.floor(rating);
+ const hasHalf = rating % 1 >= 0.5;
+ const emptyStars = 5 - fullStars - (hasHalf ? 1 : 0);
+
+ return (
+ <div className='flex items-center gap-1'>
+ {Array.from({ length: fullStars }).map((_, i) => (
+ <Icons.starFilled
+ key={`full-${i}`}
+ className='h-4 w-4 text-(--overview-star)'
+ aria-hidden='true'
+ />
+ ))}
+ {hasHalf && (
+ <Icons.starFilled
+ className='text-(--overview-star) h-4 w-4 opacity-50'
+ aria-hidden='true'
+ />
+ )}
+ {Array.from({ length: emptyStars }).map((_, i) => (
+ <Icons.exclusive
+ key={`empty-${i}`}
+ className='text-muted-foreground/40 h-4 w-4'
+ aria-hidden='true'
+ />
+ ))}
+ <span className='text-muted-foreground ml-1 text-sm font-medium'>{rating}</span>
+ </div>
+ );
+}
+
+function ReviewCard({ review }: { review: Review }) {
+ return (
+ <div className='bg-card border-border relative flex h-full min-h-[260px] flex-col justify-between overflow-hidden rounded-2xl border p-6 shadow-sm'>
+ {/* Profile */}
+ <div className='flex items-center gap-3'>
+ <img
+ src={review.avatar}
+ alt={review.name}
+ width={48}
+ height={48}
+ className='h-12 w-12 rounded-full object-cover'
+ />
+ <div>
+ <p className='text-foreground text-base font-semibold leading-tight'>{review.name}</p>
+ <p className='text-muted-foreground text-sm'>{review.timeAgo}</p>
+ </div>
+ </div>
+
+ {/* Review text */}
+ <p className='text-foreground/80 mt-4 line-clamp-4 text-sm leading-relaxed'>{review.text}</p>
+
+ {/* Rating */}
+ <div className='mt-4'>
+ <StarRating rating={review.rating} />
+ </div>
+
+ {/* Decorative product image */}
+ <div
+ className={cn(
+ 'pointer-events-none absolute -right-6 -bottom-6',
+ 'h-32 w-32 overflow-hidden rounded-full shadow-lg'
+ )}
+ aria-hidden='true'
+ >
+ <img
+ src={review.productImage}
+ alt=''
+ width={128}
+ height={128}
+ className='h-full w-full object-cover'
+ />
+ </div>
+ </div>
+ );
+}
+
+export function CustomerReviews() {
+ return (
+ <section className='mt-6'>
+ <div className='mb-4 flex items-start justify-between'>
+ <div>
+ <h2 className='text-foreground text-2xl font-semibold'>Customer Feedback</h2>
+ <p className='text-muted-foreground mt-1 text-sm'>See what users are saying about us.</p>
+ </div>
+ </div>
+ <Carousel opts={{ align: 'start', loop: false }} className='w-full'>
+ <div className='mb-4 flex justify-end gap-2'>
+ <CarouselPrevious className='static translate-y-0 rounded-full' />
+ <CarouselNext className='static translate-y-0 rounded-full' />
+ </div>
+ <CarouselContent className='-ml-4'>
+ {reviews.map((review) => (
+ <CarouselItem key={review.id} className='pl-4 md:basis-1/2 lg:basis-1/3'>
+ <ReviewCard review={review} />
+ </CarouselItem>
+ ))}
+ </CarouselContent>
+ </Carousel>
+ </section>
+ );
+}
diff --git a/src/features/overview/components/pie-graph.tsx b/src/features/overview/components/pie-graph.tsx
index b28e5ea..7bd1d0f 100644
--- a/src/features/overview/components/pie-graph.tsx
+++ b/src/features/overview/components/pie-graph.tsx
@@ -1,90 +1,83 @@
'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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Icons } from '@/components/icons';
+import React from 'react';
+import { PieChart, Pie, Cell } from 'recharts';
+
+interface DonutChartProps {
+ value: number;
+ color: string;
+ label: string;
+}
-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)' }
-];
+function DonutChart({ value, color, label }: DonutChartProps) {
+ const remainder = 100 - value;
+ const data = [
+ { name: 'value', value },
+ { name: 'rest', value: remainder }
+ ];
-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;
+ return (
+ <div className='flex flex-col items-center gap-3'>
+ <div className='relative'>
+ <PieChart width={160} height={160}>
+ <Pie
+ data={data}
+ cx={75}
+ cy={75}
+ innerRadius={52}
+ outerRadius={72}
+ startAngle={90}
+ endAngle={-270}
+ dataKey='value'
+ strokeWidth={0}
+ >
+ <Cell fill={color} />
+ <Cell fill='#f0f0f0' />
+ </Pie>
+ </PieChart>
+ <div className='pointer-events-none absolute inset-0 flex items-center justify-center'>
+ <span className='text-xl font-semibold' style={{ color }}>
+ {value}%
+ </span>
+ </div>
+ </div>
+ <span className='text-muted-foreground text-sm font-medium'>{label}</span>
+ </div>
+ );
+}
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}
+ <Card className='h-full'>
+ <CardHeader className='pb-2'>
+ <div className='flex items-center justify-between'>
+ <CardTitle className='text-lg font-bold'>Distribution Overview</CardTitle>
+ <div className='flex items-center gap-4'>
+ <label className='flex cursor-pointer items-center gap-1.5 text-sm'>
+ <input type='checkbox' className='accent-(--overview-accent) h-4 w-4 rounded' />
+ <span className='text-muted-foreground'>Chart</span>
+ </label>
+ <label className='flex cursor-pointer items-center gap-1.5 text-sm'>
+ <span className='bg-(--overview-red) inline-block h-3.5 w-3.5 rounded-xs' />
+ <span className='text-muted-foreground'>Show Value</span>
+ </label>
+ <button
+ className='text-muted-foreground hover:text-foreground transition-colors'
+ aria-label='More options'
>
- <LabelList
- dataKey='visitors'
- stroke='none'
- fontSize={12}
- fontWeight={500}
- fill='currentColor'
- formatter={(value: number) => value.toString()}
- />
- </Pie>
- </PieChart>
- </ChartContainer>
+ <Icons.ellipsis className='h-5 w-5' />
+ </button>
+ </div>
+ </div>
+ </CardHeader>
+ <CardContent>
+ <div className='flex items-center justify-around py-4'>
+ <DonutChart value={81} color='#ff5b5b' label='Total Users' />
+ <DonutChart value={22} color='#00b074' label='User Growth' />
+ <DonutChart value={62} color='#2d9cdb' label='Revenue Share' />
+ </div>
</CardContent>
</Card>
);
diff --git a/src/features/overview/components/recent-sales.tsx b/src/features/overview/components/recent-sales.tsx
index 7ceff1f..4d8c257 100644
--- a/src/features/overview/components/recent-sales.tsx
+++ b/src/features/overview/components/recent-sales.tsx
@@ -1,64 +1,136 @@
-import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
-import { Card, CardHeader, CardContent, CardTitle, CardDescription } from '@/components/ui/card';
+'use client';
-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'
+import { Bar, BarChart, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { ChartConfig, ChartContainer } from '@/components/ui/chart';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue
+} from '@/components/ui/select';
+import { Icons } from '@/components/icons';
+import React, { useState } from 'react';
+
+const weeklyData = [
+ { day: 'Sun', enterprise: 60, growth: 25, starter: 30 },
+ { day: 'Mon', enterprise: 80, growth: 45, starter: 15 },
+ { day: 'Tue', enterprise: 40, growth: 55, starter: 20 },
+ { day: 'Wed', enterprise: 65, growth: 30, starter: 40 },
+ { day: 'Thu', enterprise: 35, growth: 60, starter: 15 },
+ { day: 'Fri', enterprise: 20, growth: 10, starter: 45 },
+ { day: 'Sat', enterprise: 50, growth: 25, starter: 20 }
+];
+
+const monthlyData = [
+ { day: 'Jan', enterprise: 240, growth: 180, starter: 120 },
+ { day: 'Feb', enterprise: 310, growth: 220, starter: 160 },
+ { day: 'Mar', enterprise: 280, growth: 195, starter: 145 },
+ { day: 'Apr', enterprise: 350, growth: 260, starter: 200 }
+];
+
+const dailyData = [
+ { day: '6am', enterprise: 12, growth: 8, starter: 5 },
+ { day: '9am', enterprise: 35, growth: 28, starter: 18 },
+ { day: '12pm', enterprise: 55, growth: 42, starter: 30 },
+ { day: '3pm', enterprise: 48, growth: 35, starter: 22 },
+ { day: '6pm', enterprise: 30, growth: 22, starter: 14 },
+ { day: '9pm', enterprise: 15, growth: 10, starter: 7 }
+];
+
+const chartConfig = {
+ enterprise: {
+ label: 'Enterprise',
+ color: '#ff5b5b'
},
- {
- name: 'William Kim',
- email: 'will@email.com',
- avatar: 'https://api.slingacademy.com/public/sample-users/4.png',
- fallback: 'WK',
- amount: '+$99.00'
+ growth: {
+ label: 'Growth',
+ color: '#f7c604'
},
- {
- name: 'Sofia Davis',
- email: 'sofia.davis@email.com',
- avatar: 'https://api.slingacademy.com/public/sample-users/5.png',
- fallback: 'SD',
- amount: '+$39.00'
+ starter: {
+ label: 'Starter',
+ color: '#00b074'
}
-];
+} satisfies ChartConfig;
+
+type FilterType = 'weekly' | 'monthly' | 'daily';
+
+const dataMap: Record<FilterType, typeof weeklyData> = {
+ weekly: weeklyData,
+ monthly: monthlyData,
+ daily: dailyData
+};
export function RecentSales() {
+ const [filter, setFilter] = useState<FilterType>('weekly');
+ const data = dataMap[filter];
+
return (
<Card className='h-full'>
- <CardHeader>
- <CardTitle>Recent Sales</CardTitle>
- <CardDescription>You made 265 sales this month.</CardDescription>
+ <CardHeader className='pb-2'>
+ <div className='flex items-center justify-between'>
+ <CardTitle className='text-lg font-bold'>User Distribution</CardTitle>
+ <div className='flex items-center gap-2'>
+ <Select value={filter} onValueChange={(v) => setFilter(v as FilterType)}>
+ <SelectTrigger className='h-8 w-28 text-sm'>
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value='weekly'>Weekly</SelectItem>
+ <SelectItem value='monthly'>Monthly</SelectItem>
+ <SelectItem value='daily'>Daily</SelectItem>
+ </SelectContent>
+ </Select>
+ <button
+ className='text-muted-foreground hover:text-foreground transition-colors'
+ aria-label='More options'
+ >
+ <Icons.ellipsis className='h-5 w-5' />
+ </button>
+ </div>
+ </div>
</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>
+ <ChartContainer config={chartConfig} className='h-[280px] w-full'>
+ <BarChart
+ data={data}
+ barGap={3}
+ barCategoryGap='25%'
+ margin={{ top: 8, right: 4, left: -16, bottom: 0 }}
+ >
+ <CartesianGrid vertical={false} stroke='var(--border)' strokeOpacity={0.5} />
+ <XAxis
+ dataKey='day'
+ tickLine={false}
+ axisLine={false}
+ tickMargin={8}
+ tick={{ fill: 'var(--muted-foreground)', fontSize: 12 }}
+ />
+ <YAxis
+ tickLine={false}
+ axisLine={false}
+ tick={{ fill: 'var(--muted-foreground)', fontSize: 12 }}
+ />
+ <Tooltip
+ cursor={{ fill: 'var(--muted)', opacity: 0.4 }}
+ contentStyle={{
+ background: 'var(--background)',
+ border: '1px solid var(--border)',
+ borderRadius: '12px',
+ fontSize: '13px'
+ }}
+ />
+ <Bar dataKey='enterprise' fill='#ff5b5b' radius={[4, 4, 0, 0]} maxBarSize={18} />
+ <Bar dataKey='growth' fill='#f7c604' radius={[4, 4, 0, 0]} maxBarSize={18} />
+ <Bar dataKey='starter' fill='#00b074' radius={[4, 4, 0, 0]} maxBarSize={18} />
+ </BarChart>
+ </ChartContainer>
+ <div className='mt-3 flex items-center justify-center gap-4'>
+ {Object.entries(chartConfig).map(([key, config]) => (
+ <div key={key} className='flex items-center gap-1.5'>
+ <span className='h-2.5 w-2.5 rounded-full' style={{ background: config.color }} />
+ <span className='text-muted-foreground text-xs'>{config.label}</span>
</div>
))}
</div>
diff --git a/src/styles/globals.css b/src/styles/globals.css
index 443a990..7a12b33 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -4,6 +4,23 @@
@custom-variant dark (&:is(.dark *));
+/* Overview dashboard accent colors - additive, do not replace theme tokens */
+:root {
+ --overview-accent: #00b074;
+ --overview-accent-muted: #e6f7f2;
+ --overview-blue: #2d9cdb;
+ --overview-red: #ff5b5b;
+ --overview-star: #f7c604;
+}
+
+@theme inline {
+ --color-overview-accent: var(--overview-accent);
+ --color-overview-accent-muted: var(--overview-accent-muted);
+ --color-overview-blue: var(--overview-blue);
+ --color-overview-red: var(--overview-red);
+ --color-overview-star: var(--overview-star);
+}
+
@import './theme.css';
@layer base {
--
2.53.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment