Profil: Begrüßung in Zielsprache, Kategorie-Punkte-Übersicht, ruhigerer Header (kein rotierender Avatar/Online-Dot), Notch-Fix und kompaktere Aktivitäts-Heatmap. Außerdem Capacitor-iOS-Projekt und diverse Auth/Feed/Audio-Verbesserungen aus dem Premium-Redesign. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
777 lines
25 KiB
JavaScript
777 lines
25 KiB
JavaScript
#!/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 (
|
||
<AuthContext.Provider value={{ token, user, setUser, saveToken, logout, loading }}>
|
||
{children}
|
||
</AuthContext.Provider>
|
||
)
|
||
}
|
||
|
||
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 (
|
||
<div className={styles.formGroup}>
|
||
{label && <label className={styles.label}>{label}</label>}
|
||
{children}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export function Input({ className, ...props }) {
|
||
return <input className={styles.input} {...props} />
|
||
}
|
||
|
||
export function Select({ children, ...props }) {
|
||
return (
|
||
<div className={styles.selectWrap}>
|
||
<select className={styles.input} {...props}>{children}</select>
|
||
<div className={styles.selectArrow} />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export function Button({ loading, children, ...props }) {
|
||
return (
|
||
<button className={styles.btn} {...props}>
|
||
{children}
|
||
{loading && <span className={styles.spinner} />}
|
||
</button>
|
||
)
|
||
}
|
||
|
||
export function Alert({ message }) {
|
||
if (!message) return null
|
||
return <div className={styles.alert}>{message}</div>
|
||
}
|
||
|
||
export function StepDots({ current, total }) {
|
||
return (
|
||
<div className={styles.stepDots}>
|
||
{Array.from({ length: total }).map((_, i) => (
|
||
<div
|
||
key={i}
|
||
className={styles.stepDot}
|
||
style={{ background: i === current ? 'var(--accent)' : 'var(--border)', width: i === current ? '18px' : '6px' }}
|
||
/>
|
||
))}
|
||
<span className={styles.stepLabel}>Schritt {current + 1} von {total}</span>
|
||
</div>
|
||
)
|
||
}
|
||
`,
|
||
|
||
// ─── 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 (
|
||
<form onSubmit={handleSubmit}>
|
||
<Alert message={error} />
|
||
<FormGroup label="E-Mail">
|
||
<Input type="email" placeholder="deine@email.de" value={email}
|
||
onChange={e => setEmail(e.target.value)} autoComplete="email" autoFocus />
|
||
</FormGroup>
|
||
<FormGroup label="Passwort">
|
||
<Input type="password" placeholder="••••••••" value={pw}
|
||
onChange={e => setPw(e.target.value)} autoComplete="current-password" />
|
||
</FormGroup>
|
||
<Button type="submit" loading={loading} disabled={loading}>
|
||
{loading ? 'Anmelden…' : 'Anmelden'}
|
||
</Button>
|
||
</form>
|
||
)
|
||
}
|
||
`,
|
||
|
||
// ─── 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 (
|
||
<form onSubmit={handleSubmit}>
|
||
<StepDots current={0} total={2} />
|
||
<Alert message={error} />
|
||
<FormGroup label="E-Mail">
|
||
<Input type="email" placeholder="deine@email.de" value={email}
|
||
onChange={e => setEmail(e.target.value)} autoComplete="email" autoFocus />
|
||
</FormGroup>
|
||
<FormGroup label="Passwort">
|
||
<Input type="password" placeholder="Mindestens 8 Zeichen" value={pw}
|
||
onChange={e => setPw(e.target.value)} autoComplete="new-password" />
|
||
</FormGroup>
|
||
<Button type="submit" loading={loading} disabled={loading}>
|
||
{loading ? 'Weiter…' : 'Weiter'}
|
||
</Button>
|
||
</form>
|
||
)
|
||
}
|
||
`,
|
||
|
||
// ─── 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 (
|
||
<form onSubmit={handleSubmit}>
|
||
<StepDots current={1} total={2} />
|
||
<Alert message={error} />
|
||
<FormGroup label="Username">
|
||
<div style={{ position: 'relative' }}>
|
||
<Input type="text" placeholder="dein_username" value={username}
|
||
onChange={e => handleUsernameChange(e.target.value)}
|
||
autoComplete="off" autoFocus style={{ paddingRight: '110px' }} />
|
||
{usernameState !== 'idle' && (
|
||
<span style={{
|
||
position: 'absolute', right: '12px', top: '50%',
|
||
transform: 'translateY(-50%)', fontSize: '12px', fontWeight: 500,
|
||
color: statusColor[usernameState], whiteSpace: 'nowrap',
|
||
}}>
|
||
{statusText[usernameState]}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</FormGroup>
|
||
<FormGroup label="Muttersprache">
|
||
<Select value={nativeLang} onChange={e => setNativeLang(e.target.value)}>
|
||
<option value="">Bitte wählen…</option>
|
||
{LANGUAGE_OPTIONS.map(l => <option key={l.id} value={l.id}>{l.flag} {l.label}</option>)}
|
||
</Select>
|
||
</FormGroup>
|
||
<FormGroup label="Ich möchte lernen">
|
||
<Select value={targetLang} onChange={e => setTargetLang(e.target.value)}>
|
||
<option value="">Bitte wählen…</option>
|
||
{LANGUAGE_OPTIONS.filter(l => l.id !== nativeLang).map(l => <option key={l.id} value={l.id}>{l.flag} {l.label}</option>)}
|
||
</Select>
|
||
</FormGroup>
|
||
<Button type="submit" loading={loading}
|
||
disabled={loading || usernameState === 'taken' || usernameState === 'checking'}>
|
||
{loading ? 'Speichere…' : 'Profil erstellen'}
|
||
</Button>
|
||
</form>
|
||
)
|
||
}
|
||
`,
|
||
|
||
// ─── 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 (
|
||
<div className={styles.brand}>
|
||
<div className={styles.brandMark}>
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round">
|
||
<circle cx="12" cy="12" r="10" />
|
||
<path d="M8 12q2-5 4-4t4 4-4 4-4-4" />
|
||
</svg>
|
||
</div>
|
||
<h1 className={styles.brandTitle}>HejYou</h1>
|
||
<p className={styles.brandSub}>Sprachen lernen wie ein Kind</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function ModeToggle({ mode, onChange }) {
|
||
return (
|
||
<div className={styles.toggle}>
|
||
{['login', 'register'].map(m => (
|
||
<button key={m} onClick={() => onChange(m)}
|
||
className={mode === m ? \`\${styles.toggleBtn} \${styles.toggleBtnActive}\` : styles.toggleBtn}>
|
||
{m === 'login' ? 'Anmelden' : 'Registrieren'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function SuccessScreen({ username }) {
|
||
return (
|
||
<div className={styles.success}>
|
||
<div className={styles.successCheck}>
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||
<polyline points="20 6 9 17 4 12" />
|
||
</svg>
|
||
</div>
|
||
<strong className={styles.successTitle}>Willkommen{username ? \`, \${username}\` : ''}!</strong>
|
||
<p className={styles.successSub}>Dein Abenteuer beginnt jetzt.</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
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 (
|
||
<div className={styles.page}>
|
||
<div className={styles.card}>
|
||
<Brand />
|
||
{showToggle && step === 'main' && <ModeToggle mode={mode} onChange={handleModeChange} />}
|
||
{step === 'main' && mode === 'login' && <LoginForm onNeedsProfile={handleNeedsProfile} />}
|
||
{step === 'main' && mode === 'register' && <RegisterStep1 onSuccess={(id, t) => handleNeedsProfile(id, t)} />}
|
||
{step === 'profile' && <RegisterStep2 userId={pendingUserId} userToken={pendingToken} onSuccess={(name) => { setSuccessName(name); setStep('success') }} />}
|
||
{step === 'success' && <SuccessScreen username={successName} />}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
`,
|
||
|
||
// ─── 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 (
|
||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||
<div style={{
|
||
width: '32px', height: '32px',
|
||
border: '2px solid #E2DAD0',
|
||
borderTopColor: '#5C7A5E',
|
||
borderRadius: '50%',
|
||
animation: 'spin 0.7s linear infinite',
|
||
}} />
|
||
<style>{\`@keyframes spin { to { transform: rotate(360deg); } }\`}</style>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Nicht eingeloggt oder Profil unvollständig → Auth Screen
|
||
if (!user || !user.username || !user.language_native || !user.language_target) {
|
||
return <AuthScreen />
|
||
}
|
||
|
||
// ─── Eingeloggt → hier kommt dein bestehender App-Content ──
|
||
return (
|
||
<div>
|
||
{/* TODO: Deine bestehenden Pages/Routes hier einbauen */}
|
||
<p style={{ padding: 32, fontFamily: 'sans-serif' }}>
|
||
Eingeloggt ✓ — hier kommt der Feed.
|
||
<button onClick={logout} style={{ marginLeft: 16, cursor: 'pointer' }}>Logout</button>
|
||
</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default function App() {
|
||
return (
|
||
<AuthProvider>
|
||
<AppContent />
|
||
</AuthProvider>
|
||
)
|
||
}
|
||
`,
|
||
}
|
||
|
||
// 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')
|