From 67ecc950c296931b70237de3a8932d3d99aa14b4 Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Fri, 21 Feb 2025 18:00:23 +0700 Subject: [PATCH] add theme switcher and inbox drawer to header layout admin panel --- .../crime-management/crime-overview/page.tsx | 17 -- .../app/protected/(admin-pages)/layout.tsx | 51 +++- sigap-website/components/inbox-drawer.tsx | 280 ++++++++++++++++++ sigap-website/components/nav-main.tsx | 18 +- sigap-website/components/nav-user.tsx | 49 +-- sigap-website/components/theme-switcher.tsx | 97 +++--- sigap-website/components/ui/drawer.tsx | 118 ++++++++ sigap-website/components/ui/scroll-area.tsx | 48 +++ sigap-website/package-lock.json | 46 +++ sigap-website/package.json | 2 + 10 files changed, 638 insertions(+), 88 deletions(-) create mode 100644 sigap-website/components/inbox-drawer.tsx create mode 100644 sigap-website/components/ui/drawer.tsx create mode 100644 sigap-website/components/ui/scroll-area.tsx diff --git a/sigap-website/app/protected/(admin-pages)/crime-management/crime-overview/page.tsx b/sigap-website/app/protected/(admin-pages)/crime-management/crime-overview/page.tsx index 114d30e..46fd9f1 100644 --- a/sigap-website/app/protected/(admin-pages)/crime-management/crime-overview/page.tsx +++ b/sigap-website/app/protected/(admin-pages)/crime-management/crime-overview/page.tsx @@ -17,23 +17,6 @@ import { export default function CrimeOverview() { return ( <> -
-
- - - - - - Sigap - v - - - - Map - - - -
-
diff --git a/sigap-website/app/protected/(admin-pages)/layout.tsx b/sigap-website/app/protected/(admin-pages)/layout.tsx index 682f6a6..f533417 100644 --- a/sigap-website/app/protected/(admin-pages)/layout.tsx +++ b/sigap-website/app/protected/(admin-pages)/layout.tsx @@ -7,13 +7,22 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; +import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { SidebarInset, SidebarProvider, SidebarTrigger, } from "@/components/ui/sidebar"; -import { redirect } from "next/navigation"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { MoreHorizontal, MoreVertical } from "lucide-react"; +import { InboxDrawer } from "@/components/inbox-drawer"; +import { ThemeSwitcher } from "@/components/theme-switcher"; export default async function Layout({ children, @@ -23,7 +32,43 @@ export default async function Layout({ return ( - {children} + +
+
+ + + + + + Sigap - v + + + + Map + + + +
+
+ + + + + + + + Settings + Help + About + + +
+
+ {children} +
); -} +} \ No newline at end of file diff --git a/sigap-website/components/inbox-drawer.tsx b/sigap-website/components/inbox-drawer.tsx new file mode 100644 index 0000000..4e9a420 --- /dev/null +++ b/sigap-website/components/inbox-drawer.tsx @@ -0,0 +1,280 @@ +"use client"; + +import * as React from "react"; +import { Inbox, Search, ArrowLeft } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Badge } from "@/components/ui/badge"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; + +interface InboxDrawerProps { + showTitle?: boolean; + showAvatar?: boolean; +} + +interface MailMessage { + id: string; + name: string; + email: string; + subject: string; + teaser: string; + content: string; + date: string; + read: boolean; +} + +const ICON_SIZE = 20; + +// Sample data +const sampleMails: MailMessage[] = [ + { + id: "1", + name: "William Smith", + email: "williamsmith@example.com", + subject: "Meeting Tomorrow", + teaser: + "Hi team, just a reminder about our meeting tomorrow at 10 AM. Please come prepared with your project updates.", + content: + "Hi team,\n\nJust a reminder about our meeting tomorrow at 10 AM. Please come prepared with your project updates. We'll be discussing the progress of Project X and setting goals for the next sprint.\n\nIf you have any questions or need to reschedule, please let me know as soon as possible.\n\nBest regards,\nWilliam", + date: "09:34 AM", + read: false, + }, + { + id: "2", + name: "Alice Johnson", + email: "alicejohnson@example.com", + subject: "Re: Project Update", + teaser: + "Thanks for the update. The progress looks great so far. Let's schedule a call to discuss the next steps.", + content: + "Hi team,\n\nThanks for the update. The progress looks great so far. I'm impressed with the work you've done on the new feature.\n\nLet's schedule a call to discuss the next steps. How does tomorrow at 2 PM sound? We can go over any challenges you're facing and plan out the rest of the sprint.\n\nLooking forward to our discussion.\n\nBest,\nAlice", + date: "Yesterday", + read: true, + }, + { + id: "3", + name: "Bob Davis", + email: "bobdavis@example.com", + subject: "Weekend Plans", + teaser: + "Hey everyone! I'm thinking of organizing a team outing this weekend. Would you be interested in a hiking trip or a beach day?", + content: + "Hey everyone!\n\nI hope this email finds you well. I'm thinking of organizing a team outing this weekend to boost morale and have some fun outside of work.\n\nWould you be interested in a hiking trip or a beach day? I'm open to other suggestions as well. Let me know your preferences, and we can plan accordingly.\n\nLooking forward to spending some quality time with the team!\n\nCheers,\nBob", + date: "2 days ago", + read: false, + }, + { + id: "4", + name: "Charlie Brown", + email: "charliebrown@example.com", + subject: "Client Feedback", + teaser: + "The client has provided feedback on the recent deliverables. Please review and make the necessary adjustments.", + content: + "Hi team,\n\nThe client has provided feedback on the recent deliverables. Please review the attached document and make the necessary adjustments.\n\nLet's aim to have the revisions completed by the end of the week so we can send the updated version to the client.\n\nThanks,\nCharlie", + date: "3 days ago", + read: false, + }, + { + id: "5", + name: "Diana Prince", + email: "dianaprince@example.com", + subject: "Team Lunch", + teaser: + "We're planning a team lunch this Friday at 1 PM. Please RSVP by tomorrow.", + content: + "Hello team,\n\nWe're planning a team lunch this Friday at 1 PM at the new Italian restaurant downtown. Please RSVP by tomorrow so we can make a reservation.\n\nLooking forward to seeing you all there!\n\nBest,\nDiana", + date: "4 days ago", + read: true, + }, +]; + +const InboxDrawerComponent: React.FC = ({ + showTitle = false, + showAvatar = false, +}) => { + const [messages, setMessages] = React.useState(sampleMails); + const [isOpen, setIsOpen] = React.useState(false); + const [searchTerm, setSearchTerm] = React.useState(""); + const [selectedMessage, setSelectedMessage] = + React.useState(null); + + const unreadCount = React.useMemo(() => { + return messages.filter((message) => !message.read).length; + }, [messages]); + + const filteredMessages = React.useMemo(() => { + return messages.filter( + (message) => + message.subject.toLowerCase().includes(searchTerm.toLowerCase()) || + message.name.toLowerCase().includes(searchTerm.toLowerCase()) || + message.teaser.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }, [messages, searchTerm]); + + const markAsRead = React.useCallback((id: string) => { + setMessages((prevMessages) => + prevMessages.map((message) => + message.id === id ? { ...message, read: true } : message + ) + ); + }, []); + + const handleMessageClick = (message: MailMessage) => { + setSelectedMessage(message); + markAsRead(message.id); + }; + + const handleBackToList = () => { + setSelectedMessage(null); + }; + + return ( + + + + + + {selectedMessage ? ( +
+ + + + +

+ {selectedMessage.subject} +

+
+ {showAvatar && ( +
+ + + + {selectedMessage.name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase()} + + +
+

{selectedMessage.name}

+

+ {selectedMessage.email} +

+
+
+ )} +

+ {selectedMessage.date} +

+
+
+ {selectedMessage.content} +
+
+
+ ) : ( + <> + + Inbox +
+ + setSearchTerm(e.target.value)} + /> +
+
+ + {filteredMessages.map((message) => ( +
handleMessageClick(message)} + > + {showAvatar && ( + + + + {message.name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase()} + + + )} +
+
+ {message.name} + + {message.date} + +
+ {message.subject} + + {message.teaser} + + {!message.read && ( + + New + + )} +
+
+ ))} +
+ + )} +
+
+ ); +}; + +export const InboxDrawer = React.memo(InboxDrawerComponent); diff --git a/sigap-website/components/nav-main.tsx b/sigap-website/components/nav-main.tsx index ccdacf7..3908662 100644 --- a/sigap-website/components/nav-main.tsx +++ b/sigap-website/components/nav-main.tsx @@ -17,6 +17,7 @@ import { } from "@/components/ui/sidebar"; import * as TablerIcons from "@tabler/icons-react"; +import { useNavigations } from "@/hooks/use-navigations"; interface SubSubItem { title: string; @@ -39,8 +40,11 @@ interface NavItem { } function SubSubItemComponent({ item }: { item: SubSubItem }) { + const router = useNavigations(); + const isActive = router.pathname === item.url; + return ( - + {item.title} @@ -51,11 +55,13 @@ function SubSubItemComponent({ item }: { item: SubSubItem }) { } function SubItemComponent({ item }: { item: SubItem }) { + const router = useNavigations(); + const isActive = router.pathname === item.url; const hasSubSubItems = item.subSubItems && item.subSubItems.length > 0; if (!hasSubSubItems) { return ( - + {item.icon && } @@ -68,7 +74,7 @@ function SubItemComponent({ item }: { item: SubItem }) { return ( - + {item.icon && } @@ -89,11 +95,13 @@ function SubItemComponent({ item }: { item: SubItem }) { } function RecursiveNavItem({ item, index }: { item: NavItem; index: number }) { + const router = useNavigations(); + const isActive = router.pathname === item.url; const hasSubItems = item.subItems && item.subItems.length > 0; if (!hasSubItems) { return ( - + {item.icon && } @@ -111,7 +119,7 @@ function RecursiveNavItem({ item, index }: { item: NavItem; index: number }) { defaultOpen={index === 1} className="group/collapsible" > - + {item.icon && } diff --git a/sigap-website/components/nav-user.tsx b/sigap-website/components/nav-user.tsx index d12ef78..93f51d3 100644 --- a/sigap-website/components/nav-user.tsx +++ b/sigap-website/components/nav-user.tsx @@ -29,17 +29,24 @@ import { SidebarMenuItem, useSidebar, } from "@/components/ui/sidebar" +import { + IconBadgeCc, + IconBell, + IconCreditCard, + IconLogout, + IconSparkles, +} from "@tabler/icons-react"; export function NavUser({ user, }: { user: { - name: string - email: string - avatar: string - } + name: string; + email: string; + avatar: string; + }; }) { - const { isMobile } = useSidebar() + const { isMobile } = useSidebar(); return ( @@ -81,34 +88,34 @@ export function NavUser({ - - - Upgrade to Pro + + + Upgrade to Pro - - - Account + + + Account - - - Billing + + + Billing - - - Notifications + + + Notifications - - - Log out + + + Log out - ) + ); } diff --git a/sigap-website/components/theme-switcher.tsx b/sigap-website/components/theme-switcher.tsx index 6e7450b..ea2cf7b 100644 --- a/sigap-website/components/theme-switcher.tsx +++ b/sigap-website/components/theme-switcher.tsx @@ -1,5 +1,7 @@ "use client"; +import React from "react"; + import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -8,71 +10,82 @@ import { DropdownMenuRadioItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Laptop, Moon, Sun } from "lucide-react"; +import { Laptop, Moon, Sun, type LucideIcon } from "lucide-react"; import { useTheme } from "next-themes"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; -const ThemeSwitcher = () => { +interface ThemeSwitcherComponentProps { + showTitle?: boolean; +} + +type ThemeOption = { + value: string; + icon: LucideIcon; + label: string; +}; + +const ICON_SIZE = 16; + +const themeOptions: ThemeOption[] = [ + { value: "light", icon: Sun, label: "Light" }, + { value: "dark", icon: Moon, label: "Dark" }, + { value: "system", icon: Laptop, label: "System" }, +]; + +const ThemeSwitcherComponent = ({ + showTitle = false, +}: ThemeSwitcherComponentProps) => { const [mounted, setMounted] = useState(false); const { theme, setTheme } = useTheme(); - // useEffect only runs on the client, so now we can safely show the UI useEffect(() => { setMounted(true); }, []); + const currentTheme = useMemo( + () => + themeOptions.find((option) => option.value === theme) || themeOptions[2], + [theme] + ); + if (!mounted) { return null; } - const ICON_SIZE = 16; - return ( - - setTheme(e)} - > - - {" "} - Light - - - {" "} - Dark - - - {" "} - System - + + {themeOptions.map((option) => ( + + {/* */} + {option.label} + + ))} ); }; -export { ThemeSwitcher }; +export const ThemeSwitcher = React.memo(ThemeSwitcherComponent); diff --git a/sigap-website/components/ui/drawer.tsx b/sigap-website/components/ui/drawer.tsx new file mode 100644 index 0000000..6a0ef53 --- /dev/null +++ b/sigap-website/components/ui/drawer.tsx @@ -0,0 +1,118 @@ +"use client" + +import * as React from "react" +import { Drawer as DrawerPrimitive } from "vaul" + +import { cn } from "@/lib/utils" + +const Drawer = ({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps) => ( + +) +Drawer.displayName = "Drawer" + +const DrawerTrigger = DrawerPrimitive.Trigger + +const DrawerPortal = DrawerPrimitive.Portal + +const DrawerClose = DrawerPrimitive.Close + +const DrawerOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName + +const DrawerContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +
+ {children} + + +)) +DrawerContent.displayName = "DrawerContent" + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerHeader.displayName = "DrawerHeader" + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DrawerFooter.displayName = "DrawerFooter" + +const DrawerTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerTitle.displayName = DrawerPrimitive.Title.displayName + +const DrawerDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DrawerDescription.displayName = DrawerPrimitive.Description.displayName + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/sigap-website/components/ui/scroll-area.tsx b/sigap-website/components/ui/scroll-area.tsx new file mode 100644 index 0000000..0b4a48d --- /dev/null +++ b/sigap-website/components/ui/scroll-area.tsx @@ -0,0 +1,48 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/sigap-website/package-lock.json b/sigap-website/package-lock.json index 2075005..4ebb40b 100644 --- a/sigap-website/package-lock.json +++ b/sigap-website/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", @@ -40,6 +41,7 @@ "react-dom": "18.3.1", "react-hook-form": "^7.54.2", "resend": "^4.1.2", + "vaul": "^1.1.2", "zod": "^3.24.2" }, "devDependencies": { @@ -2209,6 +2211,37 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz", + "integrity": "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz", @@ -7090,6 +7123,19 @@ "node": ">= 0.8" } }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/vt-pbf": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", diff --git a/sigap-website/package.json b/sigap-website/package.json index 4046b26..c5f0348 100644 --- a/sigap-website/package.json +++ b/sigap-website/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", @@ -41,6 +42,7 @@ "react-dom": "18.3.1", "react-hook-form": "^7.54.2", "resend": "^4.1.2", + "vaul": "^1.1.2", "zod": "^3.24.2" }, "devDependencies": {