@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":"PayForOrder.js","names":[],"sources":["../../../src/coolandlovely/components/PayForOrder.tsx"],"sourcesContent":["/**\n * PayForOrder — collects a payment through the provider global-billing chose: an order's payment\n * (BOFF-3370), or a brand's payment of a collaboration fee (BOFF-9637).\n * @module coolandlovely/components\n *\n * Razorpay: opens Razorpay's checkout widget for the billing-created order.\n * Stripe: mounts a Stripe card element and confirms the billing-issued client secret.\n * Whatever the provider reports, the order is only marked paid by `onSettle`, which asks the\n * backend to re-read billing's payment record (BOFF-3370).\n */\n\nimport { type FC, useEffect, useRef, useState } from 'react';\nimport { CreditCard, Loader2, Lock } from 'lucide-react';\n\nimport type { CooPaymentHandles } from '../types';\nimport { formatPrice } from '../utils/formatters';\nimport {\n type StripeCardElement,\n type StripeInstance,\n loadStripe,\n openRazorpayCheckout,\n} from '../utils/paymentProviders';\nimport { useTr } from '../hooks';\n\ninterface PayForOrderProps {\n checkout: CooPaymentHandles & { order?: { orderNumber: string } };\n /** Ask the backend to settle the payment against billing; called after every attempt. */\n onSettle: () => Promise<void>;\n /** What the provider widget shows the payment is for (defaults to the order number). */\n description?: string;\n /** Shown when billing offered no provider this browser can use. */\n unavailableMessage?: string;\n}\n\nconst primaryButton =\n 'inline-flex 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/** Read a semantic colour token so the Stripe iframe matches the current theme. */\nfunction cssToken(name: string): string | undefined {\n const value = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim();\n return value || undefined;\n}\n\nconst StripeCardForm: FC<PayForOrderProps & { publishableKey: string; clientSecret: string }> = ({\n checkout,\n onSettle,\n publishableKey,\n clientSecret,\n}) => {\n const tr = useTr();\n const mountRef = useRef<HTMLDivElement | null>(null);\n const stripeRef = useRef<StripeInstance | null>(null);\n const cardRef = useRef<StripeCardElement | null>(null);\n const [ready, setReady] = useState(false);\n const [complete, setComplete] = useState(false);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n loadStripe(publishableKey)\n .then((stripe) => {\n if (cancelled || !mountRef.current) return;\n stripeRef.current = stripe;\n const card = stripe.elements().create('card', {\n style: {\n base: {\n fontSize: '16px',\n color: cssToken('--color-text-primary'),\n iconColor: cssToken('--color-text-secondary'),\n '::placeholder': { color: cssToken('--color-text-secondary') },\n },\n invalid: { color: cssToken('--color-status-error-text') },\n },\n });\n card.on('change', (event) => {\n setComplete(event.complete);\n setError(event.error?.message ?? null);\n });\n card.mount(mountRef.current);\n cardRef.current = card;\n setReady(true);\n })\n .catch((err: unknown) => {\n if (!cancelled) setError(err instanceof Error ? err.message : String(err));\n });\n return () => {\n cancelled = true;\n cardRef.current?.destroy();\n cardRef.current = null;\n };\n }, [publishableKey]);\n\n const pay = async () => {\n const stripe = stripeRef.current;\n const card = cardRef.current;\n if (!stripe || !card) return;\n setBusy(true);\n setError(null);\n try {\n const result = await stripe.confirmCardPayment(clientSecret, { payment_method: { card } });\n if (result.error)\n setError(\n result.error.message ??\n tr('coolandlovely.checkout.payFailed', 'The payment did not go through.')\n );\n await onSettle();\n } finally {\n setBusy(false);\n }\n };\n\n return (\n <div className=\"space-y-3\">\n <div\n ref={mountRef}\n data-testid=\"stripe-card-element\"\n className=\"rounded-lg border border-border-seam bg-bg-surface px-3 py-3\"\n />\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <button\n type=\"button\"\n className={primaryButton}\n disabled={!ready || !complete || busy}\n onClick={() => void pay()}\n >\n {busy ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Lock className=\"h-4 w-4\" />}\n {tr('coolandlovely.checkout.payAmount', 'Pay {{amount}}', {\n amount: formatPrice(checkout.total, checkout.currency),\n })}\n </button>\n </div>\n );\n};\n\nexport const PayForOrder: FC<PayForOrderProps> = ({\n checkout,\n onSettle,\n description,\n unavailableMessage,\n}) => {\n const tr = useTr();\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n if (checkout.clientSecret && checkout.stripePublishableKey) {\n return (\n <StripeCardForm\n checkout={checkout}\n onSettle={onSettle}\n publishableKey={checkout.stripePublishableKey}\n clientSecret={checkout.clientSecret}\n />\n );\n }\n\n if (checkout.providerOrderId && checkout.razorpayKeyId) {\n const { providerOrderId, razorpayKeyId } = checkout;\n const pay = async () => {\n setBusy(true);\n setError(null);\n try {\n await openRazorpayCheckout({\n keyId: razorpayKeyId,\n providerOrderId,\n total: checkout.total,\n currency: checkout.currency,\n description:\n description ??\n tr('coolandlovely.checkout.orderLabel', 'Order {{number}}', {\n number: checkout.order?.orderNumber ?? '',\n }),\n });\n await onSettle();\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setBusy(false);\n }\n };\n return (\n <div className=\"space-y-3\">\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <button type=\"button\" className={primaryButton} disabled={busy} onClick={() => void pay()}>\n {busy ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <CreditCard className=\"h-4 w-4\" />}\n {tr('coolandlovely.checkout.payAmount', 'Pay {{amount}}', {\n amount: formatPrice(checkout.total, checkout.currency),\n })}\n </button>\n </div>\n );\n }\n\n return (\n <p className=\"rounded-lg border border-border-seam bg-bg-sunken p-3 text-sm text-text-secondary\">\n {unavailableMessage ??\n tr(\n 'coolandlovely.checkout.providerUnavailable',\n 'Online payment is not available for this order right now. Your order is saved — try again from My Orders.'\n )}\n </p>\n );\n};\n"],"mappings":";;;;;;;;AAkCA,IAAM,IACJ;AAGF,SAAS,EAAS,GAAkC;AAElD,QADc,OAAO,iBAAiB,SAAS,gBAAgB,CAAC,iBAAiB,EAAK,CAAC,MAAM,IAC7E,KAAA;;AAGlB,IAAM,KAA2F,EAC/F,aACA,aACA,mBACA,sBACI;CACJ,IAAM,IAAK,GAAO,EACZ,IAAW,EAA8B,KAAK,EAC9C,IAAY,EAA8B,KAAK,EAC/C,IAAU,EAAiC,KAAK,EAChD,CAAC,GAAO,KAAY,EAAS,GAAM,EACnC,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,CAAC,GAAO,KAAY,EAAwB,KAAK;AAEvD,SAAgB;EACd,IAAI,IAAY;AA2BhB,SA1BA,EAAW,EAAe,CACvB,MAAM,MAAW;AAChB,OAAI,KAAa,CAAC,EAAS,QAAS;AACpC,KAAU,UAAU;GACpB,IAAM,IAAO,EAAO,UAAU,CAAC,OAAO,QAAQ,EAC5C,OAAO;IACL,MAAM;KACJ,UAAU;KACV,OAAO,EAAS,uBAAuB;KACvC,WAAW,EAAS,yBAAyB;KAC7C,iBAAiB,EAAE,OAAO,EAAS,yBAAyB,EAAE;KAC/D;IACD,SAAS,EAAE,OAAO,EAAS,4BAA4B,EAAE;IAC1D,EACF,CAAC;AAOF,GANA,EAAK,GAAG,WAAW,MAAU;AAE3B,IADA,EAAY,EAAM,SAAS,EAC3B,EAAS,EAAM,OAAO,WAAW,KAAK;KACtC,EACF,EAAK,MAAM,EAAS,QAAQ,EAC5B,EAAQ,UAAU,GAClB,EAAS,GAAK;IACd,CACD,OAAO,MAAiB;AACvB,GAAK,KAAW,EAAS,aAAe,QAAQ,EAAI,UAAU,OAAO,EAAI,CAAC;IAC1E,QACS;AAGX,GAFA,IAAY,IACZ,EAAQ,SAAS,SAAS,EAC1B,EAAQ,UAAU;;IAEnB,CAAC,EAAe,CAAC;CAEpB,IAAM,IAAM,YAAY;EACtB,IAAM,IAAS,EAAU,SACnB,IAAO,EAAQ;AACjB,SAAC,KAAU,CAAC,IAEhB;GADA,EAAQ,GAAK,EACb,EAAS,KAAK;AACd,OAAI;IACF,IAAM,IAAS,MAAM,EAAO,mBAAmB,GAAc,EAAE,gBAAgB,EAAE,SAAM,EAAE,CAAC;AAM1F,IALI,EAAO,SACT,EACE,EAAO,MAAM,WACX,EAAG,oCAAoC,kCAAkC,CAC5E,EACH,MAAM,GAAU;aACR;AACR,MAAQ,GAAM;;;;AAIlB,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IACE,KAAK;IACL,eAAY;IACZ,WAAU;IACV,CAAA;GACD,KAAS,kBAAC,KAAD;IAAG,WAAU;cAAkC;IAAU,CAAA;GACnE,kBAAC,UAAD;IACE,MAAK;IACL,WAAW;IACX,UAAU,CAAC,KAAS,CAAC,KAAY;IACjC,eAAe,KAAK,GAAK;cAJ3B,CAMG,IAAO,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAAG,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAClF,EAAG,oCAAoC,kBAAkB,EACxD,QAAQ,EAAY,EAAS,OAAO,EAAS,SAAS,EACvD,CAAC,CACK;;GACL;;GAIG,KAAqC,EAChD,aACA,aACA,gBACA,4BACI;CACJ,IAAM,IAAK,GAAO,EACZ,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,CAAC,GAAO,KAAY,EAAwB,KAAK;AAEvD,KAAI,EAAS,gBAAgB,EAAS,qBACpC,QACE,kBAAC,GAAD;EACY;EACA;EACV,gBAAgB,EAAS;EACzB,cAAc,EAAS;EACvB,CAAA;AAIN,KAAI,EAAS,mBAAmB,EAAS,eAAe;EACtD,IAAM,EAAE,oBAAiB,qBAAkB,GACrC,IAAM,YAAY;AAEtB,GADA,EAAQ,GAAK,EACb,EAAS,KAAK;AACd,OAAI;AAYF,IAXA,MAAM,EAAqB;KACzB,OAAO;KACP;KACA,OAAO,EAAS;KAChB,UAAU,EAAS;KACnB,aACE,KACA,EAAG,qCAAqC,oBAAoB,EAC1D,QAAQ,EAAS,OAAO,eAAe,IACxC,CAAC;KACL,CAAC,EACF,MAAM,GAAU;YACT,GAAK;AACZ,MAAS,aAAe,QAAQ,EAAI,UAAU,OAAO,EAAI,CAAC;aAClD;AACR,MAAQ,GAAM;;;AAGlB,SACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,KAAS,kBAAC,KAAD;IAAG,WAAU;cAAkC;IAAU,CAAA,EACnE,kBAAC,UAAD;IAAQ,MAAK;IAAS,WAAW;IAAe,UAAU;IAAM,eAAe,KAAK,GAAK;cAAzF,CACG,IAAO,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAAG,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA,EACxF,EAAG,oCAAoC,kBAAkB,EACxD,QAAQ,EAAY,EAAS,OAAO,EAAS,SAAS,EACvD,CAAC,CACK;MACL;;;AAIV,QACE,kBAAC,KAAD;EAAG,WAAU;YACV,KACC,EACE,8CACA,4GACD;EACD,CAAA"}
|
|
1
|
+
{"version":3,"file":"PayForOrder.js","names":[],"sources":["../../../src/coolandlovely/components/PayForOrder.tsx"],"sourcesContent":["/**\n * PayForOrder — collects a payment through the provider global-billing chose: an order's payment\n * (BOFF-3370), or a brand's payment of a collaboration fee (BOFF-9637).\n * @module coolandlovely/components\n *\n * Razorpay: opens Razorpay's checkout widget for the billing-created order.\n * Stripe: mounts a Stripe card element and confirms the billing-issued client secret.\n * Whatever the provider reports, the order is only marked paid by `onSettle`, which asks the\n * backend to re-read billing's payment record (BOFF-3370).\n */\n\nimport { type FC, useEffect, useRef, useState } from 'react';\nimport { CreditCard, Loader2, Lock } from 'lucide-react';\n\nimport type { CooPaymentHandles } from '../types';\nimport { formatPrice } from '../utils/formatters';\nimport {\n type StripeCardElement,\n type StripeInstance,\n loadStripe,\n openRazorpayCheckout,\n} from '../utils/paymentProviders';\nimport { useTr } from '../hooks';\n\ninterface PayForOrderProps {\n checkout: CooPaymentHandles & { order?: { orderNumber: string } };\n /** Ask the backend to settle the payment against billing; called after every attempt. */\n onSettle: () => Promise<void>;\n /** What the provider widget shows the payment is for (defaults to the order number). */\n description?: string;\n /** Shown when billing offered no provider this browser can use. */\n unavailableMessage?: string;\n}\n\nconst primaryButton =\n 'inline-flex 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/** Read a semantic colour token so the Stripe iframe matches the current theme. */\nfunction cssToken(name: string): string | undefined {\n const value = window.getComputedStyle(document.documentElement).getPropertyValue(name).trim();\n return value || undefined;\n}\n\nconst StripeCardForm: FC<PayForOrderProps & { publishableKey: string; clientSecret: string }> = ({\n checkout,\n onSettle,\n publishableKey,\n clientSecret,\n}) => {\n const tr = useTr();\n const mountRef = useRef<HTMLDivElement | null>(null);\n const stripeRef = useRef<StripeInstance | null>(null);\n const cardRef = useRef<StripeCardElement | null>(null);\n const [ready, setReady] = useState(false);\n const [complete, setComplete] = useState(false);\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n loadStripe(publishableKey)\n .then((stripe) => {\n if (cancelled || !mountRef.current) return;\n stripeRef.current = stripe;\n const card = stripe.elements().create('card', {\n style: {\n base: {\n fontSize: '16px',\n color: cssToken('--color-text-primary'),\n iconColor: cssToken('--color-text-secondary'),\n '::placeholder': { color: cssToken('--color-text-secondary') },\n },\n invalid: { color: cssToken('--color-status-error-text') },\n },\n });\n card.on('change', (event) => {\n setComplete(event.complete);\n setError(event.error?.message ?? null);\n });\n card.mount(mountRef.current);\n cardRef.current = card;\n setReady(true);\n })\n .catch((err: unknown) => {\n if (!cancelled) setError(err instanceof Error ? err.message : String(err));\n });\n return () => {\n cancelled = true;\n cardRef.current?.destroy();\n cardRef.current = null;\n };\n }, [publishableKey]);\n\n const pay = async () => {\n const stripe = stripeRef.current;\n const card = cardRef.current;\n if (!stripe || !card) return;\n setBusy(true);\n setError(null);\n try {\n const result = await stripe.confirmCardPayment(clientSecret, { payment_method: { card } });\n if (result.error)\n setError(\n result.error.message ??\n tr('coolandlovely.checkout.payFailed', 'The payment did not go through.')\n );\n await onSettle();\n } finally {\n setBusy(false);\n }\n };\n\n return (\n <div className=\"space-y-3\">\n <div\n ref={mountRef}\n data-testid=\"stripe-card-element\"\n className=\"rounded-lg border border-border-seam bg-bg-surface px-3 py-3\"\n />\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <button\n type=\"button\"\n className={primaryButton}\n disabled={!ready || !complete || busy}\n onClick={() => void pay()}\n >\n {busy ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <Lock className=\"h-4 w-4\" />}\n {tr('coolandlovely.checkout.payAmount', 'Pay {{amount}}', {\n amount: formatPrice(checkout.total, checkout.currency),\n })}\n </button>\n </div>\n );\n};\n\nexport const PayForOrder: FC<PayForOrderProps> = ({\n checkout,\n onSettle,\n description,\n unavailableMessage,\n}) => {\n const tr = useTr();\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n if (checkout.clientSecret && checkout.stripePublishableKey) {\n return (\n <StripeCardForm\n checkout={checkout}\n onSettle={onSettle}\n publishableKey={checkout.stripePublishableKey}\n clientSecret={checkout.clientSecret}\n />\n );\n }\n\n if (checkout.providerOrderId && checkout.razorpayKeyId) {\n const { providerOrderId, razorpayKeyId } = checkout;\n const pay = async () => {\n setBusy(true);\n setError(null);\n try {\n await openRazorpayCheckout({\n keyId: razorpayKeyId,\n providerOrderId,\n total: checkout.total,\n currency: checkout.currency,\n description:\n description ??\n tr('coolandlovely.checkout.orderLabel', 'Order {{number}}', {\n number: checkout.order?.orderNumber ?? '',\n }),\n });\n await onSettle();\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setBusy(false);\n }\n };\n return (\n <div className=\"space-y-3\">\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <button type=\"button\" className={primaryButton} disabled={busy} onClick={() => void pay()}>\n {busy ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <CreditCard className=\"h-4 w-4\" />}\n {tr('coolandlovely.checkout.payAmount', 'Pay {{amount}}', {\n amount: formatPrice(checkout.total, checkout.currency),\n })}\n </button>\n </div>\n );\n }\n\n return (\n <p className=\"rounded-lg border border-border-seam bg-bg-sunken p-3 text-sm text-text-secondary\">\n {unavailableMessage ??\n tr(\n 'coolandlovely.checkout.providerUnavailable',\n 'Online payment is not available for this order right now. Your order is saved — try again from My Orders.'\n )}\n </p>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RefundPanel.js","names":[],"sources":["../../../src/coolandlovely/components/RefundPanel.tsx"],"sourcesContent":["/**\n * Brand refund panel (BOFF-9702)\n * @module coolandlovely/components\n *\n * On the brand's order detail: refund all of the brand's pieces of the order to the shopper\n * (`cooRefundBrandOrder`), or see the refund once it exists. The money goes back through\n * global-billing to the shopper's original payment method, and the creator commissions on the\n * refunded pieces are reversed while they are still unpaid.\n *\n * When some of the pieces were stock-tracked, the brand can return them to stock with the refund\n * (\"Return these pieces to stock\", BOFF-9901), or afterwards once they physically come back.\n */\n\nimport { useState } from 'react';\nimport { Loader2, PackagePlus, RotateCcw } from 'lucide-react';\n\nimport { useTr } from '../hooks';\nimport { useRefundBrandOrder } from '../hooks/useBrandOrdersApi';\nimport type { CooBrandOrder } from '../types';\nimport { cn } from '../utils/cn';\nimport { formatDate, formatPrice } from '../utils/formatters';\nimport { TextAreaField, ToggleField } from './Field';\nimport { StatusBadge } from './StatusBadge';\n\nconst MIN_REASON = 3;\nconst MAX_REASON = 1000;\n\nconst dangerButton =\n 'inline-flex w-full items-center justify-center gap-2 rounded-lg border border-status-error-text px-4 py-2.5 text-sm font-semibold text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-50';\nconst secondaryButton =\n 'inline-flex w-full items-center justify-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 disabled:opacity-50';\n\n/** Whether the brand can (still) start or retry a refund of this order. */\nexport function canRefund(order: CooBrandOrder): boolean {\n const status = order.refund?.status;\n if (status === 'COMPLETED' || status === 'PENDING') return false;\n return order.status === 'PAID' || order.status === 'PARTIALLY_REFUNDED';\n}\n\n/** Units of the brand's pieces in this order that took stock at checkout and can be restocked. */\nexport function restockableUnits(order: CooBrandOrder): number {\n return order.items\n .filter((item) => item.stockTracked === true)\n .reduce((sum, item) => sum + item.quantity, 0);\n}\n\n/** Whether a completed refund can still return its pieces to stock afterwards. */\nexport function canRestockLater(order: CooBrandOrder): boolean {\n return (\n order.refund?.status === 'COMPLETED' &&\n order.refund.restockStatus === 'NONE' &&\n restockableUnits(order) > 0\n );\n}\n\nfunction RestockStatusLine({ order }: { order: CooBrandOrder }) {\n const tr = useTr();\n const refund = order.refund;\n if (!refund || refund.restockStatus === 'NONE') return null;\n if (refund.restockStatus === 'RESTOCKED') {\n return (\n <p className=\"text-xs text-status-success-text\" data-testid=\"brand-refund-restock-status\">\n {tr('coolandlovely.refund.restocked', '{{count}} pieces returned to stock {{date}}', {\n count: refund.restockedQuantity,\n date: refund.restockedAt ? formatDate(refund.restockedAt) : '',\n })}\n </p>\n );\n }\n if (refund.restockStatus === 'SKIPPED') {\n return (\n <p className=\"text-xs text-text-secondary\" data-testid=\"brand-refund-restock-status\">\n {tr(\n 'coolandlovely.refund.restockSkipped',\n 'Nothing went back to stock: none of these pieces were stock-tracked when they were bought.'\n )}\n </p>\n );\n }\n return (\n <p className=\"text-xs text-status-warning-text\" data-testid=\"brand-refund-restock-status\">\n {tr(\n 'coolandlovely.refund.restockPending',\n 'The pieces go back to stock once the refund and the sale have settled. Refresh in a moment.'\n )}\n </p>\n );\n}\n\nexport function RefundPanel({ order }: { order: CooBrandOrder }) {\n const tr = useTr();\n const refund = useRefundBrandOrder();\n const [open, setOpen] = useState(false);\n const [reason, setReason] = useState('');\n const [restock, setRestock] = useState(false);\n const existing = order.refund;\n const units = restockableUnits(order);\n const trimmed = reason.trim();\n const reasonValid = trimmed.length >= MIN_REASON && trimmed.length <= MAX_REASON;\n\n return (\n <div className=\"space-y-3 border-t border-border-seam pt-4\" data-testid=\"brand-refund-panel\">\n {existing && (\n <div className=\"space-y-1 text-sm\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-text-secondary\">\n {tr('coolandlovely.refund.label', 'Refund')}\n </span>\n <span data-testid=\"brand-refund-status\">\n <StatusBadge status={existing.status} />\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-text-secondary\">\n {tr('coolandlovely.refund.amount', 'Amount')}\n </span>\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {formatPrice(existing.amount, existing.currency)}\n </span>\n </div>\n {existing.refundedAt && (\n <p className=\"text-xs text-text-secondary\">\n {tr('coolandlovely.refund.refundedOn', 'Refunded {{date}}', {\n date: formatDate(existing.refundedAt),\n })}\n </p>\n )}\n <p className=\"text-xs text-text-secondary\">\n {tr('coolandlovely.refund.reasonShown', 'Reason: {{reason}}', {\n reason: existing.reason,\n })}\n </p>\n {existing.status === 'PENDING' && (\n <p className=\"text-xs text-status-warning-text\">\n {tr(\n 'coolandlovely.refund.pendingHint',\n 'The refund is being processed with the payment provider. Refresh in a moment.'\n )}\n </p>\n )}\n {existing.status === 'FAILED' && existing.failureReason && (\n <p className=\"text-xs text-status-error-text\">{existing.failureReason}</p>\n )}\n <RestockStatusLine order={order} />\n </div>\n )}\n\n {canRestockLater(order) && existing && (\n <div className=\"space-y-2\">\n {refund.error && (\n <p className=\"text-sm text-status-error-text\" role=\"alert\">\n {refund.error.message}\n </p>\n )}\n <button\n type=\"button\"\n disabled={refund.isPending}\n onClick={() =>\n refund.mutate({\n brandId: order.brandId,\n orderId: order.id,\n reason: existing.reason,\n restock: true,\n })\n }\n className={secondaryButton}\n data-testid=\"brand-refund-restock-later\"\n >\n {refund.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <PackagePlus className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.refund.restockLater', 'Return {{count}} pieces to stock', {\n count: units,\n })}\n </button>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'coolandlovely.refund.restockLaterHint',\n 'Use this once the refunded pieces have come back to you. It can only be done once.'\n )}\n </p>\n </div>\n )}\n\n {canRefund(order) &&\n (open ? (\n <div className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.refund.explain',\n 'Refunds {{amount}} plus its share of tax to the shopper, for all of your pieces in this order. Creator commissions on them are reversed. This cannot be undone.',\n { amount: formatPrice(order.brandSubtotal, order.currency) }\n )}\n </p>\n <TextAreaField\n id=\"coo-refund-reason\"\n label={tr('coolandlovely.refund.reason', 'Reason (shown to the shopper)')}\n value={reason}\n onChange={setReason}\n rows={3}\n />\n {units > 0 && (\n <ToggleField\n id=\"coo-refund-restock\"\n label={tr('coolandlovely.refund.restock', 'Return these pieces to stock')}\n hint={tr(\n 'coolandlovely.refund.restockHint',\n 'Tick this if the {{count}} stock-tracked pieces came back to you. Their sizes and colours go back on sale.',\n { count: units }\n )}\n checked={restock}\n onChange={setRestock}\n disabled={refund.isPending}\n testId=\"brand-refund-restock\"\n />\n )}\n {refund.error && (\n <p className=\"text-sm text-status-error-text\" role=\"alert\">\n {refund.error.message}\n </p>\n )}\n <button\n type=\"button\"\n disabled={refund.isPending || !reasonValid}\n onClick={() =>\n refund.mutate(\n {\n brandId: order.brandId,\n orderId: order.id,\n reason: trimmed,\n restock: units > 0 && restock,\n },\n { onSuccess: () => setOpen(false) }\n )\n }\n className={dangerButton}\n data-testid=\"brand-refund-confirm\"\n >\n {refund.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <RotateCcw className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.refund.confirm', 'Refund my pieces')}\n </button>\n <button\n type=\"button\"\n disabled={refund.isPending}\n onClick={() => setOpen(false)}\n className={secondaryButton}\n >\n {tr('coolandlovely.common.cancel', 'Cancel')}\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n className={cn(secondaryButton)}\n data-testid=\"brand-refund-open\"\n >\n <RotateCcw className=\"h-4 w-4\" />\n {existing?.status === 'FAILED'\n ? tr('coolandlovely.refund.retry', 'Retry refund')\n : tr('coolandlovely.refund.start', 'Refund this order')}\n </button>\n ))}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AAwBA,IAAM,IAAa,GACb,IAAa,KAEb,IACJ,kOACI,IACJ;AAGF,SAAgB,EAAU,GAA+B;CACvD,IAAM,IAAS,EAAM,QAAQ;AAE7B,QADI,MAAW,eAAe,MAAW,YAAkB,KACpD,EAAM,WAAW,UAAU,EAAM,WAAW;;AAIrD,SAAgB,EAAiB,GAA8B;AAC7D,QAAO,EAAM,MACV,QAAQ,MAAS,EAAK,iBAAiB,GAAK,CAC5C,QAAQ,GAAK,MAAS,IAAM,EAAK,UAAU,EAAE;;AAIlD,SAAgB,EAAgB,GAA+B;AAC7D,QACE,EAAM,QAAQ,WAAW,eACzB,EAAM,OAAO,kBAAkB,UAC/B,EAAiB,EAAM,GAAG;;AAI9B,SAAS,EAAkB,EAAE,YAAmC;CAC9D,IAAM,IAAK,GAAO,EACZ,IAAS,EAAM;AAsBrB,QArBI,CAAC,KAAU,EAAO,kBAAkB,SAAe,OACnD,EAAO,kBAAkB,cAEzB,kBAAC,KAAD;EAAG,WAAU;EAAmC,eAAY;YACzD,EAAG,kCAAkC,+CAA+C;GACnF,OAAO,EAAO;GACd,MAAM,EAAO,cAAc,EAAW,EAAO,YAAY,GAAG;GAC7D,CAAC;EACA,CAAA,GAGJ,EAAO,kBAAkB,YAEzB,kBAAC,KAAD;EAAG,WAAU;EAA8B,eAAY;YACpD,EACC,uCACA,6FACD;EACC,CAAA,GAIN,kBAAC,KAAD;EAAG,WAAU;EAAmC,eAAY;YACzD,EACC,uCACA,8FACD;EACC,CAAA;;AAIR,SAAgB,EAAY,EAAE,YAAmC;CAC/D,IAAM,IAAK,GAAO,EACZ,IAAS,GAAqB,EAC9B,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAS,KAAc,EAAS,GAAM,EACvC,IAAW,EAAM,QACjB,IAAQ,EAAiB,EAAM,EAC/B,IAAU,EAAO,MAAM,EACvB,IAAc,EAAQ,UAAU,KAAc,EAAQ,UAAU;AAEtE,QACE,kBAAC,OAAD;EAAK,WAAU;EAA6C,eAAY;YAAxE;GACG,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAG,8BAA8B,SAAS;OACtC,CAAA,EACP,kBAAC,QAAD;OAAM,eAAY;iBAChB,kBAAC,GAAD,EAAa,QAAQ,EAAS,QAAU,CAAA;OACnC,CAAA,CACH;;KACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAG,+BAA+B,SAAS;OACvC,CAAA,EACP,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAY,EAAS,QAAQ,EAAS,SAAS;OAC3C,CAAA,CACH;;KACL,EAAS,cACR,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAG,mCAAmC,qBAAqB,EAC1D,MAAM,EAAW,EAAS,WAAW,EACtC,CAAC;MACA,CAAA;KAEN,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAG,oCAAoC,sBAAsB,EAC5D,QAAQ,EAAS,QAClB,CAAC;MACA,CAAA;KACH,EAAS,WAAW,aACnB,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,oCACA,gFACD;MACC,CAAA;KAEL,EAAS,WAAW,YAAY,EAAS,iBACxC,kBAAC,KAAD;MAAG,WAAU;gBAAkC,EAAS;MAAkB,CAAA;KAE5E,kBAAC,GAAD,EAA0B,UAAS,CAAA;KAC/B;;GAGP,EAAgB,EAAM,IAAI,KACzB,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,EAAO,SACN,kBAAC,KAAD;MAAG,WAAU;MAAiC,MAAK;gBAChD,EAAO,MAAM;MACZ,CAAA;KAEN,kBAAC,UAAD;MACE,MAAK;MACL,UAAU,EAAO;MACjB,eACE,EAAO,OAAO;OACZ,SAAS,EAAM;OACf,SAAS,EAAM;OACf,QAAQ,EAAS;OACjB,SAAS;OACV,CAAC;MAEJ,WAAW;MACX,eAAY;gBAZd,CAcG,EAAO,YACN,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA,EAEpC,EAAG,qCAAqC,oCAAoC,EAC3E,OAAO,GACR,CAAC,CACK;;KACT,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,yCACA,qFACD;MACC,CAAA;KACA;;GAGP,EAAU,EAAM,KACd,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,gCACA,mKACA,EAAE,QAAQ,EAAY,EAAM,eAAe,EAAM,SAAS,EAAE,CAC7D;MACC,CAAA;KACJ,kBAAC,GAAD;MACE,IAAG;MACH,OAAO,EAAG,+BAA+B,gCAAgC;MACzE,OAAO;MACP,UAAU;MACV,MAAM;MACN,CAAA;KACD,IAAQ,KACP,kBAAC,GAAD;MACE,IAAG;MACH,OAAO,EAAG,gCAAgC,+BAA+B;MACzE,MAAM,EACJ,oCACA,8GACA,EAAE,OAAO,GAAO,CACjB;MACD,SAAS;MACT,UAAU;MACV,UAAU,EAAO;MACjB,QAAO;MACP,CAAA;KAEH,EAAO,SACN,kBAAC,KAAD;MAAG,WAAU;MAAiC,MAAK;gBAChD,EAAO,MAAM;MACZ,CAAA;KAEN,kBAAC,UAAD;MACE,MAAK;MACL,UAAU,EAAO,aAAa,CAAC;MAC/B,eACE,EAAO,OACL;OACE,SAAS,EAAM;OACf,SAAS,EAAM;OACf,QAAQ;OACR,SAAS,IAAQ,KAAK;OACvB,EACD,EAAE,iBAAiB,EAAQ,GAAM,EAAE,CACpC;MAEH,WAAW;MACX,eAAY;gBAfd,CAiBG,EAAO,YACN,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAElC,EAAG,gCAAgC,mBAAmB,CAChD;;KACT,kBAAC,UAAD;MACE,MAAK;MACL,UAAU,EAAO;MACjB,eAAe,EAAQ,GAAM;MAC7B,WAAW;gBAEV,EAAG,+BAA+B,SAAS;MACrC,CAAA;KACL;QAEN,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAQ,GAAK;IAC5B,WAAW,EAAG,EAAgB;IAC9B,eAAY;cAJd,CAME,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAChC,GAAU,WAAW,WAClB,EAAG,8BAA8B,eAAe,GAChD,EAAG,8BAA8B,oBAAoB,CAClD;;GAET"}
|
|
1
|
+
{"version":3,"file":"RefundPanel.js","names":[],"sources":["../../../src/coolandlovely/components/RefundPanel.tsx"],"sourcesContent":["/**\n * Brand refund panel (BOFF-9702)\n * @module coolandlovely/components\n *\n * On the brand's order detail: refund all of the brand's pieces of the order to the shopper\n * (`cooRefundBrandOrder`), or see the refund once it exists. The money goes back through\n * global-billing to the shopper's original payment method, and the creator commissions on the\n * refunded pieces are reversed while they are still unpaid.\n *\n * When some of the pieces were stock-tracked, the brand can return them to stock with the refund\n * (\"Return these pieces to stock\", BOFF-9901), or afterwards once they physically come back.\n */\n\nimport { useState } from 'react';\nimport { Loader2, PackagePlus, RotateCcw } from 'lucide-react';\n\nimport { useTr } from '../hooks';\nimport { useRefundBrandOrder } from '../hooks/useBrandOrdersApi';\nimport type { CooBrandOrder } from '../types';\nimport { cn } from '../utils/cn';\nimport { formatDate, formatPrice } from '../utils/formatters';\nimport { TextAreaField, ToggleField } from './Field';\nimport { StatusBadge } from './StatusBadge';\n\nconst MIN_REASON = 3;\nconst MAX_REASON = 1000;\n\nconst dangerButton =\n 'inline-flex w-full items-center justify-center gap-2 rounded-lg border border-status-error-text px-4 py-2.5 text-sm font-semibold text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-50';\nconst secondaryButton =\n 'inline-flex w-full items-center justify-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 disabled:opacity-50';\n\n/** Whether the brand can (still) start or retry a refund of this order. */\nexport function canRefund(order: CooBrandOrder): boolean {\n const status = order.refund?.status;\n if (status === 'COMPLETED' || status === 'PENDING') return false;\n return order.status === 'PAID' || order.status === 'PARTIALLY_REFUNDED';\n}\n\n/** Units of the brand's pieces in this order that took stock at checkout and can be restocked. */\nexport function restockableUnits(order: CooBrandOrder): number {\n return order.items\n .filter((item) => item.stockTracked === true)\n .reduce((sum, item) => sum + item.quantity, 0);\n}\n\n/** Whether a completed refund can still return its pieces to stock afterwards. */\nexport function canRestockLater(order: CooBrandOrder): boolean {\n return (\n order.refund?.status === 'COMPLETED' &&\n order.refund.restockStatus === 'NONE' &&\n restockableUnits(order) > 0\n );\n}\n\nfunction RestockStatusLine({ order }: { order: CooBrandOrder }) {\n const tr = useTr();\n const refund = order.refund;\n if (!refund || refund.restockStatus === 'NONE') return null;\n if (refund.restockStatus === 'RESTOCKED') {\n return (\n <p className=\"text-xs text-status-success-text\" data-testid=\"brand-refund-restock-status\">\n {tr('coolandlovely.refund.restocked', '{{count}} pieces returned to stock {{date}}', {\n count: refund.restockedQuantity,\n date: refund.restockedAt ? formatDate(refund.restockedAt) : '',\n })}\n </p>\n );\n }\n if (refund.restockStatus === 'SKIPPED') {\n return (\n <p className=\"text-xs text-text-secondary\" data-testid=\"brand-refund-restock-status\">\n {tr(\n 'coolandlovely.refund.restockSkipped',\n 'Nothing went back to stock: none of these pieces were stock-tracked when they were bought.'\n )}\n </p>\n );\n }\n return (\n <p className=\"text-xs text-status-warning-text\" data-testid=\"brand-refund-restock-status\">\n {tr(\n 'coolandlovely.refund.restockPending',\n 'The pieces go back to stock once the refund and the sale have settled. Refresh in a moment.'\n )}\n </p>\n );\n}\n\nexport function RefundPanel({ order }: { order: CooBrandOrder }) {\n const tr = useTr();\n const refund = useRefundBrandOrder();\n const [open, setOpen] = useState(false);\n const [reason, setReason] = useState('');\n const [restock, setRestock] = useState(false);\n const existing = order.refund;\n const units = restockableUnits(order);\n const trimmed = reason.trim();\n const reasonValid = trimmed.length >= MIN_REASON && trimmed.length <= MAX_REASON;\n\n return (\n <div className=\"space-y-3 border-t border-border-seam pt-4\" data-testid=\"brand-refund-panel\">\n {existing && (\n <div className=\"space-y-1 text-sm\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-text-secondary\">\n {tr('coolandlovely.refund.label', 'Refund')}\n </span>\n <span data-testid=\"brand-refund-status\">\n <StatusBadge status={existing.status} />\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span className=\"text-text-secondary\">\n {tr('coolandlovely.refund.amount', 'Amount')}\n </span>\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {formatPrice(existing.amount, existing.currency)}\n </span>\n </div>\n {existing.refundedAt && (\n <p className=\"text-xs text-text-secondary\">\n {tr('coolandlovely.refund.refundedOn', 'Refunded {{date}}', {\n date: formatDate(existing.refundedAt),\n })}\n </p>\n )}\n <p className=\"text-xs text-text-secondary\">\n {tr('coolandlovely.refund.reasonShown', 'Reason: {{reason}}', {\n reason: existing.reason,\n })}\n </p>\n {existing.status === 'PENDING' && (\n <p className=\"text-xs text-status-warning-text\">\n {tr(\n 'coolandlovely.refund.pendingHint',\n 'The refund is being processed with the payment provider. Refresh in a moment.'\n )}\n </p>\n )}\n {existing.status === 'FAILED' && existing.failureReason && (\n <p className=\"text-xs text-status-error-text\">{existing.failureReason}</p>\n )}\n <RestockStatusLine order={order} />\n </div>\n )}\n\n {canRestockLater(order) && existing && (\n <div className=\"space-y-2\">\n {refund.error && (\n <p className=\"text-sm text-status-error-text\" role=\"alert\">\n {refund.error.message}\n </p>\n )}\n <button\n type=\"button\"\n disabled={refund.isPending}\n onClick={() =>\n refund.mutate({\n brandId: order.brandId,\n orderId: order.id,\n reason: existing.reason,\n restock: true,\n })\n }\n className={secondaryButton}\n data-testid=\"brand-refund-restock-later\"\n >\n {refund.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <PackagePlus className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.refund.restockLater', 'Return {{count}} pieces to stock', {\n count: units,\n })}\n </button>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'coolandlovely.refund.restockLaterHint',\n 'Use this once the refunded pieces have come back to you. It can only be done once.'\n )}\n </p>\n </div>\n )}\n\n {canRefund(order) &&\n (open ? (\n <div className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.refund.explain',\n 'Refunds {{amount}} plus its share of tax to the shopper, for all of your pieces in this order. Creator commissions on them are reversed. This cannot be undone.',\n { amount: formatPrice(order.brandSubtotal, order.currency) }\n )}\n </p>\n <TextAreaField\n id=\"coo-refund-reason\"\n label={tr('coolandlovely.refund.reason', 'Reason (shown to the shopper)')}\n value={reason}\n onChange={setReason}\n rows={3}\n />\n {units > 0 && (\n <ToggleField\n id=\"coo-refund-restock\"\n label={tr('coolandlovely.refund.restock', 'Return these pieces to stock')}\n hint={tr(\n 'coolandlovely.refund.restockHint',\n 'Tick this if the {{count}} stock-tracked pieces came back to you. Their sizes and colours go back on sale.',\n { count: units }\n )}\n checked={restock}\n onChange={setRestock}\n disabled={refund.isPending}\n testId=\"brand-refund-restock\"\n />\n )}\n {refund.error && (\n <p className=\"text-sm text-status-error-text\" role=\"alert\">\n {refund.error.message}\n </p>\n )}\n <button\n type=\"button\"\n disabled={refund.isPending || !reasonValid}\n onClick={() =>\n refund.mutate(\n {\n brandId: order.brandId,\n orderId: order.id,\n reason: trimmed,\n restock: units > 0 && restock,\n },\n { onSuccess: () => setOpen(false) }\n )\n }\n className={dangerButton}\n data-testid=\"brand-refund-confirm\"\n >\n {refund.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <RotateCcw className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.refund.confirm', 'Refund my pieces')}\n </button>\n <button\n type=\"button\"\n disabled={refund.isPending}\n onClick={() => setOpen(false)}\n className={secondaryButton}\n >\n {tr('coolandlovely.common.cancel', 'Cancel')}\n </button>\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n className={cn(secondaryButton)}\n data-testid=\"brand-refund-open\"\n >\n <RotateCcw className=\"h-4 w-4\" />\n {existing?.status === 'FAILED'\n ? tr('coolandlovely.refund.retry', 'Retry refund')\n : tr('coolandlovely.refund.start', 'Refund this order')}\n </button>\n ))}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -213,6 +213,6 @@ function E({ order: n }) {
|
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
//#endregion
|
|
216
|
-
export { T as OrderReturns, E as ReturnRequestPanel };
|
|
216
|
+
export { T as OrderReturns, E as ReturnRequestPanel, S as canRequestReturn, x as isReturnWindowOpen };
|
|
217
217
|
|
|
218
218
|
//# sourceMappingURL=ReturnRequests.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReturnRequests.js","names":[],"sources":["../../../src/coolandlovely/components/ReturnRequests.tsx"],"sourcesContent":["/**\n * Shopper return requests (BOFF-9900)\n * @module coolandlovely/components\n *\n * `OrderReturns` sits on the shopper's order detail: for each brand in a paid order the shopper\n * can ask for a return (`cooRequestReturn`) inside the return window, and sees the request and\n * the brand's decision once it exists. `ReturnRequestPanel` sits on the brand's order detail: the\n * brand approves the request, which refunds its pieces through the brand refund\n * (`cooApproveReturnRequest`), or declines it with a note (`cooDeclineReturnRequest`).\n */\n\nimport { useState } from 'react';\nimport { CheckCircle2, Loader2, Undo2, XCircle } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { useTr } from '../hooks';\nimport { useApproveReturnRequest, useDeclineReturnRequest } from '../hooks/useBrandOrdersApi';\nimport { useRequestReturn } from '../hooks/useCommerceApi';\nimport type { CooBrandOrder, CooOrder, CooReturnRequest } from '../types';\nimport { formatDate } from '../utils/formatters';\nimport { TextAreaField } from './Field';\nimport { StatusBadge } from './StatusBadge';\n\nconst MIN_TEXT = 3;\nconst MAX_TEXT = 1000;\nconst RETURNABLE_ORDER_STATUSES = ['PAID', 'PARTIALLY_REFUNDED'];\n\nconst isValidText = (value: string): boolean => {\n const trimmed = value.trim();\n return trimmed.length >= MIN_TEXT && trimmed.length <= MAX_TEXT;\n};\n\nconst primaryButton =\n 'inline-flex items-center justify-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-semibold text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50';\nconst secondaryButton =\n 'inline-flex items-center justify-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 disabled:opacity-50';\n\n/** Whether the return window of a paid order is still open. */\nexport function isReturnWindowOpen(order: Pick<CooOrder, 'returnWindowEndsAt'>, now = Date.now()) {\n if (!order.returnWindowEndsAt) return false;\n return new Date(order.returnWindowEndsAt).getTime() > now;\n}\n\n/** Whether the shopper can still ask the brand to take back its pieces of this order. */\nexport function canRequestReturn(order: CooOrder, brandId: string, now = Date.now()): boolean {\n if (!RETURNABLE_ORDER_STATUSES.includes(order.status)) return false;\n if (order.returnRequests?.some((request) => request.brandId === brandId)) return false;\n const refund = order.refunds?.find((entry) => entry.brandId === brandId);\n if (refund && (refund.status === 'COMPLETED' || refund.status === 'PENDING')) return false;\n return isReturnWindowOpen(order, now);\n}\n\nfunction RequestDetails({ request }: { request: CooReturnRequest }) {\n const tr = useTr();\n return (\n <div className=\"space-y-1 text-xs text-text-secondary\">\n <p>\n {tr('coolandlovely.returns.reasonShown', 'Reason: {{reason}}', { reason: request.reason })}\n </p>\n {request.status === 'REQUESTED' && (\n <p>\n {tr(\n 'coolandlovely.returns.awaitingBrand',\n 'Asked {{date}}. The brand will approve or decline it.',\n { date: formatDate(request.createdAt) }\n )}\n </p>\n )}\n {request.status !== 'REQUESTED' && request.decisionNote && (\n <p data-testid=\"return-decision-note\">\n {tr('coolandlovely.returns.brandNote', 'Brand’s note: {{note}}', {\n note: request.decisionNote,\n })}\n </p>\n )}\n {request.status !== 'REQUESTED' && request.decidedAt && (\n <p>\n {request.status === 'APPROVED'\n ? tr(\n 'coolandlovely.returns.approvedOn',\n 'Approved {{date}}. The pieces were refunded.',\n {\n date: formatDate(request.decidedAt),\n }\n )\n : tr('coolandlovely.returns.declinedOn', 'Declined {{date}}.', {\n date: formatDate(request.decidedAt),\n })}\n </p>\n )}\n </div>\n );\n}\n\nfunction RequestReturnForm({ orderId, brandId }: { orderId: string; brandId: string }) {\n const tr = useTr();\n const request = useRequestReturn();\n const [open, setOpen] = useState(false);\n const [reason, setReason] = useState('');\n\n if (!open) {\n return (\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n className={secondaryButton}\n data-testid=\"order-return-open\"\n >\n <Undo2 className=\"h-4 w-4\" />\n {tr('coolandlovely.returns.request', 'Request a return')}\n </button>\n );\n }\n return (\n <div className=\"space-y-2\">\n <TextAreaField\n id={`coo-return-reason-${brandId}`}\n label={tr('coolandlovely.returns.reason', 'Why are you returning these pieces?')}\n hint={tr('coolandlovely.returns.reasonHint', 'Shown to the brand.')}\n value={reason}\n onChange={setReason}\n rows={3}\n />\n {request.error && (\n <p className=\"text-sm text-status-error-text\" role=\"alert\">\n {request.error.message}\n </p>\n )}\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n disabled={request.isPending || !isValidText(reason)}\n onClick={() =>\n request.mutate(\n { orderId, brandId, reason: reason.trim() },\n { onSuccess: () => setOpen(false) }\n )\n }\n className={primaryButton}\n data-testid=\"order-return-submit\"\n >\n {request.isPending && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {tr('coolandlovely.returns.send', 'Send return request')}\n </button>\n <button\n type=\"button\"\n disabled={request.isPending}\n onClick={() => setOpen(false)}\n className={secondaryButton}\n >\n {tr('coolandlovely.common.cancel', 'Cancel')}\n </button>\n </div>\n </div>\n );\n}\n\n/** The shopper's returns card on a paid order: one row per brand in the order. */\nexport function OrderReturns({ order }: { order: CooOrder }) {\n const tr = useTr();\n const paid = order.status === 'PAID' || order.status === 'PARTIALLY_REFUNDED';\n const requests = order.returnRequests ?? [];\n if (!paid && requests.length === 0) return null;\n\n const brandIds = [...new Set(order.items.map((item) => item.brandId))];\n const brandName = (brandId: string): string =>\n requests.find((request) => request.brandId === brandId)?.brandName ??\n order.fulfilments?.find((fulfilment) => fulfilment.brandId === brandId)?.brandName ??\n order.refunds?.find((refund) => refund.brandId === brandId)?.brandName ??\n tr('coolandlovely.orders.brand', 'Brand');\n const windowOpen = isReturnWindowOpen(order);\n\n return (\n <GlassCard treatment=\"plain\" className=\"space-y-4 p-4\" data-testid=\"order-returns\">\n <div className=\"space-y-1\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {tr('coolandlovely.returns.title', 'Returns')}\n </p>\n {paid && order.returnWindowEndsAt && (\n <p className=\"text-xs text-text-secondary\" data-testid=\"order-return-window\">\n {windowOpen\n ? tr(\n 'coolandlovely.returns.windowOpen',\n 'You can ask a brand to take back its pieces until {{date}}.',\n { date: formatDate(order.returnWindowEndsAt) }\n )\n : tr('coolandlovely.returns.windowClosed', 'The return window closed on {{date}}.', {\n date: formatDate(order.returnWindowEndsAt),\n })}\n </p>\n )}\n </div>\n {brandIds.map((brandId) => {\n const request = requests.find((entry) => entry.brandId === brandId);\n const eligible = canRequestReturn(order, brandId);\n if (!request && !eligible) return null;\n return (\n <div key={brandId} className=\"space-y-2\" data-testid=\"order-return\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-primary\">{brandName(brandId)}</span>\n {request && (\n <span data-testid=\"order-return-status\">\n <StatusBadge status={request.status} />\n </span>\n )}\n </div>\n {request ? (\n <RequestDetails request={request} />\n ) : (\n <RequestReturnForm orderId={order.id} brandId={brandId} />\n )}\n </div>\n );\n })}\n </GlassCard>\n );\n}\n\n/** The brand's view of the shopper's return request, with approve and decline. */\nexport function ReturnRequestPanel({ order }: { order: CooBrandOrder }) {\n const tr = useTr();\n const approve = useApproveReturnRequest();\n const decline = useDeclineReturnRequest();\n const [mode, setMode] = useState<'idle' | 'approve' | 'decline'>('idle');\n const [note, setNote] = useState('');\n const request = order.returnRequest;\n if (!request) return null;\n\n const busy = approve.isPending || decline.isPending;\n const error = approve.error ?? decline.error;\n const noteOk = mode === 'approve' ? note.trim() === '' || isValidText(note) : isValidText(note);\n const reset = () => {\n setMode('idle');\n setNote('');\n };\n const submit = () => {\n const input = { brandId: order.brandId, orderId: order.id };\n if (mode === 'approve') {\n approve.mutate({ ...input, note: note.trim() || null }, { onSuccess: reset });\n } else {\n decline.mutate({ ...input, note: note.trim() }, { onSuccess: reset });\n }\n };\n\n return (\n <div className=\"space-y-3 border-t border-border-seam pt-4\" data-testid=\"brand-return-panel\">\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n {tr('coolandlovely.returns.brandTitle', 'Return request')}\n </span>\n <span data-testid=\"brand-return-status\">\n <StatusBadge status={request.status} />\n </span>\n </div>\n <RequestDetails request={request} />\n\n {request.status === 'REQUESTED' &&\n (mode === 'idle' ? (\n <div className=\"grid grid-cols-2 gap-2\">\n <button\n type=\"button\"\n onClick={() => setMode('approve')}\n className={primaryButton}\n data-testid=\"brand-return-approve-open\"\n >\n <CheckCircle2 className=\"h-4 w-4\" />\n {tr('coolandlovely.returns.approve', 'Approve')}\n </button>\n <button\n type=\"button\"\n onClick={() => setMode('decline')}\n className={secondaryButton}\n data-testid=\"brand-return-decline-open\"\n >\n <XCircle className=\"h-4 w-4\" />\n {tr('coolandlovely.returns.decline', 'Decline')}\n </button>\n </div>\n ) : (\n <div className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">\n {mode === 'approve'\n ? tr(\n 'coolandlovely.returns.approveExplain',\n 'Approving refunds all of your pieces in this order to the shopper, with their share of tax, and reverses the creator commissions on them. This cannot be undone.'\n )\n : tr(\n 'coolandlovely.returns.declineExplain',\n 'The shopper sees your note. A declined return cannot be reopened.'\n )}\n </p>\n <TextAreaField\n id=\"coo-return-decision-note\"\n label={\n mode === 'approve'\n ? tr('coolandlovely.returns.approveNote', 'Note to the shopper (optional)')\n : tr('coolandlovely.returns.declineNote', 'Why are you declining?')\n }\n value={note}\n onChange={setNote}\n rows={3}\n />\n {error && (\n <p className=\"text-sm text-status-error-text\" role=\"alert\">\n {error.message}\n </p>\n )}\n <button\n type=\"button\"\n disabled={busy || !noteOk}\n onClick={submit}\n className={\n mode === 'approve'\n ? `${primaryButton} w-full`\n : `${secondaryButton} w-full text-status-error-text`\n }\n data-testid=\"brand-return-confirm\"\n >\n {busy && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {mode === 'approve'\n ? tr('coolandlovely.returns.approveConfirm', 'Approve and refund')\n : tr('coolandlovely.returns.declineConfirm', 'Decline return')}\n </button>\n <button\n type=\"button\"\n disabled={busy}\n onClick={reset}\n className={`${secondaryButton} w-full`}\n >\n {tr('coolandlovely.common.cancel', 'Cancel')}\n </button>\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ReturnRequests.js","names":[],"sources":["../../../src/coolandlovely/components/ReturnRequests.tsx"],"sourcesContent":["/**\n * Shopper return requests (BOFF-9900)\n * @module coolandlovely/components\n *\n * `OrderReturns` sits on the shopper's order detail: for each brand in a paid order the shopper\n * can ask for a return (`cooRequestReturn`) inside the return window, and sees the request and\n * the brand's decision once it exists. `ReturnRequestPanel` sits on the brand's order detail: the\n * brand approves the request, which refunds its pieces through the brand refund\n * (`cooApproveReturnRequest`), or declines it with a note (`cooDeclineReturnRequest`).\n */\n\nimport { useState } from 'react';\nimport { CheckCircle2, Loader2, Undo2, XCircle } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { useTr } from '../hooks';\nimport { useApproveReturnRequest, useDeclineReturnRequest } from '../hooks/useBrandOrdersApi';\nimport { useRequestReturn } from '../hooks/useCommerceApi';\nimport type { CooBrandOrder, CooOrder, CooReturnRequest } from '../types';\nimport { formatDate } from '../utils/formatters';\nimport { TextAreaField } from './Field';\nimport { StatusBadge } from './StatusBadge';\n\nconst MIN_TEXT = 3;\nconst MAX_TEXT = 1000;\nconst RETURNABLE_ORDER_STATUSES = ['PAID', 'PARTIALLY_REFUNDED'];\n\nconst isValidText = (value: string): boolean => {\n const trimmed = value.trim();\n return trimmed.length >= MIN_TEXT && trimmed.length <= MAX_TEXT;\n};\n\nconst primaryButton =\n 'inline-flex items-center justify-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-semibold text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50';\nconst secondaryButton =\n 'inline-flex items-center justify-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 disabled:opacity-50';\n\n/** Whether the return window of a paid order is still open. */\nexport function isReturnWindowOpen(order: Pick<CooOrder, 'returnWindowEndsAt'>, now = Date.now()) {\n if (!order.returnWindowEndsAt) return false;\n return new Date(order.returnWindowEndsAt).getTime() > now;\n}\n\n/** Whether the shopper can still ask the brand to take back its pieces of this order. */\nexport function canRequestReturn(order: CooOrder, brandId: string, now = Date.now()): boolean {\n if (!RETURNABLE_ORDER_STATUSES.includes(order.status)) return false;\n if (order.returnRequests?.some((request) => request.brandId === brandId)) return false;\n const refund = order.refunds?.find((entry) => entry.brandId === brandId);\n if (refund && (refund.status === 'COMPLETED' || refund.status === 'PENDING')) return false;\n return isReturnWindowOpen(order, now);\n}\n\nfunction RequestDetails({ request }: { request: CooReturnRequest }) {\n const tr = useTr();\n return (\n <div className=\"space-y-1 text-xs text-text-secondary\">\n <p>\n {tr('coolandlovely.returns.reasonShown', 'Reason: {{reason}}', { reason: request.reason })}\n </p>\n {request.status === 'REQUESTED' && (\n <p>\n {tr(\n 'coolandlovely.returns.awaitingBrand',\n 'Asked {{date}}. The brand will approve or decline it.',\n { date: formatDate(request.createdAt) }\n )}\n </p>\n )}\n {request.status !== 'REQUESTED' && request.decisionNote && (\n <p data-testid=\"return-decision-note\">\n {tr('coolandlovely.returns.brandNote', 'Brand’s note: {{note}}', {\n note: request.decisionNote,\n })}\n </p>\n )}\n {request.status !== 'REQUESTED' && request.decidedAt && (\n <p>\n {request.status === 'APPROVED'\n ? tr(\n 'coolandlovely.returns.approvedOn',\n 'Approved {{date}}. The pieces were refunded.',\n {\n date: formatDate(request.decidedAt),\n }\n )\n : tr('coolandlovely.returns.declinedOn', 'Declined {{date}}.', {\n date: formatDate(request.decidedAt),\n })}\n </p>\n )}\n </div>\n );\n}\n\nfunction RequestReturnForm({ orderId, brandId }: { orderId: string; brandId: string }) {\n const tr = useTr();\n const request = useRequestReturn();\n const [open, setOpen] = useState(false);\n const [reason, setReason] = useState('');\n\n if (!open) {\n return (\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n className={secondaryButton}\n data-testid=\"order-return-open\"\n >\n <Undo2 className=\"h-4 w-4\" />\n {tr('coolandlovely.returns.request', 'Request a return')}\n </button>\n );\n }\n return (\n <div className=\"space-y-2\">\n <TextAreaField\n id={`coo-return-reason-${brandId}`}\n label={tr('coolandlovely.returns.reason', 'Why are you returning these pieces?')}\n hint={tr('coolandlovely.returns.reasonHint', 'Shown to the brand.')}\n value={reason}\n onChange={setReason}\n rows={3}\n />\n {request.error && (\n <p className=\"text-sm text-status-error-text\" role=\"alert\">\n {request.error.message}\n </p>\n )}\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n disabled={request.isPending || !isValidText(reason)}\n onClick={() =>\n request.mutate(\n { orderId, brandId, reason: reason.trim() },\n { onSuccess: () => setOpen(false) }\n )\n }\n className={primaryButton}\n data-testid=\"order-return-submit\"\n >\n {request.isPending && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {tr('coolandlovely.returns.send', 'Send return request')}\n </button>\n <button\n type=\"button\"\n disabled={request.isPending}\n onClick={() => setOpen(false)}\n className={secondaryButton}\n >\n {tr('coolandlovely.common.cancel', 'Cancel')}\n </button>\n </div>\n </div>\n );\n}\n\n/** The shopper's returns card on a paid order: one row per brand in the order. */\nexport function OrderReturns({ order }: { order: CooOrder }) {\n const tr = useTr();\n const paid = order.status === 'PAID' || order.status === 'PARTIALLY_REFUNDED';\n const requests = order.returnRequests ?? [];\n if (!paid && requests.length === 0) return null;\n\n const brandIds = [...new Set(order.items.map((item) => item.brandId))];\n const brandName = (brandId: string): string =>\n requests.find((request) => request.brandId === brandId)?.brandName ??\n order.fulfilments?.find((fulfilment) => fulfilment.brandId === brandId)?.brandName ??\n order.refunds?.find((refund) => refund.brandId === brandId)?.brandName ??\n tr('coolandlovely.orders.brand', 'Brand');\n const windowOpen = isReturnWindowOpen(order);\n\n return (\n <GlassCard treatment=\"plain\" className=\"space-y-4 p-4\" data-testid=\"order-returns\">\n <div className=\"space-y-1\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {tr('coolandlovely.returns.title', 'Returns')}\n </p>\n {paid && order.returnWindowEndsAt && (\n <p className=\"text-xs text-text-secondary\" data-testid=\"order-return-window\">\n {windowOpen\n ? tr(\n 'coolandlovely.returns.windowOpen',\n 'You can ask a brand to take back its pieces until {{date}}.',\n { date: formatDate(order.returnWindowEndsAt) }\n )\n : tr('coolandlovely.returns.windowClosed', 'The return window closed on {{date}}.', {\n date: formatDate(order.returnWindowEndsAt),\n })}\n </p>\n )}\n </div>\n {brandIds.map((brandId) => {\n const request = requests.find((entry) => entry.brandId === brandId);\n const eligible = canRequestReturn(order, brandId);\n if (!request && !eligible) return null;\n return (\n <div key={brandId} className=\"space-y-2\" data-testid=\"order-return\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-primary\">{brandName(brandId)}</span>\n {request && (\n <span data-testid=\"order-return-status\">\n <StatusBadge status={request.status} />\n </span>\n )}\n </div>\n {request ? (\n <RequestDetails request={request} />\n ) : (\n <RequestReturnForm orderId={order.id} brandId={brandId} />\n )}\n </div>\n );\n })}\n </GlassCard>\n );\n}\n\n/** The brand's view of the shopper's return request, with approve and decline. */\nexport function ReturnRequestPanel({ order }: { order: CooBrandOrder }) {\n const tr = useTr();\n const approve = useApproveReturnRequest();\n const decline = useDeclineReturnRequest();\n const [mode, setMode] = useState<'idle' | 'approve' | 'decline'>('idle');\n const [note, setNote] = useState('');\n const request = order.returnRequest;\n if (!request) return null;\n\n const busy = approve.isPending || decline.isPending;\n const error = approve.error ?? decline.error;\n const noteOk = mode === 'approve' ? note.trim() === '' || isValidText(note) : isValidText(note);\n const reset = () => {\n setMode('idle');\n setNote('');\n };\n const submit = () => {\n const input = { brandId: order.brandId, orderId: order.id };\n if (mode === 'approve') {\n approve.mutate({ ...input, note: note.trim() || null }, { onSuccess: reset });\n } else {\n decline.mutate({ ...input, note: note.trim() }, { onSuccess: reset });\n }\n };\n\n return (\n <div className=\"space-y-3 border-t border-border-seam pt-4\" data-testid=\"brand-return-panel\">\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">\n {tr('coolandlovely.returns.brandTitle', 'Return request')}\n </span>\n <span data-testid=\"brand-return-status\">\n <StatusBadge status={request.status} />\n </span>\n </div>\n <RequestDetails request={request} />\n\n {request.status === 'REQUESTED' &&\n (mode === 'idle' ? (\n <div className=\"grid grid-cols-2 gap-2\">\n <button\n type=\"button\"\n onClick={() => setMode('approve')}\n className={primaryButton}\n data-testid=\"brand-return-approve-open\"\n >\n <CheckCircle2 className=\"h-4 w-4\" />\n {tr('coolandlovely.returns.approve', 'Approve')}\n </button>\n <button\n type=\"button\"\n onClick={() => setMode('decline')}\n className={secondaryButton}\n data-testid=\"brand-return-decline-open\"\n >\n <XCircle className=\"h-4 w-4\" />\n {tr('coolandlovely.returns.decline', 'Decline')}\n </button>\n </div>\n ) : (\n <div className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">\n {mode === 'approve'\n ? tr(\n 'coolandlovely.returns.approveExplain',\n 'Approving refunds all of your pieces in this order to the shopper, with their share of tax, and reverses the creator commissions on them. This cannot be undone.'\n )\n : tr(\n 'coolandlovely.returns.declineExplain',\n 'The shopper sees your note. A declined return cannot be reopened.'\n )}\n </p>\n <TextAreaField\n id=\"coo-return-decision-note\"\n label={\n mode === 'approve'\n ? tr('coolandlovely.returns.approveNote', 'Note to the shopper (optional)')\n : tr('coolandlovely.returns.declineNote', 'Why are you declining?')\n }\n value={note}\n onChange={setNote}\n rows={3}\n />\n {error && (\n <p className=\"text-sm text-status-error-text\" role=\"alert\">\n {error.message}\n </p>\n )}\n <button\n type=\"button\"\n disabled={busy || !noteOk}\n onClick={submit}\n className={\n mode === 'approve'\n ? `${primaryButton} w-full`\n : `${secondaryButton} w-full text-status-error-text`\n }\n data-testid=\"brand-return-confirm\"\n >\n {busy && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {mode === 'approve'\n ? tr('coolandlovely.returns.approveConfirm', 'Approve and refund')\n : tr('coolandlovely.returns.declineConfirm', 'Decline return')}\n </button>\n <button\n type=\"button\"\n disabled={busy}\n onClick={reset}\n className={`${secondaryButton} w-full`}\n >\n {tr('coolandlovely.common.cancel', 'Cancel')}\n </button>\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ShopItemCard.js","names":[],"sources":["../../../src/coolandlovely/components/ShopItemCard.tsx"],"sourcesContent":["/**\n * ShopItemCard — a product card for the \"shop the look\" grid.\n * @module coolandlovely/components\n *\n * Image + name + brand line + price + a rose \"Shop\" chevron that drills through\n * to the item detail page (the platform has no external shop URL, so \"Shop\" is\n * the internal drill-through). Reuses `Thumbnail`/`FileImage` for imagery.\n */\n\nimport type { FC } from 'react';\nimport { ChevronRight, ShoppingBag } from 'lucide-react';\nimport { Thumbnail } from './Thumbnail';\nimport { formatPrice } from '../utils/formatters';\nimport { cn } from '../utils/cn';\n\ninterface ShopItemCardProps {\n id: string;\n name: string;\n brandName?: string;\n price: number;\n currency: string;\n imageFileId?: string | null;\n onOpen: () => void;\n className?: string;\n}\n\nexport const ShopItemCard: FC<ShopItemCardProps> = ({\n id,\n name,\n brandName,\n price,\n currency,\n imageFileId,\n onOpen,\n className,\n}) => {\n return (\n <button\n type=\"button\"\n onClick={onOpen}\n aria-label={`Shop ${name}`}\n className={cn(\n 'group flex w-full flex-col overflow-hidden rounded-xl border border-border-seam bg-bg-surface text-left transition-shadow hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n className\n )}\n >\n <Thumbnail\n icon={<ShoppingBag className=\"h-7 w-7\" />}\n seed={id}\n fileId={imageFileId}\n alt={name}\n className=\"aspect-square w-full rounded-none\"\n />\n <div className=\"flex flex-col gap-0.5 p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{name}</p>\n {brandName ? <p className=\"truncate text-xs text-text-secondary\">{brandName}</p> : null}\n <div className=\"mt-2 flex items-center justify-between gap-2\">\n <span className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {formatPrice(price, currency)}\n </span>\n <span className=\"inline-flex items-center gap-0.5 text-xs font-semibold text-accent-primary\">\n Shop\n <ChevronRight className=\"h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5\" />\n </span>\n </div>\n </div>\n </button>\n );\n};\n"],"mappings":";;;;;;AA0BA,IAAa,KAAuC,EAClD,OACA,SACA,cACA,UACA,aACA,gBACA,WACA,mBAGE,kBAAC,UAAD;CACE,MAAK;CACL,SAAS;CACT,cAAY,QAAQ;CACpB,WAAW,EACT,0OACA,
|
|
1
|
+
{"version":3,"file":"ShopItemCard.js","names":[],"sources":["../../../src/coolandlovely/components/ShopItemCard.tsx"],"sourcesContent":["/**\n * ShopItemCard — a product card for the \"shop the look\" grid.\n * @module coolandlovely/components\n *\n * Image + name + brand line + price + a rose \"Shop\" chevron that drills through\n * to the item detail page (the platform has no external shop URL, so \"Shop\" is\n * the internal drill-through). Reuses `Thumbnail`/`FileImage` for imagery.\n */\n\nimport type { FC } from 'react';\nimport { ChevronRight, ShoppingBag } from 'lucide-react';\nimport { Thumbnail } from './Thumbnail';\nimport { formatPrice } from '../utils/formatters';\nimport { cn } from '../utils/cn';\n\ninterface ShopItemCardProps {\n id: string;\n name: string;\n brandName?: string;\n price: number;\n currency: string;\n imageFileId?: string | null;\n onOpen: () => void;\n className?: string;\n}\n\nexport const ShopItemCard: FC<ShopItemCardProps> = ({\n id,\n name,\n brandName,\n price,\n currency,\n imageFileId,\n onOpen,\n className,\n}) => {\n return (\n <button\n type=\"button\"\n onClick={onOpen}\n aria-label={`Shop ${name}`}\n className={cn(\n 'group flex w-full flex-col overflow-hidden rounded-xl border border-border-seam bg-bg-surface text-left transition-shadow hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n className\n )}\n >\n <Thumbnail\n icon={<ShoppingBag className=\"h-7 w-7\" />}\n seed={id}\n fileId={imageFileId}\n alt={name}\n className=\"aspect-square w-full rounded-none\"\n />\n <div className=\"flex flex-col gap-0.5 p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{name}</p>\n {brandName ? <p className=\"truncate text-xs text-text-secondary\">{brandName}</p> : null}\n <div className=\"mt-2 flex items-center justify-between gap-2\">\n <span className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {formatPrice(price, currency)}\n </span>\n <span className=\"inline-flex items-center gap-0.5 text-xs font-semibold text-accent-primary\">\n Shop\n <ChevronRight className=\"h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5\" />\n </span>\n </div>\n </div>\n </button>\n );\n};\n"],"mappings":";;;;;;AA0BA,IAAa,KAAuC,EAClD,OACA,SACA,cACA,UACA,aACA,gBACA,WACA,mBAGE,kBAAC,UAAD;CACE,MAAK;CACL,SAAS;CACT,cAAY,QAAQ;CACpB,WAAW,EACT,0OACA,CACF;WAPF,CASE,kBAAC,GAAD;EACE,MAAM,kBAAC,GAAD,EAAa,WAAU,UAAW,CAAA;EACxC,MAAM;EACN,QAAQ;EACR,KAAK;EACL,WAAU;CACX,CAAA,GACD,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,KAAD;IAAG,WAAU;cAAkD;GAAQ,CAAA;GACtE,IAAY,kBAAC,KAAD;IAAG,WAAU;cAAwC;GAAa,CAAA,IAAI;GACnF,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eACb,EAAY,GAAO,CAAQ;IACxB,CAAA,GACN,kBAAC,QAAD;KAAM,WAAU;eAAhB,CAA6F,QAE3F,kBAAC,GAAD,EAAc,WAAU,+DAAgE,CAAA,CACpF;MACH;;EACF;GACC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StateViews.js","names":[],"sources":["../../../src/coolandlovely/components/StateViews.tsx"],"sourcesContent":["/**\n * Loading / Error state views\n * @module coolandlovely/components\n *\n * Calm, on-token loading and error panels reused across the domain pages so\n * every screen has consistent loading/empty/error treatment.\n */\n\nimport type { FC } from 'react';\nimport { AlertCircle, Loader2, RotateCw } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { useTr } from '../hooks';\n\ninterface LoadingViewProps {\n label?: string;\n className?: string;\n}\n\nexport const LoadingView: FC<LoadingViewProps> = ({ label = 'Loading…', className }) => (\n <div className={cn('flex min-h-[40vh] flex-col items-center justify-center gap-3', className)}>\n <Loader2 className=\"h-8 w-8 animate-spin text-primary\" />\n <p className=\"text-sm text-text-secondary\">{label}</p>\n </div>\n);\n\ninterface ErrorViewProps {\n title?: string;\n message?: string;\n onRetry?: () => void;\n className?: string;\n}\n\nexport const ErrorView: FC<ErrorViewProps> = ({ title, message, onRetry, className }) => {\n const tr = useTr();\n return (\n <div\n className={cn(\n 'flex min-h-[40vh] flex-col items-center justify-center gap-3 text-center',\n className\n )}\n >\n <AlertCircle className=\"h-10 w-10 text-status-error-text\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {title ?? tr('coolandlovely.error.title', 'Something went wrong')}\n </h2>\n <p className=\"max-w-sm text-sm text-text-secondary\">\n {message ??\n tr('coolandlovely.error.message', 'We could not load this data. Please try again.')}\n </p>\n {onRetry && (\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"mt-1 inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <RotateCw className=\"h-4 w-4\" /> {tr('coolandlovely.error.retry', 'Retry')}\n </button>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;AAkBA,IAAa,KAAqC,EAAE,WAAQ,YAAY,mBACtE,kBAAC,OAAD;CAAK,WAAW,EAAG,gEAAgE,
|
|
1
|
+
{"version":3,"file":"StateViews.js","names":[],"sources":["../../../src/coolandlovely/components/StateViews.tsx"],"sourcesContent":["/**\n * Loading / Error state views\n * @module coolandlovely/components\n *\n * Calm, on-token loading and error panels reused across the domain pages so\n * every screen has consistent loading/empty/error treatment.\n */\n\nimport type { FC } from 'react';\nimport { AlertCircle, Loader2, RotateCw } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { useTr } from '../hooks';\n\ninterface LoadingViewProps {\n label?: string;\n className?: string;\n}\n\nexport const LoadingView: FC<LoadingViewProps> = ({ label = 'Loading…', className }) => (\n <div className={cn('flex min-h-[40vh] flex-col items-center justify-center gap-3', className)}>\n <Loader2 className=\"h-8 w-8 animate-spin text-primary\" />\n <p className=\"text-sm text-text-secondary\">{label}</p>\n </div>\n);\n\ninterface ErrorViewProps {\n title?: string;\n message?: string;\n onRetry?: () => void;\n className?: string;\n}\n\nexport const ErrorView: FC<ErrorViewProps> = ({ title, message, onRetry, className }) => {\n const tr = useTr();\n return (\n <div\n className={cn(\n 'flex min-h-[40vh] flex-col items-center justify-center gap-3 text-center',\n className\n )}\n >\n <AlertCircle className=\"h-10 w-10 text-status-error-text\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {title ?? tr('coolandlovely.error.title', 'Something went wrong')}\n </h2>\n <p className=\"max-w-sm text-sm text-text-secondary\">\n {message ??\n tr('coolandlovely.error.message', 'We could not load this data. Please try again.')}\n </p>\n {onRetry && (\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"mt-1 inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <RotateCw className=\"h-4 w-4\" /> {tr('coolandlovely.error.retry', 'Retry')}\n </button>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;AAkBA,IAAa,KAAqC,EAAE,WAAQ,YAAY,mBACtE,kBAAC,OAAD;CAAK,WAAW,EAAG,gEAAgE,CAAS;WAA5F,CACE,kBAAC,GAAD,EAAS,WAAU,oCAAqC,CAAA,GACxD,kBAAC,KAAD;EAAG,WAAU;YAA+B;CAAS,CAAA,CAClD;IAUM,KAAiC,EAAE,UAAO,YAAS,YAAS,mBAAgB;CACvF,IAAM,IAAK,EAAM;CACjB,OACE,kBAAC,OAAD;EACE,WAAW,EACT,4EACA,CACF;YAJF;GAME,kBAAC,GAAD,EAAa,WAAU,mCAAoC,CAAA;GAC3D,kBAAC,MAAD;IAAI,WAAU;cACX,KAAS,EAAG,6BAA6B,sBAAsB;GAC9D,CAAA;GACJ,kBAAC,KAAD;IAAG,WAAU;cACV,KACC,EAAG,+BAA+B,gDAAgD;GACnF,CAAA;GACF,KACC,kBAAC,UAAD;IACE,MAAK;IACL,SAAS;IACT,WAAU;cAHZ;KAKE,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA;KAAC;KAAE,EAAG,6BAA6B,OAAO;IACnE;;EAEP;;AAET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/coolandlovely/components/StatusBadge.tsx"],"sourcesContent":["/**\n * Status Badge Component\n * @module coolandlovely/components\n *\n * A colored badge based on status value with semantic color mapping\n * for various order, cart, and investment statuses.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport { useTr } from '../hooks';\n\ninterface StatusBadgeProps {\n status: string;\n className?: string;\n}\n\ntype BadgeVariant = 'success' | 'warning' | 'destructive' | 'secondary' | 'info' | 'outline';\n\nconst variantStyles: Record<BadgeVariant, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n destructive: 'bg-status-error-bg-subtle text-status-error-text',\n secondary: 'bg-bg-sunken text-text-secondary',\n info: 'bg-accent-blue-subtle text-accent-blue',\n outline: 'border border-border-seam text-text-primary',\n};\n\nconst dotStyles: Record<BadgeVariant, string> = {\n success: 'bg-status-success-text',\n warning: 'bg-status-warning-text',\n destructive: 'bg-status-error-text',\n secondary: 'bg-text-secondary',\n info: 'bg-accent-blue',\n outline: 'bg-text-secondary',\n};\n\nconst statusVariantMap: Record<string, BadgeVariant> = {\n // Brand lifecycle\n draft: 'secondary',\n pending_review: 'warning',\n published: 'success',\n suspended: 'destructive',\n archived: 'outline',\n\n // Collection lifecycle\n scheduled: 'info',\n live: 'success',\n ended: 'outline',\n\n // Style item status\n active: 'success',\n out_of_stock: 'warning',\n retired: 'outline',\n\n // Collaboration lifecycle\n proposed: 'secondary',\n accepted: 'info',\n in_progress: 'warning',\n declined: 'destructive',\n cancelled: 'destructive',\n\n // Visibility\n public: 'success',\n followers: 'info',\n private: 'secondary',\n\n // Wardrobe source\n owned: 'success',\n wishlist: 'info',\n purchased_on_platform: 'success',\n gifted: 'warning',\n\n // Orders + creator commissions (BOFF-3370)\n pending_payment: 'warning',\n paid: 'success',\n pending: 'secondary',\n synced: 'success',\n\n // Collaboration fees (BOFF-9637)\n awaiting_payment: 'warning',\n\n // Brand fulfilment (BOFF-9636)\n unfulfilled: 'warning',\n confirmed: 'info',\n shipped: 'info',\n delivered: 'success',\n\n // Refunds and commission reversal (BOFF-9702)\n partially_refunded: 'warning',\n refunded: 'outline',\n reversed: 'outline',\n not_reversible: 'warning',\n\n // Shopper return requests (BOFF-9900)\n requested: 'warning',\n approved: 'success',\n\n // Generic\n inactive: 'secondary',\n error: 'destructive',\n failed: 'destructive',\n completed: 'success',\n};\n\n/** Statuses that represent a live/active state get a calm opacity pulse on the dot. */\nconst liveStatuses = new Set(['live', 'published', 'active', 'in_progress']);\n\nconst STATUS_LABEL_KEYS: Record<string, string> = {\n draft: 'coolandlovely.status.draft',\n pending_review: 'coolandlovely.status.pendingReview',\n published: 'coolandlovely.status.published',\n suspended: 'coolandlovely.status.suspended',\n archived: 'coolandlovely.status.archived',\n scheduled: 'coolandlovely.status.scheduled',\n live: 'coolandlovely.status.live',\n ended: 'coolandlovely.status.ended',\n active: 'coolandlovely.status.active',\n out_of_stock: 'coolandlovely.status.outOfStock',\n retired: 'coolandlovely.status.retired',\n proposed: 'coolandlovely.status.proposed',\n accepted: 'coolandlovely.status.accepted',\n in_progress: 'coolandlovely.status.inProgress',\n declined: 'coolandlovely.status.declined',\n cancelled: 'coolandlovely.status.cancelled',\n public: 'coolandlovely.status.public',\n followers: 'coolandlovely.status.followers',\n private: 'coolandlovely.status.private',\n owned: 'coolandlovely.status.owned',\n wishlist: 'coolandlovely.status.wishlist',\n purchased_on_platform: 'coolandlovely.status.purchasedOnPlatform',\n gifted: 'coolandlovely.status.gifted',\n inactive: 'coolandlovely.status.inactive',\n error: 'coolandlovely.status.error',\n failed: 'coolandlovely.status.failed',\n completed: 'coolandlovely.status.completed',\n unfulfilled: 'coolandlovely.status.unfulfilled',\n confirmed: 'coolandlovely.status.confirmed',\n shipped: 'coolandlovely.status.shipped',\n delivered: 'coolandlovely.status.delivered',\n partially_refunded: 'coolandlovely.status.partiallyRefunded',\n refunded: 'coolandlovely.status.refunded',\n reversed: 'coolandlovely.status.reversed',\n not_reversible: 'coolandlovely.status.notReversible',\n awaiting_payment: 'coolandlovely.status.awaitingPayment',\n requested: 'coolandlovely.status.returnRequested',\n approved: 'coolandlovely.status.approved',\n};\n\nfunction formatStatusLabel(status: string): string {\n return status.replace(/_/g, ' ').replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n\nexport const StatusBadge: FC<StatusBadgeProps> = ({ status, className }) => {\n const tr = useTr();\n const key = status.toLowerCase();\n const variant = statusVariantMap[key] || 'secondary';\n const isLive = liveStatuses.has(key);\n const labelKey = STATUS_LABEL_KEYS[key];\n const label = labelKey ? tr(labelKey, formatStatusLabel(status)) : formatStatusLabel(status);\n\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',\n variantStyles[variant],\n className\n )}\n >\n <span\n aria-hidden=\"true\"\n className={cn(\n 'h-1.5 w-1.5 rounded-full',\n dotStyles[variant],\n isLive && 'motion-safe:animate-vc-glow-pulse'\n )}\n />\n {label}\n </span>\n );\n};\n"],"mappings":";;;;;AAmBA,IAAM,IAA8C;CAClD,SAAS;CACT,SAAS;CACT,aAAa;CACb,WAAW;CACX,MAAM;CACN,SAAS;
|
|
1
|
+
{"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/coolandlovely/components/StatusBadge.tsx"],"sourcesContent":["/**\n * Status Badge Component\n * @module coolandlovely/components\n *\n * A colored badge based on status value with semantic color mapping\n * for various order, cart, and investment statuses.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport { useTr } from '../hooks';\n\ninterface StatusBadgeProps {\n status: string;\n className?: string;\n}\n\ntype BadgeVariant = 'success' | 'warning' | 'destructive' | 'secondary' | 'info' | 'outline';\n\nconst variantStyles: Record<BadgeVariant, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n destructive: 'bg-status-error-bg-subtle text-status-error-text',\n secondary: 'bg-bg-sunken text-text-secondary',\n info: 'bg-accent-blue-subtle text-accent-blue',\n outline: 'border border-border-seam text-text-primary',\n};\n\nconst dotStyles: Record<BadgeVariant, string> = {\n success: 'bg-status-success-text',\n warning: 'bg-status-warning-text',\n destructive: 'bg-status-error-text',\n secondary: 'bg-text-secondary',\n info: 'bg-accent-blue',\n outline: 'bg-text-secondary',\n};\n\nconst statusVariantMap: Record<string, BadgeVariant> = {\n // Brand lifecycle\n draft: 'secondary',\n pending_review: 'warning',\n published: 'success',\n suspended: 'destructive',\n archived: 'outline',\n\n // Collection lifecycle\n scheduled: 'info',\n live: 'success',\n ended: 'outline',\n\n // Style item status\n active: 'success',\n out_of_stock: 'warning',\n retired: 'outline',\n\n // Collaboration lifecycle\n proposed: 'secondary',\n accepted: 'info',\n in_progress: 'warning',\n declined: 'destructive',\n cancelled: 'destructive',\n\n // Visibility\n public: 'success',\n followers: 'info',\n private: 'secondary',\n\n // Wardrobe source\n owned: 'success',\n wishlist: 'info',\n purchased_on_platform: 'success',\n gifted: 'warning',\n\n // Orders + creator commissions (BOFF-3370)\n pending_payment: 'warning',\n paid: 'success',\n pending: 'secondary',\n synced: 'success',\n\n // Collaboration fees (BOFF-9637)\n awaiting_payment: 'warning',\n\n // Brand fulfilment (BOFF-9636)\n unfulfilled: 'warning',\n confirmed: 'info',\n shipped: 'info',\n delivered: 'success',\n\n // Refunds and commission reversal (BOFF-9702)\n partially_refunded: 'warning',\n refunded: 'outline',\n reversed: 'outline',\n not_reversible: 'warning',\n\n // Shopper return requests (BOFF-9900)\n requested: 'warning',\n approved: 'success',\n\n // Generic\n inactive: 'secondary',\n error: 'destructive',\n failed: 'destructive',\n completed: 'success',\n};\n\n/** Statuses that represent a live/active state get a calm opacity pulse on the dot. */\nconst liveStatuses = new Set(['live', 'published', 'active', 'in_progress']);\n\nconst STATUS_LABEL_KEYS: Record<string, string> = {\n draft: 'coolandlovely.status.draft',\n pending_review: 'coolandlovely.status.pendingReview',\n published: 'coolandlovely.status.published',\n suspended: 'coolandlovely.status.suspended',\n archived: 'coolandlovely.status.archived',\n scheduled: 'coolandlovely.status.scheduled',\n live: 'coolandlovely.status.live',\n ended: 'coolandlovely.status.ended',\n active: 'coolandlovely.status.active',\n out_of_stock: 'coolandlovely.status.outOfStock',\n retired: 'coolandlovely.status.retired',\n proposed: 'coolandlovely.status.proposed',\n accepted: 'coolandlovely.status.accepted',\n in_progress: 'coolandlovely.status.inProgress',\n declined: 'coolandlovely.status.declined',\n cancelled: 'coolandlovely.status.cancelled',\n public: 'coolandlovely.status.public',\n followers: 'coolandlovely.status.followers',\n private: 'coolandlovely.status.private',\n owned: 'coolandlovely.status.owned',\n wishlist: 'coolandlovely.status.wishlist',\n purchased_on_platform: 'coolandlovely.status.purchasedOnPlatform',\n gifted: 'coolandlovely.status.gifted',\n inactive: 'coolandlovely.status.inactive',\n error: 'coolandlovely.status.error',\n failed: 'coolandlovely.status.failed',\n completed: 'coolandlovely.status.completed',\n unfulfilled: 'coolandlovely.status.unfulfilled',\n confirmed: 'coolandlovely.status.confirmed',\n shipped: 'coolandlovely.status.shipped',\n delivered: 'coolandlovely.status.delivered',\n partially_refunded: 'coolandlovely.status.partiallyRefunded',\n refunded: 'coolandlovely.status.refunded',\n reversed: 'coolandlovely.status.reversed',\n not_reversible: 'coolandlovely.status.notReversible',\n awaiting_payment: 'coolandlovely.status.awaitingPayment',\n requested: 'coolandlovely.status.returnRequested',\n approved: 'coolandlovely.status.approved',\n};\n\nfunction formatStatusLabel(status: string): string {\n return status.replace(/_/g, ' ').replace(/\\b\\w/g, (char) => char.toUpperCase());\n}\n\nexport const StatusBadge: FC<StatusBadgeProps> = ({ status, className }) => {\n const tr = useTr();\n const key = status.toLowerCase();\n const variant = statusVariantMap[key] || 'secondary';\n const isLive = liveStatuses.has(key);\n const labelKey = STATUS_LABEL_KEYS[key];\n const label = labelKey ? tr(labelKey, formatStatusLabel(status)) : formatStatusLabel(status);\n\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',\n variantStyles[variant],\n className\n )}\n >\n <span\n aria-hidden=\"true\"\n className={cn(\n 'h-1.5 w-1.5 rounded-full',\n dotStyles[variant],\n isLive && 'motion-safe:animate-vc-glow-pulse'\n )}\n />\n {label}\n </span>\n );\n};\n"],"mappings":";;;;;AAmBA,IAAM,IAA8C;CAClD,SAAS;CACT,SAAS;CACT,aAAa;CACb,WAAW;CACX,MAAM;CACN,SAAS;AACX,GAEM,IAA0C;CAC9C,SAAS;CACT,SAAS;CACT,aAAa;CACb,WAAW;CACX,MAAM;CACN,SAAS;AACX,GAEM,IAAiD;CAErD,OAAO;CACP,gBAAgB;CAChB,WAAW;CACX,WAAW;CACX,UAAU;CAGV,WAAW;CACX,MAAM;CACN,OAAO;CAGP,QAAQ;CACR,cAAc;CACd,SAAS;CAGT,UAAU;CACV,UAAU;CACV,aAAa;CACb,UAAU;CACV,WAAW;CAGX,QAAQ;CACR,WAAW;CACX,SAAS;CAGT,OAAO;CACP,UAAU;CACV,uBAAuB;CACvB,QAAQ;CAGR,iBAAiB;CACjB,MAAM;CACN,SAAS;CACT,QAAQ;CAGR,kBAAkB;CAGlB,aAAa;CACb,WAAW;CACX,SAAS;CACT,WAAW;CAGX,oBAAoB;CACpB,UAAU;CACV,UAAU;CACV,gBAAgB;CAGhB,WAAW;CACX,UAAU;CAGV,UAAU;CACV,OAAO;CACP,QAAQ;CACR,WAAW;AACb,GAGM,oBAAe,IAAI,IAAI;CAAC;CAAQ;CAAa;CAAU;AAAa,CAAC,GAErE,IAA4C;CAChD,OAAO;CACP,gBAAgB;CAChB,WAAW;CACX,WAAW;CACX,UAAU;CACV,WAAW;CACX,MAAM;CACN,OAAO;CACP,QAAQ;CACR,cAAc;CACd,SAAS;CACT,UAAU;CACV,UAAU;CACV,aAAa;CACb,UAAU;CACV,WAAW;CACX,QAAQ;CACR,WAAW;CACX,SAAS;CACT,OAAO;CACP,UAAU;CACV,uBAAuB;CACvB,QAAQ;CACR,UAAU;CACV,OAAO;CACP,QAAQ;CACR,WAAW;CACX,aAAa;CACb,WAAW;CACX,SAAS;CACT,WAAW;CACX,oBAAoB;CACpB,UAAU;CACV,UAAU;CACV,gBAAgB;CAChB,kBAAkB;CAClB,WAAW;CACX,UAAU;AACZ;AAEA,SAAS,EAAkB,GAAwB;CACjD,OAAO,EAAO,QAAQ,MAAM,GAAG,CAAC,CAAC,QAAQ,UAAU,MAAS,EAAK,YAAY,CAAC;AAChF;AAEA,IAAa,KAAqC,EAAE,WAAQ,mBAAgB;CAC1E,IAAM,IAAK,EAAM,GACX,IAAM,EAAO,YAAY,GACzB,IAAU,EAAiB,MAAQ,aACnC,IAAS,EAAa,IAAI,CAAG,GAC7B,IAAW,EAAkB,IAC7B,IAAQ,IAAW,EAAG,GAAU,EAAkB,CAAM,CAAC,IAAI,EAAkB,CAAM;CAE3F,OACE,kBAAC,QAAD;EACE,WAAW,EACT,mFACA,EAAc,IACd,CACF;YALF,CAOE,kBAAC,QAAD;GACE,eAAY;GACZ,WAAW,EACT,4BACA,EAAU,IACV,KAAU,mCACZ;EACD,CAAA,GACA,CACG;;AAEV"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StepTracker.js","names":[],"sources":["../../../src/coolandlovely/components/StepTracker.tsx"],"sourcesContent":["/**\n * StepTracker — a horizontal lifecycle tracker for any `Coo*Status` machine.\n * @module coolandlovely/components\n *\n * Renders a row of numbered nodes with connectors: steps before `current` are\n * done (accent-filled + check), the `current` step is ringed, upcoming steps are\n * muted. Token-only. Used by the collaboration lifecycle (Proposed → Accepted →\n * In progress → Published) and reusable for brand/collection lifecycles.\n */\n\nimport type { FC } from 'react';\nimport { Check } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { titleCase } from '../utils/formatters';\n\ninterface StepTrackerProps {\n /** Ordered step values (e.g. ['PROPOSED','ACCEPTED','IN_PROGRESS','PUBLISHED']). */\n steps: string[];\n /** The current step value — must be one of `steps`. */\n current: string;\n className?: string;\n}\n\nexport const StepTracker: FC<StepTrackerProps> = ({ steps, current, className }) => {\n const foundIdx = steps.findIndex((step) => step === current);\n const currentIdx = foundIdx === -1 ? 0 : foundIdx;\n\n return (\n <ol className={cn('flex w-full items-start', className)}>\n {steps.map((step, index) => {\n const state = index < currentIdx ? 'done' : index === currentIdx ? 'current' : 'upcoming';\n const connectorDone = index <= currentIdx;\n return (\n <li key={step} className=\"relative flex flex-1 flex-col items-center gap-2 text-center\">\n {index > 0 ? (\n <span\n aria-hidden=\"true\"\n className={cn(\n 'absolute left-[-50%] top-4 z-0 h-0.5 w-full -translate-y-1/2',\n connectorDone ? 'bg-accent-primary' : 'bg-border-seam'\n )}\n />\n ) : null}\n <span\n className={cn(\n 'relative z-10 flex h-8 w-8 items-center justify-center rounded-full text-xs font-semibold tabular-nums transition-colors',\n state === 'done' && 'bg-accent-primary text-action-primary-text',\n state === 'current' &&\n 'border-2 border-accent-primary bg-bg-surface text-accent-primary',\n state === 'upcoming' &&\n 'border border-border-seam bg-bg-surface text-text-secondary'\n )}\n >\n {state === 'done' ? <Check className=\"h-4 w-4\" /> : index + 1}\n </span>\n <span\n className={cn(\n 'text-xs font-medium',\n state === 'upcoming' ? 'text-text-secondary' : 'text-text-primary'\n )}\n >\n {titleCase(step)}\n </span>\n </li>\n );\n })}\n </ol>\n );\n};\n"],"mappings":";;;;;AAuBA,IAAa,KAAqC,EAAE,UAAO,YAAS,mBAAgB;CAClF,IAAM,IAAW,EAAM,WAAW,MAAS,MAAS,
|
|
1
|
+
{"version":3,"file":"StepTracker.js","names":[],"sources":["../../../src/coolandlovely/components/StepTracker.tsx"],"sourcesContent":["/**\n * StepTracker — a horizontal lifecycle tracker for any `Coo*Status` machine.\n * @module coolandlovely/components\n *\n * Renders a row of numbered nodes with connectors: steps before `current` are\n * done (accent-filled + check), the `current` step is ringed, upcoming steps are\n * muted. Token-only. Used by the collaboration lifecycle (Proposed → Accepted →\n * In progress → Published) and reusable for brand/collection lifecycles.\n */\n\nimport type { FC } from 'react';\nimport { Check } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { titleCase } from '../utils/formatters';\n\ninterface StepTrackerProps {\n /** Ordered step values (e.g. ['PROPOSED','ACCEPTED','IN_PROGRESS','PUBLISHED']). */\n steps: string[];\n /** The current step value — must be one of `steps`. */\n current: string;\n className?: string;\n}\n\nexport const StepTracker: FC<StepTrackerProps> = ({ steps, current, className }) => {\n const foundIdx = steps.findIndex((step) => step === current);\n const currentIdx = foundIdx === -1 ? 0 : foundIdx;\n\n return (\n <ol className={cn('flex w-full items-start', className)}>\n {steps.map((step, index) => {\n const state = index < currentIdx ? 'done' : index === currentIdx ? 'current' : 'upcoming';\n const connectorDone = index <= currentIdx;\n return (\n <li key={step} className=\"relative flex flex-1 flex-col items-center gap-2 text-center\">\n {index > 0 ? (\n <span\n aria-hidden=\"true\"\n className={cn(\n 'absolute left-[-50%] top-4 z-0 h-0.5 w-full -translate-y-1/2',\n connectorDone ? 'bg-accent-primary' : 'bg-border-seam'\n )}\n />\n ) : null}\n <span\n className={cn(\n 'relative z-10 flex h-8 w-8 items-center justify-center rounded-full text-xs font-semibold tabular-nums transition-colors',\n state === 'done' && 'bg-accent-primary text-action-primary-text',\n state === 'current' &&\n 'border-2 border-accent-primary bg-bg-surface text-accent-primary',\n state === 'upcoming' &&\n 'border border-border-seam bg-bg-surface text-text-secondary'\n )}\n >\n {state === 'done' ? <Check className=\"h-4 w-4\" /> : index + 1}\n </span>\n <span\n className={cn(\n 'text-xs font-medium',\n state === 'upcoming' ? 'text-text-secondary' : 'text-text-primary'\n )}\n >\n {titleCase(step)}\n </span>\n </li>\n );\n })}\n </ol>\n );\n};\n"],"mappings":";;;;;AAuBA,IAAa,KAAqC,EAAE,UAAO,YAAS,mBAAgB;CAClF,IAAM,IAAW,EAAM,WAAW,MAAS,MAAS,CAAO,GACrD,IAAa,MAAa,KAAK,IAAI;CAEzC,OACE,kBAAC,MAAD;EAAI,WAAW,EAAG,2BAA2B,CAAS;YACnD,EAAM,KAAK,GAAM,MAAU;GAC1B,IAAM,IAAQ,IAAQ,IAAa,SAAS,MAAU,IAAa,YAAY;GAE/E,OACE,kBAAC,MAAD;IAAe,WAAU;cAAzB;KACG,IAAQ,IACP,kBAAC,QAAD;MACE,eAAY;MACZ,WAAW,EACT,gEAPY,KAAS,IAQL,sBAAsB,gBACxC;KACD,CAAA,IACC;KACJ,kBAAC,QAAD;MACE,WAAW,EACT,4HACA,MAAU,UAAU,8CACpB,MAAU,aACR,oEACF,MAAU,cACR,6DACJ;gBAEC,MAAU,SAAS,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA,IAAI,IAAQ;KACxD,CAAA;KACN,kBAAC,QAAD;MACE,WAAW,EACT,uBACA,MAAU,aAAa,wBAAwB,mBACjD;gBAEC,EAAU,CAAI;KACX,CAAA;IACJ;MA9BK,CA8BL;EAER,CAAC;CACC,CAAA;AAER"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StockEditor.js","names":[],"sources":["../../../src/coolandlovely/components/StockEditor.tsx"],"sourcesContent":["/**\n * StockEditor — per-size/colour stock for a style item (BOFF-9637).\n * @module coolandlovely/components\n *\n * A brand switches stock tracking on and records how many units of each variant it can sell.\n * With tracking on, checkout reserves stock and the piece goes ACTIVE / OUT_OF_STOCK on its own\n * as stock runs out or comes back. Units held by unpaid orders are shown, never edited here.\n */\n\nimport { type FC, useEffect, useMemo, useState } from 'react';\nimport { Boxes, Loader2, Save } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { useTr } from '../hooks';\nimport { useSetStyleItemInventory, useStyleItemInventory } from '../hooks/useInventoryApi';\n\ninterface StockEditorProps {\n styleItemId: string;\n}\n\nconst variantKey = (size: string | null, colour: string | null): string =>\n `${size ?? ''}\\u0000${colour ?? ''}`;\n\nconst MAX_STOCK = 100_000;\n\nexport const StockEditor: FC<StockEditorProps> = ({ styleItemId }) => {\n const tr = useTr();\n const inventory = useStyleItemInventory(styleItemId);\n const save = useSetStyleItemInventory();\n const [track, setTrack] = useState(false);\n const [counts, setCounts] = useState<Record<string, string>>({});\n\n useEffect(() => {\n if (!inventory.data) return;\n setTrack(inventory.data.trackInventory);\n setCounts(\n Object.fromEntries(\n inventory.data.variants.map((variant) => [\n variantKey(variant.size, variant.colour),\n String(variant.stockQuantity),\n ])\n )\n );\n }, [inventory.data]);\n\n const invalid = useMemo(\n () =>\n Object.values(counts).some((value) => {\n const n = Number(value);\n return value.trim() === '' || !Number.isInteger(n) || n < 0 || n > MAX_STOCK;\n }),\n [counts]\n );\n\n if (inventory.isLoading || !inventory.data) {\n return inventory.isError ? null : (\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <Loader2 className=\"h-4 w-4 animate-spin text-text-secondary\" />\n </GlassCard>\n );\n }\n\n const variants = inventory.data.variants;\n const label = (size: string | null, colour: string | null): string =>\n [size, colour].filter(Boolean).join(' / ') ||\n tr('coolandlovely.stock.oneSize', 'One size, one colour');\n\n const submit = (): void => {\n save.mutate({\n id: styleItemId,\n input: {\n trackInventory: track,\n variants: variants.map((variant) => ({\n size: variant.size,\n colour: variant.colour,\n stockQuantity: Number(counts[variantKey(variant.size, variant.colour)] ?? 0),\n })),\n },\n });\n };\n\n return (\n <div data-testid=\"stock-editor\">\n <GlassCard treatment=\"plain\" className=\"space-y-4 p-6\">\n <div className=\"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 <Boxes className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">\n {tr('coolandlovely.stock.title', 'Stock')}\n </h2>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'coolandlovely.stock.hint',\n 'Track units per size and colour. Checkout reserves stock, and the piece shows as out of stock on its own when it runs out.'\n )}\n </p>\n </div>\n </div>\n\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={track}\n onChange={(event) => setTrack(event.target.checked)}\n data-testid=\"stock-track-toggle\"\n />\n {tr('coolandlovely.stock.track', 'Track stock for this piece')}\n </label>\n\n {track && (\n <div className=\"overflow-x-auto rounded-xl border border-border-seam\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-bg-sunken text-left text-xs uppercase tracking-wide text-text-secondary\">\n <tr>\n <th className=\"px-4 py-2\">{tr('coolandlovely.stock.variant', 'Variant')}</th>\n <th className=\"px-4 py-2\">{tr('coolandlovely.stock.available', 'Available')}</th>\n <th className=\"px-4 py-2 text-right\">\n {tr('coolandlovely.stock.reserved', 'Held by unpaid orders')}\n </th>\n </tr>\n </thead>\n <tbody>\n {variants.map((variant) => {\n const key = variantKey(variant.size, variant.colour);\n return (\n <tr key={key} className=\"border-t border-border-seam\">\n <td className=\"px-4 py-2 text-text-primary\">\n {label(variant.size, variant.colour)}\n </td>\n <td className=\"px-4 py-2\">\n <input\n type=\"number\"\n min={0}\n max={MAX_STOCK}\n step={1}\n aria-label={tr('coolandlovely.stock.unitsFor', 'Units of {{variant}}', {\n variant: label(variant.size, variant.colour),\n })}\n value={counts[key] ?? '0'}\n onChange={(event) =>\n setCounts((prev) => ({ ...prev, [key]: event.target.value }))\n }\n className=\"w-24 rounded-lg border border-border-seam bg-bg-surface px-2 py-1 text-sm tabular-nums text-text-primary\"\n />\n </td>\n <td className=\"px-4 py-2 text-right tabular-nums text-text-secondary\">\n {variant.reservedQuantity}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n )}\n\n {invalid && track && (\n <p className=\"text-sm text-status-error-text\">\n {tr('coolandlovely.stock.invalid', 'Enter a whole number of units for every variant.')}\n </p>\n )}\n {save.error && <p className=\"text-sm text-status-error-text\">{save.error.message}</p>}\n {save.isSuccess && !save.isPending && (\n <p className=\"text-sm text-status-success-text\">\n {tr('coolandlovely.stock.saved', 'Stock saved. Sale status: {{status}}', {\n status: save.data.status.replace(/_/g, ' ').toLowerCase(),\n })}\n </p>\n )}\n\n <div className=\"flex justify-end\">\n <button\n type=\"button\"\n disabled={save.isPending || (track && invalid)}\n onClick={submit}\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 {save.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.stock.save', 'Save stock')}\n </button>\n </div>\n </GlassCard>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StockEditor.js","names":[],"sources":["../../../src/coolandlovely/components/StockEditor.tsx"],"sourcesContent":["/**\n * StockEditor — per-size/colour stock for a style item (BOFF-9637).\n * @module coolandlovely/components\n *\n * A brand switches stock tracking on and records how many units of each variant it can sell.\n * With tracking on, checkout reserves stock and the piece goes ACTIVE / OUT_OF_STOCK on its own\n * as stock runs out or comes back. Units held by unpaid orders are shown, never edited here.\n */\n\nimport { type FC, useEffect, useMemo, useState } from 'react';\nimport { Boxes, Loader2, Save } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { useTr } from '../hooks';\nimport { useSetStyleItemInventory, useStyleItemInventory } from '../hooks/useInventoryApi';\n\ninterface StockEditorProps {\n styleItemId: string;\n}\n\nconst variantKey = (size: string | null, colour: string | null): string =>\n `${size ?? ''}\\u0000${colour ?? ''}`;\n\nconst MAX_STOCK = 100_000;\n\nexport const StockEditor: FC<StockEditorProps> = ({ styleItemId }) => {\n const tr = useTr();\n const inventory = useStyleItemInventory(styleItemId);\n const save = useSetStyleItemInventory();\n const [track, setTrack] = useState(false);\n const [counts, setCounts] = useState<Record<string, string>>({});\n\n useEffect(() => {\n if (!inventory.data) return;\n setTrack(inventory.data.trackInventory);\n setCounts(\n Object.fromEntries(\n inventory.data.variants.map((variant) => [\n variantKey(variant.size, variant.colour),\n String(variant.stockQuantity),\n ])\n )\n );\n }, [inventory.data]);\n\n const invalid = useMemo(\n () =>\n Object.values(counts).some((value) => {\n const n = Number(value);\n return value.trim() === '' || !Number.isInteger(n) || n < 0 || n > MAX_STOCK;\n }),\n [counts]\n );\n\n if (inventory.isLoading || !inventory.data) {\n return inventory.isError ? null : (\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <Loader2 className=\"h-4 w-4 animate-spin text-text-secondary\" />\n </GlassCard>\n );\n }\n\n const variants = inventory.data.variants;\n const label = (size: string | null, colour: string | null): string =>\n [size, colour].filter(Boolean).join(' / ') ||\n tr('coolandlovely.stock.oneSize', 'One size, one colour');\n\n const submit = (): void => {\n save.mutate({\n id: styleItemId,\n input: {\n trackInventory: track,\n variants: variants.map((variant) => ({\n size: variant.size,\n colour: variant.colour,\n stockQuantity: Number(counts[variantKey(variant.size, variant.colour)] ?? 0),\n })),\n },\n });\n };\n\n return (\n <div data-testid=\"stock-editor\">\n <GlassCard treatment=\"plain\" className=\"space-y-4 p-6\">\n <div className=\"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 <Boxes className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">\n {tr('coolandlovely.stock.title', 'Stock')}\n </h2>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'coolandlovely.stock.hint',\n 'Track units per size and colour. Checkout reserves stock, and the piece shows as out of stock on its own when it runs out.'\n )}\n </p>\n </div>\n </div>\n\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={track}\n onChange={(event) => setTrack(event.target.checked)}\n data-testid=\"stock-track-toggle\"\n />\n {tr('coolandlovely.stock.track', 'Track stock for this piece')}\n </label>\n\n {track && (\n <div className=\"overflow-x-auto rounded-xl border border-border-seam\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-bg-sunken text-left text-xs uppercase tracking-wide text-text-secondary\">\n <tr>\n <th className=\"px-4 py-2\">{tr('coolandlovely.stock.variant', 'Variant')}</th>\n <th className=\"px-4 py-2\">{tr('coolandlovely.stock.available', 'Available')}</th>\n <th className=\"px-4 py-2 text-right\">\n {tr('coolandlovely.stock.reserved', 'Held by unpaid orders')}\n </th>\n </tr>\n </thead>\n <tbody>\n {variants.map((variant) => {\n const key = variantKey(variant.size, variant.colour);\n return (\n <tr key={key} className=\"border-t border-border-seam\">\n <td className=\"px-4 py-2 text-text-primary\">\n {label(variant.size, variant.colour)}\n </td>\n <td className=\"px-4 py-2\">\n <input\n type=\"number\"\n min={0}\n max={MAX_STOCK}\n step={1}\n aria-label={tr('coolandlovely.stock.unitsFor', 'Units of {{variant}}', {\n variant: label(variant.size, variant.colour),\n })}\n value={counts[key] ?? '0'}\n onChange={(event) =>\n setCounts((prev) => ({ ...prev, [key]: event.target.value }))\n }\n className=\"w-24 rounded-lg border border-border-seam bg-bg-surface px-2 py-1 text-sm tabular-nums text-text-primary\"\n />\n </td>\n <td className=\"px-4 py-2 text-right tabular-nums text-text-secondary\">\n {variant.reservedQuantity}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n )}\n\n {invalid && track && (\n <p className=\"text-sm text-status-error-text\">\n {tr('coolandlovely.stock.invalid', 'Enter a whole number of units for every variant.')}\n </p>\n )}\n {save.error && <p className=\"text-sm text-status-error-text\">{save.error.message}</p>}\n {save.isSuccess && !save.isPending && (\n <p className=\"text-sm text-status-success-text\">\n {tr('coolandlovely.stock.saved', 'Stock saved. Sale status: {{status}}', {\n status: save.data.status.replace(/_/g, ' ').toLowerCase(),\n })}\n </p>\n )}\n\n <div className=\"flex justify-end\">\n <button\n type=\"button\"\n disabled={save.isPending || (track && invalid)}\n onClick={submit}\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 {save.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.stock.save', 'Save stock')}\n </button>\n </div>\n </GlassCard>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StoreListingCard.js","names":[],"sources":["../../../src/coolandlovely/components/StoreListingCard.tsx"],"sourcesContent":["/**\n * StoreListingCard — a piece's listing in the Burdenoff store catalogue (BOFF-3430 / BOFF-9666).\n * @module coolandlovely/components\n *\n * On-sale pieces of a live brand storefront are mirrored into the Burdenoff store as discovery\n * listings that link back here; CoolAndLovely stays the seller. The service keeps the listing in\n * sync on every change of the piece or its brand and records the outcome on the piece. This card\n * shows that state and offers \"Sync now\" when the last push failed.\n */\n\nimport type { FC } from 'react';\nimport { AlertTriangle, CheckCircle2, EyeOff, Loader2, RefreshCw, Store } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { useTr } from '../hooks';\nimport { useSyncStyleItemStoreListing } from '../hooks/useCoolAndLovelyApi';\nimport type { CooStyleItem } from '../types';\nimport { cn } from '../utils/cn';\nimport { formatDate } from '../utils/formatters';\n\ntype Tone = 'success' | 'warning' | 'error' | 'neutral';\n\nconst toneStyles: Record<Tone, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n error: 'bg-status-error-bg-subtle text-status-error-text',\n neutral: 'bg-bg-sunken text-text-secondary',\n};\n\ninterface StoreListingCardProps {\n item: Pick<\n CooStyleItem,\n 'id' | 'storeProductId' | 'storeListingStatus' | 'storeSyncStatus' | 'storeSyncedAt'\n >;\n}\n\nexport const StoreListingCard: FC<StoreListingCardProps> = ({ item }) => {\n const tr = useTr();\n const sync = useSyncStyleItemStoreListing();\n\n const failed = item.storeSyncStatus === 'FAILED';\n const pending = item.storeSyncStatus === 'PENDING';\n\n let tone: Tone;\n let label: string;\n let hint: string;\n if (failed) {\n tone = 'error';\n label = tr('coolandlovely.storeListing.failed', 'Sync failed');\n hint = tr(\n 'coolandlovely.storeListing.failedHint',\n 'The latest changes could not be sent to the Burdenoff store. Sync now, or they are sent again with the next change to this piece.'\n );\n } else if (pending) {\n tone = 'neutral';\n label = tr('coolandlovely.storeListing.pending', 'Syncing with the Burdenoff store');\n hint = tr(\n 'coolandlovely.storeListing.pendingHint',\n 'The listing is being updated. Refresh in a moment to see the result.'\n );\n } else if (item.storeListingStatus === 'PUBLISHED') {\n tone = 'success';\n label = tr('coolandlovely.storeListing.published', 'Listed in the Burdenoff store');\n hint = tr(\n 'coolandlovely.storeListing.publishedHint',\n 'Shoppers can find this piece in the store. Its listing links back here, and CoolAndLovely handles checkout.'\n );\n } else if (item.storeListingStatus === 'UNLISTED') {\n tone = 'warning';\n label = tr('coolandlovely.storeListing.unlisted', 'Hidden while out of stock');\n hint = tr(\n 'coolandlovely.storeListing.unlistedHint',\n 'The listing is kept but hidden from the store until the piece is back in stock.'\n );\n } else if (item.storeListingStatus === 'ARCHIVED') {\n tone = 'neutral';\n label = tr('coolandlovely.storeListing.archived', 'Taken down from the Burdenoff store');\n hint = tr(\n 'coolandlovely.storeListing.archivedHint',\n 'Only pieces that are on sale, with a price, in a live brand storefront are listed.'\n );\n } else {\n tone = 'neutral';\n label = tr('coolandlovely.storeListing.notListed', 'Not listed in the Burdenoff store');\n hint = tr(\n 'coolandlovely.storeListing.notListedHint',\n 'Pieces that are on sale, with a price, in a live brand storefront are listed automatically.'\n );\n }\n\n const Icon = failed\n ? AlertTriangle\n : pending\n ? Loader2\n : tone === 'success'\n ? CheckCircle2\n : tone === 'warning'\n ? EyeOff\n : Store;\n\n return (\n <GlassCard treatment=\"plain\" className=\"space-y-3 p-5\" data-testid=\"coo-store-listing-card\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {tr('coolandlovely.storeListing.title', 'Burdenoff store')}\n </h3>\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium',\n toneStyles[tone]\n )}\n data-testid=\"coo-store-listing-status\"\n >\n <Icon className={cn('h-3.5 w-3.5', pending && 'animate-spin')} aria-hidden=\"true\" />\n {label}\n </span>\n <p className=\"text-sm text-text-secondary\">{hint}</p>\n {item.storeSyncedAt && (\n <p className=\"text-xs text-text-secondary\">\n {tr('coolandlovely.storeListing.lastSynced', 'Last synced {{date}}', {\n date: formatDate(item.storeSyncedAt),\n })}\n </p>\n )}\n {failed && (\n <button\n type=\"button\"\n disabled={sync.isPending}\n onClick={() => sync.mutate(item.id)}\n data-testid=\"coo-store-listing-sync\"\n className=\"inline-flex w-full items-center justify-center gap-2 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50\"\n >\n {sync.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <RefreshCw className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.storeListing.syncNow', 'Sync now')}\n </button>\n )}\n {sync.isError && (\n <p role=\"alert\" className=\"text-xs text-status-error-text\">\n {tr(\n 'coolandlovely.storeListing.syncError',\n 'Could not reach the store right now. Try again in a moment.'\n )}\n </p>\n )}\n </GlassCard>\n );\n};\n"],"mappings":";;;;;;;;;AAsBA,IAAM,IAAmC;CACvC,SAAS;CACT,SAAS;CACT,OAAO;CACP,SAAS;
|
|
1
|
+
{"version":3,"file":"StoreListingCard.js","names":[],"sources":["../../../src/coolandlovely/components/StoreListingCard.tsx"],"sourcesContent":["/**\n * StoreListingCard — a piece's listing in the Burdenoff store catalogue (BOFF-3430 / BOFF-9666).\n * @module coolandlovely/components\n *\n * On-sale pieces of a live brand storefront are mirrored into the Burdenoff store as discovery\n * listings that link back here; CoolAndLovely stays the seller. The service keeps the listing in\n * sync on every change of the piece or its brand and records the outcome on the piece. This card\n * shows that state and offers \"Sync now\" when the last push failed.\n */\n\nimport type { FC } from 'react';\nimport { AlertTriangle, CheckCircle2, EyeOff, Loader2, RefreshCw, Store } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\n\nimport { useTr } from '../hooks';\nimport { useSyncStyleItemStoreListing } from '../hooks/useCoolAndLovelyApi';\nimport type { CooStyleItem } from '../types';\nimport { cn } from '../utils/cn';\nimport { formatDate } from '../utils/formatters';\n\ntype Tone = 'success' | 'warning' | 'error' | 'neutral';\n\nconst toneStyles: Record<Tone, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n error: 'bg-status-error-bg-subtle text-status-error-text',\n neutral: 'bg-bg-sunken text-text-secondary',\n};\n\ninterface StoreListingCardProps {\n item: Pick<\n CooStyleItem,\n 'id' | 'storeProductId' | 'storeListingStatus' | 'storeSyncStatus' | 'storeSyncedAt'\n >;\n}\n\nexport const StoreListingCard: FC<StoreListingCardProps> = ({ item }) => {\n const tr = useTr();\n const sync = useSyncStyleItemStoreListing();\n\n const failed = item.storeSyncStatus === 'FAILED';\n const pending = item.storeSyncStatus === 'PENDING';\n\n let tone: Tone;\n let label: string;\n let hint: string;\n if (failed) {\n tone = 'error';\n label = tr('coolandlovely.storeListing.failed', 'Sync failed');\n hint = tr(\n 'coolandlovely.storeListing.failedHint',\n 'The latest changes could not be sent to the Burdenoff store. Sync now, or they are sent again with the next change to this piece.'\n );\n } else if (pending) {\n tone = 'neutral';\n label = tr('coolandlovely.storeListing.pending', 'Syncing with the Burdenoff store');\n hint = tr(\n 'coolandlovely.storeListing.pendingHint',\n 'The listing is being updated. Refresh in a moment to see the result.'\n );\n } else if (item.storeListingStatus === 'PUBLISHED') {\n tone = 'success';\n label = tr('coolandlovely.storeListing.published', 'Listed in the Burdenoff store');\n hint = tr(\n 'coolandlovely.storeListing.publishedHint',\n 'Shoppers can find this piece in the store. Its listing links back here, and CoolAndLovely handles checkout.'\n );\n } else if (item.storeListingStatus === 'UNLISTED') {\n tone = 'warning';\n label = tr('coolandlovely.storeListing.unlisted', 'Hidden while out of stock');\n hint = tr(\n 'coolandlovely.storeListing.unlistedHint',\n 'The listing is kept but hidden from the store until the piece is back in stock.'\n );\n } else if (item.storeListingStatus === 'ARCHIVED') {\n tone = 'neutral';\n label = tr('coolandlovely.storeListing.archived', 'Taken down from the Burdenoff store');\n hint = tr(\n 'coolandlovely.storeListing.archivedHint',\n 'Only pieces that are on sale, with a price, in a live brand storefront are listed.'\n );\n } else {\n tone = 'neutral';\n label = tr('coolandlovely.storeListing.notListed', 'Not listed in the Burdenoff store');\n hint = tr(\n 'coolandlovely.storeListing.notListedHint',\n 'Pieces that are on sale, with a price, in a live brand storefront are listed automatically.'\n );\n }\n\n const Icon = failed\n ? AlertTriangle\n : pending\n ? Loader2\n : tone === 'success'\n ? CheckCircle2\n : tone === 'warning'\n ? EyeOff\n : Store;\n\n return (\n <GlassCard treatment=\"plain\" className=\"space-y-3 p-5\" data-testid=\"coo-store-listing-card\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {tr('coolandlovely.storeListing.title', 'Burdenoff store')}\n </h3>\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium',\n toneStyles[tone]\n )}\n data-testid=\"coo-store-listing-status\"\n >\n <Icon className={cn('h-3.5 w-3.5', pending && 'animate-spin')} aria-hidden=\"true\" />\n {label}\n </span>\n <p className=\"text-sm text-text-secondary\">{hint}</p>\n {item.storeSyncedAt && (\n <p className=\"text-xs text-text-secondary\">\n {tr('coolandlovely.storeListing.lastSynced', 'Last synced {{date}}', {\n date: formatDate(item.storeSyncedAt),\n })}\n </p>\n )}\n {failed && (\n <button\n type=\"button\"\n disabled={sync.isPending}\n onClick={() => sync.mutate(item.id)}\n data-testid=\"coo-store-listing-sync\"\n className=\"inline-flex w-full items-center justify-center gap-2 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50\"\n >\n {sync.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <RefreshCw className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.storeListing.syncNow', 'Sync now')}\n </button>\n )}\n {sync.isError && (\n <p role=\"alert\" className=\"text-xs text-status-error-text\">\n {tr(\n 'coolandlovely.storeListing.syncError',\n 'Could not reach the store right now. Try again in a moment.'\n )}\n </p>\n )}\n </GlassCard>\n );\n};\n"],"mappings":";;;;;;;;;AAsBA,IAAM,IAAmC;CACvC,SAAS;CACT,SAAS;CACT,OAAO;CACP,SAAS;AACX,GASa,KAA+C,EAAE,cAAW;CACvE,IAAM,IAAK,EAAM,GACX,IAAO,EAA6B,GAEpC,IAAS,EAAK,oBAAoB,UAClC,IAAU,EAAK,oBAAoB,WAErC,GACA,GACA;CACJ,AAAI,KACF,IAAO,SACP,IAAQ,EAAG,qCAAqC,aAAa,GAC7D,IAAO,EACL,yCACA,mIACF,KACS,KACT,IAAO,WACP,IAAQ,EAAG,sCAAsC,kCAAkC,GACnF,IAAO,EACL,0CACA,sEACF,KACS,EAAK,uBAAuB,eACrC,IAAO,WACP,IAAQ,EAAG,wCAAwC,+BAA+B,GAClF,IAAO,EACL,4CACA,6GACF,KACS,EAAK,uBAAuB,cACrC,IAAO,WACP,IAAQ,EAAG,uCAAuC,2BAA2B,GAC7E,IAAO,EACL,2CACA,iFACF,KACS,EAAK,uBAAuB,cACrC,IAAO,WACP,IAAQ,EAAG,uCAAuC,qCAAqC,GACvF,IAAO,EACL,2CACA,oFACF,MAEA,IAAO,WACP,IAAQ,EAAG,wCAAwC,mCAAmC,GACtF,IAAO,EACL,4CACA,6FACF;CAGF,IAAM,IAAO,IACT,IACA,IACE,IACA,MAAS,YACP,IACA,MAAS,YACP,IACA;CAEV,OACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;EAAgB,eAAY;YAAnE;GACE,kBAAC,MAAD;IAAI,WAAU;cACX,EAAG,oCAAoC,iBAAiB;GACvD,CAAA;GACJ,kBAAC,QAAD;IACE,WAAW,EACT,+EACA,EAAW,EACb;IACA,eAAY;cALd,CAOE,kBAAC,GAAD;KAAM,WAAW,EAAG,eAAe,KAAW,cAAc;KAAG,eAAY;IAAQ,CAAA,GAClF,CACG;;GACN,kBAAC,KAAD;IAAG,WAAU;cAA+B;GAAQ,CAAA;GACnD,EAAK,iBACJ,kBAAC,KAAD;IAAG,WAAU;cACV,EAAG,yCAAyC,wBAAwB,EACnE,MAAM,EAAW,EAAK,aAAa,EACrC,CAAC;GACA,CAAA;GAEJ,KACC,kBAAC,UAAD;IACE,MAAK;IACL,UAAU,EAAK;IACf,eAAe,EAAK,OAAO,EAAK,EAAE;IAClC,eAAY;IACZ,WAAU;cALZ,CAOG,EAAK,YACJ,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,IAE3C,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA,GAEjC,EAAG,sCAAsC,UAAU,CAC9C;;GAET,EAAK,WACJ,kBAAC,KAAD;IAAG,MAAK;IAAQ,WAAU;cACvB,EACC,wCACA,6DACF;GACC,CAAA;EAEI;;AAEf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TagList.js","names":[],"sources":["../../../src/coolandlovely/components/TagList.tsx"],"sourcesContent":["/**\n * TagList Component\n * @module coolandlovely/components\n *\n * Renders a compact, capped set of tag/aesthetic chips on semantic tokens.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface TagListProps {\n tags: string[];\n /** Maximum chips to show before collapsing into a \"+N\" chip. */\n max?: number;\n className?: string;\n tone?: 'default' | 'accent';\n}\n\nexport const TagList: FC<TagListProps> = ({ tags, max = 6, className, tone = 'default' }) => {\n if (!tags.length) return null;\n const shown = tags.slice(0, max);\n const overflow = tags.length - shown.length;\n\n const chip =\n tone === 'accent'\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary';\n\n return (\n <div className={cn('flex flex-wrap gap-1.5', className)}>\n {shown.map((tag) => (\n <span\n key={tag}\n className={cn('inline-flex items-center rounded-full px-2 py-0.5 text-xs', chip)}\n >\n {tag}\n </span>\n ))}\n {overflow > 0 && (\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n +{overflow}\n </span>\n )}\n </div>\n );\n};\n"],"mappings":";;;AAkBA,IAAa,KAA6B,EAAE,SAAM,SAAM,GAAG,cAAW,UAAO,gBAAgB;
|
|
1
|
+
{"version":3,"file":"TagList.js","names":[],"sources":["../../../src/coolandlovely/components/TagList.tsx"],"sourcesContent":["/**\n * TagList Component\n * @module coolandlovely/components\n *\n * Renders a compact, capped set of tag/aesthetic chips on semantic tokens.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface TagListProps {\n tags: string[];\n /** Maximum chips to show before collapsing into a \"+N\" chip. */\n max?: number;\n className?: string;\n tone?: 'default' | 'accent';\n}\n\nexport const TagList: FC<TagListProps> = ({ tags, max = 6, className, tone = 'default' }) => {\n if (!tags.length) return null;\n const shown = tags.slice(0, max);\n const overflow = tags.length - shown.length;\n\n const chip =\n tone === 'accent'\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary';\n\n return (\n <div className={cn('flex flex-wrap gap-1.5', className)}>\n {shown.map((tag) => (\n <span\n key={tag}\n className={cn('inline-flex items-center rounded-full px-2 py-0.5 text-xs', chip)}\n >\n {tag}\n </span>\n ))}\n {overflow > 0 && (\n <span className=\"inline-flex items-center rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n +{overflow}\n </span>\n )}\n </div>\n );\n};\n"],"mappings":";;;AAkBA,IAAa,KAA6B,EAAE,SAAM,SAAM,GAAG,cAAW,UAAO,gBAAgB;CAC3F,IAAI,CAAC,EAAK,QAAQ,OAAO;CACzB,IAAM,IAAQ,EAAK,MAAM,GAAG,CAAG,GACzB,IAAW,EAAK,SAAS,EAAM,QAE/B,IACJ,MAAS,WACL,8CACA;CAEN,OACE,kBAAC,OAAD;EAAK,WAAW,EAAG,0BAA0B,CAAS;YAAtD,CACG,EAAM,KAAK,MACV,kBAAC,QAAD;GAEE,WAAW,EAAG,6DAA6D,CAAI;aAE9E;EACG,GAJC,CAID,CACP,GACA,IAAW,KACV,kBAAC,QAAD;GAAM,WAAU;aAAhB,CAA6G,KACzG,CACE;IAEL;;AAET"}
|
|
@@ -17,7 +17,7 @@ function a(e) {
|
|
|
17
17
|
}
|
|
18
18
|
var o = ({ icon: i, seed: o, className: s, label: c, fileId: l, alt: u, overlay: d }) => /* @__PURE__ */ r("div", {
|
|
19
19
|
className: e("relative flex items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br", a(o), s),
|
|
20
|
-
"aria-hidden": c
|
|
20
|
+
"aria-hidden": !c || void 0,
|
|
21
21
|
"aria-label": c,
|
|
22
22
|
children: [
|
|
23
23
|
/* @__PURE__ */ n("span", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Thumbnail.js","names":[],"sources":["../../../src/coolandlovely/components/Thumbnail.tsx"],"sourcesContent":["/**\n * Thumbnail Component\n * @module coolandlovely/components\n *\n * Renders a calm, on-token visual placeholder for a fashion entity. Imagery in\n * CoolAndLovely is referenced from wspace-files-svc via federation (we only\n * have the File id here, not a resolvable URL), so the catalogue/wardrobe/look\n * surfaces render a branded glyph tile keyed by an icon rather than a broken\n * <img>. The seed (entity id) gives each tile a stable accent tint.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { cn } from '../utils/cn';\nimport { FileImage } from './FileImage';\n\ninterface ThumbnailProps {\n icon: ReactNode;\n /** Stable seed (e.g. entity id) used to pick a deterministic accent tint. */\n seed?: string;\n className?: string;\n /** Whether a File reference exists (shows a subtle \"image attached\" hint). */\n hasImage?: boolean;\n label?: string;\n /** wspace-files File id — when it resolves, its image overlays the glyph. */\n fileId?: string | null;\n /** Alt text when a real image is shown. */\n alt?: string;\n /**\n * Optional content rendered absolutely over the image (e.g. a gradient scrim\n * + overlaid caption). Positioned by the caller; sits above the FileImage.\n */\n overlay?: ReactNode;\n}\n\nconst tints = [\n 'from-accent-purple-subtle to-bg-sunken',\n 'from-accent-blue-subtle to-bg-sunken',\n 'from-status-success-bg-subtle to-bg-sunken',\n 'from-status-warning-bg-subtle to-bg-sunken',\n 'from-[var(--color-accent-soft)] to-bg-sunken',\n];\n\nfunction tintFor(seed: string | undefined): string {\n if (!seed) return tints[0];\n let hash = 0;\n for (let i = 0; i < seed.length; i += 1) {\n hash = (hash * 31 + seed.charCodeAt(i)) % 997;\n }\n return tints[hash % tints.length];\n}\n\nexport const Thumbnail: FC<ThumbnailProps> = ({\n icon,\n seed,\n className,\n label,\n fileId,\n alt,\n overlay,\n}) => {\n return (\n <div\n className={cn(\n 'relative flex items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br',\n tintFor(seed),\n className\n )}\n aria-hidden={label ? undefined : true}\n aria-label={label}\n >\n <span className=\"text-text-secondary opacity-80 transition-transform duration-300 motion-safe:group-hover:scale-[1.03]\">\n {icon}\n </span>\n {fileId ? (\n <span className=\"absolute inset-0 transition-transform duration-300 motion-safe:group-hover:scale-[1.03]\">\n <FileImage fileId={fileId} alt={alt ?? label ?? ''} />\n </span>\n ) : null}\n {overlay}\n </div>\n );\n};\n"],"mappings":";;;;AAkCA,IAAM,IAAQ;CACZ;CACA;CACA;CACA;CACA;
|
|
1
|
+
{"version":3,"file":"Thumbnail.js","names":[],"sources":["../../../src/coolandlovely/components/Thumbnail.tsx"],"sourcesContent":["/**\n * Thumbnail Component\n * @module coolandlovely/components\n *\n * Renders a calm, on-token visual placeholder for a fashion entity. Imagery in\n * CoolAndLovely is referenced from wspace-files-svc via federation (we only\n * have the File id here, not a resolvable URL), so the catalogue/wardrobe/look\n * surfaces render a branded glyph tile keyed by an icon rather than a broken\n * <img>. The seed (entity id) gives each tile a stable accent tint.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { cn } from '../utils/cn';\nimport { FileImage } from './FileImage';\n\ninterface ThumbnailProps {\n icon: ReactNode;\n /** Stable seed (e.g. entity id) used to pick a deterministic accent tint. */\n seed?: string;\n className?: string;\n /** Whether a File reference exists (shows a subtle \"image attached\" hint). */\n hasImage?: boolean;\n label?: string;\n /** wspace-files File id — when it resolves, its image overlays the glyph. */\n fileId?: string | null;\n /** Alt text when a real image is shown. */\n alt?: string;\n /**\n * Optional content rendered absolutely over the image (e.g. a gradient scrim\n * + overlaid caption). Positioned by the caller; sits above the FileImage.\n */\n overlay?: ReactNode;\n}\n\nconst tints = [\n 'from-accent-purple-subtle to-bg-sunken',\n 'from-accent-blue-subtle to-bg-sunken',\n 'from-status-success-bg-subtle to-bg-sunken',\n 'from-status-warning-bg-subtle to-bg-sunken',\n 'from-[var(--color-accent-soft)] to-bg-sunken',\n];\n\nfunction tintFor(seed: string | undefined): string {\n if (!seed) return tints[0];\n let hash = 0;\n for (let i = 0; i < seed.length; i += 1) {\n hash = (hash * 31 + seed.charCodeAt(i)) % 997;\n }\n return tints[hash % tints.length];\n}\n\nexport const Thumbnail: FC<ThumbnailProps> = ({\n icon,\n seed,\n className,\n label,\n fileId,\n alt,\n overlay,\n}) => {\n return (\n <div\n className={cn(\n 'relative flex items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br',\n tintFor(seed),\n className\n )}\n aria-hidden={label ? undefined : true}\n aria-label={label}\n >\n <span className=\"text-text-secondary opacity-80 transition-transform duration-300 motion-safe:group-hover:scale-[1.03]\">\n {icon}\n </span>\n {fileId ? (\n <span className=\"absolute inset-0 transition-transform duration-300 motion-safe:group-hover:scale-[1.03]\">\n <FileImage fileId={fileId} alt={alt ?? label ?? ''} />\n </span>\n ) : null}\n {overlay}\n </div>\n );\n};\n"],"mappings":";;;;AAkCA,IAAM,IAAQ;CACZ;CACA;CACA;CACA;CACA;AACF;AAEA,SAAS,EAAQ,GAAkC;CACjD,IAAI,CAAC,GAAM,OAAO,EAAM;CACxB,IAAI,IAAO;CACX,KAAK,IAAI,IAAI,GAAG,IAAI,EAAK,QAAQ,KAAK,GACpC,KAAQ,IAAO,KAAK,EAAK,WAAW,CAAC,KAAK;CAE5C,OAAO,EAAM,IAAO,EAAM;AAC5B;AAEA,IAAa,KAAiC,EAC5C,SACA,SACA,cACA,UACA,WACA,QACA,iBAGE,kBAAC,OAAD;CACE,WAAW,EACT,0FACA,EAAQ,CAAI,GACZ,CACF;CACA,eAAa,MAAQ,KAAA;CACrB,cAAY;WAPd;EASE,kBAAC,QAAD;GAAM,WAAU;aACb;EACG,CAAA;EACL,IACC,kBAAC,QAAD;GAAM,WAAU;aACd,kBAAC,GAAD;IAAmB;IAAQ,KAAK,KAAO,KAAS;GAAK,CAAA;EACjD,CAAA,IACJ;EACH;CACE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VisibilityChip.js","names":[],"sources":["../../../src/coolandlovely/components/VisibilityChip.tsx"],"sourcesContent":["/**\n * VisibilityChip — a small icon + label chip for a look/lookbook/profile\n * visibility (PRIVATE / FOLLOWERS / PUBLIC).\n * @module coolandlovely/components\n *\n * Token-only. The lock/users/globe glyph makes the visibility scannable at a\n * glance on lookbook cards, the lookbook detail header and the style profile.\n */\n\nimport type { FC } from 'react';\nimport { Globe, Lock, Users } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { titleCase } from '../utils/formatters';\nimport { useTr } from '../hooks';\n\ninterface VisibilityChipProps {\n /** A `CooVisibility` value (PUBLIC / FOLLOWERS / PRIVATE) — case-insensitive. */\n visibility: string;\n className?: string;\n}\n\nconst CONFIG: Record<string, { icon: typeof Globe; label: string; labelKey: string }> = {\n public: { icon: Globe, label: 'Public', labelKey: 'coolandlovely.visibility.public' },\n followers: { icon: Users, label: 'Followers', labelKey: 'coolandlovely.visibility.followers' },\n private: { icon: Lock, label: 'Private', labelKey: 'coolandlovely.visibility.private' },\n};\n\nexport const VisibilityChip: FC<VisibilityChipProps> = ({ visibility, className }) => {\n const tr = useTr();\n const key = visibility.trim().toLowerCase();\n const config = CONFIG[key];\n const Icon = config?.icon ?? Lock;\n const label = config ? tr(config.labelKey, config.label) : titleCase(visibility);\n\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary',\n className\n )}\n >\n <Icon className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {label}\n </span>\n );\n};\n"],"mappings":";;;;;;;AAqBA,IAAM,IAAkF;CACtF,QAAQ;EAAE,MAAM;EAAO,OAAO;EAAU,UAAU;
|
|
1
|
+
{"version":3,"file":"VisibilityChip.js","names":[],"sources":["../../../src/coolandlovely/components/VisibilityChip.tsx"],"sourcesContent":["/**\n * VisibilityChip — a small icon + label chip for a look/lookbook/profile\n * visibility (PRIVATE / FOLLOWERS / PUBLIC).\n * @module coolandlovely/components\n *\n * Token-only. The lock/users/globe glyph makes the visibility scannable at a\n * glance on lookbook cards, the lookbook detail header and the style profile.\n */\n\nimport type { FC } from 'react';\nimport { Globe, Lock, Users } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { titleCase } from '../utils/formatters';\nimport { useTr } from '../hooks';\n\ninterface VisibilityChipProps {\n /** A `CooVisibility` value (PUBLIC / FOLLOWERS / PRIVATE) — case-insensitive. */\n visibility: string;\n className?: string;\n}\n\nconst CONFIG: Record<string, { icon: typeof Globe; label: string; labelKey: string }> = {\n public: { icon: Globe, label: 'Public', labelKey: 'coolandlovely.visibility.public' },\n followers: { icon: Users, label: 'Followers', labelKey: 'coolandlovely.visibility.followers' },\n private: { icon: Lock, label: 'Private', labelKey: 'coolandlovely.visibility.private' },\n};\n\nexport const VisibilityChip: FC<VisibilityChipProps> = ({ visibility, className }) => {\n const tr = useTr();\n const key = visibility.trim().toLowerCase();\n const config = CONFIG[key];\n const Icon = config?.icon ?? Lock;\n const label = config ? tr(config.labelKey, config.label) : titleCase(visibility);\n\n return (\n <span\n className={cn(\n 'inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary',\n className\n )}\n >\n <Icon className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {label}\n </span>\n );\n};\n"],"mappings":";;;;;;;AAqBA,IAAM,IAAkF;CACtF,QAAQ;EAAE,MAAM;EAAO,OAAO;EAAU,UAAU;CAAkC;CACpF,WAAW;EAAE,MAAM;EAAO,OAAO;EAAa,UAAU;CAAqC;CAC7F,SAAS;EAAE,MAAM;EAAM,OAAO;EAAW,UAAU;CAAmC;AACxF,GAEa,KAA2C,EAAE,eAAY,mBAAgB;CACpF,IAAM,IAAK,EAAM,GAEX,IAAS,EADH,EAAW,KAAK,CAAC,CAAC,YACR,IAChB,IAAO,GAAQ,QAAQ,GACvB,IAAQ,IAAS,EAAG,EAAO,UAAU,EAAO,KAAK,IAAI,EAAU,CAAU;CAE/E,OACE,kBAAC,QAAD;EACE,WAAW,EACT,gHACA,CACF;YAJF,CAME,kBAAC,GAAD;GAAM,WAAU;GAAc,eAAY;EAAQ,CAAA,GACjD,CACG;;AAEV"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WardrobeItemCard.js","names":[],"sources":["../../../src/coolandlovely/components/WardrobeItemCard.tsx"],"sourcesContent":["/**\n * WardrobeItemCard — a digital wardrobe piece card for list/grids.\n * @module coolandlovely/components\n *\n * Image-forward card with a worn-count badge, hover action bar (log wear / remove),\n * details, size, source and cost-per-wear. Extracted from WardrobeListPage for\n * isolated rendering/tests.\n */\n\nimport type { FC } from 'react';\nimport { CheckCircle2, Loader2, Trash2 } from 'lucide-react';\nimport { Thumbnail } from './Thumbnail';\nimport { StatusBadge } from './StatusBadge';\nimport { categoryIcon } from '../utils/icons';\nimport { titleCase, formatRelativeTime, formatPrice } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { CooWardrobeItem } from '../types';\n\ninterface WardrobeItemCardProps {\n item: CooWardrobeItem;\n onLogWear?: () => void;\n onRemove?: () => void;\n isLoggingWear?: boolean;\n isRemoving?: boolean;\n className?: string;\n}\n\nexport const WardrobeItemCard: FC<WardrobeItemCardProps> = ({\n item,\n onLogWear,\n onRemove,\n isLoggingWear,\n isRemoving,\n className,\n}) => {\n const subtitle = item.brandName || item.colour || titleCase(item.category);\n\n return (\n <div\n className={cn(\n 'group flex flex-col overflow-hidden rounded-xl border border-border-seam bg-bg-surface shadow-sm transition-shadow hover:shadow-lg',\n className\n )}\n >\n {/* Image-forward hero */}\n <div className=\"relative\">\n <Thumbnail\n icon={categoryIcon(item.category, 40)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"aspect-[4/5] w-full rounded-none\"\n />\n\n {/* Worn-count badge */}\n <div className=\"absolute right-2.5 top-2.5 flex h-12 w-12 flex-col items-center justify-center rounded-full bg-bg-surface/90 text-center leading-none shadow-sm ring-1 ring-[var(--color-accent-soft)] backdrop-blur\">\n <span className=\"text-[9px] font-medium uppercase tracking-wide text-text-secondary\">\n worn\n </span>\n <span className=\"text-sm font-bold tabular-nums text-text-primary\">\n {item.timesWorn}×\n </span>\n </div>\n\n {/* Hover action bar */}\n <div className=\"absolute inset-x-0 bottom-0 flex items-center gap-1.5 bg-gradient-to-t from-bg-canvas/90 to-transparent p-2.5 pt-8 opacity-100 transition-opacity duration-200 sm:opacity-0 sm:focus-within:opacity-100 sm:group-hover:opacity-100\">\n <button\n type=\"button\"\n disabled={isLoggingWear}\n onClick={onLogWear}\n aria-label={`Log a wear for ${item.name}`}\n className=\"inline-flex items-center gap-1 rounded-lg bg-bg-surface/90 px-2.5 py-1 text-xs font-medium text-status-success-text shadow-sm backdrop-blur transition-colors hover:bg-bg-surface disabled:opacity-50\"\n >\n {isLoggingWear ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"h-3.5 w-3.5\" />\n )}\n Wore it\n </button>\n <button\n type=\"button\"\n disabled={isRemoving}\n onClick={onRemove}\n aria-label={`Remove ${item.name}`}\n className=\"ml-auto inline-flex h-7 w-7 items-center justify-center rounded-lg bg-bg-surface/90 text-text-secondary shadow-sm backdrop-blur transition-colors hover:text-status-error-text disabled:opacity-50\"\n >\n {isRemoving ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n ) : (\n <Trash2 className=\"h-3.5 w-3.5\" />\n )}\n </button>\n </div>\n </div>\n\n {/* Details */}\n <div className=\"flex flex-1 flex-col p-4\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">{item.name}</p>\n <p className=\"truncate text-xs text-text-secondary\">{subtitle}</p>\n <div className=\"mt-2 flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={item.source} />\n {item.size && (\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {item.size}\n </span>\n )}\n </div>\n {item.notes && (\n <p className=\"mt-2 line-clamp-2 text-xs text-text-secondary\">{item.notes}</p>\n )}\n <div className=\"mt-3 flex items-center justify-between gap-2 border-t border-border-seam pt-2 text-xs text-text-secondary\">\n <span>\n {item.lastWornAt ? `Last worn ${formatRelativeTime(item.lastWornAt)}` : 'Not worn yet'}\n </span>\n {item.costPerWear != null ? (\n <span\n title={`${formatPrice(item.purchasePrice ?? 0, item.currency ?? 'USD')} ÷ ${item.timesWorn} wears`}\n className=\"shrink-0 font-medium text-text-primary\"\n >\n {formatPrice(item.costPerWear, item.currency ?? 'USD')}/wear\n </span>\n ) : item.purchasePrice != null ? (\n <span className=\"shrink-0 text-text-secondary\">\n {formatPrice(item.purchasePrice, item.currency ?? 'USD')} paid\n </span>\n ) : null}\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":";;;;;;;;AA2BA,IAAa,KAA+C,EAC1D,SACA,cACA,aACA,kBACA,eACA,mBACI;CACJ,IAAM,IAAW,EAAK,aAAa,EAAK,UAAU,EAAU,EAAK,
|
|
1
|
+
{"version":3,"file":"WardrobeItemCard.js","names":[],"sources":["../../../src/coolandlovely/components/WardrobeItemCard.tsx"],"sourcesContent":["/**\n * WardrobeItemCard — a digital wardrobe piece card for list/grids.\n * @module coolandlovely/components\n *\n * Image-forward card with a worn-count badge, hover action bar (log wear / remove),\n * details, size, source and cost-per-wear. Extracted from WardrobeListPage for\n * isolated rendering/tests.\n */\n\nimport type { FC } from 'react';\nimport { CheckCircle2, Loader2, Trash2 } from 'lucide-react';\nimport { Thumbnail } from './Thumbnail';\nimport { StatusBadge } from './StatusBadge';\nimport { categoryIcon } from '../utils/icons';\nimport { titleCase, formatRelativeTime, formatPrice } from '../utils/formatters';\nimport { cn } from '../utils/cn';\nimport type { CooWardrobeItem } from '../types';\n\ninterface WardrobeItemCardProps {\n item: CooWardrobeItem;\n onLogWear?: () => void;\n onRemove?: () => void;\n isLoggingWear?: boolean;\n isRemoving?: boolean;\n className?: string;\n}\n\nexport const WardrobeItemCard: FC<WardrobeItemCardProps> = ({\n item,\n onLogWear,\n onRemove,\n isLoggingWear,\n isRemoving,\n className,\n}) => {\n const subtitle = item.brandName || item.colour || titleCase(item.category);\n\n return (\n <div\n className={cn(\n 'group flex flex-col overflow-hidden rounded-xl border border-border-seam bg-bg-surface shadow-sm transition-shadow hover:shadow-lg',\n className\n )}\n >\n {/* Image-forward hero */}\n <div className=\"relative\">\n <Thumbnail\n icon={categoryIcon(item.category, 40)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"aspect-[4/5] w-full rounded-none\"\n />\n\n {/* Worn-count badge */}\n <div className=\"absolute right-2.5 top-2.5 flex h-12 w-12 flex-col items-center justify-center rounded-full bg-bg-surface/90 text-center leading-none shadow-sm ring-1 ring-[var(--color-accent-soft)] backdrop-blur\">\n <span className=\"text-[9px] font-medium uppercase tracking-wide text-text-secondary\">\n worn\n </span>\n <span className=\"text-sm font-bold tabular-nums text-text-primary\">\n {item.timesWorn}×\n </span>\n </div>\n\n {/* Hover action bar */}\n <div className=\"absolute inset-x-0 bottom-0 flex items-center gap-1.5 bg-gradient-to-t from-bg-canvas/90 to-transparent p-2.5 pt-8 opacity-100 transition-opacity duration-200 sm:opacity-0 sm:focus-within:opacity-100 sm:group-hover:opacity-100\">\n <button\n type=\"button\"\n disabled={isLoggingWear}\n onClick={onLogWear}\n aria-label={`Log a wear for ${item.name}`}\n className=\"inline-flex items-center gap-1 rounded-lg bg-bg-surface/90 px-2.5 py-1 text-xs font-medium text-status-success-text shadow-sm backdrop-blur transition-colors hover:bg-bg-surface disabled:opacity-50\"\n >\n {isLoggingWear ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"h-3.5 w-3.5\" />\n )}\n Wore it\n </button>\n <button\n type=\"button\"\n disabled={isRemoving}\n onClick={onRemove}\n aria-label={`Remove ${item.name}`}\n className=\"ml-auto inline-flex h-7 w-7 items-center justify-center rounded-lg bg-bg-surface/90 text-text-secondary shadow-sm backdrop-blur transition-colors hover:text-status-error-text disabled:opacity-50\"\n >\n {isRemoving ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n ) : (\n <Trash2 className=\"h-3.5 w-3.5\" />\n )}\n </button>\n </div>\n </div>\n\n {/* Details */}\n <div className=\"flex flex-1 flex-col p-4\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">{item.name}</p>\n <p className=\"truncate text-xs text-text-secondary\">{subtitle}</p>\n <div className=\"mt-2 flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={item.source} />\n {item.size && (\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {item.size}\n </span>\n )}\n </div>\n {item.notes && (\n <p className=\"mt-2 line-clamp-2 text-xs text-text-secondary\">{item.notes}</p>\n )}\n <div className=\"mt-3 flex items-center justify-between gap-2 border-t border-border-seam pt-2 text-xs text-text-secondary\">\n <span>\n {item.lastWornAt ? `Last worn ${formatRelativeTime(item.lastWornAt)}` : 'Not worn yet'}\n </span>\n {item.costPerWear != null ? (\n <span\n title={`${formatPrice(item.purchasePrice ?? 0, item.currency ?? 'USD')} ÷ ${item.timesWorn} wears`}\n className=\"shrink-0 font-medium text-text-primary\"\n >\n {formatPrice(item.costPerWear, item.currency ?? 'USD')}/wear\n </span>\n ) : item.purchasePrice != null ? (\n <span className=\"shrink-0 text-text-secondary\">\n {formatPrice(item.purchasePrice, item.currency ?? 'USD')} paid\n </span>\n ) : null}\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":";;;;;;;;AA2BA,IAAa,KAA+C,EAC1D,SACA,cACA,aACA,kBACA,eACA,mBACI;CACJ,IAAM,IAAW,EAAK,aAAa,EAAK,UAAU,EAAU,EAAK,QAAQ;CAEzE,OACE,kBAAC,OAAD;EACE,WAAW,EACT,sIACA,CACF;YAJF,CAOE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KACE,MAAM,EAAa,EAAK,UAAU,EAAE;KACpC,MAAM,EAAK;KACX,QAAQ,EAAK,OAAO;KACpB,KAAK,EAAK;KACV,WAAU;IACX,CAAA;IAGD,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAqE;KAE/E,CAAA,GACN,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACG,EAAK,WAAU,GACZ;OACH;;IAGL,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,MAAK;MACL,UAAU;MACV,SAAS;MACT,cAAY,kBAAkB,EAAK;MACnC,WAAU;gBALZ,CAOG,IACC,kBAAC,GAAD,EAAS,WAAU,2BAA4B,CAAA,IAE/C,kBAAC,GAAD,EAAc,WAAU,cAAe,CAAA,GACvC,SAEI;SACR,kBAAC,UAAD;MACE,MAAK;MACL,UAAU;MACV,SAAS;MACT,cAAY,UAAU,EAAK;MAC3B,WAAU;gBAET,IACC,kBAAC,GAAD,EAAS,WAAU,2BAA4B,CAAA,IAE/C,kBAAC,GAAD,EAAQ,WAAU,cAAe,CAAA;KAE7B,CAAA,CACL;;GACF;MAGL,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,KAAD;KAAG,WAAU;eAAoD,EAAK;IAAQ,CAAA;IAC9E,kBAAC,KAAD;KAAG,WAAU;eAAwC;IAAY,CAAA;IACjE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAa,QAAQ,EAAK,OAAS,CAAA,GAClC,EAAK,QACJ,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAK;KACF,CAAA,CAEL;;IACJ,EAAK,SACJ,kBAAC,KAAD;KAAG,WAAU;eAAiD,EAAK;IAAS,CAAA;IAE9E,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD,EAAA,UACG,EAAK,aAAa,aAAa,EAAmB,EAAK,UAAU,MAAM,eACpE,CAAA,GACL,EAAK,eAAe,OAOjB,EAAK,iBAAiB,OAItB,OAHF,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACG,EAAY,EAAK,eAAe,EAAK,YAAY,KAAK,GAAE,OACrD;UATN,kBAAC,QAAD;MACE,OAAO,GAAG,EAAY,EAAK,iBAAiB,GAAG,EAAK,YAAY,KAAK,EAAE,KAAK,EAAK,UAAU;MAC3F,WAAU;gBAFZ,CAIG,EAAY,EAAK,aAAa,EAAK,YAAY,KAAK,GAAE,OACnD;OAML;;GACF;IACF;;AAET"}
|