Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client"

import Link from "next/link"
import { useParams } from "next/navigation"
import { ChevronLeft } from "lucide-react"

import type { LocaleType } from "@/types"
import type { CustomerType } from "../../types"

import { ensureLocalizedPathname } from "@/lib/i18n"

import { Button } from "@/components/ui/button"
import { CustomerHeaderCard } from "./customer-header-card"
import { CustomerInfoCard } from "./customer-info-card"
import { CustomerOrdersCard } from "./customer-orders-card"
import { CustomerStatsCard } from "./customer-stats-card"

interface CustomerDetailsProps {
customer: CustomerType
}

export function CustomerDetails({ customer }: CustomerDetailsProps) {
const params = useParams()
const locale = params.lang as LocaleType

return (
<div className="space-y-4">
<Button variant="ghost" size="sm" asChild>
<Link href={ensureLocalizedPathname("/pages/customers/list", locale)}>
<ChevronLeft className="me-1 h-4 w-4" />
Back to Customers
</Link>
</Button>

<div className="grid gap-4 md:grid-cols-3">
<div className="space-y-4">
<CustomerHeaderCard customer={customer} />
<CustomerInfoCard customer={customer} />
</div>
<div className="space-y-4 md:col-span-2">
<CustomerStatsCard customer={customer} />
<CustomerOrdersCard customer={customer} />
</div>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { CustomerType } from "../../types"

import { formatDate, getInitials } from "@/lib/utils"

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent } from "@/components/ui/card"

interface CustomerHeaderCardProps {
customer: CustomerType
}

export function CustomerHeaderCard({ customer }: CustomerHeaderCardProps) {
const statusVariant =
customer.status === "VIP"
? "secondary"
: customer.status === "Inactive"
? "outline"
: "default"

return (
<Card>
<CardContent className="flex flex-col items-center gap-3 pt-6 text-center">
<Avatar className="h-20 w-20">
<AvatarImage src={customer.avatar} alt={customer.name} />
<AvatarFallback className="text-lg">
{getInitials(customer.name)}
</AvatarFallback>
</Avatar>
<div>
<h2 className="text-lg font-semibold">{customer.name}</h2>
<p className="text-sm text-muted-foreground">{customer.email}</p>
</div>
<div className="flex items-center gap-2">
<Badge variant={statusVariant}>{customer.status}</Badge>
<span className="text-xs text-muted-foreground">
Joined {formatDate(customer.joinDate)}
</span>
</div>
</CardContent>
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Building2, Globe, MapPin, Phone } from "lucide-react"

import type { CustomerType } from "../../types"

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"

interface CustomerInfoCardProps {
customer: CustomerType
}

export function CustomerInfoCard({ customer }: CustomerInfoCardProps) {
const items = [
{ icon: Phone, label: "Phone", value: customer.phone },
{
icon: MapPin,
label: "Address",
value: `${customer.address}, ${customer.city}, ${customer.state} ${customer.zipCode}`,
},
{ icon: Globe, label: "Country", value: customer.country },
{ icon: Building2, label: "Organization", value: customer.organization },
]

return (
<Card>
<CardHeader>
<CardTitle>Information</CardTitle>
</CardHeader>
<CardContent className="grid gap-3">
{items.map((item) => (
<div key={item.label} className="flex items-start gap-3">
<item.icon className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
<div>
<p className="text-xs text-muted-foreground">{item.label}</p>
<p className="text-sm">{item.value}</p>
</div>
</div>
))}
</CardContent>
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client"

import Link from "next/link"
import { useParams } from "next/navigation"
import { UserX } from "lucide-react"

import type { LocaleType } from "@/types"

import { ensureLocalizedPathname } from "@/lib/i18n"

import { Button } from "@/components/ui/button"

export function CustomerNotFound() {
const params = useParams()
const locale = params.lang as LocaleType

return (
<div className="flex flex-col items-center justify-center gap-4 p-16 text-center">
<UserX className="h-16 w-16 text-muted-foreground" />
<h2 className="text-xl font-semibold">Customer Not Found</h2>
<p className="text-sm text-muted-foreground">
The customer you are looking for does not exist or has been removed.
</p>
<Button asChild>
<Link href={ensureLocalizedPathname("/pages/customers/list", locale)}>
Back to Customers
</Link>
</Button>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { CustomerType } from "../../types"

import { formatCurrency, formatDate } from "@/lib/utils"

import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"

interface CustomerOrdersCardProps {
customer: CustomerType
}

export function CustomerOrdersCard({ customer }: CustomerOrdersCardProps) {
const { recentOrders } = customer

const paymentVariant = (status: string) =>
status === "Paid"
? "default"
: status === "Pending"
? "secondary"
: status === "Failed"
? "destructive"
: "outline"

return (
<Card>
<CardHeader>
<CardTitle>Recent Orders</CardTitle>
</CardHeader>
<CardContent className="p-0">
<ScrollArea
orientation="horizontal"
className="w-[calc(100vw-2.25rem)] md:w-auto"
>
<Table>
<TableHeader>
<TableRow>
<TableHead>Order ID</TableHead>
<TableHead>Date</TableHead>
<TableHead className="text-end">Amount</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{recentOrders.length > 0 ? (
recentOrders.map((order) => (
<TableRow key={order.orderId}>
<TableCell className="text-primary font-medium">
#{order.orderId}
</TableCell>
<TableCell>{formatDate(order.date)}</TableCell>
<TableCell className="text-end">
{formatCurrency(order.amount)}
</TableCell>
<TableCell>
<Badge variant={paymentVariant(order.status)}>
{order.status}
</Badge>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={4} className="h-16 text-center">
No recent orders.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</ScrollArea>
</CardContent>
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { DollarSign, ShoppingCart, TrendingUp } from "lucide-react"

import type { CustomerType } from "../../types"

import { formatCurrency } from "@/lib/utils"

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"

interface CustomerStatsCardProps {
customer: CustomerType
}

export function CustomerStatsCard({ customer }: CustomerStatsCardProps) {
const avgOrderValue =
customer.totalOrders > 0
? Math.round(customer.totalSpent / customer.totalOrders)
: 0

const stats = [
{
icon: ShoppingCart,
label: "Total Orders",
value: customer.totalOrders.toString(),
},
{
icon: DollarSign,
label: "Total Spent",
value: formatCurrency(customer.totalSpent),
},
{
icon: TrendingUp,
label: "Avg. Order Value",
value: formatCurrency(avgOrderValue),
},
]

return (
<div className="grid grid-cols-3 gap-4">
{stats.map((stat) => (
<Card key={stat.label}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{stat.label}
</CardTitle>
<stat.icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">{stat.value}</p>
</CardContent>
</Card>
))}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { LocaleType } from "@/types"
import type { Metadata } from "next"

import { customersData } from "../list/_data/customers"

import { CustomerDetails } from "./_components/customer-details"
import { CustomerNotFound } from "./_components/customer-not-found"

export const metadata: Metadata = {
title: "Customer Details",
}

export default async function CustomerDetailsPage(props: {
params: Promise<{ lang: LocaleType; id: string }>
}) {
const params = await props.params

const customer = customersData.find((c) => c.id === params.id)

if (!customer) {
return (
<section className="container p-4">
<CustomerNotFound />
</section>
)
}

return (
<section className="container p-4">
<CustomerDetails customer={customer} />
</section>
)
}
Loading