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:
Anish Sarkar 2026-05-03 18:42:29 +05:30
parent 3cdfe8b5b6
commit a9192beae3
16 changed files with 189 additions and 160 deletions

View file

@ -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.",
}, },
}; };

View file

@ -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>

View file

@ -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);

View file

@ -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>
);
}

View file

@ -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>
); );

View file

@ -38,7 +38,7 @@ export function FooterNew() {
href: "/contact", href: "/contact",
}, },
{ {
title: "Announcements", title: "What's New",
href: "/announcements", href: "/announcements",
}, },
]; ];

View file

@ -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

View file

@ -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}

View file

@ -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"

View file

@ -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>
);
}

View file

@ -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}

View file

@ -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}
/> />
); );
})} })}

View file

@ -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}

View file

@ -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>

View file

@ -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";

View file

@ -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",