@burdenoff/microfe-coolandlovely 2026.1031.2 → 2026.1101.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coolandlovely/CoolAndLovelyRoot.d.ts.map +1 -1
- package/dist/coolandlovely/CoolAndLovelyRoot.js +16 -4
- package/dist/coolandlovely/CoolAndLovelyRoot.js.map +1 -1
- package/dist/coolandlovely/CoolAndLovelyRoutes.d.ts.map +1 -1
- package/dist/coolandlovely/CoolAndLovelyRoutes.js +19 -15
- package/dist/coolandlovely/CoolAndLovelyRoutes.js.map +1 -1
- package/dist/coolandlovely/components/ActivityTrail.js +1 -1
- package/dist/coolandlovely/components/ActivityTrail.js.map +1 -1
- package/dist/coolandlovely/components/ActorName.js.map +1 -1
- package/dist/coolandlovely/components/AddToCartPanel.js.map +1 -1
- package/dist/coolandlovely/components/AddToLookbookButton.js.map +1 -1
- package/dist/coolandlovely/components/BrandCard.js.map +1 -1
- package/dist/coolandlovely/components/BrandCover.js.map +1 -1
- package/dist/coolandlovely/components/BrandCreatorAvatars.js.map +1 -1
- package/dist/coolandlovely/components/CollaborationFeeCard.js.map +1 -1
- package/dist/coolandlovely/components/CollectionCard.js.map +1 -1
- package/dist/coolandlovely/components/ColourSwatchRow.js.map +1 -1
- package/dist/coolandlovely/components/Comments.js.map +1 -1
- package/dist/coolandlovely/components/EmptyState.js.map +1 -1
- package/dist/coolandlovely/components/FeedbackWidget.js.map +1 -1
- package/dist/coolandlovely/components/Field.js.map +1 -1
- package/dist/coolandlovely/components/FileImage.js.map +1 -1
- package/dist/coolandlovely/components/FollowButton.js.map +1 -1
- package/dist/coolandlovely/components/FulfilmentTimeline.js.map +1 -1
- package/dist/coolandlovely/components/ImageUploadField.js +3 -1
- package/dist/coolandlovely/components/ImageUploadField.js.map +1 -1
- package/dist/coolandlovely/components/LookCard.js.map +1 -1
- package/dist/coolandlovely/components/LookbookCard.js.map +1 -1
- package/dist/coolandlovely/components/MetricsCard.js.map +1 -1
- package/dist/coolandlovely/components/PageLayout.js.map +1 -1
- package/dist/coolandlovely/components/PayForOrder.js.map +1 -1
- package/dist/coolandlovely/components/RefundPanel.js +1 -1
- package/dist/coolandlovely/components/RefundPanel.js.map +1 -1
- package/dist/coolandlovely/components/ReturnRequests.js +1 -1
- package/dist/coolandlovely/components/ReturnRequests.js.map +1 -1
- package/dist/coolandlovely/components/ShopItemCard.js.map +1 -1
- package/dist/coolandlovely/components/StateViews.js.map +1 -1
- package/dist/coolandlovely/components/StatusBadge.js +1 -1
- package/dist/coolandlovely/components/StatusBadge.js.map +1 -1
- package/dist/coolandlovely/components/StepTracker.js.map +1 -1
- package/dist/coolandlovely/components/StockEditor.js.map +1 -1
- package/dist/coolandlovely/components/StoreListingCard.js.map +1 -1
- package/dist/coolandlovely/components/TagList.js.map +1 -1
- package/dist/coolandlovely/components/Thumbnail.js +1 -1
- package/dist/coolandlovely/components/Thumbnail.js.map +1 -1
- package/dist/coolandlovely/components/VisibilityChip.js.map +1 -1
- package/dist/coolandlovely/components/WardrobeItemCard.js.map +1 -1
- package/dist/coolandlovely/components/index.js +2 -1
- package/dist/coolandlovely/context/CoolAndLovelyContext.js +2 -1
- package/dist/coolandlovely/context/CoolAndLovelyContext.js.map +1 -1
- package/dist/coolandlovely/context/index.js +2 -1
- package/dist/coolandlovely/hooks/index.js +4 -3
- package/dist/coolandlovely/hooks/useActivity.d.ts +1 -1
- package/dist/coolandlovely/hooks/useActivity.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useActivity.js.map +1 -1
- package/dist/coolandlovely/hooks/useAssistantEntity.js.map +1 -1
- package/dist/coolandlovely/hooks/useBrandOrdersApi.d.ts +4 -4
- package/dist/coolandlovely/hooks/useBrandOrdersApi.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useBrandOrdersApi.js.map +1 -1
- package/dist/coolandlovely/hooks/useCatalogImport.js.map +1 -1
- package/dist/coolandlovely/hooks/useComments.d.ts +1 -1
- package/dist/coolandlovely/hooks/useComments.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useComments.js.map +1 -1
- package/dist/coolandlovely/hooks/useCommerceApi.d.ts +7 -7
- package/dist/coolandlovely/hooks/useCommerceApi.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useCommerceApi.js.map +1 -1
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.d.ts +24 -24
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js.map +1 -1
- package/dist/coolandlovely/hooks/useEditorialApi.d.ts +95 -0
- package/dist/coolandlovely/hooks/useEditorialApi.d.ts.map +1 -0
- package/dist/coolandlovely/hooks/useEditorialApi.js +107 -0
- package/dist/coolandlovely/hooks/useEditorialApi.js.map +1 -0
- package/dist/coolandlovely/hooks/useFiles.js.map +1 -1
- package/dist/coolandlovely/hooks/useInventoryApi.d.ts +1 -1
- package/dist/coolandlovely/hooks/useInventoryApi.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useInventoryApi.js.map +1 -1
- package/dist/coolandlovely/hooks/useTr.js.map +1 -1
- package/dist/coolandlovely/index.js +6 -5
- package/dist/coolandlovely/pages/HomePage.js.map +1 -1
- package/dist/coolandlovely/pages/admin/AdminBrandsPage.js.map +1 -1
- package/dist/coolandlovely/pages/admin/AdminCollaborationsPage.js.map +1 -1
- package/dist/coolandlovely/pages/admin/AdminFeaturedItemsPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/ImportCatalogPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/commerce/BrandOrdersPage.js.map +1 -1
- package/dist/coolandlovely/pages/commerce/CartPage.js.map +1 -1
- package/dist/coolandlovely/pages/commerce/CreatorEarningsPage.js.map +1 -1
- package/dist/coolandlovely/pages/commerce/OrdersPage.js.map +1 -1
- package/dist/coolandlovely/pages/discover/DiscoverPage.js +1 -1
- package/dist/coolandlovely/pages/discover/DiscoverPage.js.map +1 -1
- package/dist/coolandlovely/pages/editorial/EditorialPage.d.ts +2 -0
- package/dist/coolandlovely/pages/editorial/EditorialPage.d.ts.map +1 -0
- package/dist/coolandlovely/pages/editorial/EditorialPage.js +486 -0
- package/dist/coolandlovely/pages/editorial/EditorialPage.js.map +1 -0
- package/dist/coolandlovely/pages/items/StoreListedPieceView.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js +1 -1
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js.map +1 -1
- package/dist/coolandlovely/pages/trends/TrendsPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js.map +1 -1
- package/dist/coolandlovely/store/coolandlovelyStore.js.map +1 -1
- package/dist/coolandlovely/store/index.js +2 -1
- package/dist/coolandlovely/types/commerce.d.ts.map +1 -1
- package/dist/coolandlovely/types/index.d.ts.map +1 -1
- package/dist/coolandlovely/utils/cn.js +1 -1
- package/dist/coolandlovely/utils/cn.js.map +1 -1
- package/dist/coolandlovely/utils/formatters.js.map +1 -1
- package/dist/coolandlovely/utils/icons.js.map +1 -1
- package/dist/coolandlovely/utils/index.js +2 -1
- package/dist/coolandlovely/utils/navigation.js.map +1 -1
- package/dist/coolandlovely/utils/paymentProviders.js +22 -22
- package/dist/coolandlovely/utils/paymentProviders.js.map +1 -1
- package/dist/coolandlovely/utils/sparkline.js +13 -15
- package/dist/coolandlovely/utils/sparkline.js.map +1 -1
- package/dist/node_modules/clsx/dist/clsx.js +1 -1
- package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +225 -146
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
- package/dist/services/createSandboxAssistantTransport.js.map +1 -1
- package/dist/stories/DiscoverPage.stories.d.ts.map +1 -1
- package/dist/utils/pageContext.js +1 -1
- package/dist/utils/pageContext.js.map +1 -1
- package/package.json +6 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StoreListedPieceView.js","names":[],"sources":["../../../../src/coolandlovely/pages/items/StoreListedPieceView.tsx"],"sourcesContent":["/**\n * Store-listed piece view (BOFF-10017)\n * @module coolandlovely/pages/items\n *\n * What a shopper sees when they follow a Burdenoff store listing (\"View on CoolAndLovely\") to a\n * piece that belongs to a brand outside their workspace. The workspace catalogue has no such\n * piece, so the page falls back to `cooStoreListedPiece` (product facts only, readable from any\n * workspace) and lets the shopper buy it directly with `cooBuyStoreListedPiece`. The order is\n * held by the brand, paid through the provider global-billing chose, and settled only once the\n * backend re-reads billing's record; the shopper follows it from My Orders.\n */\n\nimport { type FC, useState } from 'react';\nimport { ArrowLeft, Loader2, ShoppingBag, Store } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { ColourSwatchRow } from '../../components/ColourSwatchRow';\nimport { TextAreaField, TextField } from '../../components/Field';\nimport { PageLayout } from '../../components/PageLayout';\nimport { PayForOrder } from '../../components/PayForOrder';\nimport { ErrorView, LoadingView } from '../../components/StateViews';\nimport { TagList } from '../../components/TagList';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useTr } from '../../hooks';\nimport {\n useBuyStoreListedPiece,\n useStoreListedPiece,\n useSyncOrderPayment,\n} from '../../hooks/useCommerceApi';\nimport type { CooOrderCheckout, CooStoreListedPiece } from '../../types';\nimport { cn } from '../../utils/cn';\nimport { formatPrice, titleCase } from '../../utils/formatters';\nimport { categoryIcon } from '../../utils/icons';\n\nconst MAX_QUANTITY = 10;\n/** Stock at or below this is shown to the shopper as \"Only N left\". */\nconst LOW_STOCK = 5;\n\nconst chip = (selected: boolean) =>\n cn(\n 'inline-flex min-w-9 items-center justify-center rounded-full border px-3 py-1 text-sm font-medium transition-colors disabled:opacity-40 disabled:line-through',\n selected\n ? 'border-action-primary-bg bg-action-primary-bg text-action-primary-text'\n : 'border-border-seam text-text-primary hover:bg-bg-sunken'\n );\n\nconst primaryButton =\n 'inline-flex w-full items-center justify-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2.5 text-sm font-semibold text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50';\n\n/**\n * Units left of a (possibly partial) size/colour choice while the piece tracks stock; null when\n * it does not. The service checks the stock again when the order is placed.\n */\nfunction unitsLeft(\n piece: CooStoreListedPiece,\n size: string | null,\n colour: string | null\n): number | null {\n if (!piece.trackInventory) return null;\n return piece.variants\n .filter(\n (variant) =>\n (size === null || (variant.size ?? '') === size) &&\n (colour === null || (variant.colour ?? '') === colour)\n )\n .reduce((sum, variant) => sum + variant.available, 0);\n}\n\n/** Size/colour/quantity, shipping details and \"Buy now\" for a store-listed piece. */\nconst BuyListedPiecePanel: FC<{\n piece: CooStoreListedPiece;\n onCheckout: (checkout: CooOrderCheckout) => void;\n}> = ({ piece, onCheckout }) => {\n const tr = useTr();\n const buy = useBuyStoreListedPiece();\n const [size, setSize] = useState<string | null>(\n piece.sizes.length === 1 ? (piece.sizes[0] ?? null) : null\n );\n const [colour, setColour] = useState<string | null>(\n piece.colours.length === 1 ? (piece.colours[0] ?? null) : null\n );\n const [quantity, setQuantity] = useState(1);\n const [shippingName, setShippingName] = useState('');\n const [shippingAddress, setShippingAddress] = useState('');\n const [buyerNote, setBuyerNote] = useState('');\n\n if (!piece.inStock) {\n return (\n <p className=\"rounded-lg border border-border-seam bg-bg-sunken p-3 text-sm text-text-secondary\">\n {tr('coolandlovely.storePiece.soldOut', 'This piece is sold out right now.')}\n </p>\n );\n }\n\n const needsSize = piece.sizes.length > 0 && !size;\n const needsColour = piece.colours.length > 0 && !colour;\n const left = needsSize || needsColour ? null : unitsLeft(piece, size ?? '', colour ?? '');\n const soldOut = left !== null && left <= 0;\n const maxQuantity = left === null ? MAX_QUANTITY : Math.max(1, Math.min(MAX_QUANTITY, left));\n const canSubmit =\n !needsSize &&\n !needsColour &&\n !soldOut &&\n shippingName.trim() !== '' &&\n shippingAddress.trim() !== '';\n\n const submit = () => {\n buy.mutate(\n {\n styleItemId: piece.id,\n quantity,\n size,\n colour,\n shippingName: shippingName.trim(),\n shippingAddress: shippingAddress.trim(),\n buyerNote: buyerNote.trim() || null,\n },\n { onSuccess: onCheckout }\n );\n };\n\n return (\n <div className=\"space-y-4\" data-testid=\"store-buy-panel\">\n {piece.sizes.length > 0 && (\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {tr('coolandlovely.cart.chooseSize', 'Choose a size')}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {piece.sizes.map((option) => (\n <button\n key={option}\n type=\"button\"\n aria-pressed={size === option}\n disabled={unitsLeft(piece, option, colour) === 0}\n onClick={() => {\n setSize(option);\n setQuantity(1);\n }}\n className={chip(size === option)}\n >\n {option}\n </button>\n ))}\n </div>\n </div>\n )}\n {piece.colours.length > 0 && (\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {tr('coolandlovely.cart.chooseColour', 'Choose a colour')}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {piece.colours.map((option) => (\n <button\n key={option}\n type=\"button\"\n aria-pressed={colour === option}\n disabled={unitsLeft(piece, size, option) === 0}\n onClick={() => {\n setColour(option);\n setQuantity(1);\n }}\n className={chip(colour === option)}\n >\n {option}\n </button>\n ))}\n </div>\n </div>\n )}\n <div className=\"flex items-center gap-3\">\n <label htmlFor=\"coo-store-quantity\" className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.cart.quantity', 'Quantity')}\n </label>\n <select\n id=\"coo-store-quantity\"\n value={quantity}\n onChange={(event) => setQuantity(Number(event.target.value))}\n className=\"rounded-lg border border-border-seam bg-bg-surface px-2 py-1.5 text-sm text-text-primary\"\n >\n {Array.from({ length: maxQuantity }, (_, index) => index + 1).map((value) => (\n <option key={value} value={value}>\n {value}\n </option>\n ))}\n </select>\n </div>\n {left !== null && (\n <p\n data-testid=\"store-stock-left\"\n className={cn(\n 'text-sm font-medium',\n soldOut || left <= LOW_STOCK ? 'text-status-warning-text' : 'text-text-secondary'\n )}\n >\n {soldOut\n ? tr('coolandlovely.cart.soldOut', 'Sold out in this size and colour')\n : left <= LOW_STOCK\n ? tr('coolandlovely.cart.onlyLeft', 'Only {{count}} left', { count: left })\n : tr('coolandlovely.cart.inStock', 'In stock')}\n </p>\n )}\n <div className=\"space-y-3 border-t border-border-seam pt-4\">\n <TextField\n id=\"coo-store-shipping-name\"\n label={tr('coolandlovely.checkout.shippingName', 'Full name')}\n value={shippingName}\n onChange={setShippingName}\n required\n />\n <TextAreaField\n id=\"coo-store-shipping-address\"\n label={tr('coolandlovely.checkout.shippingAddress', 'Shipping address')}\n value={shippingAddress}\n onChange={setShippingAddress}\n rows={3}\n required\n />\n <TextField\n id=\"coo-store-buyer-note\"\n label={tr('coolandlovely.checkout.note', 'Note for the brand (optional)')}\n value={buyerNote}\n onChange={setBuyerNote}\n />\n </div>\n <p className=\"text-xs text-text-secondary\">\n {tr('coolandlovely.cart.taxNote', 'Tax is calculated when you place the order.')}\n </p>\n {buy.error && <p className=\"text-sm text-status-error-text\">{buy.error.message}</p>}\n <button\n type=\"button\"\n disabled={!canSubmit || buy.isPending}\n onClick={submit}\n className={primaryButton}\n >\n {buy.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <ShoppingBag className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.storePiece.buyNow', 'Buy now · {{amount}}', {\n amount: formatPrice(piece.price * quantity, piece.currency),\n })}\n </button>\n </div>\n );\n};\n\n/** Pay for the order the store purchase created. */\nconst PayForStorePurchase: FC<{ session: CooOrderCheckout }> = ({ session }) => {\n const tr = useTr();\n const { navigateTo } = useCoolAndLovely();\n const sync = useSyncOrderPayment();\n const settle = async () => {\n await sync.mutateAsync(session.order.id).catch(() => undefined);\n navigateTo(`/orders/${session.order.id}`);\n };\n return (\n <PageLayout\n title={tr('coolandlovely.checkout.payTitle', 'Pay for your order')}\n description={session.order.orderNumber}\n >\n <div className=\"mx-auto max-w-lg space-y-4\">\n <GlassCard treatment=\"glass\" className=\"space-y-4 p-6\">\n <dl className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">\n {tr('coolandlovely.checkout.subtotal', 'Subtotal')}\n </dt>\n <dd className=\"tabular-nums text-text-primary\">\n {formatPrice(session.subtotal, session.currency)}\n </dd>\n </div>\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">{tr('coolandlovely.checkout.tax', 'Tax')}</dt>\n <dd className=\"tabular-nums text-text-primary\">\n {formatPrice(session.taxAmount, session.currency)}\n </dd>\n </div>\n <div className=\"flex justify-between border-t border-border-seam pt-2 text-base font-semibold\">\n <dt className=\"text-text-primary\">{tr('coolandlovely.checkout.total', 'Total')}</dt>\n <dd className=\"tabular-nums text-text-primary\" data-testid=\"store-checkout-total\">\n {formatPrice(session.total, session.currency)}\n </dd>\n </div>\n </dl>\n <PayForOrder checkout={session} onSettle={settle} />\n <button\n type=\"button\"\n onClick={() => navigateTo(`/orders/${session.order.id}`)}\n className=\"w-full text-center text-sm font-medium text-text-secondary hover:underline\"\n >\n {tr('coolandlovely.checkout.payLater', 'Pay later from My Orders')}\n </button>\n </GlassCard>\n </div>\n </PageLayout>\n );\n};\n\n/**\n * The fallback piece page for a store shopper. Renders \"Item not found\" when the store does not\n * list the piece either (drafts, retired pieces, pieces of unpublished brands).\n */\nexport const StoreListedPieceView: FC<{ itemId: string | undefined; onRetry: () => void }> = ({\n itemId,\n onRetry,\n}) => {\n const tr = useTr();\n const { navigateTo } = useCoolAndLovely();\n const query = useStoreListedPiece(itemId, true);\n const [session, setSession] = useState<CooOrderCheckout | null>(null);\n const pageTitle = tr('coolandlovely.storePiece.pageTitle', 'Style item');\n\n if (session) return <PayForStorePurchase session={session} />;\n\n if (query.isLoading) {\n return (\n <PageLayout title={pageTitle}>\n <LoadingView label={tr('coolandlovely.storePiece.loading', 'Loading item…')} />\n </PageLayout>\n );\n }\n\n const piece = query.data;\n if (query.isError || !piece) {\n return (\n <PageLayout title={pageTitle}>\n <ErrorView\n title={tr('coolandlovely.storePiece.notFound', 'Item not found')}\n onRetry={() => {\n onRetry();\n void query.refetch();\n }}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={piece.name}\n description={piece.brandName}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/discover')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> {tr('coolandlovely.storePiece.back', 'Discover')}\n </button>\n }\n >\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\" data-testid=\"store-listed-piece\">\n <div className=\"lg:col-span-2\">\n <GlassCard treatment=\"glass\" className=\"overflow-hidden p-0\">\n <Thumbnail\n icon={categoryIcon(piece.category, 48)}\n seed={piece.id}\n alt={piece.name}\n className=\"aspect-[4/5] w-full\"\n />\n {piece.description && (\n <div className=\"p-6\">\n <p className=\"text-sm leading-relaxed text-text-secondary\">{piece.description}</p>\n </div>\n )}\n </GlassCard>\n </div>\n\n <div className=\"space-y-4\">\n <GlassCard treatment=\"glass\" className=\"space-y-5 p-6\">\n <div>\n <p className=\"inline-flex items-center gap-1.5 text-xs font-medium text-accent-primary\">\n <Store className=\"h-3.5 w-3.5\" />\n {tr('coolandlovely.storePiece.fromStore', 'Listed in the Burdenoff store')}\n </p>\n <h2 className=\"mt-2 text-2xl font-semibold leading-tight text-text-primary\">\n {piece.name}\n </h2>\n <p className=\"mt-1 text-sm font-medium text-text-secondary\">\n {piece.brandName}\n <span className=\"ml-1 text-text-tertiary\">@{piece.brandHandle}</span>\n </p>\n <p className=\"mt-3 text-3xl font-bold text-text-primary tabular-nums\">\n {formatPrice(piece.price, piece.currency)}\n <span className=\"ml-2 text-base font-medium text-text-secondary\">\n {piece.currency}\n </span>\n </p>\n </div>\n {piece.colours.length > 0 && <ColourSwatchRow colours={piece.colours} max={12} />}\n <div className=\"border-t border-border-seam pt-5\">\n <BuyListedPiecePanel piece={piece} onCheckout={setSession} />\n </div>\n {piece.materials.length > 0 && (\n <div>\n <p className=\"mb-1 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {tr('coolandlovely.storePiece.materials', 'Materials')}\n </p>\n <p className=\"text-sm text-text-primary\">{piece.materials.join(', ')}</p>\n </div>\n )}\n {piece.tags.length > 0 && <TagList tags={piece.tags} max={12} tone=\"accent\" />}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {tr('coolandlovely.storePiece.details', 'Details')}\n </h3>\n <dl className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">\n {tr('coolandlovely.storePiece.category', 'Category')}\n </dt>\n <dd className=\"text-text-primary\">{titleCase(piece.category)}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">SKU</dt>\n <dd className=\"text-text-primary\">{piece.sku ?? '—'}</dd>\n </div>\n </dl>\n <p className=\"mt-3 text-xs text-text-secondary\">\n {tr(\n 'coolandlovely.storePiece.soldBy',\n 'Sold and shipped by {{brand}}. Your order appears in My Orders.',\n { brand: piece.brandName }\n )}\n </p>\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StoreListedPieceView.js","names":[],"sources":["../../../../src/coolandlovely/pages/items/StoreListedPieceView.tsx"],"sourcesContent":["/**\n * Store-listed piece view (BOFF-10017)\n * @module coolandlovely/pages/items\n *\n * What a shopper sees when they follow a Burdenoff store listing (\"View on CoolAndLovely\") to a\n * piece that belongs to a brand outside their workspace. The workspace catalogue has no such\n * piece, so the page falls back to `cooStoreListedPiece` (product facts only, readable from any\n * workspace) and lets the shopper buy it directly with `cooBuyStoreListedPiece`. The order is\n * held by the brand, paid through the provider global-billing chose, and settled only once the\n * backend re-reads billing's record; the shopper follows it from My Orders.\n */\n\nimport { type FC, useState } from 'react';\nimport { ArrowLeft, Loader2, ShoppingBag, Store } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { ColourSwatchRow } from '../../components/ColourSwatchRow';\nimport { TextAreaField, TextField } from '../../components/Field';\nimport { PageLayout } from '../../components/PageLayout';\nimport { PayForOrder } from '../../components/PayForOrder';\nimport { ErrorView, LoadingView } from '../../components/StateViews';\nimport { TagList } from '../../components/TagList';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useTr } from '../../hooks';\nimport {\n useBuyStoreListedPiece,\n useStoreListedPiece,\n useSyncOrderPayment,\n} from '../../hooks/useCommerceApi';\nimport type { CooOrderCheckout, CooStoreListedPiece } from '../../types';\nimport { cn } from '../../utils/cn';\nimport { formatPrice, titleCase } from '../../utils/formatters';\nimport { categoryIcon } from '../../utils/icons';\n\nconst MAX_QUANTITY = 10;\n/** Stock at or below this is shown to the shopper as \"Only N left\". */\nconst LOW_STOCK = 5;\n\nconst chip = (selected: boolean) =>\n cn(\n 'inline-flex min-w-9 items-center justify-center rounded-full border px-3 py-1 text-sm font-medium transition-colors disabled:opacity-40 disabled:line-through',\n selected\n ? 'border-action-primary-bg bg-action-primary-bg text-action-primary-text'\n : 'border-border-seam text-text-primary hover:bg-bg-sunken'\n );\n\nconst primaryButton =\n 'inline-flex w-full items-center justify-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2.5 text-sm font-semibold text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50';\n\n/**\n * Units left of a (possibly partial) size/colour choice while the piece tracks stock; null when\n * it does not. The service checks the stock again when the order is placed.\n */\nfunction unitsLeft(\n piece: CooStoreListedPiece,\n size: string | null,\n colour: string | null\n): number | null {\n if (!piece.trackInventory) return null;\n return piece.variants\n .filter(\n (variant) =>\n (size === null || (variant.size ?? '') === size) &&\n (colour === null || (variant.colour ?? '') === colour)\n )\n .reduce((sum, variant) => sum + variant.available, 0);\n}\n\n/** Size/colour/quantity, shipping details and \"Buy now\" for a store-listed piece. */\nconst BuyListedPiecePanel: FC<{\n piece: CooStoreListedPiece;\n onCheckout: (checkout: CooOrderCheckout) => void;\n}> = ({ piece, onCheckout }) => {\n const tr = useTr();\n const buy = useBuyStoreListedPiece();\n const [size, setSize] = useState<string | null>(\n piece.sizes.length === 1 ? (piece.sizes[0] ?? null) : null\n );\n const [colour, setColour] = useState<string | null>(\n piece.colours.length === 1 ? (piece.colours[0] ?? null) : null\n );\n const [quantity, setQuantity] = useState(1);\n const [shippingName, setShippingName] = useState('');\n const [shippingAddress, setShippingAddress] = useState('');\n const [buyerNote, setBuyerNote] = useState('');\n\n if (!piece.inStock) {\n return (\n <p className=\"rounded-lg border border-border-seam bg-bg-sunken p-3 text-sm text-text-secondary\">\n {tr('coolandlovely.storePiece.soldOut', 'This piece is sold out right now.')}\n </p>\n );\n }\n\n const needsSize = piece.sizes.length > 0 && !size;\n const needsColour = piece.colours.length > 0 && !colour;\n const left = needsSize || needsColour ? null : unitsLeft(piece, size ?? '', colour ?? '');\n const soldOut = left !== null && left <= 0;\n const maxQuantity = left === null ? MAX_QUANTITY : Math.max(1, Math.min(MAX_QUANTITY, left));\n const canSubmit =\n !needsSize &&\n !needsColour &&\n !soldOut &&\n shippingName.trim() !== '' &&\n shippingAddress.trim() !== '';\n\n const submit = () => {\n buy.mutate(\n {\n styleItemId: piece.id,\n quantity,\n size,\n colour,\n shippingName: shippingName.trim(),\n shippingAddress: shippingAddress.trim(),\n buyerNote: buyerNote.trim() || null,\n },\n { onSuccess: onCheckout }\n );\n };\n\n return (\n <div className=\"space-y-4\" data-testid=\"store-buy-panel\">\n {piece.sizes.length > 0 && (\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {tr('coolandlovely.cart.chooseSize', 'Choose a size')}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {piece.sizes.map((option) => (\n <button\n key={option}\n type=\"button\"\n aria-pressed={size === option}\n disabled={unitsLeft(piece, option, colour) === 0}\n onClick={() => {\n setSize(option);\n setQuantity(1);\n }}\n className={chip(size === option)}\n >\n {option}\n </button>\n ))}\n </div>\n </div>\n )}\n {piece.colours.length > 0 && (\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {tr('coolandlovely.cart.chooseColour', 'Choose a colour')}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {piece.colours.map((option) => (\n <button\n key={option}\n type=\"button\"\n aria-pressed={colour === option}\n disabled={unitsLeft(piece, size, option) === 0}\n onClick={() => {\n setColour(option);\n setQuantity(1);\n }}\n className={chip(colour === option)}\n >\n {option}\n </button>\n ))}\n </div>\n </div>\n )}\n <div className=\"flex items-center gap-3\">\n <label htmlFor=\"coo-store-quantity\" className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.cart.quantity', 'Quantity')}\n </label>\n <select\n id=\"coo-store-quantity\"\n value={quantity}\n onChange={(event) => setQuantity(Number(event.target.value))}\n className=\"rounded-lg border border-border-seam bg-bg-surface px-2 py-1.5 text-sm text-text-primary\"\n >\n {Array.from({ length: maxQuantity }, (_, index) => index + 1).map((value) => (\n <option key={value} value={value}>\n {value}\n </option>\n ))}\n </select>\n </div>\n {left !== null && (\n <p\n data-testid=\"store-stock-left\"\n className={cn(\n 'text-sm font-medium',\n soldOut || left <= LOW_STOCK ? 'text-status-warning-text' : 'text-text-secondary'\n )}\n >\n {soldOut\n ? tr('coolandlovely.cart.soldOut', 'Sold out in this size and colour')\n : left <= LOW_STOCK\n ? tr('coolandlovely.cart.onlyLeft', 'Only {{count}} left', { count: left })\n : tr('coolandlovely.cart.inStock', 'In stock')}\n </p>\n )}\n <div className=\"space-y-3 border-t border-border-seam pt-4\">\n <TextField\n id=\"coo-store-shipping-name\"\n label={tr('coolandlovely.checkout.shippingName', 'Full name')}\n value={shippingName}\n onChange={setShippingName}\n required\n />\n <TextAreaField\n id=\"coo-store-shipping-address\"\n label={tr('coolandlovely.checkout.shippingAddress', 'Shipping address')}\n value={shippingAddress}\n onChange={setShippingAddress}\n rows={3}\n required\n />\n <TextField\n id=\"coo-store-buyer-note\"\n label={tr('coolandlovely.checkout.note', 'Note for the brand (optional)')}\n value={buyerNote}\n onChange={setBuyerNote}\n />\n </div>\n <p className=\"text-xs text-text-secondary\">\n {tr('coolandlovely.cart.taxNote', 'Tax is calculated when you place the order.')}\n </p>\n {buy.error && <p className=\"text-sm text-status-error-text\">{buy.error.message}</p>}\n <button\n type=\"button\"\n disabled={!canSubmit || buy.isPending}\n onClick={submit}\n className={primaryButton}\n >\n {buy.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <ShoppingBag className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.storePiece.buyNow', 'Buy now · {{amount}}', {\n amount: formatPrice(piece.price * quantity, piece.currency),\n })}\n </button>\n </div>\n );\n};\n\n/** Pay for the order the store purchase created. */\nconst PayForStorePurchase: FC<{ session: CooOrderCheckout }> = ({ session }) => {\n const tr = useTr();\n const { navigateTo } = useCoolAndLovely();\n const sync = useSyncOrderPayment();\n const settle = async () => {\n await sync.mutateAsync(session.order.id).catch(() => undefined);\n navigateTo(`/orders/${session.order.id}`);\n };\n return (\n <PageLayout\n title={tr('coolandlovely.checkout.payTitle', 'Pay for your order')}\n description={session.order.orderNumber}\n >\n <div className=\"mx-auto max-w-lg space-y-4\">\n <GlassCard treatment=\"glass\" className=\"space-y-4 p-6\">\n <dl className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">\n {tr('coolandlovely.checkout.subtotal', 'Subtotal')}\n </dt>\n <dd className=\"tabular-nums text-text-primary\">\n {formatPrice(session.subtotal, session.currency)}\n </dd>\n </div>\n <div className=\"flex justify-between\">\n <dt className=\"text-text-secondary\">{tr('coolandlovely.checkout.tax', 'Tax')}</dt>\n <dd className=\"tabular-nums text-text-primary\">\n {formatPrice(session.taxAmount, session.currency)}\n </dd>\n </div>\n <div className=\"flex justify-between border-t border-border-seam pt-2 text-base font-semibold\">\n <dt className=\"text-text-primary\">{tr('coolandlovely.checkout.total', 'Total')}</dt>\n <dd className=\"tabular-nums text-text-primary\" data-testid=\"store-checkout-total\">\n {formatPrice(session.total, session.currency)}\n </dd>\n </div>\n </dl>\n <PayForOrder checkout={session} onSettle={settle} />\n <button\n type=\"button\"\n onClick={() => navigateTo(`/orders/${session.order.id}`)}\n className=\"w-full text-center text-sm font-medium text-text-secondary hover:underline\"\n >\n {tr('coolandlovely.checkout.payLater', 'Pay later from My Orders')}\n </button>\n </GlassCard>\n </div>\n </PageLayout>\n );\n};\n\n/**\n * The fallback piece page for a store shopper. Renders \"Item not found\" when the store does not\n * list the piece either (drafts, retired pieces, pieces of unpublished brands).\n */\nexport const StoreListedPieceView: FC<{ itemId: string | undefined; onRetry: () => void }> = ({\n itemId,\n onRetry,\n}) => {\n const tr = useTr();\n const { navigateTo } = useCoolAndLovely();\n const query = useStoreListedPiece(itemId, true);\n const [session, setSession] = useState<CooOrderCheckout | null>(null);\n const pageTitle = tr('coolandlovely.storePiece.pageTitle', 'Style item');\n\n if (session) return <PayForStorePurchase session={session} />;\n\n if (query.isLoading) {\n return (\n <PageLayout title={pageTitle}>\n <LoadingView label={tr('coolandlovely.storePiece.loading', 'Loading item…')} />\n </PageLayout>\n );\n }\n\n const piece = query.data;\n if (query.isError || !piece) {\n return (\n <PageLayout title={pageTitle}>\n <ErrorView\n title={tr('coolandlovely.storePiece.notFound', 'Item not found')}\n onRetry={() => {\n onRetry();\n void query.refetch();\n }}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={piece.name}\n description={piece.brandName}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/discover')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> {tr('coolandlovely.storePiece.back', 'Discover')}\n </button>\n }\n >\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\" data-testid=\"store-listed-piece\">\n <div className=\"lg:col-span-2\">\n <GlassCard treatment=\"glass\" className=\"overflow-hidden p-0\">\n <Thumbnail\n icon={categoryIcon(piece.category, 48)}\n seed={piece.id}\n alt={piece.name}\n className=\"aspect-[4/5] w-full\"\n />\n {piece.description && (\n <div className=\"p-6\">\n <p className=\"text-sm leading-relaxed text-text-secondary\">{piece.description}</p>\n </div>\n )}\n </GlassCard>\n </div>\n\n <div className=\"space-y-4\">\n <GlassCard treatment=\"glass\" className=\"space-y-5 p-6\">\n <div>\n <p className=\"inline-flex items-center gap-1.5 text-xs font-medium text-accent-primary\">\n <Store className=\"h-3.5 w-3.5\" />\n {tr('coolandlovely.storePiece.fromStore', 'Listed in the Burdenoff store')}\n </p>\n <h2 className=\"mt-2 text-2xl font-semibold leading-tight text-text-primary\">\n {piece.name}\n </h2>\n <p className=\"mt-1 text-sm font-medium text-text-secondary\">\n {piece.brandName}\n <span className=\"ml-1 text-text-tertiary\">@{piece.brandHandle}</span>\n </p>\n <p className=\"mt-3 text-3xl font-bold text-text-primary tabular-nums\">\n {formatPrice(piece.price, piece.currency)}\n <span className=\"ml-2 text-base font-medium text-text-secondary\">\n {piece.currency}\n </span>\n </p>\n </div>\n {piece.colours.length > 0 && <ColourSwatchRow colours={piece.colours} max={12} />}\n <div className=\"border-t border-border-seam pt-5\">\n <BuyListedPiecePanel piece={piece} onCheckout={setSession} />\n </div>\n {piece.materials.length > 0 && (\n <div>\n <p className=\"mb-1 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {tr('coolandlovely.storePiece.materials', 'Materials')}\n </p>\n <p className=\"text-sm text-text-primary\">{piece.materials.join(', ')}</p>\n </div>\n )}\n {piece.tags.length > 0 && <TagList tags={piece.tags} max={12} tone=\"accent\" />}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {tr('coolandlovely.storePiece.details', 'Details')}\n </h3>\n <dl className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">\n {tr('coolandlovely.storePiece.category', 'Category')}\n </dt>\n <dd className=\"text-text-primary\">{titleCase(piece.category)}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">SKU</dt>\n <dd className=\"text-text-primary\">{piece.sku ?? '—'}</dd>\n </div>\n </dl>\n <p className=\"mt-3 text-xs text-text-secondary\">\n {tr(\n 'coolandlovely.storePiece.soldBy',\n 'Sold and shipped by {{brand}}. Your order appears in My Orders.',\n { brand: piece.brandName }\n )}\n </p>\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StyleItemDetailPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/items/StyleItemDetailPage.tsx"],"sourcesContent":["/**\n * Style Item Detail Page\n * @module coolandlovely/pages/items\n *\n * Renders a single catalogue piece (`cooStyleItem`) with edit + archive\n * (`cooArchiveStyleItem`) actions and its Burdenoff store listing state.\n *\n * A shopper who follows a Burdenoff store listing to a brand outside their\n * workspace finds no such piece in the workspace catalogue; the page then\n * falls back to the store-listed piece view, where they can buy it (BOFF-10017).\n */\n\nimport { useParams, useSearchParams } from 'react-router';\nimport { ArrowLeft, Pencil, Archive, Loader2, ChevronRight, Shapes } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { LookCard } from '../../components/LookCard';\nimport { TagList } from '../../components/TagList';\nimport { ColourSwatchRow } from '../../components/ColourSwatchRow';\nimport { AddToLookbookButton } from '../../components/AddToLookbookButton';\nimport { AddToCartPanel } from '../../components/AddToCartPanel';\nimport { StoreListingCard } from '../../components/StoreListingCard';\nimport { StoreListedPieceView } from './StoreListedPieceView';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport {\n useAssistantEntityName,\n useStyleItem,\n useArchiveStyleItem,\n useLooksWithItems,\n} from '../../hooks';\nimport { categoryIcon } from '../../utils/icons';\nimport { formatPrice, formatDate, titleCase } from '../../utils/formatters';\n\nexport function StyleItemDetailPage() {\n const { itemId } = useParams<{ itemId: string }>();\n // \"Shop the look\" links carry the look id so the look's creator earns the commission.\n const [searchParams] = useSearchParams();\n const fromLookId = searchParams.get('look');\n const { navigateTo } = useCoolAndLovely();\n const query = useStyleItem(itemId);\n const looksQuery = useLooksWithItems({ pagination: { limit: 100, offset: 0 } });\n const archive = useArchiveStyleItem();\n // Name the piece for the assistant's screen context (see useAssistantEntity).\n useAssistantEntityName(itemId, query.data?.name);\n\n if (query.isLoading) {\n return (\n <PageLayout title=\"Style item\">\n <LoadingView label=\"Loading item…\" />\n </PageLayout>\n );\n }\n\n // Not in this workspace's catalogue: a store shopper from outside the brand's workspace.\n if (!query.isError && !query.data) {\n return <StoreListedPieceView itemId={itemId} onRetry={() => void query.refetch()} />;\n }\n\n if (query.isError || !query.data) {\n return (\n <PageLayout title=\"Style item\">\n <ErrorView title=\"Item not found\" onRetry={() => void query.refetch()} />\n </PageLayout>\n );\n }\n\n const item = query.data;\n const isArchived = item.status === 'RETIRED';\n const appearsInLooks = (looksQuery.data?.items ?? [])\n .filter((look) => look.items?.some((lookItem) => lookItem.styleItemId === item.id))\n .slice(0, 4);\n\n return (\n <PageLayout\n title={item.name}\n description={item.brand?.name ?? undefined}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/items')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <AddToLookbookButton styleItemId={item.id} />\n <button\n type=\"button\"\n onClick={() => navigateTo(`/items/${item.id}/edit`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Pencil className=\"h-4 w-4\" /> Edit\n </button>\n {!isArchived && (\n <button\n type=\"button\"\n disabled={archive.isPending}\n onClick={() => archive.mutate(item.id, { onSuccess: () => navigateTo('/items') })}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-50\"\n >\n {archive.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Archive className=\"h-4 w-4\" />\n )}\n Archive\n </button>\n )}\n </div>\n }\n >\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Product image */}\n <div className=\"lg:col-span-2\">\n <GlassCard treatment=\"glass\" className=\"group overflow-hidden p-0\">\n <Thumbnail\n icon={categoryIcon(item.category, 48)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"aspect-[4/5] w-full\"\n />\n {item.description && (\n <div className=\"p-6\">\n <p className=\"text-sm leading-relaxed text-text-secondary\">{item.description}</p>\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Product panel */}\n <div className=\"space-y-4\">\n <GlassCard treatment=\"glass\" className=\"space-y-5 p-6\">\n <div>\n <h2 className=\"text-2xl font-semibold leading-tight text-text-primary\">\n {item.name}\n </h2>\n {item.brand && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/brands/${item.brand?.id}`)}\n className=\"mt-1 inline-flex items-center gap-0.5 text-sm font-medium text-action-primary-bg hover:underline\"\n >\n {item.brand.name}\n <ChevronRight className=\"h-3.5 w-3.5\" />\n </button>\n )}\n <div className=\"mt-3 flex flex-wrap items-center justify-between gap-2\">\n <p className=\"text-3xl font-bold text-text-primary tabular-nums\">\n {formatPrice(item.price, item.currency)}\n <span className=\"ml-2 text-base font-medium text-text-secondary\">\n {item.currency}\n </span>\n </p>\n <StatusBadge status={item.status} />\n </div>\n </div>\n\n <div className=\"border-t border-border-seam\" />\n\n {item.colours.length > 0 && (\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Colours\n </p>\n <ColourSwatchRow colours={item.colours} max={12} />\n </div>\n )}\n\n {item.sizes.length > 0 && (\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Size\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {item.sizes.map((size) => (\n <span\n key={size}\n className=\"inline-flex min-w-9 items-center justify-center rounded-full border border-border-seam px-3 py-1 text-sm font-medium text-text-primary\"\n >\n {size}\n </span>\n ))}\n </div>\n </div>\n )}\n\n <div className=\"border-t border-border-seam pt-5\">\n <AddToCartPanel item={item} lookId={fromLookId} />\n </div>\n\n {item.materials.length > 0 && (\n <div>\n <p className=\"mb-1 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Materials\n </p>\n <p className=\"text-sm text-text-primary\">{item.materials.join(', ')}</p>\n </div>\n )}\n\n {item.tags.length > 0 && (\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Tags\n </p>\n <TagList tags={item.tags} max={12} tone=\"accent\" />\n </div>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Details</h3>\n <dl className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Category</dt>\n <dd className=\"text-text-primary\">{titleCase(item.category)}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">SKU</dt>\n <dd className=\"text-text-primary\">{item.sku ?? '—'}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Currency</dt>\n <dd className=\"text-text-primary\">{item.currency}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Created</dt>\n <dd className=\"text-text-primary\">\n {formatDate(item.createdAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n </dl>\n </GlassCard>\n\n <StoreListingCard item={item} />\n\n {item.collection && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/collections/${item.collection?.id}`)}\n className=\"flex w-full items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-4 text-left transition-colors hover:bg-bg-sunken/50\"\n >\n <Thumbnail\n icon={<Shapes className=\"h-5 w-5\" />}\n seed={item.collection.id}\n className=\"h-10 w-10 shrink-0\"\n />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {item.collection.name}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">View collection</p>\n </div>\n </button>\n )}\n </div>\n </div>\n\n <section className=\"mt-8\">\n <div className=\"mb-3 flex items-center justify-between gap-3\">\n <h3 className=\"text-lg font-semibold text-text-primary\">Appears in looks</h3>\n {appearsInLooks.length > 0 && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/looks')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-action-primary-bg hover:underline\"\n >\n View all <ChevronRight className=\"h-3.5 w-3.5\" />\n </button>\n )}\n </div>\n {looksQuery.isLoading ? (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" /> Loading referenced looks…\n </div>\n ) : appearsInLooks.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n This piece has not been tagged in any looks yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {appearsInLooks.map((look) => (\n <LookCard\n key={look.id}\n id={look.id}\n title={look.title}\n occasion={look.occasion}\n imageFileId={look.image?.id}\n likeCount={look.likeCount}\n isLikedByMe={look.isLikedByMe}\n isSavedByMe={look.isSavedByMe}\n onOpen={() => navigateTo(`/looks/${look.id}`)}\n showActions\n />\n ))}\n </div>\n )}\n </section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StyleItemDetailPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/items/StyleItemDetailPage.tsx"],"sourcesContent":["/**\n * Style Item Detail Page\n * @module coolandlovely/pages/items\n *\n * Renders a single catalogue piece (`cooStyleItem`) with edit + archive\n * (`cooArchiveStyleItem`) actions and its Burdenoff store listing state.\n *\n * A shopper who follows a Burdenoff store listing to a brand outside their\n * workspace finds no such piece in the workspace catalogue; the page then\n * falls back to the store-listed piece view, where they can buy it (BOFF-10017).\n */\n\nimport { useParams, useSearchParams } from 'react-router';\nimport { ArrowLeft, Pencil, Archive, Loader2, ChevronRight, Shapes } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { LookCard } from '../../components/LookCard';\nimport { TagList } from '../../components/TagList';\nimport { ColourSwatchRow } from '../../components/ColourSwatchRow';\nimport { AddToLookbookButton } from '../../components/AddToLookbookButton';\nimport { AddToCartPanel } from '../../components/AddToCartPanel';\nimport { StoreListingCard } from '../../components/StoreListingCard';\nimport { StoreListedPieceView } from './StoreListedPieceView';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport {\n useAssistantEntityName,\n useStyleItem,\n useArchiveStyleItem,\n useLooksWithItems,\n} from '../../hooks';\nimport { categoryIcon } from '../../utils/icons';\nimport { formatPrice, formatDate, titleCase } from '../../utils/formatters';\n\nexport function StyleItemDetailPage() {\n const { itemId } = useParams<{ itemId: string }>();\n // \"Shop the look\" links carry the look id so the look's creator earns the commission.\n const [searchParams] = useSearchParams();\n const fromLookId = searchParams.get('look');\n const { navigateTo } = useCoolAndLovely();\n const query = useStyleItem(itemId);\n const looksQuery = useLooksWithItems({ pagination: { limit: 100, offset: 0 } });\n const archive = useArchiveStyleItem();\n // Name the piece for the assistant's screen context (see useAssistantEntity).\n useAssistantEntityName(itemId, query.data?.name);\n\n if (query.isLoading) {\n return (\n <PageLayout title=\"Style item\">\n <LoadingView label=\"Loading item…\" />\n </PageLayout>\n );\n }\n\n // Not in this workspace's catalogue: a store shopper from outside the brand's workspace.\n if (!query.isError && !query.data) {\n return <StoreListedPieceView itemId={itemId} onRetry={() => void query.refetch()} />;\n }\n\n if (query.isError || !query.data) {\n return (\n <PageLayout title=\"Style item\">\n <ErrorView title=\"Item not found\" onRetry={() => void query.refetch()} />\n </PageLayout>\n );\n }\n\n const item = query.data;\n const isArchived = item.status === 'RETIRED';\n const appearsInLooks = (looksQuery.data?.items ?? [])\n .filter((look) => look.items?.some((lookItem) => lookItem.styleItemId === item.id))\n .slice(0, 4);\n\n return (\n <PageLayout\n title={item.name}\n description={item.brand?.name ?? undefined}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/items')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <AddToLookbookButton styleItemId={item.id} />\n <button\n type=\"button\"\n onClick={() => navigateTo(`/items/${item.id}/edit`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Pencil className=\"h-4 w-4\" /> Edit\n </button>\n {!isArchived && (\n <button\n type=\"button\"\n disabled={archive.isPending}\n onClick={() => archive.mutate(item.id, { onSuccess: () => navigateTo('/items') })}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-50\"\n >\n {archive.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Archive className=\"h-4 w-4\" />\n )}\n Archive\n </button>\n )}\n </div>\n }\n >\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Product image */}\n <div className=\"lg:col-span-2\">\n <GlassCard treatment=\"glass\" className=\"group overflow-hidden p-0\">\n <Thumbnail\n icon={categoryIcon(item.category, 48)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"aspect-[4/5] w-full\"\n />\n {item.description && (\n <div className=\"p-6\">\n <p className=\"text-sm leading-relaxed text-text-secondary\">{item.description}</p>\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Product panel */}\n <div className=\"space-y-4\">\n <GlassCard treatment=\"glass\" className=\"space-y-5 p-6\">\n <div>\n <h2 className=\"text-2xl font-semibold leading-tight text-text-primary\">\n {item.name}\n </h2>\n {item.brand && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/brands/${item.brand?.id}`)}\n className=\"mt-1 inline-flex items-center gap-0.5 text-sm font-medium text-action-primary-bg hover:underline\"\n >\n {item.brand.name}\n <ChevronRight className=\"h-3.5 w-3.5\" />\n </button>\n )}\n <div className=\"mt-3 flex flex-wrap items-center justify-between gap-2\">\n <p className=\"text-3xl font-bold text-text-primary tabular-nums\">\n {formatPrice(item.price, item.currency)}\n <span className=\"ml-2 text-base font-medium text-text-secondary\">\n {item.currency}\n </span>\n </p>\n <StatusBadge status={item.status} />\n </div>\n </div>\n\n <div className=\"border-t border-border-seam\" />\n\n {item.colours.length > 0 && (\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Colours\n </p>\n <ColourSwatchRow colours={item.colours} max={12} />\n </div>\n )}\n\n {item.sizes.length > 0 && (\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Size\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {item.sizes.map((size) => (\n <span\n key={size}\n className=\"inline-flex min-w-9 items-center justify-center rounded-full border border-border-seam px-3 py-1 text-sm font-medium text-text-primary\"\n >\n {size}\n </span>\n ))}\n </div>\n </div>\n )}\n\n <div className=\"border-t border-border-seam pt-5\">\n <AddToCartPanel item={item} lookId={fromLookId} />\n </div>\n\n {item.materials.length > 0 && (\n <div>\n <p className=\"mb-1 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Materials\n </p>\n <p className=\"text-sm text-text-primary\">{item.materials.join(', ')}</p>\n </div>\n )}\n\n {item.tags.length > 0 && (\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Tags\n </p>\n <TagList tags={item.tags} max={12} tone=\"accent\" />\n </div>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Details</h3>\n <dl className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Category</dt>\n <dd className=\"text-text-primary\">{titleCase(item.category)}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">SKU</dt>\n <dd className=\"text-text-primary\">{item.sku ?? '—'}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Currency</dt>\n <dd className=\"text-text-primary\">{item.currency}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Created</dt>\n <dd className=\"text-text-primary\">\n {formatDate(item.createdAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n </dl>\n </GlassCard>\n\n <StoreListingCard item={item} />\n\n {item.collection && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/collections/${item.collection?.id}`)}\n className=\"flex w-full items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-4 text-left transition-colors hover:bg-bg-sunken/50\"\n >\n <Thumbnail\n icon={<Shapes className=\"h-5 w-5\" />}\n seed={item.collection.id}\n className=\"h-10 w-10 shrink-0\"\n />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {item.collection.name}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">View collection</p>\n </div>\n </button>\n )}\n </div>\n </div>\n\n <section className=\"mt-8\">\n <div className=\"mb-3 flex items-center justify-between gap-3\">\n <h3 className=\"text-lg font-semibold text-text-primary\">Appears in looks</h3>\n {appearsInLooks.length > 0 && (\n <button\n type=\"button\"\n onClick={() => navigateTo('/looks')}\n className=\"inline-flex items-center gap-1 text-sm font-medium text-action-primary-bg hover:underline\"\n >\n View all <ChevronRight className=\"h-3.5 w-3.5\" />\n </button>\n )}\n </div>\n {looksQuery.isLoading ? (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" /> Loading referenced looks…\n </div>\n ) : appearsInLooks.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n This piece has not been tagged in any looks yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {appearsInLooks.map((look) => (\n <LookCard\n key={look.id}\n id={look.id}\n title={look.title}\n occasion={look.occasion}\n imageFileId={look.image?.id}\n likeCount={look.likeCount}\n isLikedByMe={look.isLikedByMe}\n isSavedByMe={look.isSavedByMe}\n onOpen={() => navigateTo(`/looks/${look.id}`)}\n showActions\n />\n ))}\n </div>\n )}\n </section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StyleItemFormPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/items/StyleItemFormPage.tsx"],"sourcesContent":["/**\n * Style Item Create / Edit Page\n * @module coolandlovely/pages/items\n *\n * Drives `cooCreateStyleItem` (new) and `cooUpdateStyleItem` (edit).\n * Brand selection is required on create (and locked on edit). The collection\n * select is scoped to the chosen brand.\n */\n\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams, useSearchParams } from 'react-router';\nimport { ArrowLeft, Loader2, Save, Shirt } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { TextField, TextAreaField, SelectField } from '../../components/Field';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { StockEditor } from '../../components/StockEditor';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport {\n useStyleItem,\n useCreateStyleItem,\n useUpdateStyleItem,\n useBrands,\n useCollections,\n} from '../../hooks';\nimport { COO_FILE_FOLDERS } from '../../hooks/useFiles';\nimport { STYLE_CATEGORIES } from '../../utils/icons';\nimport { parseCsv, titleCase } from '../../utils/formatters';\nimport type { CooStyleCategory, CooStyleItemStatus } from '../../types';\n\ninterface FormState {\n brandId: string;\n collectionId: string;\n name: string;\n description: string;\n category: CooStyleCategory;\n sku: string;\n price: string;\n currency: string;\n colours: string;\n sizes: string;\n materials: string;\n tags: string;\n imageFileId: string;\n /** Sale status (edit only). RETIRED is archive-only and never offered here. */\n status: CooStyleItemStatus;\n}\n\n/** Sale statuses a brand can set; checkout only sells ACTIVE pieces (BOFF-3370). */\nconst SALE_STATUS_OPTIONS: Array<{ value: CooStyleItemStatus; label: string }> = [\n { value: 'DRAFT', label: 'Draft (not on sale)' },\n { value: 'ACTIVE', label: 'On sale' },\n { value: 'OUT_OF_STOCK', label: 'Out of stock' },\n];\n\nconst EMPTY: FormState = {\n brandId: '',\n collectionId: '',\n name: '',\n description: '',\n category: 'OTHER',\n sku: '',\n price: '',\n currency: 'USD',\n colours: '',\n sizes: '',\n materials: '',\n tags: '',\n imageFileId: '',\n status: 'DRAFT',\n};\n\nexport function StyleItemFormPage() {\n const { itemId } = useParams<{ itemId: string }>();\n const [searchParams] = useSearchParams();\n const isEdit = Boolean(itemId);\n const { navigateTo } = useCoolAndLovely();\n\n const existing = useStyleItem(isEdit ? itemId : undefined);\n const brands = useBrands({ pagination: { limit: 100, offset: 0 } });\n const createItem = useCreateStyleItem();\n const updateItem = useUpdateStyleItem();\n\n const [form, setForm] = useState<FormState>(() => ({\n ...EMPTY,\n brandId: searchParams.get('brandId') ?? '',\n collectionId: searchParams.get('collectionId') ?? '',\n }));\n const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});\n\n // Collections scoped to the selected brand (for the optional collection select)\n const collections = useCollections(form.brandId ? { brandId: form.brandId } : {});\n\n useEffect(() => {\n if (isEdit && existing.data) {\n const it = existing.data;\n setForm({\n brandId: it.brandId,\n collectionId: it.collectionId ?? '',\n name: it.name,\n description: it.description ?? '',\n category: it.category,\n sku: it.sku ?? '',\n price: String(it.price),\n currency: it.currency,\n colours: it.colours.join(', '),\n sizes: it.sizes.join(', '),\n materials: it.materials.join(', '),\n tags: it.tags.join(', '),\n imageFileId: it.image?.id ?? '',\n status: it.status,\n });\n }\n }, [isEdit, existing.data]);\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((f) => ({ ...f, [key]: value }));\n\n const mutation = isEdit ? updateItem : createItem;\n\n const brandOptions = useMemo(\n () => (brands.data?.items ?? []).map((b) => ({ value: b.id, label: b.name })),\n [brands.data]\n );\n const collectionOptions = useMemo(\n () => (collections.data ?? []).map((c) => ({ value: c.id, label: c.name })),\n [collections.data]\n );\n const categoryOptions = STYLE_CATEGORIES.map((c) => ({ value: c, label: titleCase(c) }));\n\n const validate = (): boolean => {\n const next: Partial<Record<keyof FormState, string>> = {};\n if (!form.name.trim()) next.name = 'Name is required';\n if (!isEdit && !form.brandId) next.brandId = 'Brand is required';\n if (form.price && Number.isNaN(Number(form.price))) next.price = 'Price must be a number';\n setErrors(next);\n return Object.keys(next).length === 0;\n };\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (!validate()) return;\n\n const priceNum = form.price.trim() ? Number(form.price) : undefined;\n\n if (isEdit && itemId) {\n updateItem.mutate(\n {\n id: itemId,\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n category: form.category,\n collectionId: form.collectionId || undefined,\n sku: form.sku.trim() || undefined,\n price: priceNum,\n currency: form.currency.trim() || undefined,\n colours: parseCsv(form.colours),\n sizes: parseCsv(form.sizes),\n materials: parseCsv(form.materials),\n tags: parseCsv(form.tags),\n imageFileId: form.imageFileId || undefined,\n // A retired piece keeps its terminal status; only live statuses are sent.\n status: form.status === 'RETIRED' ? undefined : form.status,\n },\n },\n { onSuccess: () => navigateTo(`/items/${itemId}`) }\n );\n } else {\n createItem.mutate(\n {\n brandId: form.brandId,\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n category: form.category,\n collectionId: form.collectionId || undefined,\n sku: form.sku.trim() || undefined,\n price: priceNum,\n currency: form.currency.trim() || undefined,\n colours: parseCsv(form.colours),\n sizes: parseCsv(form.sizes),\n materials: parseCsv(form.materials),\n tags: parseCsv(form.tags),\n imageFileId: form.imageFileId || undefined,\n },\n { onSuccess: (item) => navigateTo(`/items/${item.id}`) }\n );\n }\n };\n\n if (isEdit && existing.isLoading) {\n return (\n <PageLayout title=\"Edit item\">\n <LoadingView label=\"Loading item…\" />\n </PageLayout>\n );\n }\n\n if (isEdit && (existing.isError || !existing.data)) {\n return (\n <PageLayout title=\"Edit item\">\n <ErrorView title=\"Item not found\" onRetry={() => void existing.refetch()} />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={isEdit ? 'Edit item' : 'New style item'}\n description={isEdit ? 'Update this catalogue piece' : 'List a new shoppable piece'}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && itemId ? `/items/${itemId}` : '/items')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Shirt className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Item details</h2>\n <p className=\"text-xs text-text-secondary\">\n A piece belongs to a brand and optionally a collection.\n </p>\n </div>\n </div>\n\n {!isEdit && (\n <SelectField\n id=\"brandId\"\n label=\"Brand\"\n value={form.brandId}\n onChange={(v) => setField('brandId', v)}\n options={brandOptions}\n required\n placeholder={brands.isLoading ? 'Loading brands…' : 'Select a brand'}\n error={errors.brandId}\n className=\"mb-4\"\n />\n )}\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"name\"\n label=\"Name\"\n value={form.name}\n onChange={(v) => setField('name', v)}\n placeholder=\"Silk slip dress\"\n required\n error={errors.name}\n />\n <SelectField\n id=\"category\"\n label=\"Category\"\n value={form.category}\n onChange={(v) => setField('category', v as CooStyleCategory)}\n options={categoryOptions}\n />\n {isEdit && form.status !== 'RETIRED' && (\n <SelectField\n id=\"status\"\n label=\"Sale status\"\n value={form.status}\n onChange={(v) => setField('status', v as CooStyleItemStatus)}\n options={SALE_STATUS_OPTIONS}\n hint=\"While stock is tracked, On sale and Out of stock follow the stock\"\n />\n )}\n </div>\n\n <TextAreaField\n id=\"description\"\n label=\"Description\"\n value={form.description}\n onChange={(v) => setField('description', v)}\n placeholder=\"Describe the piece…\"\n rows={3}\n className=\"mt-4\"\n />\n\n <ImageUploadField\n label=\"Product photo\"\n value={form.imageFileId}\n onChange={(id) => setField('imageFileId', id)}\n folder={COO_FILE_FOLDERS.items}\n hint=\"Shown across the catalogue and item page\"\n className=\"mt-4\"\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <TextField\n id=\"price\"\n label=\"Price\"\n type=\"number\"\n value={form.price}\n onChange={(v) => setField('price', v)}\n placeholder=\"0.00\"\n error={errors.price}\n />\n <TextField\n id=\"currency\"\n label=\"Currency\"\n value={form.currency}\n onChange={(v) => setField('currency', v)}\n placeholder=\"USD\"\n />\n <TextField\n id=\"sku\"\n label=\"SKU\"\n value={form.sku}\n onChange={(v) => setField('sku', v)}\n placeholder=\"ABC-123\"\n />\n </div>\n\n <SelectField\n id=\"collectionId\"\n label=\"Collection (optional)\"\n value={form.collectionId}\n onChange={(v) => setField('collectionId', v)}\n options={collectionOptions}\n placeholder={\n !form.brandId\n ? 'Select a brand first'\n : collections.isLoading\n ? 'Loading collections…'\n : 'No collection'\n }\n className=\"mt-4\"\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"colours\"\n label=\"Colours\"\n value={form.colours}\n onChange={(v) => setField('colours', v)}\n placeholder=\"black, ivory\"\n hint=\"Comma-separated\"\n />\n <TextField\n id=\"sizes\"\n label=\"Sizes\"\n value={form.sizes}\n onChange={(v) => setField('sizes', v)}\n placeholder=\"XS, S, M, L\"\n hint=\"Comma-separated\"\n />\n <TextField\n id=\"materials\"\n label=\"Materials\"\n value={form.materials}\n onChange={(v) => setField('materials', v)}\n placeholder=\"silk, cotton\"\n hint=\"Comma-separated\"\n />\n <TextField\n id=\"tags\"\n label=\"Tags\"\n value={form.tags}\n onChange={(v) => setField('tags', v)}\n placeholder=\"summer, party\"\n hint=\"Comma-separated\"\n />\n </div>\n\n {mutation.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {mutation.error instanceof Error ? mutation.error.message : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"submit\"\n disabled={mutation.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {mutation.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {isEdit ? 'Save changes' : 'Create item'}\n </button>\n </div>\n </GlassCard>\n </form>\n {isEdit && itemId && form.status !== 'RETIRED' && (\n <div className=\"mx-auto mt-6 max-w-2xl\">\n <StockEditor styleItemId={itemId} />\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StyleItemFormPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/items/StyleItemFormPage.tsx"],"sourcesContent":["/**\n * Style Item Create / Edit Page\n * @module coolandlovely/pages/items\n *\n * Drives `cooCreateStyleItem` (new) and `cooUpdateStyleItem` (edit).\n * Brand selection is required on create (and locked on edit). The collection\n * select is scoped to the chosen brand.\n */\n\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams, useSearchParams } from 'react-router';\nimport { ArrowLeft, Loader2, Save, Shirt } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { TextField, TextAreaField, SelectField } from '../../components/Field';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { StockEditor } from '../../components/StockEditor';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport {\n useStyleItem,\n useCreateStyleItem,\n useUpdateStyleItem,\n useBrands,\n useCollections,\n} from '../../hooks';\nimport { COO_FILE_FOLDERS } from '../../hooks/useFiles';\nimport { STYLE_CATEGORIES } from '../../utils/icons';\nimport { parseCsv, titleCase } from '../../utils/formatters';\nimport type { CooStyleCategory, CooStyleItemStatus } from '../../types';\n\ninterface FormState {\n brandId: string;\n collectionId: string;\n name: string;\n description: string;\n category: CooStyleCategory;\n sku: string;\n price: string;\n currency: string;\n colours: string;\n sizes: string;\n materials: string;\n tags: string;\n imageFileId: string;\n /** Sale status (edit only). RETIRED is archive-only and never offered here. */\n status: CooStyleItemStatus;\n}\n\n/** Sale statuses a brand can set; checkout only sells ACTIVE pieces (BOFF-3370). */\nconst SALE_STATUS_OPTIONS: Array<{ value: CooStyleItemStatus; label: string }> = [\n { value: 'DRAFT', label: 'Draft (not on sale)' },\n { value: 'ACTIVE', label: 'On sale' },\n { value: 'OUT_OF_STOCK', label: 'Out of stock' },\n];\n\nconst EMPTY: FormState = {\n brandId: '',\n collectionId: '',\n name: '',\n description: '',\n category: 'OTHER',\n sku: '',\n price: '',\n currency: 'USD',\n colours: '',\n sizes: '',\n materials: '',\n tags: '',\n imageFileId: '',\n status: 'DRAFT',\n};\n\nexport function StyleItemFormPage() {\n const { itemId } = useParams<{ itemId: string }>();\n const [searchParams] = useSearchParams();\n const isEdit = Boolean(itemId);\n const { navigateTo } = useCoolAndLovely();\n\n const existing = useStyleItem(isEdit ? itemId : undefined);\n const brands = useBrands({ pagination: { limit: 100, offset: 0 } });\n const createItem = useCreateStyleItem();\n const updateItem = useUpdateStyleItem();\n\n const [form, setForm] = useState<FormState>(() => ({\n ...EMPTY,\n brandId: searchParams.get('brandId') ?? '',\n collectionId: searchParams.get('collectionId') ?? '',\n }));\n const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});\n\n // Collections scoped to the selected brand (for the optional collection select)\n const collections = useCollections(form.brandId ? { brandId: form.brandId } : {});\n\n useEffect(() => {\n if (isEdit && existing.data) {\n const it = existing.data;\n setForm({\n brandId: it.brandId,\n collectionId: it.collectionId ?? '',\n name: it.name,\n description: it.description ?? '',\n category: it.category,\n sku: it.sku ?? '',\n price: String(it.price),\n currency: it.currency,\n colours: it.colours.join(', '),\n sizes: it.sizes.join(', '),\n materials: it.materials.join(', '),\n tags: it.tags.join(', '),\n imageFileId: it.image?.id ?? '',\n status: it.status,\n });\n }\n }, [isEdit, existing.data]);\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((f) => ({ ...f, [key]: value }));\n\n const mutation = isEdit ? updateItem : createItem;\n\n const brandOptions = useMemo(\n () => (brands.data?.items ?? []).map((b) => ({ value: b.id, label: b.name })),\n [brands.data]\n );\n const collectionOptions = useMemo(\n () => (collections.data ?? []).map((c) => ({ value: c.id, label: c.name })),\n [collections.data]\n );\n const categoryOptions = STYLE_CATEGORIES.map((c) => ({ value: c, label: titleCase(c) }));\n\n const validate = (): boolean => {\n const next: Partial<Record<keyof FormState, string>> = {};\n if (!form.name.trim()) next.name = 'Name is required';\n if (!isEdit && !form.brandId) next.brandId = 'Brand is required';\n if (form.price && Number.isNaN(Number(form.price))) next.price = 'Price must be a number';\n setErrors(next);\n return Object.keys(next).length === 0;\n };\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (!validate()) return;\n\n const priceNum = form.price.trim() ? Number(form.price) : undefined;\n\n if (isEdit && itemId) {\n updateItem.mutate(\n {\n id: itemId,\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n category: form.category,\n collectionId: form.collectionId || undefined,\n sku: form.sku.trim() || undefined,\n price: priceNum,\n currency: form.currency.trim() || undefined,\n colours: parseCsv(form.colours),\n sizes: parseCsv(form.sizes),\n materials: parseCsv(form.materials),\n tags: parseCsv(form.tags),\n imageFileId: form.imageFileId || undefined,\n // A retired piece keeps its terminal status; only live statuses are sent.\n status: form.status === 'RETIRED' ? undefined : form.status,\n },\n },\n { onSuccess: () => navigateTo(`/items/${itemId}`) }\n );\n } else {\n createItem.mutate(\n {\n brandId: form.brandId,\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n category: form.category,\n collectionId: form.collectionId || undefined,\n sku: form.sku.trim() || undefined,\n price: priceNum,\n currency: form.currency.trim() || undefined,\n colours: parseCsv(form.colours),\n sizes: parseCsv(form.sizes),\n materials: parseCsv(form.materials),\n tags: parseCsv(form.tags),\n imageFileId: form.imageFileId || undefined,\n },\n { onSuccess: (item) => navigateTo(`/items/${item.id}`) }\n );\n }\n };\n\n if (isEdit && existing.isLoading) {\n return (\n <PageLayout title=\"Edit item\">\n <LoadingView label=\"Loading item…\" />\n </PageLayout>\n );\n }\n\n if (isEdit && (existing.isError || !existing.data)) {\n return (\n <PageLayout title=\"Edit item\">\n <ErrorView title=\"Item not found\" onRetry={() => void existing.refetch()} />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={isEdit ? 'Edit item' : 'New style item'}\n description={isEdit ? 'Update this catalogue piece' : 'List a new shoppable piece'}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && itemId ? `/items/${itemId}` : '/items')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <Shirt className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Item details</h2>\n <p className=\"text-xs text-text-secondary\">\n A piece belongs to a brand and optionally a collection.\n </p>\n </div>\n </div>\n\n {!isEdit && (\n <SelectField\n id=\"brandId\"\n label=\"Brand\"\n value={form.brandId}\n onChange={(v) => setField('brandId', v)}\n options={brandOptions}\n required\n placeholder={brands.isLoading ? 'Loading brands…' : 'Select a brand'}\n error={errors.brandId}\n className=\"mb-4\"\n />\n )}\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"name\"\n label=\"Name\"\n value={form.name}\n onChange={(v) => setField('name', v)}\n placeholder=\"Silk slip dress\"\n required\n error={errors.name}\n />\n <SelectField\n id=\"category\"\n label=\"Category\"\n value={form.category}\n onChange={(v) => setField('category', v as CooStyleCategory)}\n options={categoryOptions}\n />\n {isEdit && form.status !== 'RETIRED' && (\n <SelectField\n id=\"status\"\n label=\"Sale status\"\n value={form.status}\n onChange={(v) => setField('status', v as CooStyleItemStatus)}\n options={SALE_STATUS_OPTIONS}\n hint=\"While stock is tracked, On sale and Out of stock follow the stock\"\n />\n )}\n </div>\n\n <TextAreaField\n id=\"description\"\n label=\"Description\"\n value={form.description}\n onChange={(v) => setField('description', v)}\n placeholder=\"Describe the piece…\"\n rows={3}\n className=\"mt-4\"\n />\n\n <ImageUploadField\n label=\"Product photo\"\n value={form.imageFileId}\n onChange={(id) => setField('imageFileId', id)}\n folder={COO_FILE_FOLDERS.items}\n hint=\"Shown across the catalogue and item page\"\n className=\"mt-4\"\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <TextField\n id=\"price\"\n label=\"Price\"\n type=\"number\"\n value={form.price}\n onChange={(v) => setField('price', v)}\n placeholder=\"0.00\"\n error={errors.price}\n />\n <TextField\n id=\"currency\"\n label=\"Currency\"\n value={form.currency}\n onChange={(v) => setField('currency', v)}\n placeholder=\"USD\"\n />\n <TextField\n id=\"sku\"\n label=\"SKU\"\n value={form.sku}\n onChange={(v) => setField('sku', v)}\n placeholder=\"ABC-123\"\n />\n </div>\n\n <SelectField\n id=\"collectionId\"\n label=\"Collection (optional)\"\n value={form.collectionId}\n onChange={(v) => setField('collectionId', v)}\n options={collectionOptions}\n placeholder={\n !form.brandId\n ? 'Select a brand first'\n : collections.isLoading\n ? 'Loading collections…'\n : 'No collection'\n }\n className=\"mt-4\"\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"colours\"\n label=\"Colours\"\n value={form.colours}\n onChange={(v) => setField('colours', v)}\n placeholder=\"black, ivory\"\n hint=\"Comma-separated\"\n />\n <TextField\n id=\"sizes\"\n label=\"Sizes\"\n value={form.sizes}\n onChange={(v) => setField('sizes', v)}\n placeholder=\"XS, S, M, L\"\n hint=\"Comma-separated\"\n />\n <TextField\n id=\"materials\"\n label=\"Materials\"\n value={form.materials}\n onChange={(v) => setField('materials', v)}\n placeholder=\"silk, cotton\"\n hint=\"Comma-separated\"\n />\n <TextField\n id=\"tags\"\n label=\"Tags\"\n value={form.tags}\n onChange={(v) => setField('tags', v)}\n placeholder=\"summer, party\"\n hint=\"Comma-separated\"\n />\n </div>\n\n {mutation.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {mutation.error instanceof Error ? mutation.error.message : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"submit\"\n disabled={mutation.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {mutation.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {isEdit ? 'Save changes' : 'Create item'}\n </button>\n </div>\n </GlassCard>\n </form>\n {isEdit && itemId && form.status !== 'RETIRED' && (\n <div className=\"mx-auto mt-6 max-w-2xl\">\n <StockEditor styleItemId={itemId} />\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StyleItemsListPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/items/StyleItemsListPage.tsx"],"sourcesContent":["/**\n * Style Items List Page\n * @module coolandlovely/pages/items\n *\n * Cursor-paginated catalogue (`cooListStyleItems`) with category & status\n * filters. Cards link to the item detail page.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, ChevronLeft, ChevronRight, Shirt } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { ColourSwatchRow } from '../../components/ColourSwatchRow';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useStyleItems, useTr } from '../../hooks';\nimport { categoryIcon, STYLE_CATEGORIES } from '../../utils/icons';\nimport { formatPrice, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooStyleCategory, CooStyleItemStatus } from '../../types';\n\nconst PAGE_SIZE = 24;\nconst STATUS_ORDER: CooStyleItemStatus[] = ['ACTIVE', 'DRAFT', 'OUT_OF_STOCK', 'RETIRED'];\n\nfunction formatStatusLabel(status: string): string {\n return status.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nexport function StyleItemsListPage() {\n const { navigateTo } = useCoolAndLovely();\n const tr = useTr();\n const [category, setCategory] = useState<CooStyleCategory | 'ALL'>('ALL');\n const [status, setStatus] = useState<CooStyleItemStatus | 'ALL'>('ALL');\n const [offset, setOffset] = useState(0);\n\n const statusOptions = useMemo(\n () => [\n { value: 'ALL' as const, label: tr('coolandlovely.filters.all', 'All') },\n ...STATUS_ORDER.map((value) => ({\n value,\n label: tr(`coolandlovely.status.${value.toLowerCase()}`, formatStatusLabel(value)),\n })),\n ],\n [tr]\n );\n\n const query = useStyleItems({\n category: category === 'ALL' ? undefined : category,\n status: status === 'ALL' ? undefined : status,\n pagination: { limit: PAGE_SIZE, offset },\n });\n\n const items = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title={tr('coolandlovely.page.styleItemsTitle', 'Style Items')}\n description={tr(\n 'coolandlovely.page.styleItemsSubtitle',\n 'The shoppable catalogue across every brand'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/items/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('coolandlovely.actions.newItem', 'New item')}\n </button>\n }\n >\n {/* Status filter */}\n <div className=\"mb-3 flex flex-wrap items-center gap-2\">\n {statusOptions.map((opt) => (\n <button\n key={opt.value}\n type=\"button\"\n onClick={() => {\n setStatus(opt.value);\n setOffset(0);\n }}\n className={cn(\n 'rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n status === opt.value\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {opt.label}\n </button>\n ))}\n </div>\n\n {/* Category filter */}\n <div className=\"mb-6 flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setCategory('ALL');\n setOffset(0);\n }}\n className={cn(\n 'rounded-full px-3 py-1.5 text-xs font-medium transition-colors',\n category === 'ALL'\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {tr('coolandlovely.filters.allCategories', 'All categories')}\n </button>\n {STYLE_CATEGORIES.map((c) => (\n <button\n key={c}\n type=\"button\"\n onClick={() => {\n setCategory(c);\n setOffset(0);\n }}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors',\n category === c\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {categoryIcon(c, 14)} {tr(`coolandlovely.category.${c.toLowerCase()}`, titleCase(c))}\n </button>\n ))}\n </div>\n\n {query.isLoading ? (\n <LoadingView label={tr('coolandlovely.loading.catalogue', 'Loading catalogue…')} />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Shirt className=\"h-10 w-10\" />}\n title={tr('coolandlovely.empty.itemsTitle', 'No items found')}\n description={tr(\n 'coolandlovely.empty.itemsDescription',\n 'No style items match these filters yet.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/items/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('coolandlovely.actions.newItem', 'New item')}\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4\">\n {items.map((item) => (\n <button\n key={item.id}\n type=\"button\"\n onClick={() => navigateTo(`/items/${item.id}`)}\n className=\"group overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left shadow-sm transition-all hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <div className=\"relative\">\n <Thumbnail\n icon={categoryIcon(item.category, 32)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"aspect-[3/4] w-full\"\n />\n <div className=\"absolute right-2 top-2\">\n <StatusBadge status={item.status} className=\"shadow-sm\" />\n </div>\n </div>\n <div className=\"p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{item.name}</p>\n {item.brand?.name && (\n <p className=\"truncate text-xs text-text-secondary\">{item.brand.name}</p>\n )}\n <div className=\"mt-1.5 flex items-center justify-between gap-2\">\n <span className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {formatPrice(item.price, item.currency)}\n </span>\n <span className=\"text-xs text-text-secondary\">{titleCase(item.category)}</span>\n </div>\n {item.colours.length > 0 && (\n <ColourSwatchRow colours={item.colours} max={5} className=\"mt-2\" />\n )}\n </div>\n </button>\n ))}\n </div>\n\n {(offset > 0 || hasMore) && (\n <div className=\"mt-6 flex items-center justify-center gap-2\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n <ChevronLeft className=\"h-4 w-4\" />{' '}\n {tr('coolandlovely.pagination.previous', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.pagination.page', 'Page {{page}}', {\n page: offset / PAGE_SIZE + 1,\n })}\n </span>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset((o) => o + PAGE_SIZE)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n {tr('coolandlovely.pagination.next', 'Next')} <ChevronRight className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StyleItemsListPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/items/StyleItemsListPage.tsx"],"sourcesContent":["/**\n * Style Items List Page\n * @module coolandlovely/pages/items\n *\n * Cursor-paginated catalogue (`cooListStyleItems`) with category & status\n * filters. Cards link to the item detail page.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, ChevronLeft, ChevronRight, Shirt } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { ColourSwatchRow } from '../../components/ColourSwatchRow';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useStyleItems, useTr } from '../../hooks';\nimport { categoryIcon, STYLE_CATEGORIES } from '../../utils/icons';\nimport { formatPrice, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooStyleCategory, CooStyleItemStatus } from '../../types';\n\nconst PAGE_SIZE = 24;\nconst STATUS_ORDER: CooStyleItemStatus[] = ['ACTIVE', 'DRAFT', 'OUT_OF_STOCK', 'RETIRED'];\n\nfunction formatStatusLabel(status: string): string {\n return status.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nexport function StyleItemsListPage() {\n const { navigateTo } = useCoolAndLovely();\n const tr = useTr();\n const [category, setCategory] = useState<CooStyleCategory | 'ALL'>('ALL');\n const [status, setStatus] = useState<CooStyleItemStatus | 'ALL'>('ALL');\n const [offset, setOffset] = useState(0);\n\n const statusOptions = useMemo(\n () => [\n { value: 'ALL' as const, label: tr('coolandlovely.filters.all', 'All') },\n ...STATUS_ORDER.map((value) => ({\n value,\n label: tr(`coolandlovely.status.${value.toLowerCase()}`, formatStatusLabel(value)),\n })),\n ],\n [tr]\n );\n\n const query = useStyleItems({\n category: category === 'ALL' ? undefined : category,\n status: status === 'ALL' ? undefined : status,\n pagination: { limit: PAGE_SIZE, offset },\n });\n\n const items = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title={tr('coolandlovely.page.styleItemsTitle', 'Style Items')}\n description={tr(\n 'coolandlovely.page.styleItemsSubtitle',\n 'The shoppable catalogue across every brand'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/items/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('coolandlovely.actions.newItem', 'New item')}\n </button>\n }\n >\n {/* Status filter */}\n <div className=\"mb-3 flex flex-wrap items-center gap-2\">\n {statusOptions.map((opt) => (\n <button\n key={opt.value}\n type=\"button\"\n onClick={() => {\n setStatus(opt.value);\n setOffset(0);\n }}\n className={cn(\n 'rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n status === opt.value\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {opt.label}\n </button>\n ))}\n </div>\n\n {/* Category filter */}\n <div className=\"mb-6 flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setCategory('ALL');\n setOffset(0);\n }}\n className={cn(\n 'rounded-full px-3 py-1.5 text-xs font-medium transition-colors',\n category === 'ALL'\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {tr('coolandlovely.filters.allCategories', 'All categories')}\n </button>\n {STYLE_CATEGORIES.map((c) => (\n <button\n key={c}\n type=\"button\"\n onClick={() => {\n setCategory(c);\n setOffset(0);\n }}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors',\n category === c\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {categoryIcon(c, 14)} {tr(`coolandlovely.category.${c.toLowerCase()}`, titleCase(c))}\n </button>\n ))}\n </div>\n\n {query.isLoading ? (\n <LoadingView label={tr('coolandlovely.loading.catalogue', 'Loading catalogue…')} />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Shirt className=\"h-10 w-10\" />}\n title={tr('coolandlovely.empty.itemsTitle', 'No items found')}\n description={tr(\n 'coolandlovely.empty.itemsDescription',\n 'No style items match these filters yet.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/items/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('coolandlovely.actions.newItem', 'New item')}\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4\">\n {items.map((item) => (\n <button\n key={item.id}\n type=\"button\"\n onClick={() => navigateTo(`/items/${item.id}`)}\n className=\"group overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left shadow-sm transition-all hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <div className=\"relative\">\n <Thumbnail\n icon={categoryIcon(item.category, 32)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"aspect-[3/4] w-full\"\n />\n <div className=\"absolute right-2 top-2\">\n <StatusBadge status={item.status} className=\"shadow-sm\" />\n </div>\n </div>\n <div className=\"p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{item.name}</p>\n {item.brand?.name && (\n <p className=\"truncate text-xs text-text-secondary\">{item.brand.name}</p>\n )}\n <div className=\"mt-1.5 flex items-center justify-between gap-2\">\n <span className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {formatPrice(item.price, item.currency)}\n </span>\n <span className=\"text-xs text-text-secondary\">{titleCase(item.category)}</span>\n </div>\n {item.colours.length > 0 && (\n <ColourSwatchRow colours={item.colours} max={5} className=\"mt-2\" />\n )}\n </div>\n </button>\n ))}\n </div>\n\n {(offset > 0 || hasMore) && (\n <div className=\"mt-6 flex items-center justify-center gap-2\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n <ChevronLeft className=\"h-4 w-4\" />{' '}\n {tr('coolandlovely.pagination.previous', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.pagination.page', 'Page {{page}}', {\n page: offset / PAGE_SIZE + 1,\n })}\n </span>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset((o) => o + PAGE_SIZE)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n {tr('coolandlovely.pagination.next', 'Next')} <ChevronRight className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LookbookDetailPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/lookbooks/LookbookDetailPage.tsx"],"sourcesContent":["/**\n * Lookbook Detail Page\n * @module coolandlovely/pages/lookbooks\n *\n * Single lookbook (`cooLookbook`) rendered as a Pinterest-style masonry board.\n * Each entry's `lookId` / `styleItemId` is resolved client-side against the\n * already-fetched looks/items lists to a real title + image, with an optional\n * note badge and a hover X-remove (`cooRemoveLookbookEntry`). New entries are\n * added inline (`cooAddLookbookEntry`).\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { ArrowLeft, BookHeart, Sparkles, Tag, Loader2, Plus, Trash2, X, Clock } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { TagList } from '../../components/TagList';\nimport { VisibilityChip } from '../../components/VisibilityChip';\nimport { SelectField, TextField } from '../../components/Field';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport {\n useLookbook,\n useRemoveLookbookEntry,\n useAddLookbookEntry,\n useDeleteLookbook,\n useLooks,\n useStyleItems,\n useAssistantEntityName,\n} from '../../hooks';\nimport { formatRelativeTime } from '../../utils/formatters';\n\ntype EntryKind = 'look' | 'item';\n\n/** Alternating aspect ratios give the board its masonry rhythm. */\nconst TILE_ASPECTS = ['aspect-[3/4]', 'aspect-square', 'aspect-[4/5]'];\n\nexport function LookbookDetailPage() {\n const { lookbookId } = useParams<{ lookbookId: string }>();\n const { navigateTo } = useCoolAndLovely();\n const query = useLookbook(lookbookId);\n const removeEntry = useRemoveLookbookEntry();\n const addEntry = useAddLookbookEntry();\n const deleteLookbook = useDeleteLookbook();\n // Name the board for the assistant's screen context.\n useAssistantEntityName(lookbookId, query.data?.name);\n\n const [adding, setAdding] = useState(false);\n const [entryKind, setEntryKind] = useState<EntryKind>('look');\n const [selectedId, setSelectedId] = useState('');\n const [note, setNote] = useState('');\n\n const looks = useLooks({ pagination: { limit: 100, offset: 0 } });\n const items = useStyleItems({ pagination: { limit: 100, offset: 0 } });\n\n const lookById = useMemo(\n () => new Map((looks.data?.items ?? []).map((l) => [l.id, l])),\n [looks.data]\n );\n const itemById = useMemo(\n () => new Map((items.data?.items ?? []).map((i) => [i.id, i])),\n [items.data]\n );\n const usableItems = useMemo(\n () => (items.data?.items ?? []).filter((i) => i.status !== 'RETIRED'),\n [items.data]\n );\n\n const lookOptions = useMemo(\n () => (looks.data?.items ?? []).map((l) => ({ value: l.id, label: l.title })),\n [looks.data]\n );\n const itemOptions = useMemo(\n () =>\n usableItems.map((i) => ({\n value: i.id,\n label: `${i.name}${i.brand?.name ? ` · ${i.brand.name}` : ''}`,\n })),\n [usableItems]\n );\n\n if (query.isLoading) {\n return (\n <PageLayout title=\"Lookbook\">\n <LoadingView label=\"Loading lookbook…\" />\n </PageLayout>\n );\n }\n\n if (query.isError || !query.data) {\n return (\n <PageLayout title=\"Lookbook\">\n <ErrorView title=\"Lookbook not found\" onRetry={() => void query.refetch()} />\n </PageLayout>\n );\n }\n\n const lookbook = query.data;\n const entries = [...(lookbook.entries ?? [])].sort((a, b) => a.position - b.position);\n\n const handleAddEntry = (e: React.FormEvent) => {\n e.preventDefault();\n if (!lookbookId || !selectedId) return;\n addEntry.mutate(\n {\n lookbookId,\n lookId: entryKind === 'look' ? selectedId : undefined,\n styleItemId: entryKind === 'item' ? selectedId : undefined,\n note: note.trim() || undefined,\n },\n {\n onSuccess: () => {\n setSelectedId('');\n setNote('');\n setAdding(false);\n },\n }\n );\n };\n\n return (\n <PageLayout\n title={lookbook.name}\n description={lookbook.description ?? undefined}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/lookbooks')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setAdding((v) => !v);\n setSelectedId('');\n }}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n {adding ? <X className=\"h-4 w-4\" /> : <Plus className=\"h-4 w-4\" />}\n {adding ? 'Close' : 'Add entry'}\n </button>\n <button\n type=\"button\"\n disabled={deleteLookbook.isPending}\n onClick={() => {\n if (!lookbookId) return;\n deleteLookbook.mutate(lookbookId, { onSuccess: () => navigateTo('/lookbooks') });\n }}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-50\"\n >\n {deleteLookbook.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Trash2 className=\"h-4 w-4\" />\n )}\n Delete\n </button>\n </div>\n }\n >\n <div className=\"mx-auto max-w-5xl\">\n <GlassCard treatment=\"glass\" className=\"p-6\">\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-accent-purple-subtle text-text-primary\">\n <BookHeart className=\"h-6 w-6\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <h2 className=\"truncate text-xl font-semibold text-text-primary\">{lookbook.name}</h2>\n <div className=\"mt-1.5 flex flex-wrap items-center gap-2\">\n <VisibilityChip visibility={lookbook.visibility} />\n <span className=\"inline-flex items-center gap-1 text-xs text-text-secondary\">\n <Clock className=\"h-3.5 w-3.5\" /> Updated {formatRelativeTime(lookbook.updatedAt)}\n </span>\n <span className=\"text-xs text-text-secondary tabular-nums\">\n · {entries.length} {entries.length === 1 ? 'item' : 'items'}\n </span>\n </div>\n </div>\n </div>\n {lookbook.tags.length > 0 && <TagList tags={lookbook.tags} className=\"mt-4\" />}\n </GlassCard>\n\n {adding && (\n <form onSubmit={handleAddEntry} className=\"mt-6\">\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"mb-4 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <Plus className=\"h-5 w-5\" /> Add to this lookbook\n </h3>\n <div className=\"mb-4 flex flex-wrap gap-2\">\n {(['look', 'item'] as EntryKind[]).map((k) => (\n <button\n key={k}\n type=\"button\"\n onClick={() => {\n setEntryKind(k);\n setSelectedId('');\n }}\n className={\n entryKind === k\n ? 'rounded-full bg-action-primary-bg px-3 py-1.5 text-sm font-medium text-action-primary-text'\n : 'rounded-full bg-bg-sunken px-3 py-1.5 text-sm font-medium text-text-secondary hover:text-text-primary'\n }\n >\n {k === 'look' ? 'Save a look' : 'Clip a piece'}\n </button>\n ))}\n </div>\n <SelectField\n label={entryKind === 'look' ? 'Look' : 'Style item'}\n value={selectedId}\n onChange={setSelectedId}\n options={entryKind === 'look' ? lookOptions : itemOptions}\n placeholder={\n entryKind === 'look'\n ? looks.isLoading\n ? 'Loading looks…'\n : 'Select a look'\n : items.isLoading\n ? 'Loading items…'\n : 'Select a style item'\n }\n />\n <TextField\n label=\"Note (optional)\"\n value={note}\n onChange={setNote}\n placeholder=\"Why you saved it…\"\n className=\"mt-4\"\n />\n {addEntry.isError && (\n <p className=\"mt-3 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {addEntry.error instanceof Error ? addEntry.error.message : 'Could not add entry'}\n </p>\n )}\n <div className=\"mt-4 flex justify-end\">\n <button\n type=\"submit\"\n disabled={!selectedId || addEntry.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {addEntry.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n Add entry\n </button>\n </div>\n </GlassCard>\n </form>\n )}\n\n <section className=\"mt-6\">\n <h3 className=\"mb-3 text-lg font-semibold text-text-primary\">Board ({entries.length})</h3>\n {entries.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n This lookbook has no entries yet. Clip a catalogue piece or save a look to add one.\n </p>\n ) : (\n <div className=\"columns-2 gap-4 md:columns-3\">\n {entries.map((entry, idx) => {\n const isLook = Boolean(entry.lookId);\n const look = entry.lookId ? lookById.get(entry.lookId) : undefined;\n const item = entry.styleItemId ? itemById.get(entry.styleItemId) : undefined;\n const title = isLook\n ? (look?.title ?? 'Saved look')\n : (item?.name ?? 'Clipped piece');\n const imageFileId = isLook ? look?.image?.id : item?.image?.id;\n const removing = removeEntry.isPending && removeEntry.variables === entry.id;\n const onOpen = () => {\n if (isLook && entry.lookId) navigateTo(`/looks/${entry.lookId}`);\n else if (entry.styleItemId) navigateTo(`/items/${entry.styleItemId}`);\n };\n return (\n <div\n key={entry.id}\n className=\"group relative mb-4 break-inside-avoid overflow-hidden rounded-xl border border-border-seam bg-bg-surface shadow-sm transition-shadow hover:shadow-lg\"\n >\n <button\n type=\"button\"\n onClick={onOpen}\n aria-label={`Open ${title}`}\n className=\"block w-full text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Thumbnail\n icon={\n isLook ? <Sparkles className=\"h-8 w-8\" /> : <Tag className=\"h-8 w-8\" />\n }\n seed={entry.id}\n fileId={imageFileId}\n alt={title}\n className={`w-full rounded-none ${TILE_ASPECTS[idx % TILE_ASPECTS.length]}`}\n overlay={\n <div className=\"pointer-events-none absolute inset-x-0 bottom-0 flex flex-col gap-1.5 bg-gradient-to-t from-bg-canvas/90 via-bg-canvas/40 to-transparent p-3 pt-10\">\n {entry.note && (\n <span className=\"inline-flex w-fit max-w-full items-center rounded-md bg-bg-canvas/80 px-2 py-0.5 text-[11px] font-medium text-text-primary backdrop-blur\">\n <span className=\"truncate\">{entry.note}</span>\n </span>\n )}\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {title}\n </p>\n </div>\n }\n />\n </button>\n <button\n type=\"button\"\n disabled={removeEntry.isPending}\n onClick={() => removeEntry.mutate(entry.id)}\n aria-label={`Remove ${title}`}\n className=\"absolute right-2 top-2 z-10 inline-flex h-7 w-7 items-center justify-center rounded-full bg-bg-surface/90 text-text-secondary opacity-100 shadow-sm backdrop-blur transition-opacity hover:text-status-error-text sm:opacity-0 sm:focus-visible:opacity-100 sm:group-hover:opacity-100 disabled:opacity-60\"\n >\n {removing ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n ) : (\n <X className=\"h-3.5 w-3.5\" />\n )}\n </button>\n </div>\n );\n })}\n </div>\n )}\n </section>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"LookbookDetailPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/lookbooks/LookbookDetailPage.tsx"],"sourcesContent":["/**\n * Lookbook Detail Page\n * @module coolandlovely/pages/lookbooks\n *\n * Single lookbook (`cooLookbook`) rendered as a Pinterest-style masonry board.\n * Each entry's `lookId` / `styleItemId` is resolved client-side against the\n * already-fetched looks/items lists to a real title + image, with an optional\n * note badge and a hover X-remove (`cooRemoveLookbookEntry`). New entries are\n * added inline (`cooAddLookbookEntry`).\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { ArrowLeft, BookHeart, Sparkles, Tag, Loader2, Plus, Trash2, X, Clock } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { TagList } from '../../components/TagList';\nimport { VisibilityChip } from '../../components/VisibilityChip';\nimport { SelectField, TextField } from '../../components/Field';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport {\n useLookbook,\n useRemoveLookbookEntry,\n useAddLookbookEntry,\n useDeleteLookbook,\n useLooks,\n useStyleItems,\n useAssistantEntityName,\n} from '../../hooks';\nimport { formatRelativeTime } from '../../utils/formatters';\n\ntype EntryKind = 'look' | 'item';\n\n/** Alternating aspect ratios give the board its masonry rhythm. */\nconst TILE_ASPECTS = ['aspect-[3/4]', 'aspect-square', 'aspect-[4/5]'];\n\nexport function LookbookDetailPage() {\n const { lookbookId } = useParams<{ lookbookId: string }>();\n const { navigateTo } = useCoolAndLovely();\n const query = useLookbook(lookbookId);\n const removeEntry = useRemoveLookbookEntry();\n const addEntry = useAddLookbookEntry();\n const deleteLookbook = useDeleteLookbook();\n // Name the board for the assistant's screen context.\n useAssistantEntityName(lookbookId, query.data?.name);\n\n const [adding, setAdding] = useState(false);\n const [entryKind, setEntryKind] = useState<EntryKind>('look');\n const [selectedId, setSelectedId] = useState('');\n const [note, setNote] = useState('');\n\n const looks = useLooks({ pagination: { limit: 100, offset: 0 } });\n const items = useStyleItems({ pagination: { limit: 100, offset: 0 } });\n\n const lookById = useMemo(\n () => new Map((looks.data?.items ?? []).map((l) => [l.id, l])),\n [looks.data]\n );\n const itemById = useMemo(\n () => new Map((items.data?.items ?? []).map((i) => [i.id, i])),\n [items.data]\n );\n const usableItems = useMemo(\n () => (items.data?.items ?? []).filter((i) => i.status !== 'RETIRED'),\n [items.data]\n );\n\n const lookOptions = useMemo(\n () => (looks.data?.items ?? []).map((l) => ({ value: l.id, label: l.title })),\n [looks.data]\n );\n const itemOptions = useMemo(\n () =>\n usableItems.map((i) => ({\n value: i.id,\n label: `${i.name}${i.brand?.name ? ` · ${i.brand.name}` : ''}`,\n })),\n [usableItems]\n );\n\n if (query.isLoading) {\n return (\n <PageLayout title=\"Lookbook\">\n <LoadingView label=\"Loading lookbook…\" />\n </PageLayout>\n );\n }\n\n if (query.isError || !query.data) {\n return (\n <PageLayout title=\"Lookbook\">\n <ErrorView title=\"Lookbook not found\" onRetry={() => void query.refetch()} />\n </PageLayout>\n );\n }\n\n const lookbook = query.data;\n const entries = [...(lookbook.entries ?? [])].sort((a, b) => a.position - b.position);\n\n const handleAddEntry = (e: React.FormEvent) => {\n e.preventDefault();\n if (!lookbookId || !selectedId) return;\n addEntry.mutate(\n {\n lookbookId,\n lookId: entryKind === 'look' ? selectedId : undefined,\n styleItemId: entryKind === 'item' ? selectedId : undefined,\n note: note.trim() || undefined,\n },\n {\n onSuccess: () => {\n setSelectedId('');\n setNote('');\n setAdding(false);\n },\n }\n );\n };\n\n return (\n <PageLayout\n title={lookbook.name}\n description={lookbook.description ?? undefined}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/lookbooks')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setAdding((v) => !v);\n setSelectedId('');\n }}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n {adding ? <X className=\"h-4 w-4\" /> : <Plus className=\"h-4 w-4\" />}\n {adding ? 'Close' : 'Add entry'}\n </button>\n <button\n type=\"button\"\n disabled={deleteLookbook.isPending}\n onClick={() => {\n if (!lookbookId) return;\n deleteLookbook.mutate(lookbookId, { onSuccess: () => navigateTo('/lookbooks') });\n }}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-50\"\n >\n {deleteLookbook.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Trash2 className=\"h-4 w-4\" />\n )}\n Delete\n </button>\n </div>\n }\n >\n <div className=\"mx-auto max-w-5xl\">\n <GlassCard treatment=\"glass\" className=\"p-6\">\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-accent-purple-subtle text-text-primary\">\n <BookHeart className=\"h-6 w-6\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <h2 className=\"truncate text-xl font-semibold text-text-primary\">{lookbook.name}</h2>\n <div className=\"mt-1.5 flex flex-wrap items-center gap-2\">\n <VisibilityChip visibility={lookbook.visibility} />\n <span className=\"inline-flex items-center gap-1 text-xs text-text-secondary\">\n <Clock className=\"h-3.5 w-3.5\" /> Updated {formatRelativeTime(lookbook.updatedAt)}\n </span>\n <span className=\"text-xs text-text-secondary tabular-nums\">\n · {entries.length} {entries.length === 1 ? 'item' : 'items'}\n </span>\n </div>\n </div>\n </div>\n {lookbook.tags.length > 0 && <TagList tags={lookbook.tags} className=\"mt-4\" />}\n </GlassCard>\n\n {adding && (\n <form onSubmit={handleAddEntry} className=\"mt-6\">\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"mb-4 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <Plus className=\"h-5 w-5\" /> Add to this lookbook\n </h3>\n <div className=\"mb-4 flex flex-wrap gap-2\">\n {(['look', 'item'] as EntryKind[]).map((k) => (\n <button\n key={k}\n type=\"button\"\n onClick={() => {\n setEntryKind(k);\n setSelectedId('');\n }}\n className={\n entryKind === k\n ? 'rounded-full bg-action-primary-bg px-3 py-1.5 text-sm font-medium text-action-primary-text'\n : 'rounded-full bg-bg-sunken px-3 py-1.5 text-sm font-medium text-text-secondary hover:text-text-primary'\n }\n >\n {k === 'look' ? 'Save a look' : 'Clip a piece'}\n </button>\n ))}\n </div>\n <SelectField\n label={entryKind === 'look' ? 'Look' : 'Style item'}\n value={selectedId}\n onChange={setSelectedId}\n options={entryKind === 'look' ? lookOptions : itemOptions}\n placeholder={\n entryKind === 'look'\n ? looks.isLoading\n ? 'Loading looks…'\n : 'Select a look'\n : items.isLoading\n ? 'Loading items…'\n : 'Select a style item'\n }\n />\n <TextField\n label=\"Note (optional)\"\n value={note}\n onChange={setNote}\n placeholder=\"Why you saved it…\"\n className=\"mt-4\"\n />\n {addEntry.isError && (\n <p className=\"mt-3 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {addEntry.error instanceof Error ? addEntry.error.message : 'Could not add entry'}\n </p>\n )}\n <div className=\"mt-4 flex justify-end\">\n <button\n type=\"submit\"\n disabled={!selectedId || addEntry.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {addEntry.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n Add entry\n </button>\n </div>\n </GlassCard>\n </form>\n )}\n\n <section className=\"mt-6\">\n <h3 className=\"mb-3 text-lg font-semibold text-text-primary\">Board ({entries.length})</h3>\n {entries.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n This lookbook has no entries yet. Clip a catalogue piece or save a look to add one.\n </p>\n ) : (\n <div className=\"columns-2 gap-4 md:columns-3\">\n {entries.map((entry, idx) => {\n const isLook = Boolean(entry.lookId);\n const look = entry.lookId ? lookById.get(entry.lookId) : undefined;\n const item = entry.styleItemId ? itemById.get(entry.styleItemId) : undefined;\n const title = isLook\n ? (look?.title ?? 'Saved look')\n : (item?.name ?? 'Clipped piece');\n const imageFileId = isLook ? look?.image?.id : item?.image?.id;\n const removing = removeEntry.isPending && removeEntry.variables === entry.id;\n const onOpen = () => {\n if (isLook && entry.lookId) navigateTo(`/looks/${entry.lookId}`);\n else if (entry.styleItemId) navigateTo(`/items/${entry.styleItemId}`);\n };\n return (\n <div\n key={entry.id}\n className=\"group relative mb-4 break-inside-avoid overflow-hidden rounded-xl border border-border-seam bg-bg-surface shadow-sm transition-shadow hover:shadow-lg\"\n >\n <button\n type=\"button\"\n onClick={onOpen}\n aria-label={`Open ${title}`}\n className=\"block w-full text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Thumbnail\n icon={\n isLook ? <Sparkles className=\"h-8 w-8\" /> : <Tag className=\"h-8 w-8\" />\n }\n seed={entry.id}\n fileId={imageFileId}\n alt={title}\n className={`w-full rounded-none ${TILE_ASPECTS[idx % TILE_ASPECTS.length]}`}\n overlay={\n <div className=\"pointer-events-none absolute inset-x-0 bottom-0 flex flex-col gap-1.5 bg-gradient-to-t from-bg-canvas/90 via-bg-canvas/40 to-transparent p-3 pt-10\">\n {entry.note && (\n <span className=\"inline-flex w-fit max-w-full items-center rounded-md bg-bg-canvas/80 px-2 py-0.5 text-[11px] font-medium text-text-primary backdrop-blur\">\n <span className=\"truncate\">{entry.note}</span>\n </span>\n )}\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {title}\n </p>\n </div>\n }\n />\n </button>\n <button\n type=\"button\"\n disabled={removeEntry.isPending}\n onClick={() => removeEntry.mutate(entry.id)}\n aria-label={`Remove ${title}`}\n className=\"absolute right-2 top-2 z-10 inline-flex h-7 w-7 items-center justify-center rounded-full bg-bg-surface/90 text-text-secondary opacity-100 shadow-sm backdrop-blur transition-opacity hover:text-status-error-text sm:opacity-0 sm:focus-visible:opacity-100 sm:group-hover:opacity-100 disabled:opacity-60\"\n >\n {removing ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n ) : (\n <X className=\"h-3.5 w-3.5\" />\n )}\n </button>\n </div>\n );\n })}\n </div>\n )}\n </section>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LookbooksListPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/lookbooks/LookbooksListPage.tsx"],"sourcesContent":["/**\n * Lookbooks List Page\n * @module coolandlovely/pages/lookbooks\n *\n * Lists themed lookbook boards (`cooListLookbooks`) as mood-board cards — a\n * cover, a visibility chip and (when the list selection carries entries) an\n * item count — and supports creating one inline (`cooCreateLookbook`).\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, BookHeart, Loader2, X } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LookbookCard } from '../../components/LookbookCard';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { TextField, TextAreaField, SelectField } from '../../components/Field';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useLookbooks, useCreateLookbook, useTr } from '../../hooks';\nimport { COO_FILE_FOLDERS } from '../../hooks/useFiles';\nimport { parseCsv, titleCase } from '../../utils/formatters';\nimport type { CooVisibility } from '../../types';\n\nconst VISIBILITIES: CooVisibility[] = ['PRIVATE', 'FOLLOWERS', 'PUBLIC'];\n\nexport function LookbooksListPage() {\n const { navigateTo } = useCoolAndLovely();\n const tr = useTr();\n const query = useLookbooks();\n const createLookbook = useCreateLookbook();\n\n const [creating, setCreating] = useState(false);\n const [name, setName] = useState('');\n const [description, setDescription] = useState('');\n const [visibility, setVisibility] = useState<CooVisibility>('PRIVATE');\n const [tags, setTags] = useState('');\n const [coverFileId, setCoverFileId] = useState('');\n const [nameError, setNameError] = useState<string | undefined>();\n\n const lookbooks = query.data ?? [];\n const visibilityOptions = useMemo(\n () =>\n VISIBILITIES.map((v) => ({\n value: v,\n label: tr(`coolandlovely.visibility.${v.toLowerCase()}`, titleCase(v)),\n })),\n [tr]\n );\n\n const handleCreate = (e: React.FormEvent) => {\n e.preventDefault();\n if (!name.trim()) {\n setNameError(tr('coolandlovely.validation.nameRequired', 'Name is required'));\n return;\n }\n setNameError(undefined);\n createLookbook.mutate(\n {\n name: name.trim(),\n description: description.trim() || undefined,\n visibility,\n tags: parseCsv(tags),\n coverFileId: coverFileId || undefined,\n },\n {\n onSuccess: (lb) => {\n setCreating(false);\n setName('');\n setDescription('');\n setTags('');\n setVisibility('PRIVATE');\n setCoverFileId('');\n navigateTo(`/lookbooks/${lb.id}`);\n },\n }\n );\n };\n\n return (\n <PageLayout\n title={tr('coolandlovely.page.lookbooksTitle', 'Lookbooks')}\n description={tr(\n 'coolandlovely.page.lookbooksSubtitle',\n 'Themed boards of saved catalogue pieces and looks'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => setCreating((v) => !v)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n {creating ? <X className=\"h-4 w-4\" /> : <Plus className=\"h-4 w-4\" />}\n {creating\n ? tr('coolandlovely.actions.close', 'Close')\n : tr('coolandlovely.actions.newLookbook', 'New lookbook')}\n </button>\n }\n >\n {creating && (\n <form onSubmit={handleCreate} className=\"mb-6\">\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h2 className=\"mb-4 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <BookHeart className=\"h-5 w-5\" />{' '}\n {tr('coolandlovely.actions.newLookbook', 'New lookbook')}\n </h2>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n label={tr('coolandlovely.field.name', 'Name')}\n value={name}\n onChange={setName}\n placeholder={tr('coolandlovely.placeholder.lookbookName', 'Summer wedding guest')}\n required\n error={nameError}\n />\n <SelectField\n label={tr('coolandlovely.field.visibility', 'Visibility')}\n value={visibility}\n onChange={(v) => setVisibility(v as CooVisibility)}\n options={visibilityOptions}\n />\n </div>\n <TextAreaField\n label={tr('coolandlovely.field.description', 'Description')}\n value={description}\n onChange={setDescription}\n placeholder={tr(\n 'coolandlovely.placeholder.lookbookDescription',\n 'What this board is for…'\n )}\n rows={2}\n className=\"mt-4\"\n />\n <TextField\n label={tr('coolandlovely.field.tags', 'Tags')}\n value={tags}\n onChange={setTags}\n placeholder={tr('coolandlovely.placeholder.tags', 'summer, formal')}\n hint={tr('coolandlovely.hint.commaSeparated', 'Comma-separated')}\n className=\"mt-4\"\n />\n <ImageUploadField\n label={tr('coolandlovely.field.coverImage', 'Cover image')}\n aspect=\"wide\"\n value={coverFileId}\n onChange={setCoverFileId}\n folder={COO_FILE_FOLDERS.lookbooks}\n className=\"mt-4\"\n />\n {createLookbook.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {createLookbook.error instanceof Error\n ? createLookbook.error.message\n : tr('coolandlovely.error.generic', 'Something went wrong')}\n </p>\n )}\n <div className=\"mt-4 flex justify-end\">\n <button\n type=\"submit\"\n disabled={createLookbook.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {createLookbook.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.actions.createLookbook', 'Create lookbook')}\n </button>\n </div>\n </GlassCard>\n </form>\n )}\n\n {query.isLoading ? (\n <LoadingView label={tr('coolandlovely.loading.lookbooks', 'Loading lookbooks…')} />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : lookbooks.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<BookHeart className=\"h-10 w-10\" />}\n title={tr('coolandlovely.empty.lookbooksTitle', 'No lookbooks yet')}\n description={tr(\n 'coolandlovely.empty.lookbooksDescription',\n 'Create a themed board to clip catalogue pieces and looks.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => setCreating(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('coolandlovely.actions.newLookbook', 'New lookbook')}\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3\">\n {lookbooks.map((lb) => (\n <LookbookCard\n key={lb.id}\n lookbook={lb}\n onClick={() => navigateTo(`/lookbooks/${lb.id}`)}\n />\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAwBA,IAAM,IAAgC;CAAC;CAAW;CAAa;CAAS;AAExE,SAAgB,IAAoB;CAClC,IAAM,EAAE,kBAAe,GAAkB,EACnC,IAAK,GAAO,EACZ,IAAQ,GAAc,EACtB,IAAiB,GAAmB,EAEpC,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAM,KAAW,EAAS,GAAG,EAC9B,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAY,KAAiB,EAAwB,UAAU,EAChE,CAAC,GAAM,KAAW,EAAS,GAAG,EAC9B,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAW,KAAgB,GAA8B,EAE1D,IAAY,EAAM,QAAQ,EAAE,EAC5B,IAAoB,QAEtB,EAAa,KAAK,OAAO;EACvB,OAAO;EACP,OAAO,EAAG,4BAA4B,EAAE,aAAa,IAAI,EAAU,EAAE,CAAC;EACvE,EAAE,EACL,CAAC,EAAG,CACL;AA+BD,QACE,kBAAC,GAAD;EACE,OAAO,EAAG,qCAAqC,YAAY;EAC3D,aAAa,EACX,wCACA,oDACD;EACD,SACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,GAAa,MAAM,CAAC,EAAE;GACrC,WAAU;aAHZ,CAKc,EAAX,IAAY,IAA4B,GAA7B,EAAG,WAAU,WAAY,CAA+B,EACnE,IACG,EAAG,+BAA+B,QAAQ,GAC1C,EAAG,qCAAqC,eAAe,CACpD;;YAhBb,CAmBG,KACC,kBAAC,QAAD;GAAM,WAlDU,MAAuB;AAE3C,QADA,EAAE,gBAAgB,EACd,CAAC,EAAK,MAAM,EAAE;AAChB,OAAa,EAAG,yCAAyC,mBAAmB,CAAC;AAC7E;;AAGF,IADA,EAAa,KAAA,EAAU,EACvB,EAAe,OACb;KACE,MAAM,EAAK,MAAM;KACjB,aAAa,EAAY,MAAM,IAAI,KAAA;KACnC;KACA,MAAM,EAAS,EAAK;KACpB,aAAa,KAAe,KAAA;KAC7B,EACD,EACE,YAAY,MAAO;AAOjB,KANA,EAAY,GAAM,EAClB,EAAQ,GAAG,EACX,EAAe,GAAG,EAClB,EAAQ,GAAG,EACX,EAAc,UAAU,EACxB,EAAe,GAAG,EAClB,EAAW,cAAc,EAAG,KAAK;OAEpC,CACF;;GAwBiC,WAAU;aACtC,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cAAvC;KACE,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA;OAAC;OACjC,EAAG,qCAAqC,eAAe;OACrD;;KACL,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,OAAO,EAAG,4BAA4B,OAAO;OAC7C,OAAO;OACP,UAAU;OACV,aAAa,EAAG,0CAA0C,uBAAuB;OACjF,UAAA;OACA,OAAO;OACP,CAAA,EACF,kBAAC,GAAD;OACE,OAAO,EAAG,kCAAkC,aAAa;OACzD,OAAO;OACP,WAAW,MAAM,EAAc,EAAmB;OAClD,SAAS;OACT,CAAA,CACE;;KACN,kBAAC,GAAD;MACE,OAAO,EAAG,mCAAmC,cAAc;MAC3D,OAAO;MACP,UAAU;MACV,aAAa,EACX,iDACA,0BACD;MACD,MAAM;MACN,WAAU;MACV,CAAA;KACF,kBAAC,GAAD;MACE,OAAO,EAAG,4BAA4B,OAAO;MAC7C,OAAO;MACP,UAAU;MACV,aAAa,EAAG,kCAAkC,iBAAiB;MACnE,MAAM,EAAG,qCAAqC,kBAAkB;MAChE,WAAU;MACV,CAAA;KACF,kBAAC,GAAD;MACE,OAAO,EAAG,kCAAkC,cAAc;MAC1D,QAAO;MACP,OAAO;MACP,UAAU;MACV,QAAQ,EAAiB;MACzB,WAAU;MACV,CAAA;KACD,EAAe,WACd,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAe,iBAAiB,QAC7B,EAAe,MAAM,UACrB,EAAG,+BAA+B,uBAAuB;MAC3D,CAAA;KAEN,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,UAAD;OACE,MAAK;OACL,UAAU,EAAe;OACzB,WAAU;iBAHZ,CAKG,EAAe,YACd,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAE7B,EAAG,wCAAwC,kBAAkB,CACvD;;MACL,CAAA;KACI;;GACP,CAAA,EAGR,EAAM,YACL,kBAAC,GAAD,EAAa,OAAO,EAAG,mCAAmC,qBAAqB,EAAI,CAAA,GACjF,EAAM,UACR,kBAAC,GAAD,EAAW,eAAe,KAAK,EAAM,SAAS,EAAI,CAAA,GAChD,EAAU,WAAW,IACvB,kBAAC,GAAD;GACE,cAAa;GACb,MAAM,kBAAC,GAAD,EAAW,WAAU,aAAc,CAAA;GACzC,OAAO,EAAG,sCAAsC,mBAAmB;GACnE,aAAa,EACX,4CACA,4DACD;GACD,QACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAY,GAAK;IAChC,WAAU;cAHZ;KAKE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;;KAAE,EAAG,qCAAqC,eAAe;KAC9E;;GAEX,CAAA,GAEF,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAU,KAAK,MACd,kBAAC,GAAD;IAEE,UAAU;IACV,eAAe,EAAW,cAAc,EAAG,KAAK;IAChD,EAHK,EAAG,GAGR,CACF;GACE,CAAA,CAEG"}
|
|
1
|
+
{"version":3,"file":"LookbooksListPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/lookbooks/LookbooksListPage.tsx"],"sourcesContent":["/**\n * Lookbooks List Page\n * @module coolandlovely/pages/lookbooks\n *\n * Lists themed lookbook boards (`cooListLookbooks`) as mood-board cards — a\n * cover, a visibility chip and (when the list selection carries entries) an\n * item count — and supports creating one inline (`cooCreateLookbook`).\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, BookHeart, Loader2, X } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LookbookCard } from '../../components/LookbookCard';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { TextField, TextAreaField, SelectField } from '../../components/Field';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useLookbooks, useCreateLookbook, useTr } from '../../hooks';\nimport { COO_FILE_FOLDERS } from '../../hooks/useFiles';\nimport { parseCsv, titleCase } from '../../utils/formatters';\nimport type { CooVisibility } from '../../types';\n\nconst VISIBILITIES: CooVisibility[] = ['PRIVATE', 'FOLLOWERS', 'PUBLIC'];\n\nexport function LookbooksListPage() {\n const { navigateTo } = useCoolAndLovely();\n const tr = useTr();\n const query = useLookbooks();\n const createLookbook = useCreateLookbook();\n\n const [creating, setCreating] = useState(false);\n const [name, setName] = useState('');\n const [description, setDescription] = useState('');\n const [visibility, setVisibility] = useState<CooVisibility>('PRIVATE');\n const [tags, setTags] = useState('');\n const [coverFileId, setCoverFileId] = useState('');\n const [nameError, setNameError] = useState<string | undefined>();\n\n const lookbooks = query.data ?? [];\n const visibilityOptions = useMemo(\n () =>\n VISIBILITIES.map((v) => ({\n value: v,\n label: tr(`coolandlovely.visibility.${v.toLowerCase()}`, titleCase(v)),\n })),\n [tr]\n );\n\n const handleCreate = (e: React.FormEvent) => {\n e.preventDefault();\n if (!name.trim()) {\n setNameError(tr('coolandlovely.validation.nameRequired', 'Name is required'));\n return;\n }\n setNameError(undefined);\n createLookbook.mutate(\n {\n name: name.trim(),\n description: description.trim() || undefined,\n visibility,\n tags: parseCsv(tags),\n coverFileId: coverFileId || undefined,\n },\n {\n onSuccess: (lb) => {\n setCreating(false);\n setName('');\n setDescription('');\n setTags('');\n setVisibility('PRIVATE');\n setCoverFileId('');\n navigateTo(`/lookbooks/${lb.id}`);\n },\n }\n );\n };\n\n return (\n <PageLayout\n title={tr('coolandlovely.page.lookbooksTitle', 'Lookbooks')}\n description={tr(\n 'coolandlovely.page.lookbooksSubtitle',\n 'Themed boards of saved catalogue pieces and looks'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => setCreating((v) => !v)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n {creating ? <X className=\"h-4 w-4\" /> : <Plus className=\"h-4 w-4\" />}\n {creating\n ? tr('coolandlovely.actions.close', 'Close')\n : tr('coolandlovely.actions.newLookbook', 'New lookbook')}\n </button>\n }\n >\n {creating && (\n <form onSubmit={handleCreate} className=\"mb-6\">\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h2 className=\"mb-4 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <BookHeart className=\"h-5 w-5\" />{' '}\n {tr('coolandlovely.actions.newLookbook', 'New lookbook')}\n </h2>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n label={tr('coolandlovely.field.name', 'Name')}\n value={name}\n onChange={setName}\n placeholder={tr('coolandlovely.placeholder.lookbookName', 'Summer wedding guest')}\n required\n error={nameError}\n />\n <SelectField\n label={tr('coolandlovely.field.visibility', 'Visibility')}\n value={visibility}\n onChange={(v) => setVisibility(v as CooVisibility)}\n options={visibilityOptions}\n />\n </div>\n <TextAreaField\n label={tr('coolandlovely.field.description', 'Description')}\n value={description}\n onChange={setDescription}\n placeholder={tr(\n 'coolandlovely.placeholder.lookbookDescription',\n 'What this board is for…'\n )}\n rows={2}\n className=\"mt-4\"\n />\n <TextField\n label={tr('coolandlovely.field.tags', 'Tags')}\n value={tags}\n onChange={setTags}\n placeholder={tr('coolandlovely.placeholder.tags', 'summer, formal')}\n hint={tr('coolandlovely.hint.commaSeparated', 'Comma-separated')}\n className=\"mt-4\"\n />\n <ImageUploadField\n label={tr('coolandlovely.field.coverImage', 'Cover image')}\n aspect=\"wide\"\n value={coverFileId}\n onChange={setCoverFileId}\n folder={COO_FILE_FOLDERS.lookbooks}\n className=\"mt-4\"\n />\n {createLookbook.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {createLookbook.error instanceof Error\n ? createLookbook.error.message\n : tr('coolandlovely.error.generic', 'Something went wrong')}\n </p>\n )}\n <div className=\"mt-4 flex justify-end\">\n <button\n type=\"submit\"\n disabled={createLookbook.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {createLookbook.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.actions.createLookbook', 'Create lookbook')}\n </button>\n </div>\n </GlassCard>\n </form>\n )}\n\n {query.isLoading ? (\n <LoadingView label={tr('coolandlovely.loading.lookbooks', 'Loading lookbooks…')} />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : lookbooks.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<BookHeart className=\"h-10 w-10\" />}\n title={tr('coolandlovely.empty.lookbooksTitle', 'No lookbooks yet')}\n description={tr(\n 'coolandlovely.empty.lookbooksDescription',\n 'Create a themed board to clip catalogue pieces and looks.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => setCreating(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('coolandlovely.actions.newLookbook', 'New lookbook')}\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3\">\n {lookbooks.map((lb) => (\n <LookbookCard\n key={lb.id}\n lookbook={lb}\n onClick={() => navigateTo(`/lookbooks/${lb.id}`)}\n />\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|