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:
@@ -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 };
|
||||
Reference in New Issue
Block a user