mirror of
https://git.coastit.co.za/caitlin/CRM_ENVR.git
synced 2026-07-11 19:47:04 +02:00
adbcc4b9af
feat: implement conversations API with message retrieval and posting feat: add avatar URL handling for users and update user role definitions feat: create chat system tables in the database with initial seed data fix: update user roles from "sales_user" to "sales" for consistency chore: add middleware for system API route access fixed fuckups on john's side, added a benchmark Made Graphs actualy load from database and realtime data, graphs will update and percentages will be mathematically made, and made realtime added chatcart edits, made graphs glow. added in some little small home feeling fuctions added in a slide show, in login with qoutes from creators because i want to leave a print on it saying we made this shit, we built it brick for brick login page added qoutes some random, and made them shuffle from left to right one dissapears other come in adding shape to the textbox for the qoutes Fixed my chat fuckups when it comes to chats
246 lines
8.3 KiB
TypeScript
246 lines
8.3 KiB
TypeScript
"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 { useNotifications } from "@/providers/notification-provider";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Search,
|
|
Bell,
|
|
Sun,
|
|
Moon,
|
|
Menu,
|
|
LogOut,
|
|
User,
|
|
Settings,
|
|
CheckCheck,
|
|
Trash2,
|
|
} from "lucide-react";
|
|
|
|
interface TopbarProps {
|
|
onMenuClick: () => void;
|
|
}
|
|
|
|
export function Topbar({ onMenuClick }: TopbarProps) {
|
|
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;
|
|
|
|
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 (
|
|
<header className="sticky top-0 z-20 flex h-16 items-center gap-4 border-b bg-background px-4 lg:px-6">
|
|
{/* Logo */}
|
|
<div className="flex items-center gap-2 mr-2 shrink-0">
|
|
<div className="w-3 h-3 rounded-full bg-[#0d9488]" />
|
|
<span className="text-[#0d9488] font-bold text-sm tracking-wide">
|
|
CRM
|
|
</span>
|
|
</div>
|
|
|
|
{/* Mobile menu button */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="lg:hidden"
|
|
onClick={onMenuClick}
|
|
>
|
|
<Menu className="h-5 w-5" />
|
|
</Button>
|
|
|
|
{/* Search */}
|
|
<div className="relative hidden flex-1 sm:block md:w-80">
|
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Search leads, companies..."
|
|
className="h-9 w-full max-w-sm pl-9 bg-muted/50"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex flex-1 items-center justify-end gap-3">
|
|
{/* Mobile search toggle */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="sm:hidden"
|
|
onClick={() => setSearchOpen(!searchOpen)}
|
|
>
|
|
<Search className="h-5 w-5" />
|
|
</Button>
|
|
|
|
{/* Theme toggle */}
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
|
className="text-muted-foreground"
|
|
>
|
|
{mounted ? (
|
|
theme === "dark" ? (
|
|
<Sun className="h-5 w-5" />
|
|
) : (
|
|
<Moon className="h-5 w-5" />
|
|
)
|
|
) : (
|
|
<div className="h-5 w-5" />
|
|
)}
|
|
</Button>
|
|
|
|
{/* Notifications */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="relative text-muted-foreground"
|
|
>
|
|
<Bell className="h-5 w-5" />
|
|
<span className="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[10px] font-medium text-primary-foreground">
|
|
3
|
|
</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-80">
|
|
<DropdownMenuLabel className="flex items-center justify-between">
|
|
<span>Notifications</span>
|
|
{notifications.length > 0 && (
|
|
<button
|
|
onClick={markAllAsRead}
|
|
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
|
>
|
|
<CheckCheck className="h-3 w-3" />
|
|
Mark all read
|
|
</button>
|
|
)}
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
{notifications.length === 0 ? (
|
|
<div className="py-6 text-center text-sm text-muted-foreground">
|
|
No notifications
|
|
</div>
|
|
) : (
|
|
<div className="max-h-[360px] space-y-1 overflow-y-auto p-2">
|
|
{notifications.slice(0, 20).map((n) => (
|
|
<div
|
|
key={n.id}
|
|
role="button"
|
|
tabIndex={0}
|
|
onClick={() => {
|
|
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" : ""}`}
|
|
>
|
|
<div
|
|
className={`mt-2 h-2 w-2 shrink-0 rounded-full ${n.read ? "bg-transparent" : "bg-primary"}`}
|
|
/>
|
|
<div className="flex-1 space-y-0.5">
|
|
<p className="text-sm font-medium">{n.title}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{n.description}
|
|
</p>
|
|
<p className="text-[10px] text-muted-foreground/60">
|
|
{formatTimeAgo(n.timestamp)}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
dismiss(n.id);
|
|
}}
|
|
className="mt-1 shrink-0 text-muted-foreground/40 hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{/* User profile */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" className="relative h-9 gap-2 pl-2 pr-3">
|
|
<Avatar className="h-7 w-7">
|
|
<AvatarImage src={user.avatar} />
|
|
<AvatarFallback>{initials}</AvatarFallback>
|
|
</Avatar>
|
|
<span className="hidden text-sm font-medium md:inline-block">
|
|
{user.name}
|
|
</span>
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
<DropdownMenuLabel className="font-normal">
|
|
<div className="flex flex-col space-y-1">
|
|
<p className="text-sm font-medium">{user.name}</p>
|
|
<p className="text-xs text-muted-foreground">{user.email}</p>
|
|
<Badge
|
|
variant="secondary"
|
|
className="mt-1 w-fit text-xs capitalize"
|
|
>
|
|
{user.role}
|
|
</Badge>
|
|
</div>
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => router.push("/profile")}>
|
|
<User className="mr-2 h-4 w-4" />
|
|
Profile
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => router.push("/settings")}>
|
|
<Settings className="mr-2 h-4 w-4" />
|
|
Settings
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem className="text-destructive" onClick={logout}>
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
Log out
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|