diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 5ded77e..1895ebd 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -1,7 +1,7 @@ -import { useMemo } from 'react'; +import { useMemo, useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useLiveQuery } from 'dexie-react-hooks'; -import { db } from '../lib/db'; +import { db, importData, loadImportedImages, ImportResult } from '../lib/db'; import { computeStats, getExpiryStatus, getDaysUntilExpiry, formatDate, formatDaysUntil } from '../lib/utils'; import { useAppStore } from '../store/useAppStore'; import type { ProductCategory } from '../types'; @@ -12,6 +12,14 @@ import { PlusCircle, TrendingDown, ChevronRight, + Upload, + Camera, + Image, + BellRing, + WifiOff, + Lock, + HardDrive, + Loader2, } from 'lucide-react'; const URGENT_STATUS_COLORS: Record = { @@ -34,6 +42,40 @@ export function Dashboard() { const setPage = useAppStore((s) => s.setPage); const products = useLiveQuery(() => db.products.toArray()) ?? []; const { t } = useTranslation(); + const [importStatus, setImportStatus] = useState<{ message: string; type: 'success' | 'warning' | 'error' } | null>(null); + const [imageLoadProgress, setImageLoadProgress] = useState<{ loaded: number; total: number } | null>(null); + const fileInputRef = useRef(null); + + async function handleImport(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + try { + const text = await file.text(); + const result = await importData(text); + setImportStatus({ message: t('import.success', { count: result.imported }), type: 'success' }); + if (result.productsNeedingImages.length > 0) { + setImageLoadProgress({ loaded: 0, total: result.productsNeedingImages.length }); + await loadImportedImages(result.productsNeedingImages, (loaded, total) => { + setImageLoadProgress({ loaded, total }); + }); + setImageLoadProgress(null); + } + } catch (err) { + if (err instanceof ImportResult) { + setImportStatus({ message: err.message, type: 'warning' }); + if (err.productsNeedingImages.length > 0) { + setImageLoadProgress({ loaded: 0, total: err.productsNeedingImages.length }); + await loadImportedImages(err.productsNeedingImages, (loaded, total) => { + setImageLoadProgress({ loaded, total }); + }); + setImageLoadProgress(null); + } + } else { + setImportStatus({ message: t('import.error', { message: err instanceof Error ? err.message : t('import.importFailed') }), type: 'error' }); + } + } + e.target.value = ''; + } const { stats, activeProducts, urgentProducts, categoryBreakdown, total } = useMemo(() => { const s = computeStats(products); @@ -66,21 +108,109 @@ export function Dashboard() { if (activeProducts.length === 0) { return ( -
-
- +
+ {/* Header */} +
+
+ +
+

{t('onboarding.title')}

+

{t('onboarding.subtitle')}

-

{t('dashboard.noProducts')}

-

{t('dashboard.noProductsDesc')}

-
- + + - + +
+ + {/* Import Status */} + {importStatus && ( +

+ {importStatus.message} +

+ )} + + {imageLoadProgress && ( +
+
+ + {t('import.loadingImages', { loaded: imageLoadProgress.loaded, total: imageLoadProgress.total })} +
+
+
+
+
+ )} + + {/* Feature-Übersicht */} +
+

{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') }, + ].map((item, i) => ( +
+ {item.icon} + {item.text} +
+ ))} +
); diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 7bf7f26..d8cae01 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -29,6 +29,12 @@ import { Info, Globe, Loader2, + Camera, + Image, + BellRing, + WifiOff, + Lock, + HardDrive, } from 'lucide-react'; const LANGUAGES = [ @@ -49,6 +55,7 @@ export function Settings() { const [newLocation, setNewLocation] = useState(''); const [importStatus, setImportStatus] = useState<{ message: string; type: 'success' | 'warning' | 'error' } | null>(null); const [imageLoadProgress, setImageLoadProgress] = useState<{ loaded: number; total: number } | null>(null); + const [showInfo, setShowInfo] = useState(false); const [showImpressum, setShowImpressum] = useState(false); const [showDatenschutz, setShowDatenschutz] = useState(false); const [showAGB, setShowAGB] = useState(false); @@ -133,6 +140,39 @@ export function Settings() {

{t('settings.title')}

+ {/* Über PrepTrack / Info */} +
+ + {showInfo && ( +
+
+ {[ + { 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} + {item.text} +
+ ))} +
+
+ )} +
+ {/* Language */}

diff --git a/src/i18n/locales/ar/translation.json b/src/i18n/locales/ar/translation.json index 3ab6849..6e79100 100644 --- a/src/i18n/locales/ar/translation.json +++ b/src/i18n/locales/ar/translation.json @@ -305,6 +305,28 @@ "importResult": "تم استيراد {{imported}} منتجات، تم تخطي {{skipped}} (مكررة أو غير صالحة).", "unknownProduct": "منتج غير معروف" }, + "onboarding": { + "title": "مرحباً بك في PrepTrack", + "subtitle": "مدير المخزون الرقمي الخاص بك", + "getStarted": "كيف تبدأ", + "step1Title": "إضافة المنتجات", + "step1Desc": "امسح الباركود أو أضف المنتجات يدوياً مع تاريخ الصلاحية والفئة وموقع التخزين.", + "step2Title": "تتبع تواريخ الصلاحية", + "step2Desc": "احصل على إشعارات تلقائية قبل 30 و14 و7 و3 ويوم واحد من انتهاء الصلاحية.", + "step3Title": "النسخ الاحتياطي والمزامنة", + "step3Desc": "صدّر بياناتك كنسخة احتياطية JSON. عند الاستيراد، يتم تحميل صور المنتجات تلقائياً عبر الباركود.", + "features": "الميزات", + "featureOffline": "أولوية العمل بدون إنترنت — يعمل بالكامل بدون اتصال", + "featureCamera": "ماسح باركود مع كشف تلقائي للكاميرا الرئيسية", + "featureImages": "يتم تحميل صور المنتجات تلقائياً من Open Food Facts", + "featureNotifications": "تذكيرات محلية بالصلاحية — لا حاجة للسحابة", + "featureExport": "نسخ احتياطي JSON وتصدير CSV لبياناتك", + "featurePrivacy": "100% خاص — جميع البيانات تبقى على جهازك", + "startScan": "مسح الباركود", + "startManual": "إضافة يدوية", + "startImport": "استيراد نسخة احتياطية", + "dismiss": "فهمت، هيا نبدأ!" + }, "common": { "yes": "نعم", "no": "لا" diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index bc8212e..db2f8dc 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -305,6 +305,28 @@ "importResult": "{{imported}} Produkte importiert, {{skipped}} übersprungen (Duplikate oder ungültig).", "unknownProduct": "Unbekanntes Produkt" }, + "onboarding": { + "title": "Willkommen bei PrepTrack", + "subtitle": "Dein digitaler Vorratsmanager", + "getStarted": "So startest du", + "step1Title": "Produkte erfassen", + "step1Desc": "Scanne Barcodes oder erfasse Produkte manuell mit MHD, Kategorie und Lagerort.", + "step2Title": "MHD im Blick", + "step2Desc": "Erhalte automatische Benachrichtigungen 30, 14, 7, 3 und 1 Tag vor Ablauf.", + "step3Title": "Backup & Sync", + "step3Desc": "Exportiere deine Daten als JSON-Backup. Beim Import werden Produktbilder automatisch per Barcode geladen.", + "features": "Funktionen", + "featureOffline": "Offline-first — funktioniert komplett ohne Internet", + "featureCamera": "Barcode-Scanner mit automatischer Hauptkamera-Erkennung", + "featureImages": "Produktbilder werden automatisch von Open Food Facts geladen", + "featureNotifications": "Lokale MHD-Erinnerungen — keine Cloud nötig", + "featureExport": "JSON-Backup & CSV-Export für deine Daten", + "featurePrivacy": "100% privat — alle Daten bleiben auf deinem Gerät", + "startScan": "Barcode scannen", + "startManual": "Manuell erfassen", + "startImport": "Backup importieren", + "dismiss": "Verstanden, los geht's!" + }, "common": { "yes": "Ja", "no": "Nein" diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 1ecec87..5b796ed 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -305,6 +305,28 @@ "importResult": "{{imported}} products imported, {{skipped}} skipped (duplicates or invalid).", "unknownProduct": "Unknown product" }, + "onboarding": { + "title": "Welcome to PrepTrack", + "subtitle": "Your digital supply manager", + "getStarted": "Getting started", + "step1Title": "Add products", + "step1Desc": "Scan barcodes or add products manually with expiry date, category and storage location.", + "step2Title": "Track expiry dates", + "step2Desc": "Get automatic notifications 30, 14, 7, 3 and 1 day before expiry.", + "step3Title": "Backup & Sync", + "step3Desc": "Export your data as JSON backup. On import, product images are automatically loaded via barcode.", + "features": "Features", + "featureOffline": "Offline-first — works completely without internet", + "featureCamera": "Barcode scanner with automatic main camera detection", + "featureImages": "Product images loaded automatically from Open Food Facts", + "featureNotifications": "Local expiry reminders — no cloud needed", + "featureExport": "JSON backup & CSV export for your data", + "featurePrivacy": "100% private — all data stays on your device", + "startScan": "Scan barcode", + "startManual": "Add manually", + "startImport": "Import backup", + "dismiss": "Got it, let's go!" + }, "common": { "yes": "Yes", "no": "No" diff --git a/src/i18n/locales/fr/translation.json b/src/i18n/locales/fr/translation.json index 0880949..797a0b2 100644 --- a/src/i18n/locales/fr/translation.json +++ b/src/i18n/locales/fr/translation.json @@ -305,6 +305,28 @@ "importResult": "{{imported}} produits import\u00e9s, {{skipped}} ignor\u00e9(s) (doublons ou invalides).", "unknownProduct": "Produit inconnu" }, + "onboarding": { + "title": "Bienvenue sur PrepTrack", + "subtitle": "Votre gestionnaire de provisions num\u00e9rique", + "getStarted": "Pour commencer", + "step1Title": "Ajouter des produits", + "step1Desc": "Scannez des codes-barres ou ajoutez des produits manuellement avec date de p\u00e9remption, cat\u00e9gorie et lieu de stockage.", + "step2Title": "Suivre les dates de p\u00e9remption", + "step2Desc": "Recevez des notifications automatiques 30, 14, 7, 3 et 1 jour avant l'expiration.", + "step3Title": "Sauvegarde et synchronisation", + "step3Desc": "Exportez vos donn\u00e9es en sauvegarde JSON. Lors de l'importation, les images des produits sont charg\u00e9es automatiquement via le code-barres.", + "features": "Fonctionnalit\u00e9s", + "featureOffline": "Hors ligne d'abord \u2014 fonctionne compl\u00e8tement sans internet", + "featureCamera": "Scanner de codes-barres avec d\u00e9tection automatique de la cam\u00e9ra principale", + "featureImages": "Images des produits charg\u00e9es automatiquement depuis Open Food Facts", + "featureNotifications": "Rappels locaux de p\u00e9remption \u2014 pas de cloud n\u00e9cessaire", + "featureExport": "Sauvegarde JSON et export CSV de vos donn\u00e9es", + "featurePrivacy": "100% priv\u00e9 \u2014 toutes les donn\u00e9es restent sur votre appareil", + "startScan": "Scanner un code-barres", + "startManual": "Ajouter manuellement", + "startImport": "Importer une sauvegarde", + "dismiss": "Compris, c'est parti !" + }, "common": { "yes": "Oui", "no": "Non" diff --git a/src/i18n/locales/it/translation.json b/src/i18n/locales/it/translation.json index 104a99c..22e25c9 100644 --- a/src/i18n/locales/it/translation.json +++ b/src/i18n/locales/it/translation.json @@ -305,6 +305,28 @@ "importResult": "{{imported}} prodotti importati, {{skipped}} saltati (duplicati o non validi).", "unknownProduct": "Prodotto sconosciuto" }, + "onboarding": { + "title": "Benvenuto in PrepTrack", + "subtitle": "Il tuo gestore digitale delle scorte", + "getStarted": "Come iniziare", + "step1Title": "Aggiungi prodotti", + "step1Desc": "Scansiona codici a barre o aggiungi prodotti manualmente con scadenza, categoria e luogo di conservazione.", + "step2Title": "Tieni d'occhio le scadenze", + "step2Desc": "Ricevi notifiche automatiche 30, 14, 7, 3 e 1 giorno prima della scadenza.", + "step3Title": "Backup e sincronizzazione", + "step3Desc": "Esporta i tuoi dati come backup JSON. All'importazione, le immagini dei prodotti vengono caricate automaticamente tramite codice a barre.", + "features": "Funzionalit\u00e0", + "featureOffline": "Offline-first \u2014 funziona completamente senza internet", + "featureCamera": "Scanner di codici a barre con rilevamento automatico della fotocamera principale", + "featureImages": "Immagini dei prodotti caricate automaticamente da Open Food Facts", + "featureNotifications": "Promemoria locali sulle scadenze \u2014 nessun cloud necessario", + "featureExport": "Backup JSON ed esportazione CSV dei tuoi dati", + "featurePrivacy": "100% privato \u2014 tutti i dati restano sul tuo dispositivo", + "startScan": "Scansiona codice a barre", + "startManual": "Aggiungi manualmente", + "startImport": "Importa backup", + "dismiss": "Capito, iniziamo!" + }, "common": { "yes": "S\u00ec", "no": "No" diff --git a/src/i18n/locales/pt/translation.json b/src/i18n/locales/pt/translation.json index 6682c66..7c2c92c 100644 --- a/src/i18n/locales/pt/translation.json +++ b/src/i18n/locales/pt/translation.json @@ -305,6 +305,28 @@ "importResult": "{{imported}} produtos importados, {{skipped}} ignorados (duplicados ou inválidos).", "unknownProduct": "Produto desconhecido" }, + "onboarding": { + "title": "Bem-vindo ao PrepTrack", + "subtitle": "Seu gerenciador digital de estoque", + "getStarted": "Como começar", + "step1Title": "Cadastrar produtos", + "step1Desc": "Escaneie códigos de barras ou adicione produtos manualmente com validade, categoria e local de armazenamento.", + "step2Title": "Acompanhe a validade", + "step2Desc": "Receba notificações automáticas 30, 14, 7, 3 e 1 dia antes do vencimento.", + "step3Title": "Backup e sincronização", + "step3Desc": "Exporte seus dados como backup JSON. Na importação, as imagens dos produtos são carregadas automaticamente via código de barras.", + "features": "Funcionalidades", + "featureOffline": "Offline-first — funciona completamente sem internet", + "featureCamera": "Scanner de código de barras com detecção automática da câmera principal", + "featureImages": "Imagens dos produtos carregadas automaticamente do Open Food Facts", + "featureNotifications": "Lembretes locais de validade — sem necessidade de nuvem", + "featureExport": "Backup JSON e exportação CSV dos seus dados", + "featurePrivacy": "100% privado — todos os dados ficam no seu dispositivo", + "startScan": "Escanear código de barras", + "startManual": "Adicionar manualmente", + "startImport": "Importar backup", + "dismiss": "Entendi, vamos lá!" + }, "common": { "yes": "Sim", "no": "Não"