diff --git a/backend/package.json b/backend/package.json index a7e4f7a..5c80ad8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-backend", - "version": "0.12.10", + "version": "0.12.11", "description": "RosterChirp backend server", "main": "src/index.js", "scripts": { diff --git a/build.sh b/build.sh index 4f58bd5..230aaa9 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.12.10}" +VERSION="${1:-0.12.11}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="rosterchirp" diff --git a/frontend/package.json b/frontend/package.json index 18528db..80e3eac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "rosterchirp-frontend", - "version": "0.12.10", + "version": "0.12.11", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/GlobalBar.jsx b/frontend/src/components/GlobalBar.jsx index 9d9b75d..0d61d08 100644 --- a/frontend/src/components/GlobalBar.jsx +++ b/frontend/src/components/GlobalBar.jsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useSocket } from '../contexts/SocketContext.jsx'; import { api } from '../utils/api.js'; -export default function GlobalBar({ isMobile, showSidebar, onBurger }) { +export default function GlobalBar({ isMobile, showSidebar, onBurger, hasUnread = false }) { const { connected } = useSocket(); const [settings, setSettings] = useState({ app_name: 'rosterchirp', logo_url: '' }); const [isDark, setIsDark] = useState(() => document.documentElement.getAttribute('data-theme') === 'dark'); @@ -41,11 +41,22 @@ export default function GlobalBar({ isMobile, showSidebar, onBurger }) { title="Menu" aria-label="Open menu" > - - - - - +
+ + + + + + {hasUnread && ( + + )} +
{appName} diff --git a/frontend/src/components/NavDrawer.css b/frontend/src/components/NavDrawer.css index 9434e9b..1efb3bc 100644 --- a/frontend/src/components/NavDrawer.css +++ b/frontend/src/components/NavDrawer.css @@ -83,3 +83,12 @@ color: var(--primary); } .nav-drawer-item.active:hover { background: var(--primary-light); } + +.nav-drawer-unread-dot { + margin-left: auto; + width: 9px; + height: 9px; + border-radius: 50%; + background: var(--primary); + flex-shrink: 0; +} diff --git a/frontend/src/components/NavDrawer.jsx b/frontend/src/components/NavDrawer.jsx index f675b0f..f7e12ad 100644 --- a/frontend/src/components/NavDrawer.jsx +++ b/frontend/src/components/NavDrawer.jsx @@ -13,7 +13,7 @@ const NAV_ICON = { settings: , }; -export default function NavDrawer({ open, onClose, onMessages, onGroupMessages, onSchedule, onScheduleManager, onBranding, onSettings, onUsers, onGroupManager, onHostPanel, features = {}, currentPage = 'chat', isMobile = false }) { +export default function NavDrawer({ open, onClose, onMessages, onGroupMessages, onSchedule, onScheduleManager, onBranding, onSettings, onUsers, onGroupManager, onHostPanel, features = {}, currentPage = 'chat', isMobile = false, unreadMessages = false, unreadGroupMessages = false }) { const { user } = useAuth(); const drawerRef = useRef(null); const isAdmin = user?.role === 'admin'; @@ -36,7 +36,7 @@ export default function NavDrawer({ open, onClose, onMessages, onGroupMessages, }, [open, onClose]); const item = (icon, label, onClick, opts = {}) => { - const { active, disabled, badge } = opts; + const { active, disabled, badge, dot } = opts; return ( ); }; @@ -64,8 +65,8 @@ export default function NavDrawer({ open, onClose, onMessages, onGroupMessages,
{/* User section */} - {item(NAV_ICON.messages, 'Messages', onMessages, { active: currentPage === 'chat' })} - {hasUserGroups && item(NAV_ICON.groupmessages, 'Group Messages', onGroupMessages, { active: currentPage === 'groupmessages' })} + {item(NAV_ICON.messages, 'Messages', onMessages, { active: currentPage === 'chat', dot: unreadMessages })} + {hasUserGroups && item(NAV_ICON.groupmessages, 'Group Messages', onGroupMessages, { active: currentPage === 'groupmessages', dot: unreadGroupMessages })} {features.scheduleManager && item(NAV_ICON.schedules, 'Schedules', onSchedule, { active: currentPage === 'schedule' })} {/* Admin section */} diff --git a/frontend/src/pages/Chat.jsx b/frontend/src/pages/Chat.jsx index b90fd9d..0772de0 100644 --- a/frontend/src/pages/Chat.jsx +++ b/frontend/src/pages/Chat.jsx @@ -395,10 +395,20 @@ export default function Chat() { const isToolManager = user?.role === 'admin' || (features.teamToolManagers || []).some(gid => (features.userGroupMemberships || []).includes(gid)); + // Unread indicators for burger icon and nav drawer + const allGroupsFlat = [...(groups.publicGroups || []), ...(groups.privateGroups || [])]; + const hasUnreadChat = allGroupsFlat.some(g => + (g.type === 'public' || !g.is_managed) && (unreadGroups.get(g.id) || 0) > 0 + ); + const hasUnreadGroupMessages = (groups.privateGroups || []).some(g => + g.is_managed && (unreadGroups.get(g.id) || 0) > 0 + ); + const hasAnyUnread = hasUnreadChat || hasUnreadGroupMessages; + if (page === 'users') { return (
- setDrawerOpen(true)} /> + setDrawerOpen(true)} hasUnread={hasAnyUnread} />
setModal('profile')} onHelp={() => setModal('help')} onAbout={() => setModal('about')} />
@@ -412,7 +422,8 @@ export default function Chat() { onSettings={() => { setDrawerOpen(false); setModal('settings'); }} onUsers={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('users'); }} onHostPanel={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }} - features={features} currentPage={page} isMobile={isMobile} /> + features={features} currentPage={page} isMobile={isMobile} + unreadMessages={hasUnreadChat} unreadGroupMessages={hasUnreadGroupMessages} /> {modal === 'profile' && setModal(null)} />} {modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />} {modal === 'branding' && setModal(null)} />} @@ -425,20 +436,10 @@ export default function Chat() { if (page === 'groups') { return (
- setDrawerOpen(true)} /> + setDrawerOpen(true)} hasUnread={hasAnyUnread} />
setModal('profile')} onHelp={() => setModal('help')} onAbout={() => setModal('about')} />
- setDrawerOpen(false)} - onMessages={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('chat'); }} - onSchedule={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('schedule'); }} - onGroupManager={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('groups'); }} - onBranding={() => { setDrawerOpen(false); setModal('branding'); }} - onSettings={() => { setDrawerOpen(false); setModal('settings'); }} - onUsers={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('users'); }} - onHostPanel={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }} - features={features} currentPage={page} isMobile={isMobile} /> setDrawerOpen(false)} onMessages={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('chat'); }} @@ -449,7 +450,8 @@ export default function Chat() { onSettings={() => { setDrawerOpen(false); setModal('settings'); }} onUsers={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('users'); }} onHostPanel={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }} - features={features} currentPage={page} isMobile={isMobile} /> + features={features} currentPage={page} isMobile={isMobile} + unreadMessages={hasUnreadChat} unreadGroupMessages={hasUnreadGroupMessages} /> {modal === 'profile' && setModal(null)} />} {modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />} {modal === 'branding' && setModal(null)} />} @@ -462,7 +464,7 @@ export default function Chat() { if (page === 'groupmessages') { return (
- setDrawerOpen(true)} /> + setDrawerOpen(true)} hasUnread={hasAnyUnread} />
{(!isMobile || showSidebar) && ( { setDrawerOpen(false); setModal('settings'); }} onUsers={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('users'); }} onHostPanel={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }} - features={features} currentPage={page} isMobile={isMobile} /> + features={features} currentPage={page} isMobile={isMobile} + unreadMessages={hasUnreadChat} unreadGroupMessages={hasUnreadGroupMessages} /> {modal === 'profile' && setModal(null)} />} {modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />} {modal === 'branding' && setModal(null)} />} @@ -520,7 +523,7 @@ export default function Chat() { if (page === 'hostpanel') { return (
- setDrawerOpen(true)} /> + setDrawerOpen(true)} hasUnread={hasAnyUnread} />
setModal('profile')} onHelp={() => setModal('help')} onAbout={() => setModal('about')} />
@@ -538,7 +541,8 @@ export default function Chat() { onHostPanel={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }} features={features} currentPage={page} - isMobile={isMobile} /> + isMobile={isMobile} + unreadMessages={hasUnreadChat} unreadGroupMessages={hasUnreadGroupMessages} /> {modal === 'profile' && setModal(null)} />} {modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />} {modal === 'branding' && setModal(null)} />} @@ -551,7 +555,7 @@ export default function Chat() { if (page === 'schedule') { return (
- setDrawerOpen(true)} /> + setDrawerOpen(true)} hasUnread={hasAnyUnread} />
{ setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }} features={features} currentPage={page} - isMobile={isMobile} /> + isMobile={isMobile} + unreadMessages={hasUnreadChat} unreadGroupMessages={hasUnreadGroupMessages} /> {modal === 'profile' && setModal(null)} />} {modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />} {modal === 'branding' && setModal(null)} />} @@ -594,7 +599,7 @@ export default function Chat() { return (
{/* Global top bar — spans full width on desktop, visible on mobile sidebar view */} - setDrawerOpen(true)} /> + setDrawerOpen(true)} hasUnread={hasAnyUnread} />
{(!isMobile || showSidebar) && ( @@ -645,7 +650,8 @@ export default function Chat() { onHostPanel={() => { setDrawerOpen(false); setActiveGroupId(null); setChatHasText(false); setPage('hostpanel'); }} features={features} currentPage={page} - isMobile={isMobile} /> + isMobile={isMobile} + unreadMessages={hasUnreadChat} unreadGroupMessages={hasUnreadGroupMessages} /> {modal === 'profile' && setModal(null)} />} {modal === 'settings' && setModal(null)} onFeaturesChanged={setFeatures} />} {modal === 'branding' && setModal(null)} />}