code spliting und language 2
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"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
|
node_modules
|
||||||
|
dist
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
Offen
|
Offen
|
||||||
Rangliste
|
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
|
Admin Oberfläche (anzahl user mit Mail Adressen/ anelegten Tests / anbindung von Werbung Links und rechts von den Test Übersichten
|
||||||
2fa
|
2fa
|
||||||
|
|
||||||
@@ -11,11 +11,12 @@ Erledigt:
|
|||||||
Final Speichern do
|
Final Speichern do
|
||||||
Max Punkte bei Bewertung PAT1
|
Max Punkte bei Bewertung PAT1
|
||||||
User Profil mit Land und Sprach Auswahl
|
User Profil mit Land und Sprach Auswahl
|
||||||
|
Übersetzen in gängigge Sprachen Englisch Italienisch Spanisch Portugisisch
|
||||||
|
|
||||||
|
|
||||||
Termninal
|
Termninal
|
||||||
vercel deploy
|
vercel deploy
|
||||||
|
|
||||||
git add .
|
git add .
|
||||||
git commit -m "BenutzerProfil "
|
git commit -m "Übersetzung Teil 1"
|
||||||
git push
|
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" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>PAT Test Manager</title>
|
<title>PAT Test Manager</title>
|
||||||
<script type="module" crossorigin src="/assets/index-Cg2UxsRL.js"></script>
|
<script type="module" crossorigin src="/assets/index-Bp6S7Gpx.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BaBfIFek.css">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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 { Moon, Sun } from 'lucide-react'
|
||||||
import PatTestManager from './components/PatTestManager'
|
|
||||||
import AuthPanel from './components/AuthPanel'
|
|
||||||
import { supabase } from './lib/supabaseClient'
|
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 { useUserProfile } from './hooks/useUserProfile'
|
||||||
import { getCountryLabel, getCountryOption } from './data/countries'
|
import { getCountryLabel, getCountryOption } from './data/countries'
|
||||||
import CountryFlag from './components/CountryFlag'
|
import CountryFlag from './components/CountryFlag'
|
||||||
import { LanguageProvider } from './i18n/LanguageContext'
|
import { LanguageProvider } from './i18n/LanguageContext'
|
||||||
import { getTranslator } from './i18n/translations'
|
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 = () => {
|
const readShareToken = () => {
|
||||||
if (typeof window === 'undefined') return ''
|
if (typeof window === 'undefined') return ''
|
||||||
return new URLSearchParams(window.location.search).get('share') || ''
|
return new URLSearchParams(window.location.search).get('share') || ''
|
||||||
@@ -156,7 +163,9 @@ function App() {
|
|||||||
if (shareToken) {
|
if (shareToken) {
|
||||||
return (
|
return (
|
||||||
<LanguageProvider language={language}>
|
<LanguageProvider language={language}>
|
||||||
<SharedAssessmentPage shareToken={shareToken} themeToggle={renderThemeToggle()} />
|
<Suspense fallback={<PageSpinner />}>
|
||||||
|
<SharedAssessmentPage shareToken={shareToken} themeToggle={renderThemeToggle()} />
|
||||||
|
</Suspense>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -197,7 +206,9 @@ function App() {
|
|||||||
if (showAuth) {
|
if (showAuth) {
|
||||||
return (
|
return (
|
||||||
<LanguageProvider language={language}>
|
<LanguageProvider language={language}>
|
||||||
<AuthPanel onAuth={setSession} themeToggle={renderThemeToggle()} onBack={() => setShowAuth(false)} />
|
<Suspense fallback={<PageSpinner />}>
|
||||||
|
<AuthPanel onAuth={setSession} themeToggle={renderThemeToggle()} onBack={() => setShowAuth(false)} />
|
||||||
|
</Suspense>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -205,23 +216,27 @@ function App() {
|
|||||||
if (publicPath === '/datenschutz' || publicPath === '/impressum' || publicPath === '/kontakt') {
|
if (publicPath === '/datenschutz' || publicPath === '/impressum' || publicPath === '/kontakt') {
|
||||||
return (
|
return (
|
||||||
<LanguageProvider language={language}>
|
<LanguageProvider language={language}>
|
||||||
<PublicInfoPage
|
<Suspense fallback={<PageSpinner />}>
|
||||||
path={publicPath}
|
<PublicInfoPage
|
||||||
onGetStarted={() => setShowAuth(true)}
|
path={publicPath}
|
||||||
onNavigate={navigatePublic}
|
onGetStarted={() => setShowAuth(true)}
|
||||||
themeToggle={renderThemeToggle()}
|
onNavigate={navigatePublic}
|
||||||
/>
|
themeToggle={renderThemeToggle()}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LanguageProvider language={language}>
|
<LanguageProvider language={language}>
|
||||||
<Landing
|
<Suspense fallback={<PageSpinner />}>
|
||||||
onGetStarted={() => setShowAuth(true)}
|
<Landing
|
||||||
onNavigate={navigatePublic}
|
onGetStarted={() => setShowAuth(true)}
|
||||||
themeToggle={renderThemeToggle()}
|
onNavigate={navigatePublic}
|
||||||
/>
|
themeToggle={renderThemeToggle()}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -306,27 +321,29 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{activeTab !== 'profile' && (
|
<Suspense fallback={<PageSpinner />}>
|
||||||
<PatTestManager
|
{activeTab !== 'profile' && (
|
||||||
user={session?.user}
|
<PatTestManager
|
||||||
activeTab={activeTab}
|
user={session?.user}
|
||||||
onEditingStateChange={setIsEditingAssessment}
|
activeTab={activeTab}
|
||||||
testsVisible={Boolean(profile?.testsVisible)}
|
onEditingStateChange={setIsEditingAssessment}
|
||||||
isProfileLoading={profileLoading}
|
testsVisible={Boolean(profile?.testsVisible)}
|
||||||
/>
|
isProfileLoading={profileLoading}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{activeTab === 'profile' && (
|
{activeTab === 'profile' && (
|
||||||
<ProfileTab
|
<ProfileTab
|
||||||
session={session}
|
session={session}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
loading={profileLoading}
|
loading={profileLoading}
|
||||||
error={profileError}
|
error={profileError}
|
||||||
onSave={saveProfile}
|
onSave={saveProfile}
|
||||||
onDelete={deleteProfileData}
|
onDelete={deleteProfileData}
|
||||||
onSessionChange={setSession}
|
onSessionChange={setSession}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from '../../i18n/LanguageContext';
|
||||||
|
|
||||||
const polarToCartesian = (centerX, centerY, radius, angle) => ({
|
const polarToCartesian = (centerX, centerY, radius, angle) => ({
|
||||||
x: centerX + radius * Math.cos(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);
|
const truncate = (value, maxLength = 16) => (value.length > maxLength ? `${value.slice(0, maxLength - 1)}…` : value);
|
||||||
|
|
||||||
export default function RadarChart({ radarSeries = [] }) {
|
export default function RadarChart({ radarSeries = [] }) {
|
||||||
|
const t = useTranslation();
|
||||||
const data = radarSeries.slice(0, 6);
|
const data = radarSeries.slice(0, 6);
|
||||||
|
|
||||||
if (!data.length) {
|
if (!data.length) {
|
||||||
return (
|
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">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -40,8 +42,8 @@ export default function RadarChart({ radarSeries = [] }) {
|
|||||||
|
|
||||||
return (
|
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">
|
<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>
|
<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="Radarprofil">
|
<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) => (
|
{[0.25, 0.5, 0.75, 1].map((level) => (
|
||||||
<polygon
|
<polygon
|
||||||
key={level}
|
key={level}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from '../../i18n/LanguageContext';
|
||||||
|
|
||||||
export default function TrendChart({ trendSeries = [] }) {
|
export default function TrendChart({ trendSeries = [] }) {
|
||||||
|
const t = useTranslation();
|
||||||
|
|
||||||
if (trendSeries.length < 2) {
|
if (trendSeries.length < 2) {
|
||||||
return (
|
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">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -35,8 +38,8 @@ export default function TrendChart({ trendSeries = [] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-4 shadow-sm">
|
<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>
|
<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="Trenddiagramm">
|
<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} 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" />
|
<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 ExerciseInput from './ExerciseInput';
|
||||||
import SaveDialog from './SaveDialog';
|
import SaveDialog from './SaveDialog';
|
||||||
import { downloadAssessmentPdf } from '../../utils/pdfExport';
|
import { downloadAssessmentPdf } from '../../utils/pdfExport';
|
||||||
import { useTranslation } from '../../i18n/LanguageContext';
|
import { useTranslation, useLanguage } from '../../i18n/LanguageContext';
|
||||||
|
import { translateExerciseName } from '../../i18n/exerciseNames';
|
||||||
|
|
||||||
const PatDetail = ({
|
const PatDetail = ({
|
||||||
currentAssessment,
|
currentAssessment,
|
||||||
@@ -32,11 +33,18 @@ const PatDetail = ({
|
|||||||
onRevokeShare
|
onRevokeShare
|
||||||
}) => {
|
}) => {
|
||||||
const t = useTranslation();
|
const t = useTranslation();
|
||||||
|
const language = useLanguage();
|
||||||
|
|
||||||
if (!currentAssessment) return null;
|
if (!currentAssessment) return null;
|
||||||
|
|
||||||
const totalPoints = calculateTotal();
|
const totalPoints = calculateTotal();
|
||||||
const achievement = getAchievement(currentAssessment?.patType, totalPoints);
|
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 () => {
|
const handleDownloadPdf = async () => {
|
||||||
await downloadAssessmentPdf({
|
await downloadAssessmentPdf({
|
||||||
assessment: currentAssessment,
|
assessment: currentAssessment,
|
||||||
@@ -104,7 +112,7 @@ const PatDetail = ({
|
|||||||
<button
|
<button
|
||||||
onClick={onFinalize}
|
onClick={onFinalize}
|
||||||
disabled={!canFinalize}
|
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 ${
|
className={`px-4 py-2 rounded-lg transition flex items-center gap-2 text-sm ${
|
||||||
canFinalize
|
canFinalize
|
||||||
? 'bg-emerald-700 text-white hover:bg-emerald-800'
|
? 'bg-emerald-700 text-white hover:bg-emerald-800'
|
||||||
@@ -126,7 +134,7 @@ const PatDetail = ({
|
|||||||
<button
|
<button
|
||||||
onClick={onShare}
|
onClick={onShare}
|
||||||
disabled={!canShare}
|
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 ${
|
className={`px-4 py-2 rounded-lg transition flex items-center gap-2 text-sm ${
|
||||||
canShare
|
canShare
|
||||||
? 'bg-sky-600 text-white hover:bg-sky-700'
|
? 'bg-sky-600 text-white hover:bg-sky-700'
|
||||||
@@ -191,7 +199,7 @@ const PatDetail = ({
|
|||||||
|
|
||||||
{currentAssessment?.exercises.map((exercise, exIndex) => (
|
{currentAssessment?.exercises.map((exercise, exIndex) => (
|
||||||
<div key={exIndex} className="mb-6 border-b pb-4 border-gray-200 dark:border-gray-800">
|
<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} />
|
<ExerciseInput exercise={exercise} exIndex={exIndex} onChange={onUpdateExercise} disabled={isReadOnly} />
|
||||||
|
|
||||||
@@ -222,7 +230,7 @@ const PatDetail = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`mt-4 px-6 py-4 rounded-lg ${achievement.color} ${achievement.text} text-center`}>
|
<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>}
|
{achievement.subtitle && <p className="text-lg mt-2">{achievement.subtitle}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ const PatList = ({
|
|||||||
<h2 className="text-lg font-bold text-gray-800 dark:text-gray-100">{title}</h2>
|
<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">
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
{t('patlist.count', { count: entries.length })}
|
{t('patlist.count', { count: entries.length })}
|
||||||
{entries.length !== totalEntries ? ` von ${totalEntries}` : ''}
|
{entries.length !== totalEntries ? ` ${t('patlist.of')} ${totalEntries}` : ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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 { useTranslation } from '../i18n/LanguageContext';
|
||||||
import PatList from './PatList/PatList';
|
import PatList from './PatList/PatList';
|
||||||
import PatDetail from './PatDetail/PatDetail';
|
import PatDetail from './PatDetail/PatDetail';
|
||||||
import AnalysisTab from './Analysis/AnalysisTab';
|
const AnalysisTab = lazy(() => import('./Analysis/AnalysisTab'));
|
||||||
import { patTypes } from '../data/patTypes';
|
import { patTypes } from '../data/patTypes';
|
||||||
import { useAssessments } from '../hooks/useAssessments';
|
import { useAssessments } from '../hooks/useAssessments';
|
||||||
import { getAchievement, getPatTypeColor } from '../utils/patCalculations';
|
import { getAchievement, getPatTypeColor } from '../utils/patCalculations';
|
||||||
@@ -309,13 +309,15 @@ export default function PatTestManager({
|
|||||||
|
|
||||||
if (activeTab === 'analysis') {
|
if (activeTab === 'analysis') {
|
||||||
return (
|
return (
|
||||||
<AnalysisTab
|
<Suspense fallback={<div className="min-h-screen flex items-center justify-center text-gray-500 dark:text-gray-400">Laden…</div>}>
|
||||||
assessments={visibleAssessments}
|
<AnalysisTab
|
||||||
selectedPatType={selectedPatType}
|
assessments={visibleAssessments}
|
||||||
onPatTypeChange={setSelectedPatType}
|
selectedPatType={selectedPatType}
|
||||||
patTypes={visiblePatTypes}
|
onPatTypeChange={setSelectedPatType}
|
||||||
user={user}
|
patTypes={visiblePatTypes}
|
||||||
/>
|
user={user}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Check, ChevronDown } from 'lucide-react';
|
|||||||
import { countryOptions, getCountryLabel, getCountryOption } from '../data/countries';
|
import { countryOptions, getCountryLabel, getCountryOption } from '../data/countries';
|
||||||
import CountryFlag from './CountryFlag';
|
import CountryFlag from './CountryFlag';
|
||||||
import { useTranslation } from '../i18n/LanguageContext';
|
import { useTranslation } from '../i18n/LanguageContext';
|
||||||
|
import { getTranslator } from '../i18n/translations';
|
||||||
|
|
||||||
const emptyStatus = {
|
const emptyStatus = {
|
||||||
message: '',
|
message: '',
|
||||||
@@ -81,6 +82,8 @@ export default function ProfileTab({
|
|||||||
setStatus(emptyStatus);
|
setStatus(emptyStatus);
|
||||||
|
|
||||||
const result = await onSave(formValues);
|
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?.ok) {
|
||||||
if (result.session) {
|
if (result.session) {
|
||||||
@@ -89,14 +92,14 @@ export default function ProfileTab({
|
|||||||
|
|
||||||
setStatus({
|
setStatus({
|
||||||
message: result.emailUpdateRequested
|
message: result.emailUpdateRequested
|
||||||
? t('profile.saved_email')
|
? tNew('profile.saved_email')
|
||||||
: t('profile.saved'),
|
: tNew('profile.saved'),
|
||||||
error: ''
|
error: ''
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setStatus({
|
setStatus({
|
||||||
message: '',
|
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 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 }) {
|
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 (
|
return (
|
||||||
<LanguageContext.Provider value={t}>
|
<LanguageContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</LanguageContext.Provider>
|
</LanguageContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTranslation() {
|
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_factor': 'Faktor',
|
||||||
'detail.col_points': 'Points',
|
'detail.col_points': 'Points',
|
||||||
'detail.col_result': 'Ergebnis',
|
'detail.col_result': 'Ergebnis',
|
||||||
|
'detail.finalize_title': 'Test final abschließen',
|
||||||
|
'detail.share_title': 'Test als Link teilen',
|
||||||
|
'patlist.of': 'von',
|
||||||
|
|
||||||
// ── Speichern-Dialog ─────────────────────────────────────────────────────
|
// ── Speichern-Dialog ─────────────────────────────────────────────────────
|
||||||
'savedialog.title': 'Ungespeicherte Änderungen',
|
'savedialog.title': 'Ungespeicherte Änderungen',
|
||||||
@@ -182,6 +185,20 @@ const translations = {
|
|||||||
'training.history': 'Plan-Historie',
|
'training.history': 'Plan-Historie',
|
||||||
'training.no_history': 'Keine archivierten Pläne vorhanden.',
|
'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 ────────────────────────────────────────────────────────────
|
// ── Gap-Chart ────────────────────────────────────────────────────────────
|
||||||
'gapchart.no_data': 'Kein Gap-Chart verfügbar.',
|
'gapchart.no_data': 'Kein Gap-Chart verfügbar.',
|
||||||
'gapchart.title': 'Gap-Bar-Chart (Priorität)',
|
'gapchart.title': 'Gap-Bar-Chart (Priorität)',
|
||||||
@@ -347,6 +364,9 @@ const translations = {
|
|||||||
'detail.col_factor': 'Factor',
|
'detail.col_factor': 'Factor',
|
||||||
'detail.col_points': 'Points',
|
'detail.col_points': 'Points',
|
||||||
'detail.col_result': 'Result',
|
'detail.col_result': 'Result',
|
||||||
|
'detail.finalize_title': 'Finalize test',
|
||||||
|
'detail.share_title': 'Share test as link',
|
||||||
|
'patlist.of': 'of',
|
||||||
|
|
||||||
'savedialog.title': 'Unsaved changes',
|
'savedialog.title': 'Unsaved changes',
|
||||||
'savedialog.message': 'You have unsaved changes. Would you like to save them?',
|
'savedialog.message': 'You have unsaved changes. Would you like to save them?',
|
||||||
@@ -412,6 +432,14 @@ const translations = {
|
|||||||
'training.history': 'Plan history',
|
'training.history': 'Plan history',
|
||||||
'training.no_history': 'No archived plans available.',
|
'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.no_data': 'No gap chart available.',
|
||||||
'gapchart.title': 'Gap Bar Chart (Priority)',
|
'gapchart.title': 'Gap Bar Chart (Priority)',
|
||||||
'gapchart.aria': 'Gap bar chart',
|
'gapchart.aria': 'Gap bar chart',
|
||||||
@@ -573,6 +601,9 @@ const translations = {
|
|||||||
'detail.col_factor': 'Fattore',
|
'detail.col_factor': 'Fattore',
|
||||||
'detail.col_points': 'Punti',
|
'detail.col_points': 'Punti',
|
||||||
'detail.col_result': 'Risultato',
|
'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.title': 'Modifiche non salvate',
|
||||||
'savedialog.message': 'Hai modifiche non salvate. Vuoi salvarle?',
|
'savedialog.message': 'Hai modifiche non salvate. Vuoi salvarle?',
|
||||||
@@ -638,6 +669,14 @@ const translations = {
|
|||||||
'training.history': 'Cronologia piani',
|
'training.history': 'Cronologia piani',
|
||||||
'training.no_history': 'Nessun piano archiviato disponibile.',
|
'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.no_data': 'Nessun grafico gap disponibile.',
|
||||||
'gapchart.title': 'Grafico Gap (Priorità)',
|
'gapchart.title': 'Grafico Gap (Priorità)',
|
||||||
'gapchart.aria': 'Grafico a barre gap',
|
'gapchart.aria': 'Grafico a barre gap',
|
||||||
@@ -799,6 +838,9 @@ const translations = {
|
|||||||
'detail.col_factor': 'Factor',
|
'detail.col_factor': 'Factor',
|
||||||
'detail.col_points': 'Puntos',
|
'detail.col_points': 'Puntos',
|
||||||
'detail.col_result': 'Resultado',
|
'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.title': 'Cambios sin guardar',
|
||||||
'savedialog.message': 'Tienes cambios sin guardar. ¿Deseas guardarlos?',
|
'savedialog.message': 'Tienes cambios sin guardar. ¿Deseas guardarlos?',
|
||||||
@@ -864,6 +906,14 @@ const translations = {
|
|||||||
'training.history': 'Historial de planes',
|
'training.history': 'Historial de planes',
|
||||||
'training.no_history': 'No hay planes archivados disponibles.',
|
'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.no_data': 'No hay gráfico de gap disponible.',
|
||||||
'gapchart.title': 'Gráfico de Gap (Prioridad)',
|
'gapchart.title': 'Gráfico de Gap (Prioridad)',
|
||||||
'gapchart.aria': 'Gráfico de barras de gap',
|
'gapchart.aria': 'Gráfico de barras de gap',
|
||||||
@@ -1025,6 +1075,9 @@ const translations = {
|
|||||||
'detail.col_factor': 'Fator',
|
'detail.col_factor': 'Fator',
|
||||||
'detail.col_points': 'Pontos',
|
'detail.col_points': 'Pontos',
|
||||||
'detail.col_result': 'Resultado',
|
'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.title': 'Alterações não guardadas',
|
||||||
'savedialog.message': 'Tem alterações não guardadas. Deseja guardá-las?',
|
'savedialog.message': 'Tem alterações não guardadas. Deseja guardá-las?',
|
||||||
@@ -1090,6 +1143,14 @@ const translations = {
|
|||||||
'training.history': 'Histórico de planos',
|
'training.history': 'Histórico de planos',
|
||||||
'training.no_history': 'Sem planos arquivados disponíveis.',
|
'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.no_data': 'Sem gráfico de gap disponível.',
|
||||||
'gapchart.title': 'Gráfico de Gap (Prioridade)',
|
'gapchart.title': 'Gráfico de Gap (Prioridade)',
|
||||||
'gapchart.aria': 'Gráfico de barras de gap',
|
'gapchart.aria': 'Gráfico de barras de gap',
|
||||||
|
|||||||
@@ -3,6 +3,19 @@ import react from '@vitejs/plugin-react'
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
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: {
|
test: {
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
include: ['src/**/*.test.{js,jsx}']
|
include: ['src/**/*.test.{js,jsx}']
|
||||||
|
|||||||
Reference in New Issue
Block a user