Skip to content
Merged
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
141 changes: 126 additions & 15 deletions apps/web/app/influencer-dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,80 @@
"use client";

import React, { useState } from "react";
import React, { useState, useEffect, useRef } from "react";
import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";
import { useRouter, usePathname } from "next/navigation";
import {
LayoutDashboard,
Users,
Briefcase,
MessageSquare,
Calendar,
DollarSign,
LogOut,
Menu,
X
} from "lucide-react";

import { useAuthStore } from "@/store/auth.store";
import RoleGuard from "@/components/rbac/RoleGuard";
import DashboardHeader from "@/components/DashboardHeader";
import api from "@/lib/axios.client";
import NotificationBell from "@/components/NotificationBell";


export default function InfluencerDashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const pathname = usePathname();
const { user, clearAuth } = useAuthStore();
const [showLogoutModal, setShowLogoutModal] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [profile, setProfile] = useState<{ fullName?: string; profileImageUrl?: string; companyName?: string } | null>(null);
const [isLoadingProfile, setIsLoadingProfile] = useState(true);

// Fetch real profile data for name & photo
useEffect(() => {
setIsLoadingProfile(true);
api.get("/profile/get_profile")
.then((res) => setProfile(res.data.data))
.catch(() => { }) // silently fail — fallback to auth store values
.finally(() => setIsLoadingProfile(false));
}, []);

const rawDisplayName = profile?.fullName || user?.fullName || user?.email?.split("@")[0] || "User";
const displayName = rawDisplayName.trim() || "User";
const profileImage = (profile?.profileImageUrl || user?.profileImageUrl || user?.profileImage || "").trim() || null;

const dropdownRef = useRef<HTMLDivElement>(null);

// Close dropdown on outside click
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setShowLogoutModal(false);
}
}
if (showLogoutModal) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [showLogoutModal]);

const handleLogout = async () => {
try {
await api.post("/auth/logout");
} catch (e) {
console.error("Logout error", e);
} finally {
clearAuth();
router.push("/login");
}
};

