// ===== src/data.jsx ===== // Courses, programs, sisters, testimonials, blog for Miss Deutsch const PRODUCTS = [ { id: 'a1-starter', name: 'A1 Starter', version: 'Level A1.1', category: 'Course', subtitle: 'Your first 8 weeks of Deutsch', color: 'var(--c-yellow)', glyph: 'A1', short: 'Alphabet, greetings, numbers, daily phrases. Build confidence speaking real German from week one.', long: 'Our A1 Starter is a warm, structured introduction to German for absolute beginners. Across 8 weeks you will master the alphabet, pronunciation, greetings, present-tense verbs, and your first 300 high-frequency words — enough to handle daily conversations, introductions, and travel basics.', tags: ['Beginner', 'Live class', 'Hindi support', 'A1.1'], stats: [ { v: '8', l: 'Weeks' }, { v: '2×', l: 'Classes / week' }, { v: '₹4,500', l: 'Tuition' }], features: [ 'Live online classes with Shivangi & Vaishnavi', 'Hindi + English explanations for tricky grammar', 'Worksheets and audio drills after every class', 'Weekly speaking-practice pairs'] }, { id: 'a1-confident', name: 'A1 Confident', version: 'Level A1.2', category: 'Course', subtitle: 'Tenses, conversation, Goethe-ready', color: 'var(--c-pink)', glyph: 'A1+', short: 'Build real sentences, handle past and future tense, and prep for the Goethe-Zertifikat A1 exam.', long: 'A1 Confident picks up where Starter ends. We add the dative case, modal verbs, separable verbs, and the perfekt tense. By the end you can describe your day, make plans, shop, order food, and sit comfortably for the Goethe A1 exam. Includes one full mock test with detailed feedback.', tags: ['A1.2', 'Goethe prep', 'Conversation', 'Live class'], stats: [ { v: '10', l: 'Weeks' }, { v: '3×', l: 'Classes / week' }, { v: '₹6,500', l: 'Tuition' }], features: [ 'Three live classes per week + one speaking circle', 'Full Goethe A1 mock test with feedback', 'WhatsApp doubt-clearing channel', 'Cultural sessions on German daily life'] }, { id: 'a2', name: 'A2 Builder', version: 'Level A2', category: 'Course', subtitle: 'From beginner to functional speaker', color: 'var(--c-blue)', glyph: 'A2', short: 'Express opinions, narrate stories, handle real-world German situations confidently.', long: 'A2 is where German starts feeling natural. You will learn the genitive, comparative & superlative, two-way prepositions, and the past tense (präteritum). Plenty of role-play: at the doctor, at the bahnhof, at a job interview. By the end you can read short articles, write emails, and hold a 15-minute conversation about your life.', tags: ['A2', 'Goethe prep', 'Email writing', 'Role-play'], stats: [ { v: '12', l: 'Weeks' }, { v: '3×', l: 'Classes / week' }, { v: '₹8,500', l: 'Tuition' }], features: [ 'Goethe A2 exam strategy + 2 mock tests', 'Weekly writing assignments with corrections', 'Conversation partners from across India', 'Bonus: German cinema night once a month'] }, { id: 'kids', name: 'Junior Deutsch', version: 'Ages 9–14', category: 'Kids', subtitle: 'German for young learners', color: 'var(--c-violet)', glyph: 'K', short: 'Playful, story-based German for kids. No rote learning — songs, games, stories, drawings.', long: 'Junior Deutsch is designed for children aged 9 to 14. We use stories, drawing, songs, and gentle games to introduce vocabulary, basic grammar, and pronunciation. Small batches of max 6 children, weekend slots, and a monthly parent update so you always know what your child is learning.', tags: ['Kids', 'Weekend', 'Small batch', 'Story-based'], stats: [ { v: '6', l: 'Max kids / batch' }, { v: 'Sat', l: 'Weekend slot' }, { v: '₹3,500', l: 'Tuition / month' }], features: [ 'Story-based vocabulary building', 'Drawing and craft tied to each lesson', 'Monthly parent progress update', 'Optional weekly homework, never forced'] }, { id: 'exam-prep', name: 'Exam Prep Sprint', version: 'Goethe / TELC', category: 'Exam', subtitle: '4-week intensive for A1 / A2', color: 'var(--c-teal)', glyph: 'EX', short: 'A focused 4-week sprint to prepare for Goethe-Zertifikat or TELC A1/A2 exams.', long: 'Already studied German? This sprint gets you exam-ready in four weeks. Three mock tests, full feedback on writing and speaking, and targeted drills on the trickiest grammar points. We share past-paper patterns and exact strategies for each section.', tags: ['Goethe', 'TELC', 'Mock tests', 'Sprint'], stats: [ { v: '4', l: 'Weeks' }, { v: '3', l: 'Full mock tests' }, { v: '₹3,500', l: 'Tuition' }], features: [ 'Three full timed mock tests with written feedback', 'Speaking practice in exam format', 'Section-by-section past paper strategy', 'WhatsApp support up to the exam day'] }, { id: 'one-on-one', name: 'One-on-One', version: 'Private', category: 'Private', subtitle: 'Tailored coaching, your pace', color: 'var(--c-lime)', glyph: '1:1', short: 'Private sessions tailored to your goal — study abroad, job interview, or hobby.', long: 'Some learners need a fully personal path. One-on-one sessions are designed around your goal: a study-abroad application, a specific exam, a job interview in German, or simply learning faster than a group can move. Flexible scheduling, weekly homework, and full recordings of every session.', tags: ['1:1', 'Custom plan', 'Flexible', 'All levels'], stats: [ { v: '60 min', l: 'Per session' }, { v: '₹800', l: 'Per session' }, { v: 'Any', l: 'Level' }], features: [ 'Custom syllabus built around your goal', 'Session recordings shared after every class', 'Weekly homework + written feedback', 'WhatsApp doubt-clearing between sessions'] }]; const PROGRAMS = [ { id: 'a1-starter', cat: 'Group Class', title: 'A1 Starter Cohort', duration: '8 weeks', desc: 'Live online classes, twice a week. Build your first foundation in German with Hindi-friendly explanations.', price: '₹4,500', seats: '12 seats' }, { id: 'a1-confident', cat: 'Group Class', title: 'A1 Confident — Goethe ready', duration: '10 weeks', desc: 'Complete A1 syllabus + Goethe-Zertifikat A1 mock test. Three live classes per week.', price: '₹6,500', seats: '15 seats' }, { id: 'a2', cat: 'Group Class', title: 'A2 Builder', duration: '12 weeks', desc: 'From confident beginner to functional speaker. Includes 2 Goethe-A2 mock tests and writing feedback.', price: '₹8,500', seats: '15 seats' }, { id: 'exam-prep', cat: 'Sprint', title: 'Goethe / TELC Exam Sprint', duration: '4 weeks', desc: 'Already studied? Get exam-ready with 3 mock tests, speaking practice, and section-by-section strategy.', price: '₹3,500', seats: '8 seats' }, { id: 'kids', cat: 'Junior', title: 'Junior Deutsch (9–14 yrs)', duration: 'Ongoing', desc: 'Story-based, playful German for children. Weekend slots, small batches of 6, monthly parent updates.', price: '₹3,500 / mo', seats: '6 seats' }, { id: 'one-on-one', cat: 'Private', title: 'One-on-One Coaching', duration: 'Flexible', desc: 'Custom syllabus around your goal — study abroad, interview, exam, or hobby. Session recordings included.', price: '₹800 / hr', seats: 'Limited slots' }]; const FACULTY = [ { id: 'shivangi', name: 'Shivangi Tyagi', role: 'Co-founder · Speaking & Culture', tags: ['Public Speaking', 'Operations', 'Art & Culture'], glyph: 'ST', img: 'assets/Shivangi_pic.jfif' }, { id: 'vaishnavi', name: 'Vaishnavi Tyagi', role: 'Lead Educator · German B2 Aspirant', tags: ['Classroom Design', 'Patient Teaching', 'Research'], glyph: 'VT', img: 'assets/Vaishvavi_pic.jfif' }]; const TESTIMONIALS = [ { quote: 'The Hindi-first explanations made German finally click for me. I went from zero to confident greetings in three weeks.', name: 'Ananya S.', role: 'A1 Starter cohort', av: 'AS' }, { quote: 'Small batch, real attention. Vaishnavi corrects my pronunciation in every class — I have never had that before.', name: 'Rohit M.', role: 'A1 Confident cohort', av: 'RM' }, { quote: 'My daughter actually looks forward to her Saturday German class. The story-based approach is gentle and effective.', name: 'Priya K.', role: 'Parent · Junior Deutsch', av: 'PK' }, { quote: 'Honest, warm, and never rushed. The sisters teach the way they would want to be taught — that makes all the difference.', name: 'Tanvi R.', role: 'Exam Sprint · cleared Goethe A1', av: 'TR' }]; const POSTS = [ { id: 'b1', cat: 'Beginner Guide', date: 'May 12, 2026', title: 'The 50 most useful German words for your first week', excerpt: 'Start speaking from day one. A practical, no-grammar list of the words you will reach for again and again.', color: 'var(--c-yellow)', featured: true }, { id: 'b2', cat: 'Exam Tips', date: 'Apr 22, 2026', title: 'How to crack the Goethe A1 speaking section in 15 minutes', excerpt: 'A clear walkthrough of the three speaking tasks, what examiners look for, and the phrases that buy you time.', color: 'var(--c-blue)' }, { id: 'b3', cat: 'Culture', date: 'Apr 04, 2026', title: 'From Meerut to München — why Indians learn German now', excerpt: 'Ausbildung, masters programs, the EU Blue Card. The real reasons more Indians are picking up Deutsch.', color: 'var(--c-violet)' }]; const STATS = [ { v: 6, suffix: '', l: 'Live courses' }, { v: 2, suffix: '', l: 'Sister educators' }, { v: 8, suffix: '', l: 'Max students / batch' }, { v: 100, suffix: '%', l: 'Live, never recorded' }]; Object.assign(window, { PRODUCTS, PROGRAMS, FACULTY, TESTIMONIALS, POSTS, STATS }); // ===== src/atoms.jsx ===== // Small atoms: icons, scroll spy, counter, etc. const { useState, useEffect, useRef, useCallback } = React; // Minimal stroke icons — original line set function Icon({ name, size = 16, stroke = 1.6 }) { const s = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: stroke, strokeLinecap: 'round', strokeLinejoin: 'round' }; switch (name) { case 'arrow-right':return ; case 'arrow-up-right':return ; case 'arrow-left':return ; case 'close':return ; case 'sun':return ; case 'moon':return ; case 'user':return ; case 'mail':return ; case 'phone':return ; case 'pin':return ; case 'check':return ; case 'home':return ; case 'grid':return ; case 'book':return ; case 'spark':return ; case 'logout':return ; default:return null; } } // Animated counter (intersection observer) function Counter({ to, suffix = '', duration = 1400 }) { const ref = useRef(null); const [val, setVal] = useState(0); const fired = useRef(false); useEffect(() => { const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting && !fired.current) { fired.current = true; const start = performance.now(); const tick = (now) => { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); setVal(Math.round(eased * to)); if (t < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); } }); }, { threshold: 0.25 }); if (ref.current) io.observe(ref.current); return () => io.disconnect(); }, [to, duration]); return {val.toLocaleString()}{suffix}; } // Scroll lock for modal function useBodyLock(active) { useEffect(() => { if (!active) return; const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => {document.body.style.overflow = prev;}; }, [active]); } // Theme controller function applyTheme(theme) { document.documentElement.setAttribute('data-theme', theme); } Object.assign(window, { Icon, Counter, useBodyLock, applyTheme }); // ===== src/nav-hero.jsx ===== // Nav + Hero const { useState: useStateNH, useEffect: useEffectNH } = React; function Nav({ onLogin, onDashboard, isAuthed, theme, onToggleTheme, onNavigate, view }) { const [scrolled, setScrolled] = useStateNH(false); const [menuOpen, setMenuOpen] = useStateNH(false); useEffectNH(() => { const onScroll = () => setScrolled(window.scrollY > 16); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); const links = [ ['Courses', '#products'], ['About', '#about'], ['Classes', '#programs'], ['Teachers', '#faculty'], ['Journal', '#blog'], ['Contact', '#contact']]; const closeMenu = () => setMenuOpen(false); return (
{e.preventDefault();onNavigate('home');closeMenu();}}> M MissDeutsch {view === 'home' && }
{view === 'home' && } {isAuthed ? : } Get in touch
{menuOpen && view === 'home' && (
{links.map(([label, href]) => {label} )}
)} {view === 'home' && ( )}
); } function HeroViz() { // Render a network of dots + lines + 3 floating tags const nodes = [ { x: 50, y: 50, r: 26, c: 'var(--accent)' }, { x: 18, y: 22, r: 9, c: 'var(--c-teal)' }, { x: 82, y: 18, r: 7, c: 'var(--c-yellow)' }, { x: 90, y: 60, r: 11, c: 'var(--c-pink)' }, { x: 70, y: 88, r: 8, c: 'var(--c-blue)' }, { x: 22, y: 82, r: 10, c: 'var(--c-lime)' }, { x: 8, y: 55, r: 6, c: 'var(--c-violet)' }, { x: 38, y: 12, r: 5, c: 'var(--ink-2)' }, { x: 62, y: 38, r: 5, c: 'var(--ink-2)' }, { x: 40, y: 70, r: 5, c: 'var(--ink-2)' }, { x: 78, y: 78, r: 4, c: 'var(--ink-2)' }, { x: 30, y: 40, r: 4, c: 'var(--ink-2)' }]; const edges = [ [0, 1], [0, 2], [0, 3], [0, 4], [0, 5], [0, 6], [1, 7], [2, 7], [2, 8], [3, 8], [4, 9], [5, 9], [5, 10], [4, 10], [6, 11], [1, 11], [0, 7], [0, 8], [0, 9], [0, 10], [0, 11]]; return (
{edges.map(([a, b], i) => { const A = nodes[a],B = nodes[b]; return ; })} {nodes.map((n, i) => {i === 0 && } {i > 0 && i < 7 && } )} M.D DEUTSCH
Guten Tag · A1
Ich lerne Deutsch
Goethe-ready
); } function Hero() { return (
Deutsch · Live · From Meerut

Learn German
the warm way.

Two sisters teaching the language they love. Live online classes, Hindi-friendly explanations, small batches — and the patience your first language teacher should have had.

Book a free trial See courses
Live courses
Max per batch
A1–A2
Goethe pathway
); } function Strip() { const items = ['Guten Tag', 'Ich lerne Deutsch', 'A1 · A2 · Goethe', 'Live, never recorded', 'Small batches', 'Hindi-friendly', 'Made in Meerut', 'Lernen wir zusammen']; const doubled = [...items, ...items]; return (
{doubled.map((t, i) => {t})}
); } Object.assign(window, { Nav, Hero, Strip }); // ===== src/products.jsx ===== // Products carousel + modal const { useState: useStateP, useRef: useRefP, useEffect: useEffectP } = React; function ProductCard({ p, onOpen }) { return (
onOpen(p)}>
{p.glyph}
{p.version}
{p.subtitle}

