New navigation system
This commit is contained in:
parent
18863a23d9
commit
d41f9a9e75
@ -5,8 +5,8 @@ import { ChatSubmitQueryInterface } from './BackstoryQuery';
|
|||||||
import { SetSnackType } from './Snack';
|
import { SetSnackType } from './Snack';
|
||||||
|
|
||||||
interface BackstoryElementProps {
|
interface BackstoryElementProps {
|
||||||
setSnack: SetSnackType,
|
// setSnack: SetSnackType,
|
||||||
submitQuery: ChatSubmitQueryInterface,
|
// submitQuery: ChatSubmitQueryInterface,
|
||||||
sx?: SxProps<Theme>,
|
sx?: SxProps<Theme>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import { ChatMessage, ChatContext, ChatSession, ChatQuery, ChatMessageUser, Chat
|
|||||||
import { PaginatedResponse } from 'types/conversion';
|
import { PaginatedResponse } from 'types/conversion';
|
||||||
|
|
||||||
import './Conversation.css';
|
import './Conversation.css';
|
||||||
import { useSelectedCandidate } from 'hooks/GlobalContext';
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
const defaultMessage: ChatMessage = {
|
const defaultMessage: ChatMessage = {
|
||||||
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "assistant"
|
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "assistant"
|
||||||
@ -65,8 +65,6 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
preamble,
|
preamble,
|
||||||
resetAction,
|
resetAction,
|
||||||
resetLabel,
|
resetLabel,
|
||||||
setSnack,
|
|
||||||
submitQuery,
|
|
||||||
sx,
|
sx,
|
||||||
type,
|
type,
|
||||||
} = props;
|
} = props;
|
||||||
@ -84,6 +82,7 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
const stopRef = useRef(false);
|
const stopRef = useRef(false);
|
||||||
const controllerRef = useRef<StreamingResponse>(null);
|
const controllerRef = useRef<StreamingResponse>(null);
|
||||||
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
||||||
|
const { setSnack } = useAppState();
|
||||||
|
|
||||||
// Keep the ref updated whenever items changes
|
// Keep the ref updated whenever items changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -326,16 +325,16 @@ const Conversation = forwardRef<ConversationHandle, ConversationProps>((props: C
|
|||||||
<Box sx={{ p: 1, mt: 0, ...sx }}>
|
<Box sx={{ p: 1, mt: 0, ...sx }}>
|
||||||
{
|
{
|
||||||
filteredConversation.map((message, index) =>
|
filteredConversation.map((message, index) =>
|
||||||
<Message key={index} {...{ chatSession, sendQuery: processQuery, message, connectionBase, setSnack, submitQuery }} />
|
<Message key={index} {...{ chatSession, sendQuery: processQuery, message, connectionBase, }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
processingMessage !== undefined &&
|
processingMessage !== undefined &&
|
||||||
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, setSnack, message: processingMessage, submitQuery }} />
|
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, message: processingMessage, }} />
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
streamingMessage !== undefined &&
|
streamingMessage !== undefined &&
|
||||||
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, setSnack, message: streamingMessage, submitQuery }} />
|
<Message {...{ chatSession, sendQuery: processQuery, connectionBase, message: streamingMessage }} />
|
||||||
}
|
}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -7,11 +7,7 @@ interface DocumentProps extends BackstoryElementProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Document = (props: DocumentProps) => {
|
const Document = (props: DocumentProps) => {
|
||||||
const { setSnack, submitQuery, filepath } = props;
|
const { filepath } = props;
|
||||||
const backstoryProps = {
|
|
||||||
submitQuery,
|
|
||||||
setSnack,
|
|
||||||
};
|
|
||||||
|
|
||||||
const [document, setDocument] = useState<string>("");
|
const [document, setDocument] = useState<string>("");
|
||||||
|
|
||||||
@ -43,7 +39,7 @@ const Document = (props: DocumentProps) => {
|
|||||||
}, [document, setDocument, filepath])
|
}, [document, setDocument, filepath])
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<StyledMarkdown {...backstoryProps} content={document}/>
|
<StyledMarkdown content={document} />
|
||||||
</>);
|
</>);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ import { useTheme } from '@mui/material/styles';
|
|||||||
import { useAuth } from "hooks/AuthContext";
|
import { useAuth } from "hooks/AuthContext";
|
||||||
import * as Types from 'types/types';
|
import * as Types from 'types/types';
|
||||||
import { BackstoryElementProps } from './BackstoryTab';
|
import { BackstoryElementProps } from './BackstoryTab';
|
||||||
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
const VisuallyHiddenInput = styled('input')({
|
const VisuallyHiddenInput = styled('input')({
|
||||||
clip: 'rect(0 0 0 0)',
|
clip: 'rect(0 0 0 0)',
|
||||||
@ -50,8 +51,8 @@ const VisuallyHiddenInput = styled('input')({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const DocumentManager = (props: BackstoryElementProps) => {
|
const DocumentManager = (props: BackstoryElementProps) => {
|
||||||
const { setSnack, submitQuery } = props;
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { setSnack } = useAppState();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const { user, apiClient } = useAuth();
|
const { user, apiClient } = useAuth();
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { Quote } from 'components/Quote';
|
|||||||
import { BackstoryElementProps } from 'components/BackstoryTab';
|
import { BackstoryElementProps } from 'components/BackstoryTab';
|
||||||
import { Candidate, ChatSession } from 'types/types';
|
import { Candidate, ChatSession } from 'types/types';
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
interface GenerateImageProps extends BackstoryElementProps {
|
interface GenerateImageProps extends BackstoryElementProps {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
@ -13,7 +14,8 @@ interface GenerateImageProps extends BackstoryElementProps {
|
|||||||
|
|
||||||
const GenerateImage = (props: GenerateImageProps) => {
|
const GenerateImage = (props: GenerateImageProps) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { setSnack, chatSession, prompt } = props;
|
const { chatSession, prompt } = props;
|
||||||
|
const { setSnack } = useAppState();
|
||||||
const [processing, setProcessing] = useState<boolean>(false);
|
const [processing, setProcessing] = useState<boolean>(false);
|
||||||
const [status, setStatus] = useState<string>('');
|
const [status, setStatus] = useState<string>('');
|
||||||
const [image, setImage] = 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 FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||||
|
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import { useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
|
import { useAppState, useSelectedCandidate, useSelectedJob } from 'hooks/GlobalContext';
|
||||||
import { BackstoryElementProps } from './BackstoryTab';
|
import { BackstoryElementProps } from './BackstoryTab';
|
||||||
import { LoginRequired } from 'components/ui/LoginRequired';
|
import { LoginRequired } from 'components/ui/LoginRequired';
|
||||||
|
|
||||||
@ -122,8 +122,7 @@ const JobCreator = (props: JobCreator) => {
|
|||||||
const { onSave } = props;
|
const { onSave } = props;
|
||||||
const { selectedCandidate } = useSelectedCandidate();
|
const { selectedCandidate } = useSelectedCandidate();
|
||||||
const { selectedJob, setSelectedJob } = useSelectedJob();
|
const { selectedJob, setSelectedJob } = useSelectedJob();
|
||||||
const { setSnack, submitQuery } = props;
|
const { setSnack } = useAppState();
|
||||||
const backstoryProps = { setSnack, submitQuery };
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down('md'));
|
const isTablet = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
@ -31,6 +31,7 @@ import { StyledMarkdown } from './StyledMarkdown';
|
|||||||
import { Scrollable } from './Scrollable';
|
import { Scrollable } from './Scrollable';
|
||||||
import { start } from 'repl';
|
import { start } from 'repl';
|
||||||
import { TypesElement } from '@uiw/react-json-view';
|
import { TypesElement } from '@uiw/react-json-view';
|
||||||
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
interface JobAnalysisProps extends BackstoryPageProps {
|
interface JobAnalysisProps extends BackstoryPageProps {
|
||||||
job: Job;
|
job: Job;
|
||||||
@ -45,11 +46,9 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
|||||||
const {
|
const {
|
||||||
job,
|
job,
|
||||||
candidate,
|
candidate,
|
||||||
setSnack,
|
|
||||||
submitQuery
|
|
||||||
} = props
|
} = props
|
||||||
const { apiClient } = useAuth();
|
const { apiClient } = useAuth();
|
||||||
const backstoryProps = { setSnack, submitQuery };
|
const { setSnack } = useAppState();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [requirements, setRequirements] = useState<{ requirement: string, domain: string }[]>([]);
|
const [requirements, setRequirements] = useState<{ requirement: string, domain: string }[]>([]);
|
||||||
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
|
const [skillMatches, setSkillMatches] = useState<SkillMatch[]>([]);
|
||||||
@ -250,7 +249,7 @@ const JobMatchAnalysis: React.FC<JobAnalysisProps> = (props: JobAnalysisProps) =
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Paper sx={{ p: 2, maxHeight: "22rem" }}>
|
<Paper sx={{ p: 2, maxHeight: "22rem" }}>
|
||||||
<Scrollable sx={{ display: "flex", maxHeight: "100%" }}>
|
<Scrollable sx={{ display: "flex", maxHeight: "100%" }}>
|
||||||
<StyledMarkdown content={job.description} {...backstoryProps} />
|
<StyledMarkdown content={job.description} />
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -358,12 +358,8 @@ const MessageContainer = (props: MessageContainerProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Message = (props: MessageProps) => {
|
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 [metaExpanded, setMetaExpanded] = useState<boolean>(false);
|
||||||
const backstoryProps = {
|
|
||||||
submitQuery,
|
|
||||||
setSnack
|
|
||||||
};
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const type: ApiActivityType | ChatSenderType | "error" = ('activity' in message) ? message.activity : ('error' in message) ? 'error' : (message as ChatMessage).role;
|
const type: ApiActivityType | ChatSenderType | "error" = ('activity' in message) ? message.activity : ('error' in message) ? 'error' : (message as ChatMessage).role;
|
||||||
const style: any = getStyle(theme, type);
|
const style: any = getStyle(theme, type);
|
||||||
@ -385,7 +381,7 @@ const Message = (props: MessageProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const messageView = (
|
const messageView = (
|
||||||
<StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={content} {...backstoryProps} />
|
<StyledMarkdown chatSession={chatSession} streaming={message.status === "streaming"} content={content} />
|
||||||
);
|
);
|
||||||
|
|
||||||
let metadataView = (<></>);
|
let metadataView = (<></>);
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { MuiMarkdown } from 'mui-markdown';
|
import { MuiMarkdown } from 'mui-markdown';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { Link } from '@mui/material';
|
import { Link } from '@mui/material';
|
||||||
import { BackstoryQuery } from 'components/BackstoryQuery';
|
import { BackstoryQuery, BackstoryQueryInterface } from 'components/BackstoryQuery';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import JsonView from '@uiw/react-json-view';
|
import JsonView from '@uiw/react-json-view';
|
||||||
import { vscodeTheme } from '@uiw/react-json-view/vscode';
|
import { vscodeTheme } from '@uiw/react-json-view/vscode';
|
||||||
@ -13,17 +13,19 @@ import { GenerateImage } from 'components/GenerateImage';
|
|||||||
|
|
||||||
import './StyledMarkdown.css';
|
import './StyledMarkdown.css';
|
||||||
import { BackstoryElementProps } from './BackstoryTab';
|
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 {
|
interface StyledMarkdownProps extends BackstoryElementProps {
|
||||||
className?: string,
|
className?: string,
|
||||||
content: string,
|
content: string,
|
||||||
streaming?: boolean,
|
streaming?: boolean,
|
||||||
chatSession?: ChatSession,
|
chatSession?: ChatSession,
|
||||||
|
submitQuery?: ChatSubmitQueryInterface
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProps) => {
|
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 theme = useTheme();
|
||||||
|
|
||||||
const overrides: any = {
|
const overrides: any = {
|
||||||
@ -110,7 +112,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
|
|||||||
question: queryString
|
question: queryString
|
||||||
}
|
}
|
||||||
|
|
||||||
return <BackstoryQuery submitQuery={submitQuery} question={query} />
|
return submitQuery ? <BackstoryQuery submitQuery={submitQuery} question={query} /> : query.question;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("StyledMarkdown error:", queryString, e);
|
console.log("StyledMarkdown error:", queryString, e);
|
||||||
return props.query;
|
return props.query;
|
||||||
@ -124,7 +126,7 @@ const StyledMarkdown: React.FC<StyledMarkdownProps> = (props: StyledMarkdownProp
|
|||||||
component: (props: { prompt: string }) => {
|
component: (props: { prompt: string }) => {
|
||||||
const prompt = props.prompt.replace(/(\w+):/g, '"$1":');
|
const prompt = props.prompt.replace(/(\w+):/g, '"$1":');
|
||||||
try {
|
try {
|
||||||
return <GenerateImage {...{ chatSession, prompt, submitQuery, setSnack }} />
|
return <GenerateImage {...{ chatSession, prompt }} />
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("StyledMarkdown error:", prompt, e);
|
console.log("StyledMarkdown error:", prompt, e);
|
||||||
return props.prompt;
|
return props.prompt;
|
||||||
|
@ -23,7 +23,7 @@ import './VectorVisualizer.css';
|
|||||||
import { BackstoryPageProps } from './BackstoryTab';
|
import { BackstoryPageProps } from './BackstoryTab';
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import * as Types from 'types/types';
|
import * as Types from 'types/types';
|
||||||
import { useSelectedCandidate } from 'hooks/GlobalContext';
|
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface VectorVisualizerProps extends BackstoryPageProps {
|
interface VectorVisualizerProps extends BackstoryPageProps {
|
||||||
@ -180,8 +180,8 @@ type Node = {
|
|||||||
|
|
||||||
const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
|
const VectorVisualizer: React.FC<VectorVisualizerProps> = (props: VectorVisualizerProps) => {
|
||||||
const { user, apiClient } = useAuth();
|
const { user, apiClient } = useAuth();
|
||||||
const { setSnack, submitQuery, rag, inline, sx } = props;
|
const { rag, inline, sx } = props;
|
||||||
const backstoryProps = { setSnack, submitQuery };
|
const { setSnack } = useAppState();
|
||||||
const [plotData, setPlotData] = useState<PlotData | null>(null);
|
const [plotData, setPlotData] = useState<PlotData | null>(null);
|
||||||
const [newQuery, setNewQuery] = useState<string>('');
|
const [newQuery, setNewQuery] = useState<string>('');
|
||||||
const [querySet, setQuerySet] = useState<Types.ChromaDBGetResponse>(rag || emptyQuerySet);
|
const [querySet, setQuerySet] = useState<Types.ChromaDBGetResponse>(rag || emptyQuerySet);
|
||||||
|
@ -1,114 +1,57 @@
|
|||||||
|
// components/layout/BackstoryLayout.tsx
|
||||||
import React, { ReactElement, useEffect, useState } from 'react';
|
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 { Box, Container, Paper } from '@mui/material';
|
||||||
import { useNavigate } from "react-router-dom";
|
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 { SxProps, Theme } from '@mui/material';
|
||||||
|
|
||||||
|
|
||||||
import { Header } from 'components/layout/Header';
|
import { Header } from 'components/layout/Header';
|
||||||
import { Scrollable } from 'components/Scrollable';
|
import { Scrollable } from 'components/Scrollable';
|
||||||
import { Footer } from 'components/layout/Footer';
|
import { Footer } from 'components/layout/Footer';
|
||||||
import { Snack, SetSnackType } from 'components/Snack';
|
import { Snack, SetSnackType } from 'components/Snack';
|
||||||
import { User } from 'types/types';
|
import { User } from 'types/types';
|
||||||
import { getBackstoryDynamicRoutes } from 'components/layout/BackstoryRoutes';
|
|
||||||
import { LoadingComponent } from "components/LoadingComponent";
|
import { LoadingComponent } from "components/LoadingComponent";
|
||||||
import { AuthProvider, useAuth, ProtectedRoute } from 'hooks/AuthContext';
|
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;
|
label: ReactElement<any> | string;
|
||||||
path: string;
|
path: string;
|
||||||
icon?: ReactElement<any>;
|
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 {
|
interface BackstoryPageContainerProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>;
|
||||||
};
|
}
|
||||||
|
|
||||||
const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
|
const BackstoryPageContainer = (props: BackstoryPageContainerProps) => {
|
||||||
const { children, sx } = props;
|
const { children, sx } = props;
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
className="BackstoryPageContainer"
|
className="BackstoryPageContainer"
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row", // Must be row; if column, the box will expand for all children
|
flexDirection: "row",
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
p: "0 !important", // Let the first box use padding to offset main content
|
p: "0 !important",
|
||||||
m: "0 auto !important",
|
m: "0 auto !important",
|
||||||
maxWidth: '1024px', //{ xs: '100%', md: '700px', lg: '1024px' },
|
maxWidth: '1024px',
|
||||||
height: "100%", // Restrict to main-container's height
|
height: "100%",
|
||||||
minHeight: 0,//"min-content", // Prevent flex overflow
|
minHeight: 0,
|
||||||
...sx
|
...sx
|
||||||
}}>
|
}}>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: "flex", p: { xs: 0, sm: 0.5 }, flexGrow: 1, minHeight: "min-content", // Prevent flex overflow
|
display: "flex",
|
||||||
|
p: { xs: 0, sm: 0.5 },
|
||||||
|
flexGrow: 1,
|
||||||
|
minHeight: "min-content",
|
||||||
}}>
|
}}>
|
||||||
<Paper
|
<Paper
|
||||||
elevation={2}
|
elevation={2}
|
||||||
@ -117,7 +60,7 @@ const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
|
|||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
m: 0,
|
m: 0,
|
||||||
p: 0.5,
|
p: 0.5,
|
||||||
minHeight: "min-content", // Prevent flex overflow
|
minHeight: "min-content",
|
||||||
backgroundColor: 'background.paper',
|
backgroundColor: 'background.paper',
|
||||||
borderRadius: 0.5,
|
borderRadius: 0.5,
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
@ -128,41 +71,66 @@ const BackstoryPageContainer = (props : BackstoryPageContainerProps) => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
interface BackstoryLayoutProps {
|
|
||||||
setSnack: SetSnackType;
|
|
||||||
page: string;
|
|
||||||
chatRef: React.Ref<any>;
|
|
||||||
snackRef: React.Ref<any>;
|
|
||||||
submitQuery: any;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface BackstoryLayoutProps {
|
||||||
|
page: string;
|
||||||
|
chatRef: React.Ref<any>;
|
||||||
|
}
|
||||||
|
|
||||||
const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutProps) => {
|
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 navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { guest, user } = useAuth();
|
const { guest, user } = useAuth();
|
||||||
const { selectedCandidate } = useSelectedCandidate();
|
const { selectedCandidate } = useSelectedCandidate();
|
||||||
const [navigationLinks, setNavigationLinks] = useState<NavigationLinkType[]>([]);
|
const [navigationItems, setNavigationItems] = useState<NavigationItem[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNavigationLinks(getNavigationLinks(user));
|
const userType = user?.userType || null;
|
||||||
|
setNavigationItems(getMainNavigationItems(userType));
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
let dynamicRoutes;
|
// Generate dynamic routes from navigation config
|
||||||
if (guest) {
|
const generateRoutes = () => {
|
||||||
dynamicRoutes = getBackstoryDynamicRoutes({
|
if (!guest) return null;
|
||||||
user,
|
|
||||||
setSnack,
|
const userType = user?.userType || null;
|
||||||
submitQuery,
|
const routes = getAllRoutes(userType);
|
||||||
chatRef
|
|
||||||
|
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 (
|
return (
|
||||||
<Box sx={{ height: "100%", maxHeight: "100%", minHeight: "100%", flexDirection: "column" }}>
|
<Route
|
||||||
<Header {...{ setSnack, currentPath: page, navigate, navigationLinks }} />
|
key={`${route.id}-${index}`}
|
||||||
|
path={route.path}
|
||||||
|
element={componentWithProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}).filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{
|
||||||
|
height: "100%",
|
||||||
|
maxHeight: "100%",
|
||||||
|
minHeight: "100%",
|
||||||
|
flexDirection: "column"
|
||||||
|
}}>
|
||||||
|
<Header
|
||||||
|
setSnack={setSnack}
|
||||||
|
currentPath={page}
|
||||||
|
navigate={navigate}
|
||||||
|
navigationItems={navigationItems}
|
||||||
|
/>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -179,7 +147,7 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
|
|||||||
sx={{
|
sx={{
|
||||||
m: 0,
|
m: 0,
|
||||||
p: 0,
|
p: 0,
|
||||||
mt: "72px", /* Needs to be kept in sync with the height of Header if the Header theme changes */
|
mt: "72px", /* Needs to be kept in sync with the height of Header */
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
backgroundColor: "background.default",
|
backgroundColor: "background.default",
|
||||||
@ -190,33 +158,32 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BackstoryPageContainer>
|
<BackstoryPageContainer>
|
||||||
{!guest &&
|
{!guest && (
|
||||||
<Box>
|
<Box>
|
||||||
<LoadingComponent
|
<LoadingComponent
|
||||||
loadingText="Creating session..."
|
loadingText="Creating session..."
|
||||||
loaderType="linear"
|
loaderType="linear"
|
||||||
withFade={true}
|
withFade={true}
|
||||||
fadeDuration={1200} />
|
fadeDuration={1200}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
)}
|
||||||
{guest && <>
|
{guest && (
|
||||||
|
<>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
{dynamicRoutes !== undefined && <Routes>{dynamicRoutes}</Routes>}
|
<Routes>
|
||||||
|
{generateRoutes()}
|
||||||
|
</Routes>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{location.pathname === "/" && <Footer />}
|
{location.pathname === "/" && <Footer />}
|
||||||
</BackstoryPageContainer>
|
</BackstoryPageContainer>
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
<Snack ref={snackRef} />
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type {
|
|
||||||
NavigationLinkType
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
BackstoryLayout
|
BackstoryLayout
|
||||||
};
|
};
|
@ -1,95 +1,45 @@
|
|||||||
|
// components/layout/BackstoryRoutes.tsx
|
||||||
import React, { Ref, ReactNode } from "react";
|
import React, { Ref, ReactNode } from "react";
|
||||||
import { Route } from "react-router-dom";
|
import { Route } from "react-router-dom";
|
||||||
import { Typography } from '@mui/material';
|
|
||||||
|
|
||||||
import { BackstoryPageProps } from '../BackstoryTab';
|
import { BackstoryPageProps } from '../BackstoryTab';
|
||||||
import { ConversationHandle } from '../Conversation';
|
import { ConversationHandle } from '../Conversation';
|
||||||
import { User } from 'types/types';
|
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 {
|
interface BackstoryDynamicRoutesProps extends BackstoryPageProps {
|
||||||
chatRef: Ref<ConversationHandle>;
|
chatRef: Ref<ConversationHandle>;
|
||||||
user?: User | null;
|
user?: User | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNode => {
|
const getBackstoryDynamicRoutes = (props: BackstoryDynamicRoutesProps): ReactNode => {
|
||||||
const { user, setSnack, submitQuery, chatRef } = props;
|
const { user, chatRef } = props;
|
||||||
const backstoryProps = {
|
const userType = user?.userType || null;
|
||||||
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} />} />,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!user) {
|
// Get all routes from navigation config
|
||||||
routes.push(<Route key={`${index++}`} path="/register" element={(<BetaPage><CreateProfilePage /></BetaPage>)} />);
|
const routes = getAllRoutes(userType);
|
||||||
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 />} />);
|
|
||||||
|
|
||||||
if (user.userType === 'candidate') {
|
return routes.map((route: NavigationItem, index: number) => {
|
||||||
routes.splice(-1, 0, ...[
|
if (!route.path || !route.component) return null;
|
||||||
<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 />} />,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.userType === 'employer') {
|
// Clone the component and pass necessary props
|
||||||
routes.splice(-1, 0, ...[
|
const componentWithProps = React.cloneElement(route.component as React.ReactElement, {
|
||||||
<Route key={`${index++}`} path="/search" element={<SearchPage />} />,
|
// Special handling for chat component ref
|
||||||
<Route key={`${index++}`} path="/saved" element={<SavedPage />} />,
|
...(route.id === 'chat' && { ref: chatRef }),
|
||||||
<Route key={`${index++}`} path="/jobs" element={<JobsPage />} />,
|
// Preserve any existing props
|
||||||
<Route key={`${index++}`} path="/company" element={<CompanyPage />} />,
|
...(route.component.props || {}),
|
||||||
]);
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
routes.push(<Route key={`${index++}`} path="*" element={<BetaPage />} />);
|
return (
|
||||||
|
<Route
|
||||||
return routes;
|
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 { NavigateFunction, useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
AppBar,
|
AppBar,
|
||||||
@ -17,6 +18,14 @@ import {
|
|||||||
Fade,
|
Fade,
|
||||||
Popover,
|
Popover,
|
||||||
Paper,
|
Paper,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Collapse,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { styled, useTheme } from '@mui/material/styles';
|
import { styled, useTheme } from '@mui/material/styles';
|
||||||
import {
|
import {
|
||||||
@ -26,14 +35,15 @@ import {
|
|||||||
Logout,
|
Logout,
|
||||||
Settings,
|
Settings,
|
||||||
ExpandMore,
|
ExpandMore,
|
||||||
|
ExpandLess,
|
||||||
|
KeyboardArrowDown,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
|
||||||
import { NavigationLinkType } from 'components/layout/BackstoryLayout';
|
import { NavigationItem } from 'types/navigation';
|
||||||
import { Beta } from 'components/ui/Beta';
|
import { Beta } from 'components/ui/Beta';
|
||||||
import { Candidate, Employer } from 'types/types';
|
import { Candidate, Employer } from 'types/types';
|
||||||
import { SetSnackType } from 'components/Snack';
|
import { SetSnackType } from 'components/Snack';
|
||||||
import { CopyBubble } from 'components/CopyBubble';
|
import { CopyBubble } from 'components/CopyBubble';
|
||||||
import { BackstoryLogo } from 'components/ui/BackstoryLogo';
|
|
||||||
|
|
||||||
import 'components/layout/Header.css';
|
import 'components/layout/Header.css';
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
@ -79,40 +89,18 @@ const UserButton = styled(Button)(({ theme }) => ({
|
|||||||
|
|
||||||
const MobileDrawer = styled(Drawer)(({ theme }) => ({
|
const MobileDrawer = styled(Drawer)(({ theme }) => ({
|
||||||
'& .MuiDrawer-paper': {
|
'& .MuiDrawer-paper': {
|
||||||
width: 280,
|
width: 320,
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const MobileMenuTabs = styled(Tabs)(({ theme }) => ({
|
const DropdownButton = styled(Button)(({ theme }) => ({
|
||||||
'& .MuiTabs-flexContainer': {
|
color: theme.palette.primary.contrastText,
|
||||||
flexDirection: 'column',
|
textTransform: 'none',
|
||||||
},
|
|
||||||
'& .MuiTab-root': {
|
|
||||||
minHeight: 48,
|
minHeight: 48,
|
||||||
textTransform: 'uppercase',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: theme.spacing(1, 2),
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.action.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 UserMenuContainer = styled(Paper)(({ theme }) => ({
|
const UserMenuContainer = styled(Paper)(({ theme }) => ({
|
||||||
@ -123,51 +111,11 @@ const UserMenuContainer = styled(Paper)(({ theme }) => ({
|
|||||||
minWidth: 200,
|
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 {
|
interface HeaderProps {
|
||||||
transparent?: boolean;
|
transparent?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
navigate: NavigateFunction;
|
navigate: NavigateFunction;
|
||||||
navigationLinks: NavigationLinkType[];
|
navigationItems: NavigationItem[];
|
||||||
currentPath: string;
|
currentPath: string;
|
||||||
sessionId?: string | null;
|
sessionId?: string | null;
|
||||||
setSnack: SetSnackType;
|
setSnack: SetSnackType;
|
||||||
@ -181,7 +129,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
transparent = false,
|
transparent = false,
|
||||||
className,
|
className,
|
||||||
navigate,
|
navigate,
|
||||||
navigationLinks,
|
navigationItems,
|
||||||
sessionId,
|
sessionId,
|
||||||
setSnack,
|
setSnack,
|
||||||
} = props;
|
} = props;
|
||||||
@ -190,42 +138,17 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
|
|
||||||
const name = (user?.firstName || user?.email || '');
|
const name = (user?.firstName || user?.email || '');
|
||||||
|
|
||||||
const mainNavSections: NavigationLinkType[] = [
|
// State for desktop dropdown menus
|
||||||
{ path: '/', label: <BackstoryLogo /> },
|
const [dropdownAnchors, setDropdownAnchors] = useState<{ [key: string]: HTMLElement | null }>({});
|
||||||
...navigationLinks
|
|
||||||
];
|
|
||||||
|
|
||||||
// State for page navigation - only for main navigation
|
|
||||||
const [currentTab, setCurrentTab] = useState<string | false>("/");
|
|
||||||
const [userMenuTab, setUserMenuTab] = useState<string>("");
|
|
||||||
|
|
||||||
// State for mobile drawer
|
// State for mobile drawer
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
const [mobileExpanded, setMobileExpanded] = useState<{ [key: string]: boolean }>({});
|
||||||
|
|
||||||
// State for user menu
|
// State for user menu
|
||||||
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
|
const [userMenuAnchor, setUserMenuAnchor] = useState<null | HTMLElement>(null);
|
||||||
const userMenuOpen = Boolean(userMenuAnchor);
|
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
|
// User menu items
|
||||||
const userMenuItems = [
|
const userMenuItems = [
|
||||||
{
|
{
|
||||||
@ -263,14 +186,38 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
// Helper function to check if current path matches navigation item
|
||||||
const mainSection = getTailSection(location.pathname);
|
const isCurrentPath = (item: NavigationItem): boolean => {
|
||||||
|
if (!item.path) return false;
|
||||||
// Only update if the section is different from current tab
|
if (item.exact) {
|
||||||
if (mainSection !== currentTab) {
|
return location.pathname === item.path;
|
||||||
setCurrentTab(mainSection); // mainSection is either a string or false
|
|
||||||
}
|
}
|
||||||
}, [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>) => {
|
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setUserMenuAnchor(event.currentTarget);
|
setUserMenuAnchor(event.currentTarget);
|
||||||
@ -278,7 +225,6 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
|
|
||||||
const handleUserMenuClose = () => {
|
const handleUserMenuClose = () => {
|
||||||
setUserMenuAnchor(null);
|
setUserMenuAnchor(null);
|
||||||
setUserMenuTab("");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUserMenuAction = (item: typeof userMenuItems[0]) => {
|
const handleUserMenuAction = (item: typeof userMenuItems[0]) => {
|
||||||
@ -288,83 +234,152 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrawerToggle = () => {
|
// Navigation handlers
|
||||||
setMobileOpen(!mobileOpen);
|
const handleNavigate = (path: string) => {
|
||||||
|
navigate(path);
|
||||||
|
setMobileOpen(false);
|
||||||
|
// Close all dropdowns
|
||||||
|
setDropdownAnchors({});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTabChange = (event: React.SyntheticEvent, newValue: string | false) => {
|
// Render desktop navigation with dropdowns
|
||||||
if (newValue !== false) {
|
const renderDesktopNavigation = () => {
|
||||||
setCurrentTab(newValue);
|
|
||||||
navigate(newValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render desktop navigation links
|
|
||||||
const renderNavLinks = () => {
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
value={currentTab}
|
{navigationItems.map((item) => {
|
||||||
onChange={handleTabChange}
|
const hasChildren = item.children && item.children.length > 0;
|
||||||
indicatorColor="secondary"
|
const isActive = isCurrentPath(item) || hasActiveChild(item);
|
||||||
textColor="inherit"
|
|
||||||
variant="fullWidth"
|
if (hasChildren) {
|
||||||
allowScrollButtonsMobile
|
return (
|
||||||
aria-label="Backstory navigation"
|
<Box key={item.id}>
|
||||||
>
|
<DropdownButton
|
||||||
{mainNavSections.map((section) => (
|
onClick={(e) => handleDropdownOpen(e, item.id)}
|
||||||
<Tab
|
endIcon={<KeyboardArrowDown />}
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: section.path === '/' ? "max-content" : "auto",
|
backgroundColor: isActive ? 'action.selected' : 'transparent',
|
||||||
|
color: isActive ? 'secondary.main' : 'primary.contrastText',
|
||||||
}}
|
}}
|
||||||
key={section.path}
|
>
|
||||||
value={section.path}
|
{item.icon && <Box sx={{ mr: 1, display: 'flex' }}>{item.icon}</Box>}
|
||||||
label={section.label}
|
{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>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</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
|
// Render mobile accordion navigation
|
||||||
const renderDrawerContent = () => {
|
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 (
|
return (
|
||||||
<>
|
<Box key={item.id}>
|
||||||
<MobileMenuTabs
|
<ListItem disablePadding sx={{ pl: depth * 2 }}>
|
||||||
orientation="vertical"
|
<ListItemButton
|
||||||
value={currentTab}
|
onClick={() => {
|
||||||
onChange={handleTabChange}
|
if (hasChildren) {
|
||||||
>
|
handleMobileToggle(item.id);
|
||||||
{mainNavSections.map((section) => (
|
} else if (item.path) {
|
||||||
<Tab
|
handleNavigate(item.path);
|
||||||
key={section.path || '/'}
|
}
|
||||||
value={section.path}
|
}}
|
||||||
label={
|
selected={isActive}
|
||||||
<MenuItemBox>
|
sx={{
|
||||||
{section.label}
|
backgroundColor: isActive ? 'action.selected' : 'transparent',
|
||||||
</MenuItemBox>
|
'&.Mui-selected': {
|
||||||
|
backgroundColor: 'primary.main',
|
||||||
|
color: 'primary.contrastText',
|
||||||
|
'& .MuiListItemIcon-root': {
|
||||||
|
color: 'primary.contrastText',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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,
|
||||||
}
|
}
|
||||||
onClick={(e) => {
|
|
||||||
handleDrawerToggle();
|
|
||||||
setCurrentTab(section.path);
|
|
||||||
navigate(section.path);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
{hasChildren && (
|
||||||
</MobileMenuTabs>
|
<IconButton size="small">
|
||||||
<MenuDivider />
|
{isExpanded ? <ExpandLess /> : <ExpandMore />}
|
||||||
{!user && (
|
</IconButton>
|
||||||
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
fullWidth
|
|
||||||
onClick={() => { handleDrawerToggle(); navigate("/login"); }}
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</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 = () => {
|
const renderUserMenu = () => {
|
||||||
return (
|
return (
|
||||||
<UserMenuContainer>
|
<UserMenuContainer>
|
||||||
<UserMenuTabs
|
<List dense>
|
||||||
orientation="vertical"
|
|
||||||
value={userMenuTab}
|
|
||||||
onChange={(e, newValue) => setUserMenuTab(newValue)}
|
|
||||||
>
|
|
||||||
{userMenuItems.map((item, index) => (
|
{userMenuItems.map((item, index) => (
|
||||||
item.id === 'divider' ? (
|
item.id === 'divider' ? (
|
||||||
<MenuDivider key={`divider-${index}`} />
|
<Divider key={`divider-${index}`} />
|
||||||
) : (
|
) : (
|
||||||
<Tab
|
<ListItem key={item.id} disablePadding>
|
||||||
key={item.id}
|
<ListItemButton onClick={() => handleUserMenuAction(item)}>
|
||||||
value={item.id}
|
{item.icon && <ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>}
|
||||||
label={
|
<ListItemText primary={item.label} />
|
||||||
<MenuItemBox>
|
</ListItemButton>
|
||||||
{item.icon}
|
</ListItem>
|
||||||
<Typography variant="body2">{item.label}</Typography>
|
|
||||||
</MenuItemBox>
|
|
||||||
}
|
|
||||||
onClick={() => handleUserMenuAction(item)}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
))}
|
))}
|
||||||
</UserMenuTabs>
|
</List>
|
||||||
</UserMenuContainer>
|
</UserMenuContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -403,11 +409,10 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
const renderUserSection = () => {
|
const renderUserSection = () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Button
|
<Button
|
||||||
color="info"
|
color="info"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => navigate("/login") }
|
onClick={() => navigate("/login")}
|
||||||
sx={{
|
sx={{
|
||||||
display: { xs: 'none', sm: 'block' },
|
display: { xs: 'none', sm: 'block' },
|
||||||
color: theme.palette.primary.contrastText,
|
color: theme.palette.primary.contrastText,
|
||||||
@ -415,7 +420,6 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -472,7 +476,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
<Toolbar disableGutters>
|
<Toolbar disableGutters>
|
||||||
{/* Navigation Links - Desktop */}
|
{/* Navigation Links - Desktop */}
|
||||||
<NavLinksContainer>
|
<NavLinksContainer>
|
||||||
{renderNavLinks()}
|
{renderDesktopNavigation()}
|
||||||
</NavLinksContainer>
|
</NavLinksContainer>
|
||||||
|
|
||||||
{/* User Actions Section */}
|
{/* User Actions Section */}
|
||||||
@ -492,7 +496,8 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{sessionId && <CopyBubble
|
{sessionId && (
|
||||||
|
<CopyBubble
|
||||||
tooltip="Copy link"
|
tooltip="Copy link"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="copy link"
|
aria-label="copy link"
|
||||||
@ -505,9 +510,13 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
'&:hover': { bgcolor: 'action.hover', opacity: 1 },
|
'&:hover': { bgcolor: 'action.hover', opacity: 1 },
|
||||||
}}
|
}}
|
||||||
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
|
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
|
||||||
onClick={() => { navigate(`${window.location.pathname}?id=${sessionId}`); setSnack("Link copied!") }}
|
onClick={() => {
|
||||||
|
navigate(`${window.location.pathname}?id=${sessionId}`);
|
||||||
|
setSnack("Link copied!");
|
||||||
|
}}
|
||||||
size="large"
|
size="large"
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
</UserActionsContainer>
|
</UserActionsContainer>
|
||||||
|
|
||||||
{/* Mobile Navigation Drawer */}
|
{/* Mobile Navigation Drawer */}
|
||||||
@ -520,11 +529,14 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
|
|||||||
keepMounted: true,
|
keepMounted: true,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{renderDrawerContent()}
|
{renderMobileNavigation()}
|
||||||
</MobileDrawer>
|
</MobileDrawer>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</Container>
|
</Container>
|
||||||
<Beta sx={{ left: "-90px", "& .mobile": { right: "-72px" } }} onClick={() => { navigate('/docs/beta'); }} />
|
<Beta
|
||||||
|
sx={{ left: "-90px", "& .mobile": { right: "-72px" } }}
|
||||||
|
onClick={() => { navigate('/docs/beta'); }}
|
||||||
|
/>
|
||||||
</StyledAppBar>
|
</StyledAppBar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@ import { BackstoryElementProps } from 'components/BackstoryTab';
|
|||||||
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
||||||
import { Candidate } from "types/types";
|
import { Candidate } from "types/types";
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import { useSelectedCandidate } from 'hooks/GlobalContext';
|
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
interface CandidatePickerProps extends BackstoryElementProps {
|
interface CandidatePickerProps extends BackstoryElementProps {
|
||||||
onSelect?: (candidate: Candidate) => void
|
onSelect?: (candidate: Candidate) => void
|
||||||
@ -18,7 +18,7 @@ const CandidatePicker = (props: CandidatePickerProps) => {
|
|||||||
const { apiClient, user } = useAuth();
|
const { apiClient, user } = useAuth();
|
||||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
|
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { setSnack } = props;
|
const { setSnack } = useAppState();
|
||||||
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
|
const [candidates, setCandidates] = useState<Candidate[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -7,7 +7,7 @@ import { BackstoryElementProps } from 'components/BackstoryTab';
|
|||||||
import { JobInfo } from 'components/ui/JobInfo';
|
import { JobInfo } from 'components/ui/JobInfo';
|
||||||
import { Job, JobFull } from "types/types";
|
import { Job, JobFull } from "types/types";
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import { useSelectedJob } from 'hooks/GlobalContext';
|
import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
interface JobPickerProps extends BackstoryElementProps {
|
interface JobPickerProps extends BackstoryElementProps {
|
||||||
onSelect?: (job: JobFull) => void
|
onSelect?: (job: JobFull) => void
|
||||||
@ -17,7 +17,7 @@ const JobPicker = (props: JobPickerProps) => {
|
|||||||
const { onSelect } = props;
|
const { onSelect } = props;
|
||||||
const { apiClient } = useAuth();
|
const { apiClient } = useAuth();
|
||||||
const { selectedJob, setSelectedJob } = useSelectedJob();
|
const { selectedJob, setSelectedJob } = useSelectedJob();
|
||||||
const { setSnack } = props;
|
const { setSnack } = useAppState();
|
||||||
const [jobs, setJobs] = useState<JobFull[] | null>(null);
|
const [jobs, setJobs] = useState<JobFull[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
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';
|
import * as Types from 'types/types';
|
||||||
// Assuming you're using React Router
|
// Assuming you're using React Router
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
|
import { SetSnackType, SeverityType, Snack } from 'components/Snack';
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Storage Keys
|
// Storage Keys
|
||||||
@ -43,6 +44,10 @@ export interface AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AppStateActions {
|
export interface AppStateActions {
|
||||||
|
// Snackbar
|
||||||
|
setSnack: SetSnackType;
|
||||||
|
|
||||||
|
//
|
||||||
setSelectedCandidate: (candidate: Types.Candidate | null) => void;
|
setSelectedCandidate: (candidate: Types.Candidate | null) => void;
|
||||||
setSelectedJob: (job: Types.Job | null) => void;
|
setSelectedJob: (job: Types.Job | null) => void;
|
||||||
setSelectedEmployer: (employer: Types.Employer | null) => void;
|
setSelectedEmployer: (employer: Types.Employer | null) => void;
|
||||||
@ -358,7 +363,12 @@ export function useAppStateLogic(): AppStateContextType {
|
|||||||
console.log('Cleared all route state');
|
console.log('Cleared all route state');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const emptySetSnack: SetSnackType = (message: string, severity?: SeverityType) => {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
setSnack: emptySetSnack,
|
||||||
selectedCandidate,
|
selectedCandidate,
|
||||||
selectedJob,
|
selectedJob,
|
||||||
selectedEmployer,
|
selectedEmployer,
|
||||||
@ -385,10 +395,17 @@ const AppStateContext = createContext<AppStateContextType | null>(null);
|
|||||||
|
|
||||||
export function AppStateProvider({ children }: { children: React.ReactNode }) {
|
export function AppStateProvider({ children }: { children: React.ReactNode }) {
|
||||||
const appState = useAppStateLogic();
|
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 (
|
return (
|
||||||
<AppStateContext.Provider value={appState}>
|
<AppStateContext.Provider value={appState}>
|
||||||
{children}
|
{children}
|
||||||
|
<Snack ref={snackRef} />
|
||||||
</AppStateContext.Provider>
|
</AppStateContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import { Message } from 'components/Message';
|
|||||||
import { DeleteConfirmation } from 'components/DeleteConfirmation';
|
import { DeleteConfirmation } from 'components/DeleteConfirmation';
|
||||||
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useSelectedCandidate } from 'hooks/GlobalContext';
|
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
|
||||||
import PropagateLoader from 'react-spinners/PropagateLoader';
|
import PropagateLoader from 'react-spinners/PropagateLoader';
|
||||||
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
import { BackstoryTextField, BackstoryTextFieldRef } from 'components/BackstoryTextField';
|
||||||
import { BackstoryQuery } from 'components/BackstoryQuery';
|
import { BackstoryQuery } from 'components/BackstoryQuery';
|
||||||
@ -37,10 +37,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((pr
|
|||||||
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null);
|
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null);
|
||||||
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
|
const backstoryTextRef = useRef<BackstoryTextFieldRef>(null);
|
||||||
|
|
||||||
const {
|
const { setSnack } = useAppState();
|
||||||
setSnack,
|
|
||||||
submitQuery,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
const [chatSession, setChatSession] = useState<ChatSession | null>(null);
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
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) => (
|
{messages.map((message: ChatMessage) => (
|
||||||
<Message key={message.id} {...{ chatSession, message, setSnack, submitQuery }} />
|
<Message key={message.id} {...{ chatSession, message }} />
|
||||||
))}
|
))}
|
||||||
{processingMessage !== null && (
|
{processingMessage !== null && (
|
||||||
<Message {...{ chatSession, message: processingMessage, setSnack, submitQuery }} />
|
<Message {...{ chatSession, message: processingMessage }} />
|
||||||
)}
|
)}
|
||||||
{streamingMessage !== null && (
|
{streamingMessage !== null && (
|
||||||
<Message {...{ chatSession, message: streamingMessage, setSnack, submitQuery }} />
|
<Message {...{ chatSession, message: streamingMessage }} />
|
||||||
)}
|
)}
|
||||||
{streaming && (
|
{streaming && (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
|
@ -16,6 +16,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|||||||
|
|
||||||
import { connectionBase } from '../utils/Global';
|
import { connectionBase } from '../utils/Global';
|
||||||
import { BackstoryPageProps } from '../components/BackstoryTab';
|
import { BackstoryPageProps } from '../components/BackstoryTab';
|
||||||
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
interface ServerTunables {
|
interface ServerTunables {
|
||||||
system_prompt: string,
|
system_prompt: string,
|
||||||
@ -85,7 +86,7 @@ const SystemInfoComponent: React.FC<{ systemInfo: SystemInfo | undefined }> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ControlsPage = (props: BackstoryPageProps) => {
|
const ControlsPage = (props: BackstoryPageProps) => {
|
||||||
const { setSnack } = props;
|
const { setSnack } = useAppState();
|
||||||
const [editSystemPrompt, setEditSystemPrompt] = useState<string>("");
|
const [editSystemPrompt, setEditSystemPrompt] = useState<string>("");
|
||||||
const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined);
|
const [systemInfo, setSystemInfo] = useState<SystemInfo | undefined>(undefined);
|
||||||
const [tools, setTools] = useState<Tool[]>([]);
|
const [tools, setTools] = useState<Tool[]>([]);
|
||||||
|
@ -39,6 +39,7 @@ import { BackstoryAppAnalysisPage } from 'documents/BackstoryAppAnalysisPage';
|
|||||||
import { BackstoryThemeVisualizerPage } from 'documents/BackstoryThemeVisualizerPage';
|
import { BackstoryThemeVisualizerPage } from 'documents/BackstoryThemeVisualizerPage';
|
||||||
import { UserManagement } from 'documents/UserManagement';
|
import { UserManagement } from 'documents/UserManagement';
|
||||||
import { MockupPage } from 'documents/MockupPage';
|
import { MockupPage } from 'documents/MockupPage';
|
||||||
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
// Sidebar navigation component using MUI components
|
// Sidebar navigation component using MUI components
|
||||||
const Sidebar: React.FC<{
|
const Sidebar: React.FC<{
|
||||||
@ -170,7 +171,7 @@ const documentTitleFromRoute = (route: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DocsPage = (props: BackstoryPageProps) => {
|
const DocsPage = (props: BackstoryPageProps) => {
|
||||||
const { submitQuery, setSnack } = props;
|
const { setSnack } = useAppState();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { paramPage = '' } = useParams();
|
const { paramPage = '' } = useParams();
|
||||||
@ -245,8 +246,6 @@ const DocsPage = (props: BackstoryPageProps) => {
|
|||||||
</Box>
|
</Box>
|
||||||
{page && <Document
|
{page && <Document
|
||||||
filepath={`/docs/${page}.md`}
|
filepath={`/docs/${page}.md`}
|
||||||
submitQuery={submitQuery}
|
|
||||||
setSnack={setSnack}
|
|
||||||
/>}
|
/>}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -20,6 +20,7 @@ import { StreamingResponse } from 'services/api-client';
|
|||||||
import { ChatMessage, ChatMessageUser, ChatSession, CandidateAI, ChatMessageStatus, ChatMessageError } from 'types/types';
|
import { ChatMessage, ChatMessageUser, ChatSession, CandidateAI, ChatMessageStatus, ChatMessageError } from 'types/types';
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import { Message } from 'components/Message';
|
import { Message } from 'components/Message';
|
||||||
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
const defaultMessage: ChatMessage = {
|
const defaultMessage: ChatMessage = {
|
||||||
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "user"
|
status: "done", type: "text", sessionId: "", timestamp: new Date(), content: "", role: "user"
|
||||||
@ -27,7 +28,7 @@ const defaultMessage: ChatMessage = {
|
|||||||
|
|
||||||
const GenerateCandidate = (props: BackstoryElementProps) => {
|
const GenerateCandidate = (props: BackstoryElementProps) => {
|
||||||
const { apiClient, user } = useAuth();
|
const { apiClient, user } = useAuth();
|
||||||
const { setSnack, submitQuery } = props;
|
const { setSnack } = useAppState();
|
||||||
const [processingMessage, setProcessingMessage] = useState<ChatMessage | null>(null);
|
const [processingMessage, setProcessingMessage] = useState<ChatMessage | null>(null);
|
||||||
const [processing, setProcessing] = useState<boolean>(false);
|
const [processing, setProcessing] = useState<boolean>(false);
|
||||||
const [generatedUser, setGeneratedUser] = useState<CandidateAI | null>(null);
|
const [generatedUser, setGeneratedUser] = useState<CandidateAI | null>(null);
|
||||||
@ -220,7 +221,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
m: 2,
|
m: 2,
|
||||||
}}>
|
}}>
|
||||||
{processingMessage && chatSession && <Message message={processingMessage} {...{ chatSession, submitQuery, setSnack }} />}
|
{processingMessage && chatSession && <Message message={processingMessage} {...{ chatSession }} />}
|
||||||
<PropagateLoader
|
<PropagateLoader
|
||||||
size="10px"
|
size="10px"
|
||||||
loading={processing}
|
loading={processing}
|
||||||
@ -266,7 +267,7 @@ const GenerateCandidate = (props: BackstoryElementProps) => {
|
|||||||
{resume &&
|
{resume &&
|
||||||
<Paper sx={{pt: 1, pb: 1, pl: 2, pr: 2}}>
|
<Paper sx={{pt: 1, pb: 1, pl: 2, pr: 2}}>
|
||||||
<Scrollable sx={{flexGrow: 1}}>
|
<Scrollable sx={{flexGrow: 1}}>
|
||||||
<StyledMarkdown {...{ content: resume, setSnack, submitQuery }} />
|
<StyledMarkdown content={resume} />
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
</Paper>
|
</Paper>
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ import { Candidate, Job } from "types/types";
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
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 { CandidateInfo } from 'components/ui/CandidateInfo';
|
||||||
import { ComingSoon } from 'components/ui/ComingSoon';
|
import { ComingSoon } from 'components/ui/ComingSoon';
|
||||||
import { LoginRequired } from 'components/ui/LoginRequired';
|
import { LoginRequired } from 'components/ui/LoginRequired';
|
||||||
@ -66,9 +66,8 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
const { user, apiClient } = useAuth();
|
const { user, apiClient } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
|
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate()
|
||||||
const { selectedJob, setSelectedJob } = useSelectedJob()
|
const { selectedJob, setSelectedJob } = useSelectedJob();
|
||||||
const { setSnack, submitQuery } = props;
|
const { setSnack } = useAppState();
|
||||||
const backstoryProps = { setSnack, submitQuery };
|
|
||||||
// State management
|
// State management
|
||||||
const [activeStep, setActiveStep] = useState(0);
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
const [analysisStarted, setAnalysisStarted] = useState(false);
|
const [analysisStarted, setAnalysisStarted] = useState(false);
|
||||||
@ -161,7 +160,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
Select a Candidate
|
Select a Candidate
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<CandidatePicker onSelect={onCandidateSelect} {...backstoryProps} />
|
<CandidatePicker onSelect={onCandidateSelect} />
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -184,11 +183,10 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{jobTab === 'load' &&
|
{jobTab === 'load' &&
|
||||||
<JobPicker {...backstoryProps} onSelect={onJobSelect} />
|
<JobPicker onSelect={onJobSelect} />
|
||||||
}
|
}
|
||||||
{jobTab === 'create' &&
|
{jobTab === 'create' &&
|
||||||
<JobCreator
|
<JobCreator
|
||||||
{...backstoryProps}
|
|
||||||
onSave={onJobSelect}
|
onSave={onJobSelect}
|
||||||
/>}
|
/>}
|
||||||
</Box>
|
</Box>
|
||||||
@ -202,7 +200,6 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
|||||||
<JobMatchAnalysis
|
<JobMatchAnalysis
|
||||||
job={selectedJob}
|
job={selectedJob}
|
||||||
candidate={selectedCandidate}
|
candidate={selectedCandidate}
|
||||||
{...backstoryProps}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -4,19 +4,15 @@ import {
|
|||||||
Container,
|
Container,
|
||||||
Paper,
|
Paper,
|
||||||
Typography,
|
Typography,
|
||||||
Grid,
|
|
||||||
Alert,
|
Alert,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
Divider,
|
|
||||||
Avatar,
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Person,
|
Person,
|
||||||
PersonAdd,
|
PersonAdd,
|
||||||
AccountCircle,
|
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import 'react-phone-number-input/style.css';
|
import 'react-phone-number-input/style.css';
|
||||||
import './LoginPage.css';
|
import './LoginPage.css';
|
||||||
@ -29,10 +25,11 @@ import { BackstoryPageProps } from 'components/BackstoryTab';
|
|||||||
import { LoginForm } from "components/EmailVerificationComponents";
|
import { LoginForm } from "components/EmailVerificationComponents";
|
||||||
import { CandidateRegistrationForm } from "components/RegistrationForms";
|
import { CandidateRegistrationForm } from "components/RegistrationForms";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { setSnack } = props;
|
const { setSnack } = useAppState();
|
||||||
const [tabValue, setTabValue] = useState(0);
|
const [tabValue, setTabValue] = useState(0);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
|
@ -10,9 +10,10 @@ import { BackstoryQuery } from '../components/BackstoryQuery';
|
|||||||
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
import { CandidateInfo } from 'components/ui/CandidateInfo';
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import { Candidate } from 'types/types';
|
import { Candidate } from 'types/types';
|
||||||
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
|
const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: BackstoryPageProps, ref) => {
|
||||||
const { setSnack, submitQuery } = props;
|
const { setSnack } = useAppState();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
@ -28,7 +29,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
|
|||||||
setQuestions([
|
setQuestions([
|
||||||
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
||||||
{candidate.questions?.map((q, i: number) =>
|
{candidate.questions?.map((q, i: number) =>
|
||||||
<BackstoryQuery key={i} question={q} submitQuery={submitQuery} />
|
<BackstoryQuery key={i} question={q} />
|
||||||
)}
|
)}
|
||||||
</Box>,
|
</Box>,
|
||||||
<Box sx={{ p: 1 }}>
|
<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.`}
|
{`As with all LLM interactions, the results may not be 100% accurate. Please contact **${candidate.fullName}** if you have any questions.`}
|
||||||
</MuiMarkdown>
|
</MuiMarkdown>
|
||||||
</Box>]);
|
</Box>]);
|
||||||
}, [candidate, isMobile, submitQuery]);
|
}, [candidate, isMobile]);
|
||||||
|
|
||||||
if (!candidate) {
|
if (!candidate) {
|
||||||
return (<></>);
|
return (<></>);
|
||||||
@ -51,9 +52,7 @@ const ChatPage = forwardRef<ConversationHandle, BackstoryPageProps>((props: Back
|
|||||||
type: "chat",
|
type: "chat",
|
||||||
placeholder: `What would you like to know about ${candidate?.firstName}?`,
|
placeholder: `What would you like to know about ${candidate?.firstName}?`,
|
||||||
resetLabel: "chat",
|
resetLabel: "chat",
|
||||||
setSnack,
|
|
||||||
defaultPrompts: questions,
|
defaultPrompts: questions,
|
||||||
submitQuery,
|
|
||||||
}} />
|
}} />
|
||||||
</Box>);
|
</Box>);
|
||||||
});
|
});
|
||||||
|
@ -11,6 +11,7 @@ import { Conversation } from 'components/Conversation';
|
|||||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||||
import { ChatQuery, ChatMessage } from "types/types";
|
import { ChatQuery, ChatMessage } from "types/types";
|
||||||
import './ResumeBuilderPage.css';
|
import './ResumeBuilderPage.css';
|
||||||
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ResumeBuilder component
|
* ResumeBuilder component
|
||||||
@ -19,11 +20,6 @@ import './ResumeBuilderPage.css';
|
|||||||
* with different layouts for mobile and desktop views.
|
* with different layouts for mobile and desktop views.
|
||||||
*/
|
*/
|
||||||
const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
const ResumeBuilderPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||||
const {
|
|
||||||
sx,
|
|
||||||
setSnack,
|
|
||||||
submitQuery,
|
|
||||||
} = props
|
|
||||||
// State for editing job description
|
// State for editing job description
|
||||||
const [hasJobDescription, setHasJobDescription] = useState<boolean>(false);
|
const [hasJobDescription, setHasJobDescription] = useState<boolean>(false);
|
||||||
const [hasResume, setHasResume] = 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 { VectorVisualizer } from 'components/VectorVisualizer';
|
||||||
import { DocumentManager } from 'components/DocumentManager';
|
import { DocumentManager } from 'components/DocumentManager';
|
||||||
import { JobPicker } from 'components/ui/JobPicker';
|
import { JobPicker } from 'components/ui/JobPicker';
|
||||||
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
interface DashboardProps extends BackstoryPageProps {
|
interface DashboardProps extends BackstoryPageProps {
|
||||||
userName?: string;
|
userName?: string;
|
||||||
@ -58,15 +59,14 @@ const CandidateDashboardPage: React.FC<DashboardProps> = (props: DashboardProps)
|
|||||||
const [activeTab, setActiveTab] = useState<string>(subPage);
|
const [activeTab, setActiveTab] = useState<string>(subPage);
|
||||||
const { user, isLoading, isInitializing, isAuthenticated } = useAuth();
|
const { user, isLoading, isInitializing, isAuthenticated } = useAuth();
|
||||||
const profileCompletion = 75;
|
const profileCompletion = 75;
|
||||||
const { setSnack, submitQuery } = props;
|
const { setSnack } = useAppState();
|
||||||
const backstoryProps = { setSnack, submitQuery };
|
|
||||||
|
|
||||||
const sidebarItems = [
|
const sidebarItems = [
|
||||||
{ text: 'Dashboard', icon: <DashboardIcon />,path: '/', element: <CandidateDashboard {...backstoryProps}/> },
|
{ text: 'Dashboard', icon: <DashboardIcon />, path: '/', element: <CandidateDashboard /> },
|
||||||
{ text: 'Profile', icon: <PersonIcon />,path: '/profile', element: <CandidateProfile {...backstoryProps}/> },
|
{ text: 'Profile', icon: <PersonIcon />, path: '/profile', element: <CandidateProfile /> },
|
||||||
{ text: 'Jobs', icon: <WorkIcon />,path: '/jobs', element: <JobPicker {...backstoryProps}/> },
|
{ text: 'Jobs', icon: <WorkIcon />, path: '/jobs', element: <JobPicker /> },
|
||||||
{ text: 'Resumes', icon: <DescriptionIcon />,path: '/resumes', element: <BetaPage><Box>Candidate resumes page</Box></BetaPage> },
|
{ 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: '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: '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> },
|
{ 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 { BackstoryElementProps } from 'components/BackstoryTab';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ComingSoon } from 'components/ui/ComingSoon';
|
import { ComingSoon } from 'components/ui/ComingSoon';
|
||||||
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
interface CandidateDashboardProps extends BackstoryElementProps {
|
interface CandidateDashboardProps extends BackstoryElementProps {
|
||||||
};
|
};
|
||||||
|
|
||||||
const CandidateDashboard = (props: CandidateDashboardProps) => {
|
const CandidateDashboard = (props: CandidateDashboardProps) => {
|
||||||
const { setSnack, submitQuery } = props;
|
const { setSnack } = useAppState();
|
||||||
const backstoryProps = { setSnack, submitQuery };
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const profileCompletion = 75;
|
const profileCompletion = 75;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <LoginRequired {...backstoryProps}/>;
|
return <LoginRequired />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user?.userType !== 'candidate') {
|
if (user?.userType !== 'candidate') {
|
||||||
|
@ -50,6 +50,7 @@ import { useAuth } from "hooks/AuthContext";
|
|||||||
import * as Types from 'types/types';
|
import * as Types from 'types/types';
|
||||||
import { ComingSoon } from 'components/ui/ComingSoon';
|
import { ComingSoon } from 'components/ui/ComingSoon';
|
||||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||||
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
|
|
||||||
// Styled components
|
// Styled components
|
||||||
const VisuallyHiddenInput = styled('input')({
|
const VisuallyHiddenInput = styled('input')({
|
||||||
@ -95,7 +96,7 @@ function TabPanel(props: TabPanelProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||||
const { setSnack } = props;
|
const { setSnack } = useAppState();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const { user, updateUserData, apiClient } = useAuth();
|
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