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:
157
components/TopicSelector.tsx
Normal file
157
components/TopicSelector.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import type { Topic, SupportedLanguage } from '@/types';
|
||||
import { TOPICS } from '@/lib/topics';
|
||||
|
||||
const PYTHON_ICON = '🐍';
|
||||
const HTML_ICON = '🌐';
|
||||
|
||||
const CUSTOM_STARTER: Record<SupportedLanguage, string> = {
|
||||
python: '# Your code here\n',
|
||||
html: '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <title>Page</title>\n</head>\n<body>\n <!-- Your code here -->\n</body>\n</html>\n',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
onSelect: (topic: Topic) => void;
|
||||
onOpenSettings: () => void;
|
||||
}
|
||||
|
||||
export default function TopicSelector({ onSelect, onOpenSettings }: Props) {
|
||||
const pythonTopics = TOPICS.filter((t) => t.language === 'python');
|
||||
const htmlTopics = TOPICS.filter((t) => t.language === 'html');
|
||||
|
||||
const [customLabel, setCustomLabel] = useState('');
|
||||
const [customLang, setCustomLang] = useState<SupportedLanguage>('python');
|
||||
|
||||
function handleCustomSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const label = customLabel.trim();
|
||||
if (!label) return;
|
||||
onSelect({
|
||||
id: `custom-${Date.now()}`,
|
||||
label,
|
||||
language: customLang,
|
||||
description: 'Custom topic',
|
||||
starterCode: CUSTOM_STARTER[customLang],
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center bg-zinc-950 p-8">
|
||||
<div className="w-full max-w-2xl">
|
||||
{/* Header */}
|
||||
<div className="mb-8 text-center">
|
||||
<h1 className="mb-2 text-3xl font-bold tracking-tight text-zinc-100">Professor</h1>
|
||||
<p className="text-sm text-zinc-400">Choose a topic to start learning with your AI tutor</p>
|
||||
<button
|
||||
onClick={onOpenSettings}
|
||||
className="mt-3 inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} suppressHydrationWarning>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14" />
|
||||
<path d="M12 2v2M12 20v2M2 12h2M20 12h2" />
|
||||
</svg>
|
||||
AI Provider Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Python section */}
|
||||
<Section icon={PYTHON_ICON} label="Python" topics={pythonTopics} onSelect={onSelect} />
|
||||
|
||||
<div className="my-6 border-t border-zinc-800" />
|
||||
|
||||
{/* HTML / CSS section */}
|
||||
<Section icon={HTML_ICON} label="HTML & CSS" topics={htmlTopics} onSelect={onSelect} />
|
||||
|
||||
<div className="my-6 border-t border-zinc-800" />
|
||||
|
||||
{/* Custom topic */}
|
||||
<div>
|
||||
<h2 className="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-widest text-zinc-500">
|
||||
<span>✦</span>
|
||||
Custom
|
||||
</h2>
|
||||
<form onSubmit={handleCustomSubmit} className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={customLabel}
|
||||
onChange={(e) => setCustomLabel(e.target.value)}
|
||||
placeholder="e.g. Python Classes, CSS Grid, JavaScript Promises…"
|
||||
className="min-w-0 flex-1 rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-2.5 text-sm text-zinc-200 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none transition-colors"
|
||||
/>
|
||||
<div className="flex overflow-hidden rounded-lg border border-zinc-700">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCustomLang('python')}
|
||||
className={`px-3 py-2 text-xs transition-colors ${
|
||||
customLang === 'python'
|
||||
? 'bg-zinc-600 text-zinc-100'
|
||||
: 'text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300'
|
||||
}`}
|
||||
>
|
||||
{PYTHON_ICON} Python
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCustomLang('html')}
|
||||
className={`border-l border-zinc-700 px-3 py-2 text-xs transition-colors ${
|
||||
customLang === 'html'
|
||||
? 'bg-zinc-600 text-zinc-100'
|
||||
: 'text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300'
|
||||
}`}
|
||||
>
|
||||
{HTML_ICON} HTML
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!customLabel.trim()}
|
||||
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-40 transition-colors"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({
|
||||
icon,
|
||||
label,
|
||||
topics,
|
||||
onSelect,
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
topics: Topic[];
|
||||
onSelect: (topic: Topic) => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-widest text-zinc-500">
|
||||
<span>{icon}</span>
|
||||
{label}
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
{topics.map((topic) => (
|
||||
<button
|
||||
key={topic.id}
|
||||
onClick={() => onSelect(topic)}
|
||||
className="group flex flex-col gap-1 rounded-xl border border-zinc-800 bg-zinc-900 p-4 text-left transition-all hover:border-zinc-600 hover:bg-zinc-800 hover:shadow-lg hover:shadow-black/20 active:scale-[0.98]"
|
||||
>
|
||||
<span className="text-sm font-semibold text-zinc-200 group-hover:text-white transition-colors">
|
||||
{topic.label}
|
||||
</span>
|
||||
<span className="text-xs leading-relaxed text-zinc-500 group-hover:text-zinc-400 transition-colors">
|
||||
{topic.description}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user