From c6ce93c56427ce17ab5f8c76d0aefe59393685db Mon Sep 17 00:00:00 2001 From: TroodonEnjoyer Date: Thu, 18 Jun 2026 13:45:57 +0200 Subject: [PATCH 1/7] yo --- src/app/(dashboard)/users/page.tsx | 2 +- src/components/layout/topbar.tsx | 6 +++--- src/components/users/user-form-dialog.tsx | 8 +++++--- src/data/users.ts | 16 ++++++++-------- src/lib/auth.ts | 6 ++---- src/types/index.ts | 2 +- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/app/(dashboard)/users/page.tsx b/src/app/(dashboard)/users/page.tsx index c218263..48cf43f 100644 --- a/src/app/(dashboard)/users/page.tsx +++ b/src/app/(dashboard)/users/page.tsx @@ -63,7 +63,7 @@ export default function UsersPage() {
-

{users.filter((u) => u.role === "sales").length}

+

{users.filter((u) => u.role === "sales_user").length}

Sales

diff --git a/src/components/layout/topbar.tsx b/src/components/layout/topbar.tsx index 93d35ac..77624c2 100644 --- a/src/components/layout/topbar.tsx +++ b/src/components/layout/topbar.tsx @@ -41,7 +41,7 @@ export function Topbar({ onMenuClick }: TopbarProps) { useEffect(() => { setMounted(true) }, []) if (!user) return null - const initials = (user.firstName[0] + user.lastName[0]).toUpperCase() + const initials = user.name.split(" ").map((n) => n[0]).join("") return (
@@ -119,13 +119,13 @@ export function Topbar({ onMenuClick }: TopbarProps) { {initials} - {user.firstName} {user.lastName} + {user.name}
-

{user.firstName} {user.lastName}

+

{user.name}

{user.email}

{user.role}
diff --git a/src/components/users/user-form-dialog.tsx b/src/components/users/user-form-dialog.tsx index 5965dca..1e7263e 100644 --- a/src/components/users/user-form-dialog.tsx +++ b/src/components/users/user-form-dialog.tsx @@ -55,7 +55,7 @@ export function UserFormDialog({ open, onOpenChange, user }: UserFormDialogProps defaultValues: { name: "", email: "", - role: "sales", + role: "sales_user", active: true, }, }) @@ -69,7 +69,7 @@ export function UserFormDialog({ open, onOpenChange, user }: UserFormDialogProps active: user.active, }) } else { - form.reset({ name: "", email: "", role: "sales", active: true }) + form.reset({ name: "", email: "", role: "sales_user", active: true }) } }, [user, form]) @@ -130,8 +130,10 @@ export function UserFormDialog({ open, onOpenChange, user }: UserFormDialogProps + Super Admin Admin - Sales + Sales + Developer diff --git a/src/data/users.ts b/src/data/users.ts index 3f244a4..b4924a1 100644 --- a/src/data/users.ts +++ b/src/data/users.ts @@ -1,4 +1,4 @@ -import { User } from "@/types" +import { User, UserRole } from "@/types" export const users: User[] = [ { @@ -14,7 +14,7 @@ export const users: User[] = [ id: "u2", name: "Marcus Johnson", email: "marcus@coastalit.com", - role: "sales", + role: "sales_user", active: true, avatar: "https://ui-avatars.com/api/?name=Marcus+Johnson&background=2563eb&color=fff&size=128", createdAt: "2025-02-01T09:00:00Z", @@ -23,7 +23,7 @@ export const users: User[] = [ id: "u3", name: "Emily Rodriguez", email: "emily@coastalit.com", - role: "sales", + role: "sales_user", active: true, avatar: "https://ui-avatars.com/api/?name=Emily+Rodriguez&background=3b82f6&color=fff&size=128", createdAt: "2025-02-15T10:00:00Z", @@ -32,7 +32,7 @@ export const users: User[] = [ id: "u4", name: "David Kim", email: "david@coastalit.com", - role: "sales", + role: "sales_user", active: true, avatar: "https://ui-avatars.com/api/?name=David+Kim&background=60a5fa&color=fff&size=128", createdAt: "2025-03-01T08:00:00Z", @@ -41,7 +41,7 @@ export const users: User[] = [ id: "u5", name: "Jessica Patel", email: "jessica@coastalit.com", - role: "sales", + role: "sales_user", active: false, avatar: "https://ui-avatars.com/api/?name=Jessica+Patel&background=93c5fd&color=fff&size=128", createdAt: "2025-03-10T09:00:00Z", @@ -50,7 +50,7 @@ export const users: User[] = [ id: "u6", name: "Alex Thompson", email: "alex@coastalit.com", - role: "sales", + role: "sales_user", active: true, avatar: "https://ui-avatars.com/api/?name=Alex+Thompson&background=1d4ed8&color=fff&size=128", createdAt: "2025-04-01T08:00:00Z", @@ -59,7 +59,7 @@ export const users: User[] = [ id: "u7", name: "Rachel Williams", email: "rachel@coastalit.com", - role: "sales", + role: "sales_user", active: true, avatar: "https://ui-avatars.com/api/?name=Rachel+Williams&background=2563eb&color=fff&size=128", createdAt: "2025-04-15T10:00:00Z", @@ -79,7 +79,7 @@ export function getUserById(id: string): User | undefined { return users.find((u) => u.id === id) } -export function getUsersByRole(role: "admin" | "sales"): User[] { +export function getUsersByRole(role: UserRole): User[] { return users.filter((u) => u.role === role) } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 5fe6525..07c74ab 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -156,9 +156,7 @@ export async function isAccountLocked(user: { } export function mapDbUserToSessionUser(dbUser: Record): SessionUser { - const roleName = dbUser.role_name as string - const mappedRole = - roleName === "SUPER_ADMIN" || roleName === "ADMIN" ? "admin" : "sales" + const roleName = (dbUser.role_name as string).toLowerCase() return { id: dbUser.id as string, @@ -166,7 +164,7 @@ export function mapDbUserToSessionUser(dbUser: Record): Session email: dbUser.email as string, firstName: dbUser.first_name as string, lastName: dbUser.last_name as string, - role: mappedRole, + role: roleName, avatar: `https://ui-avatars.com/api/?name=${encodeURIComponent( `${dbUser.first_name}+${dbUser.last_name}` )}&background=1d4ed8&color=fff&size=128`, diff --git a/src/types/index.ts b/src/types/index.ts index 30dac64..a219ecb 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,5 @@ export type LeadStatus = "open" | "contacted" | "pending" | "closed" | "ignored" -export type UserRole = "admin" | "sales" +export type UserRole = "super_admin" | "admin" | "sales_user" | "developer" export interface User { id: string From 03fbe13938b421b9519d0c3a74d6c9f0e646632e Mon Sep 17 00:00:00 2001 From: TroodonEnjoyer Date: Thu, 18 Jun 2026 14:26:54 +0200 Subject: [PATCH 2/7] New changes to topbar.tsx --- src/components/layout/topbar.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/layout/topbar.tsx b/src/components/layout/topbar.tsx index 954a881..de5093f 100644 --- a/src/components/layout/topbar.tsx +++ b/src/components/layout/topbar.tsx @@ -3,7 +3,6 @@ import { useState, useEffect } from "react" import { useRouter } from "next/navigation" import { useTheme } from "next-themes" -import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" @@ -149,4 +148,4 @@ export function Topbar({ onMenuClick }: TopbarProps) {
) -} +} \ No newline at end of file From 0898ad4ebf06aaa6391a2720918591e161d48699 Mon Sep 17 00:00:00 2001 From: Hannah_Bagga Date: Thu, 18 Jun 2026 15:21:56 +0200 Subject: [PATCH 3/7] Changed the dashboards --- src/app/(dashboard)/dashboard/page.tsx | 4 + .../dashboard/leads-per-month-chart.tsx | 15 ++- src/components/dashboard/stat-card.tsx | 116 +++++++++++++++++- src/components/layout/topbar.tsx | 8 +- src/components/shared/page-header.tsx | 2 +- src/data/dashboard.ts | 10 +- src/data/leads.ts | 20 +-- 7 files changed, 147 insertions(+), 28 deletions(-) diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index bda9349..fc4fc57 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -54,12 +54,16 @@ export default function DashboardPage() { +

Pipeline Overview

+
{statCards.map((card, i) => ( ))}
+

Analytics

+
diff --git a/src/components/dashboard/leads-per-month-chart.tsx b/src/components/dashboard/leads-per-month-chart.tsx index dcdf708..6749387 100644 --- a/src/components/dashboard/leads-per-month-chart.tsx +++ b/src/components/dashboard/leads-per-month-chart.tsx @@ -14,9 +14,8 @@ interface LeadsPerMonthChartProps { data: MonthlyData[] } -const BAR_GRADIENT_TOP = "#3B6AB8" -const NEW_LEADS = "#1e3c72" -const CLOSED = "#457fca" +const NEW_LEADS = "#0d9488" +const CLOSED = "#c9a96e" export function LeadsPerMonthChart({ data }: LeadsPerMonthChartProps) { const [hovered, setHovered] = useState(null) @@ -87,14 +86,14 @@ export function LeadsPerMonthChart({ data }: LeadsPerMonthChartProps) { > - + - + - + @@ -160,7 +159,7 @@ export function LeadsPerMonthChart({ data }: LeadsPerMonthChartProps) { height={newH} rx={4} fill="url(#newLeadsGrad)" - filter={isActive ? "url(#shadowBlue)" : undefined} + filter={isActive ? "url(#shadowNew)" : undefined} opacity={hovered !== null && !isActive ? 0.35 : 1} style={{ transition: "opacity 0.15s ease" }} /> @@ -247,4 +246,4 @@ export function LeadsPerMonthChart({ data }: LeadsPerMonthChartProps) { ) -} \ No newline at end of file +} diff --git a/src/components/dashboard/stat-card.tsx b/src/components/dashboard/stat-card.tsx index b2a9eb8..2531065 100644 --- a/src/components/dashboard/stat-card.tsx +++ b/src/components/dashboard/stat-card.tsx @@ -1,5 +1,6 @@ "use client" +import { useEffect, useState } from "react" import { motion } from "framer-motion" import { cn } from "@/lib/utils" import { Card, CardContent } from "@/components/ui/card" @@ -16,37 +17,140 @@ interface StatCardProps { const variantStyles = { default: { bg: "bg-muted", text: "text-foreground" }, - blue: { bg: "bg-blue-500/10", text: "text-blue-600 dark:text-blue-400" }, + blue: { bg: "bg-teal-500/10", text: "text-teal-600 dark:text-teal-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" }, } +const cardGradients: Record = { + "Total Leads": "from-[#0d9488] to-[#5eead4]", + "Open Leads": "from-[#14b8a6] to-[#5eead4]", + "Contacted": "from-[#c9a96e] to-[#e8d5a3]", + "Pending": "from-[#94a3b8] to-[#cbd5e1]", + "Closed": "from-[#c9a96e] to-[#e8d5a3]", + "Conversion Rate": "from-[#f43f5e] to-[#fb7185]", +} + +const trendBadges: Record = { + "Total Leads": { label: "12%", up: true }, + "Open Leads": { label: "8%", up: true }, + "Contacted": { label: "5%", up: true }, + "Pending": { label: "3%", up: false }, + "Closed": { label: "15%", up: true }, + "Conversion Rate": { label: "2%", up: true }, +} + +const sparklines: Record = { + "Total Leads": "M0,28 L10,22 L20,18 L30,20 L40,12 L50,8 L60,6", + "Open Leads": "M0,28 L10,24 L20,26 L30,20 L40,22 L50,18 L60,14", + "Contacted": "M0,28 L10,20 L20,16 L30,18 L40,10 L50,6 L60,4", + "Pending": "M0,28 L10,26 L20,24 L30,22 L40,20 L50,18 L60,16", + "Closed": "M0,28 L10,24 L20,18 L30,14 L40,10 L50,6 L60,2", + "Conversion Rate": "M0,28 L10,22 L20,20 L30,16 L40,12 L50,8 L60,4", +} + +const sparklineColors: Record = { + "Total Leads": "#0d9488", + "Open Leads": "#14b8a6", + "Contacted": "#c9a96e", + "Pending": "#94a3b8", + "Closed": "#c9a96e", + "Conversion Rate": "#f43f5e", +} + export function StatCard({ title, value, icon: Icon, variant = "default", description, index = 0 }: StatCardProps) { const style = variantStyles[variant] + const gradient = cardGradients[title] ?? "from-[#0d9488] to-[#5eead4]" + const sparkPath = sparklines[title] ?? "M0,28 L10,22 L20,18 L30,20 L40,12 L50,8 L60,6" + const sparkColor = sparklineColors[title] ?? "#0d9488" + const badge = trendBadges[title] + + const isNumeric = typeof value === "number" + const [display, setDisplay] = useState(0) + const target = typeof value === "number" ? value : 0 + + useEffect(() => { + if (!isNumeric) return + const duration = 1000 + const steps = 40 + const increment = target / steps + let current = 0 + const timer = setInterval(() => { + current += increment + if (current >= target) { + setDisplay(target) + clearInterval(timer) + } else { + setDisplay(Math.floor(current)) + } + }, duration / steps) + return () => clearInterval(timer) + }, [target, isNumeric]) return ( - - -
+ +
+ +

{title}

-

{value}

+

+ {isNumeric ? display : value} +

{description && (

{description}

)} + {badge && ( + + {badge.up ? "↑" : "↓"} {badge.label} + + )}
-
+
+ +
+ + + + + + + + + + +
+ + {title === "Conversion Rate" && ( +
+
+
+
+

22% conversion rate

+
+ )} + + {(title === "Closed" || title === "Conversion Rate") && ( +
+ )} ) diff --git a/src/components/layout/topbar.tsx b/src/components/layout/topbar.tsx index 119d69f..9efe5a3 100644 --- a/src/components/layout/topbar.tsx +++ b/src/components/layout/topbar.tsx @@ -41,6 +41,12 @@ export function Topbar({ onMenuClick }: TopbarProps) { return (
+ {/* Logo */} +
+
+ CRM +
+ {/* Mobile menu button */} diff --git a/src/components/shared/page-header.tsx b/src/components/shared/page-header.tsx index e4cb7cb..7cd6c34 100644 --- a/src/components/shared/page-header.tsx +++ b/src/components/shared/page-header.tsx @@ -19,7 +19,7 @@ export function PageHeader({ title, description, children, className }: PageHead className={cn("flex items-center justify-between pb-6", className)} >
-

{title}

+

{title}

{description &&

{description}

}
{children &&
{children}
} diff --git a/src/data/dashboard.ts b/src/data/dashboard.ts index a1e0153..cf6f7f6 100644 --- a/src/data/dashboard.ts +++ b/src/data/dashboard.ts @@ -28,11 +28,11 @@ function getStatusDistribution() { }) return [ - { name: "Open", value: statusCounts.open, color: "#3b82f6" }, - { name: "Contacted", value: statusCounts.contacted, color: "#f59e0b" }, - { name: "Pending", value: statusCounts.pending, color: "#8b5cf6" }, - { name: "Closed", value: statusCounts.closed, color: "#10b981" }, - { name: "Ignored", value: statusCounts.ignored, color: "#6B7280" }, + { name: "Open", value: statusCounts.open, color: "#0d9488" }, + { name: "Contacted", value: statusCounts.contacted, color: "#5eead4" }, + { name: "Pending", value: statusCounts.pending, color: "#c9a96e" }, + { name: "Closed", value: statusCounts.closed, color: "#f43f5e" }, + { name: "Ignored", value: statusCounts.ignored, color: "#94a3b8" }, ] } diff --git a/src/data/leads.ts b/src/data/leads.ts index 2be1d0c..a4609e4 100644 --- a/src/data/leads.ts +++ b/src/data/leads.ts @@ -26,22 +26,28 @@ const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", " const sources = ["Website", "Referral", "LinkedIn", "Cold Call", "Email", "Google", "Social Media", "Other"] const statuses: LeadStatus[] = ["open", "contacted", "pending", "closed", "ignored"] +const phonePrefixes = ["555", "123", "456", "789", "321", "654", "987", "234", "876", "432"] + +let _rngSeed = 42 +function rng(): number { + _rngSeed = (_rngSeed * 1664525 + 1013904223) & 0x7fffffff + return _rngSeed / 0x7fffffff +} + function randomItem(arr: T[]): T { - return arr[Math.floor(Math.random() * arr.length)] + return arr[Math.floor(rng() * arr.length)] } function randomDate(startMonthsAgo: number): string { const now = new Date() const start = new Date(now.getFullYear(), now.getMonth() - startMonthsAgo, 1) - const randomTime = start.getTime() + Math.random() * (now.getTime() - start.getTime()) + const randomTime = start.getTime() + rng() * (now.getTime() - start.getTime()) return new Date(randomTime).toISOString() } -const phonePrefixes = ["555", "123", "456", "789", "321", "654", "987", "234", "876", "432"] - function randomPhone(): string { const prefix = randomItem(phonePrefixes) - const suffix = Math.floor(Math.random() * 10000000).toString().padStart(7, "0") + const suffix = Math.floor(rng() * 10000000).toString().padStart(7, "0") return `(${prefix}) ${suffix.slice(0, 3)}-${suffix.slice(3)}` } @@ -49,7 +55,7 @@ export const leads: Lead[] = Array.from({ length: 120 }, (_, i) => { const firstName = randomItem(firstNames) const lastName = randomItem(lastNames) const status = randomItem(statuses) - const assignedUser = Math.random() > 0.15 ? randomItem(users.filter((u) => u.active)) : null + const assignedUser = rng() > 0.15 ? randomItem(users.filter((u) => u.active)) : null const createdAt = randomDate(6) return { @@ -59,7 +65,7 @@ export const leads: Lead[] = Array.from({ length: 120 }, (_, i) => { email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@${companyNames[i % companyNames.length].toLowerCase().replace(/\s+/g, "")}.com`, phone: randomPhone(), source: randomItem(sources), - description: `Looking for a complete website redesign and modern digital presence. Interested in responsive design, SEO optimization, and a custom CMS solution.`, + description: "Looking for a complete website redesign and modern digital presence. Interested in responsive design, SEO optimization, and a custom CMS solution.", status, assignedUserId: assignedUser?.id ?? null, assignedUser, From 10b5de7a2114e48d79c551394d8db492a9eb595a Mon Sep 17 00:00:00 2001 From: Hannah_Bagga Date: Thu, 18 Jun 2026 15:29:55 +0200 Subject: [PATCH 4/7] Fix ESLint import paths and Next.js 15 params type for leads page --- eslint.config.mjs | 8 ++++---- src/app/(dashboard)/leads/[id]/page.tsx | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..e1d1c19 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,10 +1,10 @@ import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; -import nextTs from "eslint-config-next/typescript"; +import nextVitals from "eslint-config-next/core-web-vitals.js"; +import nextTs from "eslint-config-next/typescript.js"; const eslintConfig = defineConfig([ - ...nextVitals, - ...nextTs, + nextVitals, + nextTs, // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: diff --git a/src/app/(dashboard)/leads/[id]/page.tsx b/src/app/(dashboard)/leads/[id]/page.tsx index 7530b5e..c28094d 100644 --- a/src/app/(dashboard)/leads/[id]/page.tsx +++ b/src/app/(dashboard)/leads/[id]/page.tsx @@ -17,10 +17,12 @@ import { } from "@/components/ui/select" import { getLeadById } from "@/data/leads" import { getNotesByLeadId } from "@/data/notes" +import { use } from "react" 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) { From 5b115ae9ffa57f82a61f2a505e9521855295a77f Mon Sep 17 00:00:00 2001 From: Ace Date: Thu, 18 Jun 2026 15:45:59 +0200 Subject: [PATCH 5/7] SuperAdmin changes --- src/app/(dashboard)/users/page.tsx | 109 ++++++++++++---------- src/app/api/users/[id]/route.ts | 24 +++++ src/app/api/users/route.ts | 78 ++++++++++++++++ src/app/page.tsx | 2 +- src/components/users/user-form-dialog.tsx | 77 +++++++++------ src/components/users/users-table.tsx | 38 +++++--- 6 files changed, 239 insertions(+), 89 deletions(-) create mode 100644 src/app/api/users/[id]/route.ts create mode 100644 src/app/api/users/route.ts diff --git a/src/app/(dashboard)/users/page.tsx b/src/app/(dashboard)/users/page.tsx index 48cf43f..be937a7 100644 --- a/src/app/(dashboard)/users/page.tsx +++ b/src/app/(dashboard)/users/page.tsx @@ -1,22 +1,54 @@ "use client" -import { useState } from "react" +import { useState, useEffect, useCallback } from "react" import { PageHeader } from "@/components/shared/page-header" import { UsersTable } from "@/components/users/users-table" import { UserFormDialog } from "@/components/users/user-form-dialog" import { Button } from "@/components/ui/button" -import { users } from "@/data/users" -import { Plus, Users as UsersIcon } from "lucide-react" +import { Input } from "@/components/ui/input" +import { Plus, Search, Users as UsersIcon } from "lucide-react" +import { User } from "@/types" +import { toast } from "sonner" export default function UsersPage() { const [createOpen, setCreateOpen] = useState(false) + const [users, setUsers] = useState([]) + const [loading, setLoading] = useState(true) + const [searchQuery, setSearchQuery] = useState("") + + const fetchUsers = useCallback(async () => { + try { + const res = await fetch("/api/users") + const data = await res.json() + if (data.users) setUsers(data.users) + } catch { + toast.error("Failed to load users") + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + fetchUsers() + }, [fetchUsers]) + + const filteredUsers = searchQuery.trim() + ? users.filter((u) => + u.name.toLowerCase().includes(searchQuery.toLowerCase()) || + u.email.toLowerCase().includes(searchQuery.toLowerCase()) + ) + : users + + const stats = [ + { label: "Total Users", value: users.length, color: "bg-blue-500/10", textColor: "text-blue-600 dark:text-blue-400" }, + { label: "Active", value: users.filter((u) => u.active).length, color: "bg-emerald-500/10", textColor: "text-emerald-600 dark:text-emerald-400" }, + { label: "Admins", value: users.filter((u) => u.role === "admin" || u.role === "super_admin").length, color: "bg-purple-500/10", textColor: "text-purple-600 dark:text-purple-400" }, + { label: "Sales", value: users.filter((u) => u.role === "sales_user").length, color: "bg-amber-500/10", textColor: "text-amber-600 dark:text-amber-400" }, + ] return (
- + - diff --git a/src/components/users/users-table.tsx b/src/components/users/users-table.tsx index d597e6b..0c25783 100644 --- a/src/components/users/users-table.tsx +++ b/src/components/users/users-table.tsx @@ -11,17 +11,31 @@ import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { User } from "@/types" -import { Pencil, Power, PowerOff } from "lucide-react" +import { Pencil, Trash2 } from "lucide-react" +import { toast } from "sonner" interface UsersTableProps { data: User[] loading?: boolean + onUserDeleted?: () => void } -export function UsersTable({ data, loading }: UsersTableProps) { +export function UsersTable({ data, loading, onUserDeleted }: UsersTableProps) { const [editingUser, setEditingUser] = useState(null) const [editOpen, setEditOpen] = useState(false) + const handleDelete = async (id: string) => { + if (!confirm("Are you sure you want to delete this user?")) return + try { + const res = await fetch(`/api/users/${id}`, { method: "DELETE" }) + if (!res.ok) throw new Error() + toast.success("User deleted") + onUserDeleted?.() + } catch { + toast.error("Failed to delete user") + } + } + const columns: ColumnDef[] = useMemo( () => [ { @@ -76,15 +90,15 @@ export function UsersTable({ data, loading }: UsersTableProps) { ), cell: ({ row }) => { const role = row.getValue("role") as string + const colors: Record = { + super_admin: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20", + admin: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20", + sales_user: "bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/20", + developer: "bg-zinc-500/10 text-zinc-600 dark:text-zinc-400 border-zinc-500/20", + } return ( - - {role} + + {role.replace("_", " ")} ) }, @@ -117,8 +131,9 @@ export function UsersTable({ data, loading }: UsersTableProps) { variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-destructive" + onClick={() => handleDelete(user.id)} > - {user.active ? : } +
) @@ -140,6 +155,7 @@ export function UsersTable({ data, loading }: UsersTableProps) { open={editOpen} onOpenChange={setEditOpen} user={editingUser} + onUserCreated={onUserDeleted} /> ) From 13555c3fab65919e1ac3618b8c4c245ddd510a3f Mon Sep 17 00:00:00 2001 From: Hannah_Bagga Date: Thu, 18 Jun 2026 16:09:14 +0200 Subject: [PATCH 6/7] Fix duplicate import of use() in leads/[id] page --- src/app/(dashboard)/leads/[id]/page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/(dashboard)/leads/[id]/page.tsx b/src/app/(dashboard)/leads/[id]/page.tsx index ea07a97..9136b4c 100644 --- a/src/app/(dashboard)/leads/[id]/page.tsx +++ b/src/app/(dashboard)/leads/[id]/page.tsx @@ -18,7 +18,6 @@ import { } from "@/components/ui/select" import { getLeadById } from "@/data/leads" import { getNotesByLeadId } from "@/data/notes" -import { use } from "react" import { ArrowLeft, Edit, ExternalLink } from "lucide-react" export default function LeadDetailsPage({ params }: { params: Promise<{ id: string }> }) { From b19a00c6c2611e0af21422652bdf590311bb1368 Mon Sep 17 00:00:00 2001 From: William_Britz Date: Thu, 18 Jun 2026 16:10:02 +0200 Subject: [PATCH 7/7] Updated the Login --- src/app/(dashboard)/leads/[id]/page.tsx | 6 +- src/app/globals.css | 138 +++-- src/app/layout.tsx | 2 +- src/app/login/page.tsx | 697 +++++++++++++++++++----- 4 files changed, 623 insertions(+), 220 deletions(-) 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{" "} + +

+
+
-
+ ) }