Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function StorefrontLayout() {
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/shop" element={<ShopPage />} />
<Route path="/product/:id" element={<ProductPage />} />
<Route path="/product/:slug" element={<ProductPage />} />
<Route path="/cart" element={<CartPage />} />
<Route path="/checkout" element={<CheckoutPage />} />
<Route path="/login" element={<LoginPage />} />
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/components/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from "react";
import { Link } from "react-router";
import { Heart } from "lucide-react";
import { Product } from "../data/products";
import { getProductPath, Product } from "../data/products";
import { cartStore } from "../store/cartStore";

interface ProductCardProps {
Expand All @@ -21,7 +21,7 @@ export function ProductCard({ product, className = "" }: ProductCardProps) {

return (
<Link
to={`/product/${product.id}`}
to={getProductPath(product)}
className={`group block ${className}`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
Expand Down
351 changes: 151 additions & 200 deletions apps/web/src/app/data/products.ts

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions apps/web/src/app/pages/CartPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Link } from "react-router";
import { Minus, Plus, X, ArrowRight, ShoppingBag } from "lucide-react";
import { cartStore, CartItem } from "../store/cartStore";
import { ProductCard } from "../components/ProductCard";
import { products } from "../data/products";
import { getProductPath, products } from "../data/products";

export function CartPage() {
const [items, setItems] = useState<CartItem[]>(cartStore.getCart());
Expand Down Expand Up @@ -50,7 +50,7 @@ export function CartPage() {
<div className="border-t border-[#E5E5E5]">
{items.map((item, idx) => (
<div key={`${item.product.id}-${item.size}-${idx}`} className="flex gap-5 py-6 border-b border-[#E5E5E5]">
<Link to={`/product/${item.product.id}`} className="flex-shrink-0">
<Link to={getProductPath(item.product)} className="flex-shrink-0">
<div className="w-24 h-32 overflow-hidden bg-[#F8F8F6]">
<img src={item.product.image} alt={item.product.name} className="w-full h-full object-cover" />
</div>
Expand All @@ -61,7 +61,7 @@ export function CartPage() {
<p className="text-[#111111] mb-0.5" style={{ fontFamily: "'Jost', sans-serif", fontSize: "10px", letterSpacing: "0.12em", fontWeight: 600 }}>
{item.product.brand}
</p>
<Link to={`/product/${item.product.id}`}>
<Link to={getProductPath(item.product)}>
<p className="text-[#111111] hover:text-[#111111] transition-colors" style={{ fontFamily: "'Jost', sans-serif", fontSize: "14px" }}>
{item.product.name}
</p>
Expand Down
17 changes: 10 additions & 7 deletions apps/web/src/app/pages/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState } from "react";
import { Link } from "react-router";
import { Heart } from "lucide-react";
import { findProductByRouteParam, getProductPath } from "../data/products";

/* ─── Image registry — 22 muted/neutral/monochrome images ────────────────
6 category images
Expand Down Expand Up @@ -57,16 +58,16 @@ const CATEGORIES = [

const NEW_ARRIVALS = [
{ brand: "MAISON VOSS", name: "Oversized Black Coat", price: 1490, img: IMG.new_1, hover: IMG.edit_1, id: "1" },
{ brand: "LUMIÈRE", name: "White Tailored Blazer", price: 890, img: IMG.new_2, hover: IMG.campaign_2, id: "2" },
{ brand: "CROFT & VALE", name: "Grey Leather Bag", price: 1250, img: IMG.new_3, hover: IMG.new_4, id: "3" },
{ brand: "NORDSEN", name: "Black Leather Shoes", price: 685, img: IMG.new_4, hover: IMG.new_3, id: "6" },
{ brand: "LUMIÈRE", name: "White Tailored Blazer", price: 890, img: IMG.new_2, hover: IMG.campaign_2, id: "20" },
{ brand: "CROFT & VALE", name: "Grey Leather Bag", price: 1250, img: IMG.new_3, hover: IMG.new_4, id: "7" },
{ brand: "NORDSEN", name: "Black Leather Shoes", price: 685, img: IMG.new_4, hover: IMG.new_3, id: "12" },
];

const BEST_SELLERS = [
{ brand: "ELARA", name: "Black Sunglasses", price: 395, img: IMG.best_1, hover: IMG.edit_2, id: "7" },
{ brand: "CROFT & VALE", name: "Grey Handbag", price: 1850, img: IMG.best_2, hover: IMG.new_3, id: "3" },
{ brand: "ELARA", name: "Black Sunglasses", price: 395, img: IMG.best_1, hover: IMG.edit_2, id: "8" },
{ brand: "CROFT & VALE", name: "Grey Handbag", price: 1850, img: IMG.best_2, hover: IMG.new_3, id: "7" },
{ brand: "MAISON VOSS", name: "Black Wool Coat", price: 1290, img: IMG.best_3, hover: IMG.new_1, id: "1" },
{ brand: "VELORIA", name: "White Fragrance", price: 280, img: IMG.best_4, hover: IMG.cat_beauty, id: "4" },
{ brand: "VELORIA", name: "White Fragrance", price: 280, img: IMG.best_4, hover: IMG.cat_beauty, id: "9" },
];

const EDITORIAL = [
Expand Down Expand Up @@ -135,10 +136,12 @@ interface CardItem {
function ProductTile({ item }: { item: CardItem }) {
const [hovered, setHovered] = useState(false);
const [loved, setLoved] = useState(false);
const product = findProductByRouteParam(item.id);
const productPath = product ? getProductPath(product) : `/product/${item.id}`;

return (
<Link
to={`/product/${item.id}`}
to={productPath}
className="group block"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
Expand Down
41 changes: 37 additions & 4 deletions apps/web/src/app/pages/ProductPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useParams, Link } from "react-router";
import { Heart, Minus, Plus, ShoppingBag, Share2, ArrowRight, Star, ChevronDown } from "lucide-react";
import { products } from "../data/products";
import { findProductByRouteParam, products } from "../data/products";
import { cartStore } from "../store/cartStore";
import { ProductCard } from "../components/ProductCard";

Expand All @@ -12,8 +12,9 @@ const reviews = [
];

export function ProductPage() {
const { id } = useParams();
const product = products.find(p => p.id === id) || products[0];
const { slug } = useParams();
const resolvedProduct = findProductByRouteParam(slug);
const product = resolvedProduct || products[0];
const [activeImage, setActiveImage] = useState(0);
const [selectedSize, setSelectedSize] = useState<string | undefined>(product.sizes?.[0]);
const [selectedColor, setSelectedColor] = useState(product.colors?.[0]?.name);
Expand All @@ -25,6 +26,16 @@ export function ProductPage() {
const images = [product.image, product.hoverImage];
const related = products.filter(p => p.category === product.category && p.id !== product.id).slice(0, 4);

useEffect(() => {
setActiveImage(0);
setSelectedSize(product.sizes?.[0]);
setSelectedColor(product.colors?.[0]?.name);
setQuantity(1);
setWishlisted(cartStore.isWishlisted(product.id));
setAddedToCart(false);
setExpandedSection("description");
}, [product]);

const handleAddToCart = () => {
cartStore.addToCart(product, quantity, selectedSize, selectedColor);
setAddedToCart(true);
Expand All @@ -46,6 +57,28 @@ export function ProductPage() {
{ id: "care", label: "Care Instructions", content: "To preserve the integrity of this piece, follow the care label instructions carefully. When in doubt, professional cleaning is recommended for all luxury garments." },
];

if (!resolvedProduct) {
return (
<div className="bg-white min-h-[60vh]">
<div className="max-w-[1440px] mx-auto px-6 lg:px-12 py-24 text-center">
<p className="text-[#111111] mb-3" style={{ fontFamily: "'Playfair Display', serif", fontSize: "clamp(28px, 4vw, 44px)", fontWeight: 400 }}>
Product not found
</p>
<p className="text-[#666666] mb-8" style={{ fontFamily: "'Jost', sans-serif", fontSize: "14px" }}>
This product link may be outdated.
</p>
<Link
to="/shop"
className="inline-flex items-center gap-3 bg-[#111111] text-white px-8 py-3.5"
style={{ fontFamily: "'Jost', sans-serif", fontSize: "12px", letterSpacing: "0.12em", fontWeight: 600 }}
>
RETURN TO SHOP <ArrowRight size={14} />
</Link>
</div>
</div>
);
}

return (
<div className="bg-white">
{/* Breadcrumb */}
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/app/pages/ShopPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useMemo, useEffect } from "react";
import { useSearchParams, Link } from "react-router";
import { X, SlidersHorizontal, ChevronDown } from "lucide-react";
import { findProductByRouteParam, getProductPath } from "../data/products";

/* ─── Products ──────────────────────────────────────────────────────────── */
type Gender = "Men" | "Women" | "Unisex";
Expand Down Expand Up @@ -409,9 +410,11 @@ function ProductGrid({ items, onClear, cols = "desktop" }: { items: Item[]; onCl
/* ── Product tile ─────────────────────────────────────────────────────────── */
function ProductTile({ item }: { item: Item }) {
const [hovered, setHovered] = useState(false);
const product = findProductByRouteParam(item.id);
const productPath = product ? getProductPath(product) : `/product/${item.id}`;
return (
<Link
to={`/product/${item.id}`}
to={productPath}
className="block"
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
Expand Down
Loading