feat(spa): add wishlist functionality with localStorage and UI integration
This commit is contained in:
1 parent
ca31564ee7
commit
091eeef7d8
5 files changed
+155
-5
No files matched your search
@@ -17,6 +17,7 @@ import CheckoutPage from './pages/CheckoutPage'
|
||||
import CouponsPage from './pages/CouponsPage'
|
||||
import OrderSuccessPage from './pages/OrderSuccessPage'
|
||||
import TrackOrderPage from './pages/TrackOrderPage'
|
||||
import WishlistPage from './pages/WishlistPage'
|
||||
import CartDrawer from './components/CartDrawer'
|
||||
|
||||
function App() {
|
||||
@@ -42,6 +43,7 @@ function App() {
|
||||
<Route path="/checkout" element={<CheckoutPage />} />
|
||||
<Route path="/order/:id" element={<OrderSuccessPage />} />
|
||||
<Route path="/track" element={<TrackOrderPage />} />
|
||||
<Route path="/wishlist" element={<WishlistPage />} />
|
||||
<Route path="/search" element={<SearchPage />} />
|
||||
<Route path="/faq" element={<FaqPage />} />
|
||||
<Route path="/scent-guide" element={<ScentGuidePage />} />
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Link, NavLink, useNavigate } from 'react-router-dom'
|
||||
import { useState, useEffect } from 'react'
|
||||
import type { Category } from '../types'
|
||||
import { ShoppingBag, MapPin, Menu as MenuIcon, X } from 'lucide-react'
|
||||
import { ShoppingBag, MapPin, Menu as MenuIcon, X, Heart } from 'lucide-react'
|
||||
import { useCartStore } from '../store/cartStore'
|
||||
import { useWishlistStore } from '../store/wishlistStore'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import { logoutLiff, loginLiff } from '../lib/liff'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -21,6 +22,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
|
||||
const { toggleCart, getCartCount } = useCartStore()
|
||||
const cartCount = getCartCount()
|
||||
const wishlistCount = useWishlistStore(state => state.items.length)
|
||||
const { customer } = useAuthStore()
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
@@ -140,6 +142,19 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<Link
|
||||
to="/wishlist"
|
||||
className="relative p-2.5 rounded-full text-spa-stone hover:bg-spa-sage transition-all group"
|
||||
aria-label="Wishlist"
|
||||
>
|
||||
<Heart className="w-4 h-4 group-hover:text-rose-500 transition-colors" />
|
||||
{wishlistCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
|
||||
{wishlistCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={() => toggleCart(true)}
|
||||
className="relative p-2.5 rounded-full text-spa-stone hover:bg-spa-sage transition-all group"
|
||||
@@ -242,6 +257,20 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<Link
|
||||
to="/wishlist"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="relative p-1.5 rounded-full text-spa-stone hover:bg-spa-sage transition-all"
|
||||
aria-label="Wishlist"
|
||||
>
|
||||
<Heart className="w-4 h-4" />
|
||||
{wishlistCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
|
||||
{wishlistCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={() => { toggleCart(true); setMobileOpen(false) }}
|
||||
className="relative p-1.5 rounded-full text-spa-stone hover:bg-spa-sage transition-all"
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import type { Product } from '../types'
|
||||
import { getCategoryImage, formatPrice } from '../types'
|
||||
import { Heart } from 'lucide-react'
|
||||
import { useWishlistStore } from '../store/wishlistStore'
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product
|
||||
}
|
||||
|
||||
export default function ProductCard({ product }: ProductCardProps) {
|
||||
const { addItem, removeItem, isInWishlist } = useWishlistStore()
|
||||
const isWished = isInWishlist(product.id)
|
||||
|
||||
const handleWishlistClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault() // prevent navigating to product detail
|
||||
if (isWished) {
|
||||
removeItem(product.id)
|
||||
} else {
|
||||
addItem(product)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/products/${product.id}`}
|
||||
@@ -22,10 +36,15 @@ export default function ProductCard({ product }: ProductCardProps) {
|
||||
<div className="absolute top-3 left-3 bg-white/90 backdrop-blur-sm px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-widest text-spa-forest shadow-sm">
|
||||
{product.category.name}
|
||||
</div>
|
||||
{/* Available badge */}
|
||||
<div className="absolute top-3 right-3 bg-spa-forest/90 backdrop-blur-sm px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-widest text-white shadow-sm">
|
||||
Available
|
||||
</div>
|
||||
{/* Wishlist Button */}
|
||||
<button
|
||||
onClick={handleWishlistClick}
|
||||
className="absolute top-3 right-3 p-2 bg-white/90 backdrop-blur-sm rounded-full shadow-sm hover:scale-110 transition-transform z-10"
|
||||
>
|
||||
<Heart
|
||||
className={`w-4 h-4 transition-colors ${isWished ? 'fill-rose-500 text-rose-500' : 'text-spa-stone/60 hover:text-rose-500'}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 flex flex-col flex-1">
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import Navbar from '../components/Navbar'
|
||||
import Footer from '../components/Footer'
|
||||
import ProductCard from '../components/ProductCard'
|
||||
import { API_BASE, type Category } from '../types'
|
||||
import { useWishlistStore } from '../store/wishlistStore'
|
||||
import { Heart } from 'lucide-react'
|
||||
|
||||
export default function WishlistPage() {
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
const { items } = useWishlistStore()
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/categories`)
|
||||
.then(res => res.json())
|
||||
.then(setCategories)
|
||||
.catch(console.error)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col bg-pop-bg">
|
||||
<Navbar categories={categories} />
|
||||
|
||||
<main className="flex-1 max-w-7xl w-full mx-auto px-6 pt-32 pb-24 space-y-12 animate-fade-in">
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-4">
|
||||
<div className="w-16 h-16 bg-rose-50 rounded-full flex items-center justify-center mx-auto border border-rose-100">
|
||||
<Heart className="w-8 h-8 text-rose-500 fill-rose-500" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight text-pop-black">Your Wishlist</h1>
|
||||
<p className="text-spa-stone max-w-lg mx-auto">
|
||||
Items you've loved. Keep them here until you're ready to make them yours.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="text-center py-24 bg-white shadow-sm border border-spa-stone/10 rounded-3xl max-w-2xl mx-auto space-y-6">
|
||||
<div className="w-20 h-20 bg-spa-light rounded-full flex items-center justify-center mx-auto">
|
||||
<Heart className="w-10 h-10 text-spa-stone/30" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-pop-black">Your wishlist is empty</h2>
|
||||
<p className="text-spa-stone">Explore our products and tap the heart icon to save your favorites.</p>
|
||||
<div className="pt-4">
|
||||
<Link
|
||||
to="/shop"
|
||||
className="inline-block px-8 py-3.5 bg-spa-forest text-white font-bold rounded-lg tracking-widest uppercase hover:bg-spa-stone transition-all duration-300 shadow-sm"
|
||||
>
|
||||
Browse Shop
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-8">
|
||||
{items.map(product => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</main>
|
||||
|
||||
<Footer categories={categories} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { Product } from '../types'
|
||||
|
||||
interface WishlistState {
|
||||
items: Product[]
|
||||
addItem: (product: Product) => void
|
||||
removeItem: (productId: number) => void
|
||||
isInWishlist: (productId: number) => boolean
|
||||
}
|
||||
|
||||
export const useWishlistStore = create<WishlistState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
items: [],
|
||||
addItem: (product) => {
|
||||
const { items } = get()
|
||||
if (!items.find(item => item.id === product.id)) {
|
||||
set({ items: [...items, product] })
|
||||
}
|
||||
},
|
||||
removeItem: (productId) => {
|
||||
set({ items: get().items.filter(item => item.id !== productId) })
|
||||
},
|
||||
isInWishlist: (productId) => {
|
||||
return get().items.some(item => item.id === productId)
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'homaroma-wishlist',
|
||||
}
|
||||
)
|
||||
)
|
||||
Reference in new issue
Block a user