{p.name}

{p.tags.slice(0, 3).map((t) => {t})}

{p.short}

{p.stats[0].v} · {p.stats[0].l}
); } function ProductModal({ p, onClose, onEnquire }) { useBodyLock(!!p); useEffectP(() => { if (!p) return; const onKey = (e) => {if (e.key === 'Escape') onClose();}; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [p, onClose]); if (!p) return null; return (
e.stopPropagation()}>
{p.glyph}
{p.subtitle} · {p.version}

{p.name}

{p.tags.map((t) => {t})}

{p.long}

{p.stats.map((s, i) =>
{s.v}
{s.l}
)}
What's inside
    {p.features.map((f, i) =>
  • {f}
  • )}
); } function Products({ onOpen }) { const [tab, setTab] = useStateP('all'); const trackRef = useRefP(null); const filtered = tab === 'all' ? PRODUCTS : PRODUCTS.filter((p) => p.category.toLowerCase() === tab); const tabs = [ ['all', 'All'], ['course', 'Courses'], ['exam', 'Exam prep'], ['kids', 'Kids'], ['private', 'Private']]; const scroll = (dir) => { const el = trackRef.current; if (!el) return; const card = el.querySelector('.product-card'); const step = card ? card.offsetWidth + 24 : 360; const half = el.scrollWidth / 2; // If going backwards from the start, jump silently to the end of the first set if (dir < 0 && el.scrollLeft <= 1) { el.scrollTo({ left: half, behavior: 'auto' }); // next frame, smooth-step back requestAnimationFrame(() => el.scrollBy({ left: -step, behavior: 'smooth' })); return; } el.scrollBy({ left: dir * step, behavior: 'smooth' }); }; // Continuous infinite scroll — duplicate set + RAF translates the track // smoothly left→right. When scrollLeft crosses the duplicate boundary we // silently rewind, so the loop is seamless. const [paused, setPaused] = React.useState(false); React.useEffect(() => { const el = trackRef.current; if (!el) return; let raf = 0; let last = performance.now(); const SPEED = 70; // pixels per second const tick = (now) => { const dt = (now - last) / 1000; last = now; if (!paused) { el.scrollLeft += SPEED * dt; const half = el.scrollWidth / 2; if (half && el.scrollLeft >= half) el.scrollLeft -= half; } raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [paused, tab]); return (
Our courses

Six clear paths
from Namaste to Deutsch.

{tabs.map(([id, label]) => )}
setPaused(true)} onMouseLeave={() => setPaused(false)}>
{filtered.map((p) => )} {filtered.map((p) => )}
); } Object.assign(window, { Products, ProductModal }); // ===== src/about-results.jsx ===== // About + Results (stats) function About() { const pillars = [ { ico: 'spark', color: 'var(--c-yellow)', title: 'Live', desc: 'Every class is live. You speak, we listen, we correct. A language sticks only when it is used.' }, { ico: 'grid', color: 'var(--c-teal)', title: 'Small', desc: 'Maximum eight students per batch. You get real attention, not crowd-watching time.' }, { ico: 'book', color: 'var(--c-pink)', title: 'Hindi-first', desc: 'We explain hard grammar in the language you already think in. Switch to English when you are ready.' }, { ico: 'user', color: 'var(--c-blue)', title: 'Honest', desc: 'We are still learning German too. We teach the path we are walking — patient and real.' }]; return (
About Miss Deutsch

