#!/usr/bin/env node // Führe aus: node setup_auth.js // Im Verzeichnis: /Users/tim/Documents/GitTea/Language import { writeFileSync, mkdirSync } from 'fs' import { join } from 'path' const ROOT = '/Users/tim/Documents/GitTea/Language' const files = { // ─── .env (falls noch nicht vorhanden) ─────────────────── '.env': `VITE_DIRECTUS_URL=https://db.hejyou.com VITE_DIRECTUS_TOKEN=j6YyjhoFidnU3cI3MSrcgTXqO3t2wbZG `, // ─── API Service ───────────────────────────────────────── 'src/api/directus.js': `const BASE = import.meta.env.VITE_DIRECTUS_URL const TOKEN = import.meta.env.VITE_DIRECTUS_TOKEN const headers = { 'Content-Type': 'application/json', 'Authorization': \`Bearer \${TOKEN}\`, } export async function login(email, password) { const res = await fetch(\`\${BASE}/auth/login\`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }) const data = await res.json() if (!res.ok) throw new Error(data.errors?.[0]?.message || 'Login fehlgeschlagen.') return data.data } export async function getMe(userToken) { const res = await fetch( \`\${BASE}/users/me?fields=id,username,language_native,language_target\`, { headers: { 'Authorization': \`Bearer \${userToken}\` } } ) const data = await res.json() if (!res.ok) throw new Error('Profil konnte nicht geladen werden.') return data.data } export async function registerUser(email, password) { const res = await fetch(\`\${BASE}/users\`, { method: 'POST', headers, body: JSON.stringify({ email, password }), }) const data = await res.json() if (!res.ok) throw new Error(data.errors?.[0]?.message || 'Registrierung fehlgeschlagen.') return data.data } export async function checkUsername(username) { const clean = username.toLowerCase().replace(/[^a-z0-9_]/g, '') const res = await fetch( \`\${BASE}/items/users_language?filter[username_lowercases][_eq]=\${encodeURIComponent(clean)}&fields=id&limit=1\`, { headers } ) const data = await res.json() return data.data?.length === 0 } export async function createProfile({ userId, username, nativeLang, targetLang, userToken }) { const clean = username.toLowerCase().replace(/[^a-z0-9_]/g, '') const authHeaders = userToken ? { 'Content-Type': 'application/json', 'Authorization': \`Bearer \${userToken}\` } : headers const profileRes = await fetch(\`\${BASE}/items/users_language\`, { method: 'POST', headers, body: JSON.stringify({ username_public: username, username_lowercases: clean, status: 'published' }), }) const profileData = await profileRes.json() if (!profileRes.ok) throw new Error(profileData.errors?.[0]?.message || 'Profil konnte nicht erstellt werden.') const profileId = profileData.data.id await fetch(\`\${BASE}/users/\${userId}\`, { method: 'PATCH', headers: authHeaders, body: JSON.stringify({ username: profileId, language_native: nativeLang, language_target: targetLang }), }) await fetch(\`\${BASE}/items/users_language/\${profileId}\`, { method: 'PATCH', headers, body: JSON.stringify({ user: userId }), }) await fetch(\`\${BASE}/items/learning_pairs\`, { method: 'POST', headers, body: JSON.stringify({ user: profileId, language_from: nativeLang, language_to: targetLang, active: true, current_level: 1, points: 0, }), }) return profileId } export const LANGUAGE_OPTIONS = [ { id: '88053026-3d7e-4799-b10d-67187f7c1709', label: 'Deutsch', flag: '🇩🇪' }, { id: '99fbaa9d-3cac-48cb-a5e2-dcb320e913e4', label: 'Englisch', flag: '🇬🇧' }, { id: '25350b32-e9ab-4fec-946e-c0f11eff70dd', label: 'Schwedisch', flag: '🇸🇪' }, ] `, // ─── Auth Context ───────────────────────────────────────── 'src/context/AuthContext.jsx': `import { createContext, useContext, useState, useEffect } from 'react' import { getMe } from '../api/directus' const AuthContext = createContext(null) export function AuthProvider({ children }) { const [token, setToken] = useState(() => localStorage.getItem('hejyou_token')) const [user, setUser] = useState(null) const [loading, setLoading] = useState(true) useEffect(() => { if (!token) { setLoading(false); return } getMe(token) .then(setUser) .catch(() => { localStorage.removeItem('hejyou_token'); setToken(null) }) .finally(() => setLoading(false)) }, [token]) const saveToken = (t) => { localStorage.setItem('hejyou_token', t) setToken(t) } const logout = () => { localStorage.removeItem('hejyou_token') setToken(null) setUser(null) } return ( {children} ) } export const useAuth = () => useContext(AuthContext) `, // ─── UI Components ──────────────────────────────────────── 'src/components/auth/ui.jsx': `import { useState } from 'react' import styles from './auth.module.css' export function FormGroup({ label, children }) { return (
{label && } {children}
) } export function Input({ className, ...props }) { return } export function Select({ children, ...props }) { return (
) } export function Button({ loading, children, ...props }) { return ( ) } export function Alert({ message }) { if (!message) return null return
{message}
} export function StepDots({ current, total }) { return (
{Array.from({ length: total }).map((_, i) => (
))} Schritt {current + 1} von {total}
) } `, // ─── Auth CSS Module ────────────────────────────────────── 'src/components/auth/auth.module.css': `:root { --bg: #F5F0E8; --surface: #FFFCF7; --border: #E2DAD0; --text: #2C2520; --muted: #9A8F85; --accent: #5C7A5E; --accent-lt: #EAF0EA; --danger: #C0544A; --danger-lt: #FBF0EF; --radius: 14px; } .formGroup { margin-bottom: 16px; } .label { display: block; font-size: 11px; font-weight: 500; color: var(--muted); letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 6px; } .input { width: 100%; padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); font-family: 'DM Sans', sans-serif; font-size: 15px; color: var(--text); outline: none; transition: border-color 0.2s, box-shadow 0.2s; appearance: none; -webkit-appearance: none; } .input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(92,122,94,0.12); background: var(--surface); } .selectWrap { position: relative; } .selectArrow { position: absolute; right: 14px; top: 50%; transform: translateY(-50%); width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid var(--muted); pointer-events: none; } .selectWrap .input { padding-right: 36px; cursor: pointer; } .btn { width: 100%; padding: 13px; margin-top: 8px; background: var(--accent); color: #fff; border: none; border-radius: var(--radius); font-family: 'DM Sans', sans-serif; font-size: 15px; font-weight: 500; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: background 0.2s, transform 0.1s; } .btn:hover { background: #4a6650; } .btn:active { transform: scale(0.98); } .btn:disabled { background: var(--border); color: var(--muted); cursor: not-allowed; } .spinner { width: 14px; height: 14px; border: 2px solid rgba(255,255,255,0.35); border-top-color: #fff; border-radius: 50%; animation: spin 0.7s linear infinite; } .alert { background: var(--danger-lt); border: 1px solid #EBCBC8; border-radius: var(--radius); padding: 10px 14px; font-size: 13px; color: var(--danger); margin-bottom: 16px; } .stepDots { display: flex; align-items: center; gap: 6px; margin-bottom: 24px; } .stepDot { height: 6px; border-radius: 3px; transition: all 0.25s ease; } .stepLabel { font-size: 11px; color: var(--muted); margin-left: 4px; } @keyframes spin { to { transform: rotate(360deg); } } `, // ─── Login Form ─────────────────────────────────────────── 'src/components/auth/LoginForm.jsx': `import { useState } from 'react' import { login, getMe } from '../../api/directus' import { useAuth } from '../../context/AuthContext' import { FormGroup, Input, Button, Alert } from './ui' export default function LoginForm({ onNeedsProfile }) { const { saveToken, setUser } = useAuth() const [email, setEmail] = useState('') const [pw, setPw] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) const handleSubmit = async (e) => { e?.preventDefault() if (!email || !pw) { setError('Bitte E-Mail und Passwort eingeben.'); return } setError('') setLoading(true) try { const { access_token } = await login(email, pw) saveToken(access_token) const me = await getMe(access_token) setUser(me) if (!me.username || !me.language_native || !me.language_target) { onNeedsProfile(me.id, access_token) } } catch (err) { setError(err.message) } finally { setLoading(false) } } return (
setEmail(e.target.value)} autoComplete="email" autoFocus /> setPw(e.target.value)} autoComplete="current-password" /> ) } `, // ─── Register Step 1 ────────────────────────────────────── 'src/components/auth/RegisterStep1.jsx': `import { useState } from 'react' import { registerUser, login } from '../../api/directus' import { useAuth } from '../../context/AuthContext' import { FormGroup, Input, Button, Alert, StepDots } from './ui' export default function RegisterStep1({ onSuccess }) { const { saveToken } = useAuth() const [email, setEmail] = useState('') const [pw, setPw] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) const handleSubmit = async (e) => { e?.preventDefault() if (!email || !pw) { setError('Bitte alle Felder ausfüllen.'); return } if (pw.length < 8) { setError('Passwort muss mindestens 8 Zeichen haben.'); return } setError('') setLoading(true) try { const newUser = await registerUser(email, pw) const { access_token } = await login(email, pw) saveToken(access_token) onSuccess(newUser.id, access_token) } catch (err) { setError(err.message) } finally { setLoading(false) } } return (
setEmail(e.target.value)} autoComplete="email" autoFocus /> setPw(e.target.value)} autoComplete="new-password" /> ) } `, // ─── Register Step 2 ────────────────────────────────────── 'src/components/auth/RegisterStep2.jsx': `import { useState, useRef, useCallback } from 'react' import { checkUsername, createProfile, LANGUAGE_OPTIONS } from '../../api/directus' import { useAuth } from '../../context/AuthContext' import { FormGroup, Input, Select, Button, Alert, StepDots } from './ui' import styles from './auth.module.css' export default function RegisterStep2({ userId, userToken, onSuccess }) { const { setUser } = useAuth() const [username, setUsername] = useState('') const [nativeLang, setNativeLang] = useState('') const [targetLang, setTargetLang] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) const [usernameState, setUsernameState] = useState('idle') const debounceRef = useRef(null) const handleUsernameChange = useCallback((val) => { setUsername(val) setUsernameState('idle') clearTimeout(debounceRef.current) if (val.length < 3) return setUsernameState('checking') debounceRef.current = setTimeout(async () => { try { const available = await checkUsername(val) setUsernameState(available ? 'available' : 'taken') } catch { setUsernameState('idle') } }, 450) }, []) const handleSubmit = async (e) => { e?.preventDefault() if (!username) { setError('Bitte einen Username wählen.'); return } if (usernameState !== 'available') { setError('Bitte einen verfügbaren Username wählen.'); return } if (!nativeLang) { setError('Bitte Muttersprache wählen.'); return } if (!targetLang) { setError('Bitte Zielsprache wählen.'); return } if (nativeLang === targetLang) { setError('Mutter- und Zielsprache dürfen nicht gleich sein.'); return } setError('') setLoading(true) try { await createProfile({ userId, username, nativeLang, targetLang, userToken }) setUser(prev => ({ ...prev, username: true, language_native: nativeLang, language_target: targetLang })) onSuccess(username) } catch (err) { setError(err.message) } finally { setLoading(false) } } const statusColor = { idle: 'var(--muted)', checking: 'var(--muted)', available: 'var(--accent)', taken: 'var(--danger)' } const statusText = { idle: '', checking: '…', available: '✓ verfügbar', taken: '✕ vergeben' } return (
handleUsernameChange(e.target.value)} autoComplete="off" autoFocus style={{ paddingRight: '110px' }} /> {usernameState !== 'idle' && ( {statusText[usernameState]} )}
) } `, // ─── Auth Screen ────────────────────────────────────────── 'src/components/auth/AuthScreen.jsx': `import { useState } from 'react' import LoginForm from './LoginForm' import RegisterStep1 from './RegisterStep1' import RegisterStep2 from './RegisterStep2' import styles from './AuthScreen.module.css' function Brand() { return (

HejYou

Sprachen lernen wie ein Kind

) } function ModeToggle({ mode, onChange }) { return (
{['login', 'register'].map(m => ( ))}
) } function SuccessScreen({ username }) { return (
Willkommen{username ? \`, \${username}\` : ''}!

Dein Abenteuer beginnt jetzt.

) } export default function AuthScreen() { const [mode, setMode] = useState(() => localStorage.getItem('hejyou_last_mode') || 'login') const [step, setStep] = useState('main') const [pendingUserId, setPendingUserId] = useState(null) const [pendingToken, setPendingToken] = useState(null) const [successName, setSuccessName] = useState('') const [showToggle, setShowToggle] = useState(true) const handleModeChange = (m) => { setMode(m) localStorage.setItem('hejyou_last_mode', m) setStep('main') setShowToggle(true) } const handleNeedsProfile = (userId, token) => { setPendingUserId(userId); setPendingToken(token) setShowToggle(false); setStep('profile') } return (
{showToggle && step === 'main' && } {step === 'main' && mode === 'login' && } {step === 'main' && mode === 'register' && handleNeedsProfile(id, t)} />} {step === 'profile' && { setSuccessName(name); setStep('success') }} />} {step === 'success' && }
) } `, // ─── AuthScreen CSS Module ──────────────────────────────── 'src/components/auth/AuthScreen.module.css': `.page { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; background: var(--bg, #F5F0E8); } .card { background: var(--surface, #FFFCF7); border: 1px solid var(--border, #E2DAD0); border-radius: 24px; padding: 48px 44px; width: 100%; max-width: 420px; box-shadow: 0 2px 40px rgba(44,37,32,0.06); animation: fadeUp 0.3s ease; } .brand { text-align: center; margin-bottom: 36px; } .brandMark { width: 48px; height: 48px; background: var(--accent, #5C7A5E); border-radius: 50%; margin: 0 auto 14px; display: flex; align-items: center; justify-content: center; } .brandTitle { font-family: 'Lora', serif; font-size: 22px; font-weight: 500; letter-spacing: -0.3px; color: var(--text, #2C2520); } .brandSub { font-size: 13px; color: var(--muted, #9A8F85); margin-top: 4px; } .toggle { display: flex; background: var(--bg, #F5F0E8); border: 1px solid var(--border, #E2DAD0); border-radius: 10px; padding: 3px; gap: 3px; margin-bottom: 32px; } .toggleBtn { flex: 1; padding: 8px; border: none; border-radius: 8px; background: transparent; font-family: 'DM Sans', sans-serif; font-size: 13px; font-weight: 500; color: var(--muted, #9A8F85); cursor: pointer; transition: all 0.2s; } .toggleBtnActive { background: var(--surface, #FFFCF7); color: var(--text, #2C2520); box-shadow: 0 1px 4px rgba(44,37,32,0.08); } .success { text-align: center; padding: 24px 0; } .successCheck { width: 52px; height: 52px; background: var(--accent-lt, #EAF0EA); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; } .successTitle { font-family: 'Lora', serif; font-size: 18px; display: block; margin-bottom: 8px; } .successSub { font-size: 14px; color: var(--muted, #9A8F85); } @keyframes fadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } } `, // ─── App.jsx (ersetzt bestehende) ───────────────────────── 'src/App.jsx': `import { AuthProvider, useAuth } from './context/AuthContext' import AuthScreen from './components/auth/AuthScreen' function AppContent() { const { user, loading, logout } = useAuth() if (loading) { return (
) } // Nicht eingeloggt oder Profil unvollständig → Auth Screen if (!user || !user.username || !user.language_native || !user.language_target) { return } // ─── Eingeloggt → hier kommt dein bestehender App-Content ── return (
{/* TODO: Deine bestehenden Pages/Routes hier einbauen */}

Eingeloggt ✓ — hier kommt der Feed.

) } export default function App() { return ( ) } `, } // Erstelle alle Verzeichnisse und schreibe Dateien const dirs = new Set() for (const path of Object.keys(files)) { const parts = path.split('/') for (let i = 1; i < parts.length; i++) { dirs.add(join(ROOT, ...parts.slice(0, i))) } } for (const dir of dirs) { try { mkdirSync(dir, { recursive: true }) } catch {} } let written = 0 for (const [path, content] of Object.entries(files)) { const fullPath = join(ROOT, path) // .env nur schreiben wenn noch nicht vorhanden if (path === '.env') { try { const existing = await import('fs').then(m => m.readFileSync(fullPath, 'utf8')) console.log('⏭ .env bereits vorhanden – übersprungen') continue } catch {} } writeFileSync(fullPath, content, 'utf8') console.log(`✓ ${path}`) written++ } console.log(\`\nFertig! \${written} Dateien geschrieben.\`) console.log('\nJetzt starten:') console.log(' npm install (falls neue Abhängigkeiten fehlen)') console.log(' npm run dev')