From db00c3ec899511f8476f0b1efdc6d96dfc5d9761 Mon Sep 17 00:00:00 2001 From: jprcdev Date: Tue, 10 Feb 2026 17:51:21 +0100 Subject: [PATCH] first commit --- package-lock.json | 265 +++++++++++++++++++++- package.json | 9 +- src/app/globals.css | 66 ++++-- src/app/layout.tsx | 35 +-- src/app/page.tsx | 137 ++++++----- src/app/product/[id]/page.tsx | 149 ++++++++++++ src/app/shop/page.tsx | 52 +++++ src/components/layout/CartDrawer.tsx | 143 ++++++++++++ src/components/layout/Footer.tsx | 120 ++++++++++ src/components/layout/Navbar.tsx | 123 ++++++++++ src/components/product/ProductActions.tsx | 60 +++++ src/components/sections/FeaturedGrid.tsx | 0 src/components/sections/Hero.tsx | 80 +++++++ src/components/sections/InfoSection.tsx | 0 src/components/shop/CatalogBrowser.tsx | 150 ++++++++++++ src/components/ui/Button.tsx | 0 src/components/ui/Marquee.tsx | 17 ++ src/components/ui/ProductCard.tsx | 59 +++++ src/components/ui/SectionHeader.tsx | 0 src/lib/supabase.ts | 28 +++ src/lib/utils.ts | 7 + src/store/cart-store.ts | 77 +++++++ src/types/database.types.ts | 0 src/types/index.ts | 25 ++ structure.sh | 33 +++ 25 files changed, 1536 insertions(+), 99 deletions(-) create mode 100644 src/app/product/[id]/page.tsx create mode 100644 src/app/shop/page.tsx create mode 100644 src/components/layout/CartDrawer.tsx create mode 100644 src/components/layout/Footer.tsx create mode 100644 src/components/layout/Navbar.tsx create mode 100644 src/components/product/ProductActions.tsx create mode 100644 src/components/sections/FeaturedGrid.tsx create mode 100644 src/components/sections/Hero.tsx create mode 100644 src/components/sections/InfoSection.tsx create mode 100644 src/components/shop/CatalogBrowser.tsx create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/Marquee.tsx create mode 100644 src/components/ui/ProductCard.tsx create mode 100644 src/components/ui/SectionHeader.tsx create mode 100644 src/lib/supabase.ts create mode 100644 src/lib/utils.ts create mode 100644 src/store/cart-store.ts create mode 100644 src/types/database.types.ts create mode 100644 src/types/index.ts create mode 100644 structure.sh 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 ( -
-
- Next.js logo -
-

- 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. +

+
+

+ {products.length} productos disponibles +

+ + Ver catálogo completo + +
-
- - Vercel logomark - 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 */} +
+
+ {product.name} + {/* 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 */} +
+ {item.name} +
+ + {/* 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 ( +
+
+ + {/* SECCIÓN SUPERIOR: Newsletter y Promesa */} +
+
+

+ Recibe lo mejor de Latinoamérica en tu mesa. +

+

+ Suscríbete para recibir ofertas exclusivas, novedades de temporada y recetas auténticas. +

+ +
+ + +
+
+ +
+ {/* Redes Sociales Minimalistas */} + + + + + + +
+
+ + {/* SECCIÓN MEDIA: Enlaces y Contacto Real */} +
+ + {/* Columna 1: Marca */} +
+ + SurtiLatino. + +

+ Barcelona • Since 2024 +

+
+ + {/* Columna 2: Shop */} +
+

Tienda

+ Novedades + Despensa + Frutas & Verduras + Bebidas +
+ + {/* Columna 3: Información Real */} +
+

Visítanos

+ + + +
+ + 611 94 74 65 +
+ +
+ +
+

Mar - Dom: 10:00 - 21:00

+

Lunes cerrado

+
+
+
+ + {/* Columna 4: Legal */} +
+

Ayuda

+ Envíos y Devoluciones + Aviso Legal + Política de Privacidad + Contacto +
+
+ + {/* BOTTOM BAR: Copyright */} +
+

© {new Date().getFullYear()} SurtiLatino. Todos los derechos reservados.

+
+ Hecho con ♥ en Barcelona +
+
+ +
+
+ ); +} \ 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 */} + Bodegón de frutas frescas +
+
+
+ ); +} \ 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.

+ +
+ )} + +
+

¿Buscas algo específico?

+ + WA + Consultar disponibilidad en tienda + +
+
+ + ); +} \ 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 */} +
+ {title} +
+
+ + {/* 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