diff --git a/package-lock.json b/package-lock.json
index e8ba53f..7ca9372 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,16 @@
"name": "surtilatino-frontend-page",
"version": "0.1.0",
"dependencies": {
+ "@supabase/supabase-js": "^2.95.3",
+ "@tanstack/react-query": "^5.90.20",
+ "clsx": "^2.1.1",
+ "framer-motion": "^12.34.0",
+ "lucide-react": "^0.563.0",
"next": "16.1.6",
"react": "19.2.3",
- "react-dom": "19.2.3"
+ "react-dom": "19.2.3",
+ "tailwind-merge": "^3.4.0",
+ "zustand": "^5.0.11"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -1234,6 +1241,86 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@supabase/auth-js": {
+ "version": "2.95.3",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.95.3.tgz",
+ "integrity": "sha512-vD2YoS8E2iKIX0F7EwXTmqhUpaNsmbU6X2R0/NdFcs02oEfnHyNP/3M716f3wVJ2E5XHGiTFXki6lRckhJ0Thg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.95.3",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.95.3.tgz",
+ "integrity": "sha512-uTuOAKzs9R/IovW1krO0ZbUHSJnsnyJElTXIRhjJTqymIVGcHzkAYnBCJqd7468Fs/Foz1BQ7Dv6DCl05lr7ig==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "2.95.3",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.95.3.tgz",
+ "integrity": "sha512-LTrRBqU1gOovxRm1vRXPItSMPBmEFqrfTqdPTRtzOILV4jPSueFz6pES5hpb4LRlkFwCPRmv3nQJ5N625V2Xrg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.95.3",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.95.3.tgz",
+ "integrity": "sha512-D7EAtfU3w6BEUxDACjowWNJo/ZRo7sDIuhuOGKHIm9FHieGeoJV5R6GKTLtga/5l/6fDr2u+WcW/m8I9SYmaIw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/phoenix": "^1.6.6",
+ "@types/ws": "^8.18.1",
+ "tslib": "2.8.1",
+ "ws": "^8.18.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.95.3",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.95.3.tgz",
+ "integrity": "sha512-4GxkJiXI3HHWjxpC3sDx1BVrV87O0hfX+wvJdqGv67KeCu+g44SPnII8y0LL/Wr677jB7tpjAxKdtVWf+xhc9A==",
+ "license": "MIT",
+ "dependencies": {
+ "iceberg-js": "^0.8.1",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.95.3",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.95.3.tgz",
+ "integrity": "sha512-Fukw1cUTQ6xdLiHDJhKKPu6svEPaCEDvThqCne3OaQyZvuq2qjhJAd91kJu3PXLG18aooCgYBaB6qQz35hhABg==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/auth-js": "2.95.3",
+ "@supabase/functions-js": "2.95.3",
+ "@supabase/postgrest-js": "2.95.3",
+ "@supabase/realtime-js": "2.95.3",
+ "@supabase/storage-js": "2.95.3"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1514,6 +1601,32 @@
"tailwindcss": "4.1.18"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.90.20",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
+ "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.90.20",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz",
+ "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.90.20"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -1550,17 +1663,22 @@
"version": "20.19.33",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
"integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
+ "node_modules/@types/phoenix": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
+ "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
+ "license": "MIT"
+ },
"node_modules/@types/react": {
"version": "19.2.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1576,6 +1694,15 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz",
@@ -2587,6 +2714,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2640,7 +2776,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@@ -3596,6 +3732,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.34.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.0.tgz",
+ "integrity": "sha512-+/H49owhzkzQyxtn7nZeF4kdH++I2FWrESQ184Zbcw5cEqNHYkE5yxWxcTLSj5lNx3NWdbIRy5FHqUvetD8FWg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.34.0",
+ "motion-utils": "^12.29.2",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3901,6 +4064,15 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/iceberg-js": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
+ "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4844,6 +5016,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.563.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz",
+ "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -4911,6 +5092,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/motion-dom": {
+ "version": "12.34.0",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.0.tgz",
+ "integrity": "sha512-Lql3NuEcScRDxTAO6GgUsRHBZOWI/3fnMlkMcH5NftzcN37zJta+bpbMAV9px4Nj057TuvRooMK7QrzMCgtz6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.29.2"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.29.2",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz",
+ "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -6029,6 +6225,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tailwind-merge": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
+ "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
"node_modules/tailwindcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
@@ -6308,7 +6514,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/unrs-resolver": {
@@ -6502,6 +6707,27 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ws": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -6544,6 +6770,35 @@
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.11",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz",
+ "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index c41c79a..b0341f6 100644
--- a/package.json
+++ b/package.json
@@ -9,9 +9,16 @@
"lint": "eslint"
},
"dependencies": {
+ "@supabase/supabase-js": "^2.95.3",
+ "@tanstack/react-query": "^5.90.20",
+ "clsx": "^2.1.1",
+ "framer-motion": "^12.34.0",
+ "lucide-react": "^0.563.0",
"next": "16.1.6",
"react": "19.2.3",
- "react-dom": "19.2.3"
+ "react-dom": "19.2.3",
+ "tailwind-merge": "^3.4.0",
+ "zustand": "^5.0.11"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
diff --git a/src/app/globals.css b/src/app/globals.css
index a2dc41e..8d8c06a 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,26 +1,56 @@
+/* src/app/globals.css */
@import "tailwindcss";
-:root {
- --background: #ffffff;
- --foreground: #171717;
-}
+@theme {
+ /* Paleta Surtilatino */
+ --color-accent: #5D9C59;
+ --color-dark: #1a1a1a;
+ --color-light: #f8f8f8;
+ --color-light-hover: #f0f0f0;
-@theme inline {
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
-}
+ /* Bordes Boutique */
+ --radius-huge: 2rem;
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
+ /* Animaciones */
+ --animate-reveal: reveal 0.6s cubic-bezier(0.22, 1, 0.36, 1);
+ --animate-fade-up: fade-up 0.8s cubic-bezier(0.22, 1, 0.36, 1) forwards;
+
+ @keyframes reveal {
+ from { transform: scale(1); }
+ to { transform: scale(1.08); }
+ }
+
+ @keyframes fade-up {
+ from { opacity: 0; transform: translateY(20px); }
+ to { opacity: 1; transform: translateY(0); }
+ }
+ --animate-marquee: marquee 25s linear infinite;
+
+ @keyframes marquee {
+ from { transform: translateX(0); }
+ to { transform: translateX(-50%); }
}
}
-body {
- background: var(--background);
- color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
+/* Clases utilitarias personalizadas */
+@layer utilities {
+ .text-balance {
+ text-wrap: balance;
+ }
+
+ /* Ocultar scrollbar pero permitir scroll */
+ .no-scrollbar::-webkit-scrollbar {
+ display: none;
+ }
+ .no-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ }
}
+
+@layer base {
+ body {
+ @apply bg-white text-dark antialiased selection:bg-accent selection:text-white;
+ }
+}
+
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f7fa87e..b627015 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,20 +1,22 @@
+// src/app/layout.tsx
import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import { Outfit } from "next/font/google";
import "./globals.css";
+import Navbar from "@/components/layout/Navbar";
+import CartDrawer from "@/components/layout/CartDrawer";
+import Footer from "@/components/layout/Footer";
+// (Lo crearemos en el siguiente paso)
-const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
+// Configuración de la fuente Outfit
+const outfit = Outfit({
subsets: ["latin"],
+ variable: "--font-sans", // Vincula con Tailwind
+ weight: ["200", "300", "400", "500", "600"],
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Surtilatino | Mercado Boutique",
+ description: "Sabor auténtico, origen natural.",
};
export default function RootLayout({
@@ -23,12 +25,13 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
-
- {children}
+
+
+
+
+ {children}
+
);
-}
+}
\ No newline at end of file
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 295f8fd..e6293a6 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,65 +1,84 @@
-import Image from "next/image";
+import { supabase, getProductImageUrl } from "@/lib/supabase";
+import Hero from "@/components/sections/Hero";
+import ProductCard from "@/components/ui/ProductCard";
+import Marquee from "@/components/ui/Marquee";
+import { Product } from "@/types";
+
+// Esta función se ejecuta en el SERVIDOR cada vez que alguien entra
+async function getProducts() {
+ const { data, error } = await supabase
+ .from('products')
+ .select('*')
+ .eq('is_active', true) // Solo productos activos
+ .order('created_at', { ascending: false }) // Los más nuevos primero
+ .limit(6); // Traemos solo 6 para la home
+
+ if (error) {
+ console.error("Error cargando productos:", error);
+ return [];
+ }
+
+ return data as Product[];
+}
+
+export default async function Home() {
+ // 1. Obtenemos los datos reales
+ const products = await getProducts();
-export default function Home() {
return (
-
-
-
-
-
- To get started, edit the page.tsx file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
+
+
+
+
+
+
+
+
+ Favoritos
de la semana.
+
+
-
-
-
- Deploy Now
-
-
- Documentation
-
+
+ {/* Grid de Productos */}
+ {products.length > 0 ? (
+
+ {products.map((product, index) => (
+
+ ))}
+
+ ) : (
+ /* Estado Vacío (Empty State) Minimalista */
+
+
Aún no hemos subido productos.
+
Vuelve pronto para ver nuestras novedades.
+
+ )}
+
+
+ {/* Frase Editorial */}
+
+
+
+ "No vendemos solo ingredientes. Vendemos la nostalgia alegría de cocinar como en casa."
+
+
Familia Mercado, desde 2010
-
+
);
-}
+}
\ No newline at end of file
diff --git a/src/app/product/[id]/page.tsx b/src/app/product/[id]/page.tsx
new file mode 100644
index 0000000..7fa0105
--- /dev/null
+++ b/src/app/product/[id]/page.tsx
@@ -0,0 +1,149 @@
+// src/app/product/[id]/page.tsx
+import { notFound } from "next/navigation";
+import Link from "next/link";
+import { ArrowLeft, Truck, ShieldCheck } from "lucide-react";
+import { supabase, getProductImageUrl } from "@/lib/supabase";
+import { Product } from "@/types";
+import ProductCard from "@/components/ui/ProductCard";
+import ProductActions from "@/components/product/ProductActions";
+
+// 1. Función para obtener el producto actual
+async function getProduct(id: string) {
+ const { data, error } = await supabase
+ .from('products')
+ .select('*')
+ .eq('id', id)
+ .single();
+
+ if (error || !data) return null;
+ return data as Product;
+}
+
+// 2. Función para obtener productos relacionados
+async function getRelatedProducts(category: string, currentId: string) {
+ const { data } = await supabase
+ .from('products')
+ .select('*')
+ .eq('category', category)
+ .neq('id', currentId)
+ .eq('is_active', true)
+ .limit(3);
+
+ return (data as Product[]) || [];
+}
+
+// 3. DEFINICIÓN DEL COMPONENTE (Aquí estaba el error de tipos)
+// En lugar de una interfaz separada, tipamos inline para evitar conflictos con Next.js 15
+export default async function ProductPage(props: {
+ params: Promise<{ id: string }>
+}) {
+ // Await obligatorio en Next.js 15 antes de acceder a params
+ const params = await props.params;
+ const { id } = params;
+
+ const product = await getProduct(id);
+
+ if (!product) {
+ notFound();
+ }
+
+ const relatedProducts = await getRelatedProducts(product.category, product.id);
+ const imageUrl = getProductImageUrl(product.image_url);
+
+ return (
+
+ {/* Navegación Breadcrumb */}
+
+
+
+ Volver al catálogo
+
+
+
+
+
+
+ {/* COLUMNA IZQUIERDA: Imagen Heroica */}
+
+
+

+ {/* Badge de Stock */}
+ {product.stock < 5 && product.stock > 0 && (
+
+ Últimas {product.stock} unidades
+
+ )}
+
+
+
+ {/* COLUMNA DERECHA: Detalles */}
+
+
+ {product.category}
+
+
+
+ {product.name}
+
+
+
+
+ {new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(product.price)}
+
+ {product.stock > 0 ? (
+
+
+ En Stock
+
+ ) : (
+ Agotado
+ )}
+
+
+
+
{product.description || "Sin descripción disponible para este producto."}
+
+
+ {/* Componente Interactivo */}
+
+
+
+
+
+ Envío en 24/48h
+
+
+
+ Calidad Garantizada
+
+
+
+
+
+ {/* Relacionados */}
+ {relatedProducts.length > 0 && (
+
+ También te podría gustar
+
+ {relatedProducts.map((prod, idx) => (
+
+ ))}
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/shop/page.tsx b/src/app/shop/page.tsx
new file mode 100644
index 0000000..4c8ed85
--- /dev/null
+++ b/src/app/shop/page.tsx
@@ -0,0 +1,52 @@
+// src/app/shop/page.tsx
+import { supabase } from "@/lib/supabase";
+import { Product, Category } from "@/types";
+import CatalogBrowser from "@/components/shop/CatalogBrowser";
+
+export const revalidate = 0; // Datos siempre frescos
+
+// 1. Tu función para traer categorías
+async function getCategories() {
+ const { data, error } = await supabase
+ .from('categories')
+ .select('*')
+ .order('name', { ascending: true });
+
+ if (error) {
+ console.error('Error cargando categorías:', error.message);
+ return [];
+ }
+
+ return data as Category[];
+}
+
+// 2. Función para traer productos
+async function getAllProducts() {
+ const { data, error } = await supabase
+ .from('products')
+ .select('*')
+ .eq('is_active', true)
+ .order('created_at', { ascending: false });
+
+ if (error) {
+ console.error("Error fetching shop products:", error);
+ return [];
+ }
+
+ return data as Product[];
+}
+
+export default async function ShopPage() {
+ // 3. Ejecutamos ambas peticiones a la vez (Parallel Data Fetching)
+ const [products, categories] = await Promise.all([
+ getAllProducts(),
+ getCategories()
+ ]);
+
+ return (
+
+ {/* Pasamos ambas listas al componente cliente */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/layout/CartDrawer.tsx b/src/components/layout/CartDrawer.tsx
new file mode 100644
index 0000000..b61c3c8
--- /dev/null
+++ b/src/components/layout/CartDrawer.tsx
@@ -0,0 +1,143 @@
+"use client";
+
+import { useCartStore } from "@/store/cart-store";
+import { X, Minus, Plus, Trash2, ArrowRight } from "lucide-react";
+import { useEffect, useState } from "react";
+import Image from "next/image";
+import { getProductImageUrl } from "@/lib/supabase";
+import { cn } from "@/lib/utils";
+
+export default function CartDrawer() {
+ const {
+ items,
+ isOpen,
+ closeCart,
+ removeItem,
+ updateQuantity,
+ getTotalPrice
+ } = useCartStore();
+
+ const [mounted, setMounted] = useState(false);
+
+ // Evitar problemas de hidratación (SSR vs Client)
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (!mounted) return null;
+
+ return (
+ <>
+ {/* Overlay Oscuro (Backdrop) */}
+
+
+ {/* Panel Deslizante */}
+
+
+
+ {/* Header */}
+
+
+ Tu Pedido ({items.length})
+
+
+
+
+ {/* Lista de Items */}
+
+ {items.length === 0 ? (
+
+
🥑
+
Tu cesta está vacía.
+
+
+ ) : (
+
+ {items.map((item) => (
+
+ {/* Imagen Miniatura */}
+
+
})
+
+
+ {/* Info */}
+
+
+
+
{item.name}
+
{item.category}
+
+
+
+
+
+ {/* Control Cantidad Mini */}
+
+
+
{item.quantity}
+
+
+
+
+ {new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(item.price * item.quantity)}
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Footer / Checkout */}
+ {items.length > 0 && (
+
+
+ Subtotal
+
+ {new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(getTotalPrice())}
+
+
+
+
+
+
+ Envío calculado en el siguiente paso
+
+
+ )}
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx
new file mode 100644
index 0000000..d64ef11
--- /dev/null
+++ b/src/components/layout/Footer.tsx
@@ -0,0 +1,120 @@
+import Link from "next/link";
+import { Instagram, Facebook, ArrowRight, MapPin, Phone, Clock } from "lucide-react";
+
+export default function Footer() {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx
new file mode 100644
index 0000000..0e4e87b
--- /dev/null
+++ b/src/components/layout/Navbar.tsx
@@ -0,0 +1,123 @@
+// src/components/layout/Navbar.tsx
+"use client";
+
+import Link from "next/link";
+import { Search, ShoppingBag, Menu, X } from "lucide-react"; // Añadimos 'X' para cerrar
+import { useState, useEffect } from "react";
+import { cn } from "@/lib/utils";
+import { useCartStore } from "@/store/cart-store";
+
+export default function Navbar() {
+ const [scrolled, setScrolled] = useState(false);
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false); // Estado para menú móvil
+
+ const { openCart, items } = useCartStore();
+ const itemCount = items.reduce((acc, item) => acc + item.quantity, 0);
+
+ // Efecto: Detectar scroll para el fondo glassmorphism
+ useEffect(() => {
+ const handleScroll = () => setScrolled(window.scrollY > 20);
+ window.addEventListener("scroll", handleScroll);
+ return () => window.removeEventListener("scroll", handleScroll);
+ }, []);
+
+ // Efecto: Bloquear el scroll del body cuando el menú móvil está abierto
+ useEffect(() => {
+ if (mobileMenuOpen) {
+ document.body.style.overflow = "hidden";
+ } else {
+ document.body.style.overflow = "unset";
+ }
+ }, [mobileMenuOpen]);
+
+ // Lista de enlaces para reutilizar
+ const navLinks = ["Shop", "Novedades", "Origen"];
+
+ return (
+ <>
+
+
+ {/* MENÚ MÓVIL (Overlay Pantalla Completa) */}
+
+ {navLinks.map((item) => (
+
setMobileMenuOpen(false)}
+ className="text-4xl font-light tracking-tight hover:text-accent transition-colors duration-300"
+ >
+ {item}
+
+ ))}
+
+
+ Instagram
+ Contacto
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/components/product/ProductActions.tsx b/src/components/product/ProductActions.tsx
new file mode 100644
index 0000000..3f05518
--- /dev/null
+++ b/src/components/product/ProductActions.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import { useState } from "react";
+import { Minus, Plus, ShoppingBag } from "lucide-react";
+import { useCartStore } from "@/store/cart-store";
+import { Product } from "@/types";
+
+interface ProductActionsProps {
+ product: Product;
+}
+
+export default function ProductActions({ product }: ProductActionsProps) {
+ const [quantity, setQuantity] = useState(1);
+ const addItem = useCartStore((state) => state.addItem);
+
+ const handleIncrement = () => setQuantity((prev) => prev + 1);
+ const handleDecrement = () => setQuantity((prev) => (prev > 1 ? prev - 1 : 1));
+
+ const handleAddToCart = () => {
+ addItem(product, quantity);
+ // El store ya se encarga de abrir el drawer automáticamente
+ };
+
+ return (
+
+ {/* Selector de Cantidad */}
+
+
+
+
+ {quantity}
+
+
+
+
+
+ {/* Botón de Añadir */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/sections/FeaturedGrid.tsx b/src/components/sections/FeaturedGrid.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/sections/Hero.tsx b/src/components/sections/Hero.tsx
new file mode 100644
index 0000000..e981998
--- /dev/null
+++ b/src/components/sections/Hero.tsx
@@ -0,0 +1,80 @@
+// src/components/sections/Hero.tsx
+"use client";
+
+import { motion } from "framer-motion";
+import { PlayCircle, ArrowRight } from "lucide-react";
+import Image from "next/image";
+
+export default function Hero() {
+ return (
+
+
+ {/* Etiqueta Superior */}
+
+ Esenciales Latinos
+
+
+ {/* Titular Principal */}
+
+ Sabor auténtico,
+
+ origen natural.
+ {/* Subrayado orgánico decorativo opcional */}
+
+
+
+
+ {/* Botones de Acción */}
+
+
+
+
+
+
+ {/* Imagen Principal con Decoración */}
+
+ {/* Círculo de fondo decorativo */}
+
+
+ {/* Imagen (Usamos un placeholder de alta calidad por ahora) */}
+
+ {/* Nota: En producción usarás el componente Image de Next.js.
+ Para este ejemplo rápido uso img estándar con una URL externa */}
+

+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/sections/InfoSection.tsx b/src/components/sections/InfoSection.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/shop/CatalogBrowser.tsx b/src/components/shop/CatalogBrowser.tsx
new file mode 100644
index 0000000..7949b94
--- /dev/null
+++ b/src/components/shop/CatalogBrowser.tsx
@@ -0,0 +1,150 @@
+"use client";
+
+import { useState, useMemo } from "react";
+import { Product, Category } from "@/types";
+import ProductCard from "@/components/ui/ProductCard";
+import { getProductImageUrl } from "@/lib/supabase";
+import { cn } from "@/lib/utils";
+
+interface CatalogBrowserProps {
+ initialProducts: Product[];
+ categories: Category[];
+}
+
+export default function CatalogBrowser({ initialProducts, categories }: CatalogBrowserProps) {
+ // AHORA: activeCategory guarda el ID ('all' o el UUID de la categoría)
+ const [activeCategory, setActiveCategory] = useState("all");
+ const [sortOrder, setSortOrder] = useState("Destacados");
+
+ // 1. Preparamos el menú (añadimos la opción "Todo" con ID 'all')
+ const menuCategories = useMemo(() => {
+ return [
+ { id: 'all', name: 'Todo' },
+ ...categories
+ ];
+ }, [categories]);
+
+ // 2. Lógica de Filtrado y Ordenación
+ const filteredProducts = useMemo(() => {
+ let result = [...initialProducts];
+
+ // FILTRADO POR ID (Mucho más seguro)
+ if (activeCategory !== "all") {
+ result = result.filter((p) => p.category_id === activeCategory);
+ }
+
+ // Ordenar
+ if (sortOrder === "Precio: Menor a Mayor") {
+ result.sort((a, b) => a.price - b.price);
+ } else if (sortOrder === "Precio: Mayor a Menor") {
+ result.sort((a, b) => b.price - a.price);
+ } else if (sortOrder === "Novedades") {
+ result.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
+ }
+
+ return result;
+ }, [initialProducts, activeCategory, sortOrder]);
+
+ // Helper para obtener el nombre de la categoría de un producto
+ const getCategoryName = (catId: string) => {
+ const category = categories.find((c) => c.id === catId);
+ return category ? category.name : "General";
+ };
+
+ return (
+ <>
+
+
+
Colección {new Date().getFullYear()}
+
+ La Despensa.
+
+
+
+ Explora nuestra selección de productos auténticos, importados directamente para tu cocina.
+
+
+
+ {/* FILTROS STICKY */}
+
+
+
+
+
+ {menuCategories.map((cat) => (
+
+ ))}
+
+
+
+
+ Ver:
+
+
+
+
+
+
+ {filteredProducts.length > 0 ? (
+
+ {filteredProducts.map((product, index) => (
+
+ ))}
+
+ ) : (
+
+
No encontramos productos en esta categoría.
+
+
+ )}
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/ui/Marquee.tsx b/src/components/ui/Marquee.tsx
new file mode 100644
index 0000000..c2a7792
--- /dev/null
+++ b/src/components/ui/Marquee.tsx
@@ -0,0 +1,17 @@
+// src/components/ui/Marquee.tsx
+export default function Marquee() {
+ return (
+
+
+
+ {/* Repetimos el contenido suficientes veces para cubrir pantallas grandes */}
+ {[...Array(10)].map((_, i) => (
+
+ Orgánico • Fresco • Importado • Local
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ui/ProductCard.tsx b/src/components/ui/ProductCard.tsx
new file mode 100644
index 0000000..30f956f
--- /dev/null
+++ b/src/components/ui/ProductCard.tsx
@@ -0,0 +1,59 @@
+// src/components/ui/ProductCard.tsx
+import Image from "next/image";
+import Link from "next/link";
+import { Plus } from "lucide-react";
+
+interface ProductCardProps {
+ id: string;
+ title: string;
+ category: string;
+ price: number;
+ imageUrl: string;
+ index?: number; // Para escalonar animaciones si queremos
+}
+
+export default function ProductCard({ id, title, category, price, imageUrl, index = 1 }: ProductCardProps) {
+ return (
+
+ {/* Contenedor de Imagen (Gris pálido, bordes muy redondos) */}
+
+
+ {/* Número de índice estilo editorial */}
+
+ {index.toString().padStart(2, '0')}
+
+
+ {/* Botón flotante "Quick Add" (aparece en hover) */}
+
+
+ {/* Imagen del Producto */}
+
+

+
+
+
+ {/* Información del Producto */}
+
+
+
+ {title}
+
+
+ {category}
+
+
+
+
+ {new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' }).format(price)}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ui/SectionHeader.tsx b/src/components/ui/SectionHeader.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts
new file mode 100644
index 0000000..6a7bfc7
--- /dev/null
+++ b/src/lib/supabase.ts
@@ -0,0 +1,28 @@
+import { createClient } from '@supabase/supabase-js';
+
+// 1. Usamos tus variables exactas
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
+// Aceptamos la clave nueva (sb_publishable) O la antigua (anon) por si acaso
+const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
+
+// 2. Verificación de seguridad
+if (!supabaseUrl || !supabaseKey) {
+ throw new Error(
+ 'Faltan las variables de entorno. Asegúrate de que tu archivo .env.local tenga NEXT_PUBLIC_SUPABASE_URL y NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY'
+ );
+}
+
+// 3. Crear cliente (Supabase detecta automáticamente si es sb_publishable o anon)
+export const supabase = createClient(supabaseUrl, supabaseKey);
+
+// Helper de imágenes
+export const getProductImageUrl = (imagePath: string | null) => {
+ if (!imagePath) return 'https://placehold.co/400x600/f8f8f8/e0e0e0?text=Sin+Imagen';
+ if (imagePath.startsWith('http')) return imagePath;
+
+ const { data } = supabase.storage
+ .from('products')
+ .getPublicUrl(imagePath);
+
+ return data.publicUrl;
+};
\ No newline at end of file
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..24b9d74
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,7 @@
+// src/lib/utils.ts
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
\ No newline at end of file
diff --git a/src/store/cart-store.ts b/src/store/cart-store.ts
new file mode 100644
index 0000000..4eb2ed0
--- /dev/null
+++ b/src/store/cart-store.ts
@@ -0,0 +1,77 @@
+import { create } from 'zustand';
+import { persist, createJSONStorage } from 'zustand/middleware';
+import { Product, CartItem } from '@/types';
+
+interface CartState {
+ items: CartItem[];
+ isOpen: boolean;
+ addItem: (product: Product, quantity?: number) => void;
+ removeItem: (productId: string) => void;
+ updateQuantity: (productId: string, quantity: number) => void;
+ clearCart: () => void;
+ toggleCart: () => void;
+ openCart: () => void;
+ closeCart: () => void;
+ // Getters
+ getTotalPrice: () => number;
+ getTotalItems: () => number;
+}
+
+export const useCartStore = create()(
+ persist(
+ (set, get) => ({
+ items: [],
+ isOpen: false,
+
+ addItem: (product, quantity = 1) => {
+ const currentItems = get().items;
+ const existingItem = currentItems.find((item) => item.id === product.id);
+
+ if (existingItem) {
+ // Si ya existe, sumamos la cantidad
+ const updatedItems = currentItems.map((item) =>
+ item.id === product.id
+ ? { ...item, quantity: item.quantity + quantity }
+ : item
+ );
+ set({ items: updatedItems, isOpen: true }); // Abrimos el carrito al añadir
+ } else {
+ // Si es nuevo, lo añadimos
+ set({ items: [...currentItems, { ...product, quantity }], isOpen: true });
+ }
+ },
+
+ removeItem: (id) => {
+ set({ items: get().items.filter((item) => item.id !== id) });
+ },
+
+ updateQuantity: (id, quantity) => {
+ if (quantity < 1) return;
+ set({
+ items: get().items.map((item) =>
+ item.id === id ? { ...item, quantity } : item
+ ),
+ });
+ },
+
+ clearCart: () => set({ items: [] }),
+
+ // Control de UI del Drawer
+ toggleCart: () => set({ isOpen: !get().isOpen }),
+ openCart: () => set({ isOpen: true }),
+ closeCart: () => set({ isOpen: false }),
+
+ getTotalPrice: () => {
+ return get().items.reduce((total, item) => total + item.price * item.quantity, 0);
+ },
+
+ getTotalItems: () => {
+ return get().items.reduce((total, item) => total + item.quantity, 0);
+ },
+ }),
+ {
+ name: 'surtilatino-cart', // Nombre en LocalStorage
+ storage: createJSONStorage(() => localStorage),
+ }
+ )
+);
\ No newline at end of file
diff --git a/src/types/database.types.ts b/src/types/database.types.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..8510856
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,25 @@
+export interface Product {
+ id: string;
+ created_at: string;
+ name: string;
+ description: string | null;
+ price: number;
+ stock: number;
+ category_id: string;
+ image_url: string | null; // El path dentro del bucket
+ is_active: boolean;
+ stripe_product_id?: string;
+ stripe_price_id?: string;
+}
+
+export interface Category {
+ id: string;
+ name: string; // Ej: "Frutas & Verduras"
+ slug: string; // Ej: "frutas-verduras" (útil para URLs)
+ description?: string;
+}
+
+export interface CartItem extends Product {
+ quantity: number;
+}
+
diff --git a/structure.sh b/structure.sh
new file mode 100644
index 0000000..2d471ac
--- /dev/null
+++ b/structure.sh
@@ -0,0 +1,33 @@
+# 1. Crear estructura de directorios dentro de src
+mkdir -p src/components/layout # Navbar, Footer
+mkdir -p src/components/ui # Botones, Tarjetas, Elementos base
+mkdir -p src/components/sections # Hero, Grids de productos, Banners
+mkdir -p src/lib # Cliente Supabase, utilidades (clsx)
+mkdir -p src/store # Estado global (Zustand)
+mkdir -p src/types # Definiciones de TypeScript (DB, Productos)
+mkdir -p src/hooks # Hooks personalizados (si fueran necesarios)
+
+# 2. Crear archivos para la Lógica y Configuración
+touch src/lib/supabase.ts # Inicialización del cliente Supabase
+touch src/lib/utils.ts # Helper para clases css (tailwind-merge)
+touch src/store/cart-store.ts # Store de carrito (Zustand)
+touch src/types/database.types.ts # Tipos generados de Supabase
+touch src/types/index.ts # Tipos globales de la app
+
+# 3. Crear archivos de Componentes (Vacíos por ahora)
+# Layout
+touch src/components/layout/Navbar.tsx
+touch src/components/layout/Footer.tsx
+
+# Secciones Principales (Home)
+touch src/components/sections/Hero.tsx
+touch src/components/sections/FeaturedGrid.tsx
+touch src/components/sections/InfoSection.tsx
+
+# Componentes de UI (Estilo Boutique)
+touch src/components/ui/ProductCard.tsx
+touch src/components/ui/Button.tsx
+touch src/components/ui/SectionHeader.tsx
+
+# 4. Mensaje de confirmación
+echo "✅ Estructura 'Surtilatino Minimal' generada correctamente."
\ No newline at end of file