diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 6bf887e..6163e9d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -2,24 +2,209 @@ 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" -function LoginForm() { +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("") const [password, setPassword] = useState("") const [showPassword, setShowPassword] = useState(false) const [loading, setLoading] = useState(false) const [remember, setRemember] = useState(false) + const [error, setError] = useState("") + const [counts, setCounts] = useState({ leads: 0, conversion: 0, users: 0 }) + const counterStarted = useRef(false) + const [testimonialIndex, setTestimonialIndex] = useState(0) + + const testimonials = [ + { + text: "This CRM transformed how we manage our sales pipeline. We've seen a 40% increase in lead conversion.", + author: "Marcus Johnson, Sales Lead at Coast IT", + }, + { + text: "When you're not sure, flip a coin, because when the coin is in the air, you realize which option you're actually hoping for.", + author: "Dillen van der Merwe, Madman of Coast IT", + }, + ] + + useEffect(() => { + const timer = setInterval(() => { + setTestimonialIndex((prev) => (prev + 1) % testimonials.length) + }, 5000) + return () => clearInterval(timer) + }, [testimonials.length]) + 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() + setError("") setLoading(true) try { diff --git a/src/data/dashboard.ts b/src/data/dashboard.ts index 55e0c32..33436d2 100644 --- a/src/data/dashboard.ts +++ b/src/data/dashboard.ts @@ -38,6 +38,40 @@ export function getDashboardStats(period: string): DashboardStats { ] } + const monthlyBreakdown = groupLeadsByInterval(filtered, period).map(m => ({ + label: m.label, + total: m.leads, + open: 0, + contacted: 0, + pending: 0, + closed: m.closed, + ignored: 0, + })) + + const trendVal = (current: number, prev: number) => ({ + pct: prev > 0 ? Math.round(((current - prev) / prev) * 100) : current > 0 ? 100 : 0, + up: current >= prev, + }) + + const prevFiltered = filterLeadsByPeriod(leads, period === "7days" ? "30days" : "7days") + const prevTotal = prevFiltered.length + const prevOpen = prevFiltered.filter((l) => l.status === "open").length + const prevContacted = prevFiltered.filter((l) => l.status === "contacted").length + const prevPending = prevFiltered.filter((l) => l.status === "pending").length + const prevClosed = prevFiltered.filter((l) => l.status === "closed").length + const prevIgnored = prevFiltered.filter((l) => l.status === "ignored").length + const prevConversion = prevTotal > 0 ? Math.round((prevClosed / prevTotal) * 100) : 0 + + const trends = { + totalLeads: trendVal(totalLeads, prevTotal), + openLeads: trendVal(openLeads, prevOpen), + contactedLeads: trendVal(contactedLeads, prevContacted), + pendingLeads: trendVal(pendingLeads, prevPending), + closedLeads: trendVal(closedLeads, prevClosed), + ignoredLeads: trendVal(ignoredLeads, prevIgnored), + conversionRate: trendVal(conversionRate, prevConversion), + } + return { totalLeads, openLeads, @@ -46,9 +80,11 @@ export function getDashboardStats(period: string): DashboardStats { closedLeads, ignoredLeads, conversionRate, + monthlyBreakdown, leadsPerMonth: groupLeadsByInterval(filtered, period), recentLeads: filtered.slice(0, 10), statusDistribution: getStatusDistribution(), periodLabel: label, + trends, } } diff --git a/src/types/index.ts b/src/types/index.ts index 42096d1..072f2e6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -51,10 +51,20 @@ export interface DashboardStats { closedLeads: number; ignoredLeads: number; conversionRate: number; + monthlyBreakdown: { label: string; total: number; open: number; contacted: number; pending: number; closed: number; ignored: number }[]; leadsPerMonth: { label: string; leads: number; closed: number }[]; recentLeads: Lead[]; statusDistribution: { name: string; value: number; color: string }[]; periodLabel: string; + trends: { + totalLeads: { pct: number; up: boolean }; + openLeads: { pct: number; up: boolean }; + contactedLeads: { pct: number; up: boolean }; + pendingLeads: { pct: number; up: boolean }; + closedLeads: { pct: number; up: boolean }; + ignoredLeads: { pct: number; up: boolean }; + conversionRate: { pct: number; up: boolean }; + }; } export interface ColumnFilter {