A classroom, a sketchbook, and a love for languages.

We grew up in Meerut, both became educators, and somewhere along the way fell in love with German — its rhythm, its precision, the doors it opens. Today we teach what we wish someone had taught us.

Vaishnavi brings a teacher's structure and is herself working toward B2. Shivangi brings a debater's clarity and an artist's eye. Together that is Miss Deutsch — a small, warm classroom for serious learners.

Book a free trial Read the journal
{pillars.map((p) =>

{p.title}

{p.desc}

)}
); } function Results() { const palette = ['var(--accent)', 'var(--c-teal)', 'var(--c-pink)', 'var(--c-yellow)']; return (
Our promise

Small numbers, real attention.

A snapshot of how we run Miss Deutsch — fewer students per class, more time on every learner.

{STATS.map((s, i) =>
{s.l}
)}
); } Object.assign(window, { About, Results }); // ===== src/programs-faculty.jsx ===== // Programs + Faculty function Programs({ onEnquire, user, onOpenDashboard }) { const dotColor = { 'Group Class': 'var(--c-yellow)', Sprint: 'var(--c-pink)', Junior: 'var(--c-violet)', Private: 'var(--c-lime)' }; const [grantedIds, setGrantedIds] = React.useState(new Set()); const [moduleCounts, setModuleCounts] = React.useState({}); React.useEffect(() => { const client = getSupa && getSupa(); if (!client || !user) { setGrantedIds(new Set()); setModuleCounts({}); return; } (async () => { const { data: gs } = await client.from('access_grants') .select('item_id, item_kind').eq('status', 'active').eq('item_kind', 'course'); const ids = new Set((gs || []).map((g) => g.item_id)); setGrantedIds(ids); if (ids.size === 0) return; const { data: rows } = await client.from('course_content') .select('item_id').eq('item_kind', 'course').in('item_id', Array.from(ids)); const counts = {}; (rows || []).forEach((r) => { counts[r.item_id] = (counts[r.item_id] || 0) + 1; }); setModuleCounts(counts); })(); }, [user]); return (
Class formats

