import { useState, useEffect } from "react"; const STYLE = ` @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&family=Inter:wght@400;500;600&display=swap'); *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } :root { --forest: #1C2B1E; --forest-mid: #243228; --forest-light: #2E3F31; --sage: #7A9E7E; --sage-light: #A8C4AB; --cream: #F0EDE4; --cream-dim: #C8C4B8; --terracotta: #C4704A; --terracotta-light: #E8906A; --gold: #D4A853; --correct: #4A8C5C; --correct-bg: #1E3828; --wrong: #9C3D2E; --wrong-bg: #2E1A18; --radius: 16px; --card-w: min(420px, 92vw); --card-h: 340px; } body { font-family: 'Inter', sans-serif; background: var(--forest); color: var(--cream); min-height: 100vh; line-height: 1.6; } .app { min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 0 16px 60px; background: radial-gradient(ellipse 80% 40% at 50% 0%, #2A4A2E 0%, transparent 70%), var(--forest); } /* ── Header ─────────────────────────────── */ .header { text-align: center; padding: 48px 0 32px; max-width: 560px; } .header-eyebrow { font-family: 'Inter', sans-serif; font-size: 11px; font-weight: 600; letter-spacing: 3px; text-transform: uppercase; color: var(--sage); margin-bottom: 12px; } .header h1 { font-family: 'Playfair Display', serif; font-size: clamp(28px, 6vw, 42px); font-weight: 700; color: var(--cream); line-height: 1.15; margin-bottom: 14px; } .header h1 em { font-style: italic; color: var(--sage-light); } .header p { font-size: 15px; color: var(--cream-dim); max-width: 380px; margin: 0 auto; } /* ── Progress bar ───────────────────────── */ .progress-wrap { width: 100%; max-width: 560px; margin: 0 auto 32px; } .progress-meta { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 13px; color: var(--cream-dim); } .progress-meta strong { color: var(--cream); } .progress-track { height: 4px; background: var(--forest-light); border-radius: 99px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(90deg, var(--sage), var(--sage-light)); border-radius: 99px; transition: width 0.5s cubic-bezier(.4,0,.2,1); } /* ── Score dots ─────────────────────────── */ .score-dots { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; margin-bottom: 24px; } .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--forest-light); border: 1.5px solid transparent; transition: all 0.3s; } .dot.correct { background: var(--correct); border-color: var(--sage); } .dot.wrong { background: var(--wrong); border-color: var(--terracotta); } .dot.current { border-color: var(--sage-light); } /* ── Flip card ──────────────────────────── */ .scene { width: var(--card-w); height: var(--card-h); perspective: 1000px; margin: 0 auto 28px; } .card { width: 100%; height: 100%; position: relative; transform-style: preserve-3d; transition: transform 0.65s cubic-bezier(.4,0,.2,1); } .card.flipped { transform: rotateY(180deg); } .card-face { position: absolute; inset: 0; border-radius: var(--radius); backface-visibility: hidden; -webkit-backface-visibility: hidden; } /* Back face — botanical pattern */ .card-back { background: var(--forest-mid); border: 1.5px solid #3A5A3E; display: flex; align-items: center; justify-content: center; overflow: hidden; } .card-back svg.pattern { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0.13; } .card-back-content { position: relative; z-index: 1; text-align: center; padding: 28px; } .card-back-content .q-number { font-size: 11px; font-weight: 600; letter-spacing: 2.5px; text-transform: uppercase; color: var(--sage); margin-bottom: 16px; } .card-back-content .question-text { font-family: 'Playfair Display', serif; font-size: clamp(16px, 3.5vw, 20px); line-height: 1.45; color: var(--cream); margin-bottom: 20px; } .flip-hint { font-size: 12px; color: var(--sage); display: flex; align-items: center; gap: 6px; justify-content: center; opacity: 0.8; } /* Front face — answers */ .card-front { background: var(--forest-mid); border: 1.5px solid #3A5A3E; transform: rotateY(180deg); display: flex; flex-direction: column; padding: 20px; overflow: hidden; } .card-front .q-label { font-size: 10px; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; color: var(--sage); margin-bottom: 10px; } .card-front .q-short { font-family: 'Playfair Display', serif; font-size: 13px; color: var(--cream-dim); margin-bottom: 14px; line-height: 1.35; /* clamp to 2 lines */ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .options { display: flex; flex-direction: column; gap: 8px; flex: 1; } .option-btn { background: var(--forest-light); border: 1.5px solid #3A5A3E; border-radius: 10px; color: var(--cream); font-family: 'Inter', sans-serif; font-size: 13.5px; font-weight: 500; padding: 9px 14px; text-align: left; cursor: pointer; transition: all 0.2s; line-height: 1.35; display: flex; align-items: center; gap: 10px; } .option-btn:hover:not(:disabled) { background: #3A5A3E; border-color: var(--sage); color: var(--cream); } .option-btn:disabled { cursor: default; } .option-btn.correct { background: var(--correct-bg); border-color: var(--sage); color: #90D4A0; } .option-btn.wrong { background: var(--wrong-bg); border-color: var(--terracotta); color: #F09080; } .option-btn.reveal-correct { background: var(--correct-bg); border-color: #4A8C5C; color: #90D4A0; opacity: 0.7; } .opt-letter { min-width: 22px; height: 22px; border-radius: 6px; background: rgba(255,255,255,0.07); display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; letter-spacing: 0; flex-shrink: 0; } .option-btn.correct .opt-letter { background: rgba(74,140,92,0.3); } .option-btn.wrong .opt-letter { background: rgba(156,61,46,0.3); } /* ── Explanation panel ──────────────────── */ .explanation { background: var(--forest-mid); border: 1.5px solid; border-radius: var(--radius); padding: 18px 20px; max-width: var(--card-w); width: 100%; margin: 0 auto 24px; animation: slideUp 0.35s ease; } .explanation.correct { border-color: var(--correct); } .explanation.wrong { border-color: var(--terracotta); } .explanation-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } .result-badge { font-size: 11px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; padding: 3px 10px; border-radius: 99px; } .result-badge.correct { background: var(--correct-bg); color: #90D4A0; border: 1px solid var(--correct); } .result-badge.wrong { background: var(--wrong-bg); color: #F09080; border: 1px solid var(--terracotta); } .explanation p { font-size: 13.5px; color: var(--cream-dim); line-height: 1.6; } .explanation p em { font-style: italic; color: var(--sage-light); } /* ── Next button ────────────────────────── */ .btn-next { display: block; margin: 0 auto; padding: 13px 36px; background: linear-gradient(135deg, var(--sage), #5A8060); border: none; border-radius: 99px; color: var(--forest); font-family: 'Inter', sans-serif; font-size: 14px; font-weight: 700; letter-spacing: 0.3px; cursor: pointer; transition: all 0.2s; box-shadow: 0 4px 20px rgba(122,158,126,0.25); } .btn-next:hover { transform: translateY(-1px); box-shadow: 0 6px 24px rgba(122,158,126,0.35); } .btn-next:active { transform: translateY(0); } /* ── Loading ────────────────────────────── */ .loading-wrap { display: flex; flex-direction: column; align-items: center; gap: 24px; padding: 60px 0; } .leaf-spinner { width: 56px; height: 56px; animation: spin 1.8s linear infinite; filter: drop-shadow(0 0 12px rgba(122,158,126,0.4)); } .loading-wrap p { font-family: 'Playfair Display', serif; font-style: italic; color: var(--sage-light); font-size: 16px; } /* ── Results screen ─────────────────────── */ .results { text-align: center; padding: 40px 0; max-width: 480px; animation: fadeIn 0.5s ease; } .results-icon { font-size: 56px; margin-bottom: 20px; filter: drop-shadow(0 0 20px rgba(122,158,126,0.5)); } .results h2 { font-family: 'Playfair Display', serif; font-size: clamp(24px, 5vw, 34px); margin-bottom: 10px; color: var(--cream); } .score-big { font-family: 'Playfair Display', serif; font-size: clamp(52px, 12vw, 80px); font-weight: 700; color: var(--sage-light); line-height: 1; margin: 20px 0; letter-spacing: -2px; } .score-big span { font-size: 0.45em; color: var(--cream-dim); vertical-align: middle; } .results-comment { font-size: 15px; color: var(--cream-dim); margin-bottom: 36px; line-height: 1.7; } .btn-restart { padding: 14px 44px; background: linear-gradient(135deg, var(--terracotta), #A05A38); border: none; border-radius: 99px; color: #fff; font-family: 'Inter', sans-serif; font-size: 15px; font-weight: 700; cursor: pointer; transition: all 0.2s; box-shadow: 0 4px 20px rgba(196,112,74,0.3); } .btn-restart:hover { transform: translateY(-2px); box-shadow: 0 6px 28px rgba(196,112,74,0.4); } /* ── Error ──────────────────────────────── */ .error-box { background: var(--wrong-bg); border: 1.5px solid var(--terracotta); border-radius: var(--radius); padding: 24px; max-width: 440px; text-align: center; margin: 40px auto; } .error-box h3 { font-family: 'Playfair Display', serif; color: #F09080; margin-bottom: 10px; font-size: 20px; } .error-box p { font-size: 14px; color: var(--cream-dim); margin-bottom: 20px; } /* ── Animations ─────────────────────────── */ @keyframes spin { to { transform: rotate(360deg); } } @keyframes slideUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @media (prefers-reduced-motion: reduce) { .card { transition: none; } .explanation { animation: none; } } `; const LETTERS = ["A", "B", "C", "D"]; function BotanicalPattern() { return ( ); } function LeafSpinner() { return ( ); } async function fetchQuestions() { const prompt = `You are a botanist creating a quiz about native Victorian plants (Victoria, Australia). Generate exactly 10 multiple-choice quiz questions about native Victorian plants. Cover diverse topics: identification features, habitats, ecological roles, Aboriginal uses, common names vs scientific names, flowering seasons, conservation status. Respond ONLY with a valid JSON array, no markdown, no preamble. Format: [ { "question": "Full question text?", "options": ["Option A", "Option B", "Option C", "Option D"], "correct": 0, "explanation": "2-3 sentence explanation of the correct answer, mentioning the plant's scientific name in italics using HTML tags." } ] "correct" is the 0-based index of the correct option.`; const res = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": "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", "anthropic-version": "2023-06-01", "anthropic-dangerous-direct-browser-access": "true", }, body: JSON.stringify({ model: "claude-sonnet-4-6", max_tokens: 3000, messages: [{ role: "user", content: prompt }], }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err?.error?.message || `API error ${res.status}`); } const data = await res.json(); const text = data.content.map(b => b.text || "").join(""); const clean = text.replace(/```json|```/g, "").trim(); const questions = JSON.parse(clean); if (!Array.isArray(questions) || questions.length === 0) throw new Error("Invalid response format"); return questions; } function getResultComment(score, total) { const pct = score / total; if (pct === 1) return "A perfect score! You know Victorian flora like a seasoned field botanist. The bush would be proud."; if (pct >= 0.8) return "Excellent work — you clearly have a keen eye for our native bush. A few more walks through the Grampians and you'll be unbeatable."; if (pct >= 0.6) return "A solid effort! Victoria's flora is wonderfully complex. Keep exploring and the names will become second nature."; if (pct >= 0.4) return "A good start on your botanical journey. Victoria has over 4,000 native plant species — there's plenty more to discover."; return "Every expert started somewhere. Grab a field guide and head to your nearest bushland — the plants are waiting to introduce themselves."; } export default function App() { const [questions, setQuestions] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [current, setCurrent] = useState(0); const [flipped, setFlipped] = useState(false); const [selected, setSelected] = useState(null); const [answers, setAnswers] = useState([]); const load = async () => { setLoading(true); setError(null); setQuestions([]); setCurrent(0); setFlipped(false); setSelected(null); setAnswers([]); try { const qs = await fetchQuestions(); setQuestions(qs); } catch (e) { setError(e.message); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const q = questions[current]; const answered = selected !== null; const finished = answers.length === questions.length && questions.length > 0; const score = answers.filter(Boolean).length; const handleFlip = () => { if (!answered) setFlipped(f => !f); }; const handleSelect = (idx) => { if (answered) return; setSelected(idx); setAnswers(a => [...a, idx === q.correct]); }; const handleNext = () => { setCurrent(c => c + 1); setFlipped(false); setSelected(null); }; return ( <>
Field Guide · Victoria, Australia

Native Plants of Victoria

Ten fresh questions generated by AI — test your knowledge of the remarkable flora of the Victorian bush.

{loading && (

Consulting the field guide…

)} {error && (

Something went wrong

{error}

)} {!loading && !error && questions.length > 0 && !finished && q && ( <> {/* Progress */}
Question {current + 1} of {questions.length} Score {score}
{/* Answer dots */}
{questions.map((_, i) => (
))}
{/* Flip card */}
{/* Back = question */}
Question {current + 1} of {questions.length}
{q.question}
Tap to answer
{/* Front = options */}
Choose your answer
{q.question}
{q.options.map((opt, idx) => { let cls = ""; if (answered) { if (idx === q.correct) cls = selected === idx ? "correct" : "reveal-correct"; else if (idx === selected) cls = "wrong"; } return ( ); })}
{/* Explanation */} {answered && (
{answers[answers.length - 1] ? "✓ Correct" : "✗ Incorrect"}

)} {/* Next */} {answered && ( )} )} {finished && (
{score === questions.length ? "🌿" : score >= 7 ? "🌸" : score >= 5 ? "🍃" : "🌱"}

Quiz complete

{score} / {questions.length}

{getResultComment(score, questions.length)}

{answers.map((a, i) => (
))}
)}
); }