From 1e4b8df8dd728aeb526b1e5f6476a03158de1662 Mon Sep 17 00:00:00 2001 From: caitlin Date: Thu, 18 Jun 2026 16:43:26 +0200 Subject: [PATCH 1/7] I added functioonality to the notifications --- src/app/(dashboard)/layout.tsx | 5 +- src/app/(dashboard)/leads/new/page.tsx | 8 + src/app/(dashboard)/users/page.tsx | 40 +++- .../dashboard/lead-status-chart.tsx | 15 ++ .../dashboard/leads-per-month-chart.tsx | 18 +- src/components/layout/sidebar.tsx | 9 +- src/components/layout/topbar.tsx | 173 +++++++++++++----- src/components/leads/lead-form-dialog.tsx | 11 ++ src/components/users/user-form-dialog.tsx | 2 + src/data/users.ts | 13 +- src/lib/auth.ts | 9 +- src/providers/notification-provider.tsx | 137 ++++++++++++++ src/types/index.ts | 14 +- 13 files changed, 390 insertions(+), 64 deletions(-) create mode 100644 src/providers/notification-provider.tsx diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 2a73b5a..94b1d8d 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -2,6 +2,7 @@ import { AppShell } from "@/components/layout/app-shell" import { UserProvider, useUser } from "@/providers/user-provider" +import { NotificationProvider } from "@/providers/notification-provider" import { Loader2 } from "lucide-react" function DashboardContent({ children }: { children: React.ReactNode }) { @@ -27,7 +28,9 @@ export default function DashboardLayout({ }) { return ( - {children} + + {children} + ) } diff --git a/src/app/(dashboard)/leads/new/page.tsx b/src/app/(dashboard)/leads/new/page.tsx index 4db1ae7..6b926c8 100644 --- a/src/app/(dashboard)/leads/new/page.tsx +++ b/src/app/(dashboard)/leads/new/page.tsx @@ -29,6 +29,7 @@ import { ArrowLeft } from "lucide-react" import { Lead, LeadStatus } from "@/types" import { leads } from "@/data/leads" import { users } from "@/data/users" +import { useNotifications } from "@/providers/notification-provider" import { LEAD_SOURCES, LEAD_STATUSES } from "@/lib/constants" const leadFormSchema = z.object({ @@ -46,6 +47,7 @@ type LeadFormValues = z.infer export default function CreateLeadPage() { const router = useRouter() + const { addNotification } = useNotifications() const [saving, setSaving] = useState(false) const form = useForm({ @@ -84,6 +86,12 @@ export default function CreateLeadPage() { } leads.unshift(newLead) + addNotification( + "lead_created", + "New Lead Created", + `${newLead.companyName} — ${newLead.contactName}`, + `/leads/${newLead.id}` + ) router.push("/leads") } diff --git a/src/app/(dashboard)/users/page.tsx b/src/app/(dashboard)/users/page.tsx index c218263..1d2ee14 100644 --- a/src/app/(dashboard)/users/page.tsx +++ b/src/app/(dashboard)/users/page.tsx @@ -1,15 +1,29 @@ "use client" -import { useState } from "react" +import { useState, useMemo } 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 { Input } from "@/components/ui/input" import { users } from "@/data/users" -import { Plus, Users as UsersIcon } from "lucide-react" +import { useUser } from "@/providers/user-provider" +import { Plus, Search, Users as UsersIcon } from "lucide-react" export default function UsersPage() { + const { user } = useUser() const [createOpen, setCreateOpen] = useState(false) + const [search, setSearch] = useState("") + + const filteredUsers = useMemo(() => { + if (!search) return users + const q = search.toLowerCase() + return users.filter( + (u) => + u.name.toLowerCase().includes(q) || + u.email.toLowerCase().includes(q) + ) + }, [search]) return (
@@ -17,10 +31,12 @@ export default function UsersPage() { title="Users" description="Manage team members and their roles" > - + {user?.role === "super_admin" && ( + + )}
@@ -70,8 +86,18 @@ export default function UsersPage() {
+
+ + setSearch(e.target.value)} + className="h-9 pl-9" + /> +
+
- +
diff --git a/src/components/dashboard/lead-status-chart.tsx b/src/components/dashboard/lead-status-chart.tsx index 12ec99f..30d2780 100644 --- a/src/components/dashboard/lead-status-chart.tsx +++ b/src/components/dashboard/lead-status-chart.tsx @@ -36,6 +36,21 @@ export function LeadStatusChart({ data }: LeadStatusChartProps) { const total = data.reduce((s, d) => s + d.value, 0) + if (total === 0) { + return ( + + + Lead Status + + +
+ No data for this period +
+
+
+ ) + } + let cum = 0 const segs = data.map((d) => { const start = cum diff --git a/src/components/dashboard/leads-per-month-chart.tsx b/src/components/dashboard/leads-per-month-chart.tsx index b245d36..c758339 100644 --- a/src/components/dashboard/leads-per-month-chart.tsx +++ b/src/components/dashboard/leads-per-month-chart.tsx @@ -28,8 +28,9 @@ export function LeadsPerMonthChart({ data }: LeadsPerMonthChartProps) { const chartH = height - padding.top - padding.bottom const maxVal = useMemo(() => { + if (data.length === 0) return 1 const max = Math.max(...data.map((d) => Math.max(d.leads, d.closed))) - return Math.ceil(max / 7) * 7 + return Math.max(Math.ceil(max / 7) * 7, 1) }, [data]) const ticks = useMemo(() => { @@ -50,6 +51,21 @@ export function LeadsPerMonthChart({ data }: LeadsPerMonthChartProps) { const totalClosed = data.reduce((s, d) => s + d.closed, 0) const closeRate = totalNew > 0 ? Math.round((totalClosed / totalNew) * 100) : 0 + if (data.length === 0) { + return ( + + + Leads Per Month + + +
+ No data for this period +
+
+
+ ) + } + return ( sum + c.unread, 0) +import { useNotifications } from "@/providers/notification-provider" const navItems = [ { href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, @@ -42,6 +40,7 @@ interface SidebarProps { export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: SidebarProps) { const pathname = usePathname() const { user } = useUser() + const { unreadChatCount } = useNotifications() if (!user) return null const initials = user.name.split(" ").map((n) => n[0]).join("") @@ -104,9 +103,9 @@ export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: Side )} > - {item.label === "Chats" && totalUnread > 0 && ( + {item.label === "Chats" && unreadChatCount > 0 && ( - {totalUnread} + {unreadChatCount} )} diff --git a/src/components/layout/topbar.tsx b/src/components/layout/topbar.tsx index 954a881..7c7d0f1 100644 --- a/src/components/layout/topbar.tsx +++ b/src/components/layout/topbar.tsx @@ -1,13 +1,13 @@ -"use client" +"use client"; -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" -import { useUser } from "@/providers/user-provider" +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useTheme } from "next-themes"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { useUser } from "@/providers/user-provider"; +import { useNotifications } from "@/providers/notification-provider"; import { DropdownMenu, DropdownMenuContent, @@ -15,8 +15,8 @@ import { DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { Badge } from "@/components/ui/badge" +} from "@/components/ui/dropdown-menu"; +import { Badge } from "@/components/ui/badge"; import { Search, Bell, @@ -26,27 +26,53 @@ import { LogOut, User, Settings, -} from "lucide-react" + CheckCheck, + Trash2, +} from "lucide-react"; interface TopbarProps { - onMenuClick: () => void + onMenuClick: () => void; } export function Topbar({ onMenuClick }: TopbarProps) { - const router = useRouter() - const { theme, setTheme } = useTheme() - const { user, logout } = useUser() - const [mounted, setMounted] = useState(false) - const [searchOpen, setSearchOpen] = useState(false) + const router = useRouter(); + const { theme, setTheme } = useTheme(); + const { user, logout } = useUser(); + const { notifications, unreadCount, markAsRead, markAllAsRead, dismiss } = useNotifications(); + const [mounted, setMounted] = useState(false); + const [searchOpen, setSearchOpen] = useState(false); - useEffect(() => { setMounted(true) }, []) - if (!user) return null - const initials = user.name.split(" ").map((n: string) => n[0]).join("").toUpperCase() + useEffect(() => { + setMounted(true); + }, []); + if (!user) return null; + + function formatTimeAgo(ts: string): string { + const diff = Date.now() - new Date(ts).getTime() + const mins = Math.floor(diff / 60000) + if (mins < 1) return "Just now" + if (mins < 60) return `${mins}m ago` + const hrs = Math.floor(mins / 60) + if (hrs < 24) return `${hrs}h ago` + const days = Math.floor(hrs / 24) + if (days < 7) return `${days}d ago` + return new Date(ts).toLocaleDateString() + } + const initials = user.name + .split(" ") + .map((n: string) => n[0]) + .join("") + .toUpperCase(); return (
{/* Mobile menu button */} - @@ -77,37 +103,87 @@ export function Topbar({ onMenuClick }: TopbarProps) { onClick={() => setTheme(theme === "dark" ? "light" : "dark")} className="text-muted-foreground" > - {mounted ? (theme === "dark" ? : ) :
} + {mounted ? ( + theme === "dark" ? ( + + ) : ( + + ) + ) : ( +
+ )} {/* Notifications */} - - Notifications + + Notifications + {notifications.length > 0 && ( + + )} + -
- {[ - { text: "New lead assigned to you", time: "5m ago" }, - { text: "Lead status updated to Closed", time: "1h ago" }, - { text: "Note added to Brightwave Studios", time: "3h ago" }, - ].map((n, i) => ( -
-
-
-

{n.text}

-

{n.time}

+ {notifications.length === 0 ? ( +
+ No notifications +
+ ) : ( +
+ {notifications.slice(0, 20).map((n) => ( +
{ + if (!n.read) markAsRead(n.id) + if (n.link) router.push(n.link) + }} + className={`flex cursor-pointer items-start gap-3 rounded-lg p-2 transition-colors hover:bg-muted/50 ${!n.read ? "bg-muted/30" : ""}`} + > +
+
+

{n.title}

+

{n.description}

+

+ {formatTimeAgo(n.timestamp)} +

+
+
-
- ))} -
+ ))} +
+ )} @@ -119,7 +195,9 @@ export function Topbar({ onMenuClick }: TopbarProps) { {initials} - {user.name} + + {user.name} + @@ -127,7 +205,12 @@ export function Topbar({ onMenuClick }: TopbarProps) {

{user.name}

{user.email}

- {user.role} + + {user.role} +
@@ -148,5 +231,5 @@ export function Topbar({ onMenuClick }: TopbarProps) {
- ) + ); } diff --git a/src/components/leads/lead-form-dialog.tsx b/src/components/leads/lead-form-dialog.tsx index a7c3579..afa6081 100644 --- a/src/components/leads/lead-form-dialog.tsx +++ b/src/components/leads/lead-form-dialog.tsx @@ -33,6 +33,7 @@ import { import { Lead, LeadStatus, User } from "@/types" import { LEAD_SOURCES, LEAD_STATUSES } from "@/lib/constants" import { users } from "@/data/users" +import { useNotifications } from "@/providers/notification-provider" const leadFormSchema = z.object({ companyName: z.string().min(1, "Company name is required"), @@ -55,6 +56,7 @@ interface LeadFormDialogProps { export function LeadFormDialog({ open, onOpenChange, lead }: LeadFormDialogProps) { const isEditing = !!lead + const { addNotification } = useNotifications() const form = useForm({ resolver: zodResolver(leadFormSchema), @@ -101,6 +103,15 @@ export function LeadFormDialog({ open, onOpenChange, lead }: LeadFormDialogProps ...values, assignedUserId: values.assignedUserId === "none" ? null : values.assignedUserId, } + if (!isEditing) { + const now = new Date().toISOString() + addNotification( + "lead_created", + "New Lead Created", + `${values.companyName} — ${values.contactName}`, + "#" + ) + } console.log(payload) onOpenChange(false) } diff --git a/src/components/users/user-form-dialog.tsx b/src/components/users/user-form-dialog.tsx index 5965dca..b3ddcdc 100644 --- a/src/components/users/user-form-dialog.tsx +++ b/src/components/users/user-form-dialog.tsx @@ -130,7 +130,9 @@ export function UserFormDialog({ open, onOpenChange, user }: UserFormDialogProps + Super Admin Admin + Developer Sales diff --git a/src/data/users.ts b/src/data/users.ts index 3f244a4..227d07f 100644 --- a/src/data/users.ts +++ b/src/data/users.ts @@ -1,6 +1,15 @@ -import { User } from "@/types" +import { User, UserRole } from "@/types" export const users: User[] = [ + { + id: "u-super", + name: "Jason Oosthuizen", + email: "jason@coastalit.com", + role: "super_admin", + active: true, + avatar: "https://ui-avatars.com/api/?name=Jason+Oosthuizen&background=0f172a&color=fff&size=128", + createdAt: "2025-01-01T08:00:00Z", + }, { id: "u1", name: "Sarah Chen", @@ -79,7 +88,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..7814960 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -157,8 +157,13 @@ 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 roleMap: Record = { + SUPER_ADMIN: "super_admin", + ADMIN: "admin", + DEVELOPER: "developer", + SALES_USER: "sales", + } + const mappedRole = roleMap[roleName] || "sales" return { id: dbUser.id as string, diff --git a/src/providers/notification-provider.tsx b/src/providers/notification-provider.tsx new file mode 100644 index 0000000..2b33806 --- /dev/null +++ b/src/providers/notification-provider.tsx @@ -0,0 +1,137 @@ +"use client" + +import { createContext, useContext, useState, useCallback, useMemo, ReactNode } from "react" +import { Notification, NotificationType } from "@/types" +import { conversations } from "@/data/chats" +import { leads } from "@/data/leads" + +interface NotificationContextValue { + notifications: Notification[] + unreadCount: number + unreadChatCount: number + addNotification: (type: NotificationType, title: string, description: string, link?: string) => void + markAsRead: (id: string) => void + markAllAsRead: () => void + dismiss: (id: string) => void +} + +const NotificationContext = createContext(null) + +function generateId(): string { + return `notif-${Date.now()}-${Math.random().toString(36).slice(2, 7)}` +} + +function seedInitialNotifications(): Notification[] { + const result: Notification[] = [] + + const recentLeads = leads.slice(0, 5) + recentLeads.forEach((lead) => { + result.push({ + id: generateId(), + type: "lead_created", + title: "New Lead Created", + description: `${lead.companyName} — ${lead.contactName}`, + timestamp: lead.createdAt, + read: false, + link: `/leads/${lead.id}`, + }) + }) + + const statusChanged = leads.filter((l) => l.status !== "open").slice(0, 3) + statusChanged.forEach((lead) => { + result.push({ + id: generateId(), + type: "lead_status_changed", + title: "Lead Status Updated", + description: `${lead.companyName} moved to ${lead.status}`, + timestamp: lead.updatedAt, + read: false, + link: `/leads/${lead.id}`, + }) + }) + + const assignedLeads = leads.filter((l) => l.assignedUser).slice(0, 3) + assignedLeads.forEach((lead) => { + result.push({ + id: generateId(), + type: "lead_assigned", + title: "Lead Assigned", + description: `${lead.companyName} assigned to ${lead.assignedUser!.name}`, + timestamp: lead.updatedAt, + read: false, + link: `/leads/${lead.id}`, + }) + }) + + result.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) + + return result +} + +export function NotificationProvider({ children }: { children: ReactNode }) { + const [notifications, setNotifications] = useState(seedInitialNotifications) + + const unreadChatCount = useMemo( + () => conversations.reduce((sum, c) => sum + c.unread, 0), + [] + ) + + const unreadCount = useMemo( + () => notifications.filter((n) => !n.read).length, + [notifications] + ) + + const addNotification = useCallback( + (type: NotificationType, title: string, description: string, link?: string) => { + const notif: Notification = { + id: generateId(), + type, + title, + description, + timestamp: new Date().toISOString(), + read: false, + link, + } + setNotifications((prev) => [notif, ...prev]) + }, + [] + ) + + const markAsRead = useCallback((id: string) => { + setNotifications((prev) => + prev.map((n) => (n.id === id ? { ...n, read: true } : n)) + ) + }, []) + + const markAllAsRead = useCallback(() => { + setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))) + }, []) + + const dismiss = useCallback((id: string) => { + setNotifications((prev) => prev.filter((n) => n.id !== id)) + }, []) + + return ( + + {children} + + ) +} + +export function useNotifications(): NotificationContextValue { + const ctx = useContext(NotificationContext) + if (!ctx) { + throw new Error("useNotifications must be used within a NotificationProvider") + } + return ctx +} diff --git a/src/types/index.ts b/src/types/index.ts index a9b5f1a..6895ed7 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" | "developer" | "sales" export interface User { id: string @@ -57,6 +57,18 @@ export interface ColumnFilter { value: unknown } +export type NotificationType = "lead_created" | "lead_status_changed" | "lead_assigned" | "chat_message" | "note_added" + +export interface Notification { + id: string + type: NotificationType + title: string + description: string + timestamp: string + read: boolean + link?: string +} + export interface ChatMessage { id: string conversationId: string From 6fe41b6c9683199db1fe22c77e2391dc05d2873b Mon Sep 17 00:00:00 2001 From: caitlin Date: Thu, 18 Jun 2026 16:47:31 +0200 Subject: [PATCH 2/7] I added functionality to the notification button --- src/app/api/users/[id]/route.ts | 24 ++++++++++ src/app/api/users/route.ts | 78 +++++++++++++++++++++++++++++++++ src/app/layout.tsx | 35 --------------- src/app/login/layout.tsx | 16 +++++++ 4 files changed, 118 insertions(+), 35 deletions(-) create mode 100644 src/app/api/users/[id]/route.ts create mode 100644 src/app/api/users/route.ts delete mode 100644 src/app/layout.tsx create mode 100644 src/app/login/layout.tsx diff --git a/src/app/api/users/[id]/route.ts b/src/app/api/users/[id]/route.ts new file mode 100644 index 0000000..8b4e294 --- /dev/null +++ b/src/app/api/users/[id]/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from "next/server" +import { query } from "@/lib/db" +import { getSessionUser } from "@/lib/auth" + +export async function DELETE(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + try { + const sessionUser = await getSessionUser() + if (!sessionUser) { + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + } + + const { id } = await params + + await query( + `UPDATE users SET deleted_at = NOW() WHERE id = $1 AND deleted_at IS NULL`, + [id] + ) + + return NextResponse.json({ success: true }, { status: 200 }) + } catch (error) { + console.error("Error deleting user:", error) + return NextResponse.json({ error: "Failed to delete user" }, { status: 500 }) + } +} diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts new file mode 100644 index 0000000..32bb235 --- /dev/null +++ b/src/app/api/users/route.ts @@ -0,0 +1,78 @@ +import { NextRequest, NextResponse } from "next/server" +import { query } from "@/lib/db" +import { hashPassword, getSessionUser } from "@/lib/auth" + +export async function GET() { + try { + const result = await query( + `SELECT u.id, u.username, u.email, u.first_name, u.last_name, + u.is_active AS active, u.created_at, + r.name AS role + FROM users u + JOIN user_roles ur ON ur.user_id = u.id + JOIN roles r ON r.id = ur.role_id + WHERE u.deleted_at IS NULL + ORDER BY u.created_at DESC` + ) + const users = result.rows.map((row: any) => ({ + id: row.id, + name: `${row.first_name} ${row.last_name}`, + email: row.email, + role: row.role.toLowerCase(), + active: row.active, + avatar: `https://ui-avatars.com/api/?name=${encodeURIComponent(`${row.first_name}+${row.last_name}`)}&background=1d4ed8&color=fff&size=128`, + createdAt: row.created_at, + })) + return NextResponse.json({ users }, { status: 200 }) + } catch (error) { + console.error("Error fetching users:", error) + return NextResponse.json({ error: "Failed to fetch users" }, { status: 500 }) + } +} + +export async function POST(request: NextRequest) { + try { + const sessionUser = await getSessionUser() + if (!sessionUser) { + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + } + + const { name, email, password, role, active } = await request.json() + if (!name || !email || !password || !role) { + return NextResponse.json({ error: "Name, email, password, and role are required" }, { status: 400 }) + } + + const nameParts = name.trim().split(/\s+/) + const firstName = nameParts[0] + const lastName = nameParts.slice(1).join(" ") || firstName + const username = email.split("@")[0] + const passwordHash = await hashPassword(password) + + const result = await query( + `INSERT INTO users (username, email, password_hash, first_name, last_name, is_active, created_by) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id`, + [username.toLowerCase(), email.toLowerCase(), passwordHash, firstName, lastName, active, sessionUser.id] + ) + + const roleId = ( + await query(`SELECT id FROM roles WHERE LOWER(name) = LOWER($1)`, [role]) + ).rows[0]?.id + + if (roleId) { + await query( + `INSERT INTO user_roles (user_id, role_id, assigned_by) + VALUES ($1, $2, $3)`, + [result.rows[0].id, roleId, sessionUser.id] + ) + } + + return NextResponse.json({ success: true, id: result.rows[0].id }, { status: 201 }) + } catch (error: any) { + console.error("Error creating user:", error) + if (error?.constraint === "uq_users_username" || error?.constraint === "uq_users_email") { + return NextResponse.json({ error: "A user with this email or username already exists" }, { status: 409 }) + } + return NextResponse.json({ error: error?.message || "Failed to create user" }, { status: 500 }) + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx deleted file mode 100644 index 64a14ed..0000000 --- a/src/app/layout.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { Metadata } from "next" -import { Inter } from "next/font/google" -import { ThemeProvider } from "@/providers/theme-provider" -import { Toaster } from "@/components/ui/sonner" -import "./globals.css" - -const inter = Inter({ - variable: "--font-inter", - subsets: ["latin"], -}) - -export const metadata: Metadata = { - title: "Coast IT - CRM", - description: "Customer Relationship Management System", - icons: { - icon: "/logo/CompanyMiniLogo.png", - }, -} - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return ( - - - - {children} - - - - - ) -} diff --git a/src/app/login/layout.tsx b/src/app/login/layout.tsx new file mode 100644 index 0000000..a14e64f --- /dev/null +++ b/src/app/login/layout.tsx @@ -0,0 +1,16 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} From f5d09298a27cd2cee1bf187ce1e65c95abf57976 Mon Sep 17 00:00:00 2001 From: caitlin Date: Thu, 18 Jun 2026 16:57:59 +0200 Subject: [PATCH 3/7] fixed css --- src/app/globals.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/globals.css b/src/app/globals.css index 5615ace..a2f59d6 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -71,4 +71,4 @@ @apply bg-background text-foreground; font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif; } -} + From adbcc4b9afac89d042d5271432f1fcc8f220819e Mon Sep 17 00:00:00 2001 From: Ace Date: Thu, 18 Jun 2026 22:48:03 +0200 Subject: [PATCH 4/7] feat: add system monitor component and API for performance metrics 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 --- database/migrations/003_chat.sql | 80 +++ database/migrations/004_avatar_url.sql | 1 + database/migrations/005_last_read_at.sql | 2 + database/migrations/006_test_leads.sql | 87 +++ src/app/(dashboard)/chats/page.tsx | 407 +++++++---- src/app/(dashboard)/dashboard/page.tsx | 36 +- src/app/(dashboard)/leads/[id]/page.tsx | 180 +++-- src/app/(dashboard)/leads/page.tsx | 44 +- src/app/(dashboard)/profile/page.tsx | 26 +- src/app/(dashboard)/users/page.tsx | 63 +- .../api/conversations/[id]/messages/route.ts | 115 ++++ src/app/api/conversations/[id]/read/route.ts | 27 + src/app/api/conversations/route.ts | 129 ++++ src/app/api/dashboard/route.ts | 207 ++++++ src/app/api/leads/[id]/notes/route.ts | 62 ++ src/app/api/leads/[id]/route.ts | 68 ++ src/app/api/leads/route.ts | 105 +++ src/app/api/system/monitor/route.ts | 29 + src/app/api/users/avatar/route.ts | 25 + src/app/api/users/route.ts | 4 +- src/app/api/users/search/route.ts | 40 ++ src/app/globals.css | 250 +++++++ src/app/layout.tsx | 9 +- src/app/login/layout.tsx | 13 +- src/app/login/page.tsx | 631 +++++++++--------- .../dashboard/leads-per-month-chart.tsx | 85 ++- .../dashboard/stat-card-skeleton.tsx | 4 +- src/components/dashboard/stat-card.tsx | 188 ++++-- src/components/layout/app-shell.tsx | 2 + src/components/layout/sidebar.tsx | 11 +- src/components/layout/system-monitor.tsx | 43 ++ src/components/layout/topbar.tsx | 1 + src/components/notes/note-form.tsx | 17 +- src/data/users.ts | 12 +- src/lib/auth.ts | 27 +- src/middleware.ts | 1 + src/providers/notification-provider.tsx | 29 +- src/types/index.ts | 2 +- 38 files changed, 2321 insertions(+), 741 deletions(-) create mode 100644 database/migrations/003_chat.sql create mode 100644 database/migrations/004_avatar_url.sql create mode 100644 database/migrations/005_last_read_at.sql create mode 100644 database/migrations/006_test_leads.sql create mode 100644 src/app/api/conversations/[id]/messages/route.ts create mode 100644 src/app/api/conversations/[id]/read/route.ts create mode 100644 src/app/api/conversations/route.ts create mode 100644 src/app/api/dashboard/route.ts create mode 100644 src/app/api/leads/[id]/notes/route.ts create mode 100644 src/app/api/leads/[id]/route.ts create mode 100644 src/app/api/leads/route.ts create mode 100644 src/app/api/system/monitor/route.ts create mode 100644 src/app/api/users/avatar/route.ts create mode 100644 src/app/api/users/search/route.ts create mode 100644 src/components/layout/system-monitor.tsx diff --git a/database/migrations/003_chat.sql b/database/migrations/003_chat.sql new file mode 100644 index 0000000..9221490 --- /dev/null +++ b/database/migrations/003_chat.sql @@ -0,0 +1,80 @@ +-- ============================================================================ +-- Chat System: conversations, participants, and messages +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS conversations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS conversation_participants ( + conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (conversation_id, user_id) +); + +CREATE TABLE IF NOT EXISTS messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, + sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + content TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ, + deleted_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id); +CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at); +CREATE INDEX IF NOT EXISTS idx_conversation_participants_user_id ON conversation_participants(user_id); + +-- Seed conversations between superadmin and other users +INSERT INTO conversations (id, created_at) VALUES + ('c0000000-0000-0000-0000-000000000001', NOW() - INTERVAL '2 hours'), + ('c0000000-0000-0000-0000-000000000002', NOW() - INTERVAL '1 hour'), + ('c0000000-0000-0000-0000-000000000003', NOW() - INTERVAL '30 minutes') +ON CONFLICT DO NOTHING; + +-- Add participants (superadmin + each sales/dev user) +INSERT INTO conversation_participants (conversation_id, user_id) VALUES + ('c0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000001'), + ('c0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003'), + ('c0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000001'), + ('c0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000004'), + ('c0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000001'), + ('c0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000002') +ON CONFLICT DO NOTHING; + +-- Seed some messages +INSERT INTO messages (id, conversation_id, sender_id, content, created_at) VALUES + ('00000000-0000-0000-0000-000000000101', 'c0000000-0000-0000-0000-000000000001', + '00000000-0000-0000-0000-000000000003', 'Hey! Just finalized the TechCorp deal.', + NOW() - INTERVAL '2 hours'), + ('00000000-0000-0000-0000-000000000102', 'c0000000-0000-0000-0000-000000000001', + '00000000-0000-0000-0000-000000000001', 'Great work! What were the final terms?', + NOW() - INTERVAL '105 minutes'), + ('00000000-0000-0000-0000-000000000103', 'c0000000-0000-0000-0000-000000000001', + '00000000-0000-0000-0000-000000000003', '3-year enterprise license, 15% discount. They signed this morning.', + NOW() - INTERVAL '100 minutes'), + ('00000000-0000-0000-0000-000000000104', 'c0000000-0000-0000-0000-000000000002', + '00000000-0000-0000-0000-000000000004', 'The API integration tests are passing on staging.', + NOW() - INTERVAL '50 minutes'), + ('00000000-0000-0000-0000-000000000105', 'c0000000-0000-0000-0000-000000000002', + '00000000-0000-0000-0000-000000000001', 'Great, when can we deploy to production?', + NOW() - INTERVAL '45 minutes'), + ('00000000-0000-0000-0000-000000000106', 'c0000000-0000-0000-0000-000000000002', + '00000000-0000-0000-0000-000000000004', 'Tomorrow morning after final review.', + NOW() - INTERVAL '40 minutes'), + ('00000000-0000-0000-0000-000000000107', 'c0000000-0000-0000-0000-000000000003', + '00000000-0000-0000-0000-000000000002', 'New report shows 23% increase in lead conversion this quarter.', + NOW() - INTERVAL '25 minutes'), + ('00000000-0000-0000-0000-000000000108', 'c0000000-0000-0000-0000-000000000003', + '00000000-0000-0000-0000-000000000001', 'That is excellent! Let us discuss in the next team meeting.', + NOW() - INTERVAL '20 minutes') +ON CONFLICT DO NOTHING; + +-- Update conversation timestamps +UPDATE conversations SET updated_at = NOW() - INTERVAL '20 minutes' WHERE id = 'c0000000-0000-0000-0000-000000000003'; +UPDATE conversations SET updated_at = NOW() - INTERVAL '40 minutes' WHERE id = 'c0000000-0000-0000-0000-000000000002'; +UPDATE conversations SET updated_at = NOW() - INTERVAL '100 minutes' WHERE id = 'c0000000-0000-0000-0000-000000000001'; diff --git a/database/migrations/004_avatar_url.sql b/database/migrations/004_avatar_url.sql new file mode 100644 index 0000000..dda99f4 --- /dev/null +++ b/database/migrations/004_avatar_url.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url TEXT; \ No newline at end of file diff --git a/database/migrations/005_last_read_at.sql b/database/migrations/005_last_read_at.sql new file mode 100644 index 0000000..06510fb --- /dev/null +++ b/database/migrations/005_last_read_at.sql @@ -0,0 +1,2 @@ +ALTER TABLE conversation_participants ADD COLUMN IF NOT EXISTS last_read_at TIMESTAMPTZ; +UPDATE conversation_participants SET last_read_at = NOW() WHERE last_read_at IS NULL; \ No newline at end of file diff --git a/database/migrations/006_test_leads.sql b/database/migrations/006_test_leads.sql new file mode 100644 index 0000000..df557da --- /dev/null +++ b/database/migrations/006_test_leads.sql @@ -0,0 +1,87 @@ +-- Test leads - 2026-06-18 +INSERT INTO leads (id, stage_id, assigned_to, company_name, contact_name, email, phone, created_at, updated_at) VALUES +('c0011001-0000-0000-0000-000000000001', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 1', 'Contact 1', 'contact1@test.com', '555-2636', '2025-12-23T22:09:54Z', '2025-12-23T22:09:54Z'), +('c0011001-0000-0000-0000-000000000002', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 2', 'Contact 2', 'contact2@test.com', '555-5691', '2025-12-29T22:09:54Z', '2025-12-29T22:09:54Z'), +('c0011001-0000-0000-0000-000000000003', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 3', 'Contact 3', 'contact3@test.com', '555-6446', '2025-12-24T22:09:54Z', '2025-12-24T22:09:54Z'), +('c0011001-0000-0000-0000-000000000004', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 4', 'Contact 4', 'contact4@test.com', '555-5969', '2026-01-02T22:09:54Z', '2026-01-02T22:09:54Z'), +('c0011001-0000-0000-0000-000000000005', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 5', 'Contact 5', 'contact5@test.com', '555-4200', '2026-01-14T22:09:54Z', '2026-01-14T22:09:54Z'), +('c0011001-0000-0000-0000-000000000006', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 6', 'Contact 6', 'contact6@test.com', '555-8324', '2026-01-03T22:09:54Z', '2026-01-03T22:09:54Z'), +('c0011001-0000-0000-0000-000000000007', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 7', 'Contact 7', 'contact7@test.com', '555-1047', '2025-12-27T22:09:54Z', '2025-12-27T22:09:54Z'), +('c0011001-0000-0000-0000-000000000008', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 8', 'Contact 8', 'contact8@test.com', '555-634', '2026-01-20T22:09:54Z', '2026-01-20T22:09:54Z'), +('c0011001-0000-0000-0000-000000000009', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 9', 'Contact 9', 'contact9@test.com', '555-3245', '2026-01-15T22:09:54Z', '2026-01-15T22:09:54Z'), +('c0011001-0000-0000-0000-000000000010', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 10', 'Contact 10', 'contact10@test.com', '555-3656', '2026-01-12T22:09:54Z', '2026-01-12T22:09:54Z'), +('c0011001-0000-0000-0000-000000000011', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 11', 'Contact 11', 'contact11@test.com', '555-2993', '2026-01-23T22:09:54Z', '2026-01-23T22:09:54Z'), +('c0011001-0000-0000-0000-000000000012', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 12', 'Contact 12', 'contact12@test.com', '555-1634', '2026-02-24T22:09:54Z', '2026-02-24T22:09:54Z'), +('c0011001-0000-0000-0000-000000000013', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 13', 'Contact 13', 'contact13@test.com', '555-2032', '2026-02-10T22:09:54Z', '2026-02-10T22:09:54Z'), +('c0011001-0000-0000-0000-000000000014', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 14', 'Contact 14', 'contact14@test.com', '555-867', '2026-01-31T22:09:54Z', '2026-01-31T22:09:54Z'), +('c0011001-0000-0000-0000-000000000015', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 15', 'Contact 15', 'contact15@test.com', '555-2136', '2026-02-02T22:09:54Z', '2026-02-02T22:09:54Z'), +('c0011001-0000-0000-0000-000000000016', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 16', 'Contact 16', 'contact16@test.com', '555-89', '2026-01-20T22:09:54Z', '2026-01-20T22:09:54Z'), +('c0011001-0000-0000-0000-000000000017', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 17', 'Contact 17', 'contact17@test.com', '555-4274', '2026-01-25T22:09:54Z', '2026-01-25T22:09:54Z'), +('c0011001-0000-0000-0000-000000000018', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 18', 'Contact 18', 'contact18@test.com', '555-4294', '2026-03-16T22:09:54Z', '2026-03-16T22:09:54Z'), +('c0011001-0000-0000-0000-000000000019', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 19', 'Contact 19', 'contact19@test.com', '555-7998', '2025-12-30T22:09:54Z', '2025-12-30T22:09:54Z'), +('c0011001-0000-0000-0000-000000000020', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 20', 'Contact 20', 'contact20@test.com', '555-8557', '2026-02-13T22:09:54Z', '2026-02-13T22:09:54Z'), +('c0011001-0000-0000-0000-000000000021', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 21', 'Contact 21', 'contact21@test.com', '555-5170', '2026-04-11T22:09:54Z', '2026-04-11T22:09:54Z'), +('c0011001-0000-0000-0000-000000000022', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 22', 'Contact 22', 'contact22@test.com', '555-6250', '2025-12-21T22:09:54Z', '2025-12-21T22:09:54Z'), +('c0011001-0000-0000-0000-000000000023', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 23', 'Contact 23', 'contact23@test.com', '555-5611', '2026-01-06T22:09:54Z', '2026-01-06T22:09:54Z'), +('c0011001-0000-0000-0000-000000000024', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 24', 'Contact 24', 'contact24@test.com', '555-3467', '2026-04-16T22:09:54Z', '2026-04-16T22:09:54Z'), +('c0011001-0000-0000-0000-000000000025', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 25', 'Contact 25', 'contact25@test.com', '555-7087', '2026-05-07T22:09:54Z', '2026-05-07T22:09:54Z'), +('c0011001-0000-0000-0000-000000000026', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 26', 'Contact 26', 'contact26@test.com', '555-2945', '2025-12-22T22:09:54Z', '2025-12-22T22:09:54Z'), +('c0011001-0000-0000-0000-000000000027', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 27', 'Contact 27', 'contact27@test.com', '555-9502', '2026-01-09T22:09:54Z', '2026-01-09T22:09:54Z'), +('c0011001-0000-0000-0000-000000000028', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 28', 'Contact 28', 'contact28@test.com', '555-8114', '2026-05-10T22:09:54Z', '2026-05-10T22:09:54Z'), +('c0011001-0000-0000-0000-000000000029', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 29', 'Contact 29', 'contact29@test.com', '555-4926', '2026-03-02T22:09:54Z', '2026-03-02T22:09:54Z'), +('c0011001-0000-0000-0000-000000000030', 'e0000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000003', 'Test Company 30', 'Contact 30', 'contact30@test.com', '555-2421', '2026-03-17T22:09:54Z', '2026-03-17T22:09:54Z'), +('c0011001-0000-0000-0000-000000000031', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 31', 'Contact 31', 'contact31@test.com', '555-7506', '2025-12-21T22:09:54Z', '2025-12-21T22:09:54Z'), +('c0011001-0000-0000-0000-000000000032', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 32', 'Contact 32', 'contact32@test.com', '555-6832', '2025-12-29T22:09:54Z', '2025-12-29T22:09:54Z'), +('c0011001-0000-0000-0000-000000000033', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 33', 'Contact 33', 'contact33@test.com', '555-6548', '2026-01-07T22:09:54Z', '2026-01-07T22:09:54Z'), +('c0011001-0000-0000-0000-000000000034', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 34', 'Contact 34', 'contact34@test.com', '555-8793', '2026-01-02T22:09:54Z', '2026-01-02T22:09:54Z'), +('c0011001-0000-0000-0000-000000000035', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 35', 'Contact 35', 'contact35@test.com', '555-6217', '2026-01-29T22:09:54Z', '2026-01-29T22:09:54Z'), +('c0011001-0000-0000-0000-000000000036', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 36', 'Contact 36', 'contact36@test.com', '555-9206', '2025-12-31T22:09:54Z', '2025-12-31T22:09:54Z'), +('c0011001-0000-0000-0000-000000000037', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 37', 'Contact 37', 'contact37@test.com', '555-1599', '2025-12-26T22:09:54Z', '2025-12-26T22:09:54Z'), +('c0011001-0000-0000-0000-000000000038', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 38', 'Contact 38', 'contact38@test.com', '555-3873', '2025-12-29T22:09:54Z', '2025-12-29T22:09:54Z'), +('c0011001-0000-0000-0000-000000000039', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 39', 'Contact 39', 'contact39@test.com', '555-6122', '2026-01-09T22:09:54Z', '2026-01-09T22:09:54Z'), +('c0011001-0000-0000-0000-000000000040', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 40', 'Contact 40', 'contact40@test.com', '555-832', '2026-01-13T22:09:54Z', '2026-01-13T22:09:54Z'), +('c0011001-0000-0000-0000-000000000041', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 41', 'Contact 41', 'contact41@test.com', '555-5979', '2026-01-11T22:09:54Z', '2026-01-11T22:09:54Z'), +('c0011001-0000-0000-0000-000000000042', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 42', 'Contact 42', 'contact42@test.com', '555-1823', '2025-12-26T22:09:54Z', '2025-12-26T22:09:54Z'), +('c0011001-0000-0000-0000-000000000043', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 43', 'Contact 43', 'contact43@test.com', '555-7102', '2026-02-28T22:09:54Z', '2026-02-28T22:09:54Z'), +('c0011001-0000-0000-0000-000000000044', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 44', 'Contact 44', 'contact44@test.com', '555-5106', '2026-01-22T22:09:54Z', '2026-01-22T22:09:54Z'), +('c0011001-0000-0000-0000-000000000045', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 45', 'Contact 45', 'contact45@test.com', '555-3278', '2026-03-27T22:09:54Z', '2026-03-27T22:09:54Z'), +('c0011001-0000-0000-0000-000000000046', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 46', 'Contact 46', 'contact46@test.com', '555-4973', '2026-04-28T22:09:54Z', '2026-04-28T22:09:54Z'), +('c0011001-0000-0000-0000-000000000047', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 47', 'Contact 47', 'contact47@test.com', '555-3297', '2026-03-01T22:09:54Z', '2026-03-01T22:09:54Z'), +('c0011001-0000-0000-0000-000000000048', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 48', 'Contact 48', 'contact48@test.com', '555-4584', '2026-03-18T22:09:54Z', '2026-03-18T22:09:54Z'), +('c0011001-0000-0000-0000-000000000049', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 49', 'Contact 49', 'contact49@test.com', '555-6788', '2026-03-04T22:09:54Z', '2026-03-04T22:09:54Z'), +('c0011001-0000-0000-0000-000000000050', 'e0000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-000000000003', 'Test Company 50', 'Contact 50', 'contact50@test.com', '555-2734', '2026-01-03T22:09:54Z', '2026-01-03T22:09:54Z'), +('c0011001-0000-0000-0000-000000000051', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 51', 'Contact 51', 'contact51@test.com', '555-2625', '2025-12-19T22:09:54Z', '2025-12-19T22:09:54Z'), +('c0011001-0000-0000-0000-000000000052', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 52', 'Contact 52', 'contact52@test.com', '555-620', '2025-12-21T22:09:54Z', '2025-12-21T22:09:54Z'), +('c0011001-0000-0000-0000-000000000053', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 53', 'Contact 53', 'contact53@test.com', '555-6691', '2025-12-22T22:09:54Z', '2025-12-22T22:09:54Z'), +('c0011001-0000-0000-0000-000000000054', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 54', 'Contact 54', 'contact54@test.com', '555-8847', '2026-01-25T22:09:54Z', '2026-01-25T22:09:54Z'), +('c0011001-0000-0000-0000-000000000055', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 55', 'Contact 55', 'contact55@test.com', '555-2187', '2026-02-02T22:09:54Z', '2026-02-02T22:09:54Z'), +('c0011001-0000-0000-0000-000000000056', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 56', 'Contact 56', 'contact56@test.com', '555-3267', '2026-01-19T22:09:54Z', '2026-01-19T22:09:54Z'), +('c0011001-0000-0000-0000-000000000057', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 57', 'Contact 57', 'contact57@test.com', '555-8119', '2026-03-05T22:09:54Z', '2026-03-05T22:09:54Z'), +('c0011001-0000-0000-0000-000000000058', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 58', 'Contact 58', 'contact58@test.com', '555-3181', '2025-12-19T22:09:54Z', '2025-12-19T22:09:54Z'), +('c0011001-0000-0000-0000-000000000059', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 59', 'Contact 59', 'contact59@test.com', '555-4403', '2026-02-23T22:09:54Z', '2026-02-23T22:09:54Z'), +('c0011001-0000-0000-0000-000000000060', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 60', 'Contact 60', 'contact60@test.com', '555-1407', '2026-04-06T22:09:54Z', '2026-04-06T22:09:54Z'), +('c0011001-0000-0000-0000-000000000061', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 61', 'Contact 61', 'contact61@test.com', '555-2333', '2026-02-27T22:09:54Z', '2026-02-27T22:09:54Z'), +('c0011001-0000-0000-0000-000000000062', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 62', 'Contact 62', 'contact62@test.com', '555-1436', '2026-04-26T22:09:54Z', '2026-04-26T22:09:54Z'), +('c0011001-0000-0000-0000-000000000063', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 63', 'Contact 63', 'contact63@test.com', '555-7135', '2026-04-19T22:09:54Z', '2026-04-19T22:09:54Z'), +('c0011001-0000-0000-0000-000000000064', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 64', 'Contact 64', 'contact64@test.com', '555-9366', '2026-01-22T22:09:54Z', '2026-01-22T22:09:54Z'), +('c0011001-0000-0000-0000-000000000065', 'e0000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-000000000003', 'Test Company 65', 'Contact 65', 'contact65@test.com', '555-7135', '2026-02-18T22:09:54Z', '2026-02-18T22:09:54Z'), +('c0011001-0000-0000-0000-000000000066', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 66', 'Contact 66', 'contact66@test.com', '555-6854', '2025-12-19T22:09:54Z', '2025-12-19T22:09:54Z'), +('c0011001-0000-0000-0000-000000000067', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 67', 'Contact 67', 'contact67@test.com', '555-2322', '2025-12-24T22:09:54Z', '2025-12-24T22:09:54Z'), +('c0011001-0000-0000-0000-000000000068', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 68', 'Contact 68', 'contact68@test.com', '555-2632', '2026-01-13T22:09:54Z', '2026-01-13T22:09:54Z'), +('c0011001-0000-0000-0000-000000000069', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 69', 'Contact 69', 'contact69@test.com', '555-876', '2025-12-29T22:09:54Z', '2025-12-29T22:09:54Z'), +('c0011001-0000-0000-0000-000000000070', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 70', 'Contact 70', 'contact70@test.com', '555-974', '2026-01-15T22:09:54Z', '2026-01-15T22:09:54Z'), +('c0011001-0000-0000-0000-000000000071', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 71', 'Contact 71', 'contact71@test.com', '555-1919', '2026-01-18T22:09:54Z', '2026-01-18T22:09:54Z'), +('c0011001-0000-0000-0000-000000000072', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 72', 'Contact 72', 'contact72@test.com', '555-3034', '2025-12-25T22:09:54Z', '2025-12-25T22:09:54Z'), +('c0011001-0000-0000-0000-000000000073', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 73', 'Contact 73', 'contact73@test.com', '555-1795', '2026-01-27T22:09:54Z', '2026-01-27T22:09:54Z'), +('c0011001-0000-0000-0000-000000000074', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 74', 'Contact 74', 'contact74@test.com', '555-727', '2026-01-08T22:09:54Z', '2026-01-08T22:09:54Z'), +('c0011001-0000-0000-0000-000000000075', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 75', 'Contact 75', 'contact75@test.com', '555-3877', '2026-03-08T22:09:54Z', '2026-03-08T22:09:54Z'), +('c0011001-0000-0000-0000-000000000076', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 76', 'Contact 76', 'contact76@test.com', '555-6384', '2026-01-13T22:09:54Z', '2026-01-13T22:09:54Z'), +('c0011001-0000-0000-0000-000000000077', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 77', 'Contact 77', 'contact77@test.com', '555-5607', '2026-01-12T22:09:54Z', '2026-01-12T22:09:54Z'), +('c0011001-0000-0000-0000-000000000078', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 78', 'Contact 78', 'contact78@test.com', '555-6225', '2026-01-12T22:09:54Z', '2026-01-12T22:09:54Z'), +('c0011001-0000-0000-0000-000000000079', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 79', 'Contact 79', 'contact79@test.com', '555-9288', '2026-04-16T22:09:54Z', '2026-04-16T22:09:54Z'), +('c0011001-0000-0000-0000-000000000080', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 80', 'Contact 80', 'contact80@test.com', '555-8660', '2026-02-16T22:09:54Z', '2026-02-16T22:09:54Z'), +('c0011001-0000-0000-0000-000000000081', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 81', 'Contact 81', 'contact81@test.com', '555-826', '2025-12-28T22:09:54Z', '2025-12-28T22:09:54Z'), +('c0011001-0000-0000-0000-000000000082', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 82', 'Contact 82', 'contact82@test.com', '555-1761', '2026-03-11T22:09:54Z', '2026-03-11T22:09:54Z'), +('c0011001-0000-0000-0000-000000000083', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 83', 'Contact 83', 'contact83@test.com', '555-9860', '2026-05-18T22:09:54Z', '2026-05-18T22:09:54Z'), +('c0011001-0000-0000-0000-000000000084', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 84', 'Contact 84', 'contact84@test.com', '555-1104', '2026-06-01T22:09:54Z', '2026-06-01T22:09:54Z'), +('c0011001-0000-0000-0000-000000000085', 'e0000000-0000-0000-0000-000000000007', '00000000-0000-0000-0000-000000000003', 'Test Company 85', 'Contact 85', 'contact85@test.com', '555-9528', '2026-06-14T22:09:54Z', '2026-06-14T22:09:54Z'); diff --git a/src/app/(dashboard)/chats/page.tsx b/src/app/(dashboard)/chats/page.tsx index 580b980..0af8154 100644 --- a/src/app/(dashboard)/chats/page.tsx +++ b/src/app/(dashboard)/chats/page.tsx @@ -14,7 +14,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" -import { conversations as conversationsData } from "@/data/chats" import { Search, Send, Phone, Video, MoreHorizontal, Paperclip, Smile, Flag, Ban, Trash2, Image, FileIcon, X, Mic, Square, Play, Pause, Check, CheckCheck, @@ -94,7 +93,7 @@ function VoiceMessagePlayer({ src, initialDuration }: { src: string; initialDura export default function ChatsPage() { const { theme } = useTheme() const { user } = useUser() - const [activeChat, setActiveChat] = useState(conversationsData[0]?.id ?? null) + const [activeChat, setActiveChat] = useState(null) const [messageInput, setMessageInput] = useState("") const [showEmojiPicker, setShowEmojiPicker] = useState(false) const [attachments, setAttachments] = useState([]) @@ -103,7 +102,7 @@ export default function ChatsPage() { const [reportDialogOpen, setReportDialogOpen] = useState(false) const [reportReason, setReportReason] = useState("") const [forwardDialogOpen, setForwardDialogOpen] = useState(false) - const [forwardMessage, setForwardMessage] = useState(null) + const [forwardMessage, setForwardMessage] = useState(null) const [forwardSearch, setForwardSearch] = useState("") const [searchQuery, setSearchQuery] = useState("") const [isRecording, setIsRecording] = useState(false) @@ -112,18 +111,17 @@ export default function ChatsPage() { const recordingDurationRef = useRef(0) const [voiceMessages, setVoiceMessages] = useState>(new Map()) const [messageAttachments, setMessageAttachments] = useState>(new Map()) - const [replyingTo, setReplyingTo] = useState(null) - const [editingMessage, setEditingMessage] = useState(null) + const [replyingTo, setReplyingTo] = useState(null) + const [editingMessage, setEditingMessage] = useState(null) const [replyMap, setReplyMap] = useState>(new Map()) - const [messageStatus, setMessageStatus] = useState>(() => { - const map = new Map() - conversationsData.forEach((conv) => - conv.messages.forEach((msg) => { - if (msg.senderId === "user1") map.set(msg.id, "read") - }) - ) - return map - }) + // message status tracking removed — now using msg.read from API + const [conversations, setConversations] = useState([]) + const [conversationMessages, setConversationMessages] = useState>(new Map()) + const [loadingChats, setLoadingChats] = useState(true) + const [searchResults, setSearchResults] = useState([]) + const [searchingUsers, setSearchingUsers] = useState(false) + const [unreadMap, setUnreadMap] = useState>(new Map()) + const [previewAvatarUrl, setPreviewAvatarUrl] = useState(null) const fileInputRef = useRef(null) const textareaRef = useRef(null) const emojiPickerRef = useRef(null) @@ -141,15 +139,79 @@ export default function ChatsPage() { ta.style.height = `${ta.scrollHeight}px` }, []) - const [conversations, setConversations] = useState(conversationsData) if (!user) return null + + const messages = conversationMessages.get(activeChat || "") || [] const conversation = conversations.find((c) => c.id === activeChat) - const otherParticipant = (conv: typeof conversationsData[0]) => - conv.participants.find((p) => p.id !== "user1") ?? conv.participants[0] + const otherParticipant = (conv: any) => conv.otherUser const filteredConversations = searchQuery.trim() ? conversations.filter((conv) => otherParticipant(conv).name.toLowerCase().includes(searchQuery.toLowerCase())) : conversations + // Fetch conversations from API + useEffect(() => { + const fetchConvs = async () => { + try { + const res = await fetch("/api/conversations") + if (res.ok) { + const data = await res.json() + const convs = data.conversations || [] + setConversations(convs) + setUnreadMap(new Map(convs.map((c: any) => [c.id, c.unread]))) + if (convs.length > 0) setActiveChat(convs[0].id) + } + } catch { + // ignore + } finally { + setLoadingChats(false) + } + } + fetchConvs() + }, []) + + // Fetch messages when active chat changes, and mark as read + useEffect(() => { + if (!activeChat) return + const fetchMsgs = async () => { + try { + const res = await fetch(`/api/conversations/${activeChat}/messages`) + if (res.ok) { + const data = await res.json() + setConversationMessages((prev) => { + const next = new Map(prev) + next.set(activeChat, data.messages || []) + return next + }) + } + } catch { + // ignore + } + // Mark conversation as read + fetch(`/api/conversations/${activeChat}/read`, { method: "POST" }).catch(() => {}) + setUnreadMap((prev) => { const m = new Map(prev); m.set(activeChat, 0); return m }) + } + fetchMsgs() + }, [activeChat]) + + // Search users when search query changes + useEffect(() => { + const q = searchQuery.trim() + if (!q) { setSearchResults([]); return } + const timer = setTimeout(async () => { + setSearchingUsers(true) + try { + const res = await fetch(`/api/users/search?q=${encodeURIComponent(q)}`) + if (res.ok) { + const data = await res.json() + const existingIds = new Set(conversations.map((c) => otherParticipant(c).id)) + setSearchResults(data.users.filter((u: any) => !existingIds.has(u.id))) + } + } catch { /* ignore */ } + setSearchingUsers(false) + }, 300) + return () => clearTimeout(timer) + }, [searchQuery, conversations]) + useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (emojiPickerRef.current && !emojiPickerRef.current.contains(e.target as Node)) { @@ -202,79 +264,77 @@ export default function ChatsPage() { setAttachments((prev) => prev.filter((_, i) => i !== index)) } - const addMessageToChat = (content: string, voice?: { url: string; duration: number }, replyTo?: { senderName: string; content: string }, attachments?: { name: string; type: string; url: string }[]) => { - const id = crypto.randomUUID() - const newMessage = { - id, - conversationId: activeChat!, - senderId: "user1", - senderName: "Sarah Chen", - senderAvatar: "SC", - content, - timestamp: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), - } - setConversations((prev) => - prev.map((conv) => - conv.id === activeChat - ? { - ...conv, - messages: [...conv.messages, newMessage], - lastMessage: voice ? "🎤 Voice note" : (replyTo ? `↪ ${content}` : content), - lastMessageTime: "Just now", - unread: 0, - } - : conv - ) - ) - setMessageStatus((prev) => { - const next = new Map(prev) - next.set(id, "sent") - return next - }) - setTimeout(() => { - setMessageStatus((prev) => { + const addMessageToChat = async (content: string, voice?: { url: string; duration: number }, replyTo?: { senderName: string; content: string }, fileAttachments?: { name: string; type: string; url: string }[]) => { + if (!activeChat || !user) return + const payload = voice ? "[Voice message]" : content + try { + const res = await fetch(`/api/conversations/${activeChat}/messages`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content: payload }), + }) + if (!res.ok) return + const data = await res.json() + const msg = data.message + setConversationMessages((prev) => { const next = new Map(prev) - if (next.get(id) === "sent") next.set(id, "delivered") + const existing = next.get(activeChat) || [] + next.set(activeChat, [...existing, msg]) return next }) - }, 1000) - if (voice) { - setVoiceMessages((prev) => { - const next = new Map(prev) - next.set(id, voice) - return next - }) - } - if (replyTo) { - setReplyMap((prev) => { - const next = new Map(prev) - next.set(id, { repliedToSender: replyTo.senderName, repliedToContent: replyTo.content }) - return next - }) - } - if (attachments && attachments.length > 0) { - setMessageAttachments((prev) => { - const next = new Map(prev) - next.set(id, attachments) - return next + setConversations((prev) => { + const updated = prev.map((conv) => + conv.id === activeChat + ? { ...conv, lastMessage: content, lastMessageTime: "Just now" } + : conv + ) + const idx = updated.findIndex((c) => c.id === activeChat) + if (idx > 0) { + const [item] = updated.splice(idx, 1) + updated.unshift(item) + } + return updated }) + setUnreadMap((prev) => { const m = new Map(prev); m.set(activeChat, 0); return m }) + await fetch(`/api/conversations/${activeChat}/read`, { method: "POST" }) + if (voice) { + setVoiceMessages((prev) => { + const next = new Map(prev) + next.set(msg.id, voice) + return next + }) + } + if (replyTo) { + setReplyMap((prev) => { + const next = new Map(prev) + next.set(msg.id, { repliedToSender: replyTo.senderName, repliedToContent: replyTo.content }) + return next + }) + } + if (fileAttachments && fileAttachments.length > 0) { + setMessageAttachments((prev) => { + const next = new Map(prev) + next.set(msg.id, fileAttachments) + return next + }) + } + } catch { + toast.error("Failed to send message") } } - const handleSend = (e: React.FormEvent) => { + const handleSend = async (e: React.FormEvent) => { e.preventDefault() if (!messageInput.trim() && attachments.length === 0) return if (editingMessage) { - setConversations((prev) => - prev.map((conv) => ({ - ...conv, - messages: conv.messages.map((m) => - m.id === editingMessage.id ? { ...m, content: messageInput.trim() } : m - ), - lastMessage: conv.id === activeChat && conv.messages.some((m) => m.id === editingMessage.id) - ? messageInput.trim() : conv.lastMessage, - })) - ) + setConversationMessages((prev) => { + const next = new Map(prev) + const msgs = (next.get(activeChat || "") || []).map((m) => + m.id === editingMessage.id ? { ...m, content: messageInput.trim() } : m + ) + next.set(activeChat || "", msgs) + return next + }) setEditingMessage(null) setMessageInput("") setTimeout(() => autoResizeTextarea(), 0) @@ -284,11 +344,11 @@ export default function ChatsPage() { ? attachments.map((f) => ({ name: f.name, type: f.type, url: URL.createObjectURL(f) })) : undefined if (replyingTo) { - const replySender = replyingTo.senderId === "user1" ? user.name : otherParticipant(conversation!).name - addMessageToChat(messageInput.trim(), undefined, { senderName: replySender, content: replyingTo.content }, fileAttachments) + const replySender = replyingTo.senderId === user.id ? user.name : otherParticipant(conversation!).name + await addMessageToChat(messageInput.trim(), undefined, { senderName: replySender, content: replyingTo.content }, fileAttachments) setReplyingTo(null) } else { - addMessageToChat(messageInput.trim(), undefined, undefined, fileAttachments) + await addMessageToChat(messageInput.trim(), undefined, undefined, fileAttachments) } setMessageInput("") setTimeout(() => autoResizeTextarea(), 0) @@ -296,12 +356,18 @@ export default function ChatsPage() { } const handleDeleteMessage = (messageId: string) => { + setConversationMessages((prev) => { + const next = new Map(prev) + const msgs = (next.get(activeChat || "") || []).filter((m) => m.id !== messageId) + next.set(activeChat || "", msgs) + return next + }) setConversations((prev) => - prev.map((conv) => ({ - ...conv, - messages: conv.messages.filter((m) => m.id !== messageId), - lastMessage: conv.messages.filter((m) => m.id !== messageId).at(-1)?.content ?? conv.lastMessage, - })) + prev.map((conv) => + conv.id === activeChat + ? { ...conv, lastMessage: conversationMessages.get(activeChat || "")?.filter((m) => m.id !== messageId).at(-1)?.content ?? conv.lastMessage } + : conv + ) ) toast.success("Message deleted") } @@ -405,16 +471,30 @@ export default function ChatsPage() { const person = otherParticipant(conv) const isActive = conv.id === activeChat return ( - ) })} - {filteredConversations.length === 0 && searchQuery && ( + {searchResults.length > 0 && ( + <> + {searchQuery &&
New conversation
} + {searchResults.map((person: any) => ( + + ))} + + )} + {filteredConversations.length === 0 && searchResults.length === 0 && searchQuery && !searchingUsers && (
No conversations found
)} @@ -451,8 +571,9 @@ export default function ChatsPage() { {/* Chat header */}
- - {otherParticipant(conversation).avatar} + setPreviewAvatarUrl(otherParticipant(conversation).avatar)}> + + {otherParticipant(conversation).name?.charAt(0) || "?"}

{otherParticipant(conversation).name}

@@ -493,15 +614,15 @@ export default function ChatsPage() { {/* Messages */}
- {conversation.messages.map((msg) => { - const isMe = msg.senderId === "user1" + {messages.map((msg) => { + const isMe = msg.senderId === user?.id const voiceUrl = voiceMessages.get(msg.id) return (
- - {isMe ? : null} + setPreviewAvatarUrl(msg.senderAvatar)}> + - {msg.senderAvatar} + {msg.senderName?.charAt(0) || "?"}
@@ -608,12 +729,9 @@ export default function ChatsPage() { {msg.timestamp} {isMe && ( - (() => { - const status = messageStatus.get(msg.id) - if (status === "read") return - if (status === "delivered") return - return - })() + msg.read + ? + : )}
@@ -649,7 +767,7 @@ export default function ChatsPage() {

- Replying to {replyingTo.senderId === "user1" ? "yourself" : otherParticipant(conversation).name} + Replying to {replyingTo.senderId === user?.id ? "yourself" : otherParticipant(conversation).name}

{replyingTo.content}

@@ -690,7 +808,7 @@ export default function ChatsPage() {
) : ( <> -