570 lines
17 KiB
TypeScript
570 lines
17 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Container,
|
|
Paper,
|
|
TextField,
|
|
Button,
|
|
Typography,
|
|
Grid,
|
|
Alert,
|
|
CircularProgress,
|
|
Tabs,
|
|
Tab,
|
|
AppBar,
|
|
Toolbar,
|
|
Card,
|
|
CardContent,
|
|
Divider,
|
|
Avatar
|
|
} from '@mui/material';
|
|
import { Person, PersonAdd, AccountCircle, ExitToApp } from '@mui/icons-material';
|
|
import 'react-phone-number-input/style.css';
|
|
import PhoneInput from 'react-phone-number-input';
|
|
import { E164Number } from 'libphonenumber-js/core';
|
|
import './PhoneInput.css';
|
|
|
|
// Import conversion utilities
|
|
import {
|
|
formatApiRequest,
|
|
parseApiResponse,
|
|
handleApiResponse,
|
|
extractApiData,
|
|
isSuccessResponse,
|
|
debugConversion,
|
|
type ApiResponse
|
|
} from '../types/conversion';
|
|
|
|
import {
|
|
AuthResponse, BaseUser, Guest
|
|
} from '../types/types'
|
|
|
|
|
|
interface LoginRequest {
|
|
login: string;
|
|
password: string;
|
|
}
|
|
|
|
interface RegisterRequest {
|
|
username: string;
|
|
email: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
password: string;
|
|
phone?: string;
|
|
}
|
|
|
|
const API_BASE_URL = 'https://backstory-beta.ketrenos.com/api/1.0';
|
|
|
|
const BackstoryTestApp: React.FC = () => {
|
|
const [currentUser, setCurrentUser] = useState<BaseUser | null>(null);
|
|
const [guestSession, setGuestSession] = useState<Guest | null>(null);
|
|
const [tabValue, setTabValue] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [success, setSuccess] = useState<string | null>(null);
|
|
const [phone, setPhone] = useState<E164Number | null>(null);
|
|
|
|
// Login form state
|
|
const [loginForm, setLoginForm] = useState<LoginRequest>({
|
|
login: '',
|
|
password: ''
|
|
});
|
|
|
|
// Register form state
|
|
const [registerForm, setRegisterForm] = useState<RegisterRequest>({
|
|
username: '',
|
|
email: '',
|
|
firstName: '',
|
|
lastName: '',
|
|
password: '',
|
|
phone: ''
|
|
});
|
|
|
|
// Create guest session on component mount
|
|
useEffect(() => {
|
|
createGuestSession();
|
|
checkExistingAuth();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (phone !== registerForm.phone && phone) {
|
|
console.log({ phone });
|
|
setRegisterForm({ ...registerForm, phone });
|
|
}
|
|
}, [phone, registerForm]);
|
|
|
|
const createGuestSession = () => {
|
|
const sessionId = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
const guest: Guest = {
|
|
sessionId,
|
|
createdAt: new Date(),
|
|
lastActivity: new Date(),
|
|
ipAddress: 'unknown',
|
|
userAgent: navigator.userAgent
|
|
};
|
|
setGuestSession(guest);
|
|
debugConversion(guest, 'Guest Session');
|
|
};
|
|
|
|
const checkExistingAuth = () => {
|
|
const token = localStorage.getItem('accessToken');
|
|
const userData = localStorage.getItem('userData');
|
|
if (token && userData) {
|
|
try {
|
|
const user = JSON.parse(userData);
|
|
// Convert dates back to Date objects if they're stored as strings
|
|
if (user.createdAt && typeof user.createdAt === 'string') {
|
|
user.createdAt = new Date(user.createdAt);
|
|
}
|
|
if (user.updatedAt && typeof user.updatedAt === 'string') {
|
|
user.updatedAt = new Date(user.updatedAt);
|
|
}
|
|
if (user.lastLogin && typeof user.lastLogin === 'string') {
|
|
user.lastLogin = new Date(user.lastLogin);
|
|
}
|
|
setCurrentUser(user);
|
|
} catch (e) {
|
|
localStorage.removeItem('accessToken');
|
|
localStorage.removeItem('refreshToken');
|
|
localStorage.removeItem('userData');
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleLogin = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError(null);
|
|
setSuccess(null);
|
|
|
|
try {
|
|
// Format request data for API (camelCase to snake_case)
|
|
const requestData = formatApiRequest({
|
|
login: loginForm.login,
|
|
password: loginForm.password
|
|
});
|
|
|
|
debugConversion(requestData, 'Login Request');
|
|
|
|
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(requestData)
|
|
});
|
|
|
|
// Use conversion utility to handle response
|
|
const authResponse = await handleApiResponse<AuthResponse>(response);
|
|
|
|
debugConversion(authResponse, 'Login Response');
|
|
|
|
// Store tokens in localStorage
|
|
localStorage.setItem('accessToken', authResponse.accessToken);
|
|
localStorage.setItem('refreshToken', authResponse.refreshToken);
|
|
localStorage.setItem('userData', JSON.stringify(authResponse.user));
|
|
|
|
setCurrentUser(authResponse.user);
|
|
setSuccess('Login successful!');
|
|
|
|
// Clear form
|
|
setLoginForm({ login: '', password: '' });
|
|
|
|
} catch (err) {
|
|
console.error('Login error:', err);
|
|
setError(err instanceof Error ? err.message : 'Login failed');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRegister = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError(null);
|
|
setSuccess(null);
|
|
|
|
try {
|
|
const candidateData = {
|
|
username: registerForm.username,
|
|
email: registerForm.email,
|
|
firstName: registerForm.firstName,
|
|
lastName: registerForm.lastName,
|
|
fullName: `${registerForm.firstName} ${registerForm.lastName}`,
|
|
phone: registerForm.phone || undefined,
|
|
userType: 'candidate',
|
|
status: 'active',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
skills: [],
|
|
experience: [],
|
|
education: [],
|
|
preferredJobTypes: [],
|
|
languages: [],
|
|
certifications: [],
|
|
location: {
|
|
city: '',
|
|
country: '',
|
|
remote: true
|
|
}
|
|
};
|
|
|
|
// Format request data for API (camelCase to snake_case, dates to ISO strings)
|
|
const requestData = formatApiRequest(candidateData);
|
|
|
|
debugConversion(requestData, 'Registration Request');
|
|
|
|
const response = await fetch(`${API_BASE_URL}/candidates`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(requestData)
|
|
});
|
|
|
|
// Use conversion utility to handle response
|
|
const result = await handleApiResponse<any>(response);
|
|
|
|
debugConversion(result, 'Registration Response');
|
|
|
|
setSuccess('Registration successful! You can now login.');
|
|
|
|
// Clear form and switch to login tab
|
|
setRegisterForm({
|
|
username: '',
|
|
email: '',
|
|
firstName: '',
|
|
lastName: '',
|
|
password: '',
|
|
phone: ''
|
|
});
|
|
setTabValue(0);
|
|
|
|
} catch (err) {
|
|
console.error('Registration error:', err);
|
|
setError(err instanceof Error ? err.message : 'Registration failed');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
localStorage.removeItem('accessToken');
|
|
localStorage.removeItem('refreshToken');
|
|
localStorage.removeItem('userData');
|
|
setCurrentUser(null);
|
|
setSuccess('Logged out successfully');
|
|
createGuestSession();
|
|
};
|
|
|
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
|
setTabValue(newValue);
|
|
setError(null);
|
|
setSuccess(null);
|
|
};
|
|
|
|
// API helper function for authenticated requests
|
|
const makeAuthenticatedRequest = async (url: string, options: RequestInit = {}) => {
|
|
const token = localStorage.getItem('accessToken');
|
|
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
...(token && { 'Authorization': `Bearer ${token}` }),
|
|
...options.headers,
|
|
};
|
|
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers,
|
|
});
|
|
|
|
return handleApiResponse(response);
|
|
};
|
|
|
|
// If user is logged in, show their profile
|
|
if (currentUser) {
|
|
return (
|
|
<Box sx={{ flexGrow: 1 }}>
|
|
<AppBar position="static">
|
|
<Toolbar>
|
|
<AccountCircle sx={{ mr: 2 }} />
|
|
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
|
Welcome, {currentUser.username}
|
|
</Typography>
|
|
<Button
|
|
color="inherit"
|
|
onClick={handleLogout}
|
|
startIcon={<ExitToApp />}
|
|
>
|
|
Logout
|
|
</Button>
|
|
</Toolbar>
|
|
</AppBar>
|
|
|
|
<Container maxWidth="md" sx={{ mt: 4 }}>
|
|
<Card elevation={3}>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
|
|
<Avatar sx={{ mr: 2, bgcolor: 'primary.main' }}>
|
|
<AccountCircle />
|
|
</Avatar>
|
|
<Typography variant="h4" component="h1">
|
|
User Profile
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Divider sx={{ mb: 3 }} />
|
|
|
|
<Grid container spacing={3}>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
<strong>Username:</strong> {currentUser.username}
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
<strong>Email:</strong> {currentUser.email}
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
<strong>Status:</strong> {currentUser.status}
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
<strong>Phone:</strong> {currentUser.phone || 'Not provided'}
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
<strong>Last Login:</strong> {
|
|
currentUser.lastLogin
|
|
? currentUser.lastLogin.toLocaleString()
|
|
: 'N/A'
|
|
}
|
|
</Typography>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, md: 6 }}>
|
|
<Typography variant="body1" sx={{ mb: 1 }}>
|
|
<strong>Member Since:</strong> {currentUser.createdAt.toLocaleDateString()}
|
|
</Typography>
|
|
</Grid>
|
|
</Grid>
|
|
</CardContent>
|
|
</Card>
|
|
</Container>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const validateInput = (value: string) => {
|
|
if (!value) return 'This field is required';
|
|
|
|
// Username: alphanumeric, 3-20 characters, no @
|
|
const usernameRegex = /^[a-zA-Z0-9]{3,20}$/;
|
|
// Email: basic email format
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
|
if (usernameRegex.test(value)) return '';
|
|
if (emailRegex.test(value)) return '';
|
|
return 'Enter a valid username (3-20 alphanumeric characters) or email';
|
|
};
|
|
|
|
const handleLoginChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const { value } = event.target;
|
|
setLoginForm({ ...loginForm, login: value });
|
|
setError(validateInput(value));
|
|
};
|
|
|
|
return (
|
|
<Container maxWidth="sm" sx={{ mt: 4 }}>
|
|
<Paper elevation={3} sx={{ p: 4 }}>
|
|
<Typography variant="h4" component="h1" gutterBottom align="center" color="primary">
|
|
Backstory Platform
|
|
</Typography>
|
|
|
|
{guestSession && (
|
|
<Card sx={{ mb: 3, bgcolor: 'grey.50' }} elevation={1}>
|
|
<CardContent>
|
|
<Typography variant="h6" gutterBottom color="primary">
|
|
Guest Session Active
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
|
|
Session ID: {guestSession.sessionId}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Created: {guestSession.createdAt.toLocaleString()}
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
|
<Tabs value={tabValue} onChange={handleTabChange} centered>
|
|
<Tab icon={<Person />} label="Login" />
|
|
<Tab icon={<PersonAdd />} label="Register" />
|
|
</Tabs>
|
|
</Box>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
{success && (
|
|
<Alert severity="success" sx={{ mb: 2 }}>
|
|
{success}
|
|
</Alert>
|
|
)}
|
|
|
|
{tabValue === 0 && (
|
|
<Box component="form" onSubmit={handleLogin}>
|
|
<Typography variant="h5" gutterBottom>
|
|
Sign In
|
|
</Typography>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Username or Email"
|
|
type="text"
|
|
value={loginForm.login}
|
|
onChange={handleLoginChange}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
placeholder="Enter username or email"
|
|
/>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Password"
|
|
type="password"
|
|
value={loginForm.password}
|
|
onChange={(e) => setLoginForm({ ...loginForm, password: e.target.value })}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
autoComplete='current-password'
|
|
/>
|
|
|
|
<Button
|
|
type="submit"
|
|
fullWidth
|
|
variant="contained"
|
|
sx={{ mt: 3, mb: 2 }}
|
|
disabled={loading}
|
|
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <Person />}
|
|
>
|
|
{loading ? 'Signing In...' : 'Sign In'}
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
|
|
{tabValue === 1 && (
|
|
<Box component="form" onSubmit={handleRegister}>
|
|
<Typography variant="h5" gutterBottom>
|
|
Create Account
|
|
</Typography>
|
|
|
|
<Grid container spacing={2} sx={{ mb: 2 }}>
|
|
<Grid size={{ xs: 12, sm: 6 }}>
|
|
<TextField
|
|
fullWidth
|
|
label="First Name"
|
|
value={registerForm.firstName}
|
|
onChange={(e) => setRegisterForm({ ...registerForm, firstName: e.target.value })}
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
/>
|
|
</Grid>
|
|
|
|
<Grid size={{ xs: 12, sm: 6 }}>
|
|
<TextField
|
|
fullWidth
|
|
label="Last Name"
|
|
value={registerForm.lastName}
|
|
onChange={(e) => setRegisterForm({ ...registerForm, lastName: e.target.value })}
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Username"
|
|
value={registerForm.username}
|
|
onChange={(e) => setRegisterForm({ ...registerForm, username: e.target.value })}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
/>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Email"
|
|
type="email"
|
|
value={registerForm.email}
|
|
onChange={(e) => setRegisterForm({ ...registerForm, email: e.target.value })}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
/>
|
|
|
|
<PhoneInput
|
|
label="Phone (Optional)"
|
|
placeholder="Enter phone number"
|
|
defaultCountry='US'
|
|
value={registerForm.phone}
|
|
disabled={loading}
|
|
onChange={(v) => setPhone(v as E164Number)} />
|
|
{/* <TextField
|
|
fullWidth
|
|
label="Phone (Optional)"
|
|
type="tel"
|
|
value={registerForm.phone}
|
|
onChange={(e) => setRegisterForm({ ...registerForm, phone: e.target.value })}
|
|
margin="normal"
|
|
disabled={loading}
|
|
variant="outlined"
|
|
/> */}
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Password"
|
|
type="password"
|
|
value={registerForm.password}
|
|
onChange={(e) => setRegisterForm({ ...registerForm, password: e.target.value })}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
variant="outlined"
|
|
/>
|
|
|
|
<Button
|
|
type="submit"
|
|
fullWidth
|
|
variant="contained"
|
|
sx={{ mt: 3, mb: 2 }}
|
|
disabled={loading}
|
|
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <PersonAdd />}
|
|
>
|
|
{loading ? 'Creating Account...' : 'Create Account'}
|
|
</Button>
|
|
</Box>
|
|
)}
|
|
</Paper>
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
export { BackstoryTestApp }; |