Moved HowItWorks into home page
Before Width: | Height: | Size: 1.5 MiB |
Before Width: | Height: | Size: 226 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
BIN
frontend/src/assets/select-job-analysis.png
Executable file
After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
@ -17,7 +17,8 @@ import {
|
||||
DialogActions,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
Grid
|
||||
Grid,
|
||||
IconButton
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Email as EmailIcon,
|
||||
@ -25,7 +26,9 @@ import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
ErrorOutline as ErrorIcon,
|
||||
Refresh as RefreshIcon,
|
||||
DevicesOther as DevicesIcon
|
||||
DevicesOther as DevicesIcon,
|
||||
VisibilityOff,
|
||||
Visibility
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { BackstoryPageProps } from './BackstoryTab';
|
||||
@ -493,6 +496,7 @@ const LoginForm = () => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) {
|
||||
@ -547,14 +551,28 @@ const LoginForm = () => {
|
||||
autoFocus
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
label="Password"
|
||||
type="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
placeholder="Create a strong password"
|
||||
required
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
edge="end"
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
{errorMessage && (
|
||||
|
@ -34,44 +34,36 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
|
||||
const [resume, setResume] = useState<string>('');
|
||||
const [prompt, setPrompt] = useState<string>('');
|
||||
const [systemPrompt, setSystemPrompt] = useState<string>('');
|
||||
const [generating, setGenerating] = useState<boolean>(false);
|
||||
const [statusMessage, setStatusMessage] = useState<Types.ChatMessageStatus | null>(null);
|
||||
const [generated, setGenerated] = useState<boolean>(false);
|
||||
const [tabValue, setTabValue] = useState<string>('resume');
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||
setTabValue(newValue);
|
||||
}
|
||||
// State for editing job description
|
||||
|
||||
const generateResumeHandlers = {
|
||||
onStatus: (status: Types.ChatMessageStatus) => {
|
||||
setStatusMessage({ ...defaultMessage, content: status.content.toLowerCase() });
|
||||
},
|
||||
onStreaming: (chunk: Types.ChatMessageStreaming) =>{
|
||||
setResume(chunk.content);
|
||||
},
|
||||
onComplete: () => {
|
||||
setStatusMessage(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!job || !candidate || !skills || resume || generating) {
|
||||
if (!job || !candidate || !skills || generated) {
|
||||
return;
|
||||
}
|
||||
|
||||
setGenerated(true);
|
||||
|
||||
const generateResumeHandlers = {
|
||||
onStreaming: (chunk: Types.ChatMessageStreaming) => {
|
||||
setResume(chunk.content);
|
||||
}
|
||||
};
|
||||
|
||||
const generateResume = async () => {
|
||||
const request : any = await apiClient.generateResume(candidate.id || '', skills, generateResumeHandlers);
|
||||
const result = await request.promise;
|
||||
setSystemPrompt(result.systemPrompt)
|
||||
setPrompt(result.prompt)
|
||||
setResume(result.resume)
|
||||
};
|
||||
setGenerating(true);
|
||||
generateResume().then(() =>{
|
||||
setGenerating(false);
|
||||
});
|
||||
}, [job, candidate, apiClient, resume, skills, generating]);
|
||||
const request: any = await apiClient.generateResume(candidate.id || '', skills, generateResumeHandlers);
|
||||
const result = await request.promise;
|
||||
setSystemPrompt(result.systemPrompt)
|
||||
setPrompt(result.prompt)
|
||||
setResume(result.resume)
|
||||
};
|
||||
|
||||
generateResume();
|
||||
}, [job, candidate, apiClient, resume, skills, generated, setSystemPrompt, setPrompt, setResume]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -87,8 +79,8 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
|
||||
<Tab sx={{ display: resume ? "flex" : "none" }} value="resume" icon={<ArticleIcon />} label="Resume" />
|
||||
</Tabs>
|
||||
</Box>}
|
||||
{statusMessage && <Message message={statusMessage} />}
|
||||
<Paper elevation={3} sx={{ p: 3, m: 4, mt: 0 }}><Scrollable autoscroll sx={{ display: "flex", flexGrow: 1 }}>
|
||||
|
||||
<Paper elevation={3} sx={{ p: 3, m: 1, mt: 0 }}><Scrollable autoscroll sx={{ display: "flex", flexGrow: 1 }}>
|
||||
{tabValue === 'system' && <pre>{systemPrompt}</pre>}
|
||||
{tabValue === 'prompt' && <pre>{prompt}</pre>}
|
||||
{tabValue === 'resume' && <StyledMarkdown content={resume} />}
|
||||
|
@ -4,7 +4,7 @@ import { Outlet, useLocation, Routes, Route } from "react-router-dom";
|
||||
import { Box, Container, Paper } from '@mui/material';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { SxProps, Theme } from '@mui/material';
|
||||
|
||||
import { darken } from '@mui/material/styles';
|
||||
import { Header } from 'components/layout/Header';
|
||||
import { Scrollable } from 'components/Scrollable';
|
||||
import { Footer } from 'components/layout/Footer';
|
||||
@ -156,7 +156,7 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
|
||||
mt: "72px", /* Needs to be kept in sync with the height of Header */
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "background.default",
|
||||
backgroundColor: (theme) => darken(theme.palette.background.default, 0.4),
|
||||
height: "100%",
|
||||
maxHeight: "100%",
|
||||
minHeight: "100%",
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Box, Link, Typography, Avatar, Grid, SxProps } from '@mui/material';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Box, Link, Typography, Avatar, Grid, SxProps, Tooltip, IconButton } from '@mui/material';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@ -37,6 +37,20 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
|
||||
const ai: CandidateAI | null = ('isAI' in candidate) ? candidate as CandidateAI : null;
|
||||
const isAdmin = user?.isAdmin;
|
||||
|
||||
// State for description expansion
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
const [shouldShowMoreButton, setShouldShowMoreButton] = useState(false);
|
||||
const descriptionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Check if description needs truncation
|
||||
useEffect(() => {
|
||||
if (descriptionRef.current && candidate.description) {
|
||||
const element = descriptionRef.current;
|
||||
// Check if the scrollHeight is greater than clientHeight (meaning content is truncated)
|
||||
setShouldShowMoreButton(element.scrollHeight > element.clientHeight);
|
||||
}
|
||||
}, [candidate.description]);
|
||||
|
||||
const deleteCandidate = async (candidateId: string | undefined) => {
|
||||
if (candidateId) {
|
||||
await apiClient.deleteCandidate(candidateId);
|
||||
@ -51,12 +65,9 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 2,
|
||||
borderStyle: 'solid',
|
||||
transition: 'all 0.3s ease',
|
||||
flexGrow: 1,
|
||||
p: isMobile ? 1 : 3,
|
||||
p: isMobile ? 1 : 2,
|
||||
height: '100%',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
@ -114,23 +125,58 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
|
||||
<CopyBubble
|
||||
onClick={(event: any) => { event.stopPropagation() }}
|
||||
tooltip="Copy link" content={`${window.location.origin}/u/{candidate.username}`} />
|
||||
{isAdmin && ai &&
|
||||
<DeleteConfirmation
|
||||
onDelete={() => { deleteCandidate(candidate.id); }}
|
||||
sx={{ minWidth: 'auto', px: 2, maxHeight: "min-content", color: "red" }}
|
||||
action="delete"
|
||||
label="user"
|
||||
title="Delete AI user"
|
||||
icon=<DeleteIcon />
|
||||
message={`Are you sure you want to delete ${candidate.username}? This action cannot be undone.`}
|
||||
/>}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{(!isMobile || variant !== "small") && <Typography variant="body1" color="text.secondary">
|
||||
{candidate.description}
|
||||
</Typography>}
|
||||
<Box>
|
||||
{(!isMobile || variant !== "small") && (
|
||||
<Box sx={{ minHeight: "5rem" }}>
|
||||
<Typography
|
||||
ref={descriptionRef}
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: isDescriptionExpanded ? 'unset' : 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
lineHeight: 1.5,
|
||||
fontSize: "0.8rem !important",
|
||||
}}
|
||||
>
|
||||
{candidate.description}
|
||||
</Typography>
|
||||
{shouldShowMoreButton && (
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDescriptionExpanded(!isDescriptionExpanded);
|
||||
}}
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: 'none',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.725rem',
|
||||
fontWeight: 500,
|
||||
mt: 0.5,
|
||||
display: 'block',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
}
|
||||
}}
|
||||
>
|
||||
[{isDescriptionExpanded ? "less" : "more"}]
|
||||
</Link>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{variant !== "small" && <>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
@ -151,9 +197,20 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
|
||||
}
|
||||
</>}
|
||||
</Box>
|
||||
</Box>
|
||||
{isAdmin && ai &&
|
||||
<Box sx={{ display: "flex", flexDirection: "row", justifyContent: "flex-start" }}>
|
||||
<Tooltip title="Delete Job">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); deleteCandidate(candidate.id); }}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export { CandidateInfo };
|
||||
export { CandidateInfo };
|
@ -8,6 +8,7 @@ import { CandidateInfo } from 'components/ui/CandidateInfo';
|
||||
import { Candidate, CandidateAI } from "types/types";
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { useAppState, useSelectedCandidate } from 'hooks/GlobalContext';
|
||||
import { Paper } from '@mui/material';
|
||||
|
||||
interface CandidatePickerProps extends BackstoryElementProps {
|
||||
onSelect?: (candidate: Candidate) => void;
|
||||
@ -57,16 +58,11 @@ const CandidatePicker = (props: CandidatePickerProps) => {
|
||||
<Box sx={{ display: "flex", flexDirection: "column", ...sx }}>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", justifyContent: "center" }}>
|
||||
{candidates?.map((u, i) =>
|
||||
<Box key={`${u.username}`}
|
||||
<Paper key={`${u.username}`}
|
||||
onClick={() => { onSelect ? onSelect(u) : setSelectedCandidate(u); }}
|
||||
sx={{ cursor: "pointer" }}>
|
||||
{selectedCandidate?.id === u.id &&
|
||||
<CandidateInfo sx={{ maxWidth: "320px", "cursor": "pointer", backgroundColor: "#f0f0f0", "&:hover": { border: "2px solid orange" } }} candidate={u} />
|
||||
}
|
||||
{selectedCandidate?.id !== u.id &&
|
||||
<CandidateInfo sx={{ maxWidth: "320px", "cursor": "pointer", border: "2px solid transparent", "&:hover": { border: "2px solid orange" } }} candidate={u} />
|
||||
}
|
||||
</Box>
|
||||
<CandidateInfo variant="small" sx={{ maxWidth: "320px", "cursor": "pointer", backgroundColor: (selectedCandidate?.id === u.id) ? "#f0f0f0" : "inherit", border: "2px solid transparent", "&:hover": { border: "2px solid orange" } }} candidate={u} />
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { JSX, useActionState, useState } from 'react';
|
||||
import React, { JSX, useActionState, useEffect, useRef, useState } from 'react';
|
||||
import { Box, Link, Typography, Avatar, Grid, SxProps, CardActions, Chip, Stack, CardHeader, Button, styled, LinearProgress, IconButton, Tooltip } from '@mui/material';
|
||||
import {
|
||||
Card,
|
||||
@ -47,6 +47,19 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
const [adminStatus, setAdminStatus] = useState<string | null>(null);
|
||||
const [adminStatusType, setAdminStatusType] = useState<Types.ApiActivityType | null>(null);
|
||||
const [activeJob, setActiveJob] = useState<Types.Job>({ ...job }); /* Copy of job */
|
||||
// State for description expansion
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
const [shouldShowMoreButton, setShouldShowMoreButton] = useState(false);
|
||||
const descriptionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Check if description needs truncation
|
||||
useEffect(() => {
|
||||
if (descriptionRef.current && job.summary) {
|
||||
const element = descriptionRef.current;
|
||||
// Check if the scrollHeight is greater than clientHeight (meaning content is truncated)
|
||||
setShouldShowMoreButton(element.scrollHeight > element.clientHeight);
|
||||
}
|
||||
}, [job.summary]);
|
||||
|
||||
const deleteJob = async (jobId: string | undefined) => {
|
||||
if (jobId) {
|
||||
@ -203,28 +216,72 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<Box sx={{ display: "flex", flexGrow: 1, p: isMobile ? 1 : 3, height: '100%', flexDirection: 'column', alignItems: 'stretch', position: "relative" }}>
|
||||
<Box sx={{ display: "flex", flexGrow: 1, p: 1, pb: 0, height: '100%', flexDirection: 'column', alignItems: 'stretch', position: "relative" }}>
|
||||
<Box sx={{
|
||||
display: "flex", flexDirection: (isMobile || variant !== "small") ? "column" : "row",
|
||||
display: "flex", flexDirection: (isMobile || variant === "small") ? "column" : "row",
|
||||
"& > div > div > :first-of-type": { fontWeight: "bold" },
|
||||
"& > div > div > :last-of-type": { mb: 0.75, mr: 1 }
|
||||
}}>
|
||||
<Box sx={{ display: "flex", flexDirection: isMobile ? "row" : "column", flexGrow: 1 }}>
|
||||
{
|
||||
activeJob.title && <Box sx={{ fontSize: "0.8rem" }}>
|
||||
<Box sx={{ display: "flex", flexDirection: isMobile ? "row" : "column", flexGrow: 1, gap: 1 }}>
|
||||
{activeJob.company &&
|
||||
<Box sx={{ fontSize: "0.8rem" }}>
|
||||
<Box>Company</Box>
|
||||
<Box>{activeJob.company}</Box>
|
||||
</Box>
|
||||
}
|
||||
{activeJob.title &&
|
||||
<Box sx={{ fontSize: "0.8rem" }}>
|
||||
<Box>Title</Box>
|
||||
<Box>{activeJob.title}</Box>
|
||||
</Box>
|
||||
}
|
||||
{activeJob.company && <Box sx={{ fontSize: "0.8rem" }}>
|
||||
<Box>Company</Box>
|
||||
<Box>{activeJob.company}</Box>
|
||||
</Box>}
|
||||
</Box>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", width: "75%" }}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", width: (variant !== "small") ? "75%" : "100%" }}>
|
||||
{!isMobile && activeJob.summary && <Box sx={{ fontSize: "0.8rem" }}>
|
||||
<Box>Summary</Box>
|
||||
<Box>{activeJob.summary}</Box>
|
||||
<Box sx={{ minHeight: "5rem" }}>
|
||||
<Typography
|
||||
ref={descriptionRef}
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: isDescriptionExpanded ? 'unset' : 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
lineHeight: 1.5,
|
||||
fontSize: "0.8rem !important",
|
||||
}}
|
||||
>
|
||||
{activeJob.summary}
|
||||
</Typography>
|
||||
{shouldShowMoreButton && (
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDescriptionExpanded(!isDescriptionExpanded);
|
||||
}}
|
||||
sx={{
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: 'none',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.725rem',
|
||||
fontWeight: 500,
|
||||
mt: 0.5,
|
||||
display: 'block',
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
}
|
||||
}}
|
||||
>
|
||||
[{isDescriptionExpanded ? "less" : "more"}]
|
||||
</Link>
|
||||
)}
|
||||
</Box>
|
||||
</Box>}
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -8,6 +8,7 @@ import { JobInfo } from 'components/ui/JobInfo';
|
||||
import { Job } from "types/types";
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { useAppState, useSelectedJob } from 'hooks/GlobalContext';
|
||||
import { Paper } from '@mui/material';
|
||||
|
||||
interface JobPickerProps extends BackstoryElementProps {
|
||||
onSelect?: (job: Job) => void
|
||||
@ -48,16 +49,11 @@ const JobPicker = (props: JobPickerProps) => {
|
||||
<Box sx={{display: "flex", flexDirection: "column", mb: 1}}>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", justifyContent: "center" }}>
|
||||
{jobs?.map((j, i) =>
|
||||
<Box key={`${j.id}`}
|
||||
onClick={() => { onSelect ? onSelect(j) : setSelectedJob(j); }}
|
||||
sx={{ cursor: "pointer" }}>
|
||||
{selectedJob?.id === j.id &&
|
||||
<JobInfo sx={{ maxWidth: "320px", "cursor": "pointer", backgroundColor: "#f0f0f0", "&:hover": { border: "2px solid orange" } }} job={j} />
|
||||
}
|
||||
{selectedJob?.id !== j.id &&
|
||||
<JobInfo sx={{ maxWidth: "320px", "cursor": "pointer", border: "2px solid transparent", "&:hover": { border: "2px solid orange" } }} job={j} />
|
||||
}
|
||||
</Box>
|
||||
<Paper key={`${j.id}`}
|
||||
onClick={() => { onSelect ? onSelect(j) : setSelectedJob(j); }}
|
||||
sx={{ cursor: "pointer" }}>
|
||||
<JobInfo variant="small" sx={{ maxWidth: "320px", "cursor": "pointer", backgroundColor: (selectedJob?.id === j.id) ? "#f0f0f0" : "inherit", border: "2px solid transparent", "&:hover": { border: "2px solid orange" } }} job={j} />
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -24,7 +24,7 @@ import { BackstoryLogo } from 'components/ui/BackstoryLogo';
|
||||
import { HomePage } from 'pages/HomePage';
|
||||
import { CandidateChatPage } from 'pages/CandidateChatPage';
|
||||
import { DocsPage } from 'pages/DocsPage';
|
||||
import { CreateProfilePage } from 'pages/CreateProfilePage';
|
||||
import { CreateProfilePage } from 'pages/candidate/ProfileWizard';
|
||||
import { VectorVisualizerPage } from 'pages/VectorVisualizerPage';
|
||||
import { BetaPage } from 'pages/BetaPage';
|
||||
import { JobAnalysisPage } from 'pages/JobAnalysisPage';
|
||||
@ -51,8 +51,8 @@ const SettingsPage = () => (<BetaPage><Typography variant="h4">Settings</Typogra
|
||||
|
||||
export const navigationConfig: NavigationConfig = {
|
||||
items: [
|
||||
{ id: 'home', label: <BackstoryLogo />, path: '/', component: <HomePage />, userTypes: ['guest', 'candidate', 'employer'], exact: true, },
|
||||
{ id: 'how-it-works', label: 'How It Works', path: '/how-it-works', icon: <SchoolIcon />, component: <HowItWorks />, userTypes: ['guest', 'candidate', 'employer',], },
|
||||
{ id: 'home', label: <BackstoryLogo />, path: '/', component: <HowItWorks />, userTypes: ['guest', 'candidate', 'employer'], exact: true, },
|
||||
// { id: 'how-it-works', label: 'How It Works', path: '/how-it-works', icon: <SchoolIcon />, component: <HowItWorks />, userTypes: ['guest', 'candidate', 'employer',], },
|
||||
{ id: 'job-analysis', label: 'Job Analysis', path: '/job-analysis', icon: <WorkIcon />, component: <JobAnalysisPage />, userTypes: ['guest', 'candidate', 'employer',], },
|
||||
{ id: 'chat', label: 'Candidate Chat', path: '/chat', icon: <ChatIcon />, component: <CandidateChatPage />, userTypes: ['guest', 'candidate', 'employer',], }, {
|
||||
id: 'candidate-menu', label: 'Tools', icon: <PersonIcon />, userTypes: ['candidate'], children: [
|
||||
@ -105,7 +105,7 @@ export const navigationConfig: NavigationConfig = {
|
||||
{
|
||||
id: 'login',
|
||||
label: 'Login',
|
||||
path: '/login',
|
||||
path: '/login/*',
|
||||
component: <LoginPage />,
|
||||
userTypes: ['guest', 'candidate', 'employer'],
|
||||
},
|
||||
|
@ -1,110 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 800 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background gradient -->
|
||||
<defs>
|
||||
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#1a2b45;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#2a3b55;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<!-- Shadow filter -->
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="3" dy="3" stdDeviation="5" flood-opacity="0.3"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Base background -->
|
||||
<rect width="800" height="500" fill="url(#bgGradient)" rx="5" ry="5"/>
|
||||
|
||||
<!-- Abstract connection lines in background -->
|
||||
<path d="M100,100 C300,50 500,200 700,100" stroke="#ffffff" stroke-width="1" fill="none" stroke-opacity="0.1"/>
|
||||
<path d="M100,200 C300,150 500,300 700,200" stroke="#ffffff" stroke-width="1" fill="none" stroke-opacity="0.1"/>
|
||||
<path d="M100,300 C300,250 500,400 700,300" stroke="#ffffff" stroke-width="1" fill="none" stroke-opacity="0.1"/>
|
||||
<path d="M100,400 C300,350 500,450 700,400" stroke="#ffffff" stroke-width="1" fill="none" stroke-opacity="0.1"/>
|
||||
|
||||
<!-- Left person - more photorealistic style -->
|
||||
<g filter="url(#shadow)">
|
||||
<!-- Suit/blazer shape -->
|
||||
<path d="M190,180 L230,170 Q260,230 250,300 L200,320 Q190,250 170,230 Z" fill="#2c3e50"/>
|
||||
<!-- Shirt collar -->
|
||||
<path d="M200,175 L230,170 L235,190 L210,195 Z" fill="#f5f5f5"/>
|
||||
<!-- Head shape -->
|
||||
<circle cx="210" cy="130" r="50" fill="#e0c4a8"/>
|
||||
<!-- Hair -->
|
||||
<path d="M170,115 Q210,80 250,115 L240,135 Q215,110 180,135 Z" fill="#4a3520"/>
|
||||
<!-- Face features suggestion -->
|
||||
<ellipse cx="195" cy="120" rx="5" ry="3" fill="#333333"/>
|
||||
<ellipse cx="225" cy="120" rx="5" ry="3" fill="#333333"/>
|
||||
<path d="M195,145 Q210,155 225,145" fill="none" stroke="#333333" stroke-width="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Middle elements - digital content -->
|
||||
<g filter="url(#shadow)">
|
||||
<!-- Resume/CV element -->
|
||||
<rect x="310" y="150" width="180" height="240" rx="5" ry="5" fill="#f5f5f5"/>
|
||||
<!-- Resume content suggestion -->
|
||||
<line x1="330" y1="180" x2="470" y2="180" stroke="#333" stroke-width="3"/>
|
||||
<line x1="330" y1="200" x2="470" y2="200" stroke="#333" stroke-width="1"/>
|
||||
<line x1="330" y1="215" x2="470" y2="215" stroke="#333" stroke-width="1"/>
|
||||
<line x1="330" y1="230" x2="470" y2="230" stroke="#333" stroke-width="1"/>
|
||||
<line x1="330" y1="260" x2="390" y2="260" stroke="#333" stroke-width="2"/>
|
||||
<line x1="330" y1="280" x2="470" y2="280" stroke="#333" stroke-width="1"/>
|
||||
<line x1="330" y1="295" x2="470" y2="295" stroke="#333" stroke-width="1"/>
|
||||
<line x1="330" y1="310" x2="470" y2="310" stroke="#333" stroke-width="1"/>
|
||||
<line x1="330" y1="340" x2="390" y2="340" stroke="#333" stroke-width="2"/>
|
||||
<line x1="330" y1="360" x2="470" y2="360" stroke="#333" stroke-width="1"/>
|
||||
</g>
|
||||
|
||||
<!-- Digital connecting elements -->
|
||||
<g>
|
||||
<path d="M250,200 Q275,210 310,210" stroke="#4f97eb" stroke-width="2" fill="none" stroke-dasharray="3,3"/>
|
||||
<circle cx="250" cy="200" r="5" fill="#4f97eb"/>
|
||||
<circle cx="310" cy="210" r="5" fill="#4f97eb"/>
|
||||
|
||||
<path d="M250,250 Q285,240 310,260" stroke="#4f97eb" stroke-width="2" fill="none" stroke-dasharray="3,3"/>
|
||||
<circle cx="250" cy="250" r="5" fill="#4f97eb"/>
|
||||
<circle cx="310" cy="260" r="5" fill="#4f97eb"/>
|
||||
|
||||
<path d="M250,300 Q275,320 310,310" stroke="#4f97eb" stroke-width="2" fill="none" stroke-dasharray="3,3"/>
|
||||
<circle cx="250" cy="300" r="5" fill="#4f97eb"/>
|
||||
<circle cx="310" cy="310" r="5" fill="#4f97eb"/>
|
||||
|
||||
<path d="M490,200 Q515,210 550,190" stroke="#4f97eb" stroke-width="2" fill="none" stroke-dasharray="3,3"/>
|
||||
<circle cx="490" cy="200" r="5" fill="#4f97eb"/>
|
||||
<circle cx="550" cy="190" r="5" fill="#4f97eb"/>
|
||||
|
||||
<path d="M490,250 Q515,240 550,260" stroke="#4f97eb" stroke-width="2" fill="none" stroke-dasharray="3,3"/>
|
||||
<circle cx="490" cy="250" r="5" fill="#4f97eb"/>
|
||||
<circle cx="550" cy="260" r="5" fill="#4f97eb"/>
|
||||
|
||||
<path d="M490,300 Q515,320 550,310" stroke="#4f97eb" stroke-width="2" fill="none" stroke-dasharray="3,3"/>
|
||||
<circle cx="490" cy="300" r="5" fill="#4f97eb"/>
|
||||
<circle cx="550" cy="310" r="5" fill="#4f97eb"/>
|
||||
</g>
|
||||
|
||||
<!-- Right person - more photorealistic style -->
|
||||
<g filter="url(#shadow)">
|
||||
<!-- Suit/blazer shape -->
|
||||
<path d="M570,180 L610,170 Q630,230 620,300 L580,320 Q560,250 550,230 Z" fill="#2c3e50"/>
|
||||
<!-- Shirt collar -->
|
||||
<path d="M580,175 L610,170 L615,190 L590,195 Z" fill="#f5f5f5"/>
|
||||
<!-- Head shape -->
|
||||
<circle cx="590" cy="130" r="50" fill="#e0c4a8"/>
|
||||
<!-- Hair -->
|
||||
<path d="M550,110 Q590,80 625,110 L615,140 Q585,120 560,140 Z" fill="#774936"/>
|
||||
<!-- Face features suggestion -->
|
||||
<ellipse cx="575" cy="120" rx="5" ry="3" fill="#333333"/>
|
||||
<ellipse cx="605" cy="120" rx="5" ry="3" fill="#333333"/>
|
||||
<path d="M575,145 Q590,155 605,145" fill="none" stroke="#333333" stroke-width="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Top title and subtitle elements -->
|
||||
<g>
|
||||
<text x="400" y="60" font-family="Arial, sans-serif" font-size="24" text-anchor="middle" fill="#ffffff" font-weight="bold">Professional Conversations</text>
|
||||
<text x="400" y="90" font-family="Arial, sans-serif" font-size="16" text-anchor="middle" fill="#e1e8f0">Discover the depth of your career journey</text>
|
||||
</g>
|
||||
|
||||
<!-- Additional decorative elements -->
|
||||
<circle cx="150" cy="420" r="30" fill="#3b5998" opacity="0.2"/>
|
||||
<circle cx="650" cy="420" r="30" fill="#3b5998" opacity="0.2"/>
|
||||
<circle cx="400" cy="450" r="20" fill="#3b5998" opacity="0.2"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 5.6 KiB |
@ -18,7 +18,7 @@ import PersonSearchIcon from '@mui/icons-material/PersonSearch';
|
||||
import WorkHistoryIcon from '@mui/icons-material/WorkHistory';
|
||||
import QuestionAnswerIcon from '@mui/icons-material/QuestionAnswer';
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import professionalConversationPng from './Conversation.png';
|
||||
import professionalConversationPng from 'assets/Conversation.png';
|
||||
import { ComingSoon } from 'components/ui/ComingSoon';
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
|
||||
|
@ -13,6 +13,8 @@ import {
|
||||
Step,
|
||||
StepLabel,
|
||||
Stepper,
|
||||
Stack,
|
||||
ButtonProps,
|
||||
} from '@mui/material';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
@ -21,14 +23,23 @@ import PersonIcon from '@mui/icons-material/Person';
|
||||
import WorkIcon from '@mui/icons-material/Work';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import professionalConversationPng from 'assets/Conversation.png';
|
||||
import selectAJobPng from 'assets/select-a-job.png';
|
||||
import selectJobAnalysisPng from 'assets/select-job-analysis.png';
|
||||
import selectACandidatePng from 'assets/select-a-candidate.png';
|
||||
import selectStartAnalysisPng from 'assets/select-start-analysis.png';
|
||||
import waitPng from 'assets/wait.png';
|
||||
import finalResumePng from 'assets/final-resume.png';
|
||||
|
||||
import { Beta } from 'components/ui/Beta';
|
||||
|
||||
// Styled components matching HomePage patterns
|
||||
const HeroSection = styled(Box)(({ theme }) => ({
|
||||
padding: theme.spacing(6, 0),
|
||||
padding: theme.spacing(3, 0),
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: theme.spacing(4, 0),
|
||||
padding: theme.spacing(2, 0),
|
||||
},
|
||||
}));
|
||||
|
||||
@ -119,7 +130,7 @@ const StepContent: React.FC<StepContentProps> = ({
|
||||
<Typography variant="h3" component="h2" sx={{ color: 'primary.main', mb: 1 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, justifyContent: { xs: 'center', md: 'flex-start' } }}>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', justifyContent: { xs: 'center', md: 'flex-start' } }}>
|
||||
{icon}
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{subtitle}
|
||||
@ -173,6 +184,34 @@ const StepContent: React.FC<StepContentProps> = ({
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
interface HeroButtonProps extends ButtonProps {
|
||||
children?: string;
|
||||
path: string;
|
||||
}
|
||||
const HeroButton = (props: HeroButtonProps) => {
|
||||
const { children, onClick, path, ...rest } = props;
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
navigate(path);
|
||||
};
|
||||
|
||||
const HeroStyledButton = styled(Button)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
padding: theme.spacing(1, 3),
|
||||
fontWeight: 500,
|
||||
backgroundColor: theme.palette.action.active,
|
||||
color: theme.palette.background.paper,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action.active,
|
||||
opacity: 0.9,
|
||||
},
|
||||
}));
|
||||
return <HeroStyledButton onClick={onClick ? onClick : handleClick} {...rest}>
|
||||
{children}
|
||||
</HeroStyledButton>
|
||||
}
|
||||
|
||||
const HowItWorks: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
@ -183,10 +222,76 @@ const HowItWorks: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
||||
{/* Hero Section */}
|
||||
{/* Hero Section */}
|
||||
<HeroSection>
|
||||
<Container>
|
||||
<Box sx={{ textAlign: 'center', maxWidth: 800, mx: 'auto' }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', md: 'row' },
|
||||
gap: 4,
|
||||
alignItems: 'center',
|
||||
flexGrow: 1,
|
||||
maxWidth: "1024px"
|
||||
}}>
|
||||
<Box sx={{ flex: 1, flexGrow: 1 }}>
|
||||
<Typography
|
||||
variant="h2"
|
||||
component="h1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
fontSize: { xs: '2rem', md: '3rem' },
|
||||
mb: 2,
|
||||
color: "white"
|
||||
}}
|
||||
>
|
||||
Your complete professional story, beyond a single page
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ mb: 3, fontWeight: 400 }}>
|
||||
Let potential employers discover the depth of your experience through interactive Q&A and tailored resumes
|
||||
</Typography>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
|
||||
<HeroButton
|
||||
variant="contained"
|
||||
size="large"
|
||||
path="/login/register"
|
||||
>
|
||||
Get Started as Candidate
|
||||
</HeroButton>
|
||||
{/* <HeroButton
|
||||
variant="outlined"
|
||||
size="large"
|
||||
sx={{
|
||||
backgroundColor: 'transparent',
|
||||
border: '2px solid',
|
||||
borderColor: 'action.active'
|
||||
}}
|
||||
>
|
||||
Recruit Talent
|
||||
</HeroButton> */}
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box sx={{ justifyContent: "center", display: { xs: 'none', md: 'block' } }}>
|
||||
<Box
|
||||
component="img"
|
||||
src={professionalConversationPng}
|
||||
alt="Professional conversation"
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: 200,
|
||||
height: 'auto',
|
||||
borderRadius: 2,
|
||||
boxShadow: 3,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
</HeroSection>
|
||||
<HeroSection sx={{ display: "flex", position: "relative", overflow: "hidden", border: "2px solid orange" }}>
|
||||
<Beta sx={{ left: "-90px" }} />
|
||||
<Container sx={{ display: "flex", position: "relative" }}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", textAlign: 'center', maxWidth: 800, mx: 'auto', position: "relative" }}>
|
||||
<Typography
|
||||
variant="h2"
|
||||
component="h1"
|
||||
@ -238,7 +343,7 @@ const HowItWorks: React.FC = () => {
|
||||
description={[
|
||||
"Select 'Job Analysis' from the menu. This takes you to the interactive Job Analysis page, where you will get to evaluate a candidate for a selected job."
|
||||
]}
|
||||
imageSrc="/select-job-analysis.png"
|
||||
imageSrc={selectJobAnalysisPng}
|
||||
imageAlt="Select Job Analysis from menu"
|
||||
/>
|
||||
</Container>
|
||||
@ -255,7 +360,7 @@ const HowItWorks: React.FC = () => {
|
||||
description={[
|
||||
"Once on the Job Analysis Page, explore a little bit and then select one of the jobs. The requirements and information provided on Backstory are extracted from job postings that users have pasted as a job description or uploaded from a PDF."
|
||||
]}
|
||||
imageSrc="/select-a-job.png"
|
||||
imageSrc={selectAJobPng}
|
||||
imageAlt="Select a job from the available options"
|
||||
note="You can create your own job postings once you create an account. Until then, you need to select one that already exists."
|
||||
reversed={true}
|
||||
@ -274,7 +379,7 @@ const HowItWorks: React.FC = () => {
|
||||
description={[
|
||||
"Now that you have a Job selected, you need to select a candidate. In addition to myself (James), there are several candidates which AI has generated. Each has a unique skillset and can be used to test out the system."
|
||||
]}
|
||||
imageSrc="/select-a-candidate.png"
|
||||
imageSrc={selectACandidatePng}
|
||||
imageAlt="Select a candidate from the available profiles"
|
||||
note="If you create an account, you can opt-in to have your account show up for others to view as well, or keep it private for just your own resume generation and job research."
|
||||
/>
|
||||
@ -294,7 +399,7 @@ const HowItWorks: React.FC = () => {
|
||||
"This could be as little as a simple resume, or as complete as a full work history. Backstory performs similarity searches to identify key elements from the candidate that pertain to a given skill and provides a graded response.",
|
||||
"To see that in action, click the \"Start Skill Assessment\" button."
|
||||
]}
|
||||
imageSrc="/select-start-analysis.png"
|
||||
imageSrc={selectStartAnalysisPng}
|
||||
imageAlt="Start the skill assessment process"
|
||||
reversed={true}
|
||||
/>
|
||||
@ -313,7 +418,7 @@ const HowItWorks: React.FC = () => {
|
||||
"Once you begin that action, the Start Skill Assessment button will grey out and the page will begin updating as it discovers information about the candidate. As it does its thing, you can monitor the progress and explore the different identified skills to see how or why a candidate does or does not have that skill.",
|
||||
"Once it is done, you can see the final Overall Match. This is a weighted score based on amount of evidence a skill had, whether the skill was required or preferred, and other metrics."
|
||||
]}
|
||||
imageSrc="/wait.png"
|
||||
imageSrc={waitPng}
|
||||
imageAlt="Wait for the analysis to complete and review results"
|
||||
/>
|
||||
</Container>
|
||||
@ -331,7 +436,7 @@ const HowItWorks: React.FC = () => {
|
||||
"The final step is creating the custom resume for the Candidate tailored to the particular Job. On the bottom right you can click \"Next\" to have Backstory generate the custom resume.",
|
||||
"Note that the resume focuses on identifying key areas from the Candidate's work history that align with skills which were extracted from the original job posting."
|
||||
]}
|
||||
imageSrc="/final-resume.png"
|
||||
imageSrc={finalResumePng}
|
||||
imageAlt="Generated custom resume tailored to the job"
|
||||
success="Success! You can then click the Copy button to copy the resume into your editor, adjust, and apply for your dream job!"
|
||||
reversed={true}
|
||||
|
@ -324,16 +324,25 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps
|
||||
</Stepper>
|
||||
<Box sx={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}>
|
||||
{analysisState && analysisState.job &&
|
||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||
{!isMobile && <Box sx={{ ml: 3, fontWeight: "bold" }}>Selected Job</Box>}
|
||||
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}>
|
||||
{!isMobile &&
|
||||
<Avatar
|
||||
sx={{
|
||||
ml: 1, mt: 1,
|
||||
bgcolor: theme.palette.primary.main,
|
||||
color: 'white'
|
||||
}}
|
||||
>
|
||||
<WorkIcon />
|
||||
</Avatar>
|
||||
}
|
||||
<JobInfo variant="small" job={analysisState.job} />
|
||||
</Box>
|
||||
}
|
||||
{isMobile && <Box sx={{ display: "flex", borderBottom: "1px solid grey" }} />}
|
||||
{!isMobile && <Box sx={{ display: "flex", borderLeft: "1px solid lightgrey" }} />}
|
||||
{analysisState && analysisState.candidate &&
|
||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||
{!isMobile && <Box sx={{ ml: 3, fontWeight: "bold" }}>Selected Candidate</Box>}
|
||||
<Box sx={{ display: "flex", flexDirection: "row", width: "100%" }}>
|
||||
<CandidateInfo variant="small" candidate={analysisState.candidate} sx={{}} />
|
||||
</Box>
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ import {
|
||||
Tab,
|
||||
Card,
|
||||
CardContent,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Person,
|
||||
@ -23,7 +25,7 @@ import { BackstoryLogo } from 'components/ui/BackstoryLogo';
|
||||
import { BackstoryPageProps } from 'components/BackstoryTab';
|
||||
|
||||
import { LoginForm } from "components/EmailVerificationComponents";
|
||||
import { CandidateRegistrationForm } from "components/RegistrationForms";
|
||||
import { CandidateRegistrationForm } from "pages/candidate/RegistrationForms";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppState } from 'hooks/GlobalContext';
|
||||
import * as Types from 'types/types';
|
||||
@ -37,7 +39,8 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
const { guest, user, login, isLoading, error } = useAuth();
|
||||
const name = (user?.userType === 'candidate') ? (user as Types.Candidate).username : user?.email || '';
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const showGuest: boolean = false;
|
||||
|
||||
useEffect(() => {
|
||||
@ -69,8 +72,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm" sx={{ mt: 4 }}>
|
||||
<Paper elevation={3} sx={{ p: 4 }}>
|
||||
<Paper elevation={3} sx={{ p: isMobile ? 0 : 4 }}>
|
||||
<BackstoryLogo />
|
||||
|
||||
{showGuest && guest && (
|
||||
@ -89,7 +91,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||
<Tabs value={tabValue} onChange={handleTabChange} centered>
|
||||
<Tab icon={<Person />} label="Login" />
|
||||
<Tab icon={<PersonAdd />} label="Register" />
|
||||
@ -115,8 +117,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||
{tabValue === 1 && (
|
||||
<CandidateRegistrationForm />
|
||||
)}
|
||||
</Paper>
|
||||
</Container>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
|
874
frontend/src/pages/candidate/RegistrationForms.tsx
Normal file
@ -0,0 +1,874 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Box,
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
Stack,
|
||||
Alert,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
FormHelperText,
|
||||
LinearProgress,
|
||||
CircularProgress,
|
||||
Link,
|
||||
Card,
|
||||
CardContent,
|
||||
CardActions,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
IconButton,
|
||||
InputAdornment
|
||||
} from '@mui/material';
|
||||
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { ApiClient } from 'services/api-client';
|
||||
import { RegistrationSuccessDialog } from 'components/EmailVerificationComponents';
|
||||
import { useAuth } from 'hooks/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
// Candidate Registration Form
|
||||
const CandidateRegistrationForm = () => {
|
||||
const { apiClient } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phone: ''
|
||||
});
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [registrationResult, setRegistrationResult] = useState<any>(null);
|
||||
|
||||
// Password visibility states
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
// Email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!formData.email) {
|
||||
newErrors.email = 'Email is required';
|
||||
} else if (!emailRegex.test(formData.email)) {
|
||||
newErrors.email = 'Please enter a valid email address';
|
||||
}
|
||||
|
||||
// Username validation
|
||||
if (!formData.username) {
|
||||
newErrors.username = 'Username is required';
|
||||
} else if (formData.username.length < 3) {
|
||||
newErrors.username = 'Username must be at least 3 characters';
|
||||
} else if (!/^[a-zA-Z0-9_]+$/.test(formData.username)) {
|
||||
newErrors.username = 'Username can only contain letters, numbers, and underscores';
|
||||
}
|
||||
|
||||
// Password validation
|
||||
if (!formData.password) {
|
||||
newErrors.password = 'Password is required';
|
||||
} else {
|
||||
const passwordErrors = validatePassword(formData.password);
|
||||
if (passwordErrors.length > 0) {
|
||||
newErrors.password = passwordErrors.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm password
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'Passwords do not match';
|
||||
}
|
||||
|
||||
// Name validation
|
||||
if (!formData.firstName.trim()) {
|
||||
newErrors.firstName = 'First name is required';
|
||||
}
|
||||
if (!formData.lastName.trim()) {
|
||||
newErrors.lastName = 'Last name is required';
|
||||
}
|
||||
|
||||
// Phone validation (optional but must be valid if provided)
|
||||
if (formData.phone && !/^[+]?[1-9][\d]{0,15}$/.test(formData.phone.replace(/\s/g, ''))) {
|
||||
newErrors.phone = 'Please enter a valid phone number';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const validatePassword = (password: string): string[] => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (password.length < 8) {
|
||||
errors.push('at least 8 characters');
|
||||
}
|
||||
if (!/[a-z]/.test(password)) {
|
||||
errors.push('one lowercase letter');
|
||||
}
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
errors.push('one uppercase letter');
|
||||
}
|
||||
if (!/\d/.test(password)) {
|
||||
errors.push('one number');
|
||||
}
|
||||
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
||||
errors.push('one special character');
|
||||
}
|
||||
|
||||
return errors.length > 0 ? [`Password must contain ${errors.join(', ')}`] : [];
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await apiClient.createCandidate({
|
||||
email: formData.email,
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
firstName: formData.firstName,
|
||||
lastName: formData.lastName,
|
||||
phone: formData.phone || undefined
|
||||
});
|
||||
|
||||
// Set pending verification
|
||||
apiClient.setPendingEmailVerification(formData.email);
|
||||
|
||||
setRegistrationResult(result);
|
||||
setShowSuccess(true);
|
||||
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('already exists')) {
|
||||
if (error.message.includes('email')) {
|
||||
setErrors({ email: 'An account with this email already exists' });
|
||||
} else if (error.message.includes('username')) {
|
||||
setErrors({ username: 'This username is already taken' });
|
||||
}
|
||||
} else {
|
||||
setErrors({ general: error.message || 'Registration failed. Please try again.' });
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getPasswordStrength = (password: string) => {
|
||||
const validations = [
|
||||
password.length >= 8,
|
||||
/[a-z]/.test(password),
|
||||
/[A-Z]/.test(password),
|
||||
/\d/.test(password),
|
||||
/[!@#$%^&*(),.?":{}|<>]/.test(password)
|
||||
];
|
||||
|
||||
const strength = validations.filter(Boolean).length;
|
||||
|
||||
if (strength < 2) return { level: 'weak', color: 'error', value: 20 };
|
||||
if (strength < 4) return { level: 'medium', color: 'warning', value: 60 };
|
||||
return { level: 'strong', color: 'success', value: 100 };
|
||||
};
|
||||
|
||||
const passwordStrength = formData.password ? getPasswordStrength(formData.password) : null;
|
||||
|
||||
return (
|
||||
<Box sx={{ p: isMobile ? 1 : 5 }}>
|
||||
<Box sx={{ textAlign: 'center', mb: 4 }}>
|
||||
<Typography variant="h4" component="h1" sx={{ mb: 1 }}>
|
||||
Join as a Candidate
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Create your account to start finding your dream job
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Email Address"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
placeholder="your.email@example.com"
|
||||
error={!!errors.email}
|
||||
helperText={errors.email}
|
||||
required
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Username"
|
||||
value={formData.username}
|
||||
onChange={(e) => handleInputChange('username', e.target.value.toLowerCase())}
|
||||
placeholder="johndoe123"
|
||||
error={!!errors.username}
|
||||
helperText={errors.username}
|
||||
required
|
||||
/>
|
||||
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="First Name"
|
||||
value={formData.firstName}
|
||||
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
||||
placeholder="John"
|
||||
error={!!errors.firstName}
|
||||
helperText={errors.firstName}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Last Name"
|
||||
value={formData.lastName}
|
||||
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
||||
placeholder="Doe"
|
||||
error={!!errors.lastName}
|
||||
helperText={errors.lastName}
|
||||
required
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Phone Number"
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
placeholder="+1 (555) 123-4567"
|
||||
error={!!errors.phone}
|
||||
helperText={errors.phone || "Optional"}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={formData.password}
|
||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||
placeholder="Create a strong password"
|
||||
error={!!errors.password}
|
||||
helperText={errors.password}
|
||||
required
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
edge="end"
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{formData.password && passwordStrength && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={passwordStrength.value}
|
||||
color={passwordStrength.color as any}
|
||||
sx={{ height: 6, borderRadius: 3 }}
|
||||
/>
|
||||
<Typography variant="caption" color={`${passwordStrength.color}.main`} sx={{ mt: 0.5, display: 'block', textTransform: 'capitalize' }}>
|
||||
Password strength: {passwordStrength.level}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Confirm Password"
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
|
||||
placeholder="Confirm your password"
|
||||
error={!!errors.confirmPassword}
|
||||
helperText={errors.confirmPassword}
|
||||
required
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle confirm password visibility"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
edge="end"
|
||||
>
|
||||
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
{errors.general && (
|
||||
<Alert severity="error">
|
||||
{errors.general}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
sx={{ py: 2 }}
|
||||
>
|
||||
{loading ? (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<CircularProgress size={20} color="inherit" />
|
||||
<Typography>Creating Account...</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
'Create Account'
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Already have an account?{' '}
|
||||
<Link
|
||||
component="button"
|
||||
onClick={(e) => { e.preventDefault(); navigate('/login'); }}
|
||||
sx={{ fontWeight: 600 }}
|
||||
>
|
||||
Sign in here
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
{showSuccess && registrationResult && (
|
||||
<RegistrationSuccessDialog
|
||||
open={showSuccess}
|
||||
onClose={() => setShowSuccess(false)}
|
||||
email={registrationResult.email}
|
||||
userType="candidate"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// Employer Registration Form
|
||||
const EmployerRegistrationForm = () => {
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
companyName: '',
|
||||
industry: '',
|
||||
companySize: '',
|
||||
companyDescription: '',
|
||||
websiteUrl: '',
|
||||
phone: ''
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [registrationResult, setRegistrationResult] = useState<any>(null);
|
||||
|
||||
// Password visibility states
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
|
||||
const apiClient = new ApiClient();
|
||||
|
||||
const industryOptions = [
|
||||
'Technology', 'Healthcare', 'Finance', 'Education', 'Manufacturing',
|
||||
'Retail', 'Consulting', 'Media', 'Non-profit', 'Government', 'Other'
|
||||
];
|
||||
|
||||
const companySizeOptions = [
|
||||
'1-10 employees', '11-50 employees', '51-200 employees',
|
||||
'201-500 employees', '501-1000 employees', '1000+ employees'
|
||||
];
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
// Email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!formData.email) {
|
||||
newErrors.email = 'Email is required';
|
||||
} else if (!emailRegex.test(formData.email)) {
|
||||
newErrors.email = 'Please enter a valid email address';
|
||||
}
|
||||
|
||||
// Username validation
|
||||
if (!formData.username) {
|
||||
newErrors.username = 'Username is required';
|
||||
} else if (formData.username.length < 3) {
|
||||
newErrors.username = 'Username must be at least 3 characters';
|
||||
}
|
||||
|
||||
// Password validation
|
||||
if (!formData.password) {
|
||||
newErrors.password = 'Password is required';
|
||||
} else {
|
||||
const passwordErrors = validatePassword(formData.password);
|
||||
if (passwordErrors.length > 0) {
|
||||
newErrors.password = passwordErrors.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm password
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'Passwords do not match';
|
||||
}
|
||||
|
||||
// Company validation
|
||||
if (!formData.companyName.trim()) {
|
||||
newErrors.companyName = 'Company name is required';
|
||||
}
|
||||
if (!formData.industry) {
|
||||
newErrors.industry = 'Industry is required';
|
||||
}
|
||||
if (!formData.companySize) {
|
||||
newErrors.companySize = 'Company size is required';
|
||||
}
|
||||
if (!formData.companyDescription.trim()) {
|
||||
newErrors.companyDescription = 'Company description is required';
|
||||
} else if (formData.companyDescription.length < 50) {
|
||||
newErrors.companyDescription = 'Company description must be at least 50 characters';
|
||||
}
|
||||
|
||||
// Website URL validation (optional but must be valid if provided)
|
||||
if (formData.websiteUrl) {
|
||||
try {
|
||||
new URL(formData.websiteUrl);
|
||||
} catch {
|
||||
newErrors.websiteUrl = 'Please enter a valid website URL';
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const validatePassword = (password: string): string[] => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (password.length < 8) {
|
||||
errors.push('at least 8 characters');
|
||||
}
|
||||
if (!/[a-z]/.test(password)) {
|
||||
errors.push('one lowercase letter');
|
||||
}
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
errors.push('one uppercase letter');
|
||||
}
|
||||
if (!/\d/.test(password)) {
|
||||
errors.push('one number');
|
||||
}
|
||||
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
|
||||
errors.push('one special character');
|
||||
}
|
||||
|
||||
return errors.length > 0 ? [`Password must contain ${errors.join(', ')}`] : [];
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await apiClient.createEmployerWithVerification({
|
||||
email: formData.email,
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
companyName: formData.companyName,
|
||||
industry: formData.industry,
|
||||
companySize: formData.companySize,
|
||||
companyDescription: formData.companyDescription,
|
||||
websiteUrl: formData.websiteUrl || undefined,
|
||||
phone: formData.phone || undefined
|
||||
});
|
||||
|
||||
// Set pending verification
|
||||
apiClient.setPendingEmailVerification(formData.email);
|
||||
|
||||
setRegistrationResult(result);
|
||||
setShowSuccess(true);
|
||||
|
||||
} catch (error: any) {
|
||||
if (error.message.includes('already exists')) {
|
||||
if (error.message.includes('email')) {
|
||||
setErrors({ email: 'An account with this email already exists' });
|
||||
} else if (error.message.includes('username')) {
|
||||
setErrors({ username: 'This username is already taken' });
|
||||
}
|
||||
} else {
|
||||
setErrors({ general: error.message || 'Registration failed. Please try again.' });
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper elevation={3}>
|
||||
<Box sx={{ p: 5 }}>
|
||||
<Box sx={{ textAlign: 'center', mb: 4 }}>
|
||||
<Typography variant="h4" component="h1" sx={{ mb: 1 }}>
|
||||
Join as an Employer
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Create your company account to start hiring top talent
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Stack spacing={4}>
|
||||
{/* Account Information Section */}
|
||||
<Box sx={{ bgcolor: 'grey.50', p: 3, borderRadius: 2 }}>
|
||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||
Account Information
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={3}>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Email Address"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
placeholder="company@example.com"
|
||||
error={!!errors.email}
|
||||
helperText={errors.email}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Username"
|
||||
value={formData.username}
|
||||
onChange={(e) => handleInputChange('username', e.target.value.toLowerCase())}
|
||||
placeholder="company123"
|
||||
error={!!errors.username}
|
||||
helperText={errors.username}
|
||||
required
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={formData.password}
|
||||
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||
placeholder="Create a strong password"
|
||||
error={!!errors.password}
|
||||
helperText={errors.password}
|
||||
required
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
edge="end"
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Confirm Password"
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
|
||||
placeholder="Confirm your password"
|
||||
error={!!errors.confirmPassword}
|
||||
helperText={errors.confirmPassword}
|
||||
required
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle confirm password visibility"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
edge="end"
|
||||
>
|
||||
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Company Information Section */}
|
||||
<Box sx={{ bgcolor: 'primary.50', p: 3, borderRadius: 2 }}>
|
||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||
Company Information
|
||||
</Typography>
|
||||
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Company Name"
|
||||
value={formData.companyName}
|
||||
onChange={(e) => handleInputChange('companyName', e.target.value)}
|
||||
placeholder="Your Company Inc."
|
||||
error={!!errors.companyName}
|
||||
helperText={errors.companyName}
|
||||
required
|
||||
/>
|
||||
|
||||
<Stack direction="row" spacing={2}>
|
||||
<FormControl fullWidth error={!!errors.industry} required>
|
||||
<InputLabel>Industry</InputLabel>
|
||||
<Select
|
||||
value={formData.industry}
|
||||
onChange={(e) => handleInputChange('industry', e.target.value)}
|
||||
label="Industry"
|
||||
>
|
||||
{industryOptions.map(industry => (
|
||||
<MenuItem key={industry} value={industry}>{industry}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{errors.industry && <FormHelperText>{errors.industry}</FormHelperText>}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={!!errors.companySize} required>
|
||||
<InputLabel>Company Size</InputLabel>
|
||||
<Select
|
||||
value={formData.companySize}
|
||||
onChange={(e) => handleInputChange('companySize', e.target.value)}
|
||||
label="Company Size"
|
||||
>
|
||||
{companySizeOptions.map(size => (
|
||||
<MenuItem key={size} value={size}>{size}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{errors.companySize && <FormHelperText>{errors.companySize}</FormHelperText>}
|
||||
</FormControl>
|
||||
</Stack>
|
||||
|
||||
<Box>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Company Description"
|
||||
multiline
|
||||
rows={4}
|
||||
value={formData.companyDescription}
|
||||
onChange={(e) => handleInputChange('companyDescription', e.target.value)}
|
||||
placeholder="Tell us about your company, what you do, your mission, and what makes you unique..."
|
||||
error={!!errors.companyDescription}
|
||||
helperText={errors.companyDescription || `${formData.companyDescription.length}/50 characters minimum`}
|
||||
required
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Website URL"
|
||||
type="url"
|
||||
value={formData.websiteUrl}
|
||||
onChange={(e) => handleInputChange('websiteUrl', e.target.value)}
|
||||
placeholder="https://www.yourcompany.com"
|
||||
error={!!errors.websiteUrl}
|
||||
helperText={errors.websiteUrl || "Optional"}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Phone Number"
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
placeholder="+1 (555) 123-4567"
|
||||
error={!!errors.phone}
|
||||
helperText={errors.phone || "Optional"}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{errors.general && (
|
||||
<Alert severity="error">
|
||||
{errors.general}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
sx={{ py: 2 }}
|
||||
>
|
||||
{loading ? (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<CircularProgress size={20} color="inherit" />
|
||||
<Typography>Creating Company Account...</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
'Create Company Account'
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" sx={{ fontWeight: 600 }}>
|
||||
Sign in here
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{showSuccess && registrationResult && (
|
||||
<RegistrationSuccessDialog
|
||||
open={showSuccess}
|
||||
onClose={() => setShowSuccess(false)}
|
||||
email={registrationResult.email}
|
||||
userType="employer"
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
// Registration Type Selector Component
|
||||
export function RegistrationTypeSelector() {
|
||||
return (
|
||||
<Paper elevation={3}>
|
||||
<Box sx={{ p: 5 }}>
|
||||
<Box sx={{ textAlign: 'center', mb: 5 }}>
|
||||
<Typography variant="h3" component="h1" sx={{ mb: 2 }}>
|
||||
Join Backstory
|
||||
</Typography>
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Choose how you'd like to get started
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Stack direction="row" spacing={3}>
|
||||
{/* Candidate Option */}
|
||||
<Card
|
||||
sx={{
|
||||
flex: 1,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
border: '2px solid transparent',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-4px)',
|
||||
boxShadow: 6,
|
||||
borderColor: 'primary.main'
|
||||
}
|
||||
}}
|
||||
onClick={() => window.location.href = '/register/candidate'}
|
||||
>
|
||||
<CardContent sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography variant="h1" sx={{ mb: 2 }}>👤</Typography>
|
||||
<Typography variant="h5" component="h3" sx={{ mb: 1.5 }}>
|
||||
I'm looking for work
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Create a candidate profile to find your next opportunity
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions sx={{ justifyContent: 'center', pb: 3 }}>
|
||||
<Button variant="contained" size="large">
|
||||
Join as Candidate
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
|
||||
{/* Employer Option */}
|
||||
<Card
|
||||
sx={{
|
||||
flex: 1,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s ease',
|
||||
border: '2px solid transparent',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-4px)',
|
||||
boxShadow: 6,
|
||||
borderColor: 'primary.main'
|
||||
}
|
||||
}}
|
||||
onClick={() => window.location.href = '/register/employer'}
|
||||
>
|
||||
<CardContent sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography variant="h1" sx={{ mb: 2 }}>🏢</Typography>
|
||||
<Typography variant="h5" component="h3" sx={{ mb: 1.5 }}>
|
||||
I'm hiring
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Create a company account to find and hire talent
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions sx={{ justifyContent: 'center', pb: 3 }}>
|
||||
<Button variant="contained" size="large">
|
||||
Join as Employer
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ textAlign: 'center', mt: 4 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" sx={{ fontWeight: 600 }}>
|
||||
Sign in here
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export { CandidateRegistrationForm, EmployerRegistrationForm };
|