Initial commit

This commit is contained in:
2025-07-11 13:21:49 +00:00
commit 8a7ab3ac24
262 changed files with 28219 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
export default function AuthCallback() {
const navigate = useNavigate();
useEffect(() => {
// Redirect to login page since we're no longer supporting external authentication
navigate('/login');
}, [navigate]);
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50">
<div className="w-full max-w-md space-y-8 rounded-lg bg-white p-6 shadow-lg">
<div className="text-center">
<h2 className="text-3xl font-bold text-gray-900">Redirecting...</h2>
<p className="mt-2 text-sm text-gray-600">Please wait while we redirect you to the login page...</p>
</div>
</div>
</div>
);
}
+70
View File
@@ -0,0 +1,70 @@
import React, { useState } from 'react';
import { TextField, Button, Box, Alert } from '@mui/material';
import { EmailCredentials } from '../../services/auth/authService';
interface EmailLoginFormProps {
role: 'email_teacher' | 'email_student';
onSubmit: (credentials: EmailCredentials) => Promise<void>;
}
export const EmailLoginForm: React.FC<EmailLoginFormProps> = ({ role, onSubmit }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setIsLoading(true);
try {
await onSubmit({ email, password, role });
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to login');
} finally {
setIsLoading(false);
}
};
return (
<Box component="form" onSubmit={handleSubmit} sx={{ width: '100%' }}>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<TextField
fullWidth
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
margin="normal"
required
/>
<TextField
fullWidth
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
margin="normal"
required
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
disabled={isLoading}
sx={{ mt: 3 }}
>
{isLoading ? 'Logging in...' : 'Login'}
</Button>
</Box>
);
};
+139
View File
@@ -0,0 +1,139 @@
import React, { useState } from 'react';
import { TextField, Button, Box, Alert, Stack } from '@mui/material';
import { EmailCredentials } from '../../services/auth/authService';
import { logger } from '../../debugConfig';
interface EmailSignupFormProps {
role: 'email_teacher' | 'email_student';
onSubmit: (
credentials: EmailCredentials,
displayName: string
) => Promise<void>;
}
export const EmailSignupForm: React.FC<EmailSignupFormProps> = ({
role,
onSubmit,
}) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [displayName, setDisplayName] = useState('');
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const validateForm = () => {
if (!email || !password || !confirmPassword || !displayName) {
return 'All fields are required';
}
if (password !== confirmPassword) {
return 'Passwords do not match';
}
if (password.length < 6) {
return 'Password must be at least 6 characters';
}
if (!email.includes('@')) {
return 'Please enter a valid email address';
}
return null;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const validationError = validateForm();
if (validationError) {
setError(validationError);
return;
}
setError(null);
setIsLoading(true);
try {
logger.debug('email-signup-form', '🔄 Submitting signup form', {
email,
role,
hasDisplayName: !!displayName,
});
await onSubmit({ email, password, role }, displayName);
} catch (err) {
setError(
err instanceof Error ? err.message : 'An error occurred during signup'
);
logger.error(
'email-signup-form',
'❌ Signup form submission failed',
err
);
} finally {
setIsLoading(false);
}
};
return (
<Box component="form" onSubmit={handleSubmit} noValidate>
<Stack spacing={2}>
<TextField
required
fullWidth
id="displayName"
label="Display Name"
name="displayName"
autoComplete="name"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
autoFocus
/>
<TextField
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<TextField
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="new-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<TextField
required
fullWidth
name="confirmPassword"
label="Confirm Password"
type="password"
id="confirmPassword"
autoComplete="new-password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
{error && <Alert severity="error">{error}</Alert>}
<Button
type="submit"
fullWidth
variant="contained"
disabled={
isLoading || !email || !password || !confirmPassword || !displayName
}
>
{isLoading ? 'Signing up...' : 'Sign Up'}
</Button>
</Stack>
</Box>
);
};
+113
View File
@@ -0,0 +1,113 @@
import React, { useState } from 'react';
import { Container, Box, Typography, Tabs, Tab, Paper, Button } from '@mui/material';
import { useNavigate } from 'react-router';
import { useAuth } from '../../contexts/AuthContext';
import { SchoolUploadSection } from '../components/admin/SchoolUploadSection';
import { TimetableUploadSection } from '../components/admin/TimetableUploadSection';
import { logger } from '../../debugConfig';
const SUPER_ADMIN_EMAIL = import.meta.env.VITE_SUPER_ADMIN_EMAIL;
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div hidden={value !== index} {...other}>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}
export default function AdminDashboard() {
const { user } = useAuth();
const navigate = useNavigate();
const [tabValue, setTabValue] = useState(0);
const isSuperAdmin = user?.email === SUPER_ADMIN_EMAIL;
logger.debug('admin-page', '🔍 Super admin check', {
userEmail: user?.email,
superAdminEmail: SUPER_ADMIN_EMAIL,
isMatch: isSuperAdmin
});
const handleReturn = () => {
logger.info('admin-page', '🏠 Returning to single player page');
navigate('/single-player');
};
if (!isSuperAdmin) {
logger.error('admin-page', '🚫 Unauthorized access attempt', {
userEmail: user?.email,
requiredEmail: SUPER_ADMIN_EMAIL
});
return (
<Container>
<Typography variant="h4" color="error">Unauthorized Access</Typography>
<Button
onClick={handleReturn}
variant="contained"
sx={{ mt: 2 }}
>
Return to User Page
</Button>
</Container>
);
}
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
return (
<Container maxWidth="lg" sx={{ mt: 4 }}>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2
}}>
<Typography variant="h4">Admin Dashboard</Typography>
<Button
onClick={handleReturn}
variant="outlined"
>
Return to User Page
</Button>
</Box>
<Paper sx={{ width: '100%', mb: 2 }}>
<Tabs value={tabValue} onChange={handleTabChange}>
<Tab label="System Settings" />
<Tab label="Database Management" />
<Tab label="User Management" />
<Tab label="Schools" />
<Tab label="Timetables" />
</Tabs>
<TabPanel value={tabValue} index={0}>
<Typography variant="h6">System Settings</Typography>
{/* Add system settings components here */}
</TabPanel>
<TabPanel value={tabValue} index={1}>
<Typography variant="h6">Database Management</Typography>
{/* Add database management components here */}
</TabPanel>
<TabPanel value={tabValue} index={2}>
<Typography variant="h6">User Management</Typography>
{/* Add user management components here */}
</TabPanel>
<TabPanel value={tabValue} index={3}>
<SchoolUploadSection />
</TabPanel>
<TabPanel value={tabValue} index={4}>
<TimetableUploadSection />
</TabPanel>
</Paper>
</Container>
);
}
+75
View File
@@ -0,0 +1,75 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Container, Typography, Box, Alert } from '@mui/material';
import { useAuth } from '../../contexts/AuthContext';
import { EmailLoginForm } from './EmailLoginForm';
import { EmailCredentials } from '../../services/auth/authService';
import { logger } from '../../debugConfig';
const LoginPage: React.FC = () => {
const navigate = useNavigate();
const { user, signIn } = useAuth();
const [error, setError] = useState<string | null>(null);
logger.debug('login-page', '🔍 Login page loaded', {
hasUser: !!user
});
useEffect(() => {
if (user) {
navigate('/single-player');
}
}, [user, navigate]);
const handleLogin = async (credentials: EmailCredentials) => {
try {
setError(null);
await signIn(credentials.email, credentials.password);
navigate('/single-player');
} catch (error) {
logger.error('login-page', '❌ Login failed', error);
setError(error instanceof Error ? error.message : 'Login failed');
throw error;
}
};
if (user) {
return null;
}
return (
<Container
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
gap: 4
}}
>
<Typography variant="h2" component="h1" gutterBottom>
ClassroomCopilot.ai
</Typography>
<Typography variant="h4" gutterBottom>
Login
</Typography>
{error && (
<Alert severity="error" sx={{ width: '100%', maxWidth: 400 }}>
{error}
</Alert>
)}
<Box sx={{ width: '100%', maxWidth: 400 }}>
<EmailLoginForm
role="email_teacher"
onSubmit={handleLogin}
/>
</Box>
</Container>
);
};
export default LoginPage;
+120
View File
@@ -0,0 +1,120 @@
import React, { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import {
Container,
Typography,
Box,
Button,
Stack,
Divider,
} from '@mui/material';
import { useAuth } from '../../contexts/AuthContext';
import { EmailSignupForm } from './EmailSignupForm';
import { EmailCredentials } from '../../services/auth/authService';
import { RegistrationService } from '../../services/auth/registrationService';
import { logger } from '../../debugConfig';
import MicrosoftIcon from '@mui/icons-material/Microsoft';
const SignupPage: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const { user } = useAuth();
const registrationService = RegistrationService.getInstance();
// Get role from location state, default to teacher
const { role = 'teacher' } = location.state || {};
const roleDisplay = role === 'teacher' ? 'Teacher' : 'Student';
logger.debug('signup-page', '🔍 Signup page loaded', {
role,
hasUser: !!user,
});
useEffect(() => {
if (user) {
navigate('/single-player');
}
}, [user, navigate]);
const handleSignup = async (
credentials: EmailCredentials,
displayName: string
) => {
try {
const result = await registrationService.register(
credentials,
displayName
);
if (result.user) {
navigate('/single-player');
}
} catch (error) {
logger.error('signup-page', '❌ Registration failed', error);
throw error;
}
};
const switchRole = () => {
navigate('/signup', {
state: { role: role === 'teacher' ? 'student' : 'teacher' },
});
};
if (user) {
return null;
}
return (
<Container
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
gap: 4,
}}
>
<Typography variant="h2" component="h1" gutterBottom>
ClassroomCopilot.ai
</Typography>
<Typography variant="h4" gutterBottom>
{roleDisplay} Sign Up
</Typography>
<Box sx={{ width: '100%', maxWidth: 400 }}>
<Button
fullWidth
variant="outlined"
startIcon={<MicrosoftIcon />}
onClick={() => {}}
sx={{ mb: 3 }}
>
Sign up with Microsoft
</Button>
<Divider sx={{ my: 2 }}>OR</Divider>
<EmailSignupForm
role={`email_${role}` as 'email_teacher' | 'email_student'}
onSubmit={handleSignup}
/>
<Stack
direction="row"
spacing={2}
justifyContent="center"
sx={{ mt: 3 }}
>
<Button variant="text" onClick={switchRole}>
Switch to {role === 'teacher' ? 'Student' : 'Teacher'} Sign Up
</Button>
</Stack>
</Box>
</Container>
);
};
export default SignupPage;