251208:0010 Backend & Frontend Debug
This commit is contained in:
@@ -2,21 +2,18 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Search, FileText, Clipboard, Image } from "lucide-react";
|
||||
import { Search, FileText, Clipboard, Image, Loader2 } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Command, CommandEmpty, CommandGroup, CommandItem, CommandList,
|
||||
Command, CommandGroup, CommandItem, CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { searchApi } from "@/lib/api/search";
|
||||
import { SearchResult } from "@/types/search";
|
||||
import { useDebounce } from "@/hooks/use-debounce"; // We need to create this hook or implement debounce inline
|
||||
import { useSearchSuggestions } from "@/hooks/use-search";
|
||||
|
||||
// Simple debounce hook implementation inline for now if not exists
|
||||
function useDebounceValue<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
useEffect(() => {
|
||||
@@ -34,19 +31,18 @@ export function GlobalSearch() {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
const [suggestions, setSuggestions] = useState<SearchResult[]>([]);
|
||||
|
||||
const debouncedQuery = useDebounceValue(query, 300);
|
||||
|
||||
const { data: suggestions, isLoading } = useSearchSuggestions(debouncedQuery);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedQuery.length > 2) {
|
||||
searchApi.suggest(debouncedQuery).then(setSuggestions);
|
||||
if (debouncedQuery.length > 2 && suggestions && suggestions.length > 0) {
|
||||
setOpen(true);
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
if (debouncedQuery.length === 0) setOpen(false);
|
||||
}
|
||||
}, [debouncedQuery]);
|
||||
}, [debouncedQuery, suggestions]);
|
||||
|
||||
const handleSearch = () => {
|
||||
if (query.trim()) {
|
||||
@@ -66,7 +62,7 @@ export function GlobalSearch() {
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-sm">
|
||||
<Popover open={open && suggestions.length > 0} onOpenChange={setOpen}>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
@@ -78,29 +74,42 @@ export function GlobalSearch() {
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||
onFocus={() => {
|
||||
if (suggestions.length > 0) setOpen(true);
|
||||
if (suggestions && suggestions.length > 0) setOpen(true);
|
||||
}}
|
||||
/>
|
||||
{isLoading && (
|
||||
<Loader2 className="absolute right-2.5 top-2.5 h-4 w-4 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]" align="start">
|
||||
<PopoverContent className="p-0 w-[var(--radix-popover-trigger-width)]" align="start" onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||
<Command>
|
||||
<CommandList>
|
||||
<CommandGroup heading="Suggestions">
|
||||
{suggestions.map((item) => (
|
||||
<CommandItem
|
||||
key={`${item.type}-${item.id}`}
|
||||
onSelect={() => {
|
||||
setQuery(item.title);
|
||||
router.push(`/${item.type}s/${item.id}`);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{getIcon(item.type)}
|
||||
<span>{item.title}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
{suggestions && suggestions.length > 0 && (
|
||||
<CommandGroup heading="Suggestions">
|
||||
{suggestions.map((item: any) => (
|
||||
<CommandItem
|
||||
key={`${item.type}-${item.id}`}
|
||||
onSelect={() => {
|
||||
setQuery(item.title);
|
||||
// Assumption: item has type and id.
|
||||
// If type is missing, we might need a map or check usage in backend response
|
||||
router.push(`/${item.type}s/${item.id}`);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{getIcon(item.type)}
|
||||
<span className="truncate">{item.title}</span>
|
||||
<span className="ml-auto text-xs text-muted-foreground">{item.documentNumber}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
{(!suggestions || suggestions.length === 0) && !isLoading && (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||
No suggestions found.
|
||||
</div>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Bell, Check } from "lucide-react";
|
||||
import { Bell, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -12,62 +11,36 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { notificationApi } from "@/lib/api/notifications";
|
||||
import { Notification } from "@/types/notification";
|
||||
import { useNotifications, useMarkNotificationRead } from "@/hooks/use-notification";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export function NotificationsDropdown() {
|
||||
const router = useRouter();
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data, isLoading } = useNotifications();
|
||||
const markAsRead = useMarkNotificationRead();
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch notifications
|
||||
const fetchNotifications = async () => {
|
||||
try {
|
||||
const data = await notificationApi.getUnread();
|
||||
setNotifications(data.items);
|
||||
setUnreadCount(data.unreadCount);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch notifications", error);
|
||||
}
|
||||
};
|
||||
const notifications = data?.items || [];
|
||||
const unreadCount = data?.unreadCount || 0;
|
||||
|
||||
fetchNotifications();
|
||||
}, []);
|
||||
|
||||
const handleMarkAsRead = async (id: number, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
await notificationApi.markAsRead(id);
|
||||
setNotifications((prev) =>
|
||||
prev.map((n) => (n.notification_id === id ? { ...n, is_read: true } : n))
|
||||
);
|
||||
setUnreadCount((prev) => Math.max(0, prev - 1));
|
||||
};
|
||||
|
||||
const handleNotificationClick = async (notification: Notification) => {
|
||||
const handleNotificationClick = (notification: any) => {
|
||||
if (!notification.is_read) {
|
||||
await notificationApi.markAsRead(notification.notification_id);
|
||||
setUnreadCount((prev) => Math.max(0, prev - 1));
|
||||
markAsRead.mutate(notification.notification_id);
|
||||
}
|
||||
setIsOpen(false);
|
||||
if (notification.link) {
|
||||
router.push(notification.link);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="relative">
|
||||
<Bell className="h-5 w-5 text-gray-600" />
|
||||
{unreadCount > 0 && (
|
||||
<Bell className="h-5 w-5" />
|
||||
{unreadCount > 0 && !isLoading && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-[10px] rounded-full"
|
||||
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs"
|
||||
>
|
||||
{unreadCount}
|
||||
</Badge>
|
||||
@@ -76,50 +49,35 @@ export function NotificationsDropdown() {
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end" className="w-80">
|
||||
<DropdownMenuLabel className="flex justify-between items-center">
|
||||
<span>Notifications</span>
|
||||
{unreadCount > 0 && (
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
{unreadCount} unread
|
||||
</span>
|
||||
)}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>Notifications</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{notifications.length === 0 ? (
|
||||
<div className="p-8 text-center text-sm text-muted-foreground">
|
||||
No notifications
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center p-4">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : notifications.length === 0 ? (
|
||||
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||
No new notifications
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[400px] overflow-y-auto">
|
||||
{notifications.map((notification) => (
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{notifications.slice(0, 5).map((notification: any) => (
|
||||
<DropdownMenuItem
|
||||
key={notification.notification_id}
|
||||
className={`flex flex-col items-start p-3 cursor-pointer ${
|
||||
!notification.is_read ? "bg-muted/30" : ""
|
||||
!notification.is_read ? 'bg-muted/30' : ''
|
||||
}`}
|
||||
onClick={() => handleNotificationClick(notification)}
|
||||
>
|
||||
<div className="flex w-full justify-between items-start gap-2">
|
||||
<div className="font-medium text-sm line-clamp-1">
|
||||
{notification.title}
|
||||
</div>
|
||||
{!notification.is_read && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5 text-muted-foreground hover:text-primary"
|
||||
onClick={(e) => handleMarkAsRead(notification.notification_id, e)}
|
||||
title="Mark as read"
|
||||
>
|
||||
<Check className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex justify-between w-full">
|
||||
<span className="font-medium text-sm">{notification.title}</span>
|
||||
{!notification.is_read && <span className="h-2 w-2 rounded-full bg-blue-500 mt-1" />}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
||||
{notification.message}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground mt-2 w-full text-right">
|
||||
<div className="text-[10px] text-muted-foreground mt-1 self-end">
|
||||
{formatDistanceToNow(new Date(notification.created_at), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
@@ -130,8 +88,8 @@ export function NotificationsDropdown() {
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-center justify-center text-xs text-muted-foreground cursor-pointer">
|
||||
View All Notifications
|
||||
<DropdownMenuItem className="text-center justify-center text-xs text-muted-foreground" disabled>
|
||||
View All Notifications (Coming Soon)
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
Reference in New Issue
Block a user