New navigation system
This commit is contained in:
parent
18863a23d9
commit
d41f9a9e75
@ -5,8 +5,8 @@ import { ChatSubmitQueryInterface } from './BackstoryQuery';
|
||||
import { SetSnackType } from './Snack';
|
||||
|
||||
interface BackstoryElementProps {
|
||||
setSnack: SetSnackType,
|
||||
submitQuery: ChatSubmitQueryInterface,
|
||||
// setSnack: SetSnackType,
|
||||
// submitQuery: ChatSubmitQueryInterface,
|
||||
sx?: SxProps<Theme>,
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ import { ChatMessage, ChatContext, ChatSession, ChatQuery, ChatMessageUser, Chat
|
||||
import { PaginatedResponse } from 'types/conversion';
|
||||
|
||||
import './Conversation.css';
|
||||
import { useSelectedCandidate } from 'hooks/GlobalContext';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
|
||||
const defaultMessage: ChatMessage = {
|
||||
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "assistant"
|
||||
@ -65,8 +65,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
||||
preamble,
|
||||
resetAction,
|
||||
resetLabel,
|
||||
setSnack,
|
||||
submitQuery,
|
||||
sx,
|
||||
type,
|
||||
} = props;
|
||||
@ -84,6 +82,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
||||
const stopRef = useRef(false);
|
||||
const controllerRef = useRef<StreamingResponse>(null);
|
||||
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
||||
const { setSnack } = useAppState();
|
||||
|
||||
// Keep the ref updated whenever items changes
|
||||
useEffect(() => {
|
||||
@ -326,16 +325,16 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
||||
<Box sx={{ p: 1, mt: 0, ...sx }}>
|
||||
{
|
||||
filteredConversation.map((message, index) =>
|
||||
<Message key={index} {...{ chatSession, sendQuery: processQuery, message, connectionBase, setSnack, submitQuery }} />
|
||||
<Message key={index} {...{ chatSession, sendQuery: processQuery, message, connectionBase, }} />
|
||||
)
|
||||
}
|
||||
{
|
||||
processingMessage !== undefined &&
|
||||
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, setSnack, message: processingMessage, submitQuery }} />
|
||||
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, message: processingMessage, }} />
|
||||
}
|
||||
{
|
||||
streamingMessage !== undefined &&
|
||||
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, setSnack, message: streamingMessage, submitQuery }} />
|
||||
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, message: streamingMessage }} />
|
||||
}
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
|
@ -7,11 +7,7 @@ interface DocumentProps extends BackstoryElementProps {
|
||||
}
|
||||
|
||||
const Document = (props: DocumentProps) => {
|
||||
const { setSnack, submitQuery, filepath } = props;
|
||||
const backstoryProps = {
|
||||
submitQuery,
|
||||
setSnack,
|
||||
};
|
||||
const { filepath } = props;
|
||||
|
||||
const [document, setDocument] = useState<string>("");
|
||||
|
||||
@ -43,7 +39,7 @@ const Document = (props: DocumentProps) => {
|
||||
}, [document, setDocument, filepath])
|
||||
|
||||
return (<>
|
||||
<StyledMarkdown {...backstoryProps} content={document}/>
|
||||
<StyledMarkdown content={document} />
|
||||
</>);
|
||||
};
|
||||
|
||||
|
@ -36,6 +36,7 @@ import { useTheme } from '@mui/material/styles';
|
||||
import { useAuth } from "hooks/AuthContext";
|
||||
import * as Types from 'types/types';
|
||||
import { BackstoryElementProps } from './BackstoryTab';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
|
||||
const VisuallyHiddenInput = styled('input')({
|
||||
clip: 'rect(0 0 0 0)',
|
||||
@ -50,8 +51,8 @@ const VisuallyHiddenInput = styled('input')({
|
||||
});
|
||||
|
||||
const DocumentManager = (props: BackstoryElementProps) => {
|
||||
const { setSnack, submitQuery } = props;
|
||||
const theme = useTheme();
|
||||
const { setSnack } = useAppState();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const { user, apiClient } = useAuth();
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { Quote } from 'components/Quote';
|
||||
import { BackstoryElementProps } from 'components/BackstoryTab';
|
||||
import { Candidate, ChatSession } from 'types/types';
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
|
||||
interface GenerateImageProps extends BackstoryElementProps {
|
||||
prompt: string;
|
||||
@ -13,7 +14,8 @@ interface GenerateImageProps extends BackstoryElementProps {
|
||||
|
||||
const GenerateImage = (props: GenerateImageProps) => {
|
||||
const { user } = useAuth();
|
||||
const { setSnack, chatSession, prompt } = props;
|
||||
const { chatSession, prompt } = props;
|
||||
const { setSnack } = useAppState();
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
const [status, setStatus] = useState<string>('');
|
||||
const [image, setImage] = useState<string>('');
|
||||
|
@ -46,7 +46,7 @@ import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
|
||||
import { useAppState, useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
|
||||
import { BackstoryElementProps } from './BackstoryTab';
|
||||
import { LoginRequired } from 'components/ui/LoginRequired';
|
||||
|
||||
@ -122,8 +122,7 @@ const JobCreator = (props: JobCreator) => {
|
||||
const { onSave } = props;
|
||||
const { selectedCandidate } = useSelectedCandidate();
|
||||
const { selectedJob, setSelectedJob } = useSelectedJob();
|
||||
const { setSnack, submitQuery } = props;
|
||||
const backstoryProps = { setSnack, submitQuery };
|
||||
const { setSnack } = useAppState();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isTablet = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
@ -31,6 +31,7 @@ import { StyledMarkdown } from './StyledMarkdown';
|
||||
import { Scrollable } from './Scrollable';
|
||||
import { start } from 'repl';
|
||||
import { TypesElement } from '@uiw/react-json-view';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
|
||||
interface JobAnalysisProps extends BackstoryPageProps {
|
||||
job: Job;
|
||||
@ -45,11 +46,9 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
const {
|
||||
job,
|
||||
candidate,
|
||||
setSnack,
|
||||
submitQuery
|
||||
} = props
|
||||
const { apiClient } = useAuth();
|
||||
const backstoryProps = { setSnack, submitQuery };
|
||||
const { setSnack } = useAppState();
|
||||
const theme = useTheme();
|
||||
const [requirements, setRequirements] = useState<{ requirement: string, domain: string }[]>([]);
|
||||
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
|
||||
@ -250,7 +249,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
||||
</Typography>
|
||||
<Paper sx={{ p: 2, maxHeight: "22rem" }}>
|
||||
<Scrollable sx={{ display: "flex", maxHeight: "100%" }}>
|
||||
<StyledMarkdown content={job.description} {...backstoryProps} />
|
||||
<StyledMarkdown content={job.description} />
|
||||
</Scrollable>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
@ -358,12 +358,8 @@ const MessageContainer = (props: MessageContainerProps) => {
|
||||
};
|
||||
|
||||
const Message = (props: MessageProps) => {
|
||||
const { message, title, submitQuery, sx, className, chatSession, onExpand, setSnack, expanded, expandable } = props;
|
||||
const { message, title, sx, className, chatSession, onExpand, expanded, expandable } = props;
|
||||
const [metaExpanded, setMetaExpanded] = useState<boolean>(false);
|
||||
const backstoryProps = {
|
||||
submitQuery,
|
||||
setSnack
|
||||
};
|
||||
const theme = useTheme();
|
||||
const type: ApiActivityType | ChatSenderType | "error" = ('activity' in message) ? message.activity : ('error' in message) ? 'error' : (message as ChatMessage).role;
|
||||
const style: any = getStyle(theme, type);
|
||||
@ -385,7 +381,7 @@ const Message = (props: MessageProps) => {
|
||||
};
|
||||
|
||||
const messageView = (
|
||||
<StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={content} {...backstoryProps} />
|
||||
<StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={content} />
|
||||
);
|
||||
|
||||
let metadataView = (<></>);
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { MuiMarkdown } from 'mui-markdown';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Link } from '@mui/material';
|
||||
import { BackstoryQuery } from 'components/BackstoryQuery';
|
||||
import { BackstoryQuery, BackstoryQueryInterface } from 'components/BackstoryQuery';
|
||||
import Box from '@mui/material/Box';
|
||||
import JsonView from '@uiw/react-json-view';
|
||||
import { vscodeTheme } from '@uiw/react-json-view/vscode';
|
||||
@ -13,17 +13,19 @@ import { GenerateImage } from 'components/GenerateImage';
|
||||
|
||||
import './StyledMarkdown.css';
|
||||
import { BackstoryElementProps } from './BackstoryTab';
|
||||
import { CandidateQuestion, ChatSession } from 'types/types';
|
||||
import { CandidateQuestion, ChatQuery, ChatSession } from 'types/types';
|
||||
import { ChatSubmitQueryInterface } from 'components/BackstoryQuery';
|
||||
|
||||
interface StyledMarkdownProps extends BackstoryElementProps {
|
||||
className?: string,
|
||||
content: string,
|
||||
streaming?: boolean,
|
||||
chatSession?: ChatSession,
|
||||
submitQuery?: ChatSubmitQueryInterface
|
||||
};
|
||||
|
||||
const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProps) => {
|
||||
const { className, content, chatSession, submitQuery, sx, streaming, setSnack } = props;
|
||||
const { className, content, chatSession, submitQuery, sx, streaming } = props;
|
||||
const theme = useTheme();
|
||||
|
||||
const overrides: any = {
|
||||
@ -110,7 +112,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
|
||||
question: queryString
|
||||
}
|
||||
|
||||
return <BackstoryQuery submitQuery={submitQuery} question={query} />
|
||||
return submitQuery ? <BackstoryQuery submitQuery={submitQuery} question={query} /> : query.question;
|
||||
} catch (e) {
|
||||
console.log("StyledMarkdown error:", queryString, e);
|
||||
return props.query;
|
||||
@ -124,7 +126,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
|
||||
component: (props: { prompt: string }) => {
|
||||
const prompt = props.prompt.replace(/(\w+):/g, '"$1":');
|
||||
try {
|
||||
return <GenerateImage {...{ chatSession, prompt, submitQuery, setSnack }} />
|
||||
return <GenerateImage {...{ chatSession, prompt }} />
|
||||
} catch (e) {
|
||||
console.log("StyledMarkdown error:", prompt, e);
|
||||
return props.prompt;
|
||||
|
@ -23,7 +23,7 @@ import './VectorVisualizer.css';
|
||||
import { BackstoryPageProps } from './BackstoryTab';
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import * as Types from 'types/types';
|
||||
import { useSelectedCandidate } from 'hooks/GlobalContext';
|
||||
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface VectorVisualizerProps extends BackstoryPageProps {
|
||||
@ -180,8 +180,8 @@ type Node = {
|
||||
|
||||
const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
|
||||
const { user, apiClient } = useAuth();
|
||||
const { setSnack, submitQuery, rag, inline, sx } = props;
|
||||
const backstoryProps = { setSnack, submitQuery };
|
||||
const { rag, inline, sx } = props;
|
||||
const { setSnack } = useAppState();
|
||||
const [plotData, setPlotData] = useState<PlotData | null>(null);
|
||||
const [newQuery, setNewQuery] = useState<string>('');
|
||||
const [querySet, setQuerySet] = useState<Types.ChromaDBGetResponse>(rag || emptyQuerySet);
|
||||
|
@ -1,168 +1,136 @@
|
||||
// components/layout/BackstoryLayout.tsx
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import { Outlet, useLocation, Routes } from "react-router-dom";
|
||||
import { Outlet, useLocation, Routes, Route } from "react-router-dom";
|
||||
import { Box, Container, Paper } from '@mui/material';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import ChatIcon from '@mui/icons-material/Chat';
|
||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import BarChartIcon from '@mui/icons-material/BarChart';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import WorkIcon from '@mui/icons-material/Work';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
|
||||
|
||||
import { Header } from 'components/layout/Header';
|
||||
import { Scrollable } from 'components/Scrollable';
|
||||
import { Footer } from 'components/layout/Footer';
|
||||
import { Snack, SetSnackType } from 'components/Snack';
|
||||
import { User } from 'types/types';
|
||||
import { getBackstoryDynamicRoutes } from 'components/layout/BackstoryRoutes';
|
||||
import { LoadingComponent } from "components/LoadingComponent";
|
||||
import { AuthProvider, useAuth, ProtectedRoute } from 'hooks/AuthContext';
|
||||
import { useSelectedCandidate } from 'hooks/GlobalContext';
|
||||
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
|
||||
import {
|
||||
getMainNavigationItems,
|
||||
getAllRoutes,
|
||||
} from 'config/navigationConfig';
|
||||
import { NavigationItem } from 'types/navigation';
|
||||
|
||||
type NavigationLinkType = {
|
||||
// Legacy type for backward compatibility
|
||||
export type NavigationLinkType = {
|
||||
label: ReactElement<any> | string;
|
||||
path: string;
|
||||
path: string;
|
||||
icon?: ReactElement<any>;
|
||||
};
|
||||
|
||||
const DefaultNavItems: NavigationLinkType[] = [
|
||||
{ label: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
|
||||
{ label: 'Docs', path: '/docs', icon: <InfoIcon /> },
|
||||
// { label: 'How It Works', path: '/how-it-works', icon: <InfoIcon/> },
|
||||
// { label: 'For Candidates', path: '/for-candidates', icon: <PersonIcon/> },
|
||||
// { label: 'For Employers', path: '/for-employers', icon: <BusinessIcon/> },
|
||||
// { label: 'Pricing', path: '/pricing', icon: <AttachMoneyIcon/> },
|
||||
];
|
||||
|
||||
const ViewerNavItems: NavigationLinkType[] = [
|
||||
{ label: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||
];
|
||||
|
||||
const CandidateNavItems : NavigationLinkType[]= [
|
||||
{ label: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||
{ label: 'Job Analysis', path: '/candidate/job-analysis', icon: <WorkIcon /> },
|
||||
{ label: 'Resume Builder', path: '/candidate/resume-builder', icon: <WorkIcon /> },
|
||||
// { label: 'Knowledge Explorer', path: '/candidate/knowledge-explorer', icon: <WorkIcon /> },
|
||||
// { label: 'Dashboard', icon: <DashboardIcon />, path: '/candidate/dashboard' },
|
||||
// { label: 'Profile', icon: <PersonIcon />, path: '/candidate/dashboard/profile' },
|
||||
// { label: 'Backstory', icon: <HistoryIcon />, path: '/candidate/backstory' },
|
||||
// { label: 'Resumes', icon: <DescriptionIcon />, path: '/candidate/resumes' },
|
||||
// { label: 'Q&A Setup', icon: <QuestionAnswerIcon />, path: '/candidate/qa-setup' },
|
||||
// { label: 'Analytics', icon: <BarChartIcon />, path: '/candidate/analytics' },
|
||||
// { label: 'Settings', icon: <SettingsIcon />, path: '/candidate/settings' },
|
||||
];
|
||||
|
||||
const EmployerNavItems: NavigationLinkType[] = [
|
||||
{ label: 'Chat', path: '/chat', icon: <ChatIcon /> },
|
||||
{ label: 'Job Analysis', path: '/employer/job-analysis', icon: <WorkIcon /> },
|
||||
{ label: 'Resume Builder', path: '/employer/resume-builder', icon: <WorkIcon /> },
|
||||
{ label: 'Knowledge Explorer', path: '/employer/knowledge-explorer', icon: <WorkIcon /> },
|
||||
{ label: 'Find a Candidate', path: '/find-a-candidate', icon: <InfoIcon /> },
|
||||
// { label: 'Dashboard', icon: <DashboardIcon />, path: '/employer/dashboard' },
|
||||
// { label: 'Search', icon: <SearchIcon />, path: '/employer/search' },
|
||||
// { label: 'Saved', icon: <BookmarkIcon />, path: '/employer/saved' },
|
||||
// { label: 'Jobs', icon: <WorkIcon />, path: '/employer/jobs' },
|
||||
// { label: 'Company', icon: <BusinessIcon />, path: '/employer/company' },
|
||||
// { label: 'Analytics', icon: <BarChartIcon />, path: '/employer/analytics' },
|
||||
// { label: 'Settings', icon: <SettingsIcon />, path: '/employer/settings' },
|
||||
];
|
||||
|
||||
// Navigation links based on user type
|
||||
const getNavigationLinks = (user: User | null): NavigationLinkType[] => {
|
||||
if (!user) {
|
||||
return DefaultNavItems;
|
||||
}
|
||||
|
||||
switch (user.userType) {
|
||||
case 'candidate':
|
||||
return DefaultNavItems.concat(CandidateNavItems);
|
||||
case 'employer':
|
||||
return DefaultNavItems.concat(EmployerNavItems);
|
||||
default:
|
||||
return DefaultNavItems;
|
||||
}
|
||||
};
|
||||
|
||||
interface BackstoryPageContainerProps {
|
||||
children?: React.ReactNode;
|
||||
sx?: SxProps<Theme>;
|
||||
};
|
||||
|
||||
const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
|
||||
const { children, sx } = props;
|
||||
return (
|
||||
<Container
|
||||
className="BackstoryPageContainer"
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row", // Must be row; if column, the box will expand for all children
|
||||
flexGrow: 1,
|
||||
p: "0 !important", // Let the first box use padding to offset main content
|
||||
m: "0 auto !important",
|
||||
maxWidth: '1024px', //{ xs: '100%', md: '700px', lg: '1024px' },
|
||||
height: "100%", // Restrict to main-container's height
|
||||
minHeight: 0,//"min-content", // Prevent flex overflow
|
||||
...sx
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: "flex", p: { xs: 0, sm: 0.5 }, flexGrow: 1, minHeight: "min-content", // Prevent flex overflow
|
||||
}}>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
m: 0,
|
||||
p: 0.5,
|
||||
minHeight: "min-content", // Prevent flex overflow
|
||||
backgroundColor: 'background.paper',
|
||||
borderRadius: 0.5,
|
||||
maxWidth: '100%',
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
{children}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface BackstoryLayoutProps {
|
||||
setSnack: SetSnackType;
|
||||
page: string;
|
||||
chatRef: React.Ref<any>;
|
||||
snackRef: React.Ref<any>;
|
||||
submitQuery: any;
|
||||
const BackstoryPageContainer = (props: BackstoryPageContainerProps) => {
|
||||
const { children, sx } = props;
|
||||
return (
|
||||
<Container
|
||||
className="BackstoryPageContainer"
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexGrow: 1,
|
||||
p: "0 !important",
|
||||
m: "0 auto !important",
|
||||
maxWidth: '1024px',
|
||||
height: "100%",
|
||||
minHeight: 0,
|
||||
...sx
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
p: { xs: 0, sm: 0.5 },
|
||||
flexGrow: 1,
|
||||
minHeight: "min-content",
|
||||
}}>
|
||||
<Paper
|
||||
elevation={2}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
m: 0,
|
||||
p: 0.5,
|
||||
minHeight: "min-content",
|
||||
backgroundColor: 'background.paper',
|
||||
borderRadius: 0.5,
|
||||
maxWidth: '100%',
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
{children}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
interface BackstoryLayoutProps {
|
||||
page: string;
|
||||
chatRef: React.Ref<any>;
|
||||
}
|
||||
|
||||
const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutProps) => {
|
||||
const { setSnack, page, chatRef, snackRef, submitQuery } = props;
|
||||
const { page, chatRef } = props;
|
||||
const { setSnack } = useAppState();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { guest, user } = useAuth();
|
||||
const { selectedCandidate } = useSelectedCandidate();
|
||||
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
|
||||
const [navigationItems, setNavigationItems] = useState<NavigationItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setNavigationLinks(getNavigationLinks(user));
|
||||
const userType = user?.userType || null;
|
||||
setNavigationItems(getMainNavigationItems(userType));
|
||||
}, [user]);
|
||||
|
||||
let dynamicRoutes;
|
||||
if (guest) {
|
||||
dynamicRoutes = getBackstoryDynamicRoutes({
|
||||
user,
|
||||
setSnack,
|
||||
submitQuery,
|
||||
chatRef
|
||||
});
|
||||
}
|
||||
// Generate dynamic routes from navigation config
|
||||
const generateRoutes = () => {
|
||||
if (!guest) return null;
|
||||
|
||||
const userType = user?.userType || null;
|
||||
const routes = getAllRoutes(userType);
|
||||
|
||||
return routes.map((route, index) => {
|
||||
if (!route.path || !route.component) return null;
|
||||
|
||||
// Clone the component and pass necessary props if it's a page component
|
||||
const componentWithProps = React.cloneElement(route.component as ReactElement, {
|
||||
...(route.id === 'chat' && { ref: chatRef }),
|
||||
...(route.component.props || {}),
|
||||
});
|
||||
|
||||
return (
|
||||
<Route
|
||||
key={`${route.id}-${index}`}
|
||||
path={route.path}
|
||||
element={componentWithProps}
|
||||
/>
|
||||
);
|
||||
}).filter(Boolean);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ height: "100%", maxHeight: "100%", minHeight: "100%", flexDirection: "column" }}>
|
||||
<Header {...{ setSnack, currentPath: page, navigate, navigationLinks }} />
|
||||
<Box sx={{
|
||||
height: "100%",
|
||||
maxHeight: "100%",
|
||||
minHeight: "100%",
|
||||
flexDirection: "column"
|
||||
}}>
|
||||
<Header
|
||||
setSnack={setSnack}
|
||||
currentPath={page}
|
||||
navigate={navigate}
|
||||
navigationItems={navigationItems}
|
||||
/>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
@ -174,49 +142,48 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#D3CDBF", /* Warm Gray */
|
||||
}}>
|
||||
<Scrollable
|
||||
className="BackstoryPageScrollable"
|
||||
sx={{
|
||||
m: 0,
|
||||
p: 0,
|
||||
mt: "72px", /* Needs to be kept in sync with the height of Header if the Header theme changes */
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "background.default",
|
||||
height: "100%",
|
||||
maxHeight: "100%",
|
||||
minHeight: "100%",
|
||||
minWidth: "min-content",
|
||||
}}
|
||||
>
|
||||
<BackstoryPageContainer>
|
||||
{!guest &&
|
||||
<Box>
|
||||
<LoadingComponent
|
||||
loadingText="Creating session..."
|
||||
loaderType="linear"
|
||||
withFade={true}
|
||||
fadeDuration={1200} />
|
||||
</Box>
|
||||
}
|
||||
{guest && <>
|
||||
<Outlet />
|
||||
{dynamicRoutes !== undefined && <Routes>{dynamicRoutes}</Routes>}
|
||||
</>
|
||||
}
|
||||
{location.pathname === "/" && <Footer />}
|
||||
</BackstoryPageContainer>
|
||||
</Scrollable>
|
||||
<Snack ref={snackRef} />
|
||||
<Scrollable
|
||||
className="BackstoryPageScrollable"
|
||||
sx={{
|
||||
m: 0,
|
||||
p: 0,
|
||||
mt: "72px", /* Needs to be kept in sync with the height of Header */
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "background.default",
|
||||
height: "100%",
|
||||
maxHeight: "100%",
|
||||
minHeight: "100%",
|
||||
minWidth: "min-content",
|
||||
}}
|
||||
>
|
||||
<BackstoryPageContainer>
|
||||
{!guest && (
|
||||
<Box>
|
||||
<LoadingComponent
|
||||
loadingText="Creating session..."
|
||||
loaderType="linear"
|
||||
withFade={true}
|
||||
fadeDuration={1200}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{guest && (
|
||||
<>
|
||||
<Outlet />
|
||||
<Routes>
|
||||
{generateRoutes()}
|
||||
</Routes>
|
||||
</>
|
||||
)}
|
||||
{location.pathname === "/" && <Footer />}
|
||||
</BackstoryPageContainer>
|
||||
</Scrollable>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export type {
|
||||
NavigationLinkType
|
||||
};
|
||||
|
||||
export {
|
||||
BackstoryLayout
|
||||
};
|
||||
};
|
@ -1,95 +1,45 @@
|
||||
// components/layout/BackstoryRoutes.tsx
|
||||
import React, { Ref, ReactNode } from "react";
|
||||
import { Route } from "react-router-dom";
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
import { BackstoryPageProps } from '../BackstoryTab';
|
||||
import { ConversationHandle } from '../Conversation';
|
||||
import { User } from 'types/types';
|
||||
import { getAllRoutes } from 'config/navigationConfig';
|
||||
import { NavigationItem } from 'types/navigation';
|
||||
import { useAppState } from "hooks/GlobalContext";
|
||||
|
||||
import { CandidateChatPage } from 'pages/CandidateChatPage';
|
||||
import { ResumeBuilderPage } from 'pages/ResumeBuilderPage';
|
||||
import { DocsPage } from 'pages/DocsPage';
|
||||
import { CreateProfilePage } from 'pages/CreateProfilePage';
|
||||
import { VectorVisualizerPage } from 'pages/VectorVisualizerPage';
|
||||
import { HomePage } from 'pages/HomePage';
|
||||
import { BetaPage } from 'pages/BetaPage';
|
||||
import { CandidateListingPage } from 'pages/FindCandidatePage';
|
||||
import { JobAnalysisPage } from 'pages/JobAnalysisPage';
|
||||
import { GenerateCandidate } from "pages/GenerateCandidate";
|
||||
import { ControlsPage } from 'pages/ControlsPage';
|
||||
import { LoginPage } from "pages/LoginPage";
|
||||
import { CandidateDashboardPage } from "pages/candidate/Dashboard"
|
||||
import { EmailVerificationPage } from "components/EmailVerificationComponents";
|
||||
import { JobMatchAnalysis } from "components/JobMatchAnalysis";
|
||||
|
||||
const BackstoryPage = () => (<BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>);
|
||||
const ResumesPage = () => (<BetaPage><Typography variant="h4">Resumes</Typography></BetaPage>);
|
||||
const QASetupPage = () => (<BetaPage><Typography variant="h4">Q&A Setup</Typography></BetaPage>);
|
||||
const SearchPage = () => (<BetaPage><Typography variant="h4">Search</Typography></BetaPage>);
|
||||
const SavedPage = () => (<BetaPage><Typography variant="h4">Saved</Typography></BetaPage>);
|
||||
const JobsPage = () => (<BetaPage><Typography variant="h4">Jobs</Typography></BetaPage>);
|
||||
const CompanyPage = () => (<BetaPage><Typography variant="h4">Company</Typography></BetaPage>);
|
||||
const LogoutPage = () => (<BetaPage><Typography variant="h4">Logout page...</Typography></BetaPage>);
|
||||
// const DashboardPage = () => (<BetaPage><Typography variant="h4">Dashboard</Typography></BetaPage>);
|
||||
// const AnalyticsPage = () => (<BetaPage><Typography variant="h4">Analytics</Typography></BetaPage>);
|
||||
// const SettingsPage = () => (<BetaPage><Typography variant="h4">Settings</Typography></BetaPage>);
|
||||
interface BackstoryDynamicRoutesProps extends BackstoryPageProps {
|
||||
chatRef: Ref<ConversationHandle>;
|
||||
user?: User | null;
|
||||
}
|
||||
|
||||
const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNode => {
|
||||
const { user, setSnack, submitQuery, chatRef } = props;
|
||||
const backstoryProps = {
|
||||
setSnack, submitQuery
|
||||
};
|
||||
let index=0
|
||||
const routes = [
|
||||
<Route key={`${index++}`} path="/" element={<HomePage/>} />,
|
||||
<Route key={`${index++}`} path="/chat" element={<CandidateChatPage ref={chatRef} {...backstoryProps} />} />,
|
||||
<Route key={`${index++}`} path="/docs" element={<DocsPage {...backstoryProps} />} />,
|
||||
<Route key={`${index++}`} path="/docs/:subPage" element={<DocsPage {...backstoryProps} />} />,
|
||||
<Route key={`${index++}`} path="/resume-builder" element={<ResumeBuilderPage {...backstoryProps} />} />,
|
||||
<Route key={`${index++}`} path="/knowledge-explorer" element={<VectorVisualizerPage {...backstoryProps} />} />,
|
||||
<Route key={`${index++}`} path="/find-a-candidate" element={<CandidateListingPage {...backstoryProps} />} />,
|
||||
<Route key={`${index++}`} path="/job-analysis" element={<JobAnalysisPage {...backstoryProps} />} />,
|
||||
<Route key={`${index++}`} path="/generate-candidate" element={<GenerateCandidate {...backstoryProps} />} />,
|
||||
<Route key={`${index++}`} path="/settings" element={<ControlsPage {...backstoryProps} />} />,
|
||||
];
|
||||
const { user, chatRef } = props;
|
||||
const userType = user?.userType || null;
|
||||
|
||||
if (!user) {
|
||||
routes.push(<Route key={`${index++}`} path="/register" element={(<BetaPage><CreateProfilePage /></BetaPage>)} />);
|
||||
routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage {...backstoryProps} />} />);
|
||||
routes.push(<Route key={`${index++}`} path="/login/verify-email" element={<EmailVerificationPage {...backstoryProps} />} />);
|
||||
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />);
|
||||
} else {
|
||||
routes.push(<Route key={`${index++}`} path="/login" element={<LoginPage {...backstoryProps} />} />);
|
||||
routes.push(<Route key={`${index++}`} path="/login/verify-email" element={<EmailVerificationPage {...backstoryProps} />} />);
|
||||
routes.push(<Route key={`${index++}`} path="/logout" element={<LogoutPage />} />);
|
||||
// Get all routes from navigation config
|
||||
const routes = getAllRoutes(userType);
|
||||
|
||||
if (user.userType === 'candidate') {
|
||||
routes.splice(-1, 0, ...[
|
||||
<Route key={`${index++}`} path="/candidate/dashboard" element={<CandidateDashboardPage {...backstoryProps} />} />,
|
||||
<Route key={`${index++}`} path="/candidate/dashboard/:subPage" element={<CandidateDashboardPage {...backstoryProps} />} />,
|
||||
<Route key={`${index++}`} path="/candidate/job-analysis" element={<JobAnalysisPage {...backstoryProps} />} />,
|
||||
<Route key={`${index++}`} path="/candidate/backstory" element={<BackstoryPage />} />,
|
||||
<Route key={`${index++}`} path="/candidate/resumes" element={<ResumesPage />} />,
|
||||
<Route key={`${index++}`} path="/candidate/qa-setup" element={<QASetupPage />} />,
|
||||
]);
|
||||
}
|
||||
return routes.map((route: NavigationItem, index: number) => {
|
||||
if (!route.path || !route.component) return null;
|
||||
|
||||
if (user.userType === 'employer') {
|
||||
routes.splice(-1, 0, ...[
|
||||
<Route key={`${index++}`} path="/search" element={<SearchPage />} />,
|
||||
<Route key={`${index++}`} path="/saved" element={<SavedPage />} />,
|
||||
<Route key={`${index++}`} path="/jobs" element={<JobsPage />} />,
|
||||
<Route key={`${index++}`} path="/company" element={<CompanyPage />} />,
|
||||
]);
|
||||
}
|
||||
}
|
||||
// Clone the component and pass necessary props
|
||||
const componentWithProps = React.cloneElement(route.component as React.ReactElement, {
|
||||
// Special handling for chat component ref
|
||||
...(route.id === 'chat' && { ref: chatRef }),
|
||||
// Preserve any existing props
|
||||
...(route.component.props || {}),
|
||||
});
|
||||
|
||||
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />);
|
||||
|
||||
return routes;
|
||||
return (
|
||||
<Route
|
||||
key={`${route.id}-${index}`}
|
||||
path={route.path}
|
||||
element={componentWithProps}
|
||||
/>
|
||||
);
|
||||
}).filter(Boolean);
|
||||
};
|
||||
|
||||
export { getBackstoryDynamicRoutes };
|
||||
export { getBackstoryDynamicRoutes };
|
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
// components/layout/Header.tsx
|
||||
import React, { JSX, useEffect, useState } from 'react';
|
||||
import { NavigateFunction, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
AppBar,
|
||||
@ -17,6 +18,14 @@ import {
|
||||
Fade,
|
||||
Popover,
|
||||
Paper,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Collapse,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
} from '@mui/material';
|
||||
import { styled, useTheme } from '@mui/material/styles';
|
||||
import {
|
||||
@ -26,14 +35,15 @@ import {
|
||||
Logout,
|
||||
Settings,
|
||||
ExpandMore,
|
||||
ExpandLess,
|
||||
KeyboardArrowDown,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import { NavigationLinkType } from 'components/layout/BackstoryLayout';
|
||||
import { NavigationItem } from 'types/navigation';
|
||||
import { Beta } from 'components/ui/Beta';
|
||||
import { Candidate, Employer } from 'types/types';
|
||||
import { SetSnackType } from 'components/Snack';
|
||||
import { CopyBubble } from 'components/CopyBubble';
|
||||
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
|
||||
|
||||
import 'components/layout/Header.css';
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
@ -79,39 +89,17 @@ const UserButton = styled(Button)(({ theme }) => ({
|
||||
|
||||
const MobileDrawer = styled(Drawer)(({ theme }) => ({
|
||||
'& .MuiDrawer-paper': {
|
||||
width: 280,
|
||||
width: 320,
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
}));
|
||||
|
||||
const MobileMenuTabs = styled(Tabs)(({ theme }) => ({
|
||||
'& .MuiTabs-flexContainer': {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
'& .MuiTab-root': {
|
||||
minHeight: 48,
|
||||
textTransform: 'uppercase',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(1, 2),
|
||||
gap: theme.spacing(1),
|
||||
color: theme.palette.text.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
'& .MuiTypography-root': {
|
||||
color: theme.palette.primary.contrastText,
|
||||
},
|
||||
'& .MuiSvgIcon-root': {
|
||||
color: theme.palette.primary.contrastText,
|
||||
},
|
||||
},
|
||||
},
|
||||
'& .MuiTabs-indicator': {
|
||||
display: 'none',
|
||||
const DropdownButton = styled(Button)(({ theme }) => ({
|
||||
color: theme.palette.primary.contrastText,
|
||||
textTransform: 'none',
|
||||
minHeight: 48,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
}));
|
||||
|
||||
@ -123,51 +111,11 @@ const UserMenuContainer = styled(Paper)(({ theme }) => ({
|
||||
minWidth: 200,
|
||||
}));
|
||||
|
||||
const UserMenuTabs = styled(Tabs)(({ theme }) => ({
|
||||
'& .MuiTabs-flexContainer': {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
'& .MuiTab-root': {
|
||||
minHeight: 48,
|
||||
textTransform: 'uppercase',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(1, 2),
|
||||
gap: theme.spacing(1),
|
||||
color: theme.palette.text.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
},
|
||||
},
|
||||
'& .MuiTabs-indicator': {
|
||||
display: 'none',
|
||||
},
|
||||
}));
|
||||
|
||||
const MenuItemBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
width: '100%',
|
||||
'& .MuiSvgIcon-root': {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
}));
|
||||
|
||||
const MenuDivider = styled(Divider)(({ theme }) => ({
|
||||
margin: theme.spacing(0.5, 0),
|
||||
backgroundColor: theme.palette.divider,
|
||||
}));
|
||||
|
||||
interface HeaderProps {
|
||||
transparent?: boolean;
|
||||
className?: string;
|
||||
navigate: NavigateFunction;
|
||||
navigationLinks: NavigationLinkType[];
|
||||
navigationItems: NavigationItem[];
|
||||
currentPath: string;
|
||||
sessionId?: string | null;
|
||||
setSnack: SetSnackType;
|
||||
@ -181,7 +129,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
transparent = false,
|
||||
className,
|
||||
navigate,
|
||||
navigationLinks,
|
||||
navigationItems,
|
||||
sessionId,
|
||||
setSnack,
|
||||
} = props;
|
||||
@ -190,42 +138,17 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
|
||||
const name = (user?.firstName || user?.email || '');
|
||||
|
||||
const mainNavSections: NavigationLinkType[] = [
|
||||
{ path: '/', label: <BackstoryLogo /> },
|
||||
...navigationLinks
|
||||
];
|
||||
|
||||
// State for page navigation - only for main navigation
|
||||
const [currentTab, setCurrentTab] = useState<string | false>("/");
|
||||
const [userMenuTab, setUserMenuTab] = useState<string>("");
|
||||
// State for desktop dropdown menus
|
||||
const [dropdownAnchors, setDropdownAnchors] = useState<{ [key: string]: HTMLElement | null }>({});
|
||||
|
||||
// State for mobile drawer
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [mobileExpanded, setMobileExpanded] = useState<{ [key: string]: boolean }>({});
|
||||
|
||||
// State for user menu
|
||||
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
|
||||
const userMenuOpen = Boolean(userMenuAnchor);
|
||||
|
||||
// Helper function to determine which action page we're in
|
||||
const getTailSection = (pathname: string): string | false => {
|
||||
// Handle home page
|
||||
if (pathname === '/') return '/';
|
||||
|
||||
// Split path and check against main sections
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
if (segments.length === 0) return '/';
|
||||
|
||||
const lastSegment = `/${segments[segments.length - 1]}`;
|
||||
|
||||
// Check if this matches any of our main navigation sections
|
||||
const matchingSection = mainNavSections.find(section =>
|
||||
section.path === lastSegment ||
|
||||
(section.path !== '/' && pathname.endsWith(section.path))
|
||||
);
|
||||
|
||||
return matchingSection ? matchingSection.path : false; // Return false for routes that shouldn't show in main nav
|
||||
};
|
||||
|
||||
// User menu items
|
||||
const userMenuItems = [
|
||||
{
|
||||
@ -263,22 +186,45 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const mainSection = getTailSection(location.pathname);
|
||||
|
||||
// Only update if the section is different from current tab
|
||||
if (mainSection !== currentTab) {
|
||||
setCurrentTab(mainSection); // mainSection is either a string or false
|
||||
// Helper function to check if current path matches navigation item
|
||||
const isCurrentPath = (item: NavigationItem): boolean => {
|
||||
if (!item.path) return false;
|
||||
if (item.exact) {
|
||||
return location.pathname === item.path;
|
||||
}
|
||||
}, [location.pathname, currentTab]);
|
||||
|
||||
return location.pathname.startsWith(item.path);
|
||||
};
|
||||
|
||||
// Helper function to check if any child is current path
|
||||
const hasActiveChild = (item: NavigationItem): boolean => {
|
||||
if (!item.children) return false;
|
||||
return item.children.some(child => isCurrentPath(child) || hasActiveChild(child));
|
||||
};
|
||||
|
||||
// Desktop dropdown handlers
|
||||
const handleDropdownOpen = (event: React.MouseEvent<HTMLElement>, itemId: string) => {
|
||||
setDropdownAnchors(prev => ({ ...prev, [itemId]: event.currentTarget }));
|
||||
};
|
||||
|
||||
const handleDropdownClose = (itemId: string) => {
|
||||
setDropdownAnchors(prev => ({ ...prev, [itemId]: null }));
|
||||
};
|
||||
|
||||
// Mobile accordion handlers
|
||||
const handleMobileToggle = (itemId: string) => {
|
||||
setMobileExpanded(prev => ({ ...prev, [itemId]: !prev[itemId] }));
|
||||
};
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setMobileOpen(!mobileOpen);
|
||||
};
|
||||
|
||||
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setUserMenuAnchor(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleUserMenuClose = () => {
|
||||
setUserMenuAnchor(null);
|
||||
setUserMenuTab("");
|
||||
};
|
||||
|
||||
const handleUserMenuAction = (item: typeof userMenuItems[0]) => {
|
||||
@ -288,83 +234,152 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrawerToggle = () => {
|
||||
setMobileOpen(!mobileOpen);
|
||||
// Navigation handlers
|
||||
const handleNavigate = (path: string) => {
|
||||
navigate(path);
|
||||
setMobileOpen(false);
|
||||
// Close all dropdowns
|
||||
setDropdownAnchors({});
|
||||
};
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: string | false) => {
|
||||
if (newValue !== false) {
|
||||
setCurrentTab(newValue);
|
||||
navigate(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
// Render desktop navigation links
|
||||
const renderNavLinks = () => {
|
||||
// Render desktop navigation with dropdowns
|
||||
const renderDesktopNavigation = () => {
|
||||
return (
|
||||
<Tabs
|
||||
value={currentTab}
|
||||
onChange={handleTabChange}
|
||||
indicatorColor="secondary"
|
||||
textColor="inherit"
|
||||
variant="fullWidth"
|
||||
allowScrollButtonsMobile
|
||||
aria-label="Backstory navigation"
|
||||
>
|
||||
{mainNavSections.map((section) => (
|
||||
<Tab
|
||||
sx={{
|
||||
minWidth: section.path === '/' ? "max-content" : "auto",
|
||||
}}
|
||||
key={section.path}
|
||||
value={section.path}
|
||||
label={section.label}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{navigationItems.map((item) => {
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isActive = isCurrentPath(item) || hasActiveChild(item);
|
||||
|
||||
if (hasChildren) {
|
||||
return (
|
||||
<Box key={item.id}>
|
||||
<DropdownButton
|
||||
onClick={(e) => handleDropdownOpen(e, item.id)}
|
||||
endIcon={<KeyboardArrowDown />}
|
||||
sx={{
|
||||
backgroundColor: isActive ? 'action.selected' : 'transparent',
|
||||
color: isActive ? 'secondary.main' : 'primary.contrastText',
|
||||
}}
|
||||
>
|
||||
{item.icon && <Box sx={{ mr: 1, display: 'flex' }}>{item.icon}</Box>}
|
||||
{item.label}
|
||||
</DropdownButton>
|
||||
<Menu
|
||||
anchorEl={dropdownAnchors[item.id]}
|
||||
open={Boolean(dropdownAnchors[item.id])}
|
||||
onClose={() => handleDropdownClose(item.id)}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||
TransitionComponent={Fade}
|
||||
>
|
||||
{item.children?.map((child) => (
|
||||
<MenuItem
|
||||
key={child.id}
|
||||
onClick={() => child.path && handleNavigate(child.path)}
|
||||
selected={isCurrentPath(child)}
|
||||
disabled={!child.path}
|
||||
>
|
||||
{child.icon && <ListItemIcon>{child.icon}</ListItemIcon>}
|
||||
<ListItemText>{child.label}</ListItemText>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DropdownButton
|
||||
key={item.id}
|
||||
onClick={() => item.path && handleNavigate(item.path)}
|
||||
sx={{
|
||||
backgroundColor: isActive ? 'action.selected' : 'transparent',
|
||||
color: isActive ? 'secondary.main' : 'primary.contrastText',
|
||||
}}
|
||||
>
|
||||
{item.icon && <Box sx={{ mr: 1, display: 'flex' }}>{item.icon}</Box>}
|
||||
{item.label}
|
||||
</DropdownButton>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// Render mobile drawer content
|
||||
const renderDrawerContent = () => {
|
||||
return (
|
||||
<>
|
||||
<MobileMenuTabs
|
||||
orientation="vertical"
|
||||
value={currentTab}
|
||||
onChange={handleTabChange}
|
||||
>
|
||||
{mainNavSections.map((section) => (
|
||||
<Tab
|
||||
key={section.path || '/'}
|
||||
value={section.path}
|
||||
label={
|
||||
<MenuItemBox>
|
||||
{section.label}
|
||||
</MenuItemBox>
|
||||
}
|
||||
onClick={(e) => {
|
||||
handleDrawerToggle();
|
||||
setCurrentTab(section.path);
|
||||
navigate(section.path);
|
||||
// Render mobile accordion navigation
|
||||
const renderMobileNavigation = () => {
|
||||
const renderNavigationItem = (item: NavigationItem, depth: number = 0) => {
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isActive = isCurrentPath(item) || hasActiveChild(item);
|
||||
const isExpanded = mobileExpanded[item.id];
|
||||
|
||||
return (
|
||||
<Box key={item.id}>
|
||||
<ListItem disablePadding sx={{ pl: depth * 2 }}>
|
||||
<ListItemButton
|
||||
onClick={() => {
|
||||
if (hasChildren) {
|
||||
handleMobileToggle(item.id);
|
||||
} else if (item.path) {
|
||||
handleNavigate(item.path);
|
||||
}
|
||||
}}
|
||||
selected={isActive}
|
||||
sx={{
|
||||
backgroundColor: isActive ? 'action.selected' : 'transparent',
|
||||
'&.Mui-selected': {
|
||||
backgroundColor: 'primary.main',
|
||||
color: 'primary.contrastText',
|
||||
'& .MuiListItemIcon-root': {
|
||||
color: 'primary.contrastText',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</MobileMenuTabs>
|
||||
<MenuDivider />
|
||||
{!user && (
|
||||
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
fullWidth
|
||||
onClick={() => { handleDrawerToggle(); navigate("/login"); }}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Box>
|
||||
{item.icon && (
|
||||
<ListItemIcon sx={{ minWidth: 36 }}>
|
||||
{item.icon}
|
||||
</ListItemIcon>
|
||||
)}
|
||||
<ListItemText
|
||||
primary={item.label}
|
||||
sx={{
|
||||
'& .MuiTypography-root': {
|
||||
fontSize: depth > 0 ? '0.875rem' : '1rem',
|
||||
fontWeight: depth === 0 ? 500 : 400,
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{hasChildren && (
|
||||
<IconButton size="small">
|
||||
{isExpanded ? <ExpandLess /> : <ExpandMore />}
|
||||
</IconButton>
|
||||
)}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
{hasChildren && (
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<List disablePadding>
|
||||
{item.children?.map((child) => renderNavigationItem(child, depth + 1))}
|
||||
</List>
|
||||
</Collapse>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<List sx={{ pt: 0 }}>
|
||||
{navigationItems.map((item) => renderNavigationItem(item))}
|
||||
<Divider sx={{ my: 1 }} />
|
||||
{!user && (
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton onClick={() => handleNavigate('/login')}>
|
||||
<ListItemText primary="Login" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)}
|
||||
</>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
@ -372,29 +387,20 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
const renderUserMenu = () => {
|
||||
return (
|
||||
<UserMenuContainer>
|
||||
<UserMenuTabs
|
||||
orientation="vertical"
|
||||
value={userMenuTab}
|
||||
onChange={(e, newValue) => setUserMenuTab(newValue)}
|
||||
>
|
||||
<List dense>
|
||||
{userMenuItems.map((item, index) => (
|
||||
item.id === 'divider' ? (
|
||||
<MenuDivider key={`divider-${index}`} />
|
||||
<Divider key={`divider-${index}`} />
|
||||
) : (
|
||||
<Tab
|
||||
key={item.id}
|
||||
value={item.id}
|
||||
label={
|
||||
<MenuItemBox>
|
||||
{item.icon}
|
||||
<Typography variant="body2">{item.label}</Typography>
|
||||
</MenuItemBox>
|
||||
}
|
||||
onClick={() => handleUserMenuAction(item)}
|
||||
/>
|
||||
<ListItem key={item.id} disablePadding>
|
||||
<ListItemButton onClick={() => handleUserMenuAction(item)}>
|
||||
{item.icon && <ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>}
|
||||
<ListItemText primary={item.label} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)
|
||||
))}
|
||||
</UserMenuTabs>
|
||||
</List>
|
||||
</UserMenuContainer>
|
||||
);
|
||||
};
|
||||
@ -403,19 +409,17 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
const renderUserSection = () => {
|
||||
if (!user) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
color="info"
|
||||
variant="contained"
|
||||
onClick={() => navigate("/login") }
|
||||
sx={{
|
||||
display: { xs: 'none', sm: 'block' },
|
||||
color: theme.palette.primary.contrastText,
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
color="info"
|
||||
variant="contained"
|
||||
onClick={() => navigate("/login")}
|
||||
sx={{
|
||||
display: { xs: 'none', sm: 'block' },
|
||||
color: theme.palette.primary.contrastText,
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@ -428,10 +432,10 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
aria-expanded={userMenuOpen ? 'true' : undefined}
|
||||
>
|
||||
<Avatar sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
bgcolor: theme.palette.secondary.main,
|
||||
}}>
|
||||
width: 32,
|
||||
height: 32,
|
||||
bgcolor: theme.palette.secondary.main,
|
||||
}}>
|
||||
{name.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
|
||||
@ -472,7 +476,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
<Toolbar disableGutters>
|
||||
{/* Navigation Links - Desktop */}
|
||||
<NavLinksContainer>
|
||||
{renderNavLinks()}
|
||||
{renderDesktopNavigation()}
|
||||
</NavLinksContainer>
|
||||
|
||||
{/* User Actions Section */}
|
||||
@ -492,22 +496,27 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
{sessionId && <CopyBubble
|
||||
tooltip="Copy link"
|
||||
color="inherit"
|
||||
aria-label="copy link"
|
||||
edge="end"
|
||||
sx={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
opacity: 1,
|
||||
bgcolor: 'inherit',
|
||||
'&:hover': { bgcolor: 'action.hover', opacity: 1 },
|
||||
}}
|
||||
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
|
||||
onClick={() => { navigate(`${window.location.pathname}?id=${sessionId}`); setSnack("Link copied!") }}
|
||||
size="large"
|
||||
/>}
|
||||
{sessionId && (
|
||||
<CopyBubble
|
||||
tooltip="Copy link"
|
||||
color="inherit"
|
||||
aria-label="copy link"
|
||||
edge="end"
|
||||
sx={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
opacity: 1,
|
||||
bgcolor: 'inherit',
|
||||
'&:hover': { bgcolor: 'action.hover', opacity: 1 },
|
||||
}}
|
||||
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
|
||||
onClick={() => {
|
||||
navigate(`${window.location.pathname}?id=${sessionId}`);
|
||||
setSnack("Link copied!");
|
||||
}}
|
||||
size="large"
|
||||
/>
|
||||
)}
|
||||
</UserActionsContainer>
|
||||
|
||||
{/* Mobile Navigation Drawer */}
|
||||
@ -520,11 +529,14 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
||||
keepMounted: true,
|
||||
}}
|
||||
>
|
||||
{renderDrawerContent()}
|
||||
{renderMobileNavigation()}
|
||||
</MobileDrawer>
|
||||
</Toolbar>
|
||||
</Container>
|
||||
<Beta sx={{ left: "-90px", "& .mobile": { right: "-72px" } }} onClick={() => { navigate('/docs/beta'); }} />
|
||||
<Beta
|
||||
sx={{ left: "-90px", "& .mobile": { right: "-72px" } }}
|
||||
onClick={() => { navigate('/docs/beta'); }}
|
||||
/>
|
||||
</StyledAppBar>
|
||||
);
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import { BackstoryElementProps } from 'components/BackstoryTab';
|
||||
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
||||
import { Candidate } from "types/types";
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { useSelectedCandidate } from 'hooks/GlobalContext';
|
||||
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
|
||||
|
||||
interface CandidatePickerProps extends BackstoryElementProps {
|
||||
onSelect?: (candidate: Candidate) => void
|
||||
@ -18,7 +18,7 @@ const CandidatePicker = (props: CandidatePickerProps) => {
|
||||
const { apiClient, user } = useAuth();
|
||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
|
||||
const navigate = useNavigate();
|
||||
const { setSnack } = props;
|
||||
const { setSnack } = useAppState();
|
||||
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -7,7 +7,7 @@ import { BackstoryElementProps } from 'components/BackstoryTab';
|
||||
import { JobInfo } from 'components/ui/JobInfo';
|
||||
import { Job, JobFull } from "types/types";
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { useSelectedJob } from 'hooks/GlobalContext';
|
||||
import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
|
||||
|
||||
interface JobPickerProps extends BackstoryElementProps {
|
||||
onSelect?: (job: JobFull) => void
|
||||
@ -17,7 +17,7 @@ const JobPicker = (props: JobPickerProps) => {
|
||||
const { onSelect } = props;
|
||||
const { apiClient } = useAuth();
|
||||
const { selectedJob, setSelectedJob } = useSelectedJob();
|
||||
const { setSnack } = props;
|
||||
const { setSnack } = useAppState();
|
||||
const [jobs, setJobs] = useState<JobFull[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
397
frontend/src/config/navigationConfig.tsx
Normal file
397
frontend/src/config/navigationConfig.tsx
Normal file
@ -0,0 +1,397 @@
|
||||
|
||||
// config/navigationConfig.tsx
|
||||
import React from 'react';
|
||||
import {
|
||||
Chat as ChatIcon,
|
||||
Dashboard as DashboardIcon,
|
||||
Description as DescriptionIcon,
|
||||
BarChart as BarChartIcon,
|
||||
Settings as SettingsIcon,
|
||||
Work as WorkIcon,
|
||||
Info as InfoIcon,
|
||||
Person as PersonIcon,
|
||||
Business as BusinessIcon,
|
||||
Search as SearchIcon,
|
||||
Bookmark as BookmarkIcon,
|
||||
History as HistoryIcon,
|
||||
QuestionAnswer as QuestionAnswerIcon,
|
||||
AttachMoney as AttachMoneyIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
|
||||
import { HomePage } from 'pages/HomePage';
|
||||
import { CandidateChatPage } from 'pages/CandidateChatPage';
|
||||
import { ResumeBuilderPage } from 'pages/ResumeBuilderPage';
|
||||
import { DocsPage } from 'pages/DocsPage';
|
||||
import { CreateProfilePage } from 'pages/CreateProfilePage';
|
||||
import { VectorVisualizerPage } from 'pages/VectorVisualizerPage';
|
||||
import { BetaPage } from 'pages/BetaPage';
|
||||
import { CandidateListingPage } from 'pages/FindCandidatePage';
|
||||
import { JobAnalysisPage } from 'pages/JobAnalysisPage';
|
||||
import { GenerateCandidate } from 'pages/GenerateCandidate';
|
||||
import { ControlsPage } from 'pages/ControlsPage';
|
||||
import { LoginPage } from 'pages/LoginPage';
|
||||
import { CandidateDashboardPage } from 'pages/candidate/Dashboard';
|
||||
import { EmailVerificationPage } from 'components/EmailVerificationComponents';
|
||||
import { Typography } from '@mui/material';
|
||||
import { NavigationConfig, NavigationItem } from 'types/navigation';
|
||||
|
||||
// Beta page components for placeholder routes
|
||||
const BackstoryPage = () => (<BetaPage><Typography variant="h4">Backstory</Typography></BetaPage>);
|
||||
const ResumesPage = () => (<BetaPage><Typography variant="h4">Resumes</Typography></BetaPage>);
|
||||
const QASetupPage = () => (<BetaPage><Typography variant="h4">Q&A Setup</Typography></BetaPage>);
|
||||
const SearchPage = () => (<BetaPage><Typography variant="h4">Search</Typography></BetaPage>);
|
||||
const SavedPage = () => (<BetaPage><Typography variant="h4">Saved</Typography></BetaPage>);
|
||||
const JobsPage = () => (<BetaPage><Typography variant="h4">Jobs</Typography></BetaPage>);
|
||||
const CompanyPage = () => (<BetaPage><Typography variant="h4">Company</Typography></BetaPage>);
|
||||
const LogoutPage = () => (<BetaPage><Typography variant="h4">Logout page...</Typography></BetaPage>);
|
||||
const AnalyticsPage = () => (<BetaPage><Typography variant="h4">Analytics</Typography></BetaPage>);
|
||||
const SettingsPage = () => (<BetaPage><Typography variant="h4">Settings</Typography></BetaPage>);
|
||||
|
||||
export const navigationConfig: NavigationConfig = {
|
||||
items: [
|
||||
{
|
||||
id: 'home',
|
||||
label: <BackstoryLogo />,
|
||||
path: '/',
|
||||
component: <HomePage />,
|
||||
userTypes: ['guest', 'candidate', 'employer'],
|
||||
exact: true,
|
||||
},
|
||||
{
|
||||
id: 'find-candidate',
|
||||
label: 'Find a Candidate',
|
||||
path: '/find-a-candidate',
|
||||
icon: <InfoIcon />,
|
||||
component: <CandidateListingPage />,
|
||||
userTypes: ['guest', 'candidate', 'employer'],
|
||||
},
|
||||
{
|
||||
id: 'docs',
|
||||
label: 'Docs',
|
||||
path: '/docs',
|
||||
icon: <InfoIcon />,
|
||||
component: <DocsPage />,
|
||||
userTypes: ['guest', 'candidate', 'employer'],
|
||||
children: [
|
||||
{
|
||||
id: 'docs-subpage',
|
||||
label: 'Docs Subpage',
|
||||
path: '/docs/:subPage',
|
||||
component: <DocsPage />,
|
||||
userTypes: ['guest', 'candidate', 'employer'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'chat',
|
||||
label: 'Chat',
|
||||
path: '/chat',
|
||||
icon: <ChatIcon />,
|
||||
component: <CandidateChatPage />,
|
||||
userTypes: ['candidate', 'employer'],
|
||||
},
|
||||
{
|
||||
id: 'candidate-menu',
|
||||
label: 'Candidate Tools',
|
||||
icon: <PersonIcon />,
|
||||
userTypes: ['candidate'],
|
||||
children: [
|
||||
{
|
||||
id: 'candidate-dashboard',
|
||||
label: 'Dashboard',
|
||||
path: '/candidate/dashboard',
|
||||
icon: <DashboardIcon />,
|
||||
component: <CandidateDashboardPage />,
|
||||
userTypes: ['candidate'],
|
||||
children: [
|
||||
{
|
||||
id: 'candidate-dashboard-subpage',
|
||||
label: 'Dashboard Subpage',
|
||||
path: '/candidate/dashboard/:subPage',
|
||||
component: <CandidateDashboardPage />,
|
||||
userTypes: ['candidate'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'candidate-job-analysis',
|
||||
label: 'Job Analysis',
|
||||
path: '/candidate/job-analysis',
|
||||
icon: <WorkIcon />,
|
||||
component: <JobAnalysisPage />,
|
||||
userTypes: ['candidate'],
|
||||
},
|
||||
{
|
||||
id: 'candidate-resume-builder',
|
||||
label: 'Resume Builder',
|
||||
path: '/candidate/resume-builder',
|
||||
icon: <DescriptionIcon />,
|
||||
component: <ResumeBuilderPage />,
|
||||
userTypes: ['candidate'],
|
||||
},
|
||||
{
|
||||
id: 'candidate-backstory',
|
||||
label: 'Backstory',
|
||||
path: '/candidate/backstory',
|
||||
icon: <HistoryIcon />,
|
||||
component: <BackstoryPage />,
|
||||
userTypes: ['candidate'],
|
||||
},
|
||||
{
|
||||
id: 'candidate-resumes',
|
||||
label: 'Resumes',
|
||||
path: '/candidate/resumes',
|
||||
icon: <DescriptionIcon />,
|
||||
component: <ResumesPage />,
|
||||
userTypes: ['candidate'],
|
||||
},
|
||||
{
|
||||
id: 'candidate-qa-setup',
|
||||
label: 'Q&A Setup',
|
||||
path: '/candidate/qa-setup',
|
||||
icon: <QuestionAnswerIcon />,
|
||||
component: <QASetupPage />,
|
||||
userTypes: ['candidate'],
|
||||
},
|
||||
{
|
||||
id: 'candidate-analytics',
|
||||
label: 'Analytics',
|
||||
path: '/candidate/analytics',
|
||||
icon: <BarChartIcon />,
|
||||
component: <AnalyticsPage />,
|
||||
userTypes: ['candidate'],
|
||||
},
|
||||
{
|
||||
id: 'candidate-settings',
|
||||
label: 'Settings',
|
||||
path: '/candidate/settings',
|
||||
icon: <SettingsIcon />,
|
||||
component: <SettingsPage />,
|
||||
userTypes: ['candidate'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'employer-menu',
|
||||
label: 'Employer Tools',
|
||||
icon: <BusinessIcon />,
|
||||
userTypes: ['employer'],
|
||||
children: [
|
||||
{
|
||||
id: 'employer-job-analysis',
|
||||
label: 'Job Analysis',
|
||||
path: '/employer/job-analysis',
|
||||
icon: <WorkIcon />,
|
||||
component: <JobAnalysisPage />,
|
||||
userTypes: ['employer'],
|
||||
},
|
||||
{
|
||||
id: 'employer-resume-builder',
|
||||
label: 'Resume Builder',
|
||||
path: '/employer/resume-builder',
|
||||
icon: <DescriptionIcon />,
|
||||
component: <ResumeBuilderPage />,
|
||||
userTypes: ['employer'],
|
||||
},
|
||||
{
|
||||
id: 'employer-knowledge-explorer',
|
||||
label: 'Knowledge Explorer',
|
||||
path: '/employer/knowledge-explorer',
|
||||
icon: <WorkIcon />,
|
||||
component: <VectorVisualizerPage />,
|
||||
userTypes: ['employer'],
|
||||
},
|
||||
{
|
||||
id: 'employer-search',
|
||||
label: 'Search',
|
||||
path: '/employer/search',
|
||||
icon: <SearchIcon />,
|
||||
component: <SearchPage />,
|
||||
userTypes: ['employer'],
|
||||
},
|
||||
{
|
||||
id: 'employer-saved',
|
||||
label: 'Saved',
|
||||
path: '/employer/saved',
|
||||
icon: <BookmarkIcon />,
|
||||
component: <SavedPage />,
|
||||
userTypes: ['employer'],
|
||||
},
|
||||
{
|
||||
id: 'employer-jobs',
|
||||
label: 'Jobs',
|
||||
path: '/employer/jobs',
|
||||
icon: <WorkIcon />,
|
||||
component: <JobsPage />,
|
||||
userTypes: ['employer'],
|
||||
},
|
||||
{
|
||||
id: 'employer-company',
|
||||
label: 'Company',
|
||||
path: '/employer/company',
|
||||
icon: <BusinessIcon />,
|
||||
component: <CompanyPage />,
|
||||
userTypes: ['employer'],
|
||||
},
|
||||
{
|
||||
id: 'employer-analytics',
|
||||
label: 'Analytics',
|
||||
path: '/employer/analytics',
|
||||
icon: <BarChartIcon />,
|
||||
component: <AnalyticsPage />,
|
||||
userTypes: ['employer'],
|
||||
},
|
||||
{
|
||||
id: 'employer-settings',
|
||||
label: 'Settings',
|
||||
path: '/employer/settings',
|
||||
icon: <SettingsIcon />,
|
||||
component: <SettingsPage />,
|
||||
userTypes: ['employer'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'global-tools',
|
||||
label: 'Tools',
|
||||
icon: <SettingsIcon />,
|
||||
userTypes: ['candidate', 'employer'],
|
||||
children: [
|
||||
{
|
||||
id: 'resume-builder',
|
||||
label: 'Resume Builder',
|
||||
path: '/resume-builder',
|
||||
icon: <DescriptionIcon />,
|
||||
component: <ResumeBuilderPage />,
|
||||
userTypes: ['candidate', 'employer'],
|
||||
},
|
||||
{
|
||||
id: 'knowledge-explorer',
|
||||
label: 'Knowledge Explorer',
|
||||
path: '/knowledge-explorer',
|
||||
icon: <WorkIcon />,
|
||||
component: <VectorVisualizerPage />,
|
||||
userTypes: ['candidate', 'employer'],
|
||||
},
|
||||
{
|
||||
id: 'job-analysis',
|
||||
label: 'Job Analysis',
|
||||
path: '/job-analysis',
|
||||
icon: <WorkIcon />,
|
||||
component: <JobAnalysisPage />,
|
||||
userTypes: ['candidate', 'employer'],
|
||||
},
|
||||
{
|
||||
id: 'generate-candidate',
|
||||
label: 'Generate Candidate',
|
||||
path: '/generate-candidate',
|
||||
icon: <PersonIcon />,
|
||||
component: <GenerateCandidate />,
|
||||
userTypes: ['candidate', 'employer'],
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Settings',
|
||||
path: '/settings',
|
||||
icon: <SettingsIcon />,
|
||||
component: <ControlsPage />,
|
||||
userTypes: ['candidate', 'employer'],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Auth routes (special handling)
|
||||
{
|
||||
id: 'auth',
|
||||
label: 'Auth',
|
||||
userTypes: ['guest', 'candidate', 'employer'],
|
||||
children: [
|
||||
{
|
||||
id: 'register',
|
||||
label: 'Register',
|
||||
path: '/register',
|
||||
component: <BetaPage><CreateProfilePage /></BetaPage>,
|
||||
userTypes: ['guest'],
|
||||
},
|
||||
{
|
||||
id: 'login',
|
||||
label: 'Login',
|
||||
path: '/login',
|
||||
component: <LoginPage />,
|
||||
userTypes: ['guest', 'candidate', 'employer'],
|
||||
},
|
||||
{
|
||||
id: 'verify-email',
|
||||
label: 'Verify Email',
|
||||
path: '/login/verify-email',
|
||||
component: <EmailVerificationPage />,
|
||||
userTypes: ['guest', 'candidate', 'employer'],
|
||||
},
|
||||
{
|
||||
id: 'logout',
|
||||
label: 'Logout',
|
||||
path: '/logout',
|
||||
component: <LogoutPage />,
|
||||
userTypes: ['candidate', 'employer'],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Catch-all route
|
||||
{
|
||||
id: 'catch-all',
|
||||
label: 'Not Found',
|
||||
path: '*',
|
||||
component: <BetaPage />,
|
||||
userTypes: ['guest', 'candidate', 'employer'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Utility functions for working with navigation config
|
||||
export const getNavigationItemsForUser = (userType: 'guest' | 'candidate' | 'employer' | null): NavigationItem[] => {
|
||||
const currentUserType = userType || 'guest';
|
||||
|
||||
const filterItems = (items: NavigationItem[]): NavigationItem[] => {
|
||||
return items
|
||||
.filter(item => !item.userTypes || item.userTypes.includes(currentUserType))
|
||||
.map(item => ({
|
||||
...item,
|
||||
children: item.children ? filterItems(item.children) : undefined,
|
||||
}))
|
||||
.filter(item => item.path || (item.children && item.children.length > 0));
|
||||
};
|
||||
|
||||
return filterItems(navigationConfig.items);
|
||||
};
|
||||
|
||||
export const getAllRoutes = (userType: 'guest' | 'candidate' | 'employer' | null): NavigationItem[] => {
|
||||
const currentUserType = userType || 'guest';
|
||||
|
||||
const extractRoutes = (items: NavigationItem[]): NavigationItem[] => {
|
||||
const routes: NavigationItem[] = [];
|
||||
|
||||
items.forEach(item => {
|
||||
if (!item.userTypes || item.userTypes.includes(currentUserType)) {
|
||||
if (item.path && item.component) {
|
||||
routes.push(item);
|
||||
}
|
||||
if (item.children) {
|
||||
routes.push(...extractRoutes(item.children));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return routes;
|
||||
};
|
||||
|
||||
return extractRoutes(navigationConfig.items);
|
||||
};
|
||||
|
||||
export const getMainNavigationItems = (userType: 'guest' | 'candidate' | 'employer' | null): NavigationItem[] => {
|
||||
return getNavigationItemsForUser(userType)
|
||||
.filter(item =>
|
||||
item.id !== 'auth' &&
|
||||
item.id !== 'catch-all' &&
|
||||
(item.path || (item.children && item.children.length > 0))
|
||||
);
|
||||
};
|
@ -1,8 +1,9 @@
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
|
||||
import * as Types from 'types/types';
|
||||
// Assuming you're using React Router
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { SetSnackType, SeverityType, Snack } from 'components/Snack';
|
||||
|
||||
// ============================
|
||||
// Storage Keys
|
||||
@ -43,6 +44,10 @@ export interface AppState {
|
||||
}
|
||||
|
||||
export interface AppStateActions {
|
||||
// Snackbar
|
||||
setSnack: SetSnackType;
|
||||
|
||||
//
|
||||
setSelectedCandidate: (candidate: Types.Candidate | null) => void;
|
||||
setSelectedJob: (job: Types.Job | null) => void;
|
||||
setSelectedEmployer: (employer: Types.Employer | null) => void;
|
||||
@ -358,7 +363,12 @@ export function useAppStateLogic(): AppStateContextType {
|
||||
console.log('Cleared all route state');
|
||||
}, []);
|
||||
|
||||
const emptySetSnack: SetSnackType = (message: string, severity?: SeverityType) => {
|
||||
return;
|
||||
};
|
||||
|
||||
return {
|
||||
setSnack: emptySetSnack,
|
||||
selectedCandidate,
|
||||
selectedJob,
|
||||
selectedEmployer,
|
||||
@ -385,10 +395,17 @@ const AppStateContext = createContext<AppStateContextType | null>(null);
|
||||
|
||||
export function AppStateProvider({ children }: { children: React.ReactNode }) {
|
||||
const appState = useAppStateLogic();
|
||||
const snackRef = useRef<any>(null);
|
||||
|
||||
// Global UI components
|
||||
appState.setSnack = useCallback((message: string, severity?: SeverityType) => {
|
||||
snackRef.current?.setSnack(message, severity);
|
||||
}, [snackRef]);
|
||||
|
||||
return (
|
||||
<AppStateContext.Provider value={appState}>
|
||||
{children}
|
||||
<Snack ref={snackRef} />
|
||||
</AppStateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import { Message } from 'components/Message';
|
||||
import { DeleteConfirmation } from 'components/DeleteConfirmation';
|
||||
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSelectedCandidate } from 'hooks/GlobalContext';
|
||||
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
|
||||
import PropagateLoader from 'react-spinners/PropagateLoader';
|
||||
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
||||
import { BackstoryQuery } from 'components/BackstoryQuery';
|
||||
@ -37,10 +37,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
|
||||
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null);
|
||||
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
|
||||
|
||||
const {
|
||||
setSnack,
|
||||
submitQuery,
|
||||
} = props;
|
||||
const { setSnack } = useAppState();
|
||||
|
||||
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
@ -236,15 +233,15 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
|
||||
},
|
||||
},
|
||||
}}>
|
||||
{messages.length === 0 && <Message {...{ chatSession, message: welcomeMessage, setSnack, submitQuery }} />}
|
||||
{messages.length === 0 && <Message {...{ chatSession, message: welcomeMessage, }} />}
|
||||
{messages.map((message: ChatMessage) => (
|
||||
<Message key={message.id} {...{ chatSession, message, setSnack, submitQuery }} />
|
||||
<Message key={message.id} {...{ chatSession, message }} />
|
||||
))}
|
||||
{processingMessage !== null && (
|
||||
<Message {...{ chatSession, message: processingMessage, setSnack, submitQuery }} />
|
||||
<Message {...{ chatSession, message: processingMessage }} />
|
||||
)}
|
||||
{streamingMessage !== null && (
|
||||
<Message {...{ chatSession, message: streamingMessage, setSnack, submitQuery }} />
|
||||
<Message {...{ chatSession, message: streamingMessage }} />
|
||||
)}
|
||||
{streaming && (
|
||||
<Box sx={{
|
||||
|
@ -16,6 +16,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
|
||||
import { connectionBase } from '../utils/Global';
|
||||
import { BackstoryPageProps } from '../components/BackstoryTab';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
|
||||
interface ServerTunables {
|
||||
system_prompt: string,
|
||||
@ -85,7 +86,7 @@ const SystemInfoComponent: React.FC<{ systemInfo: SystemInfo | undefined }> = ({
|
||||
};
|
||||
|
||||
const ControlsPage = (props: BackstoryPageProps) => {
|
||||
const { setSnack } = props;
|
||||
const { setSnack } = useAppState();
|
||||
const [editSystemPrompt, setEditSystemPrompt] = useState<string>("");
|
||||
const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined);
|
||||
const [tools, setTools] = useState<Tool[]>([]);
|
||||
|
@ -39,6 +39,7 @@ import { BackstoryAppAnalysisPage } from 'documents/BackstoryAppAnalysisPage';
|
||||
import { BackstoryThemeVisualizerPage } from 'documents/BackstoryThemeVisualizerPage';
|
||||
import { UserManagement } from 'documents/UserManagement';
|
||||
import { MockupPage } from 'documents/MockupPage';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
|
||||
// Sidebar navigation component using MUI components
|
||||
const Sidebar: React.FC<{
|
||||
@ -170,7 +171,7 @@ const documentTitleFromRoute = (route: string): string => {
|
||||
}
|
||||
|
||||
const DocsPage = (props: BackstoryPageProps) => {
|
||||
const { submitQuery, setSnack } = props;
|
||||
const { setSnack } = useAppState();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { paramPage = '' } = useParams();
|
||||
@ -245,8 +246,6 @@ const DocsPage = (props: BackstoryPageProps) => {
|
||||
</Box>
|
||||
{page && <Document
|
||||
filepath={`/docs/${page}.md`}
|
||||
submitQuery={submitQuery}
|
||||
setSnack={setSnack}
|
||||
/>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
@ -20,6 +20,7 @@ import { StreamingResponse } from 'services/api-client';
|
||||
import { ChatMessage, ChatMessageUser, ChatSession, CandidateAI, ChatMessageStatus, ChatMessageError } from 'types/types';
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { Message } from 'components/Message';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
|
||||
const defaultMessage: ChatMessage = {
|
||||
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "user"
|
||||
@ -27,7 +28,7 @@ const defaultMessage: ChatMessage = {
|
||||
|
||||
const GenerateCandidate = (props: BackstoryElementProps) => {
|
||||
const { apiClient, user } = useAuth();
|
||||
const { setSnack, submitQuery } = props;
|
||||
const { setSnack } = useAppState();
|
||||
const [processingMessage, setProcessingMessage] = useState<ChatMessage | null>(null);
|
||||
const [processing, setProcessing] = useState<boolean>(false);
|
||||
const [generatedUser, setGeneratedUser] = useState<CandidateAI | null>(null);
|
||||
@ -220,7 +221,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
||||
justifyContent: "center",
|
||||
m: 2,
|
||||
}}>
|
||||
{processingMessage && chatSession && <Message message={processingMessage} {...{ chatSession, submitQuery, setSnack }} />}
|
||||
{processingMessage && chatSession && <Message message={processingMessage} {...{ chatSession }} />}
|
||||
<PropagateLoader
|
||||
size="10px"
|
||||
loading={processing}
|
||||
@ -266,7 +267,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
||||
{resume &&
|
||||
<Paper sx={{pt: 1, pb: 1, pl: 2, pr: 2}}>
|
||||
<Scrollable sx={{flexGrow: 1}}>
|
||||
<StyledMarkdown {...{ content: resume, setSnack, submitQuery }} />
|
||||
<StyledMarkdown content={resume} />
|
||||
</Scrollable>
|
||||
</Paper>
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import { Candidate, Job } from "types/types";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
|
||||
import { useAppState, useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
|
||||
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
||||
import { ComingSoon } from 'components/ui/ComingSoon';
|
||||
import { LoginRequired } from 'components/ui/LoginRequired';
|
||||
@ -66,9 +66,8 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
const { user, apiClient } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
|
||||
const { selectedJob, setSelectedJob } = useSelectedJob()
|
||||
const { setSnack, submitQuery } = props;
|
||||
const backstoryProps = { setSnack, submitQuery };
|
||||
const { selectedJob, setSelectedJob } = useSelectedJob();
|
||||
const { setSnack } = useAppState();
|
||||
// State management
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [analysisStarted, setAnalysisStarted] = useState(false);
|
||||
@ -161,7 +160,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
Select a Candidate
|
||||
</Typography>
|
||||
|
||||
<CandidatePicker onSelect={onCandidateSelect} {...backstoryProps} />
|
||||
<CandidatePicker onSelect={onCandidateSelect} />
|
||||
</Paper>
|
||||
);
|
||||
|
||||
@ -184,11 +183,10 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
</Box>
|
||||
|
||||
{jobTab === 'load' &&
|
||||
<JobPicker {...backstoryProps} onSelect={onJobSelect} />
|
||||
<JobPicker onSelect={onJobSelect} />
|
||||
}
|
||||
{jobTab === 'create' &&
|
||||
<JobCreator
|
||||
{...backstoryProps}
|
||||
onSave={onJobSelect}
|
||||
/>}
|
||||
</Box>
|
||||
@ -202,7 +200,6 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
<JobMatchAnalysis
|
||||
job={selectedJob}
|
||||
candidate={selectedCandidate}
|
||||
{...backstoryProps}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
@ -4,19 +4,15 @@ import {
|
||||
Container,
|
||||
Paper,
|
||||
Typography,
|
||||
Grid,
|
||||
Alert,
|
||||
Tabs,
|
||||
Tab,
|
||||
Card,
|
||||
CardContent,
|
||||
Divider,
|
||||
Avatar,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Person,
|
||||
PersonAdd,
|
||||
AccountCircle,
|
||||
} from '@mui/icons-material';
|
||||
import 'react-phone-number-input/style.css';
|
||||
import './LoginPage.css';
|
||||
@ -29,10 +25,11 @@ import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||
import { LoginForm } from "components/EmailVerificationComponents";
|
||||
import { CandidateRegistrationForm } from "components/RegistrationForms";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
|
||||
const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { setSnack } = props;
|
||||
const { setSnack } = useAppState();
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
@ -10,9 +10,10 @@ import { BackstoryQuery } from '../components/BackstoryQuery';
|
||||
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { Candidate } from 'types/types';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
|
||||
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
|
||||
const { setSnack, submitQuery } = props;
|
||||
const { setSnack } = useAppState();
|
||||
const { user } = useAuth();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
@ -28,7 +29,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
|
||||
setQuestions([
|
||||
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
||||
{candidate.questions?.map((q, i: number) =>
|
||||
<BackstoryQuery key={i} question={q} submitQuery={submitQuery} />
|
||||
<BackstoryQuery key={i} question={q} />
|
||||
)}
|
||||
</Box>,
|
||||
<Box sx={{ p: 1 }}>
|
||||
@ -36,7 +37,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
|
||||
{`As with all LLM interactions, the results may not be 100% accurate. Please contact **${candidate.fullName}** if you have any questions.`}
|
||||
</MuiMarkdown>
|
||||
</Box>]);
|
||||
}, [candidate, isMobile, submitQuery]);
|
||||
}, [candidate, isMobile]);
|
||||
|
||||
if (!candidate) {
|
||||
return (<></>);
|
||||
@ -51,9 +52,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
|
||||
type: "chat",
|
||||
placeholder: `What would you like to know about ${candidate?.firstName}?`,
|
||||
resetLabel: "chat",
|
||||
setSnack,
|
||||
defaultPrompts: questions,
|
||||
submitQuery,
|
||||
defaultPrompts: questions,
|
||||
}} />
|
||||
</Box>);
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ import { Conversation } from 'components/Conversation';
|
||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||
import { ChatQuery, ChatMessage } from "types/types";
|
||||
import './ResumeBuilderPage.css';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
|
||||
/**
|
||||
* ResumeBuilder component
|
||||
@ -19,11 +20,6 @@ import './ResumeBuilderPage.css';
|
||||
* with different layouts for mobile and desktop views.
|
||||
*/
|
||||
const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
const {
|
||||
sx,
|
||||
setSnack,
|
||||
submitQuery,
|
||||
} = props
|
||||
// State for editing job description
|
||||
const [hasJobDescription, setHasJobDescription] = useState<boolean>(false);
|
||||
const [hasResume, setHasResume] = useState<boolean>(false);
|
||||
|
@ -46,6 +46,7 @@ import { CandidateProfile } from 'pages/candidate/dashboard/Profile';
|
||||
import { VectorVisualizer } from 'components/VectorVisualizer';
|
||||
import { DocumentManager } from 'components/DocumentManager';
|
||||
import { JobPicker } from 'components/ui/JobPicker';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
|
||||
interface DashboardProps extends BackstoryPageProps {
|
||||
userName?: string;
|
||||
@ -58,15 +59,14 @@ const CandidateDashboardPage: React.FC<DashboardProps> = (props: DashboardProps)
|
||||
const [activeTab, setActiveTab] = useState<string>(subPage);
|
||||
const { user, isLoading, isInitializing, isAuthenticated } = useAuth();
|
||||
const profileCompletion = 75;
|
||||
const { setSnack, submitQuery } = props;
|
||||
const backstoryProps = { setSnack, submitQuery };
|
||||
const { setSnack } = useAppState();
|
||||
|
||||
const sidebarItems = [
|
||||
{ text: 'Dashboard', icon: <DashboardIcon />,path: '/', element: <CandidateDashboard {...backstoryProps}/> },
|
||||
{ text: 'Profile', icon: <PersonIcon />,path: '/profile', element: <CandidateProfile {...backstoryProps}/> },
|
||||
{ text: 'Jobs', icon: <WorkIcon />,path: '/jobs', element: <JobPicker {...backstoryProps}/> },
|
||||
{ text: 'Dashboard', icon: <DashboardIcon />, path: '/', element: <CandidateDashboard /> },
|
||||
{ text: 'Profile', icon: <PersonIcon />, path: '/profile', element: <CandidateProfile /> },
|
||||
{ text: 'Jobs', icon: <WorkIcon />, path: '/jobs', element: <JobPicker /> },
|
||||
{ text: 'Resumes', icon: <DescriptionIcon />,path: '/resumes', element: <BetaPage><Box>Candidate resumes page</Box></BetaPage> },
|
||||
{ text: 'Content', icon: <BubbleChart />, path: '/rag', element: <Box sx={{display: "flex", width: "100%", flexDirection: "column"}}><VectorVisualizer {...backstoryProps} /><DocumentManager {...backstoryProps} /></Box>},
|
||||
{ text: 'Content', icon: <BubbleChart />, path: '/rag', element: <Box sx={{ display: "flex", width: "100%", flexDirection: "column" }}><VectorVisualizer /><DocumentManager /></Box> },
|
||||
{ text: 'Q&A Setup', icon: <QuizIcon />,path: '/q-a-setup', element: <BetaPage><Box>Candidate q&a setup page</Box></BetaPage> },
|
||||
{ text: 'Analytics', icon: <AnalyticsIcon />,path: '/analytics', element: <BetaPage><Box>Candidate analytics page</Box></BetaPage> },
|
||||
{ text: 'Settings', icon: <SettingsIcon />,path: '/settings', element: <BetaPage><Box>Candidate settings page</Box></BetaPage> },
|
||||
|
@ -21,19 +21,19 @@ import { LoginRequired } from 'pages/LoginRequired';
|
||||
import { BackstoryElementProps } from 'components/BackstoryTab';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ComingSoon } from 'components/ui/ComingSoon';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
|
||||
interface CandidateDashboardProps extends BackstoryElementProps {
|
||||
};
|
||||
|
||||
const CandidateDashboard = (props: CandidateDashboardProps) => {
|
||||
const { setSnack, submitQuery } = props;
|
||||
const backstoryProps = { setSnack, submitQuery };
|
||||
const { setSnack } = useAppState();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const profileCompletion = 75;
|
||||
|
||||
if (!user) {
|
||||
return <LoginRequired {...backstoryProps}/>;
|
||||
return <LoginRequired />;
|
||||
}
|
||||
|
||||
if (user?.userType !== 'candidate') {
|
||||
|
@ -50,6 +50,7 @@ import { useAuth } from "hooks/AuthContext";
|
||||
import * as Types from 'types/types';
|
||||
import { ComingSoon } from 'components/ui/ComingSoon';
|
||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
|
||||
// Styled components
|
||||
const VisuallyHiddenInput = styled('input')({
|
||||
@ -95,7 +96,7 @@ function TabPanel(props: TabPanelProps) {
|
||||
}
|
||||
|
||||
const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
const { setSnack } = props;
|
||||
const { setSnack } = useAppState();
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const { user, updateUserData, apiClient } = useAuth();
|
||||
|
18
frontend/src/types/navigation.ts
Normal file
18
frontend/src/types/navigation.ts
Normal file
@ -0,0 +1,18 @@
|
||||
// types/navigation.ts
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
export interface NavigationItem {
|
||||
id: string;
|
||||
label: string | ReactElement;
|
||||
path?: string;
|
||||
icon?: ReactElement;
|
||||
children?: NavigationItem[];
|
||||
component?: ReactElement;
|
||||
userTypes?: ('candidate' | 'employer' | 'guest')[];
|
||||
exact?: boolean;
|
||||
divider?: boolean;
|
||||
}
|
||||
|
||||
export interface NavigationConfig {
|
||||
items: NavigationItem[];
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user