const navItems = [
{ name: "Overview", href: "/influencer-dashboard", icon: LayoutDashboard },
Expand All @@ -36,8 +86,8 @@ export default function InfluencerDashboardLayout({
];

return (
<RoleGuard allowedRoles={["INFLUENCER"]}>
<div className="flex h-screen bg-[#F1F5F9] overflow-hidden">
<RoleGuard allowedRoles={["INFLUENCER", "BRAND"]}>
<div className="flex h-screen bg-[#F8FAFC] overflow-hidden">

{/* Mobile Sidebar Overlay */}
{isSidebarOpen && (
Expand All @@ -61,6 +111,7 @@ export default function InfluencerDashboardLayout({
</button>
</div>


<nav className="flex flex-col gap-1 flex-1 px-4">
{navItems.map((item) => {
const isActive = pathname === item.href;
Expand Down Expand Up @@ -97,23 +148,83 @@ export default function InfluencerDashboardLayout({
{/* Main Area */}
<main className="flex-1 flex flex-col min-w-0 h-screen overflow-hidden">
{/* Header */}
<DashboardHeader
isFixed={false}
showSidebarToggle={true}
hideLogo={true}
onSidebarToggle={() => setIsSidebarOpen(true)}
>
<div className="flex items-center gap-6">
<header className="bg-white border-b border-gray-100 px-4 sm:px-8 py-3 lg:py-4 flex items-center justify-between lg:justify-end shrink-0 z-30">
<button
className="lg:hidden text-gray-500 hover:text-gray-900"
onClick={() => setIsSidebarOpen(true)}
>
<Menu className="w-6 h-6" />
</button>

<div className="flex items-center gap-6 ml-auto">
{/* Explore Gigs Button */}
<Link
href="/gig/create"
href="/gig-list"
className="hidden md:flex items-center justify-center bg-emerald-500 hover:bg-emerald-600 text-white px-5 py-2 rounded-xl font-bold text-sm transition-colors shadow-sm"
>
Create Gig
Explore gigs
</Link>

<NotificationBell />

<div className="relative" ref={dropdownRef}>
<button
onClick={() => setShowLogoutModal(!showLogoutModal)}
className="flex items-center gap-3 group focus:outline-none"
>
<div className="text-right hidden sm:block min-w-[80px]">
{isLoadingProfile ? (
<div className="space-y-1">
<div className="h-4 w-24 bg-gray-100 animate-pulse rounded"></div>
<div className="h-3 w-16 bg-gray-50 animate-pulse rounded ml-auto"></div>
</div>
) : (
<>
<p className="text-sm font-bold text-gray-900">{displayName}</p>
<p className="text-xs text-gray-500 font-medium">Active Member</p>
</>
)}
</div>
<div className="w-10 h-10 rounded-full bg-emerald-100 flex items-center justify-center text-emerald-600 font-bold border-2 border-white shadow-sm group-hover:shadow-md transition-all overflow-hidden relative">
{isLoadingProfile ? (
<div className="absolute inset-0 bg-gray-100 animate-pulse"></div>
) : profileImage ? (
<img
src={profileImage}
alt={displayName}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
(e.target as HTMLImageElement).parentElement!.classList.add('bg-emerald-100');
}}
/>
) : (
displayName.charAt(0).toUpperCase()
)}
</div>
</button>
{showLogoutModal && (
<div className="absolute right-0 mt-3 w-56 bg-white rounded-xl shadow-[0_10px_40px_-10px_rgba(0,0,0,0.15)] border border-gray-100 py-2 z-50">
<div className="px-4 py-3 border-b border-gray-50 mb-1">
<p className="text-sm text-gray-900 font-bold truncate">{user?.email}</p>
<p className="text-xs text-gray-500 mt-0.5">
{user?.role === "INFLUENCER" ? "Influencer" : "Brand"} Dashboard
</p>
</div>

<button
onClick={handleLogout}
className="w-full text-left px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 flex items-center gap-2 font-medium"
>
<LogOut className="w-4 h-4" />
Sign Out
</button>
</div>
)}
</div>
</DashboardHeader>
</div>
</header>

{/* Content */}
<div
Expand Down
14 changes: 10 additions & 4 deletions apps/web/components/NotificationBell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@ export default function NotificationBell() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);

const mountedRef = useRef(false);

useEffect(() => {
mountedRef.current = true;
}, []);

// Fetch notifications on mount if user is logged in
useEffect(() => {
if (accessToken) {
if (mountedRef && accessToken) {
fetchNotifications();
}
}, [accessToken, fetchNotifications]);
}, [mountedRef, accessToken, fetchNotifications]);

// Handle clicking outside to close dropdown
useEffect(() => {
Expand All @@ -38,8 +44,8 @@ export default function NotificationBell() {
};
}, [isOpen]);

// Don't render the bell if the user is not authenticated
if (!accessToken) return null;
// Don't render the bell if the user is not authenticated or component is not mounted
if (!mountedRef || !accessToken) return null;

const handleNotificationClick = (notification: Notification) => {
console.log("🔥 CLICKED NOTIFICATION FULL:", JSON.stringify(notification, null, 2));
Expand Down
5 changes: 3 additions & 2 deletions apps/web/store/notification.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface Notification {
gigRequestId?: string;
conversationId?: string;
};
isRead?: boolean;
createdAt: string;
}

Expand All @@ -35,7 +36,7 @@ export const useNotificationStore = create<NotificationStore>((set, get) => ({
// ✅ Normalize incoming data (VERY IMPORTANT)
const normalized: Notification = {
...notification,
read: notification.read ?? false,
read: notification.isRead ?? notification.read ?? false,
metadata: notification.metadata || {},
};

Expand Down Expand Up @@ -70,7 +71,7 @@ if (Array.isArray(raw)) {
// const unreadCount = notifications.filter((n) => !n.read).length;
const normalized = notifications.map((n) => ({
...n,
read: n.read ?? false,
read: n.isRead ?? n.read ?? false,
metadata: n.metadata || {},
}));

Expand Down
Binary file modified data.ms/auth/lock.mdb
Binary file not shown.
Binary file modified data.ms/indexes/1d141789-4d51-4a8c-b800-fead2df19ab9/data.mdb
Binary file not shown.
Binary file modified data.ms/indexes/1d141789-4d51-4a8c-b800-fead2df19ab9/lock.mdb
Binary file not shown.
Binary file modified data.ms/indexes/44b06117-319a-482a-ac6a-bb3a24ded65c/data.mdb
Binary file not shown.
Binary file modified data.ms/indexes/44b06117-319a-482a-ac6a-bb3a24ded65c/lock.mdb
Binary file not shown.
Binary file modified data.ms/indexes/a7b76728-97b9-4942-bdaf-9967d45f811a/data.mdb
Binary file not shown.
Binary file modified data.ms/indexes/a7b76728-97b9-4942-bdaf-9967d45f811a/lock.mdb
Binary file not shown.
Binary file modified data.ms/tasks/data.mdb
Binary file not shown.
Binary file modified data.ms/tasks/lock.mdb
Binary file not shown.
Loading