Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 84 additions & 48 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import React, { useState, useContext, useEffect } from "react";
import { NavLink, useLocation, useNavigate } from "react-router-dom";
import { Bell, Menu, X, Home, BarChart, User, Info, LogOut, Check } from "lucide-react";
import {
Bell,
Menu,
X,
Home,
BarChart,
User,
Info,
LogOut
} from "lucide-react";
import { useAtom } from "jotai";
import { userAtom } from "@/state/userAtom";
import { AuthContext } from "@/context/authContext";
Expand All @@ -21,7 +30,13 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { Button } from "@/components/ui/button";
import debateAiLogo from "@/assets/aossie.png";
import avatarImage from "@/assets/avatar2.jpg";
import { getNotifications, markNotificationAsRead, markAllNotificationsAsRead, deleteNotification, Notification } from "@/services/notificationService";
import {
getNotifications,
markNotificationAsRead,
markAllNotificationsAsRead,
deleteNotification,
Notification,
} from "@/services/notificationService";

function Header() {
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
Expand All @@ -39,7 +54,7 @@ function Header() {
if (user) {
const data = await getNotifications();
setNotifications(data);
setUnreadCount(data.filter(n => !n.isRead).length);
setUnreadCount(data.filter((n) => !n.isRead).length);
}
};

Expand All @@ -53,12 +68,12 @@ function Header() {
const handleNotificationClick = async (notification: Notification) => {
if (!notification.isRead) {
await markNotificationAsRead(notification.id);
setNotifications(prev =>
prev.map(n => n.id === notification.id ? { ...n, isRead: true } : n)
setNotifications((prev) =>
prev.map((n) => (n.id === notification.id ? { ...n, isRead: true } : n))
);
setUnreadCount(prev => Math.max(0, prev - 1));
setUnreadCount((prev) => Math.max(0, prev - 1));
}

if (notification.link) {
navigate(notification.link);
setIsNotificationsOpen(false);
Expand All @@ -68,17 +83,17 @@ function Header() {
const handleDeleteNotification = async (e: React.MouseEvent, id: string) => {
e.stopPropagation();
await deleteNotification(id);
setNotifications(prev => prev.filter(n => n.id !== id));
setNotifications((prev) => prev.filter((n) => n.id !== id));
// Update unread count if the deleted notification was unread
const notification = notifications.find(n => n.id === id);
const notification = notifications.find((n) => n.id === id);
if (notification && !notification.isRead) {
setUnreadCount(prev => Math.max(0, prev - 1));
setUnreadCount((prev) => Math.max(0, prev - 1));
}
};

const handleMarkAllRead = async () => {
await markAllNotificationsAsRead();
setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true })));
setUnreadCount(0);
};

Expand Down Expand Up @@ -121,26 +136,26 @@ function Header() {

return (
<>
<header className="flex items-center justify-between h-16 px-4 border-b border-gray-200">
<header className="flex items-center justify-between h-16 px-4 border-b border-border">
<div className="text-lg font-semibold">{getBreadcrumbs()}</div>
<div className="flex items-center gap-4">
<Popover open={isNotificationsOpen} onOpenChange={setIsNotificationsOpen}>
<PopoverTrigger asChild>
<button className="relative focus:outline-none">
<Bell className="w-5 h-5 text-gray-600" />
<Bell className="w-5 h-5 text-muted-foreground" />
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 block h-2 w-2 rounded-full bg-red-500" />
)}
</button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end">
<div className="flex items-center justify-between p-4 border-b border-gray-100">
<h4 className="font-semibold text-gray-900">Notifications</h4>
<div className="flex items-center justify-between p-4 border-b border-border">
<h4 className="font-semibold text-foreground">Notifications</h4>
{unreadCount > 0 && (
<Button
variant="ghost"
size="sm"
className="text-xs h-8 px-2 text-blue-600 hover:text-blue-800"
<Button
variant="ghost"
size="sm"
className="text-xs h-8 px-2 text-primary hover:text-primary/80"
onClick={handleMarkAllRead}
>
Mark all read
Expand All @@ -149,35 +164,47 @@ function Header() {
</div>
<ScrollArea className="h-[300px]">
{notifications.length === 0 ? (
<div className="p-8 text-center text-gray-500 text-sm">
<div className="p-8 text-center text-muted-foreground text-sm">
No notifications yet
</div>
) : (
<div className="divide-y divide-gray-100">
<div className="divide-y divide-border">
{notifications.map((notification) => (
<div
<div
key={notification.id}
className={`p-4 hover:bg-gray-50 cursor-pointer transition-colors relative group ${!notification.isRead ? 'bg-blue-50/50' : ''}`}
className={`p-4 hover:bg-muted cursor-pointer transition-colors relative group ${
!notification.isRead ? "bg-primary/10" : ""
}`}
onClick={() => handleNotificationClick(notification)}
>
<button
onClick={(e) => handleDeleteNotification(e, notification.id)}
className="absolute top-2 right-2 p-1 text-gray-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) =>
handleDeleteNotification(e, notification.id)
}
className="absolute top-2 right-2 p-1 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
title="Delete notification"
>
<X className="w-3 h-3" />
</button>
<div className="flex gap-3">
<div className={`mt-1 h-2 w-2 rounded-full flex-shrink-0 ${!notification.isRead ? 'bg-blue-500' : 'bg-transparent'}`} />
<div
className={`mt-1 h-2 w-2 rounded-full flex-shrink-0 ${
!notification.isRead
? "bg-primary"
: "bg-transparent"
}`}
/>
<div className="flex-1 space-y-1 pr-4">
<p className="text-sm font-medium leading-none text-gray-900">
<p className="text-sm font-medium leading-none text-foreground">
{notification.title}
</p>
<p className="text-sm text-gray-500 line-clamp-2">
<p className="text-sm text-muted-foreground line-clamp-2">
{notification.message}
</p>
<p className="text-xs text-gray-400">
{new Date(notification.createdAt).toLocaleDateString()}
<p className="text-xs text-muted-foreground/70">
{new Date(
notification.createdAt
).toLocaleDateString()}
</p>
</div>
</div>
Expand All @@ -188,47 +215,56 @@ function Header() {
</ScrollArea>
</PopoverContent>
</Popover>

<Popover>
<PopoverTrigger asChild>
<button className="focus:outline-none">
<img
src={user?.avatarUrl || avatarImage}
alt="User avatar"
className="w-8 h-8 rounded-full border-2 border-gray-300 object-cover cursor-pointer hover:opacity-80 transition-opacity"
className="w-8 h-8 rounded-full border-2 border-border object-cover cursor-pointer hover:opacity-80 transition-opacity"
/>
</button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0" align="end">
<div className="p-4 border-b border-gray-100">
<div className="p-4 border-b border-border">
<div className="flex items-center gap-3 mb-3">
<img
src={user?.avatarUrl || avatarImage}
alt="User avatar"
className="w-12 h-12 rounded-full border-2 border-gray-200 object-cover"
className="w-12 h-12 rounded-full border-2 border-border object-cover"
/>
<div className="overflow-hidden">
<h4 className="font-semibold text-gray-900 truncate">{user?.displayName || "User"}</h4>
<p className="text-sm text-gray-500 truncate">{user?.email || "No email"}</p>
<h4 className="font-semibold text-foreground truncate">
{user?.displayName || "User"}
</h4>
<p className="text-sm text-muted-foreground truncate">
{user?.email || "No email"}
</p>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-500">User ID</span>
<span className="font-mono text-xs bg-gray-100 px-2 py-0.5 rounded text-gray-700 truncate max-w-[150px]" title={user?.id}>
<span className="text-muted-foreground">User ID</span>
<span
className="font-mono text-xs bg-muted px-2 py-0.5 rounded text-muted-foreground truncate max-w-[150px]"
title={user?.id}
>
{user?.id || "N/A"}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500">Rating</span>
<span className="font-medium text-blue-600">{user?.rating ? Math.round(user.rating) : 1500}</span>
<span className="text-muted-foreground">Rating</span>
<span className="font-medium text-primary">
{user?.rating ? Math.round(user.rating) : 1500}
</span>
</div>
</div>
</div>
<div className="p-2">
<button
onClick={() => auth?.logout()}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-600 hover:bg-red-50 rounded-md transition-colors"
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-destructive hover:bg-destructive/10 rounded-md transition-colors"
>
<LogOut className="w-4 h-4" />
Sign out
Expand All @@ -239,7 +275,7 @@ function Header() {

<button
onClick={toggleDrawer}
className="md:hidden p-2 rounded-md text-gray-600 hover:bg-gray-100"
className="md:hidden p-2 rounded-md text-muted-foreground hover:bg-muted"
aria-label="Open menu"
>
<Menu className="h-6 w-6" />
Expand All @@ -253,10 +289,10 @@ function Header() {
className="absolute inset-0 bg-black bg-opacity-50"
onClick={toggleDrawer}
></div>
<div className="relative w-64 h-full bg-white shadow-lg transform transition-transform duration-300 ease-in-out translate-x-0 ml-auto">
<div className="flex items-center justify-between h-16 px-4 border-b border-gray-200">
<div className="relative w-64 h-full bg-background shadow-lg transform transition-transform duration-300 ease-in-out translate-x-0 ml-auto">
<div className="flex items-center justify-between h-16 px-4 border-b border-border">
<div className="flex items-center gap-2">
<span className="text-xl font-bold text-gray-900">
<span className="text-xl font-bold text-foreground">
DebateAI by
</span>
<img
Expand All @@ -267,7 +303,7 @@ function Header() {
</div>
<button
onClick={toggleDrawer}
className="p-2 text-gray-600 hover:text-gray-900"
className="p-2 text-muted-foreground hover:text-foreground"
aria-label="Close menu"
>
<X className="h-6 w-6" />
Expand Down Expand Up @@ -321,8 +357,8 @@ function NavItem({ to, label, icon, onClick }: NavItemProps) {
className={({ isActive }) =>
`group flex items-center px-2 py-2 text-sm font-medium rounded-md ${
isActive
? "bg-gray-200 text-gray-900"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
? "bg-secondary text-secondary-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
}`
}
>
Expand Down
73 changes: 37 additions & 36 deletions frontend/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// src/components/Sidebar.tsx
import React from 'react';
import { NavLink } from 'react-router-dom';
import React from "react";
import { NavLink } from "react-router-dom";
import {
MessageSquare,
BarChart,
Expand All @@ -9,60 +9,60 @@ import {
Trophy,
Users,
MessageCircle,
} from 'lucide-react';
import debateAiLogo from '@/assets/aossie.png';
import { ThemeToggle } from './ThemeToggle';
} from "lucide-react";
import debateAiLogo from "@/assets/aossie.png";
import { ThemeToggle } from "./ThemeToggle";

function Sidebar() {
return (
<aside className='hidden md:flex flex-col w-64 border-r border-gray-200'>
<aside className="hidden md:flex flex-col w-64 border-r border-border bg-background">
{/* Logo / Brand */}
<div className='flex items-center h-16 px-4 border-b border-gray-200'>
<div className='flex items-center gap-2'>
<span className='text-xl font-bold'>DebateAI by</span>
<div className="flex items-center h-16 px-4 border-b border-border">
<div className="flex items-center gap-2">
<span className="text-xl font-bold">DebateAI by</span>
<img
src={debateAiLogo}
alt='DebateAI Logo'
className='h-8 w-auto object-contain'
alt="DebateAI Logo"
className="h-8 w-auto object-contain"
/>
</div>
</div>
{/* Nav links */}
<nav className='flex-1 px-2 py-4 space-y-2 overflow-y-auto'>
<nav className="flex-1 px-2 py-4 space-y-2 overflow-y-auto">
<NavItem
to='/startDebate'
label='Start Debate'
icon={<MessageSquare className='mr-3 h-4 w-4' />}
to="/startDebate"
label="Start Debate"
icon={<MessageSquare className="mr-3 h-4 w-4" />}
/>
<NavItem
to='/tournaments'
label='Tournaments'
icon={<Trophy className='mr-3 h-4 w-4' />}
to="/tournaments"
label="Tournaments"
icon={<Trophy className="mr-3 h-4 w-4" />}
/>
<NavItem
to='/team-builder'
label='Team Debates'
icon={<Users className='mr-3 h-4 w-4' />}
to="/team-builder"
label="Team Debates"
icon={<Users className="mr-3 h-4 w-4" />}
/>
<NavItem
to='/leaderboard'
label='Leaderboard'
icon={<BarChart className='mr-3 h-4 w-4' />}
to="/leaderboard"
label="Leaderboard"
icon={<BarChart className="mr-3 h-4 w-4" />}
/>
<NavItem
to='/community'
label='Community'
icon={<MessageCircle className='mr-3 h-4 w-4' />}
to="/community"
label="Community"
icon={<MessageCircle className="mr-3 h-4 w-4" />}
/>
<NavItem
to='/profile'
label='Profile'
icon={<User className='mr-3 h-4 w-4' />}
to="/profile"
label="Profile"
icon={<User className="mr-3 h-4 w-4" />}
/>
<NavItem
to='/about'
label='About'
icon={<Info className='mr-3 h-4 w-4' />}
to="/about"
label="About"
icon={<Info className="mr-3 h-4 w-4" />}
/>
<ThemeToggle />
</nav>
Expand All @@ -81,9 +81,10 @@ function NavItem({ to, label, icon }: NavItemProps) {
<NavLink
to={to}
className={({ isActive }) =>
`group flex items-center px-2 py-2 text-sm font-medium rounded-md ${isActive
? 'bg-secondary text-secondary-foreground'
: 'text-foreground hover:bg-muted hover:text-foreground'
`group flex items-center px-2 py-2 text-sm font-medium rounded-md ${
isActive
? "bg-secondary text-secondary-foreground"
: "text-foreground hover:bg-muted hover:text-foreground"
}`
}
>
Expand Down