Initial commit
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
"use client"
|
||||
|
||||
import { motion } from "framer-motion"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts"
|
||||
|
||||
interface StatusData {
|
||||
name: string
|
||||
value: number
|
||||
color: string
|
||||
}
|
||||
|
||||
interface LeadStatusChartProps {
|
||||
data: StatusData[]
|
||||
}
|
||||
|
||||
export function LeadStatusChart({ data }: LeadStatusChartProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Lead Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={100}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
animationBegin={200}
|
||||
animationDuration={1000}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: "hsl(var(--popover))",
|
||||
border: "1px solid hsl(var(--border))",
|
||||
borderRadius: "8px",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
formatter={(value: number, name: string) => [value, name]}
|
||||
/>
|
||||
<Legend
|
||||
verticalAlign="bottom"
|
||||
height={36}
|
||||
formatter={(value: string) => (
|
||||
<span className="text-sm text-muted-foreground">{value}</span>
|
||||
)}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client"
|
||||
|
||||
import { motion } from "framer-motion"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
||||
|
||||
interface MonthlyData {
|
||||
month: string
|
||||
leads: number
|
||||
closed: number
|
||||
}
|
||||
|
||||
interface LeadsPerMonthChartProps {
|
||||
data: MonthlyData[]
|
||||
}
|
||||
|
||||
export function LeadsPerMonthChart({ data }: LeadsPerMonthChartProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.3 }}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Leads Per Month</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={data} barGap={4}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12, fill: "hsl(var(--muted-foreground))" }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
allowDecimals={false}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: "hsl(var(--popover))",
|
||||
border: "1px solid hsl(var(--border))",
|
||||
borderRadius: "8px",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
/>
|
||||
<Legend
|
||||
verticalAlign="bottom"
|
||||
height={36}
|
||||
formatter={(value: string) => (
|
||||
<span className="text-sm text-muted-foreground">{value}</span>
|
||||
)}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="leads"
|
||||
name="New Leads"
|
||||
fill="hsl(var(--primary))"
|
||||
radius={[4, 4, 0, 0]}
|
||||
animationBegin={300}
|
||||
animationDuration={800}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="closed"
|
||||
name="Closed"
|
||||
fill="#10b981"
|
||||
radius={[4, 4, 0, 0]}
|
||||
animationBegin={500}
|
||||
animationDuration={800}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { motion } from "framer-motion"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Lead } from "@/types"
|
||||
import { ArrowRight } from "lucide-react"
|
||||
|
||||
const statusStyles: Record<string, string> = {
|
||||
open: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20",
|
||||
contacted: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20",
|
||||
pending: "bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/20",
|
||||
closed: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20",
|
||||
ignored: "bg-zinc-500/10 text-zinc-600 dark:text-zinc-400 border-zinc-500/20",
|
||||
}
|
||||
|
||||
interface RecentLeadsTableProps {
|
||||
leads: Lead[]
|
||||
}
|
||||
|
||||
export function RecentLeadsTable({ leads }: RecentLeadsTableProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: 0.4 }}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Recent Leads</CardTitle>
|
||||
<Link
|
||||
href="/leads"
|
||||
className="flex items-center gap-1 text-sm text-primary hover:underline"
|
||||
>
|
||||
View all <ArrowRight className="h-3 w-3" />
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-left text-muted-foreground">
|
||||
<th className="pb-3 font-medium">Company</th>
|
||||
<th className="pb-3 font-medium">Contact</th>
|
||||
<th className="hidden pb-3 font-medium md:table-cell">Status</th>
|
||||
<th className="hidden pb-3 font-medium lg:table-cell">Assigned</th>
|
||||
<th className="hidden pb-3 font-medium sm:table-cell">Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{leads.map((lead, i) => (
|
||||
<motion.tr
|
||||
key={lead.id}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.03 }}
|
||||
className="border-b last:border-0 hover:bg-muted/30"
|
||||
>
|
||||
<td className="py-3">
|
||||
<Link href={`/leads/${lead.id}`} className="font-medium hover:text-primary">
|
||||
{lead.companyName}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="py-3 text-muted-foreground">{lead.contactName}</td>
|
||||
<td className="hidden py-3 md:table-cell">
|
||||
<Badge variant="outline" className={statusStyles[lead.status]}>
|
||||
{lead.status.charAt(0).toUpperCase() + lead.status.slice(1)}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="hidden py-3 lg:table-cell">
|
||||
{lead.assignedUser ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={lead.assignedUser.avatar} />
|
||||
<AvatarFallback>{lead.assignedUser.name[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-muted-foreground">{lead.assignedUser.name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-muted-foreground/50">Unassigned</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="hidden py-3 text-muted-foreground sm:table-cell">
|
||||
{new Date(lead.createdAt).toLocaleDateString()}
|
||||
</td>
|
||||
</motion.tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
|
||||
export function StatCardSkeleton() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-8 w-16" />
|
||||
</div>
|
||||
<Skeleton className="h-12 w-12 rounded-xl" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import { motion } from "framer-motion"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { LucideIcon } from "lucide-react"
|
||||
|
||||
interface StatCardProps {
|
||||
title: string
|
||||
value: string | number
|
||||
icon: LucideIcon
|
||||
variant?: "default" | "blue" | "amber" | "purple" | "emerald" | "zinc"
|
||||
description?: string
|
||||
index?: number
|
||||
}
|
||||
|
||||
const variantStyles = {
|
||||
default: { bg: "bg-muted", text: "text-foreground" },
|
||||
blue: { bg: "bg-blue-500/10", text: "text-blue-600 dark:text-blue-400" },
|
||||
amber: { bg: "bg-amber-500/10", text: "text-amber-600 dark:text-amber-400" },
|
||||
purple: { bg: "bg-purple-500/10", text: "text-purple-600 dark:text-purple-400" },
|
||||
emerald: { bg: "bg-emerald-500/10", text: "text-emerald-600 dark:text-emerald-400" },
|
||||
zinc: { bg: "bg-zinc-500/10", text: "text-zinc-600 dark:text-zinc-400" },
|
||||
}
|
||||
|
||||
export function StatCard({ title, value, icon: Icon, variant = "default", description, index = 0 }: StatCardProps) {
|
||||
const style = variantStyles[variant]
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
>
|
||||
<Card className="group hover:shadow-md transition-all duration-200">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-muted-foreground">{title}</p>
|
||||
<p className="text-3xl font-bold tracking-tight">{value}</p>
|
||||
{description && (
|
||||
<p className="text-xs text-muted-foreground">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn("flex h-12 w-12 items-center justify-center rounded-xl", style.bg)}>
|
||||
<Icon className={cn("h-6 w-6", style.text)} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user