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
+403 -1
View File
@@ -8,6 +8,8 @@
"name": "frontend", "name": "frontend",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-avatar": "^1.1.3",
@@ -27,6 +29,7 @@
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"emoji-mart": "^5.6.0",
"framer-motion": "^11.15.0", "framer-motion": "^11.15.0",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
"next": "15.0.4", "next": "15.0.4",
@@ -86,7 +89,6 @@
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"tslib": "^2.4.0" "tslib": "^2.4.0"
@@ -102,6 +104,22 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@emoji-mart/data": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz",
"integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==",
"license": "MIT"
},
"node_modules/@emoji-mart/react": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@emoji-mart/react/-/react-1.1.1.tgz",
"integrity": "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==",
"license": "MIT",
"peerDependencies": {
"emoji-mart": "^5.2",
"react": "^16.8 || ^17 || ^18"
}
},
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1", "version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
@@ -339,6 +357,384 @@
"url": "https://github.com/sponsors/nzakas" "url": "https://github.com/sponsors/nzakas"
} }
}, },
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.2.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": { "node_modules/@img/sharp-win32-x64": {
"version": "0.33.5", "version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
@@ -3208,6 +3604,12 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz",
"integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==" "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA=="
}, },
"node_modules/emoji-mart": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz",
"integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==",
"license": "MIT"
},
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "9.2.2", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+4 -1
View File
@@ -9,8 +9,9 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"autoprefixer": "^10.4.20",
"@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-checkbox": "^1.1.4",
@@ -26,8 +27,10 @@
"@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"@tanstack/react-table": "^8.20.6", "@tanstack/react-table": "^8.20.6",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"emoji-mart": "^5.6.0",
"framer-motion": "^11.15.0", "framer-motion": "^11.15.0",
"lucide-react": "^0.468.0", "lucide-react": "^0.468.0",
"next": "15.0.4", "next": "15.0.4",
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+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" "use client"
import { AppShell } from "@/components/layout/app-shell" import { AppShell } from "@/components/layout/app-shell"
import { UserProvider } from "@/providers/user-provider"
export default function DashboardLayout({ export default function DashboardLayout({
children, children,
}: { }: {
children: React.ReactNode 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 = { export const metadata: Metadata = {
title: "Coastal IT - CRM", title: "Coast IT - CRM",
description: "Customer Relationship Management System", description: "Customer Relationship Management System",
icons: {
icon: "/logo/CompanyMiniLogo.png",
},
} }
export default function RootLayout({ export default function RootLayout({
+11 -7
View File
@@ -35,9 +35,11 @@ export default function LoginPage() {
<div className="relative z-10"> <div className="relative z-10">
<div className="flex items-center gap-3"> <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"> <img
<span className="text-lg font-bold">C</span> src="/logo/CompanyLogo.png"
</div> alt={COMPANY_NAME}
className="h-10 w-10 rounded-xl object-contain"
/>
<span className="text-xl font-semibold">{COMPANY_NAME}</span> <span className="text-xl font-semibold">{COMPANY_NAME}</span>
</div> </div>
</div> </div>
@@ -72,7 +74,7 @@ export default function LoginPage() {
<div className="h-10 w-10 rounded-full bg-white/20" /> <div className="h-10 w-10 rounded-full bg-white/20" />
<div> <div>
<p className="text-sm font-medium">Marcus Johnson</p> <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>
</div> </div>
</motion.div> </motion.div>
@@ -93,9 +95,11 @@ export default function LoginPage() {
> >
{/* Mobile logo */} {/* Mobile logo */}
<div className="flex flex-col items-center gap-3 lg:hidden"> <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"> <img
<span className="text-xl font-bold text-primary-foreground">C</span> src="/logo/CompanyLogo.png"
</div> alt={COMPANY_NAME}
className="h-12 w-12 rounded-xl object-contain"
/>
<span className="text-xl font-semibold">{COMPANY_NAME}</span> <span className="text-xl font-semibold">{COMPANY_NAME}</span>
</div> </div>
+30 -12
View File
@@ -6,6 +6,7 @@ import { usePathname } from "next/navigation"
import { motion, AnimatePresence } from "framer-motion" import { motion, AnimatePresence } from "framer-motion"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { import {
LayoutDashboard, LayoutDashboard,
@@ -15,12 +16,18 @@ import {
ChevronRight, ChevronRight,
Building2, Building2,
PanelLeftClose, PanelLeftClose,
MessageSquare,
} from "lucide-react" } from "lucide-react"
import { COMPANY_NAME } from "@/lib/constants" 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 = [ const navItems = [
{ href: "/dashboard", label: "Dashboard", icon: LayoutDashboard }, { href: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
{ href: "/leads", label: "Leads", icon: Users }, { href: "/leads", label: "Leads", icon: Users },
{ href: "/chats", label: "Chats", icon: MessageSquare },
{ href: "/users", label: "Users", icon: Building2 }, { href: "/users", label: "Users", icon: Building2 },
{ href: "/settings", label: "Settings", icon: Settings }, { href: "/settings", label: "Settings", icon: Settings },
] ]
@@ -34,6 +41,8 @@ interface SidebarProps {
export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: SidebarProps) { export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: SidebarProps) {
const pathname = usePathname() const pathname = usePathname()
const { user } = useUser()
const initials = user.name.split(" ").map((n) => n[0]).join("")
const sidebarContent = ( const sidebarContent = (
<div <div
@@ -45,9 +54,11 @@ export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: Side
{/* Logo */} {/* Logo */}
<div className={cn("flex h-16 items-center border-b border-sidebar-border px-4", collapsed ? "justify-center" : "justify-between")}> <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"> <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"> <img
<span className="text-sm font-bold text-primary-foreground">C</span> src="/logo/CompanyLogo.png"
</div> alt={COMPANY_NAME}
className="h-8 w-8 shrink-0 rounded-lg object-contain"
/>
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{!collapsed && ( {!collapsed && (
<motion.span <motion.span
@@ -85,13 +96,18 @@ export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: Side
<Link <Link
href={item.href} href={item.href}
className={cn( 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 isActive
? "bg-sidebar-primary text-sidebar-primary-foreground" ? "bg-sidebar-primary text-sidebar-primary-foreground"
: "text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground" : "text-sidebar-foreground/60 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
)} )}
> >
<item.icon className="h-5 w-5" /> <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> </Link>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right" className="ml-2"> <TooltipContent side="right" className="ml-2">
@@ -141,17 +157,19 @@ export function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose }: Side
{/* User info */} {/* User info */}
<div className={cn("border-t border-sidebar-border p-3", collapsed && "flex justify-center")}> <div className={cn("border-t border-sidebar-border p-3", collapsed && "flex justify-center")}>
{collapsed ? ( {collapsed ? (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-sidebar-accent"> <Avatar className="h-10 w-10">
<span className="text-sm font-medium text-sidebar-accent-foreground">SC</span> <AvatarImage src={user.avatar} />
</div> <AvatarFallback className="text-sm font-medium">{initials}</AvatarFallback>
</Avatar>
) : ( ) : (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-sidebar-accent"> <Avatar className="h-9 w-9">
<span className="text-sm font-medium text-sidebar-accent-foreground">SC</span> <AvatarImage src={user.avatar} />
</div> <AvatarFallback className="text-sm font-medium">{initials}</AvatarFallback>
</Avatar>
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<p className="text-sm font-medium truncate">Sarah Chen</p> <p className="text-sm font-medium truncate">{user.name}</p>
<p className="text-xs text-sidebar-foreground/60 truncate">Admin</p> <p className="text-xs text-sidebar-foreground/60 truncate capitalize">{user.role}</p>
</div> </div>
</div> </div>
)} )}
+13 -8
View File
@@ -1,11 +1,13 @@
"use client" "use client"
import { useState } from "react" import { useState } from "react"
import { useRouter } from "next/navigation"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { useUser } from "@/providers/user-provider"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -31,8 +33,11 @@ interface TopbarProps {
} }
export function Topbar({ onMenuClick }: TopbarProps) { export function Topbar({ onMenuClick }: TopbarProps) {
const router = useRouter()
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
const { user } = useUser()
const [searchOpen, setSearchOpen] = useState(false) const [searchOpen, setSearchOpen] = useState(false)
const initials = user.name.split(" ").map((n) => n[0]).join("")
return ( return (
<header className="sticky top-0 z-20 flex h-16 items-center gap-4 border-b bg-background px-4 lg:px-6"> <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> <DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-9 gap-2 pl-2 pr-3"> <Button variant="ghost" className="relative h-9 gap-2 pl-2 pr-3">
<Avatar className="h-7 w-7"> <Avatar className="h-7 w-7">
<AvatarImage src="https://ui-avatars.com/api/?name=Sarah+Chen&background=1d4ed8&color=fff&size=64" /> <AvatarImage src={user.avatar} />
<AvatarFallback>SC</AvatarFallback> <AvatarFallback>{initials}</AvatarFallback>
</Avatar> </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> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56"> <DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel className="font-normal"> <DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<p className="text-sm font-medium">Sarah Chen</p> <p className="text-sm font-medium">{user.name}</p>
<p className="text-xs text-muted-foreground">sarah@coastalit.com</p> <p className="text-xs text-muted-foreground">{user.email}</p>
<Badge variant="secondary" className="mt-1 w-fit text-xs">Admin</Badge> <Badge variant="secondary" className="mt-1 w-fit text-xs capitalize">{user.role}</Badge>
</div> </div>
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem> <DropdownMenuItem onClick={() => router.push("/profile")}>
<User className="mr-2 h-4 w-4" /> <User className="mr-2 h-4 w-4" />
Profile Profile
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem> <DropdownMenuItem onClick={() => router.push("/settings")}>
<Settings className="mr-2 h-4 w-4" /> <Settings className="mr-2 h-4 w-4" />
Settings Settings
</DropdownMenuItem> </DropdownMenuItem>
@@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { COMPANY_NAME } from "@/lib/constants" import { COMPANY_NAME } from "@/lib/constants"
import { toast } from "sonner"
export function CompanySettingsForm() { export function CompanySettingsForm() {
return ( return (
@@ -39,7 +40,7 @@ export function CompanySettingsForm() {
</div> </div>
</div> </div>
<div className="flex justify-end pt-4"> <div className="flex justify-end pt-4">
<Button>Save Changes</Button> <Button onClick={() => toast.success("Company settings saved")}>Save Changes</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -4,6 +4,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { toast } from "sonner"
const notifications = [ const notifications = [
{ {
@@ -63,7 +64,7 @@ export function NotificationSettings() {
</div> </div>
))} ))}
<div className="flex justify-end pt-4"> <div className="flex justify-end pt-4">
<Button>Save Preferences</Button> <Button onClick={() => toast.success("Notification preferences saved")}>Save Preferences</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -3,6 +3,7 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { toast } from "sonner"
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -65,7 +66,7 @@ export function UserPreferencesForm() {
</div> </div>
</div> </div>
<div className="flex justify-end pt-4"> <div className="flex justify-end pt-4">
<Button>Save Preferences</Button> <Button onClick={() => toast.success("Preferences saved")}>Save Preferences</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </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 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 id: string
value: unknown 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[]
}