2025-05-28 00:49:25 -07:00

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 };