diff --git a/package.json b/package.json index ad7cd7d..07573ab 100644 --- a/package.json +++ b/package.json @@ -22,18 +22,19 @@ "packageManager": "pnpm@10.13.1", "type": "module", "dependencies": { - "react": "19.1.0", - "react-dom": "19.1.0", + "dotenv": "^17.2.2", + "lucide-react": "^0.544.0", "next": "15.5.3", - "dotenv": "^17.2.2" + "react": "19.1.0", + "react-dom": "19.1.0" }, "devDependencies": { - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "@biomejs/biome": "2.2.0", "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", + "@tailwindcss/postcss": "^4", "husky": "^9.1.7", - "lint-staged": "^16.1.6" + "lint-staged": "^16.1.6", + "tailwindcss": "^4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f09d38c..84d1501 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: dotenv: specifier: ^17.2.2 version: 17.2.2 + lucide-react: + specifier: ^0.544.0 + version: 0.544.0(react@19.1.0) next: specifier: 15.5.3 version: 15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -862,6 +865,11 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + lucide-react@0.544.0: + resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} @@ -1855,6 +1863,10 @@ snapshots: strip-ansi: 7.1.2 wrap-ansi: 9.0.2 + lucide-react@0.544.0(react@19.1.0): + dependencies: + react: 19.1.0 + magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 diff --git a/src/app/favicon.ico b/src/app/favicon.ico index 718d6fe..90ec259 100644 Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..9159b88 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -23,4 +23,4 @@ body { background: var(--background); color: var(--foreground); font-family: Arial, Helvetica, sans-serif; -} +} \ No newline at end of file diff --git a/src/app/layout.js b/src/app/layout.js index 7bf337d..a2b4b3d 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -1,28 +1,29 @@ -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); +"use client"; -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +import { Inter } from "next/font/google"; +import "./globals.css"; +import { ThemeProvider } from "../contexts/ThemeContext"; +import { LanguageProvider } from "../contexts/LanguageContext"; +import { ToastProvider } from "../components/ToastProvider"; -export const metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; +const inter = Inter({ subsets: ["latin"] }); export default function RootLayout({ children }) { return ( - - - {children} + + + PastePick - Smart Toothpaste Analyzer + + + + + + {children} + + ); diff --git a/src/app/page.js b/src/app/page.js index d0d0a6a..9d3bcaf 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,103 +1,100 @@ -import Image from "next/image"; +"use client"; + +import { useState } from "react"; +import BottomNavigation from "../components/BottomNavigation"; +import HomePage from "../components/pages/HomePage"; +import ScanPage from "../components/pages/ScanPage"; + +import SettingsPage from "../components/pages/SettingsPage"; +import ProductPage from "../components/pages/ProductPage"; +import CameraScanner from "../components/CameraScanner"; export default function Home() { - return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.js - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+ const [activeTab, setActiveTab] = useState("home"); + const [currentView, setCurrentView] = useState("main"); // 'main', 'product', or 'camera' + const [scannedProduct, setScannedProduct] = useState(null); -
- - Vercel logomark - Deploy now - - - Read our docs - -
-
- + ); + } + }; + + return ( +
+ {currentView === "product" + ? + : currentView === "camera" + ? + : <> + {/* Page Content */} +
{renderPage()}
+ + {/* Bottom Navigation */} + + }
); } diff --git a/src/components/BottomNavigation.js b/src/components/BottomNavigation.js new file mode 100644 index 0000000..2fdd36e --- /dev/null +++ b/src/components/BottomNavigation.js @@ -0,0 +1,74 @@ +import { Home, Heart, Scan, Clock, Settings } from "lucide-react"; +import { useLanguage } from "../contexts/LanguageContext"; + +export default function BottomNavigation({ + activeTab, + onTabChange, + onStartCamera, +}) { + const { t } = useLanguage(); + + const tabs = [ + { id: "home", label: t("home"), icon: Home }, + { id: "scan", label: t("scan"), icon: Scan }, + { id: "settings", label: t("settings"), icon: Settings }, + ]; + + return ( +
+
+ {tabs.map((tab) => { + const Icon = tab.icon; + const isActive = activeTab === tab.id; + const isScan = tab.id === "scan"; + + return ( + + ); + })} +
+
+ ); +} diff --git a/src/components/CameraScanner.js b/src/components/CameraScanner.js new file mode 100644 index 0000000..cdfe63c --- /dev/null +++ b/src/components/CameraScanner.js @@ -0,0 +1,497 @@ +"use client"; + +import { useRef, useState, useEffect } from "react"; +import { X, Camera, Image, Smartphone, Zap, ZapOff } from "lucide-react"; +import { + analyzeIngredients, + sampleIngredientTexts, +} from "../services/mockAnalysisService"; +import { useLanguage } from "../contexts/LanguageContext"; +import { useToast } from "../components/ToastProvider"; + +export default function SimpleCameraScanner({ onClose, onCapture }) { + const cameraInputRef = useRef(null); + const galleryInputRef = useRef(null); + + // State management + const [capturedImage, setCapturedImage] = useState(null); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const [analysisResult, setAnalysisResult] = useState(null); + const [currentStep, setCurrentStep] = useState("select"); // 'select', 'captured', 'analyzing', 'complete' + + // Hooks + const { t, translateIngredients } = useLanguage(); + const { showSuccess, showError, showInfo, showWarning } = useToast(); + + // Detect mobile device on component mount + useEffect(() => { + const detectMobile = () => { + const userAgent = navigator.userAgent.toLowerCase(); + const mobileKeywords = [ + "android", + "webos", + "iphone", + "ipad", + "ipod", + "blackberry", + "iemobile", + "opera mini", + ]; + const isMobileDevice = mobileKeywords.some((keyword) => + userAgent.includes(keyword), + ); + const isSmallScreen = window.innerWidth <= 768; + + setIsMobile(isMobileDevice || isSmallScreen); + }; + + detectMobile(); + window.addEventListener("resize", detectMobile); + return () => window.removeEventListener("resize", detectMobile); + }, []); + + // Open device camera + const openCamera = () => { + if (!cameraInputRef.current) return; + + try { + if (isMobile) { + // Set camera attributes for mobile + cameraInputRef.current.setAttribute("capture", "environment"); + cameraInputRef.current.setAttribute("accept", "image/*"); + cameraInputRef.current.click(); + showInfo(t("scanningInProgress")); + } else { + showWarning(t("cameraWorksBesetMobile")); + // Still allow desktop users to select files + cameraInputRef.current.click(); + } + } catch (error) { + console.error("Camera error:", error); + showError(t("cameraError")); + } + }; + + // Open gallery/file picker + const openGallery = () => { + if (!galleryInputRef.current) return; + + try { + galleryInputRef.current.click(); + showInfo(t("chooseFromGallery")); + } catch (error) { + console.error("Gallery error:", error); + showError("Unable to open gallery"); + } + }; + + // Handle file selection from camera or gallery + const handleFileSelect = (event) => { + const file = event.target.files[0]; + + if (!file) { + showWarning("No file selected"); + return; + } + + if (!file.type.startsWith("image/")) { + showError("Please select a valid image file"); + return; + } + + // Check file size (limit to 10MB) + if (file.size > 10 * 1024 * 1024) { + showError("Image too large. Please select an image smaller than 10MB"); + return; + } + + const reader = new FileReader(); + + reader.onload = (e) => { + setCapturedImage(e.target.result); + setCurrentStep("captured"); + showSuccess(t("scanSuccessful")); + }; + + reader.onerror = () => { + showError("Error reading image file"); + }; + + reader.readAsDataURL(file); + + // Reset input value to allow selecting the same file again + event.target.value = ""; + }; + + // Simulate OCR and analyze ingredients + const analyzeImage = async () => { + if (!capturedImage) { + showError("No image to analyze"); + return; + } + + setIsAnalyzing(true); + setCurrentStep("analyzing"); + showInfo(t("analyzingImage")); + + try { + // Simulate different ingredient texts including foreign languages + const mockIngredientTexts = [ + // English ingredients + "INGREDIENTS: Potassium Nitrate, Sodium Fluoride, Hydrated Silica, Titanium Dioxide, SLS, Natural Mint Flavor, Xylitol", + "Ingredients: Water, Calcium Carbonate, Glycerin, Sodium Lauryl Sulfate, Natural Flavors, Cellulose Gum, Sodium Saccharin", + + // Finnish ingredients + "AINESOSAT: Natriumfluoridi, Kalsiumkarbonaatti, Natriumlauryylisulfaatti, Luonnolliset aromit, Mentoli, Ksylitoli, Glyseriini", + "Ainesosat: Vesi, Hydratoitu piidioksidi, Kaliumnitraatti, Titaanidioksidi, Sorbitoli, Selluloosakumi", + + // German ingredients + "INHALTSSTOFFE: Natriumfluorid, Kalziumkarbonat, Natriumlaurylsulfat, Titandioxid, Glyzerin, NatĂĽrliche Aromen", + + // Mixed/challenging cases + "Ingredients: Potassium Nitrate 5%, SLS, Titanium Dioxide, Natural Flavors, Preservatives", + "AINESOSAT: Natriumfluoridi 0.32%, Kalsiumkarbonaatti, Mentoli, Artificial Colors", + ]; + + // Select random sample for demo + const randomSample = + mockIngredientTexts[ + Math.floor(Math.random() * mockIngredientTexts.length) + ]; + + // Show which text was "detected" + console.log("Mock OCR detected:", randomSample); + + // Translate ingredients from foreign languages to English + const translatedText = translateIngredients(randomSample); + console.log("Translated to:", translatedText); + + showInfo(t("processingResults")); + + // Simulate OCR processing delay + await new Promise((resolve) => setTimeout(resolve, 1500)); + + // Analyze the translated ingredients + const result = await analyzeIngredients(translatedText); + + if (result.success) { + setAnalysisResult(result.analysis); + setCurrentStep("complete"); + + // Show success message with score + const scoreMessage = `${t("analysisComplete")} - ${t("overallScore")}: ${result.analysis.overallScore}/10`; + showSuccess(scoreMessage, 6000); // Show longer for important result + + // Create complete product data object + const productData = { + // Basic info + name: "Scanned Toothpaste Product", + brand: "Unknown Brand", + image: "đź“·", + + // Analysis results + overallScore: result.analysis.overallScore, + totalIngredients: result.analysis.totalIngredients, + overallAssessment: result.analysis.overallAssessment, + keyIngredients: result.analysis.keyIngredients, + primaryConcerns: result.analysis.primaryConcerns, + primaryBenefits: result.analysis.primaryBenefits, + scoreBreakdown: result.analysis.scoreBreakdown, + ingredients: result.analysis.ingredients, + + // Debug info + rawText: randomSample, + translatedText: translatedText, + capturedImage: capturedImage, + analysisTimestamp: new Date().toISOString(), + }; + + // Pass complete data to parent component + setTimeout(() => { + onCapture && onCapture(productData); + }, 1000); // Small delay to show success message + } else { + // Handle analysis failure + setCurrentStep("captured"); + const errorMessage = result.error || t("scanError"); + showError(errorMessage); + console.error("Analysis failed:", result.error); + } + } catch (error) { + setCurrentStep("captured"); + const errorMessage = `${t("scanError")}: ${error.message}`; + showError(errorMessage); + console.error("Analysis error:", error); + } finally { + setIsAnalyzing(false); + } + }; + + // Retake photo - reset to selection state + const retakePhoto = () => { + setCapturedImage(null); + setAnalysisResult(null); + setCurrentStep("select"); + setIsAnalyzing(false); + showInfo("Ready to scan again"); + }; + + // Demo function for testing without camera + const runDemo = () => { + // Create mock image data + const mockImageData = + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iI2Y4ZmFmYyIvPjx0ZXh0IHg9IjE1MCIgeT0iNzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzM3NDE1MSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+SW5ncmVkaWVudHMgTGlzdDwvdGV4dD48dGV4dCB4PSIxNTAiIHk9IjEwMCIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBmaWxsPSIjNmI3Mjg0IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5Tb2RpdW0gRmx1b3JpZGU8L3RleHQ+PHRleHQgeD0iMTUwIiB5PSIxMjAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzZiNzI4NCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Q2FsY2l1bSBDYXJib25hdGU8L3RleHQ+PHRleHQgeD0iMTUwIiB5PSIxNDAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzZiNzI4NCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+UG90YXNzaXVtIE5pdHJhdGU8L3RleHQ+PHRleHQgeD0iMTUwIiB5PSIxNzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMCIgZmlsbD0iIzllYTNhZiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+RGVtbyBJbWFnZTwvdGV4dD48L3N2Zz4="; + + setCapturedImage(mockImageData); + setCurrentStep("captured"); + showSuccess("Demo: " + t("scanSuccessful")); + }; + + // Render different UI based on current step + const renderContent = () => { + switch (currentStep) { + case "select": + return renderSelectionScreen(); + case "captured": + return renderCapturedScreen(); + case "analyzing": + return renderAnalyzingScreen(); + case "complete": + return renderCompleteScreen(); + default: + return renderSelectionScreen(); + } + }; + + // Initial selection screen + const renderSelectionScreen = () => ( +
+
+ +

+ {t("scanIngredients")} +

+

{t("analyzeIngredients")}

+ + {/* Mobile detection indicator */} + {isMobile && ( +
+ + + {t("mobileDetected")} + +
+ )} +
+ + {/* Main action buttons */} +
+ + + +
+ + {/* Demo button */} +
+ +
+ + {/* Helpful instructions */} +
+

+ 📱 {t("scanningTips")}: +

+
    +
  • • {t("scanTip1")}
  • +
  • • {t("scanTip2")}
  • +
  • • {t("scanTip3")}
  • +
  • • {t("scanTip4")}
  • +
  • • {t("scanTip5")}
  • + {!isMobile && ( +
  • • {t("cameraWorksBesetMobile")}
  • + )} +
+
+
+ ); + + // Screen showing captured image with action buttons + const renderCapturedScreen = () => ( +
+
+ Captured ingredients +
+ +
+ + +
+ +

