diff --git a/src/App.tsx b/src/App.tsx index 948e18f..a16c62e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,9 @@ import { useEffect, lazy, Suspense } from 'react'; import { motion, MotionConfig } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; +import { useLiveQuery } from 'dexie-react-hooks'; import { useAppStore } from './store/useAppStore'; -import { seedDefaults } from './lib/db'; +import { db, seedDefaults } from './lib/db'; import { startNotificationChecker } from './lib/notifications'; import { startSyncEngine } from './lib/sync'; import { ErrorBoundary } from './components/ErrorBoundary'; @@ -32,6 +34,30 @@ function LazyFallback() { return ; } +/** Kopfzeile: Wortmarke plus die eine Zahl, die immer zaehlt — wie viel liegt im Lager. */ +function AppHeader() { + const { t } = useTranslation(); + const activeCount = useLiveQuery(() => db.products.filter((p) => !p.archived).count(), [], null); + + return ( +
+
+
+ +

+ PrepTrack +

+
+ {activeCount !== null && activeCount > 0 && ( + + {t('products.productCount', { count: activeCount })} + + )} +
+
+ ); +} + function PageContent() { const currentPage = useAppStore((s) => s.currentPage); @@ -94,16 +120,9 @@ export default function App() {
-
-
-
- PrepTrack -

PrepTrack

-
-
-
+ -
+
{/* No AnimatePresence / mode="wait": exit-gated transitions can deadlock on rapid tab switches and leave the page blank. A plain keyed fade-in remounts the page on navigation — the old page diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 3ec670b..be5ebe6 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -144,12 +144,12 @@ export function Dashboard() {

{t('dashboard.noProducts')}

{t('dashboard.noProductsDesc')}

-
- - @@ -180,15 +180,15 @@ export function Dashboard() { {/* Feature-Übersicht */}
-

{t('onboarding.features')}

+

{t('onboarding.features')}

{[ - { icon: , text: t('onboarding.featureOffline') }, - { icon: , text: t('onboarding.featureCamera') }, - { icon: , text: t('onboarding.featureImages') }, - { icon: , text: t('onboarding.featureNotifications') }, - { icon: , text: t('onboarding.featureExport') }, - { icon: , text: t('onboarding.featurePrivacy') }, + { icon: , text: t('onboarding.featureOffline') }, + { icon: , text: t('onboarding.featureCamera') }, + { icon: , text: t('onboarding.featureImages') }, + { icon: , text: t('onboarding.featureNotifications') }, + { icon: , text: t('onboarding.featureExport') }, + { icon: , text: t('onboarding.featurePrivacy') }, ].map((item, i) => (
{item.icon} @@ -203,12 +203,30 @@ export function Dashboard() { return (
-
-
- - - - + {/* Lagerstand: eine Zahl, ein Balken, eine Legende — statt vier Ringen + plus separatem Verteilungsbalken mit denselben Daten. */} +
+ +
+

+

{t('dashboard.total')}

+
+
+ {stats.expiredCount + stats.criticalCount > 0 &&
} + {stats.warningCount + stats.soonCount > 0 &&
} + {stats.goodCount > 0 &&
} + {total === 0 &&
} +
+
+ 0 ? 'text-[color:var(--pt-crit)]' : 'text-gray-500'}> + {stats.expiredCount + stats.criticalCount} {t('dashboard.critical')} + + 0 ? 'text-[color:var(--pt-warn)]' : 'text-gray-500'}> + {stats.warningCount + stats.soonCount} {t('dashboard.soon')} + + 0 ? 'text-[color:var(--pt-ok)]' : 'text-gray-500'}> + {stats.goodCount} {t('dashboard.good')} +
@@ -219,8 +237,8 @@ export function Dashboard() {
- - {t('preparedness.cardTitle')} + + {t('preparedness.cardTitle')}

{preparedness.survivalDays !== null @@ -234,8 +252,8 @@ export function Dashboard() {

- {stats.totalProducts > 0 && ( -
- -
- {stats.expiredCount > 0 &&
} - {stats.criticalCount > 0 &&
} - {stats.warningCount > 0 &&
} - {stats.soonCount > 0 &&
} - {stats.goodCount > 0 &&
} -
-
- {t('dashboard.expired_count', { count: stats.expiredCount })} - {t('dashboard.warning_count', { count: stats.warningCount + stats.soonCount })} - {t('dashboard.ok_count', { count: stats.goodCount })} -
-
- )} - {urgentProducts.length > 0 && (
setPage('products')} className="flex shrink-0 items-center gap-0.5 text-xs text-green-400 hover:text-green-300"> + } @@ -290,9 +290,9 @@ export function Dashboard() {

{product.name}

-

{product.storageLocation} · {formatDate(product.expiryDate, product.expiryPrecision)}

+

{product.storageLocation} · {formatDate(product.expiryDate, product.expiryPrecision)}

- {formatDaysUntil(product.daysLeft)} + {formatDaysUntil(product.daysLeft)}
))} @@ -303,12 +303,12 @@ export function Dashboard() {
{categoryBreakdown.length > 0 && (
-

{t('dashboard.categories')}

+

{t('dashboard.categories')}

{categoryBreakdown.map(({ key, label, count }) => (
{label} - {count} + {count}
))}
@@ -317,14 +317,14 @@ export function Dashboard() {
- - {t('dashboard.lowStock')} + 0 ? 'text-[color:var(--pt-warn)]' : 'text-[color:var(--pt-accent)]'} /> + {t('dashboard.lowStock')}
-

0 ? 'text-yellow-400' : 'text-gray-300'}`}>

+

0 ? 'text-[color:var(--pt-warn)]' : 'text-gray-300'}`}>

- {t('dashboard.storageLocations')} -

+ {t('dashboard.storageLocations')} +

diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 92e6147..07f8b92 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -32,7 +32,7 @@ function NavItem({ label, icon: Icon, isActive, onClick }: NavItemProps) { ); } diff --git a/src/components/Preparedness.tsx b/src/components/Preparedness.tsx index 41f8ef3..60a54dc 100644 --- a/src/components/Preparedness.tsx +++ b/src/components/Preparedness.tsx @@ -137,7 +137,7 @@ export function Preparedness() {
-

{t('preparedness.title')}

+

{t('preparedness.title')}

{t('preparedness.subtitle')}

diff --git a/src/components/ProductForm.tsx b/src/components/ProductForm.tsx index 41a70f0..0f1c759 100644 --- a/src/components/ProductForm.tsx +++ b/src/components/ProductForm.tsx @@ -245,7 +245,7 @@ function InlineScanner({ onScanned, autoStart = false }: { onScanned: (data: { b @@ -471,7 +471,7 @@ function DateScanner({ onResult }: { onResult: (parsed: ParsedExpiry) => void })
@@ -720,7 +720,7 @@ export function ProductForm() { )} -

+

{editingProductId ? t('form.editTitle') : t('form.addTitle')}

@@ -750,10 +750,10 @@ export function ProductForm() {
) : (
- -
diff --git a/src/components/ProductList.tsx b/src/components/ProductList.tsx index 9e2dee8..0802198 100644 --- a/src/components/ProductList.tsx +++ b/src/components/ProductList.tsx @@ -145,10 +145,10 @@ export function ProductList() { return (
-

+

{showArchived ? t('products.archive') : t('products.title')}

-
@@ -161,7 +161,7 @@ export function ProductList() { placeholder={t('products.searchPlaceholder')} value={filters.search} onChange={(e) => setFilter('search', e.target.value)} - className={`w-full rounded-lg border border-primary-600 bg-primary-800 py-2 ps-10 text-gray-200 placeholder-gray-500 focus:border-green-500 focus:outline-none ${filters.search ? 'pe-10' : 'pe-4'}`} + className={`min-h-[44px] w-full rounded-lg border border-primary-600 bg-primary-800 py-2 ps-10 text-gray-200 placeholder-gray-500 focus:border-green-500 focus:outline-none ${filters.search ? 'pe-10' : 'pe-4'}`} /> {filters.search && (
- setFilter('category', e.target.value)} className="min-h-[44px] rounded-lg border border-primary-600 bg-primary-900 px-3 py-2 text-sm text-gray-300 focus:border-green-500 focus:outline-none"> {(['konserven', 'wasser', 'medizin', 'werkzeug', 'hygiene', 'lebensmittel', 'getranke', 'elektronik', 'kleidung', 'sonstiges'] as ProductCategory[]).map((key) => ( ))} - setFilter('location', e.target.value)} className="min-h-[44px] rounded-lg border border-primary-600 bg-primary-900 px-3 py-2 text-sm text-gray-300 focus:border-green-500 focus:outline-none"> {locations.map((loc) => ())} - setFilter('status', e.target.value)} className="min-h-[44px] rounded-lg border border-primary-600 bg-primary-900 px-3 py-2 text-sm text-gray-300 focus:border-green-500 focus:outline-none"> @@ -231,7 +231,7 @@ export function ProductList() {
)} -

+

{t('products.productCount', { count: filtered.length })}

@@ -266,28 +266,28 @@ export function ProductList() {
- {t('products.mhd')}: {formatDate(product.expiryDate, product.expiryPrecision)} ({formatDaysUntil(product.daysLeft)}) - {product.quantity} {product.unit} + {t('products.mhd')}: {formatDate(product.expiryDate, product.expiryPrecision)} ({formatDaysUntil(product.daysLeft)}) + {product.quantity} {product.unit} {product.minStock !== undefined && product.minStock > 0 && product.quantity < product.minStock && ( {t('products.lowStockWarning', { min: product.minStock })} )}
{!showArchived && ( -
- - -
)} {showArchived && ( -
-
@@ -298,8 +298,8 @@ export function ProductList() {
{t('products.confirmDelete')}
- - + +
)} @@ -443,14 +443,14 @@ function ConsumeModal({ productId, products, onConfirm, onClose }: { productId: if (val < step || (fraction < 1 && val === max)) return null; const label = fraction === 1 ? t('consume.allQuick') : `${Math.round(fraction * 100)}%`; return ( - + ); })}
{isAll &&

{t('consume.archiveWarning')}

}
diff --git a/src/components/SectionHeader.tsx b/src/components/SectionHeader.tsx index 20e76b0..999f9f5 100644 --- a/src/components/SectionHeader.tsx +++ b/src/components/SectionHeader.tsx @@ -3,14 +3,16 @@ import type { LucideIcon } from 'lucide-react'; type Tone = 'green' | 'blue' | 'orange' | 'yellow' | 'purple' | 'cyan' | 'red'; +/* Struktur spricht in Messing, Farbe bleibt dem Status vorbehalten: + nur "orange" und "red" faerben wirklich, alles andere ist Beschriftung. */ const TONES: Record = { - green: 'bg-green-500/15 text-green-400', - blue: 'bg-blue-500/15 text-blue-400', - orange: 'bg-orange-500/15 text-orange-400', - yellow: 'bg-yellow-500/15 text-yellow-400', - purple: 'bg-purple-500/15 text-purple-400', - cyan: 'bg-cyan-500/15 text-cyan-400', - red: 'bg-red-500/15 text-red-400', + green: 'text-[color:var(--pt-accent)]', + blue: 'text-[color:var(--pt-accent)]', + orange: 'text-[color:var(--pt-warn)]', + yellow: 'text-[color:var(--pt-accent)]', + purple: 'text-[color:var(--pt-accent)]', + cyan: 'text-[color:var(--pt-accent)]', + red: 'text-[color:var(--pt-crit)]', }; interface SectionHeaderProps { @@ -22,16 +24,15 @@ interface SectionHeaderProps { className?: string; } -/** Consistent section header: a tinted icon badge + title (matches the score page). */ +/** Abschnittskopf im Inventarblatt-Stil: Symbol, Versalien-Beschriftung, feine Linie. */ export function SectionHeader({ icon: Icon, title, tone = 'green', action, className = 'mb-3' }: SectionHeaderProps) { + const toneClass = TONES[tone]; + return ( -
-
- - - -

{title}

-
+
+ +

{title}

+
); diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 85fbcc2..2bd9667 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -5,7 +5,6 @@ import { useLiveQuery } from 'dexie-react-hooks'; import { db, addStorageLocation, deleteStorageLocation, exportData, exportCSV, importData, loadImportedImages, ImportResult } from '../lib/db'; import { requestNotificationPermission, getNotificationPermissionStatus } from '../lib/notifications'; import { useDarkMode } from '../hooks/useDarkMode'; -import { usePWAInstall } from '../hooks/usePWAInstall'; import { useAppStore } from '../store/useAppStore'; import { downloadFile } from '../lib/utils'; import { encryptBackup, decryptBackup, isEncryptedBackup } from '../lib/cryptoBackup'; @@ -31,8 +30,6 @@ import { FileJson, Shield, Heart, - Smartphone, - Share, FileText, ChevronDown, ChevronUp, @@ -68,12 +65,10 @@ function formatSyncTime(value?: string): string { export function Settings() { const [isDark, toggleDark] = useDarkMode(); const { notificationsEnabled, setNotificationsEnabled } = useAppStore(); - const { isInstallable, isInstalled, isIOS, install } = usePWAInstall(); const locations = useLiveQuery(() => db.storageLocations.toArray()) ?? []; const allProducts = useLiveQuery(() => db.products.toArray()) ?? []; const [newLocation, setNewLocation] = useState(''); const [importStatus, setImportStatus] = useState<{ message: string; type: 'success' | 'warning' | 'error' } | null>(null); - const [installError, setInstallError] = useState(null); const [locationError, setLocationError] = useState(null); const [syncConfigState, setSyncConfigState] = useState(() => getSyncConfig()); const [syncServerUrl, setSyncServerUrl] = useState(syncConfigState.serverUrl); @@ -174,14 +169,6 @@ export function Settings() { await applyImportedJson(text); } - async function handleInstall() { - setInstallError(null); - const success = await install(); - if (!success) { - setInstallError(t('settings.installError')); - } - } - function handleLanguageChange(langCode: string) { i18n.changeLanguage(langCode); } @@ -281,14 +268,14 @@ export function Settings() { return (
-

{t('settings.title')}

+

{t('settings.title')}

{/* Über PrepTrack / Info */}
- {installError && ( -

- {installError} -

- )} - {!isInstallable && ( -

- {t('settings.installTip')} -

- )} -
- )} - - )} - - {isInstalled && ( -
-
- - {t('settings.appInstalled')} -
-
- )} {/* Appearance */}

- + {t('settings.appearance')}

@@ -531,9 +467,9 @@ export function Settings() { deleteStorageLocation(loc.id!); }} aria-label={t('settings.deleteLocation', { name: loc.name })} - className="rounded p-1.5 text-gray-400 transition-colors hover:bg-primary-600 hover:text-red-400" + className="flex min-h-[44px] min-w-[44px] items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-primary-600 hover:text-red-400" > - +
))} @@ -548,7 +484,7 @@ export function Settings() { {/* Data Management */}

- + {t('settings.dataManagement')}

@@ -556,7 +492,7 @@ export function Settings() { onClick={handleExportJSON} className="flex w-full items-center gap-3 rounded-lg bg-primary-700/50 px-4 py-3 text-gray-200 hover:bg-primary-700" > - +
{t('settings.jsonBackup')}

{t('settings.jsonBackupDesc')}

@@ -642,7 +578,7 @@ export function Settings() { {/* Sync */}

- + {t('sync.title')}

@@ -658,7 +594,7 @@ export function Settings() { value={syncServerUrl} onChange={(e) => setSyncServerUrl(e.target.value)} placeholder="http://192.168.0.20:8787" - className="w-full rounded-lg border border-primary-600 bg-primary-900 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-sky-500 focus:outline-none" + className="min-h-[44px] w-full rounded-lg border border-primary-600 bg-primary-900 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-sky-500 focus:outline-none" />
@@ -669,7 +605,7 @@ export function Settings() { value={syncDeviceName} onChange={(e) => setSyncDeviceName(e.target.value)} placeholder={t('sync.deviceNamePlaceholder')} - className="w-full rounded-lg border border-primary-600 bg-primary-900 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-sky-500 focus:outline-none" + className="min-h-[44px] w-full rounded-lg border border-primary-600 bg-primary-900 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-sky-500 focus:outline-none" />
@@ -681,7 +617,7 @@ export function Settings() { value={syncCode} onChange={(e) => setSyncCode(e.target.value)} placeholder={t('sync.syncCodePlaceholder')} - className="w-full rounded-lg border border-primary-600 bg-primary-900 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-sky-500 focus:outline-none" + className="min-h-[44px] w-full rounded-lg border border-primary-600 bg-primary-900 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-sky-500 focus:outline-none" />
)} @@ -689,7 +625,7 @@ export function Settings() {
@@ -698,7 +634,7 @@ export function Settings() { @@ -706,7 +642,7 @@ export function Settings() { @@ -822,10 +758,10 @@ export function Settings() {