diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..33774c4 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(supabase --version)" + ] + } +} diff --git a/Weitere Ideen.txt b/Weitere Ideen.txt index 08993e6..c32f453 100644 --- a/Weitere Ideen.txt +++ b/Weitere Ideen.txt @@ -1,14 +1,21 @@ Offen -nere Landing Page Rangliste Übersetzen in gängigge Sprachen Englisch Italienisch Spanisch Portugisisch -Max Punkte bei Bewertung PAT Start, Pat2 und Pat3 + Admin Oberfläche (anzahl user mit Mail Adressen/ anelegten Tests / anbindung von Werbung Links und rechts von den Test Übersichten 2fa -Erledigt + + + +Erledigt: Final Speichern do Max Punkte bei Bewertung PAT1 +User Profil mit Land und Sprach Auswahl Termninal vercel deploy + +git add . +git commit -m "BenutzerProfil " +git push \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 4c1f143..35bbfd3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,6 +10,8 @@ import ProfileTab from './components/ProfileTab' import { useUserProfile } from './hooks/useUserProfile' import { getCountryLabel, getCountryOption } from './data/countries' import CountryFlag from './components/CountryFlag' +import { LanguageProvider } from './i18n/LanguageContext' +import { getTranslator } from './i18n/translations' const readShareToken = () => { if (typeof window === 'undefined') return '' @@ -44,6 +46,9 @@ function App() { deleteProfileData } = useUserProfile(session) + const language = profile?.language || 'de' + const t = getTranslator(language) + useEffect(() => { const root = document.documentElement if (theme === 'dark') { @@ -144,64 +149,80 @@ function App() { type="button" > {theme === 'dark' ? : } - {theme === 'dark' ? 'Light Mode' : 'Dark Mode'} + {theme === 'dark' ? t('nav.theme_light') : t('nav.theme_dark')} ) if (shareToken) { - return + return ( + + + + ) } if (!supabase) { return ( -
-
{renderThemeToggle()}
-
-

Supabase noch nicht konfiguriert

-

- Lege eine .env an (siehe .env.example) und trage - VITE_SUPABASE_URL sowie VITE_SUPABASE_ANON_KEY ein, um Login und - Registrierung zu aktivieren. -

-

- Anschließend Vite neu starten, damit die Variablen geladen werden. -

+ +
+
{renderThemeToggle()}
+
+

Supabase noch nicht konfiguriert

+

+ Lege eine .env an (siehe .env.example) und trage + VITE_SUPABASE_URL sowie VITE_SUPABASE_ANON_KEY ein, um Login und + Registrierung zu aktivieren. +

+

+ Anschließend Vite neu starten, damit die Variablen geladen werden. +

