Initial commit

This commit is contained in:
2026-06-17 13:51:22 +02:00
commit 4898bf7142
81 changed files with 12522 additions and 0 deletions
@@ -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>
)
}
+53
View File
@@ -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>
)
}