added update from code pull new changes now
This commit is contained in:
+191
-6
@@ -2,24 +2,209 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useRef } from "react"
|
import { useState, useEffect, useRef } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
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 { COMPANY_NAME } from "@/lib/constants"
|
||||||
import { Eye, EyeOff, Loader2 } from "lucide-react"
|
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 router = useRouter()
|
||||||
const [email, setEmail] = useState("")
|
const [email, setEmail] = useState("")
|
||||||
const [password, setPassword] = useState("")
|
const [password, setPassword] = useState("")
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [remember, setRemember] = 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<HTMLCanvasElement>(null)
|
||||||
|
const starsCanvasRightRef = useRef<HTMLCanvasElement>(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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
setError("")
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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 {
|
return {
|
||||||
totalLeads,
|
totalLeads,
|
||||||
openLeads,
|
openLeads,
|
||||||
@@ -46,9 +80,11 @@ export function getDashboardStats(period: string): DashboardStats {
|
|||||||
closedLeads,
|
closedLeads,
|
||||||
ignoredLeads,
|
ignoredLeads,
|
||||||
conversionRate,
|
conversionRate,
|
||||||
|
monthlyBreakdown,
|
||||||
leadsPerMonth: groupLeadsByInterval(filtered, period),
|
leadsPerMonth: groupLeadsByInterval(filtered, period),
|
||||||
recentLeads: filtered.slice(0, 10),
|
recentLeads: filtered.slice(0, 10),
|
||||||
statusDistribution: getStatusDistribution(),
|
statusDistribution: getStatusDistribution(),
|
||||||
periodLabel: label,
|
periodLabel: label,
|
||||||
|
trends,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,10 +51,20 @@ export interface DashboardStats {
|
|||||||
closedLeads: number;
|
closedLeads: number;
|
||||||
ignoredLeads: number;
|
ignoredLeads: number;
|
||||||
conversionRate: 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 }[];
|
leadsPerMonth: { label: string; leads: number; closed: number }[];
|
||||||
recentLeads: Lead[];
|
recentLeads: Lead[];
|
||||||
statusDistribution: { name: string; value: number; color: string }[];
|
statusDistribution: { name: string; value: number; color: string }[];
|
||||||
periodLabel: 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 {
|
export interface ColumnFilter {
|
||||||
|
|||||||
Reference in New Issue
Block a user