code spliting und language 2
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(supabase --version)"
|
||||
"Bash(supabase --version)",
|
||||
"Bash(grep -n \"Radar-Profil\\\\|Ist/Soll\\\\|Trend \\(Gesamtpunkte\\)\\\\|Für den Trend\\\\|Kein Radarprofil\\\\|Deutsch\\\\|Englisch\\\\|Italienisch\\\\|Spanisch\\\\|Portugiesisch\\\\|Test final abschließen\\\\|Test als Link teilen\" /d/Projekte/PAT-STATS/src/components/**/*.jsx)",
|
||||
"Bash(find /d/Projekte/PAT-STATS/src -type f \\\\\\(-name *route* -o -name *Router* \\\\\\))",
|
||||
"Bash(npm run:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
@@ -1,7 +1,7 @@
|
||||
Offen
|
||||
Rangliste
|
||||
Übersetzen in gängigge Sprachen Englisch Italienisch Spanisch Portugisisch
|
||||
|
||||
code splitting
|
||||
Admin Oberfläche (anzahl user mit Mail Adressen/ anelegten Tests / anbindung von Werbung Links und rechts von den Test Übersichten
|
||||
2fa
|
||||
|
||||
@@ -11,11 +11,12 @@ Erledigt:
|
||||
Final Speichern do
|
||||
Max Punkte bei Bewertung PAT1
|
||||
User Profil mit Land und Sprach Auswahl
|
||||
Übersetzen in gängigge Sprachen Englisch Italienisch Spanisch Portugisisch
|
||||
|
||||
|
||||
Termninal
|
||||
vercel deploy
|
||||
|
||||
git add .
|
||||
git commit -m "BenutzerProfil "
|
||||
git commit -m "Übersetzung Teil 1"
|
||||
git push
|
||||
1
dist/assets/index-BaBfIFek.css
vendored
1
dist/assets/index-BaBfIFek.css
vendored
File diff suppressed because one or more lines are too long
86
dist/assets/index-Cg2UxsRL.js
vendored
86
dist/assets/index-Cg2UxsRL.js
vendored
File diff suppressed because one or more lines are too long
18
dist/assets/index.es-DQ6YK4So.js
vendored
18
dist/assets/index.es-DQ6YK4So.js
vendored
File diff suppressed because one or more lines are too long
170
dist/assets/jspdf.es.min-CLjTnO_I.js
vendored
170
dist/assets/jspdf.es.min-CLjTnO_I.js
vendored
File diff suppressed because one or more lines are too long
7
dist/index.html
vendored
7
dist/index.html
vendored
@@ -5,8 +5,11 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PAT Test Manager</title>
|
||||
<script type="module" crossorigin src="/assets/index-Cg2UxsRL.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BaBfIFek.css">
|
||||
<script type="module" crossorigin src="/assets/index-Bp6S7Gpx.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-react-VZ-Yj73K.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-supabase-PyIEoSKG.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/vendor-ui-epoOyxwp.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CRRMtHNa.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
97
src/App.jsx
97
src/App.jsx
@@ -1,18 +1,25 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { lazy, Suspense, useEffect, useState } from 'react'
|
||||
import { Moon, Sun } from 'lucide-react'
|
||||
import PatTestManager from './components/PatTestManager'
|
||||
import AuthPanel from './components/AuthPanel'
|
||||
import { supabase } from './lib/supabaseClient'
|
||||
import Landing from './components/Landing'
|
||||
import SharedAssessmentPage from './components/SharedAssessmentPage'
|
||||
import PublicInfoPage from './components/public/PublicInfoPage'
|
||||
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 PatTestManager = lazy(() => import('./components/PatTestManager'))
|
||||
const AuthPanel = lazy(() => import('./components/AuthPanel'))
|
||||
const Landing = lazy(() => import('./components/Landing'))
|
||||
const SharedAssessmentPage = lazy(() => import('./components/SharedAssessmentPage'))
|
||||
const PublicInfoPage = lazy(() => import('./components/public/PublicInfoPage'))
|
||||
const ProfileTab = lazy(() => import('./components/ProfileTab'))
|
||||
|
||||
const PageSpinner = () => (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-300">
|
||||
<span>Laden…</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
const readShareToken = () => {
|
||||
if (typeof window === 'undefined') return ''
|
||||
return new URLSearchParams(window.location.search).get('share') || ''
|
||||
@@ -156,7 +163,9 @@ function App() {
|
||||
if (shareToken) {
|
||||
return (
|
||||
<LanguageProvider language={language}>
|
||||
<SharedAssessmentPage shareToken={shareToken} themeToggle={renderThemeToggle()} />
|
||||
<Suspense fallback={<PageSpinner />}>
|
||||
<SharedAssessmentPage shareToken={shareToken} themeToggle={renderThemeToggle()} />
|
||||
</Suspense>
|
||||
</LanguageProvider>
|
||||
)
|
||||
}
|
||||
@@ -197,7 +206,9 @@ function App() {
|
||||
if (showAuth) {
|
||||
return (
|
||||
<LanguageProvider language={language}>
|
||||
<AuthPanel onAuth={setSession} themeToggle={renderThemeToggle()} onBack={() => setShowAuth(false)} />
|
||||
<Suspense fallback={<PageSpinner />}>
|
||||
<AuthPanel onAuth={setSession} themeToggle={renderThemeToggle()} onBack={() => setShowAuth(false)} />
|
||||
</Suspense>
|
||||
</LanguageProvider>
|
||||
)
|
||||
}
|
||||
@@ -205,23 +216,27 @@ function App() {
|
||||
if (publicPath === '/datenschutz' || publicPath === '/impressum' || publicPath === '/kontakt') {
|
||||
return (
|
||||
<LanguageProvider language={language}>
|
||||
<PublicInfoPage
|
||||
path={publicPath}
|
||||
onGetStarted={() => setShowAuth(true)}
|
||||
onNavigate={navigatePublic}
|
||||
themeToggle={renderThemeToggle()}
|
||||
/>
|
||||
<Suspense fallback={<PageSpinner />}>
|
||||
<PublicInfoPage
|
||||
path={publicPath}
|
||||
onGetStarted={() => setShowAuth(true)}
|
||||
onNavigate={navigatePublic}
|
||||
themeToggle={renderThemeToggle()}
|
||||
/>
|
||||
</Suspense>
|
||||
</LanguageProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<LanguageProvider language={language}>
|
||||
<Landing
|
||||
onGetStarted={() => setShowAuth(true)}
|
||||
onNavigate={navigatePublic}
|
||||
themeToggle={renderThemeToggle()}
|
||||
/>
|
||||
<Suspense fallback={<PageSpinner />}>
|
||||
<Landing
|
||||
onGetStarted={() => setShowAuth(true)}
|
||||
onNavigate={navigatePublic}
|
||||
themeToggle={renderThemeToggle()}
|
||||
/>
|
||||
</Suspense>
|
||||
</LanguageProvider>
|
||||
)
|
||||
}
|
||||
@@ -306,27 +321,29 @@ function App() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{activeTab !== 'profile' && (
|
||||
<PatTestManager
|
||||
user={session?.user}
|
||||
activeTab={activeTab}
|
||||
onEditingStateChange={setIsEditingAssessment}
|
||||
testsVisible={Boolean(profile?.testsVisible)}
|
||||
isProfileLoading={profileLoading}
|
||||
/>
|
||||
)}
|
||||
<Suspense fallback={<PageSpinner />}>
|
||||
{activeTab !== 'profile' && (
|
||||
<PatTestManager
|
||||
user={session?.user}
|
||||
activeTab={activeTab}
|
||||
onEditingStateChange={setIsEditingAssessment}
|
||||
testsVisible={Boolean(profile?.testsVisible)}
|
||||
isProfileLoading={profileLoading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'profile' && (
|
||||
<ProfileTab
|
||||
session={session}
|
||||
profile={profile}
|
||||
loading={profileLoading}
|
||||
error={profileError}
|
||||
onSave={saveProfile}
|
||||
onDelete={deleteProfileData}
|
||||
onSessionChange={setSession}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'profile' && (
|
||||
<ProfileTab
|
||||
session={session}
|
||||
profile={profile}
|
||||
loading={profileLoading}
|
||||
error={profileError}
|
||||
onSave={saveProfile}
|
||||
onDelete={deleteProfileData}
|
||||
onSessionChange={setSession}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</div>
|
||||
</LanguageProvider>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from '../../i18n/LanguageContext';
|
||||
|
||||
const polarToCartesian = (centerX, centerY, radius, angle) => ({
|
||||
x: centerX + radius * Math.cos(angle),
|
||||
@@ -8,12 +9,13 @@ const polarToCartesian = (centerX, centerY, radius, angle) => ({
|
||||
const truncate = (value, maxLength = 16) => (value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value);
|
||||
|
||||
export default function RadarChart({ radarSeries = [] }) {
|
||||
const t = useTranslation();
|
||||
const data = radarSeries.slice(0, 6);
|
||||
|
||||
if (!data.length) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-dashed border-gray-300 dark:border-gray-700 bg-white/70 dark:bg-gray-900/70 p-4 text-sm text-gray-600 dark:text-gray-300">
|
||||
Kein Radarprofil verfügbar.
|
||||
{t('radar.no_data')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -40,8 +42,8 @@ export default function RadarChart({ radarSeries = [] }) {
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-4 shadow-sm h-full flex flex-col">
|
||||
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-100 mb-3">Radar-Profil (Ist/Soll)</h3>
|
||||
<svg viewBox={`0 0 ${width} ${height}`} className="w-full h-auto max-w-[420px] mx-auto" role="img" aria-label="Radarprofil">
|
||||
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-100 mb-3">{t('radar.title')}</h3>
|
||||
<svg viewBox={`0 0 ${width} ${height}`} className="w-full h-auto max-w-[420px] mx-auto" role="img" aria-label={t('radar.aria')}>
|
||||
{[0.25, 0.5, 0.75, 1].map((level) => (
|
||||
<polygon
|
||||
key={level}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from '../../i18n/LanguageContext';
|
||||
|
||||
export default function TrendChart({ trendSeries = [] }) {
|
||||
const t = useTranslation();
|
||||
|
||||
if (trendSeries.length < 2) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-dashed border-gray-300 dark:border-gray-700 bg-white/70 dark:bg-gray-900/70 p-4 text-sm text-gray-600 dark:text-gray-300">
|
||||
Für den Trend werden mindestens 2 Assessments benötigt.
|
||||
{t('trend.no_data')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -35,8 +38,8 @@ export default function TrendChart({ trendSeries = [] }) {
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-4 shadow-sm">
|
||||
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-100 mb-3">Trend (Gesamtpunkte)</h3>
|
||||
<svg viewBox={`0 0 ${width} ${height}`} className="w-full h-auto" role="img" aria-label="Trenddiagramm">
|
||||
<h3 className="text-sm font-semibold text-gray-800 dark:text-gray-100 mb-3">{t('trend.title')}</h3>
|
||||
<svg viewBox={`0 0 ${width} ${height}`} className="w-full h-auto" role="img" aria-label={t('trend.aria')}>
|
||||
<line x1={left} y1={top} x2={left} y2={top + innerHeight} className="stroke-gray-300 dark:stroke-gray-700" strokeWidth="1" />
|
||||
<line x1={left} y1={top + innerHeight} x2={left + innerWidth} y2={top + innerHeight} className="stroke-gray-300 dark:stroke-gray-700" strokeWidth="1" />
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ import { CheckCircle2, FileDown, Lock, Save, Share2 } from 'lucide-react';
|
||||
import ExerciseInput from './ExerciseInput';
|
||||
import SaveDialog from './SaveDialog';
|
||||
import { downloadAssessmentPdf } from '../../utils/pdfExport';
|
||||
import { useTranslation } from '../../i18n/LanguageContext';
|
||||
import { useTranslation, useLanguage } from '../../i18n/LanguageContext';
|
||||
import { translateExerciseName } from '../../i18n/exerciseNames';
|
||||
|
||||
const PatDetail = ({
|
||||
currentAssessment,
|
||||
@@ -32,11 +33,18 @@ const PatDetail = ({
|
||||
onRevokeShare
|
||||
}) => {
|
||||
const t = useTranslation();
|
||||
const language = useLanguage();
|
||||
|
||||
if (!currentAssessment) return null;
|
||||
|
||||
const totalPoints = calculateTotal();
|
||||
const achievement = getAchievement(currentAssessment?.patType, totalPoints);
|
||||
|
||||
const getAchievementName = (name) => {
|
||||
if (name === 'Nicht bestanden') return t('achievement.not_passed');
|
||||
if (name.endsWith(' Teilnahme')) return name.replace(' Teilnahme', ` ${t('achievement.participation')}`);
|
||||
return name;
|
||||
};
|
||||
const handleDownloadPdf = async () => {
|
||||
await downloadAssessmentPdf({
|
||||
assessment: currentAssessment,
|
||||
@@ -104,7 +112,7 @@ const PatDetail = ({
|
||||
<button
|
||||
onClick={onFinalize}
|
||||
disabled={!canFinalize}
|
||||
title={finalizeDisabledReason || 'Test final abschließen'}
|
||||
title={finalizeDisabledReason || t('detail.finalize_title')}
|
||||
className={`px-4 py-2 rounded-lg transition flex items-center gap-2 text-sm ${
|
||||
canFinalize
|
||||
? 'bg-emerald-700 text-white hover:bg-emerald-800'
|
||||
@@ -126,7 +134,7 @@ const PatDetail = ({
|
||||
<button
|
||||
onClick={onShare}
|
||||
disabled={!canShare}
|
||||
title={shareDisabledReason || 'Test als Link teilen'}
|
||||
title={shareDisabledReason || t('detail.share_title')}
|
||||
className={`px-4 py-2 rounded-lg transition flex items-center gap-2 text-sm ${
|
||||
canShare
|
||||
? 'bg-sky-600 text-white hover:bg-sky-700'
|
||||
@@ -191,7 +199,7 @@ const PatDetail = ({
|
||||
|
||||
{currentAssessment?.exercises.map((exercise, exIndex) => (
|
||||
<div key={exIndex} className="mb-6 border-b pb-4 border-gray-200 dark:border-gray-800">
|
||||
<div className="font-bold text-lg mb-2 text-gray-700 dark:text-gray-200">{exercise.name}</div>
|
||||
<div className="font-bold text-lg mb-2 text-gray-700 dark:text-gray-200">{translateExerciseName(exercise.name, language)}</div>
|
||||
|
||||
<ExerciseInput exercise={exercise} exIndex={exIndex} onChange={onUpdateExercise} disabled={isReadOnly} />
|
||||
|
||||
@@ -222,7 +230,7 @@ const PatDetail = ({
|
||||
</div>
|
||||
|
||||
<div className={`mt-4 px-6 py-4 rounded-lg ${achievement.color} ${achievement.text} text-center`}>
|
||||
<p className="text-2xl font-bold">{achievement.name}</p>
|
||||
<p className="text-2xl font-bold">{getAchievementName(achievement.name)}</p>
|
||||
{achievement.subtitle && <p className="text-lg mt-2">{achievement.subtitle}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -218,7 +218,7 @@ const PatList = ({
|
||||
<h2 className="text-lg font-bold text-gray-800 dark:text-gray-100">{title}</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{t('patlist.count', { count: entries.length })}
|
||||
{entries.length !== totalEntries ? ` von ${totalEntries}` : ''}
|
||||
{entries.length !== totalEntries ? ` ${t('patlist.of')} ${totalEntries}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from '../i18n/LanguageContext';
|
||||
import PatList from './PatList/PatList';
|
||||
import PatDetail from './PatDetail/PatDetail';
|
||||
import AnalysisTab from './Analysis/AnalysisTab';
|
||||
const AnalysisTab = lazy(() => import('./Analysis/AnalysisTab'));
|
||||
import { patTypes } from '../data/patTypes';
|
||||
import { useAssessments } from '../hooks/useAssessments';
|
||||
import { getAchievement, getPatTypeColor } from '../utils/patCalculations';
|
||||
@@ -309,13 +309,15 @@ export default function PatTestManager({
|
||||
|
||||
if (activeTab === 'analysis') {
|
||||
return (
|
||||
<AnalysisTab
|
||||
assessments={visibleAssessments}
|
||||
selectedPatType={selectedPatType}
|
||||
onPatTypeChange={setSelectedPatType}
|
||||
patTypes={visiblePatTypes}
|
||||
user={user}
|
||||
/>
|
||||
<Suspense fallback={<div className="min-h-screen flex items-center justify-center text-gray-500 dark:text-gray-400">Laden…</div>}>
|
||||
<AnalysisTab
|
||||
assessments={visibleAssessments}
|
||||
selectedPatType={selectedPatType}
|
||||
onPatTypeChange={setSelectedPatType}
|
||||
patTypes={visiblePatTypes}
|
||||
user={user}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Check, ChevronDown } from 'lucide-react';
|
||||
import { countryOptions, getCountryLabel, getCountryOption } from '../data/countries';
|
||||
import CountryFlag from './CountryFlag';
|
||||
import { useTranslation } from '../i18n/LanguageContext';
|
||||
import { getTranslator } from '../i18n/translations';
|
||||
|
||||
const emptyStatus = {
|
||||
message: '',
|
||||
@@ -81,6 +82,8 @@ export default function ProfileTab({
|
||||
setStatus(emptyStatus);
|
||||
|
||||
const result = await onSave(formValues);
|
||||
// Nach dem Speichern in der neu gewählten Sprache übersetzen
|
||||
const tNew = getTranslator(formValues.language);
|
||||
|
||||
if (result?.ok) {
|
||||
if (result.session) {
|
||||
@@ -89,14 +92,14 @@ export default function ProfileTab({
|
||||
|
||||
setStatus({
|
||||
message: result.emailUpdateRequested
|
||||
? t('profile.saved_email')
|
||||
: t('profile.saved'),
|
||||
? tNew('profile.saved_email')
|
||||
: tNew('profile.saved'),
|
||||
error: ''
|
||||
});
|
||||
} else {
|
||||
setStatus({
|
||||
message: '',
|
||||
error: result?.error?.message || t('profile.error_save')
|
||||
error: result?.error?.message || tNew('profile.error_save')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { getTranslator } from './translations';
|
||||
import { getTranslator, SUPPORTED_LANGUAGES } from './translations';
|
||||
|
||||
const LanguageContext = createContext(() => (key) => key);
|
||||
const LanguageContext = createContext({ t: (key) => key, language: 'de' });
|
||||
|
||||
export function LanguageProvider({ language, children }) {
|
||||
const t = useMemo(() => getTranslator(language), [language]);
|
||||
const lang = SUPPORTED_LANGUAGES.includes(language) ? language : 'de';
|
||||
const t = useMemo(() => getTranslator(lang), [lang]);
|
||||
const value = useMemo(() => ({ t, language: lang }), [t, lang]);
|
||||
|
||||
return (
|
||||
<LanguageContext.Provider value={t}>
|
||||
<LanguageContext.Provider value={value}>
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTranslation() {
|
||||
return useContext(LanguageContext);
|
||||
return useContext(LanguageContext).t;
|
||||
}
|
||||
|
||||
export function useLanguage() {
|
||||
return useContext(LanguageContext).language;
|
||||
}
|
||||
|
||||
138
src/i18n/exerciseNames.js
Normal file
138
src/i18n/exerciseNames.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Übersetzungen für PAT-Übungsnamen.
|
||||
* Der deutsche Name (wie er in der Datenbank steht) ist der Schlüssel.
|
||||
*/
|
||||
const exerciseNameMap = {
|
||||
// ── PAT Start ──────────────────────────────────────────────────────────────
|
||||
'1) Stoß-Geschwindigkeit': {
|
||||
en: '1) Stroke Speed',
|
||||
it: '1) Velocità del colpo',
|
||||
es: '1) Velocidad del golpe',
|
||||
pt: '1) Velocidade do golpe',
|
||||
},
|
||||
'2) Stoß-Gradlinigkeit': {
|
||||
en: '2) Stroke Straightness',
|
||||
it: '2) Linearità del colpo',
|
||||
es: '2) Linealidad del golpe',
|
||||
pt: '2) Linearidade do golpe',
|
||||
},
|
||||
'3) Winkelbälle': {
|
||||
en: '3) Angle Balls',
|
||||
it: '3) Palle angolate',
|
||||
es: '3) Bolas en ángulo',
|
||||
pt: '3) Bolas em ângulo',
|
||||
},
|
||||
'4) Nachlauf Wirkung': {
|
||||
en: '4) Follow-Through Effect',
|
||||
it: '4) Effetto di seguita',
|
||||
es: '4) Efecto de seguimiento',
|
||||
pt: '4) Efeito de avanço',
|
||||
},
|
||||
'5) Rücklauf-Wirkung': {
|
||||
en: '5) Back Spin Effect',
|
||||
it: '5) Effetto di ritorno',
|
||||
es: '5) Efecto de retroceso',
|
||||
pt: '5) Efeito de retorno',
|
||||
},
|
||||
'6) Kl. Positions-Spiel': {
|
||||
en: '6) Sm. Position Play',
|
||||
it: '6) Pic. gioco di posizione',
|
||||
es: '6) Peq. juego de posición',
|
||||
pt: '6) Peq. jogo de posição',
|
||||
},
|
||||
'7) Gr. Pos. Bereich': {
|
||||
en: '7) Lg. Position Range',
|
||||
it: '7) Gr. area di posizione',
|
||||
es: '7) Gr. rango de posición',
|
||||
pt: '7) Gr. área de posição',
|
||||
},
|
||||
'8) Press Bande': {
|
||||
en: '8) Cushion Press',
|
||||
it: '8) Pressione sponda',
|
||||
es: '8) Presión de banda',
|
||||
pt: '8) Pressão de banda',
|
||||
},
|
||||
'9) Standardbälle': {
|
||||
en: '9) Standard Balls',
|
||||
it: '9) Palle standard',
|
||||
es: '9) Bolas estándar',
|
||||
pt: '9) Bolas padrão',
|
||||
},
|
||||
'10) 8-Ball Situation': {
|
||||
en: '10) 8-Ball Situation',
|
||||
it: '10) Situazione 8-Ball',
|
||||
es: '10) Situación 8-Ball',
|
||||
pt: '10) Situação 8-Ball',
|
||||
},
|
||||
|
||||
// ── PAT 1 ──────────────────────────────────────────────────────────────────
|
||||
'1) Geschwindigkeit': {
|
||||
en: '1) Speed',
|
||||
it: '1) Velocità',
|
||||
es: '1) Velocidad',
|
||||
pt: '1) Velocidade',
|
||||
},
|
||||
'2) Geradlinigkeit (Speed 2)': {
|
||||
en: '2) Straightness (Speed 2)',
|
||||
it: '2) Linearità (Speed 2)',
|
||||
es: '2) Linealidad (Speed 2)',
|
||||
pt: '2) Linearidade (Speed 2)',
|
||||
},
|
||||
'3) Nachlauf': {
|
||||
en: '3) Follow-Through',
|
||||
it: '3) Seguita',
|
||||
es: '3) Seguimiento',
|
||||
pt: '3) Avanço',
|
||||
},
|
||||
'4) Rücklauf': {
|
||||
en: '4) Back Spin',
|
||||
it: '4) Effetto retro',
|
||||
es: '4) Retroceso',
|
||||
pt: '4) Retorno',
|
||||
},
|
||||
'5) Kl. Pos.-Bereich': {
|
||||
en: '5) Sm. Position Range',
|
||||
it: '5) Pic. area di posizione',
|
||||
es: '5) Peq. rango de posición',
|
||||
pt: '5) Peq. área de posição',
|
||||
},
|
||||
'6) Gr. Pos.-Bereich': {
|
||||
en: '6) Lg. Position Range',
|
||||
it: '6) Gr. area di posizione',
|
||||
es: '6) Gr. rango de posición',
|
||||
pt: '6) Gr. área de posição',
|
||||
},
|
||||
'7) Press Bande': {
|
||||
en: '7) Cushion Press',
|
||||
it: '7) Pressione sponda',
|
||||
es: '7) Presión de banda',
|
||||
pt: '7) Pressão de banda',
|
||||
},
|
||||
'8) Endlos-Übung (Max: 9)': {
|
||||
en: '8) Continuous Exercise (Max: 9)',
|
||||
it: '8) Esercizio continuo (Max: 9)',
|
||||
es: '8) Ejercicio continuo (Máx: 9)',
|
||||
pt: '8) Exercício contínuo (Máx: 9)',
|
||||
},
|
||||
'9) Standardbälle': {
|
||||
en: '9) Standard Balls',
|
||||
it: '9) Palle standard',
|
||||
es: '9) Bolas estándar',
|
||||
pt: '9) Bolas padrão',
|
||||
},
|
||||
'10) 9-Ball Situation 5': {
|
||||
en: '10) 9-Ball Situation 5',
|
||||
it: '10) Situazione 9-Ball 5',
|
||||
es: '10) Situación 9-Ball 5',
|
||||
pt: '10) Situação 9-Ball 5',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Gibt den übersetzten Übungsnamen zurück.
|
||||
* Fällt auf den deutschen Namen zurück, wenn keine Übersetzung vorhanden.
|
||||
*/
|
||||
export function translateExerciseName(germanName, language) {
|
||||
if (!language || language === 'de') return germanName;
|
||||
return exerciseNameMap[germanName]?.[language] ?? germanName;
|
||||
}
|
||||
@@ -112,6 +112,9 @@ const translations = {
|
||||
'detail.col_factor': 'Faktor',
|
||||
'detail.col_points': 'Points',
|
||||
'detail.col_result': 'Ergebnis',
|
||||
'detail.finalize_title': 'Test final abschließen',
|
||||
'detail.share_title': 'Test als Link teilen',
|
||||
'patlist.of': 'von',
|
||||
|
||||
// ── Speichern-Dialog ─────────────────────────────────────────────────────
|
||||
'savedialog.title': 'Ungespeicherte Änderungen',
|
||||
@@ -182,6 +185,20 @@ const translations = {
|
||||
'training.history': 'Plan-Historie',
|
||||
'training.no_history': 'Keine archivierten Pläne vorhanden.',
|
||||
|
||||
// ── Ergebnis-Bewertungen ─────────────────────────────────────────────────
|
||||
'achievement.not_passed': 'Nicht bestanden',
|
||||
'achievement.participation': 'Teilnahme',
|
||||
|
||||
// ── Radar-Chart ──────────────────────────────────────────────────────────
|
||||
'radar.no_data': 'Kein Radarprofil verfügbar.',
|
||||
'radar.title': 'Radar-Profil (Ist/Soll)',
|
||||
'radar.aria': 'Radarprofil',
|
||||
|
||||
// ── Trend-Chart ──────────────────────────────────────────────────────────
|
||||
'trend.no_data': 'Für den Trend werden mindestens 2 Assessments benötigt.',
|
||||
'trend.title': 'Trend (Gesamtpunkte)',
|
||||
'trend.aria': 'Trenddiagramm',
|
||||
|
||||
// ── Gap-Chart ────────────────────────────────────────────────────────────
|
||||
'gapchart.no_data': 'Kein Gap-Chart verfügbar.',
|
||||
'gapchart.title': 'Gap-Bar-Chart (Priorität)',
|
||||
@@ -347,6 +364,9 @@ const translations = {
|
||||
'detail.col_factor': 'Factor',
|
||||
'detail.col_points': 'Points',
|
||||
'detail.col_result': 'Result',
|
||||
'detail.finalize_title': 'Finalize test',
|
||||
'detail.share_title': 'Share test as link',
|
||||
'patlist.of': 'of',
|
||||
|
||||
'savedialog.title': 'Unsaved changes',
|
||||
'savedialog.message': 'You have unsaved changes. Would you like to save them?',
|
||||
@@ -412,6 +432,14 @@ const translations = {
|
||||
'training.history': 'Plan history',
|
||||
'training.no_history': 'No archived plans available.',
|
||||
|
||||
'achievement.not_passed': 'Not passed',
|
||||
'achievement.participation': 'Participation',
|
||||
'radar.no_data': 'No radar profile available.',
|
||||
'radar.title': 'Radar Profile (Actual/Target)',
|
||||
'radar.aria': 'Radar profile',
|
||||
'trend.no_data': 'At least 2 assessments are needed for the trend.',
|
||||
'trend.title': 'Trend (Total points)',
|
||||
'trend.aria': 'Trend chart',
|
||||
'gapchart.no_data': 'No gap chart available.',
|
||||
'gapchart.title': 'Gap Bar Chart (Priority)',
|
||||
'gapchart.aria': 'Gap bar chart',
|
||||
@@ -573,6 +601,9 @@ const translations = {
|
||||
'detail.col_factor': 'Fattore',
|
||||
'detail.col_points': 'Punti',
|
||||
'detail.col_result': 'Risultato',
|
||||
'detail.finalize_title': 'Finalizza il test',
|
||||
'detail.share_title': 'Condividi il test come link',
|
||||
'patlist.of': 'di',
|
||||
|
||||
'savedialog.title': 'Modifiche non salvate',
|
||||
'savedialog.message': 'Hai modifiche non salvate. Vuoi salvarle?',
|
||||
@@ -638,6 +669,14 @@ const translations = {
|
||||
'training.history': 'Cronologia piani',
|
||||
'training.no_history': 'Nessun piano archiviato disponibile.',
|
||||
|
||||
'achievement.not_passed': 'Non superato',
|
||||
'achievement.participation': 'Partecipazione',
|
||||
'radar.no_data': 'Nessun profilo radar disponibile.',
|
||||
'radar.title': 'Profilo Radar (Effettivo/Obiettivo)',
|
||||
'radar.aria': 'Profilo radar',
|
||||
'trend.no_data': 'Sono necessari almeno 2 assessment per il trend.',
|
||||
'trend.title': 'Trend (Punti totali)',
|
||||
'trend.aria': 'Grafico trend',
|
||||
'gapchart.no_data': 'Nessun grafico gap disponibile.',
|
||||
'gapchart.title': 'Grafico Gap (Priorità)',
|
||||
'gapchart.aria': 'Grafico a barre gap',
|
||||
@@ -799,6 +838,9 @@ const translations = {
|
||||
'detail.col_factor': 'Factor',
|
||||
'detail.col_points': 'Puntos',
|
||||
'detail.col_result': 'Resultado',
|
||||
'detail.finalize_title': 'Finalizar test',
|
||||
'detail.share_title': 'Compartir test como enlace',
|
||||
'patlist.of': 'de',
|
||||
|
||||
'savedialog.title': 'Cambios sin guardar',
|
||||
'savedialog.message': 'Tienes cambios sin guardar. ¿Deseas guardarlos?',
|
||||
@@ -864,6 +906,14 @@ const translations = {
|
||||
'training.history': 'Historial de planes',
|
||||
'training.no_history': 'No hay planes archivados disponibles.',
|
||||
|
||||
'achievement.not_passed': 'No aprobado',
|
||||
'achievement.participation': 'Participación',
|
||||
'radar.no_data': 'No hay perfil radar disponible.',
|
||||
'radar.title': 'Perfil Radar (Real/Objetivo)',
|
||||
'radar.aria': 'Perfil radar',
|
||||
'trend.no_data': 'Se necesitan al menos 2 evaluaciones para la tendencia.',
|
||||
'trend.title': 'Tendencia (Puntos totales)',
|
||||
'trend.aria': 'Gráfico de tendencia',
|
||||
'gapchart.no_data': 'No hay gráfico de gap disponible.',
|
||||
'gapchart.title': 'Gráfico de Gap (Prioridad)',
|
||||
'gapchart.aria': 'Gráfico de barras de gap',
|
||||
@@ -1025,6 +1075,9 @@ const translations = {
|
||||
'detail.col_factor': 'Fator',
|
||||
'detail.col_points': 'Pontos',
|
||||
'detail.col_result': 'Resultado',
|
||||
'detail.finalize_title': 'Finalizar teste',
|
||||
'detail.share_title': 'Partilhar teste como link',
|
||||
'patlist.of': 'de',
|
||||
|
||||
'savedialog.title': 'Alterações não guardadas',
|
||||
'savedialog.message': 'Tem alterações não guardadas. Deseja guardá-las?',
|
||||
@@ -1090,6 +1143,14 @@ const translations = {
|
||||
'training.history': 'Histórico de planos',
|
||||
'training.no_history': 'Sem planos arquivados disponíveis.',
|
||||
|
||||
'achievement.not_passed': 'Não aprovado',
|
||||
'achievement.participation': 'Participação',
|
||||
'radar.no_data': 'Sem perfil radar disponível.',
|
||||
'radar.title': 'Perfil Radar (Real/Objetivo)',
|
||||
'radar.aria': 'Perfil radar',
|
||||
'trend.no_data': 'São necessárias pelo menos 2 avaliações para a tendência.',
|
||||
'trend.title': 'Tendência (Pontos totais)',
|
||||
'trend.aria': 'Gráfico de tendência',
|
||||
'gapchart.no_data': 'Sem gráfico de gap disponível.',
|
||||
'gapchart.title': 'Gráfico de Gap (Prioridade)',
|
||||
'gapchart.aria': 'Gráfico de barras de gap',
|
||||
|
||||
@@ -3,6 +3,19 @@ import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
chunkSizeWarningLimit: 600,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
'vendor-react': ['react', 'react-dom'],
|
||||
'vendor-supabase': ['@supabase/supabase-js'],
|
||||
'vendor-ui': ['lucide-react', 'country-flag-icons'],
|
||||
'vendor-xlsx': ['xlsx'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.{js,jsx}']
|
||||
|
||||
Reference in New Issue
Block a user