adbcc4b9af
feat: implement conversations API with message retrieval and posting feat: add avatar URL handling for users and update user role definitions feat: create chat system tables in the database with initial seed data fix: update user roles from "sales_user" to "sales" for consistency chore: add middleware for system API route access fixed fuckups on john's side, added a benchmark Made Graphs actualy load from database and realtime data, graphs will update and percentages will be mathematically made, and made realtime added chatcart edits, made graphs glow. added in some little small home feeling fuctions added in a slide show, in login with qoutes from creators because i want to leave a print on it saying we made this shit, we built it brick for brick login page added qoutes some random, and made them shuffle from left to right one dissapears other come in adding shape to the textbox for the qoutes Fixed my chat fuckups when it comes to chats
401 lines
14 KiB
TypeScript
401 lines
14 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect, useRef } from "react"
|
|
import { useRouter } from "next/navigation"
|
|
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("")
|
|
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<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) => {
|
|
e.preventDefault()
|
|
setError("")
|
|
setLoading(true)
|
|
|
|
try {
|
|
const res = await fetch("/api/auth/login", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email, password }),
|
|
})
|
|
|
|
if (res.ok) {
|
|
router.push("/dashboard")
|
|
} else {
|
|
const data = await res.json().catch(() => ({}))
|
|
setError(data.error || "Invalid email or password.")
|
|
}
|
|
} catch {
|
|
setError("Connection error. Please try again.")
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex min-h-screen bg-[#0a0a0f]">
|
|
<div className="left-panel flex flex-1 flex-col p-12">
|
|
<div className="relative z-10">
|
|
<img
|
|
src="/logo/CompanyLogo.png"
|
|
alt={COMPANY_NAME}
|
|
className="h-44 w-auto object-contain"
|
|
/>
|
|
</div>
|
|
|
|
<div className="relative z-10 flex-1 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<h1 className="text-[34px] font-extrabold text-[#e8e8ef] leading-tight tracking-[-0.6px]">
|
|
Your Agency's{" "}
|
|
<span className="growth-word">Growth</span>{" "}
|
|
Starts Here
|
|
</h1>
|
|
<p className="mt-4 text-[13px] leading-[1.7] max-w-[360px] mx-auto body-text">
|
|
Manage leads, track conversions, and grow your web development business with our CRM.
|
|
</p>
|
|
<div className="stats-row">
|
|
<div className="stat">
|
|
<div className="stat-number">{counts.leads.toLocaleString()}</div>
|
|
<div className="stat-label">leads tracked</div>
|
|
</div>
|
|
<div className="stat">
|
|
<div className="stat-number">{counts.conversion}%</div>
|
|
<div className="stat-label">conversion lift</div>
|
|
</div>
|
|
<div className="stat">
|
|
<div className="stat-number">{counts.users}</div>
|
|
<div className="stat-label">active users</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative z-10 testimonial" style={{ background: "rgba(255,255,255,0.5)", borderRadius: "12px 12px 12px 12px", padding: "16px" }}>
|
|
<div style={{ position: "absolute", right: "-12px", bottom: "24px", width: "24px", height: "24px", borderRadius: "50%", background: "#0a0a0f", zIndex: 1 }} />
|
|
<div className="transition-opacity duration-500" key={testimonialIndex}>
|
|
<p className="text-sm font-semibold leading-relaxed" style={{ color: "#0a0a0f" }}>
|
|
“{testimonials[testimonialIndex].text}”
|
|
</p>
|
|
<p className="text-xs font-bold mt-2" style={{ color: "#0a0a0f" }}>
|
|
{testimonials[testimonialIndex].author}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center justify-center gap-1.5 mt-3">
|
|
{testimonials.map((_, i) => (
|
|
<button
|
|
key={i}
|
|
type="button"
|
|
onClick={() => setTestimonialIndex(i)}
|
|
className={`h-1.5 rounded-full transition-all duration-300 ${i === testimonialIndex ? "w-5 bg-[#1BB0CE]" : "w-1.5 bg-[#1BB0CE]/30"}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<canvas ref={canvasRef} className="wave-canvas" />
|
|
</div>
|
|
|
|
<div className="panel-divider" />
|
|
|
|
<div className="right-panel flex items-center justify-center p-10">
|
|
<canvas ref={starsCanvasRightRef} className="stars-canvas" />
|
|
<div className="accent-line" />
|
|
|
|
<div className="w-full" style={{ maxWidth: 340 }}>
|
|
<h2 className="text-[24px] font-bold text-[#e8e8ef] tracking-[-0.3px] mb-[5px]">
|
|
Welcome back
|
|
</h2>
|
|
<p className="text-[13px] mb-[26px] subheading-text">
|
|
Sign in to your account to continue
|
|
</p>
|
|
|
|
{error && (
|
|
<div className="mb-4 rounded bg-red-500/10 px-4 py-2 text-sm text-red-400">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
<div>
|
|
<label htmlFor="email" className="login-label">
|
|
Email
|
|
</label>
|
|
<div className="input-sheen">
|
|
<input
|
|
id="email"
|
|
type="email"
|
|
placeholder="name@company.com"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
required
|
|
autoComplete="email"
|
|
className="login-input"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex items-center justify-between mb-[6px]">
|
|
<label htmlFor="password" className="login-label" style={{ marginBottom: 0 }}>
|
|
Password
|
|
</label>
|
|
<button type="button" className="text-[12px] text-[#1BB0CE] hover:underline">
|
|
Forgot password?
|
|
</button>
|
|
</div>
|
|
<div className="input-sheen input-sheen-delayed">
|
|
<div className="relative">
|
|
<input
|
|
id="password"
|
|
type={showPassword ? "text" : "password"}
|
|
placeholder="Enter your password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
autoComplete="current-password"
|
|
className="login-input"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="password-toggle"
|
|
>
|
|
{showPassword ? (
|
|
<EyeOff className="h-4 w-4" />
|
|
) : (
|
|
<Eye className="h-4 w-4" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={remember}
|
|
onChange={(e) => setRemember(e.target.checked)}
|
|
className="login-checkbox"
|
|
/>
|
|
<span className="text-[13px] checkbox-text">
|
|
Remember me
|
|
</span>
|
|
</label>
|
|
|
|
<button type="submit" className="auth-btn" disabled={loading}>
|
|
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
{loading ? "Signing in..." : "Sign in"}
|
|
</button>
|
|
</form>
|
|
|
|
<p className="text-center text-[11px] mt-4 footer-text">
|
|
By signing in, you agree to our{" "}
|
|
<button type="button" className="text-[#1BB0CE] hover:underline">
|
|
Terms of Service
|
|
</button>{" "}
|
|
and{" "}
|
|
<button type="button" className="text-[#1BB0CE] hover:underline">
|
|
Privacy Policy
|
|
</button>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|