fix(ui): wire and restyle dashboard notifications dropdown

This commit is contained in:
Tracewebstudio Dev 2026-04-29 11:51:37 +02:00
parent 1effa1cb6f
commit d7ddd3d9e0
2 changed files with 42 additions and 79 deletions

View file

@ -3,8 +3,9 @@
* Used for pages that need actual backend connectivity
* (My Profile, My Portfolio, Verification) instead of the preview mock.
*/
import { For, JSX, Show, createMemo, createSignal, onMount } from "solid-js";
import { For, JSX, createMemo } from "solid-js";
import { AiChatWidget } from "./AiChatWidget";
import NotificationBell from "./NotificationBell";
import {
User,
Briefcase,
@ -101,36 +102,6 @@ export default function DashboardShell(props: Props) {
return k.charAt(0).toUpperCase() + k.slice(1).toLowerCase();
});
const [unreadCount, setUnreadCount] = createSignal(0);
// Fetch unread notification count
const fetchUnreadCount = async () => {
try {
const token =
typeof window !== "undefined"
? window.sessionStorage.getItem("nxtgauge_access_token") || ""
: "";
if (!token) return;
const res = await fetch("/api/me/notifications/unread-count", {
headers: { Authorization: `Bearer ${token}` },
credentials: "include",
});
if (res.ok) {
const data = await res.json();
setUnreadCount(data.unread_count || 0);
}
} catch (e) {
console.error("Failed to fetch unread count:", e);
}
};
// Start polling on mount
onMount(() => {
fetchUnreadCount();
const interval = setInterval(fetchUnreadCount, 30000);
return () => clearInterval(interval);
});
return (
<div
style={{
@ -266,35 +237,7 @@ export default function DashboardShell(props: Props) {
{titleCase(props.activeSidebar)}
</p>
<div style={{ display: "flex", "align-items": "center", gap: "12px" }}>
<button
type="button"
style={{
position: "relative",
border: "none",
background: "transparent",
cursor: "pointer",
display: "flex",
"align-items": "center",
"justify-content": "center",
padding: 0,
}}
>
<Bell size={18} style={{ color: "#9CA3AF" }} />
<Show when={unreadCount() > 0}>
<span
style={{
position: "absolute",
top: "-2px",
right: "-2px",
width: "8px",
height: "8px",
background: "#FF5E13",
"border-radius": "50%",
border: "1px solid white",
}}
></span>
</Show>
</button>
<NotificationBell />
<div
style={{
width: "32px",

View file

@ -1,6 +1,9 @@
import { createSignal, createEffect, onCleanup, Show } from "solid-js";
import { api } from "~/lib/api";
const ORANGE = "#FF5E13";
const NAVY = "#0D0D2A";
export default function NotificationBell() {
const [unreadCount, setUnreadCount] = createSignal(0);
const [showDropdown, setShowDropdown] = createSignal(false);
@ -84,7 +87,7 @@ export default function NotificationBell() {
<div class="relative">
<button
onClick={toggleDropdown}
class="relative p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-full transition-colors"
class="relative h-8 w-8 rounded-full border border-[#E5E7EB] bg-white text-[#6B7280] hover:text-[#111827] hover:border-[#D1D5DB] transition-colors flex items-center justify-center"
aria-label="Notifications"
>
<svg
@ -104,7 +107,10 @@ export default function NotificationBell() {
{/* Unread Badge */}
<Show when={unreadCount() > 0}>
<span class="absolute top-0 right-0 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-white transform translate-x-1/4 -translate-y-1/4 bg-orange-500 rounded-full">
<span
class="absolute -top-1 -right-1 min-w-[16px] h-4 px-1 inline-flex items-center justify-center text-[10px] font-bold leading-none text-white rounded-full"
style={{ background: ORANGE }}
>
{unreadCount() > 99 ? "99+" : unreadCount()}
</span>
</Show>
@ -117,33 +123,46 @@ export default function NotificationBell() {
<div class="fixed inset-0 z-40" onClick={() => setShowDropdown(false)} />
{/* Dropdown Panel */}
<div class="absolute right-0 mt-2 w-80 bg-white rounded-xl shadow-lg border z-50 overflow-hidden">
<div class="flex justify-between items-center p-4 border-b">
<h3 class="font-semibold">Notifications</h3>
<div class="absolute right-0 mt-2 w-[360px] max-w-[calc(100vw-24px)] bg-white rounded-2xl border border-[#E5E7EB] shadow-[0_10px_30px_rgba(2,6,23,0.08)] z-50 overflow-hidden">
<div class="flex justify-between items-center px-4 py-3 border-b border-[#E5E7EB] bg-[#FCFCFD]">
<div class="flex items-center gap-2">
<h3 class="text-sm font-semibold" style={{ color: NAVY }}>Notifications</h3>
<Show when={unreadCount() > 0}>
<span class="text-[11px] font-semibold px-2 py-0.5 rounded-full bg-[#FFF3EE] text-[#C2410C]">
{unreadCount()} unread
</span>
</Show>
</div>
<Show when={unreadCount() > 0}>
<button
onClick={markAllAsRead}
class="text-sm text-orange-600 hover:text-orange-700"
class="text-xs font-semibold hover:opacity-90"
style={{ color: ORANGE }}
>
Mark all read
</button>
</Show>
</div>
<div class="max-h-96 overflow-y-auto">
<div class="max-h-[420px] overflow-y-auto">
<Show
when={notifications().length > 0}
fallback={
<div class="p-8 text-center text-gray-500">
<p class="text-4xl mb-2">🔔</p>
<p>No notifications yet</p>
<div class="px-6 py-10 text-center">
<div class="mx-auto mb-3 w-10 h-10 rounded-full bg-[#F3F4F6] flex items-center justify-center text-[#9CA3AF]">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width={1.8} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
</div>
<p class="text-sm font-semibold text-[#374151]">No notifications yet</p>
<p class="text-xs text-[#6B7280] mt-1">We will show updates here when they arrive.</p>
</div>
}
>
{notifications().map((notification) => (
<div
class={`p-4 border-b hover:bg-gray-50 cursor-pointer transition-colors ${
!notification.is_read ? "bg-orange-50" : ""
class={`px-4 py-3 border-b border-[#F1F5F9] hover:bg-[#F8FAFC] cursor-pointer transition-colors ${
!notification.is_read ? "bg-[#FFF7ED]" : "bg-white"
}`}
onClick={() => {
if (!notification.is_read) {
@ -153,20 +172,20 @@ export default function NotificationBell() {
>
<div class="flex items-start gap-3">
{/* Unread Dot */}
<div class="mt-1.5">
<div class="mt-1.5 shrink-0">
<div
class={`w-2 h-2 rounded-full ${
!notification.is_read ? "bg-orange-500" : "bg-transparent"
!notification.is_read ? "bg-[#F97316]" : "bg-transparent"
}`}
/>
</div>
<div class="flex-1 min-w-0">
<p class="font-medium text-sm text-gray-900 line-clamp-1">
<p class="font-semibold text-sm text-[#111827] line-clamp-1">
{notification.title}
</p>
<p class="text-sm text-gray-600 line-clamp-2 mt-0.5">{notification.body}</p>
<p class="text-xs text-gray-400 mt-1">
<p class="text-sm text-[#4B5563] line-clamp-2 mt-0.5">{notification.body}</p>
<p class="text-[11px] text-[#9CA3AF] mt-1.5">
{formatTime(notification.created_at)}
</p>
</div>
@ -176,10 +195,11 @@ export default function NotificationBell() {
</Show>
</div>
<div class="p-3 border-t bg-gray-50">
<div class="p-3 border-t border-[#E5E7EB] bg-[#FCFCFD]">
<a
href="/dashboard/notifications"
class="block text-center text-sm text-orange-600 hover:text-orange-700 font-medium"
class="block text-center text-sm font-semibold hover:opacity-90"
style={{ color: ORANGE }}
onClick={() => setShowDropdown(false)}
>
View all notifications