+ Review your image and click analyze to get ingredient safety scores +

+
+ ); + + // Analysis in progress screen + const renderAnalyzingScreen = () => ( +
+
+ {/* Animated analysis icon */} +
+
+ +
+ +

+ {t("analyzing")} +

+

{t("processingResults")}

+
+ + {/* Progress steps */} +
+
+
+
+ Reading ingredient text... +
+
+
+ Translating foreign ingredients... +
+
+
+ Calculating safety scores... +
+
+
+
+ ); + + // Analysis complete screen + const renderCompleteScreen = () => ( +
+
+
+ + + +
+ +

+ {t("analysisComplete")} +

+ + {analysisResult && ( +
+
+ {analysisResult.overallScore}/10 +
+
+ {analysisResult.overallAssessment} +
+
+ )} +
+ +

+ Redirecting to detailed analysis... +

+
+ ); + + return ( +
+ {/* Header */} +
+
+ +

{t("scanToothpaste")}

+
+
+
+ + {/* Hidden file inputs */} + + + + + {/* Main content area */} +
+ {renderContent()} +
+ + {/* Footer info */} +
+
+

+ Supports: English, Finnish, German, French ingredients +

+
+
+
+ ); +} diff --git a/src/components/LanguageSelector.js b/src/components/LanguageSelector.js new file mode 100644 index 0000000..f290070 --- /dev/null +++ b/src/components/LanguageSelector.js @@ -0,0 +1,395 @@ +"use client"; + +import { useState } from "react"; +import { Globe, Check, ChevronDown } from "lucide-react"; +import { useLanguage } from "../contexts/LanguageContext"; +import { useTheme } from "../contexts/ThemeContext"; + +// Main Language Selector Component +export default function LanguageSelector({ + showLabel = true, + size = "default", +}) { + const { + language, + changeLanguage, + t, + getAvailableLanguages, + getCurrentLanguageInfo, + } = useLanguage(); + const { isDark } = useTheme(); + const [isOpen, setIsOpen] = useState(false); + + const availableLanguages = getAvailableLanguages(); + const currentLang = getCurrentLanguageInfo(); + + const handleLanguageChange = (langCode) => { + changeLanguage(langCode); + setIsOpen(false); + }; + + const sizeClasses = { + small: "text-sm p-2", + default: "text-base p-3", + large: "text-lg p-4", + }; + + const iconSizes = { + small: 16, + default: 20, + large: 24, + }; + + return ( +
+ {/* Language Selector Button */} + + + {/* Dropdown Menu */} + {isOpen && ( + <> + {/* Backdrop */} +
setIsOpen(false)} + /> + + {/* Menu */} +
+ {/* Header */} +
+

+ {t("selectLanguage")} +

+
+ + {/* Language Options */} +
+ {availableLanguages.map((lang) => ( + + ))} +
+ + {/* Footer */} +
+ {t("currentLanguage")}: {currentLang.nativeName} +
+
+ + )} +
+ ); +} + +// Compact version for navigation bars +export function CompactLanguageSelector() { + const { language, changeLanguage, getAvailableLanguages } = useLanguage(); + const { isDark } = useTheme(); + const [isOpen, setIsOpen] = useState(false); + + const availableLanguages = getAvailableLanguages(); + const currentLang = availableLanguages.find((lang) => lang.code === language); + + const handleLanguageChange = (langCode) => { + changeLanguage(langCode); + setIsOpen(false); + }; + + return ( +
+ {/* Compact Button */} + + + {/* Dropdown Menu */} + {isOpen && ( + <> + {/* Backdrop */} +
setIsOpen(false)} + /> + + {/* Menu */} +
+ {availableLanguages.map((lang) => ( + + ))} +
+ + )} +
+ ); +} + +// Settings page version with full details - ALWAYS WHITE THEME +export function DetailedLanguageSelector() { + const { language, changeLanguage, t, getAvailableLanguages } = useLanguage(); + // Remove useTheme - always use light theme + const availableLanguages = getAvailableLanguages(); + + return ( +
+

