diff --git a/src/app/(dashboard)/leads/[id]/page.tsx b/src/app/(dashboard)/leads/[id]/page.tsx index 7530b5e..9136b4c 100644 --- a/src/app/(dashboard)/leads/[id]/page.tsx +++ b/src/app/(dashboard)/leads/[id]/page.tsx @@ -2,6 +2,7 @@ import Link from "next/link" import { motion } from "framer-motion" +import { use } from "react" import { PageHeader } from "@/components/shared/page-header" import { LeadDetailsCard } from "@/components/leads/lead-details-card" import { LeadStatusBadge } from "@/components/leads/lead-status-badge" @@ -19,8 +20,9 @@ import { getLeadById } from "@/data/leads" import { getNotesByLeadId } from "@/data/notes" import { ArrowLeft, Edit, ExternalLink } from "lucide-react" -export default function LeadDetailsPage({ params }: { params: { id: string } }) { - const lead = getLeadById(params.id) +export default function LeadDetailsPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params) + const lead = getLeadById(id) const leadNotes = lead ? getNotesByLeadId(lead.id) : [] if (!lead) { diff --git a/src/app/globals.css b/src/app/globals.css index b0b378f..b66e82b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,74 +2,72 @@ @tailwind components; @tailwind utilities; -@layer base { - :root { - --background: 210 40% 98%; - --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 221.2 83.2% 53.3%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 221.2 83.2% 53.3%; - --sidebar: 222.2 84% 4.9%; - --sidebar-foreground: 210 40% 98%; - --sidebar-primary: 217.2 91.2% 59.8%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 217.2 32.6% 17.5%; - --sidebar-accent-foreground: 210 40% 98%; - --sidebar-border: 217.2 32.6% 17.5%; - --sidebar-ring: 224.3 76.3% 48%; - --radius: 0.5rem; - } - - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 48%; - --sidebar: 222.2 84% 4.9%; - --sidebar-foreground: 210 40% 98%; - --sidebar-primary: 217.2 91.2% 59.8%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 217.2 32.6% 17.5%; - --sidebar-accent-foreground: 210 40% 98%; - --sidebar-border: 217.2 32.6% 17.5%; - --sidebar-ring: 224.3 76.3% 48%; - } - - * { - @apply border-border; - } - - body { - @apply bg-background text-foreground; - font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif; - } +:root { + --background: 210 40% 98%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 221.2 83.2% 53.3%; + --sidebar: 222.2 84% 4.9%; + --sidebar-foreground: 210 40% 98%; + --sidebar-primary: 217.2 91.2% 59.8%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 217.2 32.6% 17.5%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 217.2 32.6% 17.5%; + --sidebar-ring: 224.3 76.3% 48%; + --radius: 0.5rem; +} + +.dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 224.3 76.3% 48%; + --sidebar: 222.2 84% 4.9%; + --sidebar-foreground: 210 40% 98%; + --sidebar-primary: 217.2 91.2% 59.8%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 217.2 32.6% 17.5%; + --sidebar-accent-foreground: 210 40% 98%; + --sidebar-border: 217.2 32.6% 17.5%; + --sidebar-ring: 224.3 76.3% 48%; +} + +* { + @apply border-border; +} + +body { + @apply bg-background text-foreground; + font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 64a14ed..b333a78 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -25,7 +25,7 @@ export default function RootLayout({ return ( - + {children} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index ae34cc4..f9d1265 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,15 +1,18 @@ "use client" -import { useState } from "react" +import { useState, useEffect, useRef } from "react" import { useRouter } from "next/navigation" -import { motion } from "framer-motion" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Checkbox } from "@/components/ui/checkbox" import { COMPANY_NAME } from "@/lib/constants" import { Eye, EyeOff, Loader2 } from "lucide-react" +const waves = [ + { a: 16, f: 0.011, s: 0.018, y: 0.38, fill: "rgba(27,176,206,0.18)" }, + { a: 20, f: 0.008, s: 0.013, y: 0.52, fill: "rgba(27,176,206,0.25)" }, + { a: 12, f: 0.015, s: 0.025, y: 0.28, fill: "rgba(180,192,210,0.06)" }, + { a: 26, f: 0.006, s: 0.010, y: 0.65, fill: "rgba(180,192,210,0.15)" }, + { a: 10, f: 0.019, s: 0.030, y: 0.20, fill: "rgba(27,176,206,0.10)" }, +] + export default function LoginPage() { const router = useRouter() const [email, setEmail] = useState("admin@coastalit.com") @@ -17,175 +20,575 @@ export default function LoginPage() { const [showPassword, setShowPassword] = useState(false) const [loading, setLoading] = useState(false) const [remember, setRemember] = useState(false) + const [counts, setCounts] = useState({ leads: 0, conversion: 0, users: 0 }) + const counterStarted = useRef(false) + const canvasRef = useRef(null) + const starsCanvasRightRef = useRef(null) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext("2d") + if (!ctx) return + + let animId: number + let time = 0 + + const resize = () => { + const parent = canvas.parentElement! + const rect = parent.getBoundingClientRect() + const dpr = window.devicePixelRatio || 1 + canvas.width = rect.width * dpr + canvas.height = 200 * dpr + canvas.style.width = rect.width + "px" + canvas.style.height = "200px" + ctx!.setTransform(dpr, 0, 0, dpr, 0, 0) + } + + resize() + window.addEventListener("resize", resize) + + const draw = () => { + const w = canvas.width / (window.devicePixelRatio || 1) + const h = 200 + ctx!.clearRect(0, 0, w, h) + + for (const wave of waves) { + ctx!.beginPath() + ctx!.moveTo(0, h) + for (let x = 0; x <= w; x++) { + const y = wave.y * h + Math.sin(x * wave.f + time * wave.s) * wave.a + ctx!.lineTo(x, y) + } + ctx!.lineTo(w, h) + ctx!.closePath() + ctx!.fillStyle = wave.fill + ctx!.fill() + } + + time += 1 + animId = requestAnimationFrame(draw) + } + + animId = requestAnimationFrame(draw) + + return () => { + cancelAnimationFrame(animId) + window.removeEventListener("resize", resize) + } + }, []) + + useEffect(() => { + const canvases = [starsCanvasRightRef.current].filter(Boolean) as HTMLCanvasElement[] + if (canvases.length === 0) return + + const stars = Array.from({ length: 312 }, () => ({ + x: Math.random(), + y: Math.random(), + r: 0.2 + Math.random() * 0.6, + baseOpacity: 0.12 + Math.random() * 0.43, + speed: 0.003 + Math.random() * 0.015, + phase: Math.random() * Math.PI * 2, + driftX: (Math.random() - 0.5) * 0.00003, + driftY: (Math.random() - 0.5) * 0.00003, + })) + + let animId: number + let time = 0 + + const resize = () => { + for (const c of canvases) { + const dpr = window.devicePixelRatio || 1 + const rect = c.getBoundingClientRect() + c.width = rect.width * dpr + c.height = rect.height * dpr + const ctx = c.getContext("2d") + if (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0) + } + } + + resize() + const ros = canvases.map((c) => { + const ro = new ResizeObserver(() => resize()) + ro.observe(c.parentElement!) + return ro + }) + window.addEventListener("resize", resize) + + const draw = () => { + time += 1 + for (const c of canvases) { + const dpr = window.devicePixelRatio || 1 + const w = c.width / dpr + const h = c.height / dpr + const ctx = c.getContext("2d") + if (!ctx) continue + ctx.clearRect(0, 0, w, h) + + for (const star of stars) { + const x = ((star.x + time * star.driftX) % 1) * w + const y = ((star.y + time * star.driftY) % 1) * h + const opacity = star.baseOpacity * (0.5 + 0.5 * Math.sin(time * star.speed + star.phase)) + + ctx.beginPath() + ctx.arc(x, y, star.r, 0, Math.PI * 2) + ctx.fillStyle = `rgba(255,255,255,${opacity})` + ctx.fill() + + if (star.r > 0.5) { + ctx.beginPath() + ctx.arc(x, y, star.r * 1.8, 0, Math.PI * 2) + ctx.fillStyle = `rgba(255,255,255,${opacity * 0.06})` + ctx.fill() + } + } + } + + animId = requestAnimationFrame(draw) + } + + animId = requestAnimationFrame(draw) + + return () => { + cancelAnimationFrame(animId) + ros.forEach((ro) => ro.disconnect()) + window.removeEventListener("resize", resize) + } + }, []) + + useEffect(() => { + if (counterStarted.current) return + counterStarted.current = true + const targets = { leads: 1247, conversion: 40, users: 83 } + const steps = 150 + + const delayTimer = setTimeout(() => { + let step = 0 + const interval = setInterval(() => { + step++ + if (step >= steps) { + clearInterval(interval) + setCounts(targets) + return + } + setCounts({ + leads: Math.min(Math.floor((targets.leads / steps) * step), targets.leads), + conversion: Math.min(Math.floor((targets.conversion / steps) * step), targets.conversion), + users: Math.min(Math.floor((targets.users / steps) * step), targets.users), + }) + }, 15) + }, 400) + + return () => clearTimeout(delayTimer) + }, []) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setLoading(true) - await new Promise((resolve) => setTimeout(resolve, 1200)) - router.push("/dashboard") } return ( -
- {/* Left - Brand panel */} -
-
+ <> + -
-
+
+
+
{COMPANY_NAME} - {COMPANY_NAME}
-
-
- - Your Agency's Growth Starts Here - - - Manage leads, track conversions, and grow your web development business with our CRM. - +
+
+

+ Your Agency's{" "} + Growth{" "} + Starts Here +

+

+ Manage leads, track conversions, and grow your web development business with our CRM. +

+
+
+
{counts.leads.toLocaleString()}
+
leads tracked
+
+
+
{counts.conversion}%
+
conversion lift
+
+
+
{counts.users}
+
active users
+
+
+
+
- -

+

+

“This CRM transformed how we manage our sales pipeline. We've seen a 40% increase in lead conversion.”

-
-
-
-

Marcus Johnson

-

Sales Lead, Coast IT

-
-
- -
- -
- © 2026 {COMPANY_NAME}. All rights reserved. -
-
- - {/* Right - Login form */} -
- - {/* Mobile logo */} -
- {COMPANY_NAME} - {COMPANY_NAME} -
- -
-

Welcome back

-

- Sign in to your account to continue +

+ Marcus Johnson, Sales Lead at Coast IT

-
-
- - setEmail(e.target.value)} - required - autoComplete="email" - /> -
+ +
-
-
- - +
+ +
+ +
+ +
+

+ Welcome back +

+

+ Sign in to your account to continue +

+ + +
+ +
+ setEmail(e.target.value)} + required + autoComplete="email" + className="login-input" + /> +
-
- setPassword(e.target.value)} - required - autoComplete="current-password" + +
+
+ + +
+
+
+ setPassword(e.target.value)} + required + autoComplete="current-password" + className="login-input" + /> + +
+
+
+ +
-
- -
- setRemember(checked as boolean)} - /> - -
- - + + -

- By signing in, you agree to our{" "} - {" "} - and{" "} - -

- +

+ By signing in, you agree to our{" "} + {" "} + and{" "} + +

+
+
-
+ ) }