9 Commits

Author SHA1 Message Date
kudinDmitriyUp
1ca116da42 Bob AI: add a cart functionality with an add to cart button for each product ... 2026-05-09 14:22:31 +00:00
497cfbf854 Merge version_7_1778335142760 into main
Merge version_7_1778335142760 into main
2026-05-09 14:00:29 +00:00
kudinDmitriyUp
ee03fea5aa fix: remove extra margin from hero section 2026-05-09 13:59:48 +00:00
6d1cb751d0 Merge version_6_1778335009357 into main
Merge version_6_1778335009357 into main
2026-05-09 13:58:33 +00:00
kudinDmitriyUp
1384b7d561 feat: replace AboutText with AboutFeaturesSplit and add winery story 2026-05-09 13:57:57 +00:00
368345f4c4 Merge version_4_1778334771693 into main
Merge version_4_1778334771693 into main
2026-05-09 13:54:11 +00:00
kudinDmitriyUp
50b0860872 feat: Update hero section with testimonial cards and thicker title 2026-05-09 13:53:38 +00:00
a5deb35a54 Merge version_3_1778332459679 into main
Merge version_3_1778332459679 into main
2026-05-09 13:17:05 +00:00
2dca9e5f5b Merge version_3_1778332459679 into main
Merge version_3_1778332459679 into main
2026-05-09 13:16:56 +00:00
7 changed files with 249 additions and 61 deletions

View File

@@ -1,13 +1,20 @@
import { Routes, Route } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout'; import Layout from './components/Layout';
import HomePage from './pages/HomePage'; import HomePage from './pages/HomePage';
import CartPage from './pages/CartPage';
import { CartProvider } from './context/CartContext';
export default function App() { export default function App() {
return ( return (
<CartProvider>
<Router>
<Routes> <Routes>
<Route element={<Layout />}> <Route element={<Layout />}>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/cart" element={<CartPage />} />
</Route> </Route>
</Routes> </Routes>
</Router>
</CartProvider>
); );
} }

View File

@@ -34,24 +34,11 @@ const HeroBillboard = ({
videoSrc, videoSrc,
}: HeroBillboardProps) => { }: HeroBillboardProps) => {
return ( return (
<section aria-label="Hero section" className="relative pt-25 pb-20 md:py-30 mb-20"> <section aria-label="Hero section" className="relative pt-25 pb-20 md:py-30">
<HeroBackgroundSlot /> <HeroBackgroundSlot />
<div className="flex flex-col gap-10 w-content-width mx-auto"> <div className="flex flex-col gap-10 w-content-width mx-auto">
<div className="flex flex-col items-center gap-2 text-center"> <div className="flex flex-col items-center gap-2 text-center">
{testimonials && testimonials.length > 0 ? ( {tag ? (
<div className="flex flex-col items-center gap-4">
<AvatarGroup avatars={testimonials.map(t => ({ src: t.imageSrc }))} label={avatarsLabel} />
<div className="flex flex-wrap justify-center gap-8 mt-4">
{testimonials.map((testimonial, index) => (
<div key={index} className="max-w-xs text-center">
<p className="italic">"{testimonial.testimonial}"</p>
<p className="mt-2 font-semibold">{testimonial.name}</p>
<p className="text-sm text-gray-600">{testimonial.role}</p>
</div>
))}
</div>
</div>
) : tag ? (
<span className="px-3 py-1 mb-1 text-sm card rounded">{tag}</span> <span className="px-3 py-1 mb-1 text-sm card rounded">{tag}</span>
) : null} ) : null}
@@ -60,7 +47,7 @@ const HeroBillboard = ({
variant="slide-up" variant="slide-up"
gradientText={true} gradientText={true}
tag="h1" tag="h1"
className="text-6xl font-bold text-balance" className="text-6xl font-extrabold text-balance"
/> />
<TextAnimation <TextAnimation
@@ -77,8 +64,19 @@ const HeroBillboard = ({
</div> </div>
</div> </div>
<ScrollReveal variant="fade" delay={0.2} className="w-full p-3 xl:p-4 2xl:p-5 card rounded overflow-hidden"> <ScrollReveal variant="fade" delay={0.2} className="relative w-full p-3 xl:p-4 2xl:p-5 card rounded overflow-hidden">
<ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} className="aspect-4/5 md:aspect-video" /> <ImageOrVideo imageSrc={imageSrc} videoSrc={videoSrc} className="aspect-4/5 md:aspect-video" />
{testimonials && testimonials.length > 0 && (
<div className="absolute inset-0 grid grid-cols-1 md:grid-cols-3 gap-4 p-4 md:p-8 pointer-events-none">
{testimonials.map((testimonial, index) => (
<div key={index} className="card p-4 bg-white/80 backdrop-blur-sm rounded-lg shadow-lg pointer-events-auto self-start">
<p className="italic">"{testimonial.testimonial}"</p>
<p className="mt-2 font-semibold">{testimonial.name}</p>
<p className="text-sm text-gray-600">{testimonial.role}</p>
</div>
))}
</div>
)}
</ScrollReveal> </ScrollReveal>
</div> </div>
</section> </section>

View File

@@ -6,6 +6,18 @@ import ImageOrVideo from "@/components/ui/ImageOrVideo";
import GridOrCarousel from "@/components/ui/GridOrCarousel"; import GridOrCarousel from "@/components/ui/GridOrCarousel";
import ScrollReveal from "@/components/ui/ScrollReveal"; import ScrollReveal from "@/components/ui/ScrollReveal";
import useProducts from "@/hooks/useProducts"; import useProducts from "@/hooks/useProducts";
import { useCart } from "@/context/CartContext";
type Product = {
id: number;
brand: string;
name:string;
price: string;
rating: number;
reviewCount: string;
imageSrc: string;
onClick?: () => void;
};
type ProductRatingCardsProps = { type ProductRatingCardsProps = {
tag: string; tag: string;
@@ -13,15 +25,7 @@ type ProductRatingCardsProps = {
description: string; description: string;
primaryButton?: { text: string; href: string }; primaryButton?: { text: string; href: string };
secondaryButton?: { text: string; href: string }; secondaryButton?: { text: string; href: string };
products?: { products?: Product[];
brand: string;
name: string;
price: string;
rating: number;
reviewCount: string;
imageSrc: string;
onClick?: () => void;
}[];
}; };
const ProductRatingCards = ({ const ProductRatingCards = ({
@@ -33,9 +37,12 @@ const ProductRatingCards = ({
products: productsProp, products: productsProp,
}: ProductRatingCardsProps) => { }: ProductRatingCardsProps) => {
const { products: fetchedProducts, isLoading } = useProducts(); const { products: fetchedProducts, isLoading } = useProducts();
const { addToCart } = useCart();
const isFromApi = fetchedProducts.length > 0; const isFromApi = fetchedProducts.length > 0;
const products = isFromApi const products = isFromApi
? fetchedProducts.map((p) => ({ ? fetchedProducts.map((p, index) => ({
id: p.id || index,
brand: p.brand || "", brand: p.brand || "",
name: p.name, name: p.name,
price: p.price, price: p.price,
@@ -44,7 +51,7 @@ const ProductRatingCards = ({
imageSrc: p.imageSrc, imageSrc: p.imageSrc,
onClick: p.onProductClick, onClick: p.onProductClick,
})) }))
: productsProp; : productsProp?.map((p, index) => ({ ...p, id: p.id || index }));
if (isLoading && !productsProp) { if (isLoading && !productsProp) {
return ( return (
@@ -60,6 +67,11 @@ const ProductRatingCards = ({
return null; return null;
} }
const handleAddToCart = (product: Product) => {
const priceNumber = parseFloat(product.price.replace('$', ''));
addToCart({ ...product, price: priceNumber, quantity: 1, image: product.imageSrc });
};
return ( return (
<section aria-label="Products section" className="py-20"> <section aria-label="Products section" className="py-20">
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
@@ -93,12 +105,11 @@ const ProductRatingCards = ({
<ScrollReveal variant="fade-blur"> <ScrollReveal variant="fade-blur">
<GridOrCarousel> <GridOrCarousel>
{products.map((product) => ( {products.map((product) => (
<button <div
key={product.name} key={product.name}
onClick={product.onClick} className="group h-full flex flex-col gap-3 xl:gap-4 2xl:gap-5 p-3 xl:p-4 2xl:p-5 text-left card rounded"
className="group h-full flex flex-col gap-3 xl:gap-4 2xl:gap-5 p-3 xl:p-4 2xl:p-5 text-left card rounded cursor-pointer"
> >
<div className="aspect-square rounded overflow-hidden"> <div className="aspect-square rounded overflow-hidden" onClick={product.onClick}>
<ImageOrVideo imageSrc={product.imageSrc} /> <ImageOrVideo imageSrc={product.imageSrc} />
</div> </div>
@@ -127,7 +138,10 @@ const ProductRatingCards = ({
<p className="text-2xl font-medium">{product.price}</p> <p className="text-2xl font-medium">{product.price}</p>
</div> </div>
</button> <Button onClick={() => handleAddToCart(product)} variant="primary" className="w-full mt-auto">
Add to Cart
</Button>
</div>
))} ))}
</GridOrCarousel> </GridOrCarousel>
</ScrollReveal> </ScrollReveal>

View File

@@ -1,8 +1,13 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "motion/react"; import { motion, AnimatePresence } from "motion/react";
import { Plus, ArrowUpRight } from "lucide-react"; import { Plus, ArrowUpRight, ShoppingCart } from "lucide-react";
import { cls } from "@/lib/utils"; import { cls } from "@/lib/utils";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
import { useCart } from "@/context/CartContext";
import { Link } from "react-router-dom";
interface NavbarFloatingProps { interface NavbarFloatingProps {
logo: string; logo: string;
@@ -21,6 +26,8 @@ const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string, on
const NavbarFloating = ({ logo, navItems, ctaButton }: NavbarFloatingProps) => { const NavbarFloating = ({ logo, navItems, ctaButton }: NavbarFloatingProps) => {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const { cartItems } = useCart();
const cartItemCount = cartItems.reduce((acc, item) => acc + item.quantity, 0);
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
@@ -48,8 +55,17 @@ const NavbarFloating = ({ logo, navItems, ctaButton }: NavbarFloatingProps) => {
<nav className="fixed z-1000 top-5 left-1/2 -translate-x-1/2 w-content-width"> <nav className="fixed z-1000 top-5 left-1/2 -translate-x-1/2 w-content-width">
<div className="mx-auto w-full md:w-1/2 overflow-hidden rounded backdrop-blur-sm card"> <div className="mx-auto w-full md:w-1/2 overflow-hidden rounded backdrop-blur-sm card">
<div className="relative z-10 flex items-center justify-between gap-3 xl:gap-4 2xl:gap-5 p-3 xl:p-4 2xl:p-5"> <div className="relative z-10 flex items-center justify-between gap-3 xl:gap-4 2xl:gap-5 p-3 xl:p-4 2xl:p-5">
<a href="/" className="text-xl font-medium text-foreground">{logo}</a> <a href="/" className="text-xl font-medium text-foreground">{logo}</Link>
<div className="flex items-center gap-2">
<Link to="/cart" className="relative p-2">
<ShoppingCart className="w-6 h-6 text-foreground" />
{cartItemCount > 0 && (
<span className="absolute top-0 right-0 block h-4 w-4 rounded-full bg-red-500 text-white text-xs text-center">
{cartItemCount}
</span>
)}
</Link>
<button <button
className="flex items-center justify-center shrink-0 size-9 rounded cursor-pointer primary-button" className="flex items-center justify-center shrink-0 size-9 rounded cursor-pointer primary-button"
onClick={() => setMenuOpen(!menuOpen)} onClick={() => setMenuOpen(!menuOpen)}
@@ -65,6 +81,7 @@ const NavbarFloating = ({ logo, navItems, ctaButton }: NavbarFloatingProps) => {
/> />
</button> </button>
</div> </div>
</div>
<AnimatePresence> <AnimatePresence>
{menuOpen && ( {menuOpen && (
@@ -91,7 +108,7 @@ const NavbarFloating = ({ logo, navItems, ctaButton }: NavbarFloatingProps) => {
className="h-(--text-xl) md:h-(--text-2xl) w-auto text-foreground group-hover:rotate-45 group-hover:mr-3 transition-all duration-300" className="h-(--text-xl) md:h-(--text-2xl) w-auto text-foreground group-hover:rotate-45 group-hover:mr-3 transition-all duration-300"
strokeWidth={2} strokeWidth={2}
/> />
</a> </Link>
{index < navItems.length - 1 && ( {index < navItems.length - 1 && (
<div className="h-px bg-accent/50" /> <div className="h-px bg-accent/50" />
)} )}

View File

@@ -0,0 +1,70 @@
import React, { createContext, useState, useContext, ReactNode } from 'react';
interface Product {
id: number;
name: string;
price: number;
image: string;
quantity: number;
}
interface CartContextType {
cartItems: Product[];
addToCart: (product: Product) => void;
removeFromCart: (productId: number) => void;
updateQuantity: (productId: number, quantity: number) => void;
clearCart: () => void;
}
const CartContext = createContext<CartContextType | undefined>(undefined);
export const CartProvider = ({ children }: { children: ReactNode }) => {
const [cartItems, setCartItems] = useState<Product[]>([]);
const addToCart = (product: Product) => {
setCartItems(prevItems => {
const itemInCart = prevItems.find(item => item.id === product.id);
if (itemInCart) {
return prevItems.map(item =>
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
);
}
return [...prevItems, { ...product, quantity: 1 }];
});
};
const removeFromCart = (productId: number) => {
setCartItems(prevItems => prevItems.filter(item => item.id !== productId));
};
const updateQuantity = (productId: number, quantity: number) => {
if (quantity <= 0) {
removeFromCart(productId);
} else {
setCartItems(prevItems =>
prevItems.map(item =>
item.id === productId ? { ...item, quantity } : item
)
);
}
};
const clearCart = () => {
setCartItems([]);
};
return (
<CartContext.Provider value={{ cartItems, addToCart, removeFromCart, updateQuantity, clearCart }}>
{children}
</CartContext.Provider>
);
};
export const useCart = () => {
const context = useContext(CartContext);
if (context === undefined) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
};

56
src/pages/CartPage.tsx Normal file
View File

@@ -0,0 +1,56 @@
import React from 'react';
import { useCart } from '../context/CartContext';
import Button from '../components/ui/Button';
const CartPage = () => {
const { cartItems, removeFromCart, updateQuantity, clearCart } = useCart();
const total = cartItems.reduce((acc, item) => acc + item.price * item.quantity, 0);
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-4">Your Cart</h1>
{cartItems.length === 0 ? (
<p>Your cart is empty.</p>
) : (
<div>
{cartItems.map(item => (
<div key={item.id} className="flex items-center justify-between border-b py-4">
<div className="flex items-center">
<img src={item.image} alt={item.name} className="w-20 h-20 object-cover mr-4" />
<div>
<h2 className="font-bold">{item.name}</h2>
<p>${item.price.toFixed(2)}</p>
</div>
</div>
<div className="flex items-center">
<input
type="number"
value={item.quantity}
onChange={(e) => updateQuantity(item.id, parseInt(e.target.value))}
className="w-16 text-center border rounded mr-4"
min="1"
/>
<Button onClick={() => removeFromCart(item.id)} variant="destructive">
Remove
</Button>
</div>
</div>
))}
<div className="mt-4 text-right">
<h2 className="text-2xl font-bold">Total: ${total.toFixed(2)}</h2>
<Button onClick={() => alert('Checkout functionality not implemented yet.')} className="mt-4">
Checkout
</Button>
<Button onClick={clearCart} variant="secondary" className="mt-4 ml-2">
Clear Cart
</Button>
</div>
</div>
)}
</div>
);
};
export default CartPage;

View File

@@ -1,4 +1,4 @@
import AboutText from '@/components/sections/about/AboutText'; import AboutFeaturesSplit from '@/components/sections/about/AboutFeaturesSplit';
import ContactSplitForm from '@/components/sections/contact/ContactSplitForm'; import ContactSplitForm from '@/components/sections/contact/ContactSplitForm';
import FeaturesArrowCards from '@/components/sections/features/FeaturesArrowCards'; import FeaturesArrowCards from '@/components/sections/features/FeaturesArrowCards';
import HeroBillboard from '@/components/sections/hero/HeroBillboard'; import HeroBillboard from '@/components/sections/hero/HeroBillboard';
@@ -55,16 +55,36 @@ export default function HomePage() {
<div id="about" data-section="about"> <div id="about" data-section="about">
<SectionErrorBoundary name="about"> <SectionErrorBoundary name="about">
<AboutText <AboutFeaturesSplit
title="Our Heritage of Winemaking Excellence" tag="Our Story"
title="From Our Family to Yours"
description="Founded in 1924, Vine & Vintages is a family-owned winery dedicated to crafting exceptional wines. Our story is one of passion, perseverance, and a deep connection to the land. We believe that every bottle tells a story, and we invite you to become a part of ours."
primaryButton={{ primaryButton={{
text: "Read Our Story", text: "Meet the Family",
href: "#about", href: "#team",
}} }}
secondaryButton={{ secondaryButton={{
text: "View Vineyards", text: "Explore the Vineyard",
href: "#features", href: "#features",
}} }}
imageSrc="http://img.b2bpic.net/free-photo/group-people-enjoying-wine-tasting-event-vineyard-generated-by-ai_188544-45590.jpg"
items={[
{
icon: "Grape",
title: "Generations of Expertise",
description: "With over a century of winemaking experience, our family has perfected the art of cultivating grapes and producing world-class wines.",
},
{
icon: "Leaf",
title: "Sustainable Practices",
description: "We are committed to sustainable farming and winemaking practices to ensure the health of our land for generations to come.",
},
{
icon: "Award",
title: "Award-Winning Wines",
description: "Our dedication to quality has been recognized with numerous awards, a testament to our unwavering commitment to excellence.",
},
]}
/> />
</SectionErrorBoundary> </SectionErrorBoundary>
</div> </div>
@@ -117,6 +137,7 @@ export default function HomePage() {
description="Handcrafted with passion and precision, our wines embody the spirit of our vineyard. Explore our diverse range and find your next favorite vintage." description="Handcrafted with passion and precision, our wines embody the spirit of our vineyard. Explore our diverse range and find your next favorite vintage."
products={[ products={[
{ {
id: 1,
brand: "Vine & Vintages", brand: "Vine & Vintages",
name: "Estate Cabernet Sauvignon", name: "Estate Cabernet Sauvignon",
price: "$55.00", price: "$55.00",
@@ -125,6 +146,7 @@ export default function HomePage() {
imageSrc: "http://img.b2bpic.net/free-photo/liquor-round-bottle_176474-6072.jpg", imageSrc: "http://img.b2bpic.net/free-photo/liquor-round-bottle_176474-6072.jpg",
}, },
{ {
id: 2,
brand: "Vine & Vintages", brand: "Vine & Vintages",
name: "Reserve Chardonnay", name: "Reserve Chardonnay",
price: "$48.00", price: "$48.00",
@@ -133,6 +155,7 @@ export default function HomePage() {
imageSrc: "http://img.b2bpic.net/free-photo/set-wine-bottle-with-glass-carafe_23-2148261706.jpg", imageSrc: "http://img.b2bpic.net/free-photo/set-wine-bottle-with-glass-carafe_23-2148261706.jpg",
}, },
{ {
id: 3,
brand: "Vine & Vintages", brand: "Vine & Vintages",
name: "Summer Rosé", name: "Summer Rosé",
price: "$32.00", price: "$32.00",
@@ -141,6 +164,7 @@ export default function HomePage() {
imageSrc: "http://img.b2bpic.net/free-photo/still-life-stacked-aesthetic-objects_23-2150185382.jpg", imageSrc: "http://img.b2bpic.net/free-photo/still-life-stacked-aesthetic-objects_23-2150185382.jpg",
}, },
{ {
id: 4,
brand: "Vine & Vintages", brand: "Vine & Vintages",
name: "Sparkling Brut", name: "Sparkling Brut",
price: "$60.00", price: "$60.00",
@@ -149,6 +173,7 @@ export default function HomePage() {
imageSrc: "http://img.b2bpic.net/free-photo/front-view-pair-champagne-glasses-wood-board-champagne-bottle_140725-99664.jpg", imageSrc: "http://img.b2bpic.net/free-photo/front-view-pair-champagne-glasses-wood-board-champagne-bottle_140725-99664.jpg",
}, },
{ {
id: 5,
brand: "Vine & Vintages", brand: "Vine & Vintages",
name: "Late Harvest Dessert Wine", name: "Late Harvest Dessert Wine",
price: "$75.00", price: "$75.00",
@@ -157,6 +182,7 @@ export default function HomePage() {
imageSrc: "http://img.b2bpic.net/free-photo/rollcake-with-berries-drink-platter_114579-16491.jpg", imageSrc: "http://img.b2bpic.net/free-photo/rollcake-with-berries-drink-platter_114579-16491.jpg",
}, },
{ {
id: 6,
brand: "Vine & Vintages", brand: "Vine & Vintages",
name: "Grand Cru Pinot Noir", name: "Grand Cru Pinot Noir",
price: "$90.00", price: "$90.00",