feat(ui): replace the notifications bell with an About dialog

The bell rendered hard-coded template data ("Roman Joined the Team!",
"New Payment received") — a demo artifact that had nothing to do with the
product and no backing feature.

In its place, an About dialog reports the running server's build: version,
build time, and commit, read from GET /api/v1/version, with a copy button so
the exact build can be quoted in a bug report without shelling onto the host.
That probe predates the {success,data} envelope and returns bare snake_case
JSON, so it is fetched directly rather than through the api client, accepting
either shape in case it is ever normalised. Dev builds ("dev"/"unknown")
degrade to a readable "—" rather than an invalid date.

Removing the bell also stranded three other unreferenced template files, so
Notification.tsx, AppLinks.tsx, QuickLinks.tsx and their shared data.ts (fake
users, chat/ecommerce app links) are deleted with it.

Also fixes the account button announcing itself to screen readers as
"show 11 new notifications".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Alphaeus Mote
2026-09-03 20:55:27 -04:00
parent 63701dd086
commit 39b39740a3
8 changed files with 139 additions and 453 deletions
@@ -9,8 +9,8 @@ import useMediaQuery from '@mui/material/useMediaQuery';
import { styled } from '@mui/material/styles';
import { IconMenu2 } from "@tabler/icons-react";
import About from "../../vertical/header/About";
import ApiDocsLink from "../../vertical/header/ApiDocsLink";
import Notifications from "../../vertical/header/Notification";
import Profile from "../../vertical/header/Profile";
import Search from "../../vertical/header/Search";
import Logo from "../../shared/logo/Logo";
@@ -85,7 +85,7 @@ export default function Header() {
)}
</IconButton>
<ApiDocsLink />
<Notifications />
<About />
<Profile />
</Stack>
</ToolbarStyled>
@@ -0,0 +1,134 @@
"use client";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogTitle from "@mui/material/DialogTitle";
import IconButton from "@mui/material/IconButton";
import Stack from "@mui/material/Stack";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import { Icon } from "@iconify/react";
import { useEffect, useState } from "react";
interface ServerVersion {
version: string;
buildTime: string;
gitCommit: string;
}
// The /api/v1/version probe predates the {success,data} envelope the rest of
// the API uses and returns bare snake_case JSON, so it is fetched directly
// rather than through the api client (which requires the envelope). Both
// shapes are accepted in case that endpoint is ever normalised.
async function fetchVersion(signal: AbortSignal): Promise<ServerVersion> {
const res = await fetch("/api/v1/version", { signal });
if (!res.ok) throw new Error(`version request failed (${res.status})`);
const body = await res.json();
const v = body?.data ?? body ?? {};
return {
version: v.version ?? v.Version ?? "unknown",
buildTime: v.build_time ?? v.buildTime ?? "",
gitCommit: v.git_commit ?? v.gitCommit ?? "",
};
}
function formatBuildTime(raw: string): string {
if (!raw || raw === "unknown") return "—";
const d = new Date(raw);
return Number.isNaN(d.getTime()) ? raw : d.toLocaleString();
}
// About shows the running server's build so an operator can confirm which
// version is deployed — and quote it verbatim in a bug report — without
// shelling onto the host.
export default function About() {
const [open, setOpen] = useState(false);
const [info, setInfo] = useState<ServerVersion | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!open) return;
const controller = new AbortController();
setError(null);
fetchVersion(controller.signal)
.then(setInfo)
.catch((err) => {
if (err?.name !== "AbortError") setError("Could not read the server version.");
});
return () => controller.abort();
}, [open]);
const summary = info
? `OrchestrAD ${info.version}${info.gitCommit ? ` (${info.gitCommit})` : ""}`
: "";
const rows: Array<[string, string]> = info
? [
["Version", info.version],
["Built", formatBuildTime(info.buildTime)],
["Commit", info.gitCommit || "—"],
]
: [];
return (
<>
<Tooltip title="About OrchestrAD">
<IconButton size="large" color="inherit" aria-label="About OrchestrAD" onClick={() => setOpen(true)}>
<Icon icon="solar:info-circle-line-duotone" width="21" height="21" />
</IconButton>
</Tooltip>
<Dialog open={open} onClose={() => setOpen(false)} fullWidth maxWidth="xs">
<DialogTitle>About OrchestrAD</DialogTitle>
<DialogContent dividers>
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
Active Directory rule automation.
</Typography>
{error && <Typography variant="body2" color="error">{error}</Typography>}
{!info && !error && (
<Typography variant="body2" color="textSecondary">Reading server version</Typography>
)}
{info && (
<Stack spacing={1.25}>
{rows.map(([label, value]) => (
<Stack key={label} direction="row" spacing={2} alignItems="baseline">
<Typography variant="caption" color="textSecondary" sx={{ minWidth: 72 }}>
{label}
</Typography>
<Typography variant="body2" sx={{ fontFamily: "monospace", wordBreak: "break-all" }}>
{value}
</Typography>
</Stack>
))}
</Stack>
)}
<Box sx={{ mt: 3 }}>
<Typography variant="caption" color="textSecondary">
API reference:{" "}
<a href="/api/docs" target="_blank" rel="noopener noreferrer">/api/docs</a>
</Typography>
</Box>
</DialogContent>
<DialogActions>
{info && (
<Button
startIcon={<ContentCopyIcon sx={{ fontSize: 16 }} />}
onClick={() => { void navigator.clipboard?.writeText(summary).catch(() => {}); }}
>
Copy
</Button>
)}
<Button onClick={() => setOpen(false)}>Close</Button>
</DialogActions>
</Dialog>
</>
);
}
@@ -1,72 +0,0 @@
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import { Grid } from '@mui/material';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import * as dropdownData from './data';
import Link from 'next/link';
import React from 'react';
const AppLinks = () => {
return (
(<Grid container spacing={3} mb={4}>
{dropdownData.appsLink.map((links, index) => (
<Grid
key={index}
size={{
lg: 6
}}>
<Link href={links.href} className="hover-text-primary">
<Stack direction="row" spacing={2}>
<Box
minWidth="45px"
height="45px"
bgcolor="grey.100"
display="flex"
alignItems="center"
justifyContent="center"
>
<Avatar
src={links.avatar}
alt={links.avatar}
sx={{
width: 24,
height: 24,
borderRadius: 0,
}}
/>
</Box>
<Box>
<Typography
variant="subtitle2"
fontWeight={600}
color="textPrimary"
noWrap
className="text-hover"
sx={{
width: '240px',
}}
>
{links.title}
</Typography>
<Typography
color="textSecondary"
variant="subtitle2"
fontSize="12px"
sx={{
width: '240px',
}}
noWrap
>
{links.subtext}
</Typography>
</Box>
</Stack>
</Link>
</Grid>
))}
</Grid>)
);
};
export default AppLinks;
@@ -8,8 +8,8 @@ import { styled } from '@mui/material/styles';
import config from '@/app/context/config'
import { useContext } from "react";
import { Icon } from "@iconify/react";
import About from "./About";
import ApiDocsLink from "./ApiDocsLink";
import Notifications from "./Notification";
import Profile from "./Profile";
import Search from "./Search";
@@ -88,7 +88,7 @@ const Header = () => {
</IconButton>
<ApiDocsLink />
<Notifications />
<About />
<Profile />
</Stack>
</ToolbarStyled>
@@ -1,157 +0,0 @@
import React, { useState } from "react";
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Typography from '@mui/material/Typography';
import * as dropdownData from "./data";
import Scrollbar from "@/app/components/custom-scroll/Scrollbar";
import { Icon } from "@iconify/react";
import { Stack } from "@mui/system";
import Link from "next/link";
const Notifications = () => {
const [anchorEl2, setAnchorEl2] = useState<HTMLElement | null>(null);
const handleClick2 = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl2(event.currentTarget);
};
const handleClose2 = () => {
setAnchorEl2(null);
};
return (
<Box>
<Button
size="large"
aria-label="show 11 new notifications"
aria-controls="msgs-menu"
aria-haspopup="true"
className="btn-rounded-circle-40"
color="inherit"
onClick={handleClick2}
>
<Box
sx={{
position: "relative",
top: "5px",
animationName: "pulse",
}}
>
<Icon icon="solar:bell-bing-line-duotone" width="24" height="24" />
<Box
sx={{
position: "absolute",
top: "-14px",
right: "-5px",
height: "18px",
width: "18px",
zIndex: "10",
border: "2px solid #4bd08b",
borderRadius: "70px",
animationIterationCount: "infinite !important",
animation: "heartbit 1s ease-out"
}}
></Box>
<Box
sx={{
width: "4px",
height: "4px",
borderRadius: "30px",
position: "absolute",
right: "2px",
top: "-7px",
backgroundColor: "success.main"
}}
></Box>
</Box>
</Button>
{/* ------------------------------------------- */}
{/* Message Dropdown */}
{/* ------------------------------------------- */}
<Menu
id="msgs-menu"
anchorEl={anchorEl2}
keepMounted
open={Boolean(anchorEl2)}
onClose={handleClose2}
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
transformOrigin={{ horizontal: "right", vertical: "top" }}
sx={{
"& .MuiMenu-paper": {
width: "360px",
},
}}
>
<Stack
direction="row"
py={2}
px={4}
justifyContent="space-between"
alignItems="center"
>
<Typography variant="h6">Notifications</Typography>
<Chip label="5 new" color="primary" size="small" />
</Stack>
<Scrollbar sx={{ height: "385px" }}>
{dropdownData.notifications.map((notification, index) => (
<Box key={index}>
<MenuItem sx={{ py: 2, px: 4 }}>
<Stack direction="row" spacing={2}>
<Avatar
src={notification.avatar}
alt={notification.avatar}
sx={{
width: 48,
height: 48,
}}
/>
<Box>
<Typography
variant="subtitle2"
color="textPrimary"
fontWeight={600}
noWrap
sx={{
width: "240px",
}}
>
{notification.title}
</Typography>
<Typography
color="textSecondary"
variant="subtitle2"
sx={{
width: "240px",
}}
noWrap
>
{notification.subtitle}
</Typography>
</Box>
</Stack>
</MenuItem>
</Box>
))}
</Scrollbar>
<Box p={3} pb={1}>
<Button
href="/apps/email"
variant="outlined"
component={Link}
color="primary"
fullWidth
>
See all Notifications
</Button>
</Box>
</Menu>
</Box>
);
};
export default Notifications;
@@ -45,7 +45,7 @@ const Profile = () => {
<Box>
<Button
size="large"
aria-label="show 11 new notifications"
aria-label="Account menu"
color="inherit"
aria-controls="msgs-menu"
aria-haspopup="true"
@@ -1,28 +0,0 @@
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import * as dropdownData from './data';
import Link from 'next/link';
const QuickLinks = () => {
return (
<>
<Typography variant="h5">Quick Links</Typography>
<Stack spacing={2} mt={2}>
{dropdownData.pageLinks.map((pagelink, index) => (
<Link href={pagelink.href} key={index} className="hover-text-primary">
<Typography
variant="subtitle2"
color="textPrimary"
className="text-hover"
fontWeight={600}
>
{pagelink.title}
</Typography>
</Link>
))}
</Stack>
</>
);
};
export default QuickLinks;
@@ -1,191 +0,0 @@
// Notifications dropdown
interface notificationType {
avatar: string;
title: string;
subtitle: string;
}
const notifications: notificationType[] = [
{
avatar: "/images/profile/user-1.jpg",
title: "Roman Joined the Team!",
subtitle: "Congratulate him",
},
{
avatar: "/images/profile/user-2.jpg",
title: "New message received",
subtitle: "Salma sent you new message",
},
{
avatar: "/images/profile/user-3.jpg",
title: "New Payment received",
subtitle: "Check your earnings",
},
{
avatar: "/images/profile/user-4.jpg",
title: "Jolly completed tasks",
subtitle: "Assign her new tasks",
},
{
avatar: "/images/profile/user-1.jpg",
title: "Roman Joined the Team!",
subtitle: "Congratulate him",
},
{
avatar: "/images/profile/user-2.jpg",
title: "New message received",
subtitle: "Salma sent you new message",
},
{
avatar: "/images/profile/user-3.jpg",
title: "New Payment received",
subtitle: "Check your earnings",
},
{
avatar: "/images/profile/user-4.jpg",
title: "Jolly completed tasks",
subtitle: "Assign her new tasks",
},
];
// Messages dropdown
interface messageType {
avatar: string;
title: string;
subtitle: string;
time: string;
}
const messages: messageType[] = [
{
avatar: "/images/profile/user1.jpg",
title: "Roman Joined the Team!",
subtitle: "Congratulate him",
time: '9:08 AM'
},
{
avatar: "/images/profile/user2.jpg",
title: "New message received",
subtitle: "Salma sent you new message",
time: '19:08 PM'
},
{
avatar: "/images/profile/user3.jpg",
title: "New Payment received",
subtitle: "Check your earnings",
time: '4:15 AM'
},
{
avatar: "/images/profile/user4.jpg",
title: "Jolly completed tasks",
subtitle: "Assign her new tasks",
time: '9:08 AM'
},
{
avatar: "/images/profile/user5.jpg",
title: "Roman Joined the Team!",
subtitle: "Congratulate him",
time: '12:08 AM'
},
];
// apps dropdown
interface appsLinkType {
href: string;
title: string;
subtext: string;
avatar: string;
}
const appsLink: appsLinkType[] = [
{
href: "/apps/chats",
title: "Chat Application",
subtext: "New messages arrived",
avatar: "/images/svgs/icon-dd-chat.svg",
},
{
href: "/apps/ecommerce/shop",
title: "eCommerce App",
subtext: "New stock available",
avatar: "/images/svgs/icon-dd-cart.svg",
},
{
href: "/apps/notes",
title: "Notes App",
subtext: "To-do and Daily tasks",
avatar: "/images/svgs/icon-dd-invoice.svg",
},
{
href: "/apps/calendar",
title: "Calendar App",
subtext: "Get dates",
avatar: "/images/svgs/icon-dd-date.svg",
},
{
href: "/apps/contacts",
title: "Contact Application",
subtext: "2 Unsaved Contacts",
avatar: "/images/svgs/icon-dd-mobile.svg",
},
{
href: "/apps/tickets",
title: "Tickets App",
subtext: "Submit tickets",
avatar: "/images/svgs/icon-dd-lifebuoy.svg",
},
{
href: "/apps/email",
title: "Email App",
subtext: "Get new emails",
avatar: "/images/svgs/icon-dd-message-box.svg",
},
{
href: "/apps/blog/post",
title: "Blog App",
subtext: "added new blog",
avatar: "/images/svgs/icon-dd-application.svg",
},
];
interface LinkType {
href: string;
title: string;
}
const pageLinks: LinkType[] = [
{
href: "/theme-pages/pricing",
title: "Pricing Page",
},
{
href: "/login",
title: "Sign In",
},
{
href: "/404",
title: "404 Error Page",
},
{
href: "/apps/note",
title: "Notes App",
},
{
href: "/apps/user-profile/profile",
title: "User Application",
},
{
href: "/apps/blog/post",
title: "Blog Design",
},
{
href: "/apps/ecommerce/checkout",
title: "Shopping Cart",
},
];
export { notifications, pageLinks, appsLink, messages };