{product.title}
+${(product.price_cents / 100).toFixed(2)}
+diff --git a/Dockerfile.agent.example b/Dockerfile.agent.example new file mode 100644 index 0000000..e1a416a --- /dev/null +++ b/Dockerfile.agent.example @@ -0,0 +1,16 @@ +FROM node:20-slim + +WORKDIR /app + +# Install dependencies first (for better caching) +COPY package*.json ./ +RUN npm install + +# Copy application files +COPY . . + +# Expose Next.js dev port +EXPOSE 3000 + +# Start Next.js in dev mode with hot reload +CMD ["npm", "run", "dev"] diff --git a/docker-compose.agent.example.yml b/docker-compose.agent.example.yml new file mode 100644 index 0000000..078aca3 --- /dev/null +++ b/docker-compose.agent.example.yml @@ -0,0 +1,73 @@ +# Example docker-compose.agent.yml for running the generated clone +# Copy and adapt this file to your project's needs +# +# Key things to customize: +# - POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB (database credentials) +# - The dump file name in db-init (e.g., postgres_backup.dump) +# - Volume mounts for your app structure + +services: + db: + image: postgres:16 + ports: + - "5433:5432" + environment: + POSTGRES_USER: myuser + POSTGRES_PASSWORD: mypassword + POSTGRES_DB: mydb + volumes: + - postgres_data_agent:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"] + interval: 5s + timeout: 5s + retries: 10 + + db-init: + image: postgres:16 + depends_on: + db: + condition: service_healthy + volumes: + - .:/data:ro + environment: + PGPASSWORD: mypassword + command: > + sh -c " + TABLE_COUNT=$$(psql -h db -U myuser -d mydb -tAc \"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public';\" | tr -d '[:space:]'); + if [ \"$$TABLE_COUNT\" = \"0\" ] && [ -f /data/postgres_backup.dump ]; then + echo 'Empty database and dump file found. Restoring...'; + pg_restore -h db -U myuser -d mydb --no-owner --clean --if-exists /data/postgres_backup.dump 2>&1 || echo 'Restore completed'; + else + echo 'Database already initialized or no dump file found. TABLE_COUNT=$$TABLE_COUNT'; + fi" + + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + volumes: + - ./app:/app/app + - ./components:/app/components + - ./lib:/app/lib + - ./public:/app/public + - ./styles:/app/styles + environment: + NODE_ENV: development + DB_HOST: db + DB_PORT: 5432 + DB_USER: myuser + DB_PASSWORD: mypassword + DB_DATABASE: mydb + DATABASE_URL: postgresql://myuser:mypassword@db:5432/mydb + NEXT_PUBLIC_API_URL: "" + depends_on: + db: + condition: service_healthy + db-init: + condition: service_completed_successfully + +volumes: + postgres_data_agent: diff --git a/frontend-nextjs/app/api/images/products/[id]/route.ts b/frontend-nextjs/app/api/images/products/[id]/route.ts new file mode 100644 index 0000000..a7d9e3d --- /dev/null +++ b/frontend-nextjs/app/api/images/products/[id]/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from "next/server"; +import { Pool } from "pg"; + +const pool = new Pool({ + host: process.env.DB_HOST || "127.0.0.1", + port: parseInt(process.env.DB_PORT || "5432"), + user: process.env.DB_USER || "myuser", + password: process.env.DB_PASSWORD || "mypassword", + database: process.env.DB_DATABASE || "mydb", +}); + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { id } = params; + + // Try to get image from database (stored as bytea or base64) + const result = await pool.query( + `SELECT image_blob, image_mime_type + FROM products + WHERE id = $1`, + [id] + ); + + if (result.rows.length === 0 || !result.rows[0].image_blob) { + // Return a placeholder image or 404 + return NextResponse.json( + { error: "Image not found" }, + { status: 404 } + ); + } + + const { image_blob, image_mime_type } = result.rows[0]; + const mimeType = image_mime_type || "image/jpeg"; + + // If stored as base64 string, decode it + let imageBuffer: Buffer; + if (typeof image_blob === "string") { + // Remove data URL prefix if present + const base64Data = image_blob.replace(/^data:image\/\w+;base64,/, ""); + imageBuffer = Buffer.from(base64Data, "base64"); + } else { + // Already a Buffer (bytea from Postgres) + imageBuffer = Buffer.from(image_blob); + } + + return new NextResponse(imageBuffer, { + headers: { + "Content-Type": mimeType, + "Cache-Control": "public, max-age=31536000, immutable", + }, + }); + } catch (error) { + console.error("Error fetching image:", error); + return NextResponse.json( + { error: "Failed to fetch image" }, + { status: 500 } + ); + } +} diff --git a/frontend-nextjs/app/api/products/[id]/route.ts b/frontend-nextjs/app/api/products/[id]/route.ts new file mode 100644 index 0000000..74ca6f8 --- /dev/null +++ b/frontend-nextjs/app/api/products/[id]/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from "next/server"; +import { Pool } from "pg"; + +const pool = new Pool({ + host: process.env.DB_HOST || "127.0.0.1", + port: parseInt(process.env.DB_PORT || "5432"), + user: process.env.DB_USER || "myuser", + password: process.env.DB_PASSWORD || "mypassword", + database: process.env.DB_DATABASE || "mydb", +}); + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const { id } = params; + + const result = await pool.query( + `SELECT id, title, price_cents, slug, description + FROM products + WHERE id = $1`, + [id] + ); + + if (result.rows.length === 0) { + return NextResponse.json( + { error: "Product not found" }, + { status: 404 } + ); + } + + return NextResponse.json(result.rows[0]); + } catch (error) { + console.error("Error fetching product:", error); + return NextResponse.json( + { error: "Failed to fetch product" }, + { status: 500 } + ); + } +} diff --git a/frontend-nextjs/app/api/products/route.ts b/frontend-nextjs/app/api/products/route.ts new file mode 100644 index 0000000..7d0332d --- /dev/null +++ b/frontend-nextjs/app/api/products/route.ts @@ -0,0 +1,30 @@ +import { NextResponse } from "next/server"; +import { Pool } from "pg"; + +// Database connection - uses environment variables +const pool = new Pool({ + host: process.env.DB_HOST || "127.0.0.1", + port: parseInt(process.env.DB_PORT || "5432"), + user: process.env.DB_USER || "myuser", + password: process.env.DB_PASSWORD || "mypassword", + database: process.env.DB_DATABASE || "mydb", +}); + +export async function GET() { + try { + const result = await pool.query(` + SELECT id, title, price_cents, slug + FROM products + ORDER BY id + LIMIT 100 + `); + + return NextResponse.json(result.rows); + } catch (error) { + console.error("Error fetching products:", error); + return NextResponse.json( + { error: "Failed to fetch products" }, + { status: 500 } + ); + } +} diff --git a/frontend-nextjs/app/globals.css b/frontend-nextjs/app/globals.css new file mode 100644 index 0000000..152175d --- /dev/null +++ b/frontend-nextjs/app/globals.css @@ -0,0 +1,15 @@ +:root { + color-scheme: light; +} + +html, body { + margin: 0; + padding: 0; + font-family: system-ui, -apple-system, Segoe UI, sans-serif; + background: #f7f7f5; + color: #1d1d1f; +} + +main { + min-height: 100vh; +} diff --git a/frontend-nextjs/app/layout.tsx b/frontend-nextjs/app/layout.tsx new file mode 100644 index 0000000..becf5d4 --- /dev/null +++ b/frontend-nextjs/app/layout.tsx @@ -0,0 +1,9 @@ +import "./globals.css"; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + +
{children} + + ); +} diff --git a/frontend-nextjs/app/listing/[product_id]/[slug]/page.tsx b/frontend-nextjs/app/listing/[product_id]/[slug]/page.tsx new file mode 100644 index 0000000..32f174b --- /dev/null +++ b/frontend-nextjs/app/listing/[product_id]/[slug]/page.tsx @@ -0,0 +1,27 @@ +import Header from "@/components/Header"; +import Footer from "@/components/Footer"; +import { fetchProduct } from "@/lib/api"; + +export default async function Page({ params }: { params: { product_id: string; slug: string } }) { + const product = await fetchProduct(params.product_id); + + return ( +${(product.price_cents / 100).toFixed(2)}
+