From 3e9623b6fc97f067ec0feb81097fb60920e1e1ed Mon Sep 17 00:00:00 2001
From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com>
Date: Sat, 16 May 2026 22:10:52 -0400
Subject: [PATCH] Apply SHOC design system styling across frontend
- Rewrite theme with SHOC palette (#0c4f6f primary, Nunito font, 4px radius)
- Add global CSS with Google Fonts import for Nunito
- Redesign Topbar with avatar initials, role subtitle, gradient header
- Redesign Sidebar with 220px width, section headers, active state border
- Restyle LoginPage with SHOC branded card and dev-mode role selector
- Update AdminDashboard KPI cards to centered SHOC style
- Add devLogin API method for local development auth flow
---
web/src/components/Sidebar.tsx | 82 +++++++---
web/src/components/Topbar.tsx | 39 +++--
web/src/index.css | 27 ++++
web/src/lib/api/auth.ts | 5 +
web/src/main.tsx | 1 +
.../pages/admin/dashboard/AdminDashboard.tsx | 16 +-
web/src/pages/auth/LoginPage.tsx | 153 +++++++++++++++---
web/src/theme.ts | 139 ++++++++++++++--
8 files changed, 384 insertions(+), 78 deletions(-)
create mode 100644 web/src/index.css
diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx
index 1feb785..e42d026 100644
--- a/web/src/components/Sidebar.tsx
+++ b/web/src/components/Sidebar.tsx
@@ -8,7 +8,6 @@ import {
ListItemText,
Toolbar,
Box,
- Divider,
Typography,
} from '@mui/material';
import DashboardIcon from '@mui/icons-material/Dashboard';
@@ -21,23 +20,34 @@ import { selectSidebarOpen } from '../app/slices/uiSlice';
import { selectUser } from '../app/slices/authSlice';
import type { RootState } from '../app/store';
-const DRAWER_WIDTH = 240;
+const DRAWER_WIDTH = 220;
const dispatcherNav = [
- { label: 'Dashboard', path: '/', icon: },
- { label: 'My Proposals', path: '/proposals', icon: },
- { label: 'New Proposal', path: '/proposals/new', icon: },
+ { label: 'Dashboard', path: '/', icon: },
+ { label: 'My Proposals', path: '/proposals', icon: },
+ { label: 'New Proposal', path: '/proposals/new', icon: },
];
const adminNav = [
- { label: 'Admin Queue', path: '/admin', icon: },
- { label: 'All Proposals', path: '/admin/proposals', icon: },
+ { label: 'Admin Queue', path: '/admin', icon: },
+ { label: 'All Proposals', path: '/admin/proposals', icon: },
];
const sysadminNav = [
- { label: 'Users', path: '/admin/users', icon: },
+ { label: 'Users', path: '/admin/users', icon: },
];
+const ALL_NAV_PATHS = new Set(
+ [...dispatcherNav, ...adminNav, ...sysadminNav].map(n => n.path)
+);
+
+function isNavActive(pathname: string, itemPath: string): boolean {
+ if (pathname === itemPath) return true;
+ if (ALL_NAV_PATHS.has(pathname)) return false;
+ if (itemPath === '/' || itemPath === '/admin') return false;
+ return pathname.startsWith(itemPath + '/');
+}
+
export default function Sidebar() {
const navigate = useNavigate();
const location = useLocation();
@@ -61,46 +71,72 @@ export default function Sidebar() {
},
}}
>
-
-
-
+
+
+
+ Proposals
+
+
{navItems.map((item) => (
navigate(item.path)}
+ sx={{ py: 1 }}
>
{item.icon}
-
+
))}
{isAdmin && (
<>
-
-
+
Admin
-
+
{adminNav.map((item) => (
navigate(item.path)}
+ sx={{ py: 1 }}
>
{item.icon}
-
+
))}
{isSysAdmin && sysadminNav.map((item) => (
navigate(item.path)}
+ sx={{ py: 1 }}
>
{item.icon}
-
+
))}
@@ -108,9 +144,9 @@ export default function Sidebar() {
)}
{user && (
-
- {user.displayName}
- {user.role}
+
+ {user.displayName}
+ {user.role}
)}
diff --git a/web/src/components/Topbar.tsx b/web/src/components/Topbar.tsx
index 3b244eb..e384227 100644
--- a/web/src/components/Topbar.tsx
+++ b/web/src/components/Topbar.tsx
@@ -1,5 +1,5 @@
import { useDispatch } from 'react-redux';
-import { AppBar, Toolbar, IconButton, Typography, Button, Box } from '@mui/material';
+import { AppBar, Toolbar, IconButton, Typography, Box, Avatar } from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import LogoutIcon from '@mui/icons-material/Logout';
import { toggleSidebar } from '../app/slices/uiSlice';
@@ -9,9 +9,13 @@ export default function Topbar() {
const dispatch = useDispatch();
const { user, logout } = useAuth();
+ const initials = user?.displayName
+ ? user.displayName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
+ : '';
+
return (
- theme.zIndex.drawer + 1 }}>
-
+ theme.zIndex.drawer + 1, height: 64 }}>
+
-
+
Proposal System
{user && (
-
- {user.displayName}
- } size="small">
- Logout
-
+
+
+
+ {initials}
+
+
+
+ {user.displayName}
+
+
+ {user.role}
+
+
+
+
+
+
+
)}
diff --git a/web/src/index.css b/web/src/index.css
new file mode 100644
index 0000000..6ad3d12
--- /dev/null
+++ b/web/src/index.css
@@ -0,0 +1,27 @@
+@import url("https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,600;0,700;1,400&display=swap");
+
+* {
+ box-sizing: border-box;
+}
+
+html, body, #root {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ font-family: "Nunito", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ font-size: 13px;
+ background: #f4f6f7;
+ color: #333;
+ line-height: 1.5;
+}
+
+a {
+ color: #007bff;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
diff --git a/web/src/lib/api/auth.ts b/web/src/lib/api/auth.ts
index be03b96..b964dc2 100644
--- a/web/src/lib/api/auth.ts
+++ b/web/src/lib/api/auth.ts
@@ -21,6 +21,11 @@ export const authApi = {
return res.data;
},
+ devLogin: async (email: string, displayName: string, role: string): Promise => {
+ const res = await apiClient.post('/auth/dev-login', { email, displayName, role });
+ return res.data;
+ },
+
getMe: async (): Promise => {
const res = await apiClient.get('/users/me');
return res.data;
diff --git a/web/src/main.tsx b/web/src/main.tsx
index 7a535e9..d284c5d 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -6,6 +6,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider, CssBaseline } from '@mui/material';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
+import './index.css';
import { store } from './app/store';
import { queryClient } from './lib/queryClient';
import { theme } from './theme';
diff --git a/web/src/pages/admin/dashboard/AdminDashboard.tsx b/web/src/pages/admin/dashboard/AdminDashboard.tsx
index 04f2bf3..bdc54a8 100644
--- a/web/src/pages/admin/dashboard/AdminDashboard.tsx
+++ b/web/src/pages/admin/dashboard/AdminDashboard.tsx
@@ -54,13 +54,15 @@ function formatDate(iso: string): string {
function StatCard({ icon, label, value, color }: { icon: React.ReactNode; label: string; value: string; color: string }) {
return (
-
-
- {icon}
-
- {value}
- {label}
-
+
+
+ {icon}
+
+ {label}
+
+
+ {value}
+
);
diff --git a/web/src/pages/auth/LoginPage.tsx b/web/src/pages/auth/LoginPage.tsx
index 14c2f17..2ea1e04 100644
--- a/web/src/pages/auth/LoginPage.tsx
+++ b/web/src/pages/auth/LoginPage.tsx
@@ -1,10 +1,16 @@
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
-import { Box, Button, Card, CardContent, Typography } from '@mui/material';
+import { useDispatch } from 'react-redux';
+import { Box, Button, Card, CardContent, Typography, Divider, Stack } from '@mui/material';
import GoogleIcon from '@mui/icons-material/Google';
+import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
+import EngineeringIcon from '@mui/icons-material/Engineering';
+import PersonIcon from '@mui/icons-material/Person';
import { useAuth } from '../../hooks/useAuth';
+import { authApi } from '../../lib/api/auth';
+import { setUser, setError } from '../../app/slices/authSlice';
-const COGNITO_DOMAIN = import.meta.env.VITE_COGNITO_DOMAIN || 'proposal-system-seahaven.auth.us-east-1.amazoncognito.com';
+const COGNITO_DOMAIN = import.meta.env.VITE_COGNITO_DOMAIN || '';
const CLIENT_ID = import.meta.env.VITE_COGNITO_CLIENT_ID || '';
const REDIRECT_URI = `${window.location.origin}/callback`;
@@ -21,7 +27,11 @@ function buildLoginUrl(): string {
export default function LoginPage() {
const navigate = useNavigate();
+ const dispatch = useDispatch();
const { isAuthenticated } = useAuth();
+ const [loading, setLoading] = useState(false);
+
+ const isDevMode = !CLIENT_ID;
useEffect(() => {
if (isAuthenticated) {
@@ -29,36 +39,131 @@ export default function LoginPage() {
}
}, [isAuthenticated, navigate]);
+ const handleDevLogin = async (role: string) => {
+ setLoading(true);
+ try {
+ const user = await authApi.devLogin('adam@seahavenind.com', 'Adam Moussa', role);
+ dispatch(setUser(user));
+ navigate('/', { replace: true });
+ } catch (err) {
+ dispatch(setError(err instanceof Error ? err.message : 'Dev login failed'));
+ } finally {
+ setLoading(false);
+ }
+ };
+
return (
-
-
-
- Proposal System
-
-
- Sign in with your Sea Haven Google account to continue.
-
- }
- href={buildLoginUrl()}
- sx={{ py: 1.5 }}
+
+
+
- Sign in with Google
-
-
-
+
+ Sea Haven Industries
+
+
+
+
+ Proposal System
+
+
+
+ {isDevMode ? (
+ <>
+
+ Development Mode — Select a role to sign in as:
+
+
+ }
+ onClick={() => handleDevLogin('SysAdmin')}
+ disabled={loading}
+ sx={{ py: 1.5, bgcolor: '#003A5D', '&:hover': { bgcolor: '#00527d' } }}
+ >
+ Sign in as SysAdmin
+
+ }
+ onClick={() => handleDevLogin('Admin')}
+ disabled={loading}
+ sx={{ py: 1.5, bgcolor: '#0c4f6f', '&:hover': { bgcolor: '#083a52' } }}
+ >
+ Sign in as Admin
+
+ }
+ onClick={() => handleDevLogin('Dispatcher')}
+ disabled={loading}
+ sx={{ py: 1.5 }}
+ >
+ Sign in as Dispatcher
+
+
+
+
+ Dev login is only available when VITE_COGNITO_CLIENT_ID is not set
+
+ >
+ ) : (
+ <>
+
+ Sign in with your Sea Haven Google account to continue.
+
+ }
+ href={buildLoginUrl()}
+ sx={{ py: 1.5, bgcolor: '#003A5D', '&:hover': { bgcolor: '#00527d' } }}
+ >
+ Sign in with Google
+
+ >
+ )}
+
+
+
+ Sea Haven Industries © {new Date().getFullYear()}
+
+
);
}
diff --git a/web/src/theme.ts b/web/src/theme.ts
index fd8122e..5ac98ba 100644
--- a/web/src/theme.ts
+++ b/web/src/theme.ts
@@ -3,28 +3,36 @@ import { createTheme } from '@mui/material/styles';
export const theme = createTheme({
palette: {
primary: {
- main: '#1565c0',
- light: '#5e92f3',
- dark: '#003c8f',
+ main: '#0c4f6f',
+ light: '#1fa8de',
+ dark: '#083a52',
},
secondary: {
- main: '#f57c00',
- light: '#ffad42',
- dark: '#bb4d00',
+ main: '#007bff',
+ light: '#4da3ff',
+ dark: '#0056b3',
},
background: {
- default: '#f5f5f5',
+ default: '#f4f6f7',
paper: '#ffffff',
},
+ text: {
+ primary: '#333333',
+ secondary: '#666666',
+ },
+ divider: '#d9dde0',
},
typography: {
- fontFamily: '"Nunito Sans", "Helvetica", "Arial", sans-serif',
- h4: { fontWeight: 700 },
- h5: { fontWeight: 700 },
- h6: { fontWeight: 600 },
+ fontFamily: '"Nunito", "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
+ fontSize: 13,
+ h4: { fontWeight: 600, fontSize: '28px' },
+ h5: { fontWeight: 600, fontSize: '22px' },
+ h6: { fontWeight: 600, fontSize: '16px' },
+ body1: { fontSize: '14px' },
+ body2: { fontSize: '13px' },
},
shape: {
- borderRadius: 8,
+ borderRadius: 4,
},
components: {
MuiButton: {
@@ -32,12 +40,19 @@ export const theme = createTheme({
root: {
textTransform: 'none',
fontWeight: 600,
+ borderRadius: 4,
},
},
},
MuiAppBar: {
defaultProps: {
- elevation: 1,
+ elevation: 0,
+ },
+ styleOverrides: {
+ root: {
+ background: 'linear-gradient(#0c4f6f, #083a52)',
+ borderBottom: '1px solid #062c3e',
+ },
},
},
MuiCard: {
@@ -46,8 +61,102 @@ export const theme = createTheme({
},
styleOverrides: {
root: {
- border: '1px solid',
- borderColor: '#e0e0e0',
+ border: '1px solid #ddd',
+ borderRadius: 4,
+ boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
+ },
+ },
+ },
+ MuiPaper: {
+ defaultProps: {
+ elevation: 0,
+ },
+ styleOverrides: {
+ root: {
+ borderRadius: 4,
+ },
+ },
+ },
+ MuiTableHead: {
+ styleOverrides: {
+ root: {
+ '& .MuiTableCell-head': {
+ fontWeight: 600,
+ backgroundColor: '#f9f9f9',
+ borderBottom: '2px solid #ddd',
+ },
+ },
+ },
+ },
+ MuiTableCell: {
+ styleOverrides: {
+ root: {
+ fontSize: '14px',
+ padding: '12px',
+ borderBottom: '1px solid #eee',
+ },
+ },
+ },
+ MuiTableRow: {
+ styleOverrides: {
+ root: {
+ '&:hover': {
+ backgroundColor: '#f9f9f9',
+ },
+ },
+ },
+ },
+ MuiTextField: {
+ styleOverrides: {
+ root: {
+ '& .MuiOutlinedInput-root': {
+ borderRadius: 4,
+ },
+ },
+ },
+ },
+ MuiChip: {
+ styleOverrides: {
+ root: {
+ borderRadius: 4,
+ fontWeight: 600,
+ fontSize: '12px',
+ },
+ },
+ },
+ MuiDrawer: {
+ styleOverrides: {
+ paper: {
+ borderRight: '1px solid #d9dde0',
+ backgroundColor: '#fbfbfb',
+ },
+ },
+ },
+ MuiListItemButton: {
+ styleOverrides: {
+ root: {
+ borderRadius: 6,
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 4,
+ '&.Mui-selected': {
+ backgroundColor: '#e6f1f7',
+ borderLeft: '3px solid #0b80b5',
+ '&:hover': {
+ backgroundColor: '#dbedf5',
+ },
+ },
+ '&:hover': {
+ backgroundColor: '#eef2f4',
+ },
+ },
+ },
+ },
+ MuiListItemIcon: {
+ styleOverrides: {
+ root: {
+ color: '#8c949a',
+ minWidth: 36,
},
},
},