Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
sadmann7 committed Jan 27, 2024
1 parent 228b617 commit 8a3a04c
Show file tree
Hide file tree
Showing 9 changed files with 466 additions and 104 deletions.
6 changes: 2 additions & 4 deletions src/app/(dashboard)/dashboard/account/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { Metadata } from "next"
import { env } from "@/env.mjs"

import { UserProfile } from "@/components/auth/user-profile"
import {
PageHeader,
PageHeaderDescription,
PageHeaderHeading,
} from "@/components/page-header"
import { Shell } from "@/components/shells/shell"
import {dark} from "@clerk/themes";

export const metadata: Metadata = {
metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
Expand All @@ -33,9 +33,7 @@ export default function AccountPage() {
aria-labelledby="user-account-info-heading"
className="w-full overflow-hidden"
>
<UserProfile appearance={{
baseTheme: dark,
}}/>
<UserProfile />
</section>
</Shell>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
import { Icons } from "@/components/icons"

interface OverviewCardProps {
title: string
value: string
description: string
icon: keyof typeof Icons
}

export function OverviewCard({
title,
value,
description,
icon,
}: OverviewCardProps) {
const Icon = Icons[icon]

return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
<Icon className="size-4 text-muted-foreground" aria-hidden="true" />
</CardHeader>
<CardContent className="space-y-1">
<div className="text-2xl font-bold">{value}</div>
{description && (
<p className="text-xs text-muted-foreground">{description}</p>
)}
</CardContent>
</Card>
)
}

export function OverviewCardSkeleton() {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<Skeleton className="h-4 w-20" />
<Skeleton className="size-4" />
</CardHeader>
<CardContent className="space-y-2">
<Skeleton className="h-6 w-12" />
<Skeleton className="h-4 w-40" />
</CardContent>
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client"

import { LineChart, type LineChartProps } from "@tremor/react"

import { cn, formatPrice } from "@/lib/utils"

interface SalesChartProps
extends Omit<LineChartProps, "data" | "index" | "categories"> {
data: {
name: string
Total: number
}[]
}

export function SalesChart({ data, className, ...props }: SalesChartProps) {
return (
<LineChart
className={cn(className)}
data={data}
index="name"
categories={["Total"]}
colors={["indigo"]}
valueFormatter={(value) => formatPrice(value)}
yAxisWidth={48}
{...props}
/>
)
}
225 changes: 146 additions & 79 deletions src/app/(dashboard)/dashboard/stores/[storeId]/analytics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,38 @@ import { db } from "@/db"
import { stores } from "@/db/schema"
import { env } from "@/env.mjs"
import type { SearchParams } from "@/types"
import { format } from "date-fns"
import { eq } from "drizzle-orm"
import { z } from "zod"

import { getCustomers, getSalesCount } from "@/lib/fetchers/order"
import { formatNumber, formatPrice } from "@/lib/utils"
import {
getCustomers,
getOrderCount,
getSaleCount,
getSales,
} from "@/lib/fetchers/order"
import { cn, formatNumber, formatPrice } from "@/lib/utils"
import { searchParamsSchema } from "@/lib/validations/params"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
import { DateRangePicker } from "@/components/date-range-picker"
import { Icons } from "@/components/icons"

import { OverviewCard } from "./_components/overview-card"
import { SalesChart } from "./_components/sales-chart"

export const metadata: Metadata = {
metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
Expand All @@ -39,8 +56,11 @@ export default async function AnalyticsPage({
}: AnalyticsPageProps) {
const storeId = Number(params.storeId)

const { from, to } = searchParamsSchema
.pick({ from: true, to: true })
const { page, per_page, from, to } = searchParamsSchema
.omit({ per_page: true, sort: true })
.extend({
per_page: z.coerce.number().default(5),
})
.parse(searchParams)

const fromDay = from ? new Date(from) : undefined
Expand All @@ -65,107 +85,154 @@ export default async function AnalyticsPage({
notFound()
}

const salesCountPromise = getSalesCount({
const orderCountPromise = getOrderCount({
storeId,
fromDay: fromDay,
toDay: toDay,
})

const saleCountPromise = getSaleCount({
storeId,
fromDay: fromDay,
toDay: toDay,
})

const salesPromise = getSales({
storeId,
fromDay: fromDay,
toDay: toDay,
})

const customersPromise = getCustomers({
storeId,
limit: per_page ?? 5,
offset: (page - 1) * per_page,
fromDay: fromDay,
toDay: toDay,
})

const [sales, customers] = await Promise.all([
salesCountPromise,
customersPromise,
])
const [saleCount, orderCount, sales, { customers, customerCount }] =
await Promise.all([
saleCountPromise,
orderCountPromise,
salesPromise,
customersPromise,
])

return (
<div className="space-y-6 p-1">
<div className="flex flex-col gap-4 xs:flex-row xs:items-center xs:justify-between">
<h2 className="text-2xl font-bold tracking-tight">Analytics</h2>
<DateRangePicker align="end" dayCount={30} />
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
<Icons.dollarSign
className="size-4 text-muted-foreground"
aria-hidden="true"
/>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<OverviewCard
title="Total Revenue"
value={formatPrice(saleCount, {
notation: "standard",
})}
description="Total revenue for your store"
icon="dollarSign"
/>
<OverviewCard
title="Sales"
value={formatPrice(saleCount, {
notation: "standard",
})}
description="Total sales for your store"
icon="credit"
/>
<OverviewCard
title="Orders"
value={formatNumber(orderCount)}
description="Total orders for your store"
icon="cart"
/>
<OverviewCard
title="Customers"
value={formatNumber(customerCount)}
description="Total customers for your store"
icon="activity"
/>
</div>
<div className="flex flex-col gap-4 2xl:flex-row">
<Card className="flex-1">
<CardHeader>
<CardTitle>Sales</CardTitle>
<CardDescription>
Total sales in the last {dayCount} days
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatPrice(sales, {
notation: "standard",
})}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Sales</CardTitle>
<Icons.credit
className="size-4 text-muted-foreground"
aria-hidden="true"
<SalesChart
data={sales.map((sale) => ({
name: format(new Date(sale.year, sale.month - 1), "MMM"),
Total: sale.totalSales,
}))}
/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatPrice(sales, {
notation: "standard",
})}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Customers</CardTitle>
<Icons.activity
className="size-4 text-muted-foreground"
aria-hidden="true"
/>
<CardHeader>
<CardTitle>Customers</CardTitle>
<CardDescription>
Customers who have purchased in the last {dayCount} days
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{customers.length}</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Recent Customers</CardTitle>
<CardDescription>
{customers.length} customers{" "}
{dayCount && `in the last ${dayCount} days`}
</CardDescription>
</CardHeader>
<CardContent className="space-y-8">
{customers.map((customer) => (
<div key={customer.email} className="flex items-center">
<Avatar className="size-9">
<AvatarImage src="/avatars/01.png" alt="Avatar" />
<AvatarFallback>
{customer.name?.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="ml-4 space-y-1">
<CardContent className="space-y-8">
{customers.map((customer) => (
<div
key={customer.email}
className="flex flex-col gap-2 sm:flex-row sm:items-center"
>
<div className="flex flex-1 flex-col gap-4 sm:flex-row sm:items-center">
<Avatar className="size-9">
<AvatarFallback>
{customer.name?.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="w-full space-y-1 text-sm">
<p className="font-medium leading-none">{customer.name}</p>
<p className="break-all leading-none text-muted-foreground">
{customer.email}
</p>
</div>
</div>
<p className="text-sm font-medium leading-none">
{customer.name}
{formatPrice(customer.totalSpent)}
</p>
<p className="text-sm text-muted-foreground">
{customer.email}
</p>
</div>
<div className="ml-auto font-medium">
+${formatNumber(customer.totalSpent)}
</div>
</div>
))}
</CardContent>
</Card>
))}
</CardContent>
<CardFooter>
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href={`?page=${page - 1}&per_page=${per_page}&from=${from}&to=${to}`}
scroll={false}
className={cn(
"transition-opacity",
page === 1 && "pointer-events-none opacity-50"
)}
/>
</PaginationItem>
<PaginationItem>
<PaginationNext
href={`?page=${page + 1}&per_page=${per_page}&from=${from}&to=${to}`}
scroll={false}
className={cn(
"transition-opacity",
customers.length < per_page &&
"pointer-events-none opacity-50"
)}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</CardFooter>
</Card>
</div>
</div>
)
}
Loading

0 comments on commit 8a3a04c

Please sign in to comment.