Added in Logo's and worked on Avatars and Chats

This commit is contained in:
Ace
2026-06-17 16:26:32 +02:00
parent 4898bf7142
commit d6d784cef3
19 changed files with 1135 additions and 35 deletions
+401
View File
@@ -0,0 +1,401 @@
"use client"
import { useState, useRef, useCallback, useEffect } from "react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
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, File, X,
} from "lucide-react"
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
DialogFooter, DialogClose,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { useTheme } from "next-themes"
import { useUser } from "@/providers/user-provider"
import { toast } from "sonner"
import data from "@emoji-mart/data"
import Picker from "@emoji-mart/react"
export default function ChatsPage() {
const { theme } = useTheme()
const { user } = useUser()
const [activeChat, setActiveChat] = useState(conversationsData[0]?.id ?? null)
const [messageInput, setMessageInput] = useState("")
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [attachments, setAttachments] = useState<File[]>([])
const [panelWidth, setPanelWidth] = useState(320)
const [isResizing, setIsResizing] = useState(false)
const [reportDialogOpen, setReportDialogOpen] = useState(false)
const [reportReason, setReportReason] = useState("")
const fileInputRef = useRef<HTMLInputElement>(null)
const emojiPickerRef = useRef<HTMLDivElement>(null)
const resizeStartRef = useRef({ x: 0, width: 0 })
const [conversations, setConversations] = useState(conversationsData)
const conversation = conversations.find((c) => c.id === activeChat)
const otherParticipant = (conv: typeof conversationsData[0]) =>
conv.participants.find((p) => p.id !== "user1") ?? conv.participants[0]
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (emojiPickerRef.current && !emojiPickerRef.current.contains(e.target as Node)) {
setShowEmojiPicker(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => document.removeEventListener("mousedown", handleClickOutside)
}, [])
const handleResizeStart = useCallback((e: React.MouseEvent) => {
e.preventDefault()
setIsResizing(true)
resizeStartRef.current = { x: e.clientX, width: panelWidth }
}, [panelWidth])
useEffect(() => {
if (!isResizing) return
const handleMouseMove = (e: MouseEvent) => {
const delta = e.clientX - resizeStartRef.current.x
const newWidth = Math.max(240, Math.min(560, resizeStartRef.current.width + delta))
setPanelWidth(newWidth)
}
const handleMouseUp = () => setIsResizing(false)
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("mouseup", handleMouseUp)
return () => {
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("mouseup", handleMouseUp)
}
}, [isResizing])
const handleEmojiSelect = (emoji: { native: string }) => {
setMessageInput((prev) => prev + emoji.native)
setShowEmojiPicker(false)
}
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files ?? [])
setAttachments((prev) => [...prev, ...files])
if (e.target) e.target.value = ""
}
const removeAttachment = (index: number) => {
setAttachments((prev) => prev.filter((_, i) => i !== index))
}
const handleSend = (e: React.FormEvent) => {
e.preventDefault()
if (!messageInput.trim() && attachments.length === 0) return
const newMessage = {
id: crypto.randomUUID(),
conversationId: activeChat!,
senderId: "user1",
senderName: "Sarah Chen",
senderAvatar: "SC",
content: messageInput.trim(),
timestamp: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
}
setConversations((prev) =>
prev.map((conv) =>
conv.id === activeChat
? {
...conv,
messages: [...conv.messages, newMessage],
lastMessage: newMessage.content,
lastMessageTime: "Just now",
unread: 0,
}
: conv
)
)
setMessageInput("")
setAttachments([])
}
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return bytes + " B"
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
}
const isImageFile = (file: File) => file.type.startsWith("image/")
return (
<div className="flex h-[calc(100vh-8rem)] -m-4 lg:-m-6 rounded-lg border bg-card overflow-hidden">
{/* Conversations list - left panel */}
<div
className="flex flex-col border-r shrink-0 overflow-hidden"
style={{ width: panelWidth }}
>
<div className="p-4 border-b space-y-3">
<h2 className="text-lg font-semibold">Chats</h2>
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input placeholder="Search conversations..." className="h-9 pl-9" />
</div>
</div>
<ScrollArea className="flex-1">
{conversations.map((conv) => {
const person = otherParticipant(conv)
const isActive = conv.id === activeChat
return (
<button
key={conv.id}
onClick={() => setActiveChat(conv.id)}
className={cn(
"w-full flex items-start gap-3 p-4 text-left transition-colors hover:bg-muted/50",
isActive && "bg-muted"
)}
>
<Avatar className="h-10 w-10 shrink-0">
<AvatarFallback>{person.avatar}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium truncate">{person.name}</span>
<span className="text-xs text-muted-foreground shrink-0">{conv.lastMessageTime}</span>
</div>
<p className="text-xs text-muted-foreground truncate mt-0.5">{conv.lastMessage}</p>
</div>
{conv.unread > 0 && (
<span className="shrink-0 flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-[10px] font-medium text-primary-foreground">
{conv.unread}
</span>
)}
</button>
)
})}
</ScrollArea>
</div>
{/* Resize handle */}
<div
className="w-1.5 cursor-col-resize shrink-0 relative group hover:bg-primary/20 transition-colors"
onMouseDown={handleResizeStart}
>
<div className="absolute inset-y-0 -left-1 -right-1" />
</div>
{/* Chat area - right panel */}
{conversation ? (
<div className="flex-1 flex flex-col min-w-0">
{/* Chat header */}
<div className="flex items-center justify-between gap-4 px-6 h-16 border-b shrink-0">
<div className="flex items-center gap-3 min-w-0">
<Avatar className="h-9 w-9 shrink-0">
<AvatarFallback>{otherParticipant(conversation).avatar}</AvatarFallback>
</Avatar>
<div className="min-w-0">
<p className="text-sm font-medium truncate">{otherParticipant(conversation).name}</p>
<p className="text-xs text-muted-foreground truncate">{otherParticipant(conversation).role}</p>
</div>
</div>
<div className="flex items-center gap-1 shrink-0">
<Button
variant="ghost" size="icon" className="h-8 w-8"
onClick={() => toast.info("Voice calling coming soon")}
>
<Phone className="h-4 w-4" />
</Button>
<Button
variant="ghost" size="icon" className="h-8 w-8"
onClick={() => toast.info("Video calling coming soon")}
>
<Video className="h-4 w-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setReportDialogOpen(true)}>
<Flag className="mr-2 h-4 w-4" /> Report
</DropdownMenuItem>
<DropdownMenuItem onClick={() => toast.info("Blocked")}>
<Ban className="mr-2 h-4 w-4" /> Block
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
onClick={() => toast.info("Conversation deleted")}
>
<Trash2 className="mr-2 h-4 w-4" /> Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* Messages */}
<ScrollArea className="flex-1 p-6">
<div className="space-y-4">
{conversation.messages.map((msg) => {
const isMe = msg.senderId === "user1"
return (
<div key={msg.id} className={cn("flex gap-3", isMe && "flex-row-reverse")}>
<Avatar className="h-8 w-8 mt-0.5 shrink-0">
{isMe ? <AvatarImage src={user.avatar} /> : null}
<AvatarFallback className={cn("text-xs", isMe && "bg-primary text-primary-foreground")}>
{msg.senderAvatar}
</AvatarFallback>
</Avatar>
<div className={cn("max-w-[70%]", isMe && "items-end flex flex-col")}>
<div
className={cn(
"rounded-2xl px-4 py-2.5 text-sm",
isMe ? "bg-primary text-primary-foreground rounded-tr-sm" : "bg-muted rounded-tl-sm"
)}
>
{msg.content}
</div>
<span className="text-[10px] text-muted-foreground mt-1 px-1">{msg.timestamp}</span>
</div>
</div>
)
})}
</div>
</ScrollArea>
{/* Input */}
<div className="p-4 border-t shrink-0 space-y-2">
{attachments.length > 0 && (
<div className="flex flex-wrap gap-2">
{attachments.map((file, i) => (
<div
key={i}
className="flex items-center gap-2 rounded-lg border bg-muted/50 px-3 py-1.5 text-sm max-w-[200px]"
>
{isImageFile(file) ? (
<Image className="h-4 w-4 shrink-0 text-muted-foreground" />
) : (
<File className="h-4 w-4 shrink-0 text-muted-foreground" />
)}
<span className="truncate">{file.name}</span>
<span className="text-xs text-muted-foreground shrink-0">{formatFileSize(file.size)}</span>
<Button
variant="ghost" size="icon" className="h-5 w-5 -mr-1 shrink-0"
onClick={() => removeAttachment(i)}
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
<form onSubmit={handleSend} className="flex items-center gap-2">
<input
ref={fileInputRef}
type="file"
multiple
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt"
className="hidden"
onChange={handleFileSelect}
/>
<Button
type="button" variant="ghost" size="icon" className="h-9 w-9 shrink-0"
onClick={() => fileInputRef.current?.click()}
>
<Paperclip className="h-4 w-4 text-muted-foreground" />
</Button>
<div className="relative flex-1">
<Input
value={messageInput}
onChange={(e) => setMessageInput(e.target.value)}
placeholder="Type a message..."
className="h-9 w-full pr-9"
/>
<div className="absolute right-0 top-0 bottom-0 flex items-center pr-1" ref={emojiPickerRef}>
<Button
type="button" variant="ghost" size="icon" className="h-7 w-7 shrink-0"
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
>
<Smile className="h-4 w-4 text-muted-foreground" />
</Button>
{showEmojiPicker && (
<div className="absolute bottom-full right-0 mb-2 z-50">
<Picker
data={data}
onEmojiSelect={handleEmojiSelect}
theme={theme === "dark" ? "dark" : "light"}
previewPosition="none"
skinTonePosition="none"
set="native"
maxFrequentRows={2}
/>
</div>
)}
</div>
</div>
<Button type="submit" size="icon" className="h-9 w-9 shrink-0" disabled={!messageInput.trim() && attachments.length === 0}>
<Send className="h-4 w-4" />
</Button>
</form>
</div>
</div>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<p>Select a conversation to start chatting</p>
</div>
)}
{/* Resize overlay */}
{isResizing && <div className="fixed inset-0 z-50 cursor-col-resize" />}
{/* Report dialog */}
<Dialog open={reportDialogOpen} onOpenChange={setReportDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Report {conversation ? otherParticipant(conversation).name : "User"}</DialogTitle>
<DialogDescription>
Let us know why you're reporting this conversation.
</DialogDescription>
</DialogHeader>
<div className="space-y-3 py-2">
<Label htmlFor="reason">Reason</Label>
<Textarea
id="reason"
placeholder="Describe the issue..."
value={reportReason}
onChange={(e) => setReportReason(e.target.value)}
rows={4}
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button
onClick={() => {
toast.success("Report submitted")
setReportReason("")
setReportDialogOpen(false)
}}
disabled={!reportReason.trim()}
>
Submit Report
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}
+6 -1
View File
@@ -1,11 +1,16 @@
"use client"
import { AppShell } from "@/components/layout/app-shell"
import { UserProvider } from "@/providers/user-provider"
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return <AppShell>{children}</AppShell>
return (
<UserProvider>
<AppShell>{children}</AppShell>
</UserProvider>
)
}
+123
View File
@@ -0,0 +1,123 @@
"use client"
import { useRef } from "react"
import { PageHeader } from "@/components/shared/page-header"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { useUser } from "@/providers/user-provider"
import { Mail, Calendar, Shield, Activity, Camera } from "lucide-react"
import { toast } from "sonner"
export default function ProfilePage() {
const { user, updateAvatar } = useUser()
const fileInputRef = useRef<HTMLInputElement>(null)
const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
const validTypes = ["image/png", "image/jpeg"]
if (!validTypes.includes(file.type)) {
toast.error("Only PNG and JPEG files are allowed")
return
}
const url = URL.createObjectURL(file)
updateAvatar(url)
toast.success("Avatar updated")
}
return (
<div className="space-y-6">
<PageHeader title="Profile" description="Your account information" />
<div className="grid gap-6 lg:grid-cols-3">
<Card className="lg:col-span-1">
<CardContent className="flex flex-col items-center pt-8">
<div className="relative">
<Avatar className="h-24 w-24">
<AvatarImage src={user.avatar} />
<AvatarFallback className="text-2xl">{user.name.split(" ").map((n) => n[0]).join("")}</AvatarFallback>
</Avatar>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="absolute inset-0 flex items-center justify-center rounded-full bg-black/40 opacity-0 transition-opacity hover:opacity-100"
>
<Camera className="h-6 w-6 text-white" />
</button>
<input
ref={fileInputRef}
type="file"
accept=".png,.jpeg,.jpg"
className="hidden"
onChange={handleAvatarChange}
/>
</div>
<h2 className="mt-4 text-xl font-semibold">{user.name}</h2>
<Badge variant="secondary" className="mt-1 capitalize">
{user.role}
</Badge>
<div className="mt-6 flex w-full flex-col gap-3 text-sm">
<div className="flex items-center gap-3 rounded-lg bg-muted/50 px-4 py-3">
<Mail className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">{user.email}</span>
</div>
<div className="flex items-center gap-3 rounded-lg bg-muted/50 px-4 py-3">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">
Joined {new Date(user.createdAt).toLocaleDateString("en-US", { month: "long", year: "numeric" })}
</span>
</div>
<div className="flex items-center gap-3 rounded-lg bg-muted/50 px-4 py-3">
<Shield className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground capitalize">{user.role} access</span>
</div>
<div className="flex items-center gap-3 rounded-lg bg-muted/50 px-4 py-3">
<Activity className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">{user.active ? "Active" : "Inactive"}</span>
</div>
</div>
</CardContent>
</Card>
<div className="lg:col-span-2 space-y-6">
<Card>
<CardHeader>
<CardTitle>Account Details</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Full Name</span>
<p className="text-sm font-medium">{user.name}</p>
</div>
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Email</span>
<p className="text-sm font-medium">{user.email}</p>
</div>
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Role</span>
<p className="text-sm font-medium capitalize">{user.role}</p>
</div>
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Status</span>
<p className="text-sm font-medium">{user.active ? "Active" : "Inactive"}</p>
</div>
<div className="space-y-1">
<span className="text-xs text-muted-foreground">Member Since</span>
<p className="text-sm font-medium">
{new Date(user.createdAt).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}
</p>
</div>
<div className="space-y-1">
<span className="text-xs text-muted-foreground">User ID</span>
<p className="text-sm font-medium font-mono">{user.id}</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
)
}
+4 -1
View File
@@ -10,8 +10,11 @@ const inter = Inter({
})
export const metadata: Metadata = {
title: "Coastal IT - CRM",
title: "Coast IT - CRM",
description: "Customer Relationship Management System",
icons: {
icon: "/logo/CompanyMiniLogo.png",
},
}
export default function RootLayout({
+11 -7
View File
@@ -35,9 +35,11 @@ export default function LoginPage() {
<div className="relative z-10">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-white/20 backdrop-blur-sm">
<span className="text-lg font-bold">C</span>
</div>
<img
src="/logo/CompanyLogo.png"
alt={COMPANY_NAME}
className="h-10 w-10 rounded-xl object-contain"
/>
<span className="text-xl font-semibold">{COMPANY_NAME}</span>
</div>
</div>
@@ -72,7 +74,7 @@ export default function LoginPage() {
<div className="h-10 w-10 rounded-full bg-white/20" />
<div>
<p className="text-sm font-medium">Marcus Johnson</p>
<p className="text-xs text-white/60">Sales Lead, Coastal IT</p>
<p className="text-xs text-white/60">Sales Lead, Coast IT</p>
</div>
</div>
</motion.div>
@@ -93,9 +95,11 @@ export default function LoginPage() {
>
{/* Mobile logo */}
<div className="flex flex-col items-center gap-3 lg:hidden">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary">
<span className="text-xl font-bold text-primary-foreground">C</span>
</div>
<img
src="/logo/CompanyLogo.png"
alt={COMPANY_NAME}
className="h-12 w-12 rounded-xl object-contain"
/>
<span className="text-xl font-semibold">{COMPANY_NAME}</span>
</div>
+30 -12
View File
@@ -6,6 +6,7 @@ import { usePathname } from "next/navigation"
import { motion, AnimatePresence } from "framer-motion"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import {
LayoutDashboard,
@@ -15,12 +16,18 @@ import {
ChevronRight,
Building2,
PanelLeftClose,
MessageSquare,
} from "lucide-react"
import { COMPANY_NAME } from "@/lib/constants"
import { useUser } from "@/providers/user-provider"
import { conversations as conversationsData } from "@/data/chats"
const totalUnread = conversationsData.reduce((sum, c) => sum + c.unread, 0)
const navItems = [
{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
{ href: "/leads", label: "Leads", icon: Users },
{ href: "/chats", label: "Chats", icon: MessageSquare },
{ href: "/users", label: "Users", icon: Building2 },
{ href: "/settings", label: "Settings", icon: Settings },
]
@@ -34,6 +41,8 @@ interface SidebarProps {
export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: SidebarProps) {
const pathname = usePathname()
const { user } = useUser()
const initials = user.name.split(" ").map((n) => n[0]).join("")
const sidebarContent = (
<div
@@ -45,9 +54,11 @@ export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: Side
{/* Logo */}
<div className={cn("flex h-16 items-center border-b border-sidebar-border px-4", collapsed ? "justify-center" : "justify-between")}>
<Link href="/" className="flex items-center gap-3 overflow-hidden">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary">
<span className="text-sm font-bold text-primary-foreground">C</span>
</div>
<img
src="/logo/CompanyLogo.png"
alt={COMPANY_NAME}
className="h-8 w-8 shrink-0 rounded-lg object-contain"
/>
<AnimatePresence mode="wait">
{!collapsed && (
<motion.span
@@ -85,13 +96,18 @@ export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: Side
<Link
href={item.href}
className={cn(
"flex h-10 w-10 items-center justify-center rounded-lg transition-colors",
"relative flex h-10 w-10 items-center justify-center rounded-lg transition-colors",
isActive
? "bg-sidebar-primary text-sidebar-primary-foreground"
: "text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
)}
>
<item.icon className="h-5 w-5" />
{item.label === "Chats" && totalUnread > 0 && (
<span className="absolute -right-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-medium text-destructive-foreground">
{totalUnread}
</span>
)}
</Link>
</TooltipTrigger>
<TooltipContent side="right" className="ml-2">
@@ -141,17 +157,19 @@ export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: Side
{/* User info */}
<div className={cn("border-t border-sidebar-border p-3", collapsed && "flex justify-center")}>
{collapsed ? (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-sidebar-accent">
<span className="text-sm font-medium text-sidebar-accent-foreground">SC</span>
</div>
<Avatar className="h-10 w-10">
<AvatarImage src={user.avatar} />
<AvatarFallback className="text-sm font-medium">{initials}</AvatarFallback>
</Avatar>
) : (
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-sidebar-accent">
<span className="text-sm font-medium text-sidebar-accent-foreground">SC</span>
</div>
<Avatar className="h-9 w-9">
<AvatarImage src={user.avatar} />
<AvatarFallback className="text-sm font-medium">{initials}</AvatarFallback>
</Avatar>
<div className="flex-1 overflow-hidden">
<p className="text-sm font-medium truncate">Sarah Chen</p>
<p className="text-xs text-sidebar-foreground/60 truncate">Admin</p>
<p className="text-sm font-medium truncate">{user.name}</p>
<p className="text-xs text-sidebar-foreground/60 truncate capitalize">{user.role}</p>
</div>
</div>
)}
+13 -8
View File
@@ -1,11 +1,13 @@
"use client"
import { useState } 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 {
DropdownMenu,
DropdownMenuContent,
@@ -31,8 +33,11 @@ interface TopbarProps {
}
export function Topbar({ onMenuClick }: TopbarProps) {
const router = useRouter()
const { theme, setTheme } = useTheme()
const { user } = useUser()
const [searchOpen, setSearchOpen] = useState(false)
const initials = user.name.split(" ").map((n) => n[0]).join("")
return (
<header className="sticky top-0 z-20 flex h-16 items-center gap-4 border-b bg-background px-4 lg:px-6">
@@ -107,26 +112,26 @@ export function Topbar({ onMenuClick }: TopbarProps) {
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-9 gap-2 pl-2 pr-3">
<Avatar className="h-7 w-7">
<AvatarImage src="https://ui-avatars.com/api/?name=Sarah+Chen&background=1d4ed8&color=fff&size=64" />
<AvatarFallback>SC</AvatarFallback>
<AvatarImage src={user.avatar} />
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<span className="hidden text-sm font-medium md:inline-block">Sarah Chen</span>
<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">Sarah Chen</p>
<p className="text-xs text-muted-foreground">sarah@coastalit.com</p>
<Badge variant="secondary" className="mt-1 w-fit text-xs">Admin</Badge>
<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>
<DropdownMenuItem onClick={() => router.push("/profile")}>
<User className="mr-2 h-4 w-4" />
Profile
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem onClick={() => router.push("/settings")}>
<Settings className="mr-2 h-4 w-4" />
Settings
</DropdownMenuItem>
@@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Button } from "@/components/ui/button"
import { COMPANY_NAME } from "@/lib/constants"
import { toast } from "sonner"
export function CompanySettingsForm() {
return (
@@ -39,7 +40,7 @@ export function CompanySettingsForm() {
</div>
</div>
<div className="flex justify-end pt-4">
<Button>Save Changes</Button>
<Button onClick={() => toast.success("Company settings saved")}>Save Changes</Button>
</div>
</CardContent>
</Card>
@@ -4,6 +4,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Button } from "@/components/ui/button"
import { toast } from "sonner"
const notifications = [
{
@@ -63,7 +64,7 @@ export function NotificationSettings() {
</div>
))}
<div className="flex justify-end pt-4">
<Button>Save Preferences</Button>
<Button onClick={() => toast.success("Notification preferences saved")}>Save Preferences</Button>
</div>
</CardContent>
</Card>
@@ -3,6 +3,7 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
import { Button } from "@/components/ui/button"
import { toast } from "sonner"
import {
Select,
SelectContent,
@@ -65,7 +66,7 @@ export function UserPreferencesForm() {
</div>
</div>
<div className="flex justify-end pt-4">
<Button>Save Preferences</Button>
<Button onClick={() => toast.success("Preferences saved")}>Save Preferences</Button>
</div>
</CardContent>
</Card>
+84
View File
@@ -0,0 +1,84 @@
import { Conversation } from "@/types"
export const conversations: Conversation[] = [
{
id: "1",
participants: [
{ id: "user1", name: "Sarah Chen", avatar: "SC", role: "Admin" },
{ id: "user2", name: "Mike Johnson", avatar: "MJ", role: "Sales" },
],
lastMessage: "Sure, I'll send over the proposal by EOD",
lastMessageTime: "2m ago",
unread: 2,
messages: [
{ id: "m1", conversationId: "1", senderId: "user2", senderName: "Mike Johnson", senderAvatar: "MJ", content: "Hey Sarah, have you reviewed the Brightwave leads?", timestamp: "10:32 AM" },
{ id: "m2", conversationId: "1", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Yes, I went through them this morning. Looks promising!", timestamp: "10:33 AM" },
{ id: "m3", conversationId: "1", senderId: "user2", senderName: "Mike Johnson", senderAvatar: "MJ", content: "Great! Should I prepare a proposal for the top 3?", timestamp: "10:34 AM" },
{ id: "m4", conversationId: "1", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Absolutely. Focus on Brightwave and Nexus Digital first.", timestamp: "10:35 AM" },
{ id: "m5", conversationId: "1", senderId: "user2", senderName: "Mike Johnson", senderAvatar: "MJ", content: "Sure, I'll send over the proposal by EOD", timestamp: "10:36 AM" },
],
},
{
id: "2",
participants: [
{ id: "user1", name: "Sarah Chen", avatar: "SC", role: "Admin" },
{ id: "user3", name: "Emily Davis", avatar: "ED", role: "Sales" },
],
lastMessage: "Can you review the contract terms?",
lastMessageTime: "1h ago",
unread: 0,
messages: [
{ id: "m6", conversationId: "2", senderId: "user3", senderName: "Emily Davis", senderAvatar: "ED", content: "Hi Sarah, I'm finalizing the deal with Pinnacle Web Solutions.", timestamp: "9:15 AM" },
{ id: "m7", conversationId: "2", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "That's great news! What's the status?", timestamp: "9:16 AM" },
{ id: "m8", conversationId: "2", senderId: "user3", senderName: "Emily Davis", senderAvatar: "ED", content: "They're ready to sign, just need approval on the discount.", timestamp: "9:17 AM" },
{ id: "m9", conversationId: "2", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Can you review the contract terms?", timestamp: "9:18 AM" },
],
},
{
id: "3",
participants: [
{ id: "user1", name: "Sarah Chen", avatar: "SC", role: "Admin" },
{ id: "user4", name: "Alex Turner", avatar: "AT", role: "Sales" },
],
lastMessage: "Updated the lead status for Vertex Media",
lastMessageTime: "3h ago",
unread: 1,
messages: [
{ id: "m10", conversationId: "3", senderId: "user4", senderName: "Alex Turner", senderAvatar: "AT", content: "Just a heads up, Vertex Media called back.", timestamp: "7:45 AM" },
{ id: "m11", conversationId: "3", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Oh nice! What did they say?", timestamp: "7:46 AM" },
{ id: "m12", conversationId: "3", senderId: "user4", senderName: "Alex Turner", senderAvatar: "AT", content: "They're interested in the enterprise plan. Set up a meeting for next week.", timestamp: "7:48 AM" },
{ id: "m13", conversationId: "3", senderId: "user4", senderName: "Alex Turner", senderAvatar: "AT", content: "Updated the lead status for Vertex Media", timestamp: "7:50 AM" },
],
},
{
id: "4",
participants: [
{ id: "user1", name: "Sarah Chen", avatar: "SC", role: "Admin" },
{ id: "user5", name: "Lisa Wong", avatar: "LW", role: "Sales" },
],
lastMessage: "Thanks for the update!",
lastMessageTime: "Yesterday",
unread: 0,
messages: [
{ id: "m14", conversationId: "4", senderId: "user5", senderName: "Lisa Wong", senderAvatar: "LW", content: "Sarah, I just closed the deal with Crafted Web Agency!", timestamp: "4:20 PM" },
{ id: "m15", conversationId: "4", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Congratulations Lisa! That's fantastic!", timestamp: "4:21 PM" },
{ id: "m16", conversationId: "4", senderId: "user5", senderName: "Lisa Wong", senderAvatar: "LW", content: "Thanks! Contract is signed and onboarding starts Monday.", timestamp: "4:22 PM" },
{ id: "m17", conversationId: "4", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Thanks for the update!", timestamp: "4:23 PM" },
],
},
{
id: "5",
participants: [
{ id: "user1", name: "Sarah Chen", avatar: "SC", role: "Admin" },
{ id: "user6", name: "James Wilson", avatar: "JW", role: "Sales" },
],
lastMessage: "Can you assign me the new leads?",
lastMessageTime: "Yesterday",
unread: 0,
messages: [
{ id: "m18", conversationId: "5", senderId: "user6", senderName: "James Wilson", senderAvatar: "JW", content: "I've got capacity for more leads this quarter.", timestamp: "2:00 PM" },
{ id: "m19", conversationId: "5", senderId: "user1", senderName: "Sarah Chen", senderAvatar: "SC", content: "Good to know James. I'll assign you some from the new batch.", timestamp: "2:01 PM" },
{ id: "m20", conversationId: "5", senderId: "user6", senderName: "James Wilson", senderAvatar: "JW", content: "Can you assign me the new leads?", timestamp: "2:02 PM" },
],
},
]
+1 -1
View File
@@ -19,4 +19,4 @@ export const LEAD_SOURCES = [
export const ITEMS_PER_PAGE = 10
export const COMPANY_NAME = "Coastal IT"
export const COMPANY_NAME = "Coast IT"
+30
View File
@@ -0,0 +1,30 @@
"use client"
import { createContext, useContext, useState, ReactNode } from "react"
import { currentUser } from "@/data/users"
import type { User } from "@/types"
interface UserContextValue {
user: User
updateAvatar: (url: string) => void
}
const UserContext = createContext<UserContextValue | null>(null)
export function UserProvider({ children }: { children: ReactNode }) {
const [avatar, setAvatar] = useState(currentUser.avatar)
const user: User = { ...currentUser, avatar }
return (
<UserContext.Provider value={{ user, updateAvatar: setAvatar }}>
{children}
</UserContext.Provider>
)
}
export function useUser() {
const ctx = useContext(UserContext)
if (!ctx) throw new Error("useUser must be used within UserProvider")
return ctx
}
+19
View File
@@ -55,3 +55,22 @@ export interface ColumnFilter {
id: string
value: unknown
}
export interface ChatMessage {
id: string
conversationId: string
senderId: string
senderName: string
senderAvatar: string
content: string
timestamp: string
}
export interface Conversation {
id: string
participants: { id: string; name: string; avatar: string; role: string }[]
lastMessage: string
lastMessageTime: string
unread: number
messages: ChatMessage[]
}