Pick a class
that fits your week.

Talk to us
{PROGRAMS.map((p) => { const granted = grantedIds.has(p.id); const count = moduleCounts[p.id] || 0; return (
{p.cat} {p.duration}

{p.title}

{p.desc}

{granted && ( {count > 0 ? `${count} module${count === 1 ? '' : 's'} available` : 'Enrolled · content coming'} )}
{p.price}· {p.seats} {granted ? ( ) : ( )}
); })}
); } function Faculty() { const tagColors = ['var(--c-teal)', 'var(--c-pink)', 'var(--c-yellow)', 'var(--c-blue)', 'var(--c-lime)', 'var(--c-violet)']; return (
Meet your teachers

The sisters behind Miss Deutsch.

Two educators from Meerut. Patient, opinionated about teaching, and walking the same German path you are about to start.

{FACULTY.map((f, i) =>
{f.img ? {f.name} :
{f.glyph}
}

{f.name}

{f.role}

{f.tags.map((t, j) => {t} )}
)}
); } Object.assign(window, { Programs, Faculty }); // ===== src/testimonials-blog.jsx ===== // Testimonials + Blog function Testimonials() { const [items, setItems] = React.useState(TESTIMONIALS); React.useEffect(() => { const cfg = window.__SBDA_CONFIG; if (!cfg) return; fetch(`${cfg.supabase_url}/rest/v1/testimonials?select=*&featured=eq.true&order=sort_order.asc,created_at.desc`, { headers: { apikey: cfg.supabase_anon_key, Authorization: `Bearer ${cfg.supabase_anon_key}` }, }) .then((r) => r.ok ? r.json() : []) .then((rows) => { if (Array.isArray(rows) && rows.length > 0) { setItems(rows.map((r) => ({ quote: r.quote, name: r.name, role: r.role || '', av: r.avatar_text || (r.name || '?').split(' ').map((p) => p[0]).join('').slice(0, 2).toUpperCase(), }))); } }) .catch(() => {}); }, []); return (
Student stories

