diff --git a/Dockerfile b/Dockerfile index e69de29..070ab1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -0,0 +1,34 @@ +# Stage 1: Install production dependencies only +FROM node:20-alpine AS deps +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production && \ + npm cache clean --force + +# Stage 2: Build the application +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# Stage 3: Production runtime +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production + +# Copy built application from builder +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +EXPOSE 3000 + +# listen on all interfaces +ENV HOSTNAME="0.0.0.0" +ENV PORT=3000 + +# Stage 4: Start the application +CMD ["node", "server.js"] \ No newline at end of file diff --git a/src/app/[locale]/add-new/page.js b/src/app/[locale]/add-new/page.js index c725b70..a1b60ab 100644 --- a/src/app/[locale]/add-new/page.js +++ b/src/app/[locale]/add-new/page.js @@ -23,6 +23,7 @@ export default function NewToothpasteForm() { name: "", ingredients: "", }); + const BASE_URL = "http://server:3001/api"; const handleImageUpload = (e) => { const file = e.target.files?.[0]; @@ -63,10 +64,10 @@ export default function NewToothpasteForm() { formDataToSend.append("brand", formData.brand); formDataToSend.append("name", formData.name); formDataToSend.append("ingredients", JSON.stringify(ingredientsArray)); - formDataToSend.append("image", imageFile); + formDataToSend.append("file", imageFile); try { - const res = await fetch("http://localhost:3000/api/v1/toothpastes/new", { + const res = await fetch(`${BASE_URL}/v1/toothpastes/new`, { method: "POST", body: formDataToSend, }); diff --git a/src/app/[locale]/products/[id]/page.js b/src/app/[locale]/products/[id]/page.js index 8fe5fbb..28ebe2c 100644 --- a/src/app/[locale]/products/[id]/page.js +++ b/src/app/[locale]/products/[id]/page.js @@ -20,13 +20,12 @@ export default function ProductDetail() { const [product, setProduct] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const BASE_URL = "http://server:3001/api"; useEffect(() => { async function fetchProduct() { try { - const res = await fetch( - `http://localhost:3000/api/v1/toothpastes/${id}`, - ); + const res = await fetch(`${BASE_URL}/v1/toothpastes/${id}`); if (!res.ok) { if (res.status === 404) { setError("Product not found"); diff --git a/src/app/[locale]/products/page.js b/src/app/[locale]/products/page.js index 3411b66..500b7af 100644 --- a/src/app/[locale]/products/page.js +++ b/src/app/[locale]/products/page.js @@ -30,7 +30,7 @@ export default function SearchResults() { const [showFilters, setShowFilters] = useState(false); const [products, setProducts] = useState([]); const [loading, setLoading] = useState(true); - const BASE_URL = "http://localhost:3000/api"; + const BASE_URL = "http://server:3001/api"; useEffect(() => { async function fetchProducts() { diff --git a/src/app/[locale]/scan/page.js b/src/app/[locale]/scan/page.js new file mode 100644 index 0000000..d311e4e --- /dev/null +++ b/src/app/[locale]/scan/page.js @@ -0,0 +1,169 @@ +"use client"; +import { useState, useRef } from "react"; +import { Camera, Upload, X, ChevronLeft } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Field, FieldLabel } from "@/components/ui/field"; +import { useRouter } from "next/navigation"; + +export default function ScanProductPage() { + const router = useRouter(); + const [imagePreview, setImagePreview] = useState(null); + const [imageFile, setImageFile] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const fileInputRef = useRef(null); + const cameraInputRef = useRef(null); + + const handleFileSelect = (e) => { + const file = e.target.files?.[0]; + if (file) { + processImage(file); + } + }; + + const processImage = (file) => { + setImageFile(file); + const reader = new FileReader(); + reader.onloadend = () => { + setImagePreview(reader.result); + }; + reader.readAsDataURL(file); + }; + + const removeImage = () => { + setImagePreview(null); + setImageFile(null); + }; + + const handleSubmit = async () => { + if (!imageFile) { + alert("Please select or capture an image"); + return; + } + + setIsSubmitting(true); + + const formDataToSend = new FormData(); + formDataToSend.append("file", imageFile); + + try { + const res = await fetch( + "http://server-fastapi:8000/api/search-by-image", + { + method: "POST", + body: formDataToSend, + }, + ); + + if (res.ok) { + const data = await res.json(); + if (data.results.length > 0) { + router.push(`/products/${data.results[0].id}`); + } else { + router.push("/products"); + } + } else { + alert("Product not found"); + } + } catch (err) { + console.error(err); + alert("Network error — please try again later."); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+
+ + +

+ Scan Product +

+

+ Upload or capture an image to identify the product +

+
+ + + + Product Image + + {!imagePreview + ?
+ + + +
+ :
+
+ Product preview +
+ +
} +
+
+ +
+ + +
+
+
+ ); +}