mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-23 19:05:16 +02:00
feat(announcements): rename "Announcements" to "What's New" across the application; add AnnouncementsDialog component for displaying updates; update empty state messaging; remove unused AnnouncementsSidebar component.
This commit is contained in:
parent
3cdfe8b5b6
commit
a9192beae3
16 changed files with 189 additions and 160 deletions
|
|
@ -2,20 +2,20 @@ import type { Metadata } from "next";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Announcements | SurfSense",
|
title: "What's New | SurfSense",
|
||||||
description: "Latest product updates, feature releases, and news from SurfSense.",
|
description: "Latest product updates, feature releases, and news from SurfSense.",
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: "https://surfsense.com/announcements",
|
canonical: "https://surfsense.com/announcements",
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "Announcements | SurfSense",
|
title: "What's New | SurfSense",
|
||||||
description: "Latest product updates, feature releases, and news from SurfSense.",
|
description: "Latest product updates, feature releases, and news from SurfSense.",
|
||||||
url: "https://surfsense.com/announcements",
|
url: "https://surfsense.com/announcements",
|
||||||
type: "website",
|
type: "website",
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: "Announcements | SurfSense",
|
title: "What's New | SurfSense",
|
||||||
description: "Latest product updates, feature releases, and news from SurfSense.",
|
description: "Latest product updates, feature releases, and news from SurfSense.",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export default function AnnouncementsPage() {
|
||||||
<div className="max-w-5xl mx-auto relative">
|
<div className="max-w-5xl mx-auto relative">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h1 className="text-4xl font-bold tracking-tight bg-linear-to-r from-gray-900 to-gray-600 dark:from-white dark:to-gray-400 bg-clip-text text-transparent">
|
<h1 className="text-4xl font-bold tracking-tight bg-linear-to-r from-gray-900 to-gray-600 dark:from-white dark:to-gray-400 bg-clip-text text-transparent">
|
||||||
Announcements
|
What's New
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -21,3 +21,5 @@ export const userSettingsDialogAtom = atom<UserSettingsDialogState>({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const teamDialogAtom = atom<boolean>(false);
|
export const teamDialogAtom = atom<boolean>(false);
|
||||||
|
|
||||||
|
export const announcementsDialogAtom = atom<boolean>(false);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useAtom } from "jotai";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { announcementsDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
|
||||||
|
import { AnnouncementCard } from "@/components/announcements/AnnouncementCard";
|
||||||
|
import { AnnouncementsEmptyState } from "@/components/announcements/AnnouncementsEmptyState";
|
||||||
|
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { useAnnouncements } from "@/hooks/use-announcements";
|
||||||
|
|
||||||
|
export function AnnouncementsDialog() {
|
||||||
|
const [open, setOpen] = useAtom(announcementsDialogAtom);
|
||||||
|
const { announcements, markAllRead } = useAnnouncements();
|
||||||
|
|
||||||
|
// Auto-mark all visible announcements as read when the dialog opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
markAllRead();
|
||||||
|
}
|
||||||
|
}, [open, markAllRead]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent className="select-none max-w-[900px] w-[95vw] md:w-[90vw] h-[90vh] md:h-[80vh] max-h-[640px] flex flex-col p-0 gap-0 overflow-hidden [--card:var(--background)] dark:[--card:oklch(0.205_0_0)] dark:[--background:oklch(0.205_0_0)]">
|
||||||
|
<DialogTitle className="sr-only">What's New</DialogTitle>
|
||||||
|
|
||||||
|
<div className="flex flex-1 flex-col overflow-hidden min-w-0">
|
||||||
|
<div className="px-6 md:px-8 pt-6 pb-2 shrink-0">
|
||||||
|
<h2 className="text-lg font-semibold">What's New</h2>
|
||||||
|
<Separator className="mt-4" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
||||||
|
<div className="px-4 md:px-8 pt-4 pb-6 min-w-0">
|
||||||
|
{announcements.length === 0 ? (
|
||||||
|
<AnnouncementsEmptyState />
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{announcements.map((announcement) => (
|
||||||
|
<AnnouncementCard key={announcement.id} announcement={announcement} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -6,9 +6,9 @@ export function AnnouncementsEmptyState() {
|
||||||
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
|
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
|
||||||
<BellOff className="h-5 w-5 text-muted-foreground" />
|
<BellOff className="h-5 w-5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-semibold">No announcements</h3>
|
<h3 className="text-sm font-semibold">Nothing new yet</h3>
|
||||||
<p className="mt-1 max-w-xs text-xs text-muted-foreground">
|
<p className="mt-1 max-w-xs text-xs text-muted-foreground">
|
||||||
You're all caught up! New announcements will appear here.
|
You're all caught up! New updates will appear here.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export function FooterNew() {
|
||||||
href: "/contact",
|
href: "/contact",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Announcements",
|
title: "What's New",
|
||||||
href: "/announcements",
|
href: "/announcements",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Inbox, Megaphone, SquareLibrary } from "lucide-react";
|
import { Inbox, SquareLibrary } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
|
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
@ -55,28 +55,24 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
|
||||||
|
|
||||||
const navItems: NavItem[] = useMemo(
|
const navItems: NavItem[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[
|
(
|
||||||
{
|
[
|
||||||
title: "Inbox",
|
{
|
||||||
url: "#inbox",
|
title: "Inbox",
|
||||||
icon: Inbox,
|
url: "#inbox",
|
||||||
isActive: false,
|
icon: Inbox,
|
||||||
},
|
isActive: false,
|
||||||
isMobile
|
},
|
||||||
? {
|
isMobile
|
||||||
title: "Documents",
|
? {
|
||||||
url: "#documents",
|
title: "Documents",
|
||||||
icon: SquareLibrary,
|
url: "#documents",
|
||||||
isActive: false,
|
icon: SquareLibrary,
|
||||||
}
|
isActive: false,
|
||||||
: null,
|
}
|
||||||
{
|
: null,
|
||||||
title: "Announcements",
|
] as (NavItem | null)[]
|
||||||
url: "#announcements",
|
).filter((item): item is NavItem => item !== null),
|
||||||
icon: Megaphone,
|
|
||||||
isActive: false,
|
|
||||||
},
|
|
||||||
].filter((item): item is NavItem => item !== null),
|
|
||||||
[isMobile]
|
[isMobile]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -90,11 +86,12 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
|
||||||
(item: NavItem) => {
|
(item: NavItem) => {
|
||||||
if (item.title === "Inbox") gate("use the inbox");
|
if (item.title === "Inbox") gate("use the inbox");
|
||||||
else if (item.title === "Documents") setIsDocsSidebarOpen((v) => !v);
|
else if (item.title === "Documents") setIsDocsSidebarOpen((v) => !v);
|
||||||
else if (item.title === "Announcements") gate("view announcements");
|
|
||||||
},
|
},
|
||||||
[gate]
|
[gate]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleAnnouncements = useCallback(() => gate("see what's new"), [gate]);
|
||||||
|
|
||||||
const handleSearchSpaceSelect = useCallback(
|
const handleSearchSpaceSelect = useCallback(
|
||||||
(_id: number) => gate("switch search spaces"),
|
(_id: number) => gate("switch search spaces"),
|
||||||
[gate]
|
[gate]
|
||||||
|
|
@ -127,6 +124,7 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
|
||||||
onSettings={gatedAction("search space settings")}
|
onSettings={gatedAction("search space settings")}
|
||||||
onManageMembers={gatedAction("team management")}
|
onManageMembers={gatedAction("team management")}
|
||||||
onUserSettings={gatedAction("account settings")}
|
onUserSettings={gatedAction("account settings")}
|
||||||
|
onAnnouncements={handleAnnouncements}
|
||||||
onLogout={() => router.push("/register")}
|
onLogout={() => router.push("/register")}
|
||||||
pageUsage={pageUsage}
|
pageUsage={pageUsage}
|
||||||
isChatPage
|
isChatPage
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ interface IconRailProps {
|
||||||
onNavItemClick?: (item: NavItem) => void;
|
onNavItemClick?: (item: NavItem) => void;
|
||||||
user: User;
|
user: User;
|
||||||
onUserSettings?: () => void;
|
onUserSettings?: () => void;
|
||||||
|
onAnnouncements?: () => void;
|
||||||
|
announcementUnreadCount?: number;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
setTheme?: (theme: "light" | "dark" | "system") => void;
|
setTheme?: (theme: "light" | "dark" | "system") => void;
|
||||||
|
|
@ -42,6 +44,8 @@ export function IconRail({
|
||||||
onNavItemClick,
|
onNavItemClick,
|
||||||
user,
|
user,
|
||||||
onUserSettings,
|
onUserSettings,
|
||||||
|
onAnnouncements,
|
||||||
|
announcementUnreadCount = 0,
|
||||||
onLogout,
|
onLogout,
|
||||||
theme,
|
theme,
|
||||||
setTheme,
|
setTheme,
|
||||||
|
|
@ -138,6 +142,8 @@ export function IconRail({
|
||||||
<SidebarUserProfile
|
<SidebarUserProfile
|
||||||
user={user}
|
user={user}
|
||||||
onUserSettings={onUserSettings}
|
onUserSettings={onUserSettings}
|
||||||
|
onAnnouncements={onAnnouncements}
|
||||||
|
announcementUnreadCount={announcementUnreadCount}
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
isCollapsed
|
isCollapsed
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import { RightPanel, RightPanelExpandButton } from "../right-panel/RightPanel";
|
||||||
import {
|
import {
|
||||||
AllPrivateChatsSidebarContent,
|
AllPrivateChatsSidebarContent,
|
||||||
AllSharedChatsSidebarContent,
|
AllSharedChatsSidebarContent,
|
||||||
AnnouncementsSidebarContent,
|
|
||||||
DocumentsSidebar,
|
DocumentsSidebar,
|
||||||
InboxSidebarContent,
|
InboxSidebarContent,
|
||||||
MobileSidebar,
|
MobileSidebar,
|
||||||
|
|
@ -54,7 +53,7 @@ interface TabDataSource {
|
||||||
markAllAsRead: () => Promise<boolean>;
|
markAllAsRead: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ActiveSlideoutPanel = "inbox" | "shared" | "private" | "announcements" | null;
|
export type ActiveSlideoutPanel = "inbox" | "shared" | "private" | null;
|
||||||
|
|
||||||
// Inbox-related props — per-tab data sources with independent loading/pagination
|
// Inbox-related props — per-tab data sources with independent loading/pagination
|
||||||
interface InboxProps {
|
interface InboxProps {
|
||||||
|
|
@ -88,6 +87,8 @@ interface LayoutShellProps {
|
||||||
onSettings?: () => void;
|
onSettings?: () => void;
|
||||||
onManageMembers?: () => void;
|
onManageMembers?: () => void;
|
||||||
onUserSettings?: () => void;
|
onUserSettings?: () => void;
|
||||||
|
onAnnouncements?: () => void;
|
||||||
|
announcementUnreadCount?: number;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
pageUsage?: PageUsage;
|
pageUsage?: PageUsage;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
|
|
@ -189,6 +190,8 @@ export function LayoutShell({
|
||||||
onSettings,
|
onSettings,
|
||||||
onManageMembers,
|
onManageMembers,
|
||||||
onUserSettings,
|
onUserSettings,
|
||||||
|
onAnnouncements,
|
||||||
|
announcementUnreadCount = 0,
|
||||||
onLogout,
|
onLogout,
|
||||||
pageUsage,
|
pageUsage,
|
||||||
theme,
|
theme,
|
||||||
|
|
@ -237,9 +240,7 @@ export function LayoutShell({
|
||||||
? "Shared Chats"
|
? "Shared Chats"
|
||||||
: activeSlideoutPanel === "private"
|
: activeSlideoutPanel === "private"
|
||||||
? "Private Chats"
|
? "Private Chats"
|
||||||
: activeSlideoutPanel === "announcements"
|
: "Panel";
|
||||||
? "Announcements"
|
|
||||||
: "Panel";
|
|
||||||
|
|
||||||
// Mobile layout
|
// Mobile layout
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
|
|
@ -277,6 +278,8 @@ export function LayoutShell({
|
||||||
onSettings={onSettings}
|
onSettings={onSettings}
|
||||||
onManageMembers={onManageMembers}
|
onManageMembers={onManageMembers}
|
||||||
onUserSettings={onUserSettings}
|
onUserSettings={onUserSettings}
|
||||||
|
onAnnouncements={onAnnouncements}
|
||||||
|
announcementUnreadCount={announcementUnreadCount}
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
pageUsage={pageUsage}
|
pageUsage={pageUsage}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
@ -313,21 +316,6 @@ export function LayoutShell({
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
{activeSlideoutPanel === "announcements" && (
|
|
||||||
<motion.div
|
|
||||||
key="announcements"
|
|
||||||
className="h-full flex flex-col"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
|
||||||
<AnnouncementsSidebarContent
|
|
||||||
onOpenChange={(open) => closeSlideout(open)}
|
|
||||||
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
|
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="shared"
|
key="shared"
|
||||||
|
|
@ -397,6 +385,8 @@ export function LayoutShell({
|
||||||
onNavItemClick={onNavItemClick}
|
onNavItemClick={onNavItemClick}
|
||||||
user={user}
|
user={user}
|
||||||
onUserSettings={onUserSettings}
|
onUserSettings={onUserSettings}
|
||||||
|
onAnnouncements={onAnnouncements}
|
||||||
|
announcementUnreadCount={announcementUnreadCount}
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
setTheme={setTheme}
|
setTheme={setTheme}
|
||||||
|
|
@ -433,6 +423,8 @@ export function LayoutShell({
|
||||||
onSettings={onSettings}
|
onSettings={onSettings}
|
||||||
onManageMembers={onManageMembers}
|
onManageMembers={onManageMembers}
|
||||||
onUserSettings={onUserSettings}
|
onUserSettings={onUserSettings}
|
||||||
|
onAnnouncements={onAnnouncements}
|
||||||
|
announcementUnreadCount={announcementUnreadCount}
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
pageUsage={pageUsage}
|
pageUsage={pageUsage}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
@ -479,18 +471,6 @@ export function LayoutShell({
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
{activeSlideoutPanel === "announcements" && (
|
|
||||||
<motion.div
|
|
||||||
key="announcements"
|
|
||||||
className="h-full flex flex-col"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
|
||||||
<AnnouncementsSidebarContent onOpenChange={(open) => closeSlideout(open)} />
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
|
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="shared"
|
key="shared"
|
||||||
|
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ChevronLeft } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { AnnouncementCard } from "@/components/announcements/AnnouncementCard";
|
|
||||||
import { AnnouncementsEmptyState } from "@/components/announcements/AnnouncementsEmptyState";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { useAnnouncements } from "@/hooks/use-announcements";
|
|
||||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
|
||||||
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
|
|
||||||
|
|
||||||
export interface AnnouncementsSidebarContentProps {
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
onCloseMobileSidebar?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AnnouncementsSidebarProps extends AnnouncementsSidebarContentProps {
|
|
||||||
open: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AnnouncementsSidebarContent({
|
|
||||||
onOpenChange,
|
|
||||||
onCloseMobileSidebar,
|
|
||||||
}: AnnouncementsSidebarContentProps) {
|
|
||||||
const isMobile = !useMediaQuery("(min-width: 640px)");
|
|
||||||
const { announcements, markAllRead } = useAnnouncements();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
markAllRead();
|
|
||||||
}, [markAllRead]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full flex flex-col">
|
|
||||||
<div className="shrink-0 p-3 pb-1.5 space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{isMobile && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 rounded-full"
|
|
||||||
onClick={() => {
|
|
||||||
onOpenChange(false);
|
|
||||||
onCloseMobileSidebar?.();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<h2 className="text-md font-semibold">Announcements</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-3">
|
|
||||||
{announcements.length === 0 ? (
|
|
||||||
<AnnouncementsEmptyState />
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{announcements.map((announcement) => (
|
|
||||||
<AnnouncementCard key={announcement.id} announcement={announcement} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AnnouncementsSidebar({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
onCloseMobileSidebar,
|
|
||||||
}: AnnouncementsSidebarProps) {
|
|
||||||
return (
|
|
||||||
<SidebarSlideOutPanel open={open} onOpenChange={onOpenChange} ariaLabel="Announcements">
|
|
||||||
<AnnouncementsSidebarContent
|
|
||||||
onOpenChange={onOpenChange}
|
|
||||||
onCloseMobileSidebar={onCloseMobileSidebar}
|
|
||||||
/>
|
|
||||||
</SidebarSlideOutPanel>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -34,6 +34,8 @@ interface MobileSidebarProps {
|
||||||
onSettings?: () => void;
|
onSettings?: () => void;
|
||||||
onManageMembers?: () => void;
|
onManageMembers?: () => void;
|
||||||
onUserSettings?: () => void;
|
onUserSettings?: () => void;
|
||||||
|
onAnnouncements?: () => void;
|
||||||
|
announcementUnreadCount?: number;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
pageUsage?: PageUsage;
|
pageUsage?: PageUsage;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
|
|
@ -77,6 +79,8 @@ export function MobileSidebar({
|
||||||
onSettings,
|
onSettings,
|
||||||
onManageMembers,
|
onManageMembers,
|
||||||
onUserSettings,
|
onUserSettings,
|
||||||
|
onAnnouncements,
|
||||||
|
announcementUnreadCount = 0,
|
||||||
onLogout,
|
onLogout,
|
||||||
pageUsage,
|
pageUsage,
|
||||||
theme,
|
theme,
|
||||||
|
|
@ -193,6 +197,15 @@ export function MobileSidebar({
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
onAnnouncements={
|
||||||
|
onAnnouncements
|
||||||
|
? () => {
|
||||||
|
onOpenChange(false);
|
||||||
|
onAnnouncements();
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
announcementUnreadCount={announcementUnreadCount}
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
pageUsage={pageUsage}
|
pageUsage={pageUsage}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
|
||||||
|
|
@ -139,10 +139,6 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col gap-0.5 py-2", isCollapsed && "items-center")}>
|
<div className={cn("flex flex-col gap-0.5 py-2", isCollapsed && "items-center")}>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
const joyrideAttr =
|
|
||||||
item.title === "Inbox" || item.title.toLowerCase().includes("inbox")
|
|
||||||
? { "data-joyride": "inbox-sidebar" as const }
|
|
||||||
: {};
|
|
||||||
const { tooltip } = getStatusInfo(item.statusIndicator);
|
const { tooltip } = getStatusInfo(item.statusIndicator);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -159,12 +155,11 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti
|
||||||
<StatusIcon
|
<StatusIcon
|
||||||
status={item.statusIndicator}
|
status={item.statusIndicator}
|
||||||
FallbackIcon={item.icon}
|
FallbackIcon={item.icon}
|
||||||
className="h-3.5 w-3.5"
|
className="h-3.5 w-3.5"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
trailingContent={<StatusPill status={item.statusIndicator} />}
|
trailingContent={<StatusPill status={item.statusIndicator} />}
|
||||||
tooltipContent={tooltip}
|
tooltipContent={tooltip}
|
||||||
buttonProps={joyrideAttr}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { CreditCard, SquarePen, Zap } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
@ -53,6 +53,8 @@ interface SidebarProps {
|
||||||
onSettings?: () => void;
|
onSettings?: () => void;
|
||||||
onManageMembers?: () => void;
|
onManageMembers?: () => void;
|
||||||
onUserSettings?: () => void;
|
onUserSettings?: () => void;
|
||||||
|
onAnnouncements?: () => void;
|
||||||
|
announcementUnreadCount?: number;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
pageUsage?: PageUsage;
|
pageUsage?: PageUsage;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
|
|
@ -87,6 +89,8 @@ export function Sidebar({
|
||||||
onSettings,
|
onSettings,
|
||||||
onManageMembers,
|
onManageMembers,
|
||||||
onUserSettings,
|
onUserSettings,
|
||||||
|
onAnnouncements,
|
||||||
|
announcementUnreadCount = 0,
|
||||||
onLogout,
|
onLogout,
|
||||||
pageUsage,
|
pageUsage,
|
||||||
theme,
|
theme,
|
||||||
|
|
@ -101,6 +105,14 @@ export function Sidebar({
|
||||||
const t = useTranslations("sidebar");
|
const t = useTranslations("sidebar");
|
||||||
const [openDropdownChatId, setOpenDropdownChatId] = useState<number | null>(null);
|
const [openDropdownChatId, setOpenDropdownChatId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Inbox is rendered explicitly right below New Chat. Pull it out of the
|
||||||
|
// nav items list so it doesn't also appear in the bottom NavSection.
|
||||||
|
const inboxItem = useMemo(() => navItems.find((item) => item.url === "#inbox"), [navItems]);
|
||||||
|
const footerNavItems = useMemo(
|
||||||
|
() => navItems.filter((item) => item.url !== "#inbox"),
|
||||||
|
[navItems]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -138,7 +150,7 @@ export function Sidebar({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* New chat button */}
|
{/* New chat button + Inbox */}
|
||||||
<div className={cn("flex flex-col gap-0.5 py-1.5", isCollapsed && "items-center")}>
|
<div className={cn("flex flex-col gap-0.5 py-1.5", isCollapsed && "items-center")}>
|
||||||
<SidebarButton
|
<SidebarButton
|
||||||
icon={SquarePen}
|
icon={SquarePen}
|
||||||
|
|
@ -146,6 +158,21 @@ export function Sidebar({
|
||||||
onClick={onNewChat}
|
onClick={onNewChat}
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
/>
|
/>
|
||||||
|
{inboxItem && (
|
||||||
|
<SidebarButton
|
||||||
|
icon={inboxItem.icon}
|
||||||
|
label={inboxItem.title}
|
||||||
|
onClick={() => onNavItemClick?.(inboxItem)}
|
||||||
|
isCollapsed={isCollapsed}
|
||||||
|
isActive={inboxItem.isActive}
|
||||||
|
badge={inboxItem.badge}
|
||||||
|
buttonProps={
|
||||||
|
{
|
||||||
|
"data-joyride": "inbox-sidebar",
|
||||||
|
} as React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chat sections - fills available space */}
|
{/* Chat sections - fills available space */}
|
||||||
|
|
@ -271,8 +298,12 @@ export function Sidebar({
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="mt-auto border-t border-border/60">
|
<div className="mt-auto border-t border-border/60">
|
||||||
{/* Platform navigation */}
|
{/* Platform navigation */}
|
||||||
{navItems.length > 0 && (
|
{footerNavItems.length > 0 && (
|
||||||
<NavSection items={navItems} onItemClick={onNavItemClick} isCollapsed={isCollapsed} />
|
<NavSection
|
||||||
|
items={footerNavItems}
|
||||||
|
onItemClick={onNavItemClick}
|
||||||
|
isCollapsed={isCollapsed}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SidebarUsageFooter pageUsage={pageUsage} isCollapsed={isCollapsed} />
|
<SidebarUsageFooter pageUsage={pageUsage} isCollapsed={isCollapsed} />
|
||||||
|
|
@ -281,6 +312,8 @@ export function Sidebar({
|
||||||
<SidebarUserProfile
|
<SidebarUserProfile
|
||||||
user={user}
|
user={user}
|
||||||
onUserSettings={onUserSettings}
|
onUserSettings={onUserSettings}
|
||||||
|
onAnnouncements={onAnnouncements}
|
||||||
|
announcementUnreadCount={announcementUnreadCount}
|
||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
Info,
|
Info,
|
||||||
Languages,
|
Languages,
|
||||||
LogOut,
|
LogOut,
|
||||||
|
Megaphone,
|
||||||
Monitor,
|
Monitor,
|
||||||
Moon,
|
Moon,
|
||||||
Sun,
|
Sun,
|
||||||
|
|
@ -60,12 +61,22 @@ const LEARN_MORE_LINKS = [
|
||||||
interface SidebarUserProfileProps {
|
interface SidebarUserProfileProps {
|
||||||
user: User;
|
user: User;
|
||||||
onUserSettings?: () => void;
|
onUserSettings?: () => void;
|
||||||
|
onAnnouncements?: () => void;
|
||||||
|
announcementUnreadCount?: number;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
setTheme?: (theme: "light" | "dark" | "system") => void;
|
setTheme?: (theme: "light" | "dark" | "system") => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatAnnouncementCount(count: number): string {
|
||||||
|
if (count <= 999) {
|
||||||
|
return count.toString();
|
||||||
|
}
|
||||||
|
const thousands = Math.floor(count / 1000);
|
||||||
|
return `${thousands}k+`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a consistent color based on email
|
* Generates a consistent color based on email
|
||||||
*/
|
*/
|
||||||
|
|
@ -152,6 +163,8 @@ function UserAvatar({
|
||||||
export function SidebarUserProfile({
|
export function SidebarUserProfile({
|
||||||
user,
|
user,
|
||||||
onUserSettings,
|
onUserSettings,
|
||||||
|
onAnnouncements,
|
||||||
|
announcementUnreadCount = 0,
|
||||||
onLogout,
|
onLogout,
|
||||||
isCollapsed = false,
|
isCollapsed = false,
|
||||||
theme,
|
theme,
|
||||||
|
|
@ -228,6 +241,18 @@ export function SidebarUserProfile({
|
||||||
{t("user_settings")}
|
{t("user_settings")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
{onAnnouncements && (
|
||||||
|
<DropdownMenuItem onClick={onAnnouncements}>
|
||||||
|
<Megaphone className="h-4 w-4" />
|
||||||
|
<span className="flex-1">What's New</span>
|
||||||
|
{announcementUnreadCount > 0 && (
|
||||||
|
<span className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-medium">
|
||||||
|
{formatAnnouncementCount(announcementUnreadCount)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
{setTheme && (
|
{setTheme && (
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>
|
||||||
|
|
@ -382,6 +407,18 @@ export function SidebarUserProfile({
|
||||||
{t("user_settings")}
|
{t("user_settings")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
{onAnnouncements && (
|
||||||
|
<DropdownMenuItem onClick={onAnnouncements}>
|
||||||
|
<Megaphone className="h-4 w-4" />
|
||||||
|
<span className="flex-1">What's New</span>
|
||||||
|
{announcementUnreadCount > 0 && (
|
||||||
|
<span className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-medium">
|
||||||
|
{formatAnnouncementCount(announcementUnreadCount)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
{setTheme && (
|
{setTheme && (
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
export { AllPrivateChatsSidebar, AllPrivateChatsSidebarContent } from "./AllPrivateChatsSidebar";
|
export { AllPrivateChatsSidebar, AllPrivateChatsSidebarContent } from "./AllPrivateChatsSidebar";
|
||||||
export { AllSharedChatsSidebar, AllSharedChatsSidebarContent } from "./AllSharedChatsSidebar";
|
export { AllSharedChatsSidebar, AllSharedChatsSidebarContent } from "./AllSharedChatsSidebar";
|
||||||
export { AnnouncementsSidebar, AnnouncementsSidebarContent } from "./AnnouncementsSidebar";
|
|
||||||
export { ChatListItem } from "./ChatListItem";
|
export { ChatListItem } from "./ChatListItem";
|
||||||
export { DocumentsSidebar } from "./DocumentsSidebar";
|
export { DocumentsSidebar } from "./DocumentsSidebar";
|
||||||
export { InboxSidebar, InboxSidebarContent } from "./InboxSidebar";
|
export { InboxSidebar, InboxSidebarContent } from "./InboxSidebar";
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ import type { Announcement } from "@/contracts/types/announcement.types";
|
||||||
export const announcements: Announcement[] = [
|
export const announcements: Announcement[] = [
|
||||||
{
|
{
|
||||||
id: "announcement-1",
|
id: "announcement-1",
|
||||||
title: "Introducing Announcements",
|
title: "Introducing What's New",
|
||||||
description: "All major announcements will be posted here.",
|
description: "All major product updates will be posted here.",
|
||||||
category: "feature",
|
category: "feature",
|
||||||
date: "2026-02-17T00:00:00Z",
|
date: "2026-02-17T00:00:00Z",
|
||||||
startTime: "2026-02-17T00:00:00Z",
|
startTime: "2026-02-17T00:00:00Z",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue