diff --git a/package-lock.json b/package-lock.json index 82b9696..2cf38b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,10 @@ "name": "boulangerie-nextjs", "version": "0.1.0", "dependencies": { + "framer-motion": "^11.11.11", "next": "15.0.2", - "react": "19.0.0-rc-02c0e824-20241028", - "react-dom": "19.0.0-rc-02c0e824-20241028" + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { "@types/node": "^20", @@ -2722,6 +2723,31 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/framer-motion": { + "version": "11.11.11", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.11.11.tgz", + "integrity": "sha512-tuDH23ptJAKUHGydJQII9PhABNJBpB+z0P1bmgKK9QFIssHGlfPd6kxMq00LSKwE27WFsb2z0ovY0bpUyMvfRw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3564,7 +3590,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -3718,7 +3743,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -4456,24 +4480,28 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.0.0-rc-02c0e824-20241028", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0-rc-02c0e824-20241028.tgz", - "integrity": "sha512-GbZ7hpPHQMiEu53BqEaPQVM/4GG4hARo+mqEEnx4rYporDvNvUjutiAFxYFSbu6sgHwcr7LeFv8htEOwALVA2A==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.0.0-rc-02c0e824-20241028", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0-rc-02c0e824-20241028.tgz", - "integrity": "sha512-LrZf3DfHL6Fs07wwlUCHrzFTCMM19yA99MvJpfLokN4I2nBAZvREGZjZAn8VPiSfN72+i9j1eL4wB8gC695F3Q==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "license": "MIT", "dependencies": { - "scheduler": "0.25.0-rc-02c0e824-20241028" + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" }, "peerDependencies": { - "react": "19.0.0-rc-02c0e824-20241028" + "react": "^18.2.0" } }, "node_modules/react-is": { @@ -4675,10 +4703,13 @@ } }, "node_modules/scheduler": { - "version": "0.25.0-rc-02c0e824-20241028", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-02c0e824-20241028.tgz", - "integrity": "sha512-GysnKjmMSaWcwsKTLzeJO0IhU3EyIiC0ivJKE6yDNLqt3IMxDByx8b6lSNXRNdN+ULUY0WLLjSPaZ0LuU/GnTg==", - "license": "MIT" + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } }, "node_modules/semver": { "version": "7.6.3", diff --git a/package.json b/package.json index 5b195e1..d7fd6fc 100644 --- a/package.json +++ b/package.json @@ -9,18 +9,19 @@ "lint": "next lint" }, "dependencies": { - "react": "19.0.0-rc-02c0e824-20241028", - "react-dom": "19.0.0-rc-02c0e824-20241028", - "next": "15.0.2" + "framer-motion": "^11.11.11", + "next": "15.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "15.0.2", "postcss": "^8", "tailwindcss": "^3.4.1", - "eslint": "^8", - "eslint-config-next": "15.0.2" + "typescript": "^5" } } diff --git a/src/app/produits/page.tsx b/src/app/produits/page.tsx index 05f0f57..04b3f6a 100644 --- a/src/app/produits/page.tsx +++ b/src/app/produits/page.tsx @@ -1,149 +1,24 @@ -import Image from 'next/image'; +import { Metadata } from 'next'; +import ProductsPage from '@/components/ProductsPage'; -interface Product { - id: number; - name: string; - description: string; - price: string; - category: string; - image: string; -} - -const products: Product[] = [ - { - id: 1, - name: "Pagnot Traditionnel", - description: "Pain de campagne à la croûte dorée et croustillante, façonné à la main selon la tradition.", - price: "1.50€", - category: "Pains", - image: "/images/produits/pagnot.jpeg" +export const metadata: Metadata = { + title: 'Nos Produits | Au Blé d\'Or - Boulangerie Artisanale', + description: 'Découvrez notre sélection de pains traditionnels, viennoiseries et pâtisseries artisanales. Produits frais préparés chaque jour avec passion.', + keywords: 'boulangerie, pâtisserie, pain artisanal, viennoiseries, croissant, chocolatine, pagnot, baguette tradition', + openGraph: { + title: 'Nos Produits | Au Blé d\'Or', + description: 'Découvrez nos produits artisanaux frais', + images: [ + { + url: '/images/og/products.jpg', + width: 1200, + height: 630, + alt: 'Produits de la boulangerie Au Blé d\'Or', + }, + ], }, - { - id: 2, - name: "Baguette Tradition", - description: "La classique baguette française, croustillante à l'extérieur et moelleuse à l'intérieur.", - price: "0.90€", - category: "Pains", - image: "/images/produits/baguette.jpeg" - }, - { - id: 3, - name: "Cupcake Vanille", - description: "Délicieux cupcake à la vanille de Madagascar, surmonté d'une crème légère.", - price: "3.00€", - category: "Pâtisseries", - image: "/images/produits/cupcake.jpg" - }, - { - id: 4, - name: "Gâteau Royal Chocolat", - description: "Gâteau au chocolat noir intense, avec ses trois couches de mousse au chocolat.", - price: "12.50€", - category: "Pâtisseries", - image: "/images/produits/gateau-royal.jpg" - }, - { - id: 5, - name: "Croissant Pur Beurre", - description: "Croissant artisanal au beurre AOP, feuilleté et croustillant.", - price: "1.45€", - category: "Viennoiseries", - image: "/images/produits/croissant.jpeg" - }, - { - id: 6, - name: "Chocolatine", - description: "Viennoiserie au chocolat noir, feuilletée et généreusement garnie.", - price: "1.75€", - category: "Viennoiseries", - image: "/images/produits/chocolatine.jpg" - } -]; - -const ProductsPage = () => { - return ( -
- {/* Hero Section */} -
- Boutique de produits -
-
-
-

- Nos Produits -

-

- Découvrez notre sélection de produits artisanaux, préparés chaque jour avec passion -

-
-
-
- - {/* Products Section */} -
- {/* Categories */} -
- {['Tous', 'Pains', 'Viennoiseries', 'Pâtisseries'].map((category) => ( - - ))} -
- - {/* Products Grid */} -
- {products.map((product) => ( -
- {/* Image Container */} -
-
- {product.name} -
- - {/* Content */} -
-
-

- {product.name} -

- - {product.price} - -
-

- {product.description} -

-
- - {product.category} - - -
-
-
- ))} -
-
-
- ); }; -export default ProductsPage; \ No newline at end of file +export default function Page() { + return ; +} \ No newline at end of file diff --git a/src/components/LoadingSpinner.tsx b/src/components/LoadingSpinner.tsx new file mode 100644 index 0000000..1e3d63c --- /dev/null +++ b/src/components/LoadingSpinner.tsx @@ -0,0 +1,5 @@ +const LoadingSpinner = () => ( +
+); + +export default LoadingSpinner; \ No newline at end of file diff --git a/src/components/ProductModal.tsx b/src/components/ProductModal.tsx new file mode 100644 index 0000000..71f8dd2 --- /dev/null +++ b/src/components/ProductModal.tsx @@ -0,0 +1,71 @@ +import { motion, AnimatePresence } from 'framer-motion'; +import Image from 'next/image'; +import type { Product } from '@/types/product'; + +interface ProductModalProps { + product: Product | null; + onClose: () => void; +} + +const ProductModal = ({ product, onClose }: ProductModalProps) => { + if (!product) return null; + + return ( + + +
+ e.stopPropagation()} + > +
+ {product.name} +
+ +
+

{product.name}

+ {product.price} +
+ +

{product.description}

+ +
+
+

Ingrédients

+

{product.ingredients}

+
+ +
+

Conservation

+

{product.storage}

+
+
+ + +
+
+
+
+ ); +}; + +export default ProductModal; \ No newline at end of file diff --git a/src/components/ProductsPage.tsx b/src/components/ProductsPage.tsx new file mode 100644 index 0000000..b6121cb --- /dev/null +++ b/src/components/ProductsPage.tsx @@ -0,0 +1,143 @@ +'use client'; +import { useState } from 'react'; +import Image from 'next/image'; +import { motion, AnimatePresence } from 'framer-motion'; +import ProductModal from './ProductModal'; +import { products, categories } from '@/data/products'; +import type { Product } from '@/types/product'; +import LoadingSpinner from './LoadingSpinner'; + +const ProductsPage = () => { + const [selectedCategory, setSelectedCategory] = useState('Tous'); + const [selectedProduct, setSelectedProduct] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const filteredProducts = selectedCategory === 'Tous' + ? products + : products.filter(product => product.category === selectedCategory); + + const handleCategoryChange = (category: string) => { + setIsLoading(true); + setSelectedCategory(category); + // Simulate loading state + setTimeout(() => setIsLoading(false), 500); + }; + + return ( +
+ {/* Hero Section */} + +
+
+
+

+ Nos Produits +

+

+ Découvrez notre sélection de produits artisanaux, préparés chaque jour avec passion +

+
+
+ + + {/* Products Section */} +
+ {/* Categories */} +
+ {categories.map((category) => ( + handleCategoryChange(category)} + className={`px-6 py-2 rounded-full border-2 transition-colors ${ + selectedCategory === category + ? 'bg-bakery-600 text-white border-bakery-600' + : 'border-bakery-600 text-bakery-600 hover:bg-bakery-600 hover:text-white' + }`} + > + {category} + + ))} +
+ + {/* Products Grid */} + {isLoading ? ( +
+ +
+ ) : ( + + + {filteredProducts.map((product) => ( + + {/* Image Container */} +
setSelectedProduct(product)} + > +
+ {product.name} +
+ + {/* Content */} +
+
+

+ {product.name} +

+ + {product.price} + +
+

+ {product.description} +

+
+ + {product.category} + + +
+
+ + ))} + + + )} +
+ + {/* Product Modal */} + setSelectedProduct(null)} + /> +
+ ); +}; + +export default ProductsPage; \ No newline at end of file diff --git a/src/data/products.ts b/src/data/products.ts new file mode 100644 index 0000000..1c02788 --- /dev/null +++ b/src/data/products.ts @@ -0,0 +1,66 @@ +import { Product } from '@/types/product'; + +export const categories = ['Tous', 'Pains', 'Viennoiseries', 'Pâtisseries']; + +export const products: Product[] = [ + { + id: 1, + name: "Pagnot Traditionnel", + description: "Pain de campagne à la croûte dorée et croustillante, façonné à la main selon la tradition.", + price: "3.50€", + category: "Pains", + image: "/images/produits/pagnot.jpeg", + ingredients: "Farine de blé, eau, levain, sel marin", + storage: "À conserver dans un endroit frais et sec, consommer de préférence dans les 2-3 jours" + }, + { + id: 2, + name: "Baguette", + description: "Pain traditionnel français, croustillant à l'extérieur et moelleux à l'intérieur.", + price: "2.00€", + category: "Pains", + image: "/images/produits/baguette.jpeg", + ingredients: "Farine de blé, eau, levain, sel", + storage: "À conserver dans un endroit frais et sec, consommer de préférence dans les 2-3 jours" + }, + { + id: 3, + name: "Chocolatine", + description: "Viennoiserie feuilletée au beurre, garnie de chocolat.", + price: "1.50€", + category: "Viennoiseries", + image: "/images/produits/chocolatine.jpg", + ingredients: "Farine de blé, beurre, sucre, chocolat", + storage: "À conserver dans un endroit frais et sec, consommer de préférence dans les 2-3 jours" + }, + { + id: 4, + name: "Croissant", + description: "Viennoiserie feuilletée au beurre, idéale pour le petit-déjeuner.", + price: "1.50€", + category: "Viennoiseries", + image: "/images/produits/croissant.jpeg", + ingredients: "Farine de blé, beurre, sucre", + storage: "À conserver dans un endroit frais et sec, consommer de préférence dans les 2-3 jours" + }, + { + id: 5, + name: "Cupcake", + description: "Petit gâteau moelleux, idéal pour une pause sucrée.", + price: "2.00€", + category: "Pâtisseries", + image: "/images/produits/cupcake.jpg", + ingredients: "Farine de blé, beurre, sucre, oeufs", + storage: "À conserver dans un endroit frais et sec, consommer de préférence dans les 2-3 jours" + }, + { + id: 6, + name: "Gâteau Royal Chocolat", + description: "Gâteau au chocolat fondant, idéal pour les amateurs de chocolat.", + price: "4.00€", + category: "Pâtisseries", + image: "/images/produits/gateau-royal.jpg", + ingredients: "Farine de blé, beurre, sucre, chocolat, oeufs", + storage: "À conserver dans un endroit frais et sec, consommer de préférence dans les 2-3 jours" + }, +]; \ No newline at end of file diff --git a/src/types/product.ts b/src/types/product.ts new file mode 100644 index 0000000..8051142 --- /dev/null +++ b/src/types/product.ts @@ -0,0 +1,10 @@ +export interface Product { + id: number; + name: string; + description: string; + price: string; + category: string; + image: string; + ingredients: string; + storage: string; +} \ No newline at end of file