Words from learners who walked the path.

{items.map((t, i) =>
{t.quote}
{t.av}
{t.name}
{t.role}
)}
); } function Blog() { const [items, setItems] = React.useState(POSTS); React.useEffect(() => { const cfg = window.__SBDA_CONFIG; if (!cfg) return; fetch(`${cfg.supabase_url}/rest/v1/posts?select=*&published=eq.true&order=featured.desc,published_at.desc&limit=6`, { headers: { apikey: cfg.supabase_anon_key, Authorization: `Bearer ${cfg.supabase_anon_key}` }, }) .then((r) => r.ok ? r.json() : []) .then((rows) => { if (Array.isArray(rows) && rows.length > 0) { setItems(rows.map((r) => ({ id: r.id, cat: r.category, date: new Date(r.published_at || r.created_at).toLocaleDateString('en-IN', { month: 'short', day: '2-digit', year: 'numeric' }), title: r.title, excerpt: r.excerpt, color: r.color || 'var(--c-yellow)', featured: !!r.featured, cover_url: r.cover_url, }))); } }) .catch(() => {}); }, []); return (
Journal · free reads

From our classroom notebook.

e.preventDefault()}>All posts
{items.map((p) =>
{!p.cover_url && (
{p.cat}
)}
{p.cat} {p.date}

{p.title}

{p.excerpt}

)}
); } Object.assign(window, { Testimonials, Blog }); // ===== src/contact-footer.jsx ===== // Contact form (with validation) + Footer // Submits directly to Supabase using the anon key (no Pages Function needed). const { useState: useStateCF, useEffect: useEffectCF } = React; function getSupaAnon() { const cfg = window.__SBDA_CONFIG; if (!cfg || !cfg.supabase_url || !cfg.supabase_anon_key) return null; return { url: cfg.supabase_url, key: cfg.supabase_anon_key }; } const INTEREST_OPTIONS = [ 'Free trial class', 'A1 Starter — A1.1', 'A1 Confident — A1.2', 'A2 Builder', 'Junior Deutsch (kids)', 'Goethe / TELC Exam Sprint', 'One-on-One Coaching', 'General question', 'Other', ]; const WHATSAPP_NUMBER = '919999999999'; // TODO: replace with the real WhatsApp number function Contact({ initialInterest }) { const [form, setForm] = useStateCF({ name: '', email: '', phone: '', interest: initialInterest || 'Free trial class', message: '' }); useEffectCF(() => { if (initialInterest) setForm((f) => ({ ...f, interest: initialInterest })); }, [initialInterest]); const [errors, setErrors] = useStateCF({}); const [sent, setSent] = useStateCF(false); const [busy, setBusy] = useStateCF(false); const validate = () => { const e = {}; if (!form.name.trim()) e.name = 'Please enter your name'; if (!form.email.trim()) e.email = 'We need an email to reply'; else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) e.email = 'Hmm, that email looks off'; if (!form.phone.trim() || form.phone.trim().length < 7) e.phone = 'A reachable phone number please'; if (!form.message.trim() || form.message.trim().length < 8) e.message = 'A short note would help us prepare'; return e; }; const openWhatsApp = (data) => { const lines = [ `Hallo Miss Deutsch!`, `I would like to: ${data.interest}`, `Name: ${data.name}`, `Email: ${data.email}`, `Phone: ${data.phone}`, data.message ? `Note: ${data.message}` : null, ].filter(Boolean); const url = `https://wa.me/${WHATSAPP_NUMBER}?text=${encodeURIComponent(lines.join('\n'))}`; window.open(url, '_blank'); }; const onSubmit = async (ev) => { ev.preventDefault(); const e = validate(); setErrors(e); if (Object.keys(e).length > 0) return; setBusy(true); try { const cfg = getSupaAnon(); if (!cfg) throw new Error('Service not ready — please try again in a moment.'); const payload = { name: form.name.trim(), email: form.email.trim().toLowerCase(), phone: form.phone.trim(), interest: form.interest, message: form.message.trim(), source: 'website', }; const res = await fetch(`${cfg.url}/rest/v1/leads`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': cfg.key, 'Authorization': `Bearer ${cfg.key}`, 'Prefer': 'return=minimal', }, body: JSON.stringify(payload), }); if (!res.ok) { const detail = await res.json().catch(() => ({})); throw new Error(detail.message || detail.error || 'Could not save your request.'); } setSent(true); openWhatsApp(payload); setTimeout(() => { setSent(false); setForm({ name: '', email: '', phone: '', interest: 'Free trial class', message: '' }); }, 5000); } catch (err) { setErrors({ message: err.message || 'Network error — please try again.' }); } finally { setBusy(false); } }; const update = (k) => (ev) => setForm({ ...form, [k]: ev.target.value }); return (
Book a free trial

