fix(ui): wire and restyle dashboard notifications dropdown
This commit is contained in:
parent
1effa1cb6f
commit
d7ddd3d9e0
2 changed files with 42 additions and 79 deletions
|
|
@ -3,8 +3,9 @@
|
||||||
* Used for pages that need actual backend connectivity
|
* Used for pages that need actual backend connectivity
|
||||||
* (My Profile, My Portfolio, Verification) instead of the preview mock.
|
* (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 { AiChatWidget } from "./AiChatWidget";
|
||||||
|
import NotificationBell from "./NotificationBell";
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
Briefcase,
|
Briefcase,
|
||||||
|
|
@ -101,36 +102,6 @@ export default function DashboardShell(props: Props) {
|
||||||
return k.charAt(0).toUpperCase() + k.slice(1).toLowerCase();
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -266,35 +237,7 @@ export default function DashboardShell(props: Props) {
|
||||||
{titleCase(props.activeSidebar)}
|
{titleCase(props.activeSidebar)}
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
<div style={{ display: "flex", "align-items": "center", gap: "12px" }}>
|
||||||
<button
|
<NotificationBell />
|
||||||
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>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "32px",
|
width: "32px",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { createSignal, createEffect, onCleanup, Show } from "solid-js";
|
import { createSignal, createEffect, onCleanup, Show } from "solid-js";
|
||||||
import { api } from "~/lib/api";
|
import { api } from "~/lib/api";
|
||||||
|
|
||||||
|
const ORANGE = "#FF5E13";
|
||||||
|
const NAVY = "#0D0D2A";
|
||||||
|
|
||||||
export default function NotificationBell() {
|
export default function NotificationBell() {
|
||||||
const [unreadCount, setUnreadCount] = createSignal(0);
|
const [unreadCount, setUnreadCount] = createSignal(0);
|
||||||
const [showDropdown, setShowDropdown] = createSignal(false);
|
const [showDropdown, setShowDropdown] = createSignal(false);
|
||||||
|
|
@ -84,7 +87,7 @@ export default function NotificationBell() {
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button
|
<button
|
||||||
onClick={toggleDropdown}
|
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"
|
aria-label="Notifications"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -104,7 +107,10 @@ export default function NotificationBell() {
|
||||||
|
|
||||||
{/* Unread Badge */}
|
{/* Unread Badge */}
|
||||||
<Show when={unreadCount() > 0}>
|
<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()}
|
{unreadCount() > 99 ? "99+" : unreadCount()}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -117,33 +123,46 @@ export default function NotificationBell() {
|
||||||
<div class="fixed inset-0 z-40" onClick={() => setShowDropdown(false)} />
|
<div class="fixed inset-0 z-40" onClick={() => setShowDropdown(false)} />
|
||||||
|
|
||||||
{/* Dropdown Panel */}
|
{/* Dropdown Panel */}
|
||||||
<div class="absolute right-0 mt-2 w-80 bg-white rounded-xl shadow-lg border z-50 overflow-hidden">
|
<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 p-4 border-b">
|
<div class="flex justify-between items-center px-4 py-3 border-b border-[#E5E7EB] bg-[#FCFCFD]">
|
||||||
<h3 class="font-semibold">Notifications</h3>
|
<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}>
|
<Show when={unreadCount() > 0}>
|
||||||
<button
|
<button
|
||||||
onClick={markAllAsRead}
|
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
|
Mark all read
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-h-96 overflow-y-auto">
|
<div class="max-h-[420px] overflow-y-auto">
|
||||||
<Show
|
<Show
|
||||||
when={notifications().length > 0}
|
when={notifications().length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="p-8 text-center text-gray-500">
|
<div class="px-6 py-10 text-center">
|
||||||
<p class="text-4xl mb-2">🔔</p>
|
<div class="mx-auto mb-3 w-10 h-10 rounded-full bg-[#F3F4F6] flex items-center justify-center text-[#9CA3AF]">
|
||||||
<p>No notifications yet</p>
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{notifications().map((notification) => (
|
{notifications().map((notification) => (
|
||||||
<div
|
<div
|
||||||
class={`p-4 border-b hover:bg-gray-50 cursor-pointer transition-colors ${
|
class={`px-4 py-3 border-b border-[#F1F5F9] hover:bg-[#F8FAFC] cursor-pointer transition-colors ${
|
||||||
!notification.is_read ? "bg-orange-50" : ""
|
!notification.is_read ? "bg-[#FFF7ED]" : "bg-white"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!notification.is_read) {
|
if (!notification.is_read) {
|
||||||
|
|
@ -153,20 +172,20 @@ export default function NotificationBell() {
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
{/* Unread Dot */}
|
{/* Unread Dot */}
|
||||||
<div class="mt-1.5">
|
<div class="mt-1.5 shrink-0">
|
||||||
<div
|
<div
|
||||||
class={`w-2 h-2 rounded-full ${
|
class={`w-2 h-2 rounded-full ${
|
||||||
!notification.is_read ? "bg-orange-500" : "bg-transparent"
|
!notification.is_read ? "bg-[#F97316]" : "bg-transparent"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<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}
|
{notification.title}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-gray-600 line-clamp-2 mt-0.5">{notification.body}</p>
|
<p class="text-sm text-[#4B5563] line-clamp-2 mt-0.5">{notification.body}</p>
|
||||||
<p class="text-xs text-gray-400 mt-1">
|
<p class="text-[11px] text-[#9CA3AF] mt-1.5">
|
||||||
{formatTime(notification.created_at)}
|
{formatTime(notification.created_at)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -176,10 +195,11 @@ export default function NotificationBell() {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-3 border-t bg-gray-50">
|
<div class="p-3 border-t border-[#E5E7EB] bg-[#FCFCFD]">
|
||||||
<a
|
<a
|
||||||
href="/dashboard/notifications"
|
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)}
|
onClick={() => setShowDropdown(false)}
|
||||||
>
|
>
|
||||||
View all notifications
|
View all notifications
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue