Initial commit — AI-powered coding tutor (Professor)

Next.js 16, React 19, Monaco editor, Anthropic SDK, multi-provider AI,
Wandbox Python execution, iframe HTML preview, SQLite auth + session persistence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aodhan Collins
2026-03-04 21:48:34 +00:00
commit f644937604
56 changed files with 14012 additions and 0 deletions

154
components/AuthModal.tsx Normal file
View File

@@ -0,0 +1,154 @@
'use client';
import { useState } from 'react';
import type { AuthUser } from '@/types';
type Mode = 'login' | 'register';
interface Props {
onSuccess: (user: AuthUser) => void;
onClose: () => void;
}
export default function AuthModal({ onSuccess, onClose }: Props) {
const [mode, setMode] = useState<Mode>('login');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
setError('');
const endpoint = mode === 'login' ? '/api/auth/login' : '/api/auth/register';
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (!res.ok) {
setError(data.error ?? 'Something went wrong');
return;
}
onSuccess(data.user as AuthUser);
} catch {
setError('Network error. Please try again.');
} finally {
setLoading(false);
}
}
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
onClick={(e) => e.target === e.currentTarget && onClose()}
>
<div className="w-full max-w-sm rounded-2xl border border-zinc-700 bg-zinc-900 shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between border-b border-zinc-700 px-6 py-4">
<h2 className="text-sm font-semibold text-zinc-100">Save your progress</h2>
<button
onClick={onClose}
className="rounded-lg p-1 text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200 transition-colors"
aria-label="Close"
>
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>
{/* Mode tabs */}
<div className="flex border-b border-zinc-700">
{(['login', 'register'] as const).map((m) => (
<button
key={m}
onClick={() => { setMode(m); setError(''); }}
className={`flex-1 py-2.5 text-xs font-medium transition-colors ${
mode === m
? 'border-b-2 border-blue-500 text-blue-400'
: 'text-zinc-500 hover:text-zinc-300'
}`}
>
{m === 'login' ? 'Sign In' : 'Create Account'}
</button>
))}
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4 px-6 py-5">
<div>
<label className="mb-1.5 block text-xs font-medium text-zinc-400">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
autoComplete="email"
placeholder="you@example.com"
className="w-full rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-2 text-xs text-zinc-200 placeholder-zinc-600 focus:border-blue-500 focus:outline-none"
/>
</div>
<div>
<label className="mb-1.5 block text-xs font-medium text-zinc-400">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
autoComplete={mode === 'login' ? 'current-password' : 'new-password'}
placeholder={mode === 'register' ? 'At least 8 characters' : '••••••••'}
className="w-full rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-2 text-xs text-zinc-200 placeholder-zinc-600 focus:border-blue-500 focus:outline-none"
/>
</div>
{error && (
<div className="rounded-lg border border-red-800 bg-red-900/20 px-3 py-2 text-xs text-red-400">
{error}
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full rounded-lg bg-blue-600 py-2.5 text-xs font-semibold text-white hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-60 transition-colors"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<span className="h-3 w-3 animate-spin rounded-full border border-blue-300 border-t-white" />
{mode === 'login' ? 'Signing in…' : 'Creating account…'}
</span>
) : mode === 'login' ? (
'Sign In'
) : (
'Create Account'
)}
</button>
<p className="text-center text-[11px] text-zinc-600">
{mode === 'login' ? (
<>No account?{' '}
<button type="button" onClick={() => { setMode('register'); setError(''); }} className="text-zinc-400 hover:text-zinc-200 underline">
Create one
</button>
</>
) : (
<>Already have an account?{' '}
<button type="button" onClick={() => { setMode('login'); setError(''); }} className="text-zinc-400 hover:text-zinc-200 underline">
Sign in
</button>
</>
)}
</p>
</form>
</div>
</div>
);
}