From 0bc9f74c7f8ae41e44a2489237f74df66f8ea8d2 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Thu, 12 Jun 2025 07:49:21 -0700 Subject: [PATCH] Tweaked JobViewer for mobile --- frontend/src/components/layout/Header.tsx | 15 +- frontend/src/components/ui/JobViewer.tsx | 524 +++++++++++++++------- frontend/src/config/navigationConfig.tsx | 42 +- 3 files changed, 412 insertions(+), 169 deletions(-) diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index 12124fc..dc305b6 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -171,7 +171,7 @@ const Header: React.FC = (props: HeaderProps) => { id: item.id, label: item.label as string, icon: item.icon || null, - action: () => item.path && navigate(item.path), + action: () => item.path && navigate(item.path.replace(/:.*$/, '')), group: 'profile' }); } @@ -195,7 +195,7 @@ const Header: React.FC = (props: HeaderProps) => { id: item.id, label: item.label as string, icon: item.icon || null, - action: () => item.path && navigate(item.path), + action: () => item.path && navigate(item.path.replace(/:.*$/, '')), group: 'account' }); } @@ -219,7 +219,7 @@ const Header: React.FC = (props: HeaderProps) => { id: item.id, label: item.label as string, icon: item.icon || null, - action: () => item.path && navigate(item.path), + action: () => item.path && navigate(item.path.replace(/:.*$/, '')), group: 'admin' }); } @@ -254,7 +254,7 @@ const Header: React.FC = (props: HeaderProps) => { id: item.id, label: item.label as string, icon: item.icon || null, - action: () => item.path && navigate(item.path), + action: () => item.path && navigate(item.path.replace(/:.*$/, '')), group: 'system' }); } @@ -267,7 +267,7 @@ const Header: React.FC = (props: HeaderProps) => { id: item.id, label: item.label as string, icon: item.icon || null, - action: () => item.path && navigate(item.path), + action: () => item.path && navigate(item.path.replace(/:.*$/, '')), group: 'other' }); } @@ -328,7 +328,7 @@ const Header: React.FC = (props: HeaderProps) => { // Navigation handlers const handleNavigate = (path: string) => { - navigate(path); + navigate(path.replace(/:.*$/, '')); setMobileOpen(false); // Close all dropdowns setDropdownAnchors({}); @@ -372,6 +372,7 @@ const Header: React.FC = (props: HeaderProps) => { onClick={() => child.path && handleNavigate(child.path)} selected={isCurrentPath(child)} disabled={!child.path} + sx={{ display: 'flex', alignItems: 'center', "& *": { m: 0, p: 0 }, m: 0 }} > {child.icon && {child.icon}} {child.label} @@ -487,7 +488,7 @@ const Header: React.FC = (props: HeaderProps) => { item.group === 'divider' ? ( ) : ( - + handleUserMenuAction(item)}> {item.icon && {item.icon}} diff --git a/frontend/src/components/ui/JobViewer.tsx b/frontend/src/components/ui/JobViewer.tsx index e6f42a3..c0c54de 100644 --- a/frontend/src/components/ui/JobViewer.tsx +++ b/frontend/src/components/ui/JobViewer.tsx @@ -14,17 +14,24 @@ import { MenuItem, InputLabel, Chip, - Divider, IconButton, - Tooltip + Dialog, + AppBar, + Toolbar, + useMediaQuery, + useTheme, + Slide } from '@mui/material'; import { KeyboardArrowUp as ArrowUpIcon, KeyboardArrowDown as ArrowDownIcon, Business as BusinessIcon, Work as WorkIcon, - Schedule as ScheduleIcon + Schedule as ScheduleIcon, + Close as CloseIcon, + ArrowBack as ArrowBackIcon } from '@mui/icons-material'; +import { TransitionProps } from '@mui/material/transitions'; import { JobInfo } from 'components/ui/JobInfo'; import { Job } from "types/types"; import { useAuth } from 'hooks/AuthContext'; @@ -38,8 +45,21 @@ interface JobViewerProps { onSelect?: (job: Job) => void; } +const Transition = React.forwardRef(function Transition( + props: TransitionProps & { + children: React.ReactElement; + }, + ref: React.Ref, +) { + return ; +}); + const JobViewer: React.FC = ({ onSelect }) => { const navigate = useNavigate(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const isSmall = useMediaQuery(theme.breakpoints.down('sm')); + const { apiClient } = useAuth(); const { selectedJob, setSelectedJob } = useSelectedJob(); const { setSnack } = useAppState(); @@ -47,6 +67,7 @@ const JobViewer: React.FC = ({ onSelect }) => { const [loading, setLoading] = useState(false); const [sortField, setSortField] = useState('updatedAt'); const [sortOrder, setSortOrder] = useState('desc'); + const [mobileDialogOpen, setMobileDialogOpen] = useState(false); const { jobId } = useParams<{ jobId?: string }>(); useEffect(() => { @@ -56,6 +77,7 @@ const JobViewer: React.FC = ({ onSelect }) => { const results = await apiClient.getJobs(); const jobsData: Job[] = results.data || []; setJobs(jobsData); + if (jobId) { const job = jobsData.find(j => j.id === jobId); if (job) { @@ -125,7 +147,15 @@ const JobViewer: React.FC = ({ onSelect }) => { const handleJobSelect = (job: Job) => { setSelectedJob(job); onSelect?.(job); - navigate(`/candidate/jobs/${job.id}`); + if (isMobile) { + setMobileDialogOpen(true); + } else { + navigate(`/candidate/jobs/${job.id}`); + } + }; + + const handleMobileDialogClose = () => { + setMobileDialogOpen(false); }; const sortedJobs = sortJobs(jobs, sortField, sortOrder); @@ -135,9 +165,8 @@ const JobViewer: React.FC = ({ onSelect }) => { return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' + ...(isMobile ? {} : { year: 'numeric' }), + ...(isSmall ? {} : { hour: '2-digit', minute: '2-digit' }) }).format(date); }; @@ -146,170 +175,363 @@ const JobViewer: React.FC = ({ onSelect }) => { return sortOrder === 'asc' ? : ; }; - return ( - - {/* Left Panel - Job List */} - - - - Jobs ({jobs.length}) - + const JobList = () => ( + + + + Jobs ({jobs.length}) + - - Sort by - - - + + Sort by + + + - - - - + +
+ + + handleSort('company')} + > + + + + {isSmall ? 'Co.' : isMobile ? 'Company' : 'Company'} + + {getSortIcon('company')} + + + handleSort('title')} + > + + + Title + {getSortIcon('title')} + + + {!isMobile && ( handleSort('company')} - > - - - Company - {getSortIcon('company')} - - - handleSort('title')} - > - - - Title - {getSortIcon('title')} - - - handleSort('updatedAt')} > - - - Updated + + + Updated {getSortIcon('updatedAt')} - Status - - - - {sortedJobs.map((job) => ( - handleJobSelect(job)} - sx={{ - cursor: 'pointer', - '&.Mui-selected': { - backgroundColor: 'action.selected', - } - }} - > - - - {job.company || 'N/A'} + )} + + + {isMobile ? 'Status' : 'Status'} + + + + + + {sortedJobs.map((job) => ( + handleJobSelect(job)} + sx={{ + cursor: 'pointer', + height: isMobile ? 48 : 'auto', + '&.Mui-selected': { + backgroundColor: 'action.selected', + }, + '&:hover': { + backgroundColor: 'action.hover', + } + }} + > + + + {job.company || 'N/A'} + + {!isMobile && job.details?.location && ( + + {job.details.location.city}, {job.details.location.state || job.details.location.country} - {job.details?.location && ( - - {job.details.location.city}, {job.details.location.state || job.details.location.country} - - )} - - - - {job.title || 'N/A'} - - {job.details?.employmentType && ( - - )} - - - + )} + + + + {job.title || 'N/A'} + + {!isMobile && job.details?.employmentType && ( + + )} + + {!isMobile && ( + + {formatDate(job.updatedAt)} {job.createdAt && ( - + Created: {formatDate(job.createdAt)} )} - - - - - ))} - -
-
-
+ )} + + + + + ))} + + + + + ); - {/* Right Panel - Job Details */} - - - + const JobDetails = ({ inDialog = false }: { inDialog?: boolean }) => ( + + {selectedJob ? ( + + ) : ( + + + Select a job to view details + + + )} + + ); + + if (isMobile) { + return ( + + + + + + + + + + + + {selectedJob?.title} + + + {selectedJob?.company} + + + + + + + + ); + } + + return ( + + + + + + Job Details - - - {selectedJob ? ( - - ) : ( - - - Select a job to view details - - - )} - + ); diff --git a/frontend/src/config/navigationConfig.tsx b/frontend/src/config/navigationConfig.tsx index 4bc1cdb..d313d58 100644 --- a/frontend/src/config/navigationConfig.tsx +++ b/frontend/src/config/navigationConfig.tsx @@ -44,6 +44,7 @@ import { DocumentManager } from "components/DocumentManager"; import { useAuth } from "hooks/AuthContext"; import { useNavigate } from "react-router-dom"; import { JobViewer } from "components/ui/JobViewer"; +import { CandidatePicker } from "components/ui/CandidatePicker"; // Beta page components for placeholder routes const BackstoryPage = () => ( @@ -129,6 +130,36 @@ export const navigationConfig: NavigationConfig = { component: , userTypes: ["guest", "candidate", "employer"], }, + { + id: "explore", + label: "Explore", + icon: , + userTypes: ["candidate", "guest", "employer"], + children: [ + // { + // id: "explore-candidates", + // label: "Candidates", + // path: "/candidate/candidates", + // icon: , + // component: ( + // + // ), + // userTypes: ["candidate", "guest", "employer"], + // }, + { + id: "explore-jobs", + label: "Jobs", + path: "/candidate/jobs/:jobId?", + icon: , + component: ( + + ), + userTypes: ["candidate", "guest", "employer"], + }, + ], + showInNavigation: true, + }, + { id: "generate-candidate", label: "Generate Candidate", @@ -163,17 +194,6 @@ export const navigationConfig: NavigationConfig = { showInNavigation: false, showInUserMenu: true, }, - { - id: "candidate-jobs", - label: "Jobs", - icon: , - path: "/candidate/jobs/:jobId?", - component: , - userTypes: ["candidate", "guest", "employer"], - userMenuGroup: "profile", - showInNavigation: false, - showInUserMenu: true, - }, { id: "candidate-docs", label: "Content",