-
-
-
- {/* shahd --implement top bar here */}
-
+// ─── Tabs ─────────────────────────────────────────────────────────────────────
-
+const TABS = [
+ { label: "Rythmify Tracks", to: "/artists", end: true },
+ { label: "Distribution", to: "/artists/distribution", end: false },
+ { label: "Vinyl Records", to: "/artists/vinyl", end: false },
+ { label: "Comments", to: "/artists/comments", end: false },
+];
+
+// ─── Sub-components ───────────────────────────────────────────────────────────
+
+function StatItem({ value, label }: { value: number; label: string }) {
+ return (
+
+
+ {value}
+
+ {label}
+
+ );
+}
+
+function IconStat({
+ icon,
+ label,
+ to,
+ tier,
+ tierColor,
+}: {
+ icon: React.ReactNode;
+ label: string;
+ to: string;
+ tier?: string;
+ tierColor?: "purple" | "gold";
+}) {
+ const isGold = tierColor === "gold";
+ const isPurple = tierColor === "purple";
+
+ return (
+
+
+
+ {icon}
+
+
+ {tier && (
+
+ {isGold ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+ {label}
+
+
+
+ {tier ?? ""}
+
+
+ );
+}
+
+// ─── Layout ───────────────────────────────────────────────────────────────────
+
+const ArtistStudioLayout = () => {
+ const [tracks, setTracks] = useState
-);
+ );
+};
-export default ArtistStudioLayout;
\ No newline at end of file
+export default ArtistStudioLayout;
diff --git a/src/components/layout/AuthMainLayout.tsx b/src/components/layout/AuthMainLayout.tsx
index c4ea3a98..feeb3ead 100644
--- a/src/components/layout/AuthMainLayout.tsx
+++ b/src/components/layout/AuthMainLayout.tsx
@@ -1,14 +1,104 @@
+import { useEffect } from "react";
import { Outlet } from "react-router-dom";
import MainNavbar from "./MainNavbar";
+import StickyPlayer from "../player/StickyPlayer";
+import { useAuthStore } from "@/stores/auth.store";
+import { useLikesStore } from "@/stores/likes.store";
+import { getMe, normalizeDateOfBirth } from "@/services/auth.service";
+import { getMySubscription } from "@/services/api/upload/subscription.service";
+import PremiumPromoModal from "@/components/Premium/PremiumPromoModal";
+import { usePromoModal } from "@/hooks/usePromoModal";
+import { getMyWebProfiles } from "@/services/user.service";
-const AuthMainLayout = () => (
-
-
+const formatProfileTitle = (platform: string) => {
+ if (platform === "other") return "Support";
+ return platform.charAt(0).toUpperCase() + platform.slice(1);
+};
-
-
-
-
-);
+const AuthMainLayout = () => {
+ const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
+ const currentUser = useAuthStore((s) => s.user);
+ const setUser = useAuthStore((s) => s.setUser);
+ const hydrateFromApi = useLikesStore((s) => s.hydrateFromApi);
+ const { showPromo, closePromo } = usePromoModal();
-export default AuthMainLayout;
\ No newline at end of file
+ useEffect(() => {
+ if (!isAuthenticated) return;
+
+ // Refresh user profile from backend on every app load so persisted
+ // data (following_ids, avatar, etc.) never stays stale across sessions.
+ Promise.allSettled([getMe(), getMySubscription(), getMyWebProfiles({ limit: 100, offset: 0 })])
+ .then(([profileResult, subResult, webProfilesResult]) => {
+ if (profileResult.status !== "fulfilled") return;
+ const p = profileResult.value.data;
+ const currentLinks = currentUser?.links ?? [];
+ const mergedLinks =
+ webProfilesResult.status === "fulfilled"
+ ? webProfilesResult.value.map((profile) => {
+ const fallback = currentLinks.find(
+ (link) =>
+ link.id === profile.id ||
+ link.url.trim() === profile.url.trim(),
+ );
+ return {
+ id: profile.id,
+ url: profile.url,
+ title: fallback?.title || formatProfileTitle(profile.platform),
+ isSupport: fallback?.isSupport ?? profile.platform === "other",
+ };
+ })
+ : currentLinks;
+ const isPro =
+ subResult.status === "fulfilled"
+ ? subResult.value.data.user_subscription_id !== null
+ : p.isPro ?? false;
+ setUser({
+ id: p.id,
+ username: p.username,
+ displayName: p.display_name,
+ firstName: p.first_name,
+ lastName: p.last_name,
+ bio: p.bio,
+ email: p.email,
+ role: p.role,
+ isPro,
+ avatar: p.profile_picture ?? currentUser?.avatar,
+ coverUrl: p.cover_photo ?? currentUser?.coverUrl,
+ city: p.city ?? currentUser?.city,
+ country: p.country ?? currentUser?.country,
+ location:
+ [p.city ?? currentUser?.city, p.country ?? currentUser?.country]
+ .filter(Boolean)
+ .join(", ") || currentUser?.location,
+ links: mergedLinks,
+ following_ids: p.following_ids ?? currentUser?.following_ids ?? [],
+ followers_ids: p.followers_ids ?? currentUser?.followers_ids ?? [],
+ date_of_birth:
+ normalizeDateOfBirth(p.date_of_birth) ??
+ currentUser?.date_of_birth ??
+ null,
+ gender: p.gender ?? currentUser?.gender ?? null,
+ });
+ });
+
+ hydrateFromApi();
+ }, [hydrateFromApi, isAuthenticated, setUser]);
+
+
+ return (
+
+ {showPromo &&
}
+
+
+ {/* pb-14 reserves space so content doesn't hide behind the 56px player bar */}
+
+
+
+
+ {/* Global player - mounts once and persists across all routes */}
+
+
+ );
+};
+
+export default AuthMainLayout;
diff --git a/src/components/layout/CheckoutLayout.tsx b/src/components/layout/CheckoutLayout.tsx
index 3197d115..a340c350 100644
--- a/src/components/layout/CheckoutLayout.tsx
+++ b/src/components/layout/CheckoutLayout.tsx
@@ -1,5 +1,4 @@
import { Outlet } from "react-router-dom";
-import Footer from "./Footer";
const CheckoutLayout = () => (
@@ -11,8 +10,8 @@ const CheckoutLayout = () => (
-
+
);
-export default CheckoutLayout;
\ No newline at end of file
+export default CheckoutLayout;
diff --git a/src/components/layout/DualViewLayout.tsx b/src/components/layout/DualViewLayout.tsx
index 8332370b..131a190b 100644
--- a/src/components/layout/DualViewLayout.tsx
+++ b/src/components/layout/DualViewLayout.tsx
@@ -1,19 +1,25 @@
-
import { Outlet } from "react-router-dom";
import { useAuthStore } from "../../stores/auth.store";
import MainNavbar from "./MainNavbar";
import NoAuthNavbar from "./NoAuthNavbar";
+import StickyPlayer from "../player/StickyPlayer";
+import { usePromoModal } from "@/hooks/usePromoModal";
+import PremiumPromoModal from "@/components/Premium/PremiumPromoModal";
const DualViewLayout = () => {
const { isAuthenticated } = useAuthStore();
+ const { showPromo, closePromo } = usePromoModal();
return (
+ {isAuthenticated && showPromo &&
}
{isAuthenticated ?
:
}
-
+
+
+
);
};
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx
index 6cf08cdb..b728ccb1 100644
--- a/src/components/layout/Footer.tsx
+++ b/src/components/layout/Footer.tsx
@@ -1,7 +1,42 @@
-import React from 'react'
+import { Link } from "react-router-dom";
-export default function Footer() {
- return (
-
Footer
- )
-}
+const FOOTER_LINKS = [
+ { label: "Directory", to: "/people" },
+ { label: "About us", to: "/pages/contact" },
+ { label: "Artist Resources", to: "/premium" },
+ { label: "Newsroom", to: "#" },
+ { label: "Topics", to: "#" },
+ { label: "Jobs", to: "#" },
+ { label: "Developers", to: "#" },
+ { label: "Help", to: "#" },
+ { label: "Legal", to: "/terms-of-use" },
+ { label: "Privacy", to: "#" },
+ { label: "Cookie Manager", to: "#" },
+ { label: "Imprint", to: "#" },
+ { label: "Charts", to: "/feed/charts" },
+ { label: "Transparency Reports", to: "#" },
+];
+
+const Footer = () => (
+
+);
+
+export default Footer;
diff --git a/src/components/layout/LandingLayout.tsx b/src/components/layout/LandingLayout.tsx
index 9db55ad3..4b8313ed 100644
--- a/src/components/layout/LandingLayout.tsx
+++ b/src/components/layout/LandingLayout.tsx
@@ -1,12 +1,14 @@
import { Outlet } from "react-router-dom";
import Footer from "./Footer";
+import NoAuthNavbar from "./NoAuthNavbar";
const LandingLayout = () => (
-
-
+
+
+
-
+
);
diff --git a/src/components/layout/MainNavbar.tsx b/src/components/layout/MainNavbar.tsx
index e21bafd3..abbeccdc 100644
--- a/src/components/layout/MainNavbar.tsx
+++ b/src/components/layout/MainNavbar.tsx
@@ -1,24 +1,64 @@
import { useState, useRef, useEffect } from "react";
import { NavLink, Link, useNavigate } from "react-router-dom";
import { useAuthStore } from "@/stores/auth.store";
+import { getMySubscription } from "@/services/api/upload/subscription.service";
+import { useNotificationStore } from "@/stores/notification.store";
+import {
+ Bell,
+ Mail,
+ ChevronDown,
+ MoreHorizontal,
+ Menu,
+ X,
+ Search,
+} from "lucide-react";
+import { disconnectSocket } from "@/services/api/messaging/socketService";
+import { usePlayerStore } from "@/stores/player.store";
+import { audio } from "@/services/audioService";
+import NotificationCard from "@/components/notificationsComponents/notificationCard";
+import {
+ fetchNotifications,
+ type Notification,
+} from "@/services/api/notifications/notificationsAPI";
+import {
+ fetchConversations,
+ type Conversation,
+} from "@/services/api/messaging/conversationApi";
+import { ChatProfile } from "@/components/MessagingComponents/ChatProfile";
+import { useMessagingStore } from "@/stores/messaging.store";
+import UserAvatar from "@/components/UI/UserAvatar";
+import SearchBar from "@/components/UI/SearchBar";
const MainNavbar = () => {
- const { user, logout } = useAuthStore();
+ const { user, logout, setUser } = useAuthStore();
+ const resetPlayer = usePlayerStore((s) => s.reset);
+ const { unreadCount, fetchUnreadCount } = useNotificationStore();
+ const {
+ unreadCount: unreadMessages,
+ fetchUnreadCount: fetchUnreadMessages,
+ refreshUnreadCount: refreshUnreadMessages,
+ setupSocketListeners,
+ } = useMessagingStore();
const navigate = useNavigate();
- // Dropdown states
const [showAvatarMenu, setShowAvatarMenu] = useState(false);
const [showNotifications, setShowNotifications] = useState(false);
const [showMessages, setShowMessages] = useState(false);
const [showMoreMenu, setShowMoreMenu] = useState(false);
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+ const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false);
+
+ const [notifications, setNotifications] = useState([]);
+ const [notificationsLoading, setNotificationsLoading] = useState(false);
+ const [conversations, setConversations] = useState([]);
+ const [conversationsLoading, setConversationsLoading] = useState(false);
- // Refs for click outside
const avatarRef = useRef(null);
const notifRef = useRef(null);
const msgRef = useRef(null);
const moreRef = useRef(null);
+ const notifListRef = useRef(null);
- // Close all dropdowns
const closeAll = () => {
setShowAvatarMenu(false);
setShowNotifications(false);
@@ -26,21 +66,101 @@ const MainNavbar = () => {
setShowMoreMenu(false);
};
- // Toggle a specific dropdown, close others
const toggle = (setter: React.Dispatch>) => {
closeAll();
setter((prev) => !prev);
};
- // Click outside handler
+ // ── Fetch last 9 notifications ───────────────────────────────────────────
+ const handleNotificationsToggle = async () => {
+ const willOpen = !showNotifications;
+ closeAll();
+ setShowNotifications(willOpen);
+
+ if (!willOpen) return;
+
+ setNotifications([]);
+ setNotificationsLoading(true);
+
+ try {
+ const res = await fetchNotifications(1, 9);
+ setNotifications(res.data.items ?? []);
+ } catch {
+ setNotifications([]);
+ } finally {
+ setNotificationsLoading(false);
+ }
+ };
+
+ const handleMessagesToggle = async () => {
+ const willOpen = !showMessages;
+ closeAll();
+ setShowMessages(willOpen);
+
+ if (!willOpen) return;
+
+ setConversationsLoading(true);
+ try {
+ const res = await fetchConversations(1, 6);
+ setConversations((res.data.items ?? []).slice(0, 6));
+ } catch {
+ setConversations([]);
+ } finally {
+ setConversationsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (!user) return;
+ getMySubscription()
+ .then(({ data }) => {
+ const isPro = data.user_subscription_id !== null;
+ if (user.isPro !== isPro) setUser({ ...user, isPro });
+ })
+ .catch(() => {});
+ }, []);
+
+ useEffect(() => {
+ fetchUnreadCount();
+ fetchUnreadMessages();
+ }, [fetchUnreadCount, fetchUnreadMessages]);
+
+ // ── Set up the global unread-badge socket listener once on mount ─────────
+ //
+ // We intentionally do NOT tear it down on Navbar unmount — this listener
+ // is global app-level state that must survive route changes. Calling
+ // teardownSocketListeners here would kill the listener for the lifetime
+ // of the session whenever the user navigates away and back.
+ useEffect(() => {
+ setupSocketListeners();
+ }, []);
+
+ // ── Poll every 30s + refresh on tab focus as fallback ───────────────────
+ useEffect(() => {
+ const poll = () => refreshUnreadMessages();
+ const interval = setInterval(poll, 30_000);
+ const onVisible = () => {
+ if (document.visibilityState === "visible") poll();
+ };
+ document.addEventListener("visibilitychange", onVisible);
+ return () => {
+ clearInterval(interval);
+ document.removeEventListener("visibilitychange", onVisible);
+ };
+ }, [refreshUnreadMessages]);
+
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as Node;
if (
- avatarRef.current && !avatarRef.current.contains(target) &&
- notifRef.current && !notifRef.current.contains(target) &&
- msgRef.current && !msgRef.current.contains(target) &&
- moreRef.current && !moreRef.current.contains(target)
+ avatarRef.current &&
+ !avatarRef.current.contains(target) &&
+ notifRef.current &&
+ !notifRef.current.contains(target) &&
+ msgRef.current &&
+ !msgRef.current.contains(target) &&
+ moreRef.current &&
+ !moreRef.current.contains(target)
) {
closeAll();
}
@@ -50,74 +170,81 @@ const MainNavbar = () => {
}, []);
const handleSignOut = () => {
+ disconnectSocket();
+ audio.pause();
+ audio.src = "";
+ resetPlayer();
logout();
- navigate("/logout");
+ navigate("/discover");
};
- // Nav link style helper
const navLinkClass = ({ isActive }: { isActive: boolean }) =>
- `text-sm font-medium px-1 py-1 border-b-2 transition-colors ${
+ `text-md font-semibold px-1 py-1 border-b-2 transition-colors hover:text-text-hover ${
isActive
- ? "text-text border-text"
- : "text-text-secondary border-transparent hover:text-text"
+ ? "text-text-hover border-text-hover"
+ : "text-text-secondary border-transparent"
}`;
- return (
-