import { useState, useRef } from 'react'; import Card from 'antd/es/card'; import Badge from 'antd/es/badge'; import Typography from 'antd/es/typography'; import Carousel from 'antd/es/carousel'; import Button from 'antd/es/button'; import message from 'antd/es/message'; import Tag from 'antd/es/tag'; import Modal from 'antd/es/modal'; import Tooltip from 'antd/es/tooltip'; import { LeftOutlined, RightOutlined, HeartOutlined, HeartFilled } from '@ant-design/icons'; import type { CarouselRef } from 'antd/es/carousel'; import { Item } from '../api'; import MarkdownView from './MarkdownView'; import { addToCart } from '../cart/cartApi'; import { useCart } from '../cart/CartContext'; import { useCustomerAuth } from '../customer/CustomerAuthContext'; import AuthPromptModal from '../customer/AuthPromptModal'; import { useFavorites } from '../customer/FavoritesContext'; import { addFavorite, removeFavorite, setFavoriteAlerts } from '../customer/favoritesApi'; import { uploadUrl } from '../uploadUrl'; const { Text, Title } = Typography; interface Props { item: Item; onChanged: () => void; // Render exactly as the storefront does, but inert. The admin's inventory // preview uses this: the cart and favorites providers wrap the whole app, so // without it an admin looking at an item could add their own stock to their // own cart — and on a one-of-a-kind catalogue that reserves the item and // takes it off sale. // // Deliberately not `disabled` on the buttons. A disabled antd button renders // in a different colour with a different cursor and no hover, and the entire // point of the preview is to show what a customer will actually see. The // controls keep their normal appearance and their correct state for the // item's status; only the handlers stop. preview?: boolean; } export default function ItemCard({ item, onChanged, preview = false }: Readonly) { const carouselRef = useRef(null); const [authModalOpen, setAuthModalOpen] = useState(false); const [adding, setAdding] = useState(false); const [favoriting, setFavoriting] = useState(false); // Which action to run once the auth modal succeeds — the modal is shared by // Add to Cart and the heart. const [pendingAction, setPendingAction] = useState<'cart' | 'favorite'>('cart'); const { customer, loading: authLoading, refresh: refreshCustomer } = useCustomerAuth(); const { itemIds, refresh: refreshCart } = useCart(); const { itemIds: favoriteIds, refresh: refreshFavorites } = useFavorites(); const inMyCart = itemIds.has(item.id); const isFavorite = favoriteIds.has(item.id); async function doAddToCart() { setAdding(true); try { await addToCart(item.id); message.success('Added to cart'); refreshCart(); onChanged(); } catch (err) { message.error((err as Error).message); } finally { setAdding(false); } } function handleAddClick() { if (preview) return; // Until the session has resolved, `customer` is null for a signed-in // visitor too, and prompting them to sign in again would be wrong. if (authLoading) return; if (!customer) { setPendingAction('cart'); setAuthModalOpen(true); return; } void doAddToCart(); } // Asked only once a customer has actually favorited something, so the reason // for asking is concrete rather than an abstract marketing prompt. This is a // consent of its own — accepting it does not sign anyone up for marketing. function offerAlerts() { Modal.confirm({ title: 'Want to know if this sells?', content: 'Every piece is one of a kind, so a favorite can be bought by someone else at any time. ' + 'We can email you if that happens, so you are not left waiting on something that has gone. ' + 'This is only about items you favorite — it is separate from any marketing email.', okText: 'Yes, email me', cancelText: 'No thanks', onOk: async () => { try { await setFavoriteAlerts(true); refreshCustomer(); message.success('We will let you know'); } catch (err) { message.error((err as Error).message); } } }); } async function doToggleFavorite() { setFavoriting(true); const wasFavorite = isFavorite; try { if (wasFavorite) { await removeFavorite(item.id); } else { await addFavorite(item.id); } refreshFavorites(); } catch (err) { message.error((err as Error).message); return; } finally { setFavoriting(false); } if (!wasFavorite && customer && !customer.favorite_alerts) offerAlerts(); } function handleFavoriteClick() { if (preview) return; if (authLoading) return; if (!customer) { setPendingAction('favorite'); setAuthModalOpen(true); return; } void doToggleFavorite(); } const hasMultiple = item.images.length > 1; const cover = item.images.length ? (
{item.images.map(img => (
{item.name}
))}
{hasMultiple && ( <>
) : (
); let actionButton = null; if (item.status === 'available') { actionButton = ( ); } else if (item.status === 'reserved') { actionButton = inMyCart ? : ; } const card = (
{item.name}
{item.category_name && {item.category_name}} {item.tags.length > 0 && (
{item.tags.map(tag => ( {tag.name} ))}
)}
${(item.price_cents / 100).toFixed(2)}
{actionButton} setAuthModalOpen(false)} onSuccess={() => { setAuthModalOpen(false); if (pendingAction === 'favorite') void doToggleFavorite(); else void doAddToCart(); }} />
); if (item.status === 'sold') { return {card}; } return card; }