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} - + + + + {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. - - - - + + Sea Haven Industries + + + + + Proposal System + + + + {isDevMode ? ( + <> + + Development Mode — Select a role to sign in as: + + + + + + + + + Dev login is only available when VITE_COGNITO_CLIENT_ID is not set + + + ) : ( + <> + + Sign in with your Sea Haven Google account to continue. + + + + )} + + + + 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, }, }, },