Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified src/app/favicon.ico
Binary file not shown.
2 changes: 1 addition & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
}
41 changes: 21 additions & 20 deletions src/app/layout.js
Original file line number Diff line number Diff line change
@@ -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 (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<html lang="en" suppressHydrationWarning>
<head>
<title>PastePick - Smart Toothpaste Analyzer</title>
<meta
name="description"
content="Analyze toothpaste ingredients for safer, smarter choices with PastePick"
/>
</head>
<body className={inter.className} suppressHydrationWarning>
<ThemeProvider>
<LanguageProvider>
<ToastProvider>{children}</ToastProvider>
</LanguageProvider>
</ThemeProvider>
</body>
</html>
);
Expand Down
185 changes: 91 additions & 94 deletions src/app/page.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
src/app/page.js
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
const [activeTab, setActiveTab] = useState("home");
const [currentView, setCurrentView] = useState("main"); // 'main', 'product', or 'camera'
const [scannedProduct, setScannedProduct] = useState(null);

<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
const handleProductScanned = (product = null) => {
setScannedProduct(product);
setCurrentView("product");
};

const handleStartCamera = () => {
setCurrentView("camera");
};

const handleCameraCapture = (imageData) => {
// Here you would process the captured image
// For now, we'll simulate the scan result
const mockProduct = {
name: "Scanned Toothpaste Product",
brand: "Unknown Brand",
image: "📷",
overallScore: 78,
};
setScannedProduct(mockProduct);
setCurrentView("product");
};

const handleCameraClose = () => {
setCurrentView("main");
};

const handleBackToMain = () => {
setCurrentView("main");
setScannedProduct(null);
};

const renderPage = () => {
switch (activeTab) {
case "home":
return (
<HomePage
onProductScanned={handleProductScanned}
onStartCamera={handleStartCamera}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
);
case "scan":
return (
<ScanPage
onProductScanned={handleProductScanned}
onStartCamera={handleStartCamera}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
);
case "recent":
return <RecentPage onProductScanned={handleProductScanned} />;
case "settings":
return <SettingsPage />;
default:
return (
<HomePage
onProductScanned={handleProductScanned}
onStartCamera={handleStartCamera}
/>
Go to nextjs.org →
</a>
</footer>
);
}
};

return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-green-50">
{currentView === "product"
? <ProductPage product={scannedProduct} onBack={handleBackToMain} />
: currentView === "camera"
? <CameraScanner
onClose={handleCameraClose}
onCapture={handleCameraCapture}
/>
: <>
{/* Page Content */}
<main className="pt-safe-area">{renderPage()}</main>

{/* Bottom Navigation */}
<BottomNavigation
activeTab={activeTab}
onTabChange={setActiveTab}
onStartCamera={handleStartCamera}
/>
</>}
</div>
);
}
74 changes: 74 additions & 0 deletions src/components/BottomNavigation.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-2 py-2 safe-area-pb">
<div className="flex justify-around items-center max-w-md mx-auto">
{tabs.map((tab) => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
const isScan = tab.id === "scan";

return (
<button
key={tab.id}
onClick={() => {
if (isScan && onStartCamera) {
onStartCamera();
} else {
onTabChange(tab.id);
}
}}
className={`flex flex-col items-center justify-center min-w-0 flex-1 py-2 px-1 transition-all duration-200 ${
isScan ? "transform -translate-y-2" : ""
}`}
>
{/* Special styling for scan button */}
{isScan
? <div
className={`p-3 rounded-full shadow-lg transition-colors ${
isActive
? "bg-blue-600 text-white"
: "bg-blue-600 text-white hover:bg-blue-700"
}`}
>
<Icon size={24} />
</div>
: <div
className={`p-2 rounded-lg transition-colors ${
isActive
? "text-blue-600 bg-blue-50"
: "text-gray-500 hover:text-gray-700"
}`}
>
<Icon size={20} />
</div>}

{/* Label */}
<span
className={`text-xs mt-1 font-medium transition-colors ${
isActive ? "text-blue-600" : "text-gray-500"
} ${isScan ? "text-blue-600" : ""}`}
>
{tab.label}
</span>
</button>
);
})}
</div>
</div>
);
}
Loading
Loading