One free class.
Zero pressure.

Tell us a little about yourself and we will send a slot for a free 30-minute trial. We reply within a day. Faster? DM us on Instagram.

Email
hello@missdeutsch.in
WhatsApp
+91 99999 99999
Where we are
Meerut, Uttar Pradesh · Classes live online across India
Instagram
{sent &&
Thanks — opening WhatsApp so we can confirm your slot.
}
{errors.name && {errors.name}}
{errors.email && {errors.email}}
{errors.phone && {errors.phone}}
{errors.message && {errors.message}}
); } function Footer() { return ( ); } Object.assign(window, { Contact, Footer }); // ===== src/login-dashboard.jsx ===== // Login modal + Dashboard view — wired to Supabase auth const { useState: useStateLD, useEffect: useEffectLD } = React; let _supa = null; function getSupa() { if (_supa) return _supa; const cfg = window.__SBDA_CONFIG; if (!cfg || !cfg.supabase_url || !cfg.supabase_anon_key) return null; _supa = window.supabase.createClient(cfg.supabase_url, cfg.supabase_anon_key, { auth: { persistSession: true, autoRefreshToken: true }, }); return _supa; } function LoginModal({ open, onClose, onAuth }) { const [mode, setMode] = useStateLD('login'); const [form, setForm] = useStateLD({ email: '', password: '', name: '' }); const [error, setError] = useStateLD(''); const [busy, setBusy] = useStateLD(false); useBodyLock(open); if (!open) return null; const submit = async (e) => { e.preventDefault(); setError(''); if (!form.email.includes('@')) { setError('Please enter a valid email'); return; } if (form.password.length < 6) { setError('Password must be at least 6 characters'); return; } if (mode === 'signup' && !form.name.trim()) { setError('Please enter your name'); return; } const client = getSupa(); if (!client) { setError('Auth service is not configured yet.'); return; } setBusy(true); try { if (mode === 'login') { const { data, error: err } = await client.auth.signInWithPassword({ email: form.email, password: form.password }); if (err) throw err; const { data: profile } = await client.from('profiles').select('full_name, role').eq('id', data.user.id).single(); onAuth({ id: data.user.id, name: profile?.full_name || form.email.split('@')[0], email: data.user.email, role: profile?.role || 'researcher' }); } else { const { data, error: err } = await client.auth.signUp({ email: form.email, password: form.password, options: { data: { full_name: form.name.trim() } }, }); if (err) throw err; if (data.session) { onAuth({ id: data.user.id, name: form.name.trim(), email: data.user.email, role: 'researcher' }); } else { setError('Account created — please check your email to confirm, then log in.'); setMode('login'); } } } catch (err) { setError(err.message || 'Something went wrong. Please try again.'); } finally { setBusy(false); } }; return (
e.stopPropagation()}>
M