{t("language")}

+ +
+ {availableLanguages.map((lang) => ( + + ))} +
+ +
+ {t("changeLanguage")} • {t("currentLanguage")}:{" "} + {availableLanguages.find((l) => l.code === language)?.nativeName} +
+
+ ); +} + +// Inline Language Switcher (for headers/toolbars) +export function InlineLanguageSelector() { + const { language, changeLanguage, getAvailableLanguages } = useLanguage(); + const { isDark } = useTheme(); + const availableLanguages = getAvailableLanguages(); + + return ( +
+ {availableLanguages.map((lang) => ( + + ))} +
+ ); +} + +// Modal Language Selector (for first-time setup) +export function LanguageModal({ isOpen, onClose, onSelect }) { + const { getAvailableLanguages, t } = useLanguage(); + const { isDark } = useTheme(); + const availableLanguages = getAvailableLanguages(); + + const handleSelect = (langCode) => { + onSelect(langCode); + onClose(); + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

+ Choose Your Language +

+

+ Select your preferred language for PastPick +

+
+ + {/* Language Options */} +
+
+ {availableLanguages.map((lang) => ( + + ))} +
+
+
+
+ ); +} + +// Language Badge (shows current language) +export function LanguageBadge() { + const { getCurrentLanguageInfo } = useLanguage(); + const { isDark } = useTheme(); + const currentLang = getCurrentLanguageInfo(); + + return ( +
+ + {currentLang.code.toUpperCase()} +
+ ); +} diff --git a/src/components/Providers.js b/src/components/Providers.js new file mode 100644 index 0000000..30efb88 --- /dev/null +++ b/src/components/Providers.js @@ -0,0 +1,15 @@ +"use client"; + +import { ThemeProvider } from "../contexts/ThemeContext"; +import { LanguageProvider } from "../contexts/LanguageContext"; +import { ToastProvider } from "./ToastProvider"; + +export default function Providers({ children }) { + return ( + + + {children} + + + ); +} diff --git a/src/components/ToastProvider.js b/src/components/ToastProvider.js new file mode 100644 index 0000000..4de0d5d --- /dev/null +++ b/src/components/ToastProvider.js @@ -0,0 +1,132 @@ +"use client"; + +import { createContext, useContext, useState } from "react"; +import { CheckCircle, AlertTriangle, XCircle, Info, X } from "lucide-react"; +import { useTheme } from "../contexts/ThemeContext"; + +const ToastContext = createContext(); + +export const ToastProvider = ({ children }) => { + const [toasts, setToasts] = useState([]); + const { isDark } = useTheme(); + + const addToast = (message, type = "info", duration = 4000) => { + const id = Date.now() + Math.random(); + const toast = { id, message, type, duration }; + + setToasts((prev) => [...prev, toast]); + + if (duration > 0) { + setTimeout(() => { + removeToast(id); + }, duration); + } + + return id; + }; + + const removeToast = (id) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }; + + const showSuccess = (message, duration) => + addToast(message, "success", duration); + const showError = (message, duration) => addToast(message, "error", duration); + const showWarning = (message, duration) => + addToast(message, "warning", duration); + const showInfo = (message, duration) => addToast(message, "info", duration); + + return ( + + {children} + + {/* Toast Container */} +
+ {toasts.map((toast) => ( + removeToast(toast.id)} + isDark={isDark} + /> + ))} +
+
+ ); +}; + +const Toast = ({ toast, onClose, isDark }) => { + const getIcon = () => { + switch (toast.type) { + case "success": + return ; + case "error": + return ; + case "warning": + return ; + default: + return ; + } + }; + + const getColors = () => { + switch (toast.type) { + case "success": + return isDark + ? "bg-green-800 border-green-700 text-green-100" + : "bg-green-50 border-green-200 text-green-800"; + case "error": + return isDark + ? "bg-red-800 border-red-700 text-red-100" + : "bg-red-50 border-red-200 text-red-800"; + case "warning": + return isDark + ? "bg-amber-800 border-amber-700 text-amber-100" + : "bg-amber-50 border-amber-200 text-amber-800"; + default: + return isDark + ? "bg-gray-800 border-gray-700 text-gray-100" + : "bg-white border-gray-200 text-gray-800"; + } + }; + + return ( +
+
+
{getIcon()}
+
+

{toast.message}

+
+ +
+
+ ); +}; + +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { + throw new Error("useToast must be used within a ToastProvider"); + } + return context; +}; diff --git a/src/components/pages/HomePage.js b/src/components/pages/HomePage.js new file mode 100644 index 0000000..b040aac --- /dev/null +++ b/src/components/pages/HomePage.js @@ -0,0 +1,89 @@ +import { + Camera, + Search, + Shield, + Sparkles, + CheckCircle, + TrendingUp, +} from "lucide-react"; +import { useLanguage } from "../../contexts/LanguageContext"; + +export default function HomePage({ onProductScanned, onStartCamera }) { + const { t } = useLanguage(); + + return ( +
+ {/* Header */} +
+
+

{t("appName")}

+
+
+ + {/* Quick Stats */} +
+
+
2,847
+
{t("productsScanned")}
+
+
+
94%
+
{t("safeIngredients")}
+
+
+ + {/* Quick Actions */} +
+ + + +
+ + {/* Features */} +
+
+
+ +
+

+ {t("safetyAnalysis")} +

+

{t("safetyAnalysisDesc")}

+
+
+
+ +
+
+ +
+

+ {t("smartRecommendations")} +

+

+ {t("smartRecommendationsDesc")} +

+
+
+
+
+ + {/* Footer */} +
+

{t("poweredBy")}

+ +
+
+ ); +} diff --git a/src/components/pages/ProductPage.js b/src/components/pages/ProductPage.js new file mode 100644 index 0000000..8d59545 --- /dev/null +++ b/src/components/pages/ProductPage.js @@ -0,0 +1,686 @@ +"use client"; + +import { useState } from "react"; +import { + ArrowLeft, + Heart, + Share, + Star, + Shield, + CheckCircle, + AlertTriangle, + XCircle, + TrendingUp, + TrendingDown, +} from "lucide-react"; + +export default function ProductPage({ product, onBack }) { + const [activeTab, setActiveTab] = useState("ingredients"); + + // Sample product data - you'll replace this with real analysis data + const sampleProduct = product || { + name: "Sensodyne Pronamel Gentle Whitening", + brand: "Sensodyne", + image: "🦷", + overallScore: 8.7, // 1-10 scale instead of percentage + totalIngredients: 12, + overallAssessment: "Excellent choice with high-quality, safe ingredients", + keyIngredients: { + positive: ["Potassium Nitrate", "Sodium Fluoride", "Calcium Carbonate"], + negative: ["Titanium Dioxide", "Sodium Lauryl Sulfate"], + }, + primaryConcerns: [ + "May cause irritation in sensitive individuals", + "Contains whitening agents under review", + ], + primaryBenefits: [ + "Reduces sensitivity", + "Prevents cavities", + "Strengthens enamel", + ], + scoreBreakdown: { + beneficial: 5, + moderate: 4, + concerning: 2, + unknown: 1, + }, + }; + + const getScoreColor = (score) => { + if (score >= 8.5) return "text-green-600 bg-green-50 border-green-200"; + if (score >= 7.0) return "text-blue-600 bg-blue-50 border-blue-200"; + if (score >= 5.5) return "text-yellow-600 bg-yellow-50 border-yellow-200"; + if (score >= 3.5) return "text-orange-600 bg-orange-50 border-orange-200"; + return "text-red-600 bg-red-50 border-red-200"; + }; + + const getScoreLabel = (score) => { + if (score >= 8.5) return "Excellent"; + if (score >= 7.0) return "Good"; + if (score >= 5.5) return "Average"; + if (score >= 3.5) return "Below Average"; + return "Poor"; + }; + + const getScoreIcon = (score) => { + if (score >= 8.5) return ; + if (score >= 7.0) return ; + if (score >= 5.5) return ; + return ; + }; + + const renderTabContent = () => { + switch (activeTab) { + case "ingredients": + return ; + case "summary": + return ; + case "concerns": + return ; + case "benefits": + return ; + default: + return ; + } + }; + + return ( +
+ {/* Header */} +
+
+ +

Ingredient Analysis

+
+ + +
+
+
+ + {/* Product Overview */} +
+ {/* Product Image and Basic Info */} +
+
+ {sampleProduct.image} +
+
+

+ {sampleProduct.name} +

+

{sampleProduct.brand}

+
+ + 4.5/5 + + ({sampleProduct.totalIngredients} ingredients analyzed) + +
+
+
+ + {/* Overall Score - 1-10 Scale */} +
+
+
+ {sampleProduct.overallScore} +
+
+ {getScoreLabel(sampleProduct.overallScore)} +
+
+ Overall Safety Score (1-10) +
+ + {/* Score visualization */} +
+
= 8.5 + ? "bg-green-500" + : sampleProduct.overallScore >= 7.0 + ? "bg-blue-500" + : sampleProduct.overallScore >= 5.5 + ? "bg-yellow-500" + : sampleProduct.overallScore >= 3.5 + ? "bg-orange-500" + : "bg-red-500" + }`} + style={{ width: `${(sampleProduct.overallScore / 10) * 100}%` }} + >
+
+ +

+ {sampleProduct.overallAssessment} +

+
+
+ + {/* Key Ingredients Impact */} +
+ {/* Positive Ingredients */} + {sampleProduct.keyIngredients.positive.length > 0 && ( +
+
+ +

Key Benefits

+
+
+ {sampleProduct.keyIngredients.positive.map( + (ingredient, index) => ( +
+ + {ingredient} +
+ ), + )} +
+
+ )} + + {/* Negative Ingredients */} + {sampleProduct.keyIngredients.negative.length > 0 && ( +
+
+ +

Main Concerns

+
+
+ {sampleProduct.keyIngredients.negative.map( + (ingredient, index) => ( +
+ + {ingredient} +
+ ), + )} +
+
+ )} +
+ + {/* Score Breakdown */} +
+

+ Ingredient Categories +

+
+
+
+ {sampleProduct.scoreBreakdown.beneficial} +
+
Beneficial
+
+
+
+ {sampleProduct.scoreBreakdown.moderate} +
+
Moderate
+
+
+
+ {sampleProduct.scoreBreakdown.concerning} +
+
Concerning
+
+
+
+ {sampleProduct.scoreBreakdown.unknown} +
+
Unknown
+
+
+
+
+ + {/* Tab Navigation */} +
+
+ {[ + { id: "ingredients", label: "Ingredients" }, + { id: "summary", label: "Summary" }, + { id: "concerns", label: "Concerns" }, + { id: "benefits", label: "Benefits" }, + ].map((tab) => ( + + ))} +
+
+ + {/* Tab Content */} +
{renderTabContent()}
+
+ ); +} + +// Ingredients Tab Component +function IngredientsTab({ product }) { + // Mock ingredients with individual scores + const ingredients = [ + { + name: "Potassium Nitrate", + purpose: "Desensitizing agent", + score: 9.2, + category: "beneficial", + description: + "Helps reduce tooth sensitivity by blocking pain signals to nerves", + concerns: [], + benefits: ["Reduces sensitivity", "Clinically proven", "FDA approved"], + }, + { + name: "Sodium Fluoride", + purpose: "Anticavity agent", + score: 9.0, + category: "beneficial", + description: "Prevents tooth decay and strengthens enamel", + concerns: [], + benefits: ["Prevents cavities", "Strengthens enamel", "ADA recommended"], + }, + { + name: "Hydrated Silica", + purpose: "Mild abrasive", + score: 7.2, + category: "moderate", + description: "Helps remove plaque and surface stains gently", + concerns: ["May be abrasive with excessive use"], + benefits: ["Effective cleaning", "Whitening properties"], + }, + { + name: "Titanium Dioxide", + purpose: "Whitening agent", + score: 6.5, + category: "concerning", + description: "Provides whitening and opacity to toothpaste", + concerns: [ + "Potential respiratory irritant", + "Under regulatory review", + "May cause inflammation", + ], + benefits: ["Whitening effect", "Color enhancement"], + }, + { + name: "Sodium Lauryl Sulfate", + purpose: "Foaming agent", + score: 4.2, + category: "concerning", + description: "Creates foam and helps distribute toothpaste", + concerns: [ + "May cause mouth ulcers", + "Can irritate sensitive gums", + "Strips natural oils", + ], + benefits: ["Creates foam", "Helps cleaning action"], + }, + ]; + + const getCategoryColor = (category, score) => { + switch (category) { + case "beneficial": + return "bg-green-100 text-green-800 border-green-200"; + case "moderate": + return "bg-blue-100 text-blue-800 border-blue-200"; + case "concerning": + return "bg-red-100 text-red-800 border-red-200"; + default: + return "bg-gray-100 text-gray-800 border-gray-200"; + } + }; + + const getScoreColor = (score) => { + if (score >= 8.5) return "text-green-600"; + if (score >= 7.0) return "text-blue-600"; + if (score >= 5.5) return "text-yellow-600"; + if (score >= 3.5) return "text-orange-600"; + return "text-red-600"; + }; + + return ( +
+
+ Found {ingredients.length} ingredients • Analyzed with our safety + database +
+ + {ingredients.map((ingredient, index) => ( +
+
+
+

+ {ingredient.name} +

+

{ingredient.purpose}

+ + {ingredient.category.charAt(0).toUpperCase() + + ingredient.category.slice(1)} + +
+
+
+ {ingredient.score}/10 +
+
Safety Score
+
+
+ +

{ingredient.description}

+ + {ingredient.benefits.length > 0 && ( +
+
+ Benefits: +
+
+ {ingredient.benefits.map((benefit, i) => ( + + {benefit} + + ))} +
+
+ )} + + {ingredient.concerns.length > 0 && ( +
+
+ Concerns: +
+
+ {ingredient.concerns.map((concern, i) => ( +
+ + {concern} +
+ ))} +
+
+ )} +
+ ))} +
+ ); +} + +// Summary Tab Component +function SummaryTab({ product }) { + return ( +
+
+

Overall Analysis

+

+ This toothpaste received a score of{" "} + {product.overallScore}/10 based on the analysis of{" "} + {product.totalIngredients} ingredients. + {product.overallAssessment} +

+ +
+
+
+ {product.scoreBreakdown.beneficial} +
+
Beneficial Ingredients
+
+
+
+ {product.scoreBreakdown.concerning} +
+
Concerning Ingredients
+
+
+ +
+
+ + + Contains proven effective ingredients for oral health + +
+
+ + + Generally safe for daily use by most adults + +
+ {product.scoreBreakdown.concerning > 0 && ( +
+ + + Contains some ingredients that may cause sensitivity in certain + individuals + +
+ )} +
+
+ +
+

Recommendations

+
+ {product.overallScore >= 8.5 && ( +

+ âś… Highly Recommended: This is an excellent + choice for most people with high safety standards. +

+ )} + {product.overallScore >= 7.0 && product.overallScore < 8.5 && ( +

+ 👍 Good Choice: Solid formulation with mostly + beneficial ingredients. +

+ )} + {product.overallScore >= 5.5 && product.overallScore < 7.0 && ( +

+ ⚠️ Use with Caution: Consider alternatives if you + have sensitive teeth or gums. +

+ )} + {product.overallScore < 5.5 && ( +

+ ❌ Not Recommended: Consider switching to a + product with fewer concerning ingredients. +

+ )} + +

• Use twice daily for best results

+

• Discontinue use if irritation occurs

+

• Consult your dentist for personalized advice

+
+
+
+ ); +} + +// Concerns Tab Component +function ConcernsTab({ product }) { + return ( +
+
+ Potential issues identified in this product +
+ + {product.primaryConcerns.length > 0 + ?
+ {product.primaryConcerns.map((concern, index) => ( +
+
+ +
+

+ {concern} +

+
+

+ What this means: This ingredient may + cause adverse reactions in some individuals. +

+

+ Who should be careful: People with + sensitive teeth, gums, or known allergies. +

+

+ What to do: Monitor your reaction and + discontinue if irritation occurs. +

+
+
+
+
+ ))} + +
+
+ +
+

+ General Safety Tips +

+
    +
  • + • Always read ingredient labels if you have known + allergies +
  • +
  • • Start with small amounts when trying new products
  • +
  • + • Consult your dentist if you experience persistent + irritation +
  • +
  • + • Keep products away from children under 6 years old +
  • +
+
+
+
+
+ :
+ +

+ No Major Concerns +

+

+ This product has no significant safety concerns for most users. +

+
} +
+ ); +} + +// Benefits Tab Component +function BenefitsTab({ product }) { + return ( +
+
+ Key benefits and positive aspects of this product +
+ + {product.primaryBenefits.length > 0 + ?
+ {product.primaryBenefits.map((benefit, index) => ( +
+
+ +
+

+ {benefit} +

+
+

+ This ingredient or property contributes positively to + oral health and safety. +

+
+
+
+
+ ))} + +
+

+ Why These Ingredients Matter +

+
+
+

+ 🦷 Oral Health Benefits +

+
    +
  • • Cavity prevention
  • +
  • • Enamel strengthening
  • +
  • • Plaque reduction
  • +
  • • Sensitivity relief
  • +
+
+
+

+ âś… Safety Benefits +

+
    +
  • • FDA approved ingredients
  • +
  • • Clinically tested
  • +
  • • Natural components
  • +
  • • Low irritation risk
  • +
+
+
+
+
+ :
+ +

+ Limited Benefits +

+

+ This product has fewer beneficial ingredients than recommended. +

+
} +
+ ); +} diff --git a/src/components/pages/ScanPage.js b/src/components/pages/ScanPage.js new file mode 100644 index 0000000..6131031 --- /dev/null +++ b/src/components/pages/ScanPage.js @@ -0,0 +1,66 @@ +import { Camera, Upload, Type, Zap } from "lucide-react"; + +export default function ScanPage({ onProductScanned, onStartCamera }) { + return ( +
+ {/* Header */} +
+

Scan Product

+

+ Analyze toothpaste ingredients instantly +

+
+ + {/* Camera Preview Area */} +
+
+
+ +

+ Camera preview will appear here +

+
+
+
+ + {/* Scan Options */} +
+ + + + + +
+ + {/* Quick Tips */} +
+
+ +
+

+ Scanning Tips +

+
    +
  • • Ensure good lighting
  • +
  • • Hold camera steady
  • +
  • • Focus on ingredients list
  • +
  • • Keep text clear and readable
  • +
+
+
+
+
+ ); +} diff --git a/src/components/pages/SettingsPage.js b/src/components/pages/SettingsPage.js new file mode 100644 index 0000000..13587ee --- /dev/null +++ b/src/components/pages/SettingsPage.js @@ -0,0 +1,111 @@ +import { + User, + Bell, + Shield, + HelpCircle, + Info, + ChevronRight, + Moon, + Globe, +} from "lucide-react"; +import { useLanguage } from "../../contexts/LanguageContext"; +import { DetailedLanguageSelector } from "../LanguageSelector"; + +export default function SettingsPage() { + const { t } = useLanguage(); + + const settingSections = [ + { + title: t("account"), + items: [ + { icon: User, label: t("profile"), value: t("updateInfo") }, + { icon: Bell, label: t("notifications"), value: t("manageAlerts") }, + ], + }, + { + title: t("preferences"), + items: [ + { + icon: Shield, + label: t("safetyThreshold"), + value: t("highSensitivity"), + }, + { icon: Moon, label: t("darkMode"), value: t("systemTheme") }, + ], + }, + { + title: t("support"), + items: [ + { icon: HelpCircle, label: t("helpCenter"), value: t("getSupport") }, + { icon: Info, label: t("aboutApp"), value: t("version") }, + ], + }, + ]; + + return ( +
+ {/* Header */} +
+

+ {t("settings")} +

+

Manage your PastPick experience

+
+ + {/* Language Selector */} +
+ +
+ + {/* Settings Sections */} +
+ {settingSections.map((section, sectionIndex) => ( +
+

+ {section.title} +

+
+ {section.items.map((item, itemIndex) => { + const Icon = item.icon; + return ( + + ); + })} +
+
+ ))} +
+ + {/* App Info */} +
+
+

🦷 {t("appName")}

+

+ Making toothpaste ingredient analysis simple and accessible for + everyone. +

+
+

+ © 2024 {t("appName")}. Made with care for your oral health. +

+
+
+ ); +} diff --git a/src/contexts/LanguageContext.js b/src/contexts/LanguageContext.js new file mode 100644 index 0000000..8089133 --- /dev/null +++ b/src/contexts/LanguageContext.js @@ -0,0 +1,554 @@ +"use client"; + +import { createContext, useContext, useState, useEffect } from "react"; + +const LanguageContext = createContext(); + +// Complete translation data for English and Finnish +const translations = { + en: { + // App Name & Branding + appName: "PastePick", + appTagline: "Your smart companion for analyzing toothpaste ingredients", + + // Navigation + home: "Home", + favorites: "Favorites", + scan: "Scan", + recent: "Recent", + settings: "Settings", + + // Home Page + productsScanned: "Products Scanned", + safeIngredients: "Safe Ingredients", + quickScan: "Quick Scan", + searchProducts: "Search Products", + safetyAnalysis: "Safety Analysis", + safetyAnalysisDesc: "Detailed ingredient safety ratings", + smartRecommendations: "Smart Recommendations", + smartRecommendationsDesc: "Better alternatives based on your needs", + poweredBy: "Powered by AI-driven ingredient analysis", + learnHow: "Learn how PastPick works →", + + // Scanning + scanProduct: "Scan Product", + scanToothpaste: "Scan Toothpaste", + scanIngredients: "Scan Ingredients", + analyzeIngredients: "Analyze toothpaste ingredients instantly", + startCameraScan: "Start Camera Scan", + chooseFromGallery: "Choose from Gallery", + takePhoto: "Take Photo", + openCamera: "Open Camera", + scanningTips: "Scanning Tips", + scanFront: "Scan Product Front", + scanIngredientsList: "Scan Ingredients List", + scanPackage: "Scan Package/Box", + + // Analysis Results + ingredientAnalysis: "Ingredient Analysis", + overallScore: "Overall Safety Score", + overallSafetyScore: "Overall Safety Score (1-10)", + dailyUse: "Daily Use", + sensitivity: "Sensitivity", + formula: "Formula", + ingredients: "Ingredients", + summary: "Summary", + concerns: "Concerns", + benefits: "Benefits", + sources: "Sources", + claims: "Claims", + keyIngredients: "Key Ingredients", + keyBenefits: "Key Benefits", + mainConcerns: "Main Concerns", + positiveIngredients: "Beneficial Ingredients", + negativeIngredients: "Ingredients of Concern", + safetyScore: "Safety Score", + ingredientCategories: "Ingredient Categories", + + // Score Categories + beneficial: "Beneficial", + moderate: "Moderate", + concerning: "Concerning", + unknown: "Unknown", + + // Status Labels + excellent: "Excellent", + safe: "Safe", + good: "Good", + average: "Average", + belowAverage: "Below Average", + poor: "Poor", + caution: "Caution", + avoid: "Avoid", + + // Analysis Messages + scanningInProgress: "Scanning in progress...", + analyzingImage: "Analyzing image...", + processingResults: "Processing results...", + scanSuccessful: "Scan successful!", + analysisComplete: "Analysis complete", + analyzing: "Analyzing...", + analyzeIngredientsCTA: "Analyze Ingredients", + + // Error Messages + imageTooBlurry: "Image is too blurry. Please retake with better focus.", + noTextFound: + "No ingredients found. Make sure to scan the ingredients list.", + noIngredientsFound: "Could not identify ingredients. Please try again.", + productNotFound: + "Product not found. Try scanning the ingredients list directly.", + scanError: "Something went wrong. Please try again.", + cameraError: "Unable to access camera. Please check permissions.", + networkError: "Network error. Please check your connection.", + + // Camera Tips + scanTip1: "Ensure good lighting", + scanTip2: "Hold camera steady", + scanTip3: "Focus on ingredients list", + scanTip4: "Keep text clear and readable", + scanTip5: "Avoid shadows and glare", + mobileDetected: "Mobile device detected", + cameraWorksBesetMobile: "Camera works best on mobile devices", + + // Settings Page + profile: "Profile", + notifications: "Notifications", + manageAlerts: "Manage alerts", + updateInfo: "Update your information", + safetyThreshold: "Safety Threshold", + highSensitivity: "High sensitivity", + darkMode: "Dark Mode", + language: "Language", + helpCenter: "Help Center", + getSupport: "Get support", + aboutApp: "About PastPick", + version: "Version 1.0.0", + account: "Account", + preferences: "Preferences", + support: "Support", + + // Theme Options + lightTheme: "Light", + darkTheme: "Dark", + systemTheme: "System Default", + + // Actions + retake: "Retake", + analyze: "Analyze", + save: "Save", + share: "Share", + viewDetails: "View Details", + tryAgain: "Try Again", + cancel: "Cancel", + ok: "OK", + close: "Close", + back: "Back", + next: "Next", + done: "Done", + + // Favorites + noFavorites: "No favorites yet", + noFavoritesDesc: "Start scanning products to add them to your favorites", + addToFavorites: "Add to Favorites", + removeFromFavorites: "Remove from Favorites", + + // Recent Scans + recentScans: "Recent Scans", + noRecentScans: "No recent scans", + noRecentScansDesc: "Your scan history will appear here", + scanHistory: "Scan History", + scannedAt: "Scanned {time}", + + // Scan Types + productFront: "Product Front", + ingredientsList: "Ingredients List", + packageBox: "Package/Box", + + // Analysis Details + overallAnalysis: "Overall Analysis", + recommendations: "Recommendations", + highlyRecommended: "Highly Recommended", + goodChoice: "Good Choice", + useWithCaution: "Use with Caution", + notRecommended: "Not Recommended", + generalSafetyTips: "General Safety Tips", + noMajorConcerns: "No Major Concerns", + limitedBenefits: "Limited Benefits", + + // Ingredient Analysis + foundIngredients: "Found {count} ingredients", + analyzedWithDatabase: "Analyzed with our safety database", + whatThisMeans: "What this means:", + whoShouldBeCareful: "Who should be careful:", + whatToDo: "What to do:", + + // Language Selection + selectLanguage: "Select Language", + currentLanguage: "Current Language", + changeLanguage: "Change Language", + + // Time Formats + hoursAgo: "{count} hours ago", + daysAgo: "{count} days ago", + minutesAgo: "{count} minutes ago", + justNow: "just now", + + // Pluralization + ingredientsAnalyzed: "{count} ingredients analyzed", + concernsFound: "{count} concerns found", + + // Loading States + loading: "Loading...", + pleaseWait: "Please wait...", + }, + + fi: { + // App Name & Branding + appName: "PastePick", + appTagline: "Älykäs kumppanisi hammastahnien ainesosien analysointiin", + + // Navigation + home: "Koti", + favorites: "Suosikit", + scan: "Skannaa", + recent: "Viimeisimmät", + settings: "Asetukset", + + // Home Page + productsScanned: "Skannattuja Tuotteita", + safeIngredients: "Turvallisia Ainesosia", + quickScan: "Pikaskannaus", + searchProducts: "Hae Tuotteita", + safetyAnalysis: "Turvallisuusanalyysi", + safetyAnalysisDesc: "Yksityiskohtaiset ainesosien turvallisuusarviot", + smartRecommendations: "Älykkäät Suositukset", + smartRecommendationsDesc: "Parempia vaihtoehtoja tarpeidesi perusteella", + poweredBy: "Tekoälyyn perustuva ainesosien analyysi", + learnHow: "Opi kuinka PastPick toimii →", + + // Scanning + scanProduct: "Skannaa Tuote", + scanToothpaste: "Skannaa Hammastahna", + scanIngredients: "Skannaa Ainesosat", + analyzeIngredients: "Analysoi hammastahnien ainesosat välittömästi", + startCameraScan: "Aloita Kameraskannaus", + chooseFromGallery: "Valitse Galleriasta", + takePhoto: "Ota Kuva", + openCamera: "Avaa Kamera", + scanningTips: "Skannausvinkit", + scanFront: "Skannaa Tuotteen Etupuoli", + scanIngredientsList: "Skannaa Ainesosaluettelo", + scanPackage: "Skannaa Pakkaus/Laatikko", + + // Analysis Results + ingredientAnalysis: "Ainesosa-analyysi", + overallScore: "Kokonaisturvallisuuspisteet", + overallSafetyScore: "Kokonaisturvallisuuspisteet (1-10)", + dailyUse: "Päivittäinen Käyttö", + sensitivity: "Herkkyys", + formula: "Koostumus", + ingredients: "Ainesosat", + summary: "Yhteenveto", + concerns: "Huolenaiheet", + benefits: "Hyödyt", + sources: "Lähteet", + claims: "Väitteet", + keyIngredients: "Tärkeimmät Ainesosat", + keyBenefits: "Keskeiset Hyödyt", + mainConcerns: "Päähuolenaiheet", + positiveIngredients: "Hyödylliset Ainesosat", + negativeIngredients: "Huolestuttavat Ainesosat", + safetyScore: "Turvallisuuspisteet", + ingredientCategories: "Ainesosa Kategoriat", + + // Score Categories + beneficial: "Hyödyllinen", + moderate: "Kohtalainen", + concerning: "Huolestuttava", + unknown: "Tuntematon", + + // Status Labels + excellent: "Erinomainen", + safe: "Turvallinen", + good: "Hyvä", + average: "Keskiverto", + belowAverage: "Keskitason Alapuolella", + poor: "Huono", + caution: "Varoitus", + avoid: "Vältä", + + // Analysis Messages + scanningInProgress: "Skannaus käynnissä...", + analyzingImage: "Analysoidaan kuvaa...", + processingResults: "Käsitellään tuloksia...", + scanSuccessful: "Skannaus onnistui!", + analysisComplete: "Analyysi valmis", + analyzing: "Analysoidaan...", + analyzeIngredientsCTA: "Analysoi Ainesosat", + + // Error Messages + imageTooBlurry: + "Kuva on liian epätarkka. Ota uusi kuva paremmalla tarkennuksella.", + noTextFound: + "Ainesosia ei löytynyt. Varmista että skannaat ainesosaluetteloa.", + noIngredientsFound: "Ainesosia ei voitu tunnistaa. Yritä uudelleen.", + productNotFound: + "Tuotetta ei löytynyt. Kokeile skannata ainesosaluettelo suoraan.", + scanError: "Jotain meni pieleen. Yritä uudelleen.", + cameraError: "Kameraan ei saada yhteyttä. Tarkista käyttöoikeudet.", + networkError: "Verkkovirhe. Tarkista internetyhteytesi.", + + // Camera Tips + scanTip1: "Varmista hyvä valaistus", + scanTip2: "Pidä kamera vakaana", + scanTip3: "Kohdista ainesosaluetteloon", + scanTip4: "Pidä teksti selkeänä ja luettavana", + scanTip5: "Vältä varjoja ja heijastuksia", + mobileDetected: "Mobiililaite havaittu", + cameraWorksBesetMobile: "Kamera toimii parhaiten mobiililaitteilla", + + // Settings Page + profile: "Profiili", + notifications: "Ilmoitukset", + manageAlerts: "Hallinnoi hälytyksiä", + updateInfo: "Päivitä tietosi", + safetyThreshold: "Turvallisuuskynnys", + highSensitivity: "Korkea herkkyys", + darkMode: "Tumma Tila", + language: "Kieli", + helpCenter: "Ohje", + getSupport: "Hanki tukea", + aboutApp: "Tietoa PastPick", + version: "Versio 1.0.0", + account: "Tili", + preferences: "Asetukset", + support: "Tuki", + + // Theme Options + lightTheme: "Vaalea", + darkTheme: "Tumma", + systemTheme: "Järjestelmän Oletus", + + // Actions + retake: "Ota Uudelleen", + analyze: "Analysoi", + save: "Tallenna", + share: "Jaa", + viewDetails: "Näytä Tiedot", + tryAgain: "Yritä Uudelleen", + cancel: "Peruuta", + ok: "OK", + close: "Sulje", + back: "Takaisin", + next: "Seuraava", + done: "Valmis", + + // Favorites + noFavorites: "Ei suosikkeja vielä", + noFavoritesDesc: + "Aloita tuotteiden skannaaminen lisätäksesi niitä suosikkeihin", + addToFavorites: "Lisää Suosikkeihin", + removeFromFavorites: "Poista Suosikeista", + + // Recent Scans + recentScans: "Viimeisimmät Skannaukset", + noRecentScans: "Ei viimeaikaisia skannauksia", + noRecentScansDesc: "Skannaushistoriasi näkyy täällä", + scanHistory: "Skannaushistoria", + scannedAt: "Skannattu {time}", + + // Scan Types + productFront: "Tuotteen Etupuoli", + ingredientsList: "Ainesosaluettelo", + packageBox: "Pakkaus/Laatikko", + + // Analysis Details + overallAnalysis: "Kokonaisanalyysi", + recommendations: "Suositukset", + highlyRecommended: "Erittäin Suositeltava", + goodChoice: "Hyvä Valinta", + useWithCaution: "Käytä Varovasti", + notRecommended: "Ei Suositella", + generalSafetyTips: "Yleiset Turvallisuusvinkit", + noMajorConcerns: "Ei Merkittäviä Huolenaiheita", + limitedBenefits: "Rajoitetut Hyödyt", + + // Ingredient Analysis + foundIngredients: "Löydettiin {count} ainesosaa", + analyzedWithDatabase: "Analysoitu turvallisuustietokannallamme", + whatThisMeans: "Mitä tämä tarkoittaa:", + whoShouldBeCareful: "Kenen tulisi olla varovainen:", + whatToDo: "Mitä tehdä:", + + // Language Selection + selectLanguage: "Valitse Kieli", + currentLanguage: "Nykyinen Kieli", + changeLanguage: "Vaihda Kieli", + + // Time Formats + hoursAgo: "{count} tuntia sitten", + daysAgo: "{count} päivää sitten", + minutesAgo: "{count} minuuttia sitten", + justNow: "juuri nyt", + + // Pluralization + ingredientsAnalyzed: "{count} ainesosaa analysoitu", + concernsFound: "{count} huolenaihetta löydetty", + + // Loading States + loading: "Ladataan...", + pleaseWait: "Odota hetki...", + }, +}; + +// Ingredient translation mappings (Foreign → English) +const ingredientTranslations = { + // Finnish to English + natriumfluoridi: "sodium fluoride", + kalsiumkarbonaatti: "calcium carbonate", + natriumlauryylisulfaatti: "sodium lauryl sulfate", + natriumlauryylieetterinylätti: "sodium laureth sulfate", + titaanidioksidi: "titanium dioxide", + "hydratoitu piidioksidi": "hydrated silica", + kaliumnitraatti: "potassium nitrate", + ksylitoli: "xylitol", + sorbitoli: "sorbitol", + glyseriini: "glycerin", + mentoli: "menthol", + tritriumfosfaatti: "trisodium phosphate", + natriumsakariini: "sodium saccharin", + selluloosakumi: "cellulose gum", + natriumhydroksidi: "sodium hydroxide", + + // German to English + natriumfluorid: "sodium fluoride", + kalziumkarbonat: "calcium carbonate", + natriumlaurylsulfat: "sodium lauryl sulfate", + titandioxid: "titanium dioxide", + "hydratisierte kieselsäure": "hydrated silica", + kaliumnitrat: "potassium nitrate", + glyzerin: "glycerin", + natriumsaccharin: "sodium saccharin", + + // French to English + "fluorure de sodium": "sodium fluoride", + "carbonate de calcium": "calcium carbonate", + "laurylsulfate de sodium": "sodium lauryl sulfate", + "dioxyde de titane": "titanium dioxide", + "silice hydratée": "hydrated silica", + "nitrate de potassium": "potassium nitrate", + glycérine: "glycerin", + + // Spanish to English + "fluoruro de sodio": "sodium fluoride", + "carbonato de calcio": "calcium carbonate", + "lauril sulfato de sodio": "sodium lauryl sulfate", + "dióxido de titanio": "titanium dioxide", + "sílice hidratada": "hydrated silica", + "nitrato de potasio": "potassium nitrate", + glicerina: "glycerin", + + // Common abbreviations and synonyms + sls: "sodium lauryl sulfate", + sles: "sodium laureth sulfate", + peg: "polyethylene glycol", + edta: "ethylenediaminetetraacetic acid", + bht: "butylated hydroxytoluene", + bha: "butylated hydroxyanisole", + "fdc&c": "food drug and cosmetic color", + "fd&c": "food drug and cosmetic color", + ci: "color index", +}; + +export const LanguageProvider = ({ children }) => { + const [language, setLanguage] = useState("en"); + + useEffect(() => { + // Load saved language from localStorage + const savedLanguage = localStorage.getItem("pastepick-language"); + if (savedLanguage && translations[savedLanguage]) { + setLanguage(savedLanguage); + } else { + // Auto-detect browser language + const browserLang = navigator.language.slice(0, 2); + if (translations[browserLang]) { + setLanguage(browserLang); + } + } + }, []); + + const changeLanguage = (lang) => { + if (translations[lang]) { + setLanguage(lang); + localStorage.setItem("pastepick-language", lang); + } + }; + + // Translation function with interpolation support + const t = (key, params = {}) => { + let translation = + translations[language][key] || translations["en"][key] || key; + + // Handle parameter interpolation {param} + Object.keys(params).forEach((param) => { + translation = translation.replace( + new RegExp(`{${param}}`, "g"), + params[param], + ); + }); + + return translation; + }; + + // Translate ingredients from foreign languages to English + const translateIngredients = (ingredientText) => { + if (!ingredientText) return ""; + + let translatedText = ingredientText.toLowerCase(); + + // Apply ingredient translations + Object.entries(ingredientTranslations).forEach(([foreign, english]) => { + const regex = new RegExp(foreign, "gi"); + translatedText = translatedText.replace(regex, english); + }); + + return translatedText; + }; + + // Get available languages with their native names + const getAvailableLanguages = () => [ + { code: "en", name: "English", nativeName: "English" }, + { code: "fi", name: "Finnish", nativeName: "Suomi" }, + ]; + + // Get current language info + const getCurrentLanguageInfo = () => { + const languages = getAvailableLanguages(); + return languages.find((lang) => lang.code === language) || languages[0]; + }; + + return ( + + {children} + + ); +}; + +export const useLanguage = () => { + const context = useContext(LanguageContext); + if (!context) { + throw new Error("useLanguage must be used within a LanguageProvider"); + } + return context; +}; diff --git a/src/contexts/ThemeContext.js b/src/contexts/ThemeContext.js new file mode 100644 index 0000000..8e6d2fc --- /dev/null +++ b/src/contexts/ThemeContext.js @@ -0,0 +1,88 @@ +"use client"; + +import { createContext, useContext, useState, useEffect } from "react"; + +const ThemeContext = createContext(); + +export const ThemeProvider = ({ children }) => { + const [theme, setTheme] = useState("system"); // 'light', 'dark', or 'system' + const [actualTheme, setActualTheme] = useState("light"); // The actual theme being used + + useEffect(() => { + // Load saved theme from localStorage + const savedTheme = localStorage.getItem("pastepick-theme"); + if (savedTheme) { + setTheme(savedTheme); + } + }, []); + + useEffect(() => { + const updateTheme = () => { + let newActualTheme = theme; + + if (theme === "system") { + // Use system preference + newActualTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light"; + } + + setActualTheme(newActualTheme); + + // Update DOM + if (newActualTheme === "dark") { + document.documentElement.classList.add("dark"); + } else { + document.documentElement.classList.remove("dark"); + } + }; + + updateTheme(); + + // Listen for system theme changes + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handleChange = () => { + if (theme === "system") { + updateTheme(); + } + }; + + mediaQuery.addEventListener("change", handleChange); + return () => mediaQuery.removeEventListener("change", handleChange); + }, [theme]); + + const changeTheme = (newTheme) => { + setTheme(newTheme); + localStorage.setItem("pastepick-theme", newTheme); + }; + + const toggleTheme = () => { + const newTheme = actualTheme === "dark" ? "light" : "dark"; + changeTheme(newTheme); + }; + + return ( + + {children} + + ); +}; + +export const useTheme = () => { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +}; diff --git a/src/services/mockAnalysisService.js b/src/services/mockAnalysisService.js new file mode 100644 index 0000000..65ef43c --- /dev/null +++ b/src/services/mockAnalysisService.js @@ -0,0 +1,322 @@ +// Mock ingredient analysis service for PastPick + +// Database of common toothpaste ingredients with their analysis +const ingredientDatabase = { + // Safe ingredients (7-10 score) + "potassium nitrate": { + name: "Potassium Nitrate", + score: 9.2, + category: "beneficial", + purpose: "Desensitizing agent", + description: + "Helps reduce tooth sensitivity by blocking pain signals to nerves", + concerns: [], + benefits: ["Reduces sensitivity", "Clinically proven", "FDA approved"], + impact: "positive", + }, + "sodium fluoride": { + name: "Sodium Fluoride", + score: 9.0, + category: "beneficial", + purpose: "Anticavity agent", + description: "Prevents tooth decay and strengthens enamel", + concerns: [], + benefits: ["Prevents cavities", "Strengthens enamel", "ADA recommended"], + impact: "positive", + }, + "calcium carbonate": { + name: "Calcium Carbonate", + score: 8.8, + category: "beneficial", + purpose: "Natural abrasive", + description: "Natural mineral that gently removes plaque and stains", + concerns: [], + benefits: ["Natural ingredient", "Gentle cleaning", "Whitening effect"], + impact: "positive", + }, + xylitol: { + name: "Xylitol", + score: 9.5, + category: "beneficial", + purpose: "Natural sweetener", + description: "Natural sugar alcohol that helps prevent cavities", + concerns: [], + benefits: ["Prevents cavities", "Natural sweetener", "Reduces bacteria"], + impact: "positive", + }, + + // Moderate ingredients (5-6.9 score) + "hydrated silica": { + name: "Hydrated Silica", + score: 7.2, + category: "moderate", + purpose: "Mild abrasive", + description: "Helps remove plaque and surface stains gently", + concerns: ["May be too abrasive with excessive use"], + benefits: ["Effective cleaning", "Whitening properties"], + impact: "neutral", + }, + "titanium dioxide": { + name: "Titanium Dioxide", + score: 6.5, + category: "concerning", + purpose: "Whitening agent", + description: "Provides whitening and opacity to toothpaste", + concerns: [ + "Potential respiratory irritant", + "Under regulatory review", + "May cause inflammation", + ], + benefits: ["Whitening effect", "Color enhancement"], + impact: "negative", + }, + + // Concerning ingredients (3-4.9 score) + "sodium lauryl sulfate": { + name: "Sodium Lauryl Sulfate (SLS)", + score: 4.2, + category: "concerning", + purpose: "Foaming agent", + description: "Creates foam and helps distribute toothpaste", + concerns: [ + "May cause mouth ulcers", + "Can irritate sensitive gums", + "Strips natural protective oils", + "May worsen canker sores", + ], + benefits: ["Creates satisfying foam", "Helps cleaning action"], + impact: "negative", + }, + triclosan: { + name: "Triclosan", + score: 3.8, + category: "concerning", + purpose: "Antibacterial agent", + description: "Antimicrobial agent used to prevent plaque", + concerns: [ + "Potential hormone disruption", + "Antibiotic resistance concerns", + "Environmental impact", + "Banned in some countries", + ], + benefits: ["Antibacterial properties", "Reduces plaque"], + impact: "negative", + }, + "artificial colors": { + name: "Artificial Colors", + score: 4.0, + category: "concerning", + purpose: "Coloring agent", + description: "Synthetic dyes to make toothpaste visually appealing", + concerns: [ + "No oral health benefit", + "Potential allergic reactions", + "Unnecessary chemical exposure", + ], + benefits: ["Aesthetic appeal"], + impact: "negative", + }, + + // Common synonyms and abbreviations + sls: "sodium lauryl sulfate", + "sodium fluoride 0.24%": "sodium fluoride", + "calcium carbonate precipitated": "calcium carbonate", +}; + +// Parse ingredient text and normalize names +export const parseIngredients = (ingredientText) => { + if (!ingredientText) return []; + + // Remove common prefixes + let cleanText = ingredientText + .toLowerCase() + .replace(/^(ingredients|ainesosat|inhaltsstoffe):\s*/i, "") + .replace(/\([^)]*\)/g, ""); // Remove parentheses content + + // Split by common separators + const ingredients = cleanText + .split(/[,;.]/) + .map((ingredient) => ingredient.trim()) + .filter((ingredient) => ingredient.length > 2) + .map((ingredient) => { + // Handle common synonyms + const normalized = ingredient.toLowerCase(); + return ingredientDatabase[normalized] ? normalized : ingredient; + }); + + return ingredients; +}; + +// Analyze a single ingredient +const analyzeIngredient = (ingredientName) => { + const normalizedName = ingredientName.toLowerCase().trim(); + + // Check if ingredient exists in database + if (ingredientDatabase[normalizedName]) { + return ingredientDatabase[normalizedName]; + } + + // For unknown ingredients, provide generic analysis + return { + name: ingredientName.charAt(0).toUpperCase() + ingredientName.slice(1), + score: 6.5, // Neutral score for unknown ingredients + category: "unknown", + purpose: "Not identified", + description: + "This ingredient is not in our database. Consider researching it independently.", + concerns: ["Unknown safety profile"], + benefits: ["Function not determined"], + impact: "neutral", + }; +}; + +// Calculate overall score based on ingredients +const calculateOverallScore = (analyzedIngredients) => { + if (analyzedIngredients.length === 0) return 5.0; + + let weightedSum = 0; + let totalWeight = 0; + + analyzedIngredients.forEach((ingredient, index) => { + // Earlier ingredients in the list have more weight (toothpaste ingredients are listed by concentration) + const weight = Math.max(1, analyzedIngredients.length - index); + weightedSum += ingredient.score * weight; + totalWeight += weight; + }); + + const baseScore = weightedSum / totalWeight; + + // Apply penalties for concerning ingredients + const concerningIngredients = analyzedIngredients.filter( + (ing) => ing.category === "concerning", + ); + const penalty = concerningIngredients.length * 0.5; + + // Apply bonuses for beneficial ingredients + const beneficialIngredients = analyzedIngredients.filter( + (ing) => ing.category === "beneficial", + ); + const bonus = Math.min(beneficialIngredients.length * 0.3, 1.0); + + const finalScore = Math.max(1.0, Math.min(10.0, baseScore - penalty + bonus)); + return Math.round(finalScore * 10) / 10; // Round to 1 decimal place +}; + +// Get key ingredients that most affected the score +const getKeyIngredients = (analyzedIngredients) => { + const sortedIngredients = [...analyzedIngredients].sort((a, b) => { + // Sort by impact magnitude (distance from neutral score of 6.5) + const impactA = Math.abs(a.score - 6.5); + const impactB = Math.abs(b.score - 6.5); + return impactB - impactA; + }); + + const positive = sortedIngredients + .filter((ing) => ing.impact === "positive") + .slice(0, 3) + .map((ing) => ing.name); + + const negative = sortedIngredients + .filter((ing) => ing.impact === "negative") + .slice(0, 3) + .map((ing) => ing.name); + + return { positive, negative }; +}; + +// Main analysis function +export const analyzeIngredients = async (ingredientText) => { + return new Promise((resolve) => { + setTimeout(() => { + try { + // Parse ingredients from text + const ingredientNames = parseIngredients(ingredientText); + + if (ingredientNames.length === 0) { + resolve({ + success: false, + error: "No ingredients found in the text", + }); + return; + } + + // Analyze each ingredient + const analyzedIngredients = ingredientNames.map(analyzeIngredient); + + // Calculate overall score + const overallScore = calculateOverallScore(analyzedIngredients); + + // Get key ingredients + const keyIngredients = getKeyIngredients(analyzedIngredients); + + // Generate overall assessment + let overallAssessment = ""; + if (overallScore >= 8.5) { + overallAssessment = + "Excellent choice with high-quality, safe ingredients"; + } else if (overallScore >= 7.0) { + overallAssessment = "Good formulation with mostly safe ingredients"; + } else if (overallScore >= 5.5) { + overallAssessment = + "Average product with some ingredients of concern"; + } else if (overallScore >= 3.5) { + overallAssessment = + "Below average with several concerning ingredients"; + } else { + overallAssessment = + "Poor formulation with many problematic ingredients"; + } + + // Get primary concerns and benefits + const allConcerns = analyzedIngredients + .flatMap((ing) => ing.concerns) + .filter((concern, index, arr) => arr.indexOf(concern) === index) + .slice(0, 5); + + const allBenefits = analyzedIngredients + .flatMap((ing) => ing.benefits) + .filter((benefit, index, arr) => arr.indexOf(benefit) === index) + .slice(0, 5); + + resolve({ + success: true, + analysis: { + overallScore, + overallAssessment, + totalIngredients: analyzedIngredients.length, + keyIngredients, + primaryConcerns: allConcerns, + primaryBenefits: allBenefits, + ingredients: analyzedIngredients, + scoreBreakdown: { + beneficial: analyzedIngredients.filter( + (ing) => ing.category === "beneficial", + ).length, + moderate: analyzedIngredients.filter( + (ing) => ing.category === "moderate", + ).length, + concerning: analyzedIngredients.filter( + (ing) => ing.category === "concerning", + ).length, + unknown: analyzedIngredients.filter( + (ing) => ing.category === "unknown", + ).length, + }, + }, + }); + } catch (error) { + resolve({ + success: false, + error: "Error analyzing ingredients: " + error.message, + }); + } + }, 1500); // Simulate processing time + }); +}; + +// Sample ingredient texts for testing +export const sampleIngredientTexts = [ + "Ingredients: Potassium Nitrate, Sodium Fluoride, Hydrated Silica, Calcium Carbonate, Water, Natural Mint Flavor", + "Ingredients: Sodium Lauryl Sulfate, Titanium Dioxide, Triclosan, Artificial Colors, Sodium Fluoride, Water", + "Ingredients: Calcium Carbonate, Xylitol, Natural Flavors, Sodium Fluoride, Potassium Nitrate, Cellulose Gum", +];