+
-
+ ) } if (loading) { return ( -
-
{renderThemeToggle()}
- Auth wird geladen... -
+ +
+
{renderThemeToggle()}
+ Auth wird geladen... +
+
) } if (!session) { if (showAuth) { - return setShowAuth(false)} /> + return ( + + setShowAuth(false)} /> + + ) } if (publicPath === '/datenschutz' || publicPath === '/impressum' || publicPath === '/kontakt') { return ( - setShowAuth(true)} - onNavigate={navigatePublic} - themeToggle={renderThemeToggle()} - /> + + setShowAuth(true)} + onNavigate={navigatePublic} + themeToggle={renderThemeToggle()} + /> + ) } return ( - setShowAuth(true)} - onNavigate={navigatePublic} - themeToggle={renderThemeToggle()} - /> + + setShowAuth(true)} + onNavigate={navigatePublic} + themeToggle={renderThemeToggle()} + /> + ) } @@ -213,99 +234,101 @@ function App() { const hasName = Boolean(userDisplayName) return ( -
-
-
-
-

Angemeldet

-
- {countryLabel && ( - - )} - {hasName ? userDisplayName : session?.user?.email} + +
+
+
+
+

{t('nav.logged_in')}

+
+ {countryLabel && ( + + )} + {hasName ? userDisplayName : session?.user?.email} +
+
+ + + +
+ {renderThemeToggle()} +
+
- - -
- {renderThemeToggle()} - -
-
-
- - {activeTab !== 'profile' && ( - - )} - - {activeTab === 'profile' && ( - - )} -
+ {activeTab === 'profile' && ( + + )} +
+ ) } diff --git a/src/components/Analysis/AnalysisTab.jsx b/src/components/Analysis/AnalysisTab.jsx index dab2e52..978d5b2 100644 --- a/src/components/Analysis/AnalysisTab.jsx +++ b/src/components/Analysis/AnalysisTab.jsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo } from 'react'; import { Activity, AlertTriangle, Brain, Target } from 'lucide-react'; import { buildAnalysis } from '../../utils/analysisEngine'; import { useTrainingPlans } from '../../hooks/useTrainingPlans'; +import { useTranslation } from '../../i18n/LanguageContext'; import GapBarChart from './GapBarChart'; import RadarChart from './RadarChart'; import StrengthList from './StrengthList'; @@ -16,6 +17,7 @@ export default function AnalysisTab({ patTypes, user }) { + const t = useTranslation(); const patTypeOptions = Object.keys(patTypes || {}); useEffect(() => { @@ -65,10 +67,10 @@ export default function AnalysisTab({
-

Analyse

-

Wo solltest du besser werden?

+

{t('analysis.title')}

+

{t('analysis.priority_title')}

- Priorisierung nach Gap × Faktor × Konstanz basierend auf den letzten 5 Assessments. + {t('analysis.priority_desc')}

@@ -96,18 +98,18 @@ export default function AnalysisTab({
- Assessments: {selectedPatAssessments.length} + {t('analysis.assessments_count', { count: selectedPatAssessments.length })} - Analysefenster: {analysis.windowedAssessments.length}/5 + {t('analysis.window', { count: analysis.windowedAssessments.length })} {analysis.isLimitedData && ( - Begrenzte Datenbasis + {t('analysis.limited_data')} )}
@@ -116,13 +118,13 @@ export default function AnalysisTab({ {!hasAssessments ? (
-

Noch keine Daten für die Analyse

-

Lege zuerst eine Bewertung im Bereich "Bewertungen" an.

+

{t('analysis.no_data')}

+

{t('analysis.no_data_hint')}

) : selectedPatAssessments.length === 0 ? (
-

Keine Bewertungen für {selectedPatType}

-

Wähle einen anderen PAT-Typ oder erstelle eine neue Bewertung.

+

{t('analysis.no_type_data', { patType: selectedPatType })}

+

{t('analysis.no_type_hint')}

) : ( <> diff --git a/src/components/Analysis/GapBarChart.jsx b/src/components/Analysis/GapBarChart.jsx index 6fc1cf1..4f32c42 100644 --- a/src/components/Analysis/GapBarChart.jsx +++ b/src/components/Analysis/GapBarChart.jsx @@ -1,9 +1,12 @@ import React from 'react'; +import { useTranslation } from '../../i18n/LanguageContext'; const truncate = (value, maxLength = 26) => value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value; export default function GapBarChart({ metrics = [] }) { + const t = useTranslation(); + const data = [...metrics] .sort((left, right) => right.priorityScore - left.priorityScore) .slice(0, 8); @@ -11,7 +14,7 @@ export default function GapBarChart({ metrics = [] }) { if (!data.length) { return (
- Kein Gap-Chart verfügbar. + {t('gapchart.no_data')}
); } @@ -29,8 +32,8 @@ export default function GapBarChart({ metrics = [] }) { return (
-

Gap-Bar-Chart (Priorität)

- +

{t('gapchart.title')}

+ {data.map((item, index) => { const y = topPad + index * rowHeight; const barWidth = (item.priorityScore / maxScore) * chartWidth; diff --git a/src/components/Analysis/StrengthList.jsx b/src/components/Analysis/StrengthList.jsx index dabc1bb..f7e0098 100644 --- a/src/components/Analysis/StrengthList.jsx +++ b/src/components/Analysis/StrengthList.jsx @@ -1,19 +1,22 @@ import React from 'react'; +import { useTranslation } from '../../i18n/LanguageContext'; const scoreToPercent = (score) => Math.round(Math.max(0, Math.min(1.5, score)) * 100); export default function StrengthList({ strengths = [] }) { + const t = useTranslation(); + if (!strengths.length) { return (
- Keine Stärken-Daten verfügbar. + {t('strength.no_data')}
); } return (
-

Top-3 Stärken

+

{t('strength.title')}

{strengths.map((item, index) => ( @@ -30,9 +33,9 @@ export default function StrengthList({ strengths = [] }) {
-

Ist: {item.actualMean.toFixed(2)}

-

Soll: {item.soll.toFixed(2)}

-

Trend: {item.trendDelta.toFixed(2)}

+

{t('strength.actual', { score: item.actualMean.toFixed(2) })}

+

{t('strength.target', { score: item.soll.toFixed(2) })}

+

{t('strength.trend', { score: item.trendDelta.toFixed(2) })}

))} diff --git a/src/components/Analysis/TrainingPlanPanel.jsx b/src/components/Analysis/TrainingPlanPanel.jsx index 2e9a9c0..25acf44 100644 --- a/src/components/Analysis/TrainingPlanPanel.jsx +++ b/src/components/Analysis/TrainingPlanPanel.jsx @@ -1,5 +1,6 @@ import React, { useMemo, useState } from 'react'; import { generateTrainingPlan } from '../../utils/trainingPlanGenerator'; +import { useTranslation } from '../../i18n/LanguageContext'; const formatDateTime = (value) => { if (!value) return '—'; @@ -48,6 +49,7 @@ export default function TrainingPlanPanel({ onUpdateSessionPartial, onReload }) { + const t = useTranslation(); const [durationWeeks, setDurationWeeks] = useState(4); const [sessionsPerWeek, setSessionsPerWeek] = useState(3); const [draftSessions, setDraftSessions] = useState([]); @@ -72,7 +74,7 @@ export default function TrainingPlanPanel({ }); setDraftSessions(generated.sessions); - setUiMessage('Neuer Entwurf erstellt. Du kannst jetzt Intensität und Übungen je Session anpassen.'); + setUiMessage(t('training.draft_created')); }; const updateDraftSession = (index, patch) => { @@ -96,7 +98,7 @@ export default function TrainingPlanPanel({ const handleSaveDraft = async () => { if (!draftSessions.length) { - setUiMessage('Erstelle zuerst einen Entwurf.'); + setUiMessage(t('training.create_draft_first')); return; } @@ -108,11 +110,11 @@ export default function TrainingPlanPanel({ }); if (!result?.ok) { - setUiMessage(result?.error?.message || 'Trainingsplan konnte nicht gespeichert werden.'); + setUiMessage(result?.error?.message || t('training.save_error')); return; } - setUiMessage('Trainingsplan gespeichert. Der vorherige aktive Plan wurde archiviert.'); + setUiMessage(t('training.save_success')); setDraftSessions([]); }; @@ -123,7 +125,7 @@ export default function TrainingPlanPanel({

- Woche {session.weekNo} · Einheit {session.sessionNo} + {t('training.week_session', { weekNo: session.weekNo, sessionNo: session.sessionNo })}

{session.intensity}
@@ -133,7 +135,7 @@ export default function TrainingPlanPanel({ value={session.mainExercise} onChange={(event) => updateDraftSession(index, { mainExercise: event.target.value })} className="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900" - placeholder="Hauptübung" + placeholder={t('training.main_exercise')} />
@@ -174,9 +176,9 @@ export default function TrainingPlanPanel({
-

Trainingsplan ({patType})

+

{t('training.title', { patType })}

- 1 aktiver Plan + Historie, Session-Status: offen / erledigt / übersprungen. + {t('training.hint')}

@@ -185,7 +187,7 @@ export default function TrainingPlanPanel({ type="button" className="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800" > - Neu laden + {t('training.reload')}
@@ -197,20 +199,20 @@ export default function TrainingPlanPanel({