{mode === 'login' ? 'Welcome back.' : 'Join Miss Deutsch.'}

{mode === 'login' ? 'Access your enrolled courses, lessons, and class materials.' : 'Create a learner account to access your courses and class recordings.'}

{mode === 'signup' &&
setForm({ ...form, name: e.target.value })} placeholder="Your name" />
}
setForm({ ...form, email: e.target.value })} placeholder="you@institution.edu" />
setForm({ ...form, password: e.target.value })} placeholder="••••••••" />
{error && {error}}
{mode === 'login' ? <>New to Miss Deutsch? { e.preventDefault(); setMode('signup'); setError(''); }} style={{ color: 'var(--accent)' }}>Create an account : <>Already have an account? { e.preventDefault(); setMode('login'); setError(''); }} style={{ color: 'var(--accent)' }}>Log in }
); } const PRODUCT_COLORS_D = { virdb: 'var(--c-teal)', virdb2: 'var(--c-pink)', net2align: 'var(--c-blue)', plurimet: 'var(--c-violet)', tfis: 'var(--c-yellow)', hepnet: 'var(--c-lime)' }; function MaterialItem({ c }) { const kind = c.file_url ? 'file' : c.link_url ? 'link' : 'text'; const icon = kind === 'file' ? : kind === 'link' ? : ; return (
{icon}
{c.title} {kind === 'file' ? 'File' : kind === 'link' ? 'Link' : 'Note'}
{c.body &&

{c.body}

} {c.file_url && ( Download {c.file_name ? `· ${c.file_name}` : 'file'} )} {c.link_url && ( Open link )}
); } function GrantCard({ g, defaultOpen }) { const [open, setOpen] = useStateLD(!!defaultOpen); const [contents, setContents] = useStateLD(null); const [loading, setLoading] = useStateLD(false); const load = async () => { setLoading(true); const client = getSupa(); if (client) { const { data } = await client.from('course_content').select('*') .eq('item_kind', g.item_kind).eq('item_id', g.item_id) .order('sort_order').order('created_at'); setContents(data || []); } setLoading(false); }; useEffectLD(() => { if (open && contents === null) load(); }, [open]); const toggle = () => setOpen((v) => !v); const glyphBg = PRODUCT_COLORS_D[g.item_id] || 'var(--accent)'; return (
{g.item_label[0]}
{g.item_label}
{g.item_kind} Granted {new Date(g.granted_at).toLocaleDateString()}
{contents !== null && ( {contents.length} {contents.length === 1 ? 'item' : 'items'} )}
{open && (
{loading &&

Loading content…

} {!loading && contents && contents.length === 0 && (

No materials published yet — the admin will upload soon.

)} {!loading && contents && contents.map((c) => )}
)}
); } function AccountEditor({ user, onUpdated }) { const [fullName, setFullName] = useStateLD(user?.name || ''); const [newsletter, setNewsletter] = useStateLD(false); const [busy, setBusy] = useStateLD(false); const [savedAt, setSavedAt] = useStateLD(null); const [err, setErr] = useStateLD(''); useEffectLD(() => { const client = getSupa(); if (!client || !user) return; client.from('profiles').select('newsletter_optin').eq('id', user.id).single() .then(({ data }) => { if (data) setNewsletter(!!data.newsletter_optin); }); }, [user]); const save = async (e) => { e.preventDefault(); const client = getSupa(); if (!client || !user) return; setBusy(true); setErr(''); const { error } = await client.from('profiles') .update({ full_name: fullName.trim(), newsletter_optin: newsletter }) .eq('id', user.id); setBusy(false); if (error) { setErr(error.message); return; } setSavedAt(Date.now()); onUpdated && onUpdated({ ...user, name: fullName.trim() }); setTimeout(() => setSavedAt(null), 2500); }; return (
setFullName(e.target.value)} placeholder="Your name" />
{err &&
{err}
}
{savedAt && ✓ Saved}
); } function Dashboard({ user, onBack, onLogout, onUserUpdate }) { const [tab, setTab] = useStateLD('overview'); const [grants, setGrants] = useStateLD([]); const [loadingGrants, setLoadingGrants] = useStateLD(true); const [materialCount, setMaterialCount] = useStateLD(0); const initial = user?.name ? user.name[0].toUpperCase() : 'R'; useEffectLD(() => { const client = getSupa(); if (!client || !user) { setLoadingGrants(false); return; } client.from('access_grants').select('*').eq('status', 'active').order('granted_at', { ascending: false }) .then(({ data }) => { setGrants(data || []); setLoadingGrants(false); }); client.from('course_content').select('id', { count: 'exact', head: true }) .then(({ count }) => { setMaterialCount(count || 0); }); }, [user]); const handleLogout = async () => { const client = getSupa(); if (client) await client.auth.signOut(); onLogout(); }; const productGrants = grants.filter((g) => g.item_kind === 'product'); const courseGrants = grants.filter((g) => g.item_kind === 'course'); const visibleGrants = tab === 'products' ? productGrants : tab === 'courses' ? courseGrants : grants; const lastGranted = grants[0]?.granted_at ? new Date(grants[0].granted_at).toLocaleDateString() : '—'; const grantsBlock = loadingGrants ?

Loading…

: visibleGrants.length === 0 ?

No active grants yet

Reach out via the contact form to request access to a product or enrol in a program.

:
{visibleGrants.map((g, i) => )}
; return (
{initial}
{user?.role || 'researcher'}
Welcome back, {user?.name?.split(' ')[0] || 'Researcher'}.
{user?.email || ''}
{loadingGrants ? '—' : grants.length}
Active grants
{loadingGrants ? '—' : productGrants.length}
Products
{loadingGrants ? '—' : courseGrants.length}
Courses
{lastGranted}
Last granted
{tab}

{tab === 'overview' && 'Your library'} {tab === 'products' && 'Your products'} {tab === 'courses' && 'Your courses'} {tab === 'account' && 'Account settings'}

{tab === 'overview' && 'All your granted products and courses. Expand any card to read notes, download files, or open external links uploaded by the admin.'} {tab === 'products' && 'Tools and databases you have access to.'} {tab === 'courses' && 'Programs and courses you are enrolled in.'} {tab === 'account' && 'Update your name and email preferences.'}

{(tab === 'overview' || tab === 'products' || tab === 'courses') && grantsBlock} {tab === 'account' && }
); } Object.assign(window, { LoginModal, Dashboard }); // ===== src/app.jsx ===== // Root App — composes everything, manages global state, wires Tweaks const { useState: useStateApp, useEffect: useEffectApp } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "light", "accent": "#d4453a", "showStrip": true, "density": "comfortable" } /*EDITMODE-END*/; function App() { const [view, setView] = useStateApp('home'); // 'home' | 'dashboard' const [user, setUser] = useStateApp(null); const [loginOpen, setLoginOpen] = useStateApp(false); const [modalProduct, setModalProduct] = useStateApp(null); const [enquiryInterest, setEnquiryInterest] = useStateApp(null); const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const handleEnquire = (interest) => { setEnquiryInterest(interest); setView('home'); setTimeout(() => { const el = document.getElementById('contact'); if (el) el.scrollIntoView({ behavior: 'smooth' }); }, 50); }; // Restore Supabase session on mount (after config loads) useEffectApp(() => { const restore = async () => { const client = getSupa(); if (!client) return; const { data } = await client.auth.getSession(); if (data?.session?.user) { const u = data.session.user; const { data: profile } = await client.from('profiles').select('full_name, role').eq('id', u.id).single(); setUser({ id: u.id, name: profile?.full_name || u.email.split('@')[0], email: u.email, role: profile?.role || 'researcher' }); } }; if (window.__SBDA_CONFIG) { restore(); return; } const fn = () => restore(); window.addEventListener('sbda-config', fn); return () => window.removeEventListener('sbda-config', fn); }, []); // Apply theme + accent useEffectApp(() => { applyTheme(t.theme); document.documentElement.style.setProperty('--accent', t.accent); document.documentElement.style.setProperty('--accent-2', shade(t.accent, 12)); }, [t.theme, t.accent]); // Density tweak — adjusts section padding useEffectApp(() => { document.documentElement.style.setProperty('--section-pad', t.density === 'compact' ? '80px' : '120px'); }, [t.density]); const handleAuth = (u) => { setUser(u); setLoginOpen(false); setView('dashboard'); }; const handleLogout = () => { setUser(null); setView('home'); }; return ( <>