@burdenoff/microfe-coolandlovely 2026.1031.2 → 2026.1101.1
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":"AdminFeaturedItemsPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/admin/AdminFeaturedItemsPage.tsx"],"sourcesContent":["/**\n * Admin Featured Items Page\n * @module coolandlovely/pages/admin\n *\n * Curation surface for featured style items. Lists the catalogue and lets admins\n * feature or unfeature individual items for curated discovery surfaces.\n */\n\nimport { useState } from 'react';\nimport { Shirt, Star } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { useStyleItems, useFeatureStyleItem, useUnfeatureStyleItem } from '../../hooks';\nimport { categoryIcon, STYLE_CATEGORIES } from '../../utils/icons';\nimport { formatPrice, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooStyleCategory, CooStyleItem } from '../../types';\n\nconst PAGE_SIZE = 24;\n\nfunction FeaturedItemRow({ item }: { item: CooStyleItem }) {\n const feature = useFeatureStyleItem();\n const unfeature = useUnfeatureStyleItem();\n const isBusy = feature.isPending || unfeature.isPending;\n\n return (\n <div className=\"flex items-center gap-4 rounded-lg border border-border-seam bg-bg-surface p-3\">\n <Thumbnail\n icon={categoryIcon(item.category, 20)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"h-14 w-14 shrink-0 rounded-lg\"\n />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">{item.name}</h3>\n {item.isFeatured && (\n <Star className=\"h-3.5 w-3.5 shrink-0 fill-status-warning-text text-status-warning-text\" />\n )}\n </div>\n <p className=\"truncate text-xs text-text-secondary\">\n {item.brand?.name ?? 'Unknown brand'} · {titleCase(item.category)}\n </p>\n <p className=\"mt-0.5 text-sm font-medium text-text-primary\">\n {formatPrice(item.price, item.currency)}\n </p>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <StatusBadge status={item.status} />\n {item.isFeatured ? (\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={() => unfeature.mutate(item.id)}\n className={cn(\n 'inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium',\n 'bg-bg-sunken text-text-secondary hover:bg-bg-sunken/80',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n Unfeature\n </button>\n ) : (\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={() => feature.mutate(item.id)}\n className={cn(\n 'inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium',\n 'bg-status-warning-bg-subtle text-status-warning-text hover:bg-status-warning-bg-subtle/80',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n <Star className=\"h-4 w-4\" /> Feature\n </button>\n )}\n </div>\n </div>\n );\n}\n\nexport function AdminFeaturedItemsPage() {\n const [category, setCategory] = useState<CooStyleCategory | 'ALL'>('ALL');\n const [offset, setOffset] = useState(0);\n\n const query = useStyleItems({\n category: category === 'ALL' ? undefined : category,\n pagination: { limit: PAGE_SIZE, offset },\n });\n const items = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title=\"Admin — Featured Items\"\n description=\"Curate style items for discovery surfaces\"\n >\n <div className=\"mb-6 flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setCategory('ALL');\n setOffset(0);\n }}\n className={cn(\n 'rounded-full px-3 py-1.5 text-xs font-medium transition-colors',\n category === 'ALL'\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n All categories\n </button>\n {STYLE_CATEGORIES.map((c) => (\n <button\n key={c}\n type=\"button\"\n onClick={() => {\n setCategory(c);\n setOffset(0);\n }}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors',\n category === c\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {categoryIcon(c, 14)} {titleCase(c)}\n </button>\n ))}\n </div>\n\n {query.isLoading ? (\n <LoadingView label=\"Loading style items…\" />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Shirt className=\"h-10 w-10\" />}\n title=\"No style items found\"\n description=\"No items match this filter.\"\n />\n ) : (\n <>\n <div className=\"space-y-3\">\n {items.map((item) => (\n <FeaturedItemRow key={item.id} item={item} />\n ))}\n </div>\n\n {(offset > 0 || hasMore) && (\n <div className=\"mt-6 flex items-center justify-center gap-2\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n Previous\n </button>\n <span className=\"text-sm text-text-secondary\">Page {offset / PAGE_SIZE + 1}</span>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset((o) => o + PAGE_SIZE)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n Next\n </button>\n </div>\n )}\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAqBA,IAAM,IAAY;AAElB,SAAS,EAAgB,EAAE,WAAgC;CACzD,IAAM,IAAU,
|
|
1
|
+
{"version":3,"file":"AdminFeaturedItemsPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/admin/AdminFeaturedItemsPage.tsx"],"sourcesContent":["/**\n * Admin Featured Items Page\n * @module coolandlovely/pages/admin\n *\n * Curation surface for featured style items. Lists the catalogue and lets admins\n * feature or unfeature individual items for curated discovery surfaces.\n */\n\nimport { useState } from 'react';\nimport { Shirt, Star } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { useStyleItems, useFeatureStyleItem, useUnfeatureStyleItem } from '../../hooks';\nimport { categoryIcon, STYLE_CATEGORIES } from '../../utils/icons';\nimport { formatPrice, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooStyleCategory, CooStyleItem } from '../../types';\n\nconst PAGE_SIZE = 24;\n\nfunction FeaturedItemRow({ item }: { item: CooStyleItem }) {\n const feature = useFeatureStyleItem();\n const unfeature = useUnfeatureStyleItem();\n const isBusy = feature.isPending || unfeature.isPending;\n\n return (\n <div className=\"flex items-center gap-4 rounded-lg border border-border-seam bg-bg-surface p-3\">\n <Thumbnail\n icon={categoryIcon(item.category, 20)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"h-14 w-14 shrink-0 rounded-lg\"\n />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">{item.name}</h3>\n {item.isFeatured && (\n <Star className=\"h-3.5 w-3.5 shrink-0 fill-status-warning-text text-status-warning-text\" />\n )}\n </div>\n <p className=\"truncate text-xs text-text-secondary\">\n {item.brand?.name ?? 'Unknown brand'} · {titleCase(item.category)}\n </p>\n <p className=\"mt-0.5 text-sm font-medium text-text-primary\">\n {formatPrice(item.price, item.currency)}\n </p>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <StatusBadge status={item.status} />\n {item.isFeatured ? (\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={() => unfeature.mutate(item.id)}\n className={cn(\n 'inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium',\n 'bg-bg-sunken text-text-secondary hover:bg-bg-sunken/80',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n Unfeature\n </button>\n ) : (\n <button\n type=\"button\"\n disabled={isBusy}\n onClick={() => feature.mutate(item.id)}\n className={cn(\n 'inline-flex items-center gap-1 rounded-lg px-3 py-1.5 text-sm font-medium',\n 'bg-status-warning-bg-subtle text-status-warning-text hover:bg-status-warning-bg-subtle/80',\n 'disabled:cursor-not-allowed disabled:opacity-50'\n )}\n >\n <Star className=\"h-4 w-4\" /> Feature\n </button>\n )}\n </div>\n </div>\n );\n}\n\nexport function AdminFeaturedItemsPage() {\n const [category, setCategory] = useState<CooStyleCategory | 'ALL'>('ALL');\n const [offset, setOffset] = useState(0);\n\n const query = useStyleItems({\n category: category === 'ALL' ? undefined : category,\n pagination: { limit: PAGE_SIZE, offset },\n });\n const items = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title=\"Admin — Featured Items\"\n description=\"Curate style items for discovery surfaces\"\n >\n <div className=\"mb-6 flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setCategory('ALL');\n setOffset(0);\n }}\n className={cn(\n 'rounded-full px-3 py-1.5 text-xs font-medium transition-colors',\n category === 'ALL'\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n All categories\n </button>\n {STYLE_CATEGORIES.map((c) => (\n <button\n key={c}\n type=\"button\"\n onClick={() => {\n setCategory(c);\n setOffset(0);\n }}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors',\n category === c\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {categoryIcon(c, 14)} {titleCase(c)}\n </button>\n ))}\n </div>\n\n {query.isLoading ? (\n <LoadingView label=\"Loading style items…\" />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Shirt className=\"h-10 w-10\" />}\n title=\"No style items found\"\n description=\"No items match this filter.\"\n />\n ) : (\n <>\n <div className=\"space-y-3\">\n {items.map((item) => (\n <FeaturedItemRow key={item.id} item={item} />\n ))}\n </div>\n\n {(offset > 0 || hasMore) && (\n <div className=\"mt-6 flex items-center justify-center gap-2\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n Previous\n </button>\n <span className=\"text-sm text-text-secondary\">Page {offset / PAGE_SIZE + 1}</span>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset((o) => o + PAGE_SIZE)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n Next\n </button>\n </div>\n )}\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrandDetailPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandDetailPage.tsx"],"sourcesContent":["/**\n * Brand Detail Page\n * @module coolandlovely/pages/brands\n *\n * Renders a single brand storefront (`cooBrand`) with its publish-lifecycle\n * transitions (`cooTransitionBrand`), collections and style items.\n */\n\nimport { useParams } from 'react-router';\nimport {\n Store,\n ExternalLink,\n Pencil,\n ArrowLeft,\n Shapes,\n Shirt,\n Loader2,\n Plus,\n Package,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { BrandCover } from '../../components/BrandCover';\nimport { TagList } from '../../components/TagList';\nimport { Comments } from '../../components/Comments';\nimport { StepTracker } from '../../components/StepTracker';\nimport { ActivityTrail } from '../../components/ActivityTrail';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useAssistantEntityName, useBrand, useTransitionBrand, useUpdateBrand } from '../../hooks';\nimport { categoryIcon } from '../../utils/icons';\nimport { formatPrice, formatDate, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooBrandStatus } from '../../types';\n\n/** Allowed next states per the svc brand state machine (UX guidance only;\n * the service is the authority and rejects illegal jumps). */\nconst NEXT_STATES: Record<CooBrandStatus, CooBrandStatus[]> = {\n DRAFT: ['PENDING_REVIEW', 'ARCHIVED'],\n PENDING_REVIEW: ['PUBLISHED', 'DRAFT', 'ARCHIVED'],\n PUBLISHED: ['SUSPENDED', 'ARCHIVED'],\n SUSPENDED: ['PUBLISHED', 'ARCHIVED'],\n ARCHIVED: [],\n};\n\n/** The publish path a storefront walks. SUSPENDED/ARCHIVED are exits from it,\n * not steps on it, so a brand sitting in one is tracked separately below. */\nconst LIFECYCLE_STEPS: CooBrandStatus[] = ['DRAFT', 'PENDING_REVIEW', 'PUBLISHED'];\n\n/**\n * Reframe the gateway's RBAC refusal in the operator's language. ONLY a\n * permission refusal is relabelled — every other failure keeps the service's\n * own message, so a validation error is never disguised as a role problem.\n */\nfunction transitionErrorMessage(error: unknown): string {\n const raw = error instanceof Error ? error.message : 'Transition failed';\n return /forbidden|permission|not allowed|unauthori[sz]ed/i.test(raw)\n ? 'Your role does not permit this action.'\n : raw;\n}\n\nexport function BrandDetailPage() {\n const { brandId } = useParams<{ brandId: string }>();\n const { navigateTo } = useCoolAndLovely();\n const query = useBrand(brandId);\n const transition = useTransitionBrand();\n const updateBrand = useUpdateBrand();\n // Tell the assistant WHICH storefront is on screen (see useAssistantEntity).\n useAssistantEntityName(brandId, query.data?.name);\n\n if (query.isLoading) {\n return (\n <PageLayout title=\"Brand\">\n <LoadingView label=\"Loading brand…\" />\n </PageLayout>\n );\n }\n\n if (query.isError || !query.data) {\n return (\n <PageLayout title=\"Brand\">\n <ErrorView\n title=\"Brand not found\"\n message=\"This storefront could not be loaded or does not exist.\"\n onRetry={() => void query.refetch()}\n />\n </PageLayout>\n );\n }\n\n const brand = query.data;\n const collections = brand.collections ?? [];\n const items = brand.items ?? [];\n const nextStates = NEXT_STATES[brand.status];\n const trackerSteps = LIFECYCLE_STEPS.includes(brand.status)\n ? LIFECYCLE_STEPS\n : ([...LIFECYCLE_STEPS.slice(0, 1), brand.status] as CooBrandStatus[]);\n // Only render a website link for safe http(s) URLs (block javascript:/data: sinks).\n const safeWebsiteUrl =\n brand.websiteUrl && /^https?:\\/\\//i.test(brand.websiteUrl) ? brand.websiteUrl : undefined;\n\n return (\n <PageLayout title={brand.name} description={`@${brand.handle}`}>\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Main column */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* Storefront hero — cover + logo + identity overlay, floating actions */}\n <BrandCover\n coverFileId={brand.cover?.id}\n logoFileId={brand.logo?.id}\n name={brand.name}\n seed={brand.id}\n className=\"overflow-hidden rounded-xl border border-border-seam shadow-sm\"\n overlay={\n <>\n <div className=\"absolute right-3 top-3 flex flex-wrap items-center justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-bg-surface/80 px-3 py-2 text-sm font-medium text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/brand-orders?brand=${brand.id}`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-bg-surface/80 px-3 py-2 text-sm font-medium text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface\"\n >\n <Package className=\"h-4 w-4\" /> Orders\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/brands/${brand.id}/edit`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text shadow-sm transition-colors hover:bg-action-primary-bg/90\"\n >\n <Pencil className=\"h-4 w-4\" /> Edit\n </button>\n </div>\n <div className=\"absolute inset-x-0 bottom-0 flex items-end gap-3 p-4 sm:gap-4 sm:p-5\">\n <Thumbnail\n icon={<Store className=\"h-7 w-7\" />}\n seed={brand.id}\n fileId={brand.logo?.id}\n alt={`${brand.name} logo`}\n className=\"h-16 w-16 shrink-0 rounded-full shadow-md ring-2 ring-bg-surface\"\n />\n <div className=\"min-w-0 pb-1\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-0.5\">\n <h2 className=\"truncate text-xl font-semibold text-text-primary sm:text-2xl\">\n {brand.name}\n </h2>\n <span className=\"text-sm text-text-secondary\">@{brand.handle}</span>\n </div>\n {brand.tagline && (\n <p className=\"mt-1 line-clamp-2 text-sm text-text-primary\">{brand.tagline}</p>\n )}\n </div>\n </div>\n </>\n }\n />\n\n {/* Storefront meta — aesthetics, status, bio, tags, website */}\n <section className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n {brand.aesthetics.length > 0 ? (\n <TagList tags={brand.aesthetics.map(titleCase)} tone=\"accent\" max={12} />\n ) : (\n <span />\n )}\n <StatusBadge status={brand.status} />\n </div>\n {brand.bio && <p className=\"text-sm text-text-secondary\">{brand.bio}</p>}\n {brand.tags.length > 0 && <TagList tags={brand.tags} max={12} />}\n {safeWebsiteUrl ? (\n <a\n href={safeWebsiteUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex max-w-full items-center gap-1.5 text-sm font-medium text-action-primary-bg hover:underline\"\n >\n <ExternalLink className=\"h-4 w-4 shrink-0\" />{' '}\n <span className=\"break-all\">{safeWebsiteUrl}</span>\n </a>\n ) : brand.websiteUrl ? (\n <p className=\"break-all text-sm text-text-secondary\">{brand.websiteUrl}</p>\n ) : null}\n </section>\n\n {/* Collections */}\n <section className=\"mt-6\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h3 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Shapes className=\"h-5 w-5\" /> Collections ({collections.length})\n </h3>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/collections/new?brandId=${brand.id}`)}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Plus className=\"h-4 w-4\" /> Add collection\n </button>\n </div>\n {collections.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No collections yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {collections.map((c) => (\n <button\n key={c.id}\n type=\"button\"\n onClick={() => navigateTo(`/collections/${c.id}`)}\n className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3 text-left transition-colors hover:bg-bg-sunken/50\"\n >\n <Thumbnail\n icon={<Shapes className=\"h-5 w-5\" />}\n seed={c.id}\n fileId={c.cover?.id}\n alt={c.name}\n className=\"h-11 w-11 shrink-0\"\n />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{c.name}</p>\n <p className=\"text-xs text-text-secondary\">{titleCase(c.season)}</p>\n </div>\n <StatusBadge status={c.status} />\n </button>\n ))}\n </div>\n )}\n </section>\n\n {/* Items */}\n <section className=\"mt-6\">\n <h3 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Shirt className=\"h-5 w-5\" /> Style items ({items.length})\n </h3>\n {items.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No items yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3\">\n {items.map((item) => (\n <button\n key={item.id}\n type=\"button\"\n onClick={() => navigateTo(`/items/${item.id}`)}\n className=\"overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left transition-all hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <Thumbnail\n icon={categoryIcon(item.category, 24)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"h-24 w-full\"\n />\n <div className=\"p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{item.name}</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatPrice(item.price, item.currency)}\n </p>\n </div>\n </button>\n ))}\n </div>\n )}\n </section>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-4\">\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Publish lifecycle</h3>\n <StepTracker steps={trackerSteps} current={brand.status} className=\"mb-5\" />\n <div className=\"mb-3 flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">Current:</span>\n <StatusBadge status={brand.status} />\n </div>\n {nextStates.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n This brand is archived (terminal state).\n </p>\n ) : (\n <div className=\"flex flex-col gap-2\">\n {nextStates.map((next) => (\n <button\n key={next}\n type=\"button\"\n disabled={transition.isPending}\n onClick={() => transition.mutate({ id: brand.id, status: next })}\n className={cn(\n 'inline-flex 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 >\n {transition.isPending && <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />}\n Move to {titleCase(next)}\n </button>\n ))}\n </div>\n )}\n {transition.isError && (\n <p className=\"mt-2 text-xs text-status-error-text\">\n {transitionErrorMessage(transition.error)}\n </p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Details</h3>\n <dl className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Handle</dt>\n <dd className=\"text-text-primary\">@{brand.handle}</dd>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <dt className=\"text-text-secondary\">Featured</dt>\n <dd className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={brand.isFeatured}\n aria-label={brand.isFeatured ? 'Unfeature this brand' : 'Feature this brand'}\n disabled={updateBrand.isPending}\n onClick={() =>\n updateBrand.mutate({\n id: brand.id,\n input: { isFeatured: !brand.isFeatured },\n })\n }\n className={cn(\n 'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-60',\n brand.isFeatured ? 'bg-action-primary-bg' : 'bg-bg-sunken'\n )}\n >\n <span\n className={cn(\n 'inline-block h-4 w-4 rounded-full bg-bg-surface shadow transition-transform',\n brand.isFeatured ? 'translate-x-[1.125rem]' : 'translate-x-0.5'\n )}\n />\n </button>\n {updateBrand.isPending && (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin text-text-secondary\" />\n )}\n </dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Created</dt>\n <dd className=\"text-text-primary\">\n {formatDate(brand.createdAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Updated</dt>\n <dd className=\"text-text-primary\">\n {formatDate(brand.updatedAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n </dl>\n </GlassCard>\n\n {/* Audit trail — who moved this storefront through its lifecycle, from\n the platform ActivityLog rows the svc's audit plugin writes. */}\n <ActivityTrail resourceType=\"coolandlovely-brand\" resourceId={brand.id} />\n\n {/* Reviews & feedback — moved into the sidebar to match the storefront layout */}\n <Comments entityType=\"CooBrand\" entityId={brand.id} className=\"mt-0\" />\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuCA,IAAM,IAAwD;CAC5D,OAAO,CAAC,kBAAkB,WAAW;CACrC,gBAAgB;EAAC;EAAa;EAAS;EAAW;CAClD,WAAW,CAAC,aAAa,WAAW;CACpC,WAAW,CAAC,aAAa,WAAW;CACpC,UAAU,EAAE;CACb,EAIK,IAAoC;CAAC;CAAS;CAAkB;CAAY;AAOlF,SAAS,EAAuB,GAAwB;CACtD,IAAM,IAAM,aAAiB,QAAQ,EAAM,UAAU;AACrD,QAAO,oDAAoD,KAAK,EAAI,GAChE,2CACA;;AAGN,SAAgB,IAAkB;CAChC,IAAM,EAAE,eAAY,GAAgC,EAC9C,EAAE,kBAAe,GAAkB,EACnC,IAAQ,EAAS,EAAQ,EACzB,IAAa,GAAoB,EACjC,IAAc,GAAgB;AAIpC,KAFA,EAAuB,GAAS,EAAM,MAAM,KAAK,EAE7C,EAAM,UACR,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD,EAAa,OAAM,kBAAmB,CAAA;EAC3B,CAAA;AAIjB,KAAI,EAAM,WAAW,CAAC,EAAM,KAC1B,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD;GACE,OAAM;GACN,SAAQ;GACR,eAAe,KAAK,EAAM,SAAS;GACnC,CAAA;EACS,CAAA;CAIjB,IAAM,IAAQ,EAAM,MACd,IAAc,EAAM,eAAe,EAAE,EACrC,IAAQ,EAAM,SAAS,EAAE,EACzB,IAAa,EAAY,EAAM,SAC/B,IAAe,EAAgB,SAAS,EAAM,OAAO,GACvD,IACC,CAAC,GAAG,EAAgB,MAAM,GAAG,EAAE,EAAE,EAAM,OAAO,EAE7C,IACJ,EAAM,cAAc,gBAAgB,KAAK,EAAM,WAAW,GAAG,EAAM,aAAa,KAAA;AAElF,QACE,kBAAC,GAAD;EAAY,OAAO,EAAM;EAAM,aAAa,IAAI,EAAM;YACpD,kBAAC,OAAD;GAAK,WAAU;aAAf,CAEE,kBAAC,OAAD;IAAK,WAAU;cAAf;KAEE,kBAAC,GAAD;MACE,aAAa,EAAM,OAAO;MAC1B,YAAY,EAAM,MAAM;MACxB,MAAM,EAAM;MACZ,MAAM,EAAM;MACZ,WAAU;MACV,SACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,UAAD;SACE,MAAK;SACL,eAAe,EAAW,UAAU;SACpC,WAAU;mBAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAAA,QAC1B;;QACT,kBAAC,UAAD;SACE,MAAK;SACL,eAAe,EAAW,uBAAuB,EAAM,KAAK;SAC5D,WAAU;mBAHZ,CAKE,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA,EAAA,UACxB;;QACT,kBAAC,UAAD;SACE,MAAK;SACL,eAAe,EAAW,WAAW,EAAM,GAAG,OAAO;SACrD,WAAU;mBAHZ,CAKE,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA,EAAA,QACvB;;QACL;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QACE,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;QACnC,MAAM,EAAM;QACZ,QAAQ,EAAM,MAAM;QACpB,KAAK,GAAG,EAAM,KAAK;QACnB,WAAU;QACV,CAAA,EACF,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,MAAD;UAAI,WAAU;oBACX,EAAM;UACJ,CAAA,EACL,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CAA8C,KAAE,EAAM,OAAc;YAChE;YACL,EAAM,WACL,kBAAC,KAAD;SAAG,WAAU;mBAA+C,EAAM;SAAY,CAAA,CAE5E;UACF;SACL,EAAA,CAAA;MAEL,CAAA;KAGF,kBAAC,WAAD;MAAS,WAAU;gBAAnB;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACG,EAAM,WAAW,SAAS,IACzB,kBAAC,GAAD;SAAS,MAAM,EAAM,WAAW,IAAI,EAAU;SAAE,MAAK;SAAS,KAAK;SAAM,CAAA,GAEzE,kBAAC,QAAD,EAAQ,CAAA,EAEV,kBAAC,GAAD,EAAa,QAAQ,EAAM,QAAU,CAAA,CACjC;;OACL,EAAM,OAAO,kBAAC,KAAD;QAAG,WAAU;kBAA+B,EAAM;QAAQ,CAAA;OACvE,EAAM,KAAK,SAAS,KAAK,kBAAC,GAAD;QAAS,MAAM,EAAM;QAAM,KAAK;QAAM,CAAA;OAC/D,IACC,kBAAC,KAAD;QACE,MAAM;QACN,QAAO;QACP,KAAI;QACJ,WAAU;kBAJZ;SAME,kBAAC,GAAD,EAAc,WAAU,oBAAqB,CAAA;SAAC;SAC9C,kBAAC,QAAD;UAAM,WAAU;oBAAa;UAAsB,CAAA;SACjD;YACF,EAAM,aACR,kBAAC,KAAD;QAAG,WAAU;kBAAyC,EAAM;QAAe,CAAA,GACzE;OACI;;KAGV,kBAAC,WAAD;MAAS,WAAU;gBAAnB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,MAAD;QAAI,WAAU;kBAAd;SACE,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;;SAAe,EAAY;SAAO;SAC7D;WACL,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAW,4BAA4B,EAAM,KAAK;QACjE,WAAU;kBAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,kBACrB;UACL;UACL,EAAY,WAAW,IACtB,kBAAC,KAAD;OAAG,WAAU;iBAAqF;OAE9F,CAAA,GAEJ,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAY,KAAK,MAChB,kBAAC,UAAD;QAEE,MAAK;QACL,eAAe,EAAW,gBAAgB,EAAE,KAAK;QACjD,WAAU;kBAJZ;SAME,kBAAC,GAAD;UACE,MAAM,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;UACpC,MAAM,EAAE;UACR,QAAQ,EAAE,OAAO;UACjB,KAAK,EAAE;UACP,WAAU;UACV,CAAA;SACF,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAkD,EAAE;WAAS,CAAA,EAC1E,kBAAC,KAAD;WAAG,WAAU;qBAA+B,EAAU,EAAE,OAAO;WAAK,CAAA,CAChE;;SACN,kBAAC,GAAD,EAAa,QAAQ,EAAE,QAAU,CAAA;SAC1B;UAjBF,EAAE,GAiBA,CACT;OACE,CAAA,CAEA;;KAGV,kBAAC,WAAD;MAAS,WAAU;gBAAnB,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;;QAAe,EAAM;QAAO;QACtD;UACJ,EAAM,WAAW,IAChB,kBAAC,KAAD;OAAG,WAAU;iBAAqF;OAE9F,CAAA,GAEJ,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAM,KAAK,MACV,kBAAC,UAAD;QAEE,MAAK;QACL,eAAe,EAAW,UAAU,EAAK,KAAK;QAC9C,WAAU;kBAJZ,CAME,kBAAC,GAAD;SACE,MAAM,EAAa,EAAK,UAAU,GAAG;SACrC,MAAM,EAAK;SACX,QAAQ,EAAK,OAAO;SACpB,KAAK,EAAK;SACV,WAAU;SACV,CAAA,EACF,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,KAAD;UAAG,WAAU;oBAAkD,EAAK;UAAS,CAAA,EAC7E,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAY,EAAK,OAAO,EAAK,SAAS;UACrC,CAAA,CACA;WACC;UAlBF,EAAK,GAkBH,CACT;OACE,CAAA,CAEA;;KACN;OAGN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAW,WAAU;MAAU,WAAU;gBAAzC;OACE,kBAAC,MAAD;QAAI,WAAU;kBAA+C;QAAsB,CAAA;OACnF,kBAAC,GAAD;QAAa,OAAO;QAAc,SAAS,EAAM;QAAQ,WAAU;QAAS,CAAA;OAC5E,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD;SAAM,WAAU;mBAA8B;SAAe,CAAA,EAC7D,kBAAC,GAAD,EAAa,QAAQ,EAAM,QAAU,CAAA,CACjC;;OACL,EAAW,WAAW,IACrB,kBAAC,KAAD;QAAG,WAAU;kBAA8B;QAEvC,CAAA,GAEJ,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAW,KAAK,MACf,kBAAC,UAAD;SAEE,MAAK;SACL,UAAU,EAAW;SACrB,eAAe,EAAW,OAAO;UAAE,IAAI,EAAM;UAAI,QAAQ;UAAM,CAAC;SAChE,WAAW,EACT,8LACD;mBAPH;UASG,EAAW,aAAa,kBAAC,GAAD,EAAS,WAAU,4BAA6B,CAAA;UAAC;UACjE,EAAU,EAAK;UACjB;WAVF,EAUE,CACT;QACE,CAAA;OAEP,EAAW,WACV,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAuB,EAAW,MAAM;QACvC,CAAA;OAEI;;KAEZ,kBAAC,GAAD;MAAW,WAAU;MAAQ,WAAU;gBAAvC,CACE,kBAAC,MAAD;OAAI,WAAU;iBAA+C;OAAY,CAAA,EACzE,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,MAAD;UAAI,WAAU;oBAAsB;UAAW,CAAA,EAC/C,kBAAC,MAAD;UAAI,WAAU;oBAAd,CAAkC,KAAE,EAAM,OAAY;YAClD;;QACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,MAAD;UAAI,WAAU;oBAAsB;UAAa,CAAA,EACjD,kBAAC,MAAD;UAAI,WAAU;oBAAd,CACE,kBAAC,UAAD;WACE,MAAK;WACL,MAAK;WACL,gBAAc,EAAM;WACpB,cAAY,EAAM,aAAa,yBAAyB;WACxD,UAAU,EAAY;WACtB,eACE,EAAY,OAAO;YACjB,IAAI,EAAM;YACV,OAAO,EAAE,YAAY,CAAC,EAAM,YAAY;YACzC,CAAC;WAEJ,WAAW,EACT,sMACA,EAAM,aAAa,yBAAyB,eAC7C;qBAED,kBAAC,QAAD,EACE,WAAW,EACT,+EACA,EAAM,aAAa,2BAA2B,kBAC/C,EACD,CAAA;WACK,CAAA,EACR,EAAY,aACX,kBAAC,GAAD,EAAS,WAAU,gDAAiD,CAAA,CAEnE;YACD;;QACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,MAAD;UAAI,WAAU;oBAAsB;UAAY,CAAA,EAChD,kBAAC,MAAD;UAAI,WAAU;oBACX,EAAW,EAAM,WAAW;WAAE,MAAM,KAAA;WAAW,QAAQ,KAAA;WAAW,CAAC;UACjE,CAAA,CACD;;QACN,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,MAAD;UAAI,WAAU;oBAAsB;UAAY,CAAA,EAChD,kBAAC,MAAD;UAAI,WAAU;oBACX,EAAW,EAAM,WAAW;WAAE,MAAM,KAAA;WAAW,QAAQ,KAAA;WAAW,CAAC;UACjE,CAAA,CACD;;QACH;SACK;;KAIZ,kBAAC,GAAD;MAAe,cAAa;MAAsB,YAAY,EAAM;MAAM,CAAA;KAG1E,kBAAC,GAAD;MAAU,YAAW;MAAW,UAAU,EAAM;MAAI,WAAU;MAAS,CAAA;KACnE;MACF;;EACK,CAAA"}
|
|
1
|
+
{"version":3,"file":"BrandDetailPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandDetailPage.tsx"],"sourcesContent":["/**\n * Brand Detail Page\n * @module coolandlovely/pages/brands\n *\n * Renders a single brand storefront (`cooBrand`) with its publish-lifecycle\n * transitions (`cooTransitionBrand`), collections and style items.\n */\n\nimport { useParams } from 'react-router';\nimport {\n Store,\n ExternalLink,\n Pencil,\n ArrowLeft,\n Shapes,\n Shirt,\n Loader2,\n Plus,\n Package,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { BrandCover } from '../../components/BrandCover';\nimport { TagList } from '../../components/TagList';\nimport { Comments } from '../../components/Comments';\nimport { StepTracker } from '../../components/StepTracker';\nimport { ActivityTrail } from '../../components/ActivityTrail';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useAssistantEntityName, useBrand, useTransitionBrand, useUpdateBrand } from '../../hooks';\nimport { categoryIcon } from '../../utils/icons';\nimport { formatPrice, formatDate, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooBrandStatus } from '../../types';\n\n/** Allowed next states per the svc brand state machine (UX guidance only;\n * the service is the authority and rejects illegal jumps). */\nconst NEXT_STATES: Record<CooBrandStatus, CooBrandStatus[]> = {\n DRAFT: ['PENDING_REVIEW', 'ARCHIVED'],\n PENDING_REVIEW: ['PUBLISHED', 'DRAFT', 'ARCHIVED'],\n PUBLISHED: ['SUSPENDED', 'ARCHIVED'],\n SUSPENDED: ['PUBLISHED', 'ARCHIVED'],\n ARCHIVED: [],\n};\n\n/** The publish path a storefront walks. SUSPENDED/ARCHIVED are exits from it,\n * not steps on it, so a brand sitting in one is tracked separately below. */\nconst LIFECYCLE_STEPS: CooBrandStatus[] = ['DRAFT', 'PENDING_REVIEW', 'PUBLISHED'];\n\n/**\n * Reframe the gateway's RBAC refusal in the operator's language. ONLY a\n * permission refusal is relabelled — every other failure keeps the service's\n * own message, so a validation error is never disguised as a role problem.\n */\nfunction transitionErrorMessage(error: unknown): string {\n const raw = error instanceof Error ? error.message : 'Transition failed';\n return /forbidden|permission|not allowed|unauthori[sz]ed/i.test(raw)\n ? 'Your role does not permit this action.'\n : raw;\n}\n\nexport function BrandDetailPage() {\n const { brandId } = useParams<{ brandId: string }>();\n const { navigateTo } = useCoolAndLovely();\n const query = useBrand(brandId);\n const transition = useTransitionBrand();\n const updateBrand = useUpdateBrand();\n // Tell the assistant WHICH storefront is on screen (see useAssistantEntity).\n useAssistantEntityName(brandId, query.data?.name);\n\n if (query.isLoading) {\n return (\n <PageLayout title=\"Brand\">\n <LoadingView label=\"Loading brand…\" />\n </PageLayout>\n );\n }\n\n if (query.isError || !query.data) {\n return (\n <PageLayout title=\"Brand\">\n <ErrorView\n title=\"Brand not found\"\n message=\"This storefront could not be loaded or does not exist.\"\n onRetry={() => void query.refetch()}\n />\n </PageLayout>\n );\n }\n\n const brand = query.data;\n const collections = brand.collections ?? [];\n const items = brand.items ?? [];\n const nextStates = NEXT_STATES[brand.status];\n const trackerSteps = LIFECYCLE_STEPS.includes(brand.status)\n ? LIFECYCLE_STEPS\n : ([...LIFECYCLE_STEPS.slice(0, 1), brand.status] as CooBrandStatus[]);\n // Only render a website link for safe http(s) URLs (block javascript:/data: sinks).\n const safeWebsiteUrl =\n brand.websiteUrl && /^https?:\\/\\//i.test(brand.websiteUrl) ? brand.websiteUrl : undefined;\n\n return (\n <PageLayout title={brand.name} description={`@${brand.handle}`}>\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Main column */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* Storefront hero — cover + logo + identity overlay, floating actions */}\n <BrandCover\n coverFileId={brand.cover?.id}\n logoFileId={brand.logo?.id}\n name={brand.name}\n seed={brand.id}\n className=\"overflow-hidden rounded-xl border border-border-seam shadow-sm\"\n overlay={\n <>\n <div className=\"absolute right-3 top-3 flex flex-wrap items-center justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-bg-surface/80 px-3 py-2 text-sm font-medium text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/brand-orders?brand=${brand.id}`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-bg-surface/80 px-3 py-2 text-sm font-medium text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface\"\n >\n <Package className=\"h-4 w-4\" /> Orders\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/brands/${brand.id}/edit`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text shadow-sm transition-colors hover:bg-action-primary-bg/90\"\n >\n <Pencil className=\"h-4 w-4\" /> Edit\n </button>\n </div>\n <div className=\"absolute inset-x-0 bottom-0 flex items-end gap-3 p-4 sm:gap-4 sm:p-5\">\n <Thumbnail\n icon={<Store className=\"h-7 w-7\" />}\n seed={brand.id}\n fileId={brand.logo?.id}\n alt={`${brand.name} logo`}\n className=\"h-16 w-16 shrink-0 rounded-full shadow-md ring-2 ring-bg-surface\"\n />\n <div className=\"min-w-0 pb-1\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-0.5\">\n <h2 className=\"truncate text-xl font-semibold text-text-primary sm:text-2xl\">\n {brand.name}\n </h2>\n <span className=\"text-sm text-text-secondary\">@{brand.handle}</span>\n </div>\n {brand.tagline && (\n <p className=\"mt-1 line-clamp-2 text-sm text-text-primary\">{brand.tagline}</p>\n )}\n </div>\n </div>\n </>\n }\n />\n\n {/* Storefront meta — aesthetics, status, bio, tags, website */}\n <section className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n {brand.aesthetics.length > 0 ? (\n <TagList tags={brand.aesthetics.map(titleCase)} tone=\"accent\" max={12} />\n ) : (\n <span />\n )}\n <StatusBadge status={brand.status} />\n </div>\n {brand.bio && <p className=\"text-sm text-text-secondary\">{brand.bio}</p>}\n {brand.tags.length > 0 && <TagList tags={brand.tags} max={12} />}\n {safeWebsiteUrl ? (\n <a\n href={safeWebsiteUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex max-w-full items-center gap-1.5 text-sm font-medium text-action-primary-bg hover:underline\"\n >\n <ExternalLink className=\"h-4 w-4 shrink-0\" />{' '}\n <span className=\"break-all\">{safeWebsiteUrl}</span>\n </a>\n ) : brand.websiteUrl ? (\n <p className=\"break-all text-sm text-text-secondary\">{brand.websiteUrl}</p>\n ) : null}\n </section>\n\n {/* Collections */}\n <section className=\"mt-6\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h3 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Shapes className=\"h-5 w-5\" /> Collections ({collections.length})\n </h3>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/collections/new?brandId=${brand.id}`)}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Plus className=\"h-4 w-4\" /> Add collection\n </button>\n </div>\n {collections.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No collections yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {collections.map((c) => (\n <button\n key={c.id}\n type=\"button\"\n onClick={() => navigateTo(`/collections/${c.id}`)}\n className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3 text-left transition-colors hover:bg-bg-sunken/50\"\n >\n <Thumbnail\n icon={<Shapes className=\"h-5 w-5\" />}\n seed={c.id}\n fileId={c.cover?.id}\n alt={c.name}\n className=\"h-11 w-11 shrink-0\"\n />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{c.name}</p>\n <p className=\"text-xs text-text-secondary\">{titleCase(c.season)}</p>\n </div>\n <StatusBadge status={c.status} />\n </button>\n ))}\n </div>\n )}\n </section>\n\n {/* Items */}\n <section className=\"mt-6\">\n <h3 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Shirt className=\"h-5 w-5\" /> Style items ({items.length})\n </h3>\n {items.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No items yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3\">\n {items.map((item) => (\n <button\n key={item.id}\n type=\"button\"\n onClick={() => navigateTo(`/items/${item.id}`)}\n className=\"overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left transition-all hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <Thumbnail\n icon={categoryIcon(item.category, 24)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"h-24 w-full\"\n />\n <div className=\"p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{item.name}</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatPrice(item.price, item.currency)}\n </p>\n </div>\n </button>\n ))}\n </div>\n )}\n </section>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-4\">\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Publish lifecycle</h3>\n <StepTracker steps={trackerSteps} current={brand.status} className=\"mb-5\" />\n <div className=\"mb-3 flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">Current:</span>\n <StatusBadge status={brand.status} />\n </div>\n {nextStates.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n This brand is archived (terminal state).\n </p>\n ) : (\n <div className=\"flex flex-col gap-2\">\n {nextStates.map((next) => (\n <button\n key={next}\n type=\"button\"\n disabled={transition.isPending}\n onClick={() => transition.mutate({ id: brand.id, status: next })}\n className={cn(\n 'inline-flex 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 >\n {transition.isPending && <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />}\n Move to {titleCase(next)}\n </button>\n ))}\n </div>\n )}\n {transition.isError && (\n <p className=\"mt-2 text-xs text-status-error-text\">\n {transitionErrorMessage(transition.error)}\n </p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Details</h3>\n <dl className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Handle</dt>\n <dd className=\"text-text-primary\">@{brand.handle}</dd>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <dt className=\"text-text-secondary\">Featured</dt>\n <dd className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={brand.isFeatured}\n aria-label={brand.isFeatured ? 'Unfeature this brand' : 'Feature this brand'}\n disabled={updateBrand.isPending}\n onClick={() =>\n updateBrand.mutate({\n id: brand.id,\n input: { isFeatured: !brand.isFeatured },\n })\n }\n className={cn(\n 'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-60',\n brand.isFeatured ? 'bg-action-primary-bg' : 'bg-bg-sunken'\n )}\n >\n <span\n className={cn(\n 'inline-block h-4 w-4 rounded-full bg-bg-surface shadow transition-transform',\n brand.isFeatured ? 'translate-x-[1.125rem]' : 'translate-x-0.5'\n )}\n />\n </button>\n {updateBrand.isPending && (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin text-text-secondary\" />\n )}\n </dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Created</dt>\n <dd className=\"text-text-primary\">\n {formatDate(brand.createdAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Updated</dt>\n <dd className=\"text-text-primary\">\n {formatDate(brand.updatedAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n </dl>\n </GlassCard>\n\n {/* Audit trail — who moved this storefront through its lifecycle, from\n the platform ActivityLog rows the svc's audit plugin writes. */}\n <ActivityTrail resourceType=\"coolandlovely-brand\" resourceId={brand.id} />\n\n {/* Reviews & feedback — moved into the sidebar to match the storefront layout */}\n <Comments entityType=\"CooBrand\" entityId={brand.id} className=\"mt-0\" />\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrandFormPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandFormPage.tsx"],"sourcesContent":["/**\n * Brand Create / Edit Page\n * @module coolandlovely/pages/brands\n *\n * Drives `cooCreateBrand` (new) and `cooUpdateBrand` (edit). The route param\n * `brandId` distinguishes the two modes.\n */\n\nimport { useEffect, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { Loader2, Save, Store } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { TextField, TextAreaField, ToggleField } from '../../components/Field';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrand, useCreateBrand, useUpdateBrand } from '../../hooks';\nimport { COO_FILE_FOLDERS } from '../../hooks/useFiles';\nimport { parseCsv } from '../../utils/formatters';\n\ninterface FormState {\n name: string;\n handle: string;\n tagline: string;\n bio: string;\n aesthetics: string;\n tags: string;\n websiteUrl: string;\n logoFileId: string;\n coverFileId: string;\n isFeatured: boolean;\n}\n\nconst EMPTY: FormState = {\n name: '',\n handle: '',\n tagline: '',\n bio: '',\n aesthetics: '',\n tags: '',\n websiteUrl: '',\n logoFileId: '',\n coverFileId: '',\n isFeatured: false,\n};\n\nexport function BrandFormPage() {\n const { brandId } = useParams<{ brandId: string }>();\n const isEdit = Boolean(brandId);\n const { navigateTo } = useCoolAndLovely();\n\n const existing = useBrand(isEdit ? brandId : undefined);\n const createBrand = useCreateBrand();\n const updateBrand = useUpdateBrand();\n\n const [form, setForm] = useState<FormState>(EMPTY);\n const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});\n\n useEffect(() => {\n if (isEdit && existing.data) {\n const b = existing.data;\n setForm({\n name: b.name,\n handle: b.handle,\n tagline: b.tagline ?? '',\n bio: b.bio ?? '',\n aesthetics: b.aesthetics.join(', '),\n tags: b.tags.join(', '),\n websiteUrl: b.websiteUrl ?? '',\n logoFileId: b.logo?.id ?? '',\n coverFileId: b.cover?.id ?? '',\n isFeatured: b.isFeatured,\n });\n }\n }, [isEdit, existing.data]);\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((f) => ({ ...f, [key]: value }));\n\n const mutation = isEdit ? updateBrand : createBrand;\n\n const validate = (): boolean => {\n const next: Partial<Record<keyof FormState, string>> = {};\n if (!form.name.trim()) next.name = 'Name is required';\n if (!isEdit && !form.handle.trim()) next.handle = 'Handle is required';\n setErrors(next);\n return Object.keys(next).length === 0;\n };\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (!validate()) return;\n\n if (isEdit && brandId) {\n updateBrand.mutate(\n {\n id: brandId,\n input: {\n name: form.name.trim(),\n tagline: form.tagline.trim() || undefined,\n bio: form.bio.trim() || undefined,\n aesthetics: parseCsv(form.aesthetics),\n tags: parseCsv(form.tags),\n websiteUrl: form.websiteUrl.trim() || undefined,\n logoFileId: form.logoFileId || undefined,\n coverFileId: form.coverFileId || undefined,\n isFeatured: form.isFeatured,\n },\n },\n { onSuccess: () => navigateTo(`/brands/${brandId}`) }\n );\n } else {\n createBrand.mutate(\n {\n name: form.name.trim(),\n handle: form.handle.trim(),\n tagline: form.tagline.trim() || undefined,\n bio: form.bio.trim() || undefined,\n aesthetics: parseCsv(form.aesthetics),\n tags: parseCsv(form.tags),\n websiteUrl: form.websiteUrl.trim() || undefined,\n logoFileId: form.logoFileId || undefined,\n coverFileId: form.coverFileId || undefined,\n },\n {\n onSuccess: (brand) => {\n // `CooCreateBrandInput` has no `isFeatured`; if the creator toggled\n // Featured on, persist it with a follow-up `cooUpdateBrand` before\n // navigating (navigate regardless of that call's outcome).\n if (form.isFeatured) {\n updateBrand.mutate(\n { id: brand.id, input: { isFeatured: true } },\n { onSettled: () => navigateTo(`/brands/${brand.id}`) }\n );\n } else {\n navigateTo(`/brands/${brand.id}`);\n }\n },\n }\n );\n }\n };\n\n if (isEdit && existing.isLoading) {\n return (\n <PageLayout title=\"Edit brand\">\n <LoadingView label=\"Loading brand…\" />\n </PageLayout>\n );\n }\n\n if (isEdit && (existing.isError || !existing.data)) {\n return (\n <PageLayout title=\"Edit brand\">\n <ErrorView title=\"Brand not found\" onRetry={() => void existing.refetch()} />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={isEdit ? 'Edit brand' : 'New brand'}\n description={isEdit ? 'Update this storefront' : 'Launch a new storefront on the platform'}\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-accent-purple-subtle text-text-primary\">\n <Store className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Storefront details</h2>\n <p className=\"text-xs text-text-secondary\">\n Name your storefront and tell shoppers what your brand stands for.\n </p>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"name\"\n label=\"Brand name\"\n value={form.name}\n onChange={(v) => setField('name', v)}\n placeholder=\"Aurora Atelier\"\n required\n error={errors.name}\n />\n <TextField\n id=\"handle\"\n label=\"Handle\"\n value={form.handle}\n onChange={(v) => setField('handle', v)}\n placeholder=\"aurora-atelier\"\n required={!isEdit}\n hint={isEdit ? 'Handle cannot be changed after creation' : 'Unique per workspace'}\n error={errors.handle}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"tagline\"\n label=\"Tagline\"\n value={form.tagline}\n onChange={(v) => setField('tagline', v)}\n placeholder=\"Quiet luxury, loud ideas\"\n />\n <TextAreaField\n id=\"bio\"\n label=\"Bio\"\n value={form.bio}\n onChange={(v) => setField('bio', v)}\n placeholder=\"Tell shoppers what your brand stands for…\"\n rows={4}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"aesthetics\"\n label=\"Aesthetics\"\n value={form.aesthetics}\n onChange={(v) => setField('aesthetics', v)}\n placeholder=\"minimal, Y2K, coastal\"\n hint=\"Comma-separated descriptors\"\n />\n <TextField\n id=\"tags\"\n label=\"Tags\"\n value={form.tags}\n onChange={(v) => setField('tags', v)}\n placeholder=\"sustainable, handmade\"\n hint=\"Comma-separated\"\n />\n </div>\n\n <TextField\n id=\"websiteUrl\"\n label=\"Website\"\n type=\"url\"\n value={form.websiteUrl}\n onChange={(v) => setField('websiteUrl', v)}\n placeholder=\"https://example.com\"\n className=\"mt-4\"\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <ImageUploadField\n label=\"Logo\"\n value={form.logoFileId}\n onChange={(id) => setField('logoFileId', id)}\n folder={COO_FILE_FOLDERS.brands}\n hint=\"Square storefront logo\"\n />\n <ImageUploadField\n label=\"Cover\"\n aspect=\"wide\"\n value={form.coverFileId}\n onChange={(id) => setField('coverFileId', id)}\n folder={COO_FILE_FOLDERS.brands}\n hint=\"Wide storefront banner\"\n />\n </div>\n\n <ToggleField\n id=\"isFeatured\"\n label=\"Feature this brand\"\n checked={form.isFeatured}\n onChange={(v) => setField('isFeatured', v)}\n hint=\"Featured brands surface in the Featured filter and carry a star badge.\"\n className=\"mt-4\"\n />\n\n {mutation.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {mutation.error instanceof Error ? mutation.error.message : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && brandId ? `/brands/${brandId}` : '/brands')}\n className=\"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 Cancel\n </button>\n <button\n type=\"submit\"\n disabled={mutation.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {mutation.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {isEdit ? 'Save changes' : 'Create brand'}\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkCA,IAAM,IAAmB;CACvB,MAAM;CACN,QAAQ;CACR,SAAS;CACT,KAAK;CACL,YAAY;CACZ,MAAM;CACN,YAAY;CACZ,YAAY;CACZ,aAAa;CACb,YAAY;CACb;AAED,SAAgB,IAAgB;CAC9B,IAAM,EAAE,eAAY,GAAgC,EAC9C,IAAS,EAAQ,GACjB,EAAE,kBAAe,GAAkB,EAEnC,IAAW,EAAS,IAAS,IAAU,KAAA,EAAU,EACjD,IAAc,GAAgB,EAC9B,IAAc,GAAgB,EAE9B,CAAC,GAAM,KAAW,EAAoB,EAAM,EAC5C,CAAC,GAAQ,KAAa,EAAmD,EAAE,CAAC;AAElF,SAAgB;AACd,MAAI,KAAU,EAAS,MAAM;GAC3B,IAAM,IAAI,EAAS;AACnB,KAAQ;IACN,MAAM,EAAE;IACR,QAAQ,EAAE;IACV,SAAS,EAAE,WAAW;IACtB,KAAK,EAAE,OAAO;IACd,YAAY,EAAE,WAAW,KAAK,KAAK;IACnC,MAAM,EAAE,KAAK,KAAK,KAAK;IACvB,YAAY,EAAE,cAAc;IAC5B,YAAY,EAAE,MAAM,MAAM;IAC1B,aAAa,EAAE,OAAO,MAAM;IAC5B,YAAY,EAAE;IACf,CAAC;;IAEH,CAAC,GAAQ,EAAS,KAAK,CAAC;CAE3B,IAAM,KAAuC,GAAQ,MACnD,GAAS,OAAO;EAAE,GAAG;GAAI,IAAM;EAAO,EAAE,EAEpC,IAAW,IAAS,IAAc,GAElC,UAA0B;EAC9B,IAAM,IAAiD,EAAE;AAIzD,SAHK,EAAK,KAAK,MAAM,KAAE,EAAK,OAAO,qBAC/B,CAAC,KAAU,CAAC,EAAK,OAAO,MAAM,KAAE,EAAK,SAAS,uBAClD,EAAU,EAAK,EACR,OAAO,KAAK,EAAK,CAAC,WAAW;;AAyEtC,QAhBI,KAAU,EAAS,YAEnB,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD,EAAa,OAAM,kBAAmB,CAAA;EAC3B,CAAA,GAIb,MAAW,EAAS,WAAW,CAAC,EAAS,QAEzC,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD;GAAW,OAAM;GAAkB,eAAe,KAAK,EAAS,SAAS;GAAI,CAAA;EAClE,CAAA,GAKf,kBAAC,GAAD;EACE,OAAO,IAAS,eAAe;EAC/B,aAAa,IAAS,2BAA2B;YAEjD,kBAAC,QAAD;GAAM,WA3EY,MAAuB;AAC3C,MAAE,gBAAgB,EACb,GAAU,KAEX,KAAU,IACZ,EAAY,OACV;KACE,IAAI;KACJ,OAAO;MACL,MAAM,EAAK,KAAK,MAAM;MACtB,SAAS,EAAK,QAAQ,MAAM,IAAI,KAAA;MAChC,KAAK,EAAK,IAAI,MAAM,IAAI,KAAA;MACxB,YAAY,EAAS,EAAK,WAAW;MACrC,MAAM,EAAS,EAAK,KAAK;MACzB,YAAY,EAAK,WAAW,MAAM,IAAI,KAAA;MACtC,YAAY,EAAK,cAAc,KAAA;MAC/B,aAAa,EAAK,eAAe,KAAA;MACjC,YAAY,EAAK;MAClB;KACF,EACD,EAAE,iBAAiB,EAAW,WAAW,IAAU,EAAE,CACtD,GAED,EAAY,OACV;KACE,MAAM,EAAK,KAAK,MAAM;KACtB,QAAQ,EAAK,OAAO,MAAM;KAC1B,SAAS,EAAK,QAAQ,MAAM,IAAI,KAAA;KAChC,KAAK,EAAK,IAAI,MAAM,IAAI,KAAA;KACxB,YAAY,EAAS,EAAK,WAAW;KACrC,MAAM,EAAS,EAAK,KAAK;KACzB,YAAY,EAAK,WAAW,MAAM,IAAI,KAAA;KACtC,YAAY,EAAK,cAAc,KAAA;KAC/B,aAAa,EAAK,eAAe,KAAA;KAClC,EACD,EACE,YAAY,MAAU;AAIpB,KAAI,EAAK,aACP,EAAY,OACV;MAAE,IAAI,EAAM;MAAI,OAAO,EAAE,YAAY,IAAM;MAAE,EAC7C,EAAE,iBAAiB,EAAW,WAAW,EAAM,KAAK,EAAE,CACvD,GAED,EAAW,WAAW,EAAM,KAAK;OAGtC,CACF;;GAyB6B,WAAU;aACtC,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cAAvC;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;OACzB,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;OAAI,WAAU;iBAA4C;OAAuB,CAAA,EACjF,kBAAC,KAAD;OAAG,WAAU;iBAA8B;OAEvC,CAAA,CACA,EAAA,CAAA,CACF;;KAEN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,IAAG;OACH,OAAM;OACN,OAAO,EAAK;OACZ,WAAW,MAAM,EAAS,QAAQ,EAAE;OACpC,aAAY;OACZ,UAAA;OACA,OAAO,EAAO;OACd,CAAA,EACF,kBAAC,GAAD;OACE,IAAG;OACH,OAAM;OACN,OAAO,EAAK;OACZ,WAAW,MAAM,EAAS,UAAU,EAAE;OACtC,aAAY;OACZ,UAAU,CAAC;OACX,MAAM,IAAS,4CAA4C;OAC3D,OAAO,EAAO;OACd,CAAA,CACE;;KAEN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,IAAG;OACH,OAAM;OACN,OAAO,EAAK;OACZ,WAAW,MAAM,EAAS,WAAW,EAAE;OACvC,aAAY;OACZ,CAAA,EACF,kBAAC,GAAD;OACE,IAAG;OACH,OAAM;OACN,OAAO,EAAK;OACZ,WAAW,MAAM,EAAS,OAAO,EAAE;OACnC,aAAY;OACZ,MAAM;OACN,CAAA,CACE;;KAEN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,IAAG;OACH,OAAM;OACN,OAAO,EAAK;OACZ,WAAW,MAAM,EAAS,cAAc,EAAE;OAC1C,aAAY;OACZ,MAAK;OACL,CAAA,EACF,kBAAC,GAAD;OACE,IAAG;OACH,OAAM;OACN,OAAO,EAAK;OACZ,WAAW,MAAM,EAAS,QAAQ,EAAE;OACpC,aAAY;OACZ,MAAK;OACL,CAAA,CACE;;KAEN,kBAAC,GAAD;MACE,IAAG;MACH,OAAM;MACN,MAAK;MACL,OAAO,EAAK;MACZ,WAAW,MAAM,EAAS,cAAc,EAAE;MAC1C,aAAY;MACZ,WAAU;MACV,CAAA;KAEF,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,OAAM;OACN,OAAO,EAAK;OACZ,WAAW,MAAO,EAAS,cAAc,EAAG;OAC5C,QAAQ,EAAiB;OACzB,MAAK;OACL,CAAA,EACF,kBAAC,GAAD;OACE,OAAM;OACN,QAAO;OACP,OAAO,EAAK;OACZ,WAAW,MAAO,EAAS,eAAe,EAAG;OAC7C,QAAQ,EAAiB;OACzB,MAAK;OACL,CAAA,CACE;;KAEN,kBAAC,GAAD;MACE,IAAG;MACH,OAAM;MACN,SAAS,EAAK;MACd,WAAW,MAAM,EAAS,cAAc,EAAE;MAC1C,MAAK;MACL,WAAU;MACV,CAAA;KAED,EAAS,WACR,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAS,iBAAiB,QAAQ,EAAS,MAAM,UAAU;MAC1D,CAAA;KAGN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAW,KAAU,IAAU,WAAW,MAAY,UAAU;OAC/E,WAAU;iBACX;OAEQ,CAAA,EACT,kBAAC,UAAD;OACE,MAAK;OACL,UAAU,EAAS;OACnB,WAAU;iBAHZ,CAKG,EAAS,YACR,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAE7B,IAAS,iBAAiB,eACpB;SACL;;KACI;;GACP,CAAA;EACI,CAAA"}
|
|
1
|
+
{"version":3,"file":"BrandFormPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandFormPage.tsx"],"sourcesContent":["/**\n * Brand Create / Edit Page\n * @module coolandlovely/pages/brands\n *\n * Drives `cooCreateBrand` (new) and `cooUpdateBrand` (edit). The route param\n * `brandId` distinguishes the two modes.\n */\n\nimport { useEffect, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { Loader2, Save, Store } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { TextField, TextAreaField, ToggleField } from '../../components/Field';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrand, useCreateBrand, useUpdateBrand } from '../../hooks';\nimport { COO_FILE_FOLDERS } from '../../hooks/useFiles';\nimport { parseCsv } from '../../utils/formatters';\n\ninterface FormState {\n name: string;\n handle: string;\n tagline: string;\n bio: string;\n aesthetics: string;\n tags: string;\n websiteUrl: string;\n logoFileId: string;\n coverFileId: string;\n isFeatured: boolean;\n}\n\nconst EMPTY: FormState = {\n name: '',\n handle: '',\n tagline: '',\n bio: '',\n aesthetics: '',\n tags: '',\n websiteUrl: '',\n logoFileId: '',\n coverFileId: '',\n isFeatured: false,\n};\n\nexport function BrandFormPage() {\n const { brandId } = useParams<{ brandId: string }>();\n const isEdit = Boolean(brandId);\n const { navigateTo } = useCoolAndLovely();\n\n const existing = useBrand(isEdit ? brandId : undefined);\n const createBrand = useCreateBrand();\n const updateBrand = useUpdateBrand();\n\n const [form, setForm] = useState<FormState>(EMPTY);\n const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});\n\n useEffect(() => {\n if (isEdit && existing.data) {\n const b = existing.data;\n setForm({\n name: b.name,\n handle: b.handle,\n tagline: b.tagline ?? '',\n bio: b.bio ?? '',\n aesthetics: b.aesthetics.join(', '),\n tags: b.tags.join(', '),\n websiteUrl: b.websiteUrl ?? '',\n logoFileId: b.logo?.id ?? '',\n coverFileId: b.cover?.id ?? '',\n isFeatured: b.isFeatured,\n });\n }\n }, [isEdit, existing.data]);\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((f) => ({ ...f, [key]: value }));\n\n const mutation = isEdit ? updateBrand : createBrand;\n\n const validate = (): boolean => {\n const next: Partial<Record<keyof FormState, string>> = {};\n if (!form.name.trim()) next.name = 'Name is required';\n if (!isEdit && !form.handle.trim()) next.handle = 'Handle is required';\n setErrors(next);\n return Object.keys(next).length === 0;\n };\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (!validate()) return;\n\n if (isEdit && brandId) {\n updateBrand.mutate(\n {\n id: brandId,\n input: {\n name: form.name.trim(),\n tagline: form.tagline.trim() || undefined,\n bio: form.bio.trim() || undefined,\n aesthetics: parseCsv(form.aesthetics),\n tags: parseCsv(form.tags),\n websiteUrl: form.websiteUrl.trim() || undefined,\n logoFileId: form.logoFileId || undefined,\n coverFileId: form.coverFileId || undefined,\n isFeatured: form.isFeatured,\n },\n },\n { onSuccess: () => navigateTo(`/brands/${brandId}`) }\n );\n } else {\n createBrand.mutate(\n {\n name: form.name.trim(),\n handle: form.handle.trim(),\n tagline: form.tagline.trim() || undefined,\n bio: form.bio.trim() || undefined,\n aesthetics: parseCsv(form.aesthetics),\n tags: parseCsv(form.tags),\n websiteUrl: form.websiteUrl.trim() || undefined,\n logoFileId: form.logoFileId || undefined,\n coverFileId: form.coverFileId || undefined,\n },\n {\n onSuccess: (brand) => {\n // `CooCreateBrandInput` has no `isFeatured`; if the creator toggled\n // Featured on, persist it with a follow-up `cooUpdateBrand` before\n // navigating (navigate regardless of that call's outcome).\n if (form.isFeatured) {\n updateBrand.mutate(\n { id: brand.id, input: { isFeatured: true } },\n { onSettled: () => navigateTo(`/brands/${brand.id}`) }\n );\n } else {\n navigateTo(`/brands/${brand.id}`);\n }\n },\n }\n );\n }\n };\n\n if (isEdit && existing.isLoading) {\n return (\n <PageLayout title=\"Edit brand\">\n <LoadingView label=\"Loading brand…\" />\n </PageLayout>\n );\n }\n\n if (isEdit && (existing.isError || !existing.data)) {\n return (\n <PageLayout title=\"Edit brand\">\n <ErrorView title=\"Brand not found\" onRetry={() => void existing.refetch()} />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={isEdit ? 'Edit brand' : 'New brand'}\n description={isEdit ? 'Update this storefront' : 'Launch a new storefront on the platform'}\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-accent-purple-subtle text-text-primary\">\n <Store className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Storefront details</h2>\n <p className=\"text-xs text-text-secondary\">\n Name your storefront and tell shoppers what your brand stands for.\n </p>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"name\"\n label=\"Brand name\"\n value={form.name}\n onChange={(v) => setField('name', v)}\n placeholder=\"Aurora Atelier\"\n required\n error={errors.name}\n />\n <TextField\n id=\"handle\"\n label=\"Handle\"\n value={form.handle}\n onChange={(v) => setField('handle', v)}\n placeholder=\"aurora-atelier\"\n required={!isEdit}\n hint={isEdit ? 'Handle cannot be changed after creation' : 'Unique per workspace'}\n error={errors.handle}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"tagline\"\n label=\"Tagline\"\n value={form.tagline}\n onChange={(v) => setField('tagline', v)}\n placeholder=\"Quiet luxury, loud ideas\"\n />\n <TextAreaField\n id=\"bio\"\n label=\"Bio\"\n value={form.bio}\n onChange={(v) => setField('bio', v)}\n placeholder=\"Tell shoppers what your brand stands for…\"\n rows={4}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"aesthetics\"\n label=\"Aesthetics\"\n value={form.aesthetics}\n onChange={(v) => setField('aesthetics', v)}\n placeholder=\"minimal, Y2K, coastal\"\n hint=\"Comma-separated descriptors\"\n />\n <TextField\n id=\"tags\"\n label=\"Tags\"\n value={form.tags}\n onChange={(v) => setField('tags', v)}\n placeholder=\"sustainable, handmade\"\n hint=\"Comma-separated\"\n />\n </div>\n\n <TextField\n id=\"websiteUrl\"\n label=\"Website\"\n type=\"url\"\n value={form.websiteUrl}\n onChange={(v) => setField('websiteUrl', v)}\n placeholder=\"https://example.com\"\n className=\"mt-4\"\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <ImageUploadField\n label=\"Logo\"\n value={form.logoFileId}\n onChange={(id) => setField('logoFileId', id)}\n folder={COO_FILE_FOLDERS.brands}\n hint=\"Square storefront logo\"\n />\n <ImageUploadField\n label=\"Cover\"\n aspect=\"wide\"\n value={form.coverFileId}\n onChange={(id) => setField('coverFileId', id)}\n folder={COO_FILE_FOLDERS.brands}\n hint=\"Wide storefront banner\"\n />\n </div>\n\n <ToggleField\n id=\"isFeatured\"\n label=\"Feature this brand\"\n checked={form.isFeatured}\n onChange={(v) => setField('isFeatured', v)}\n hint=\"Featured brands surface in the Featured filter and carry a star badge.\"\n className=\"mt-4\"\n />\n\n {mutation.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {mutation.error instanceof Error ? mutation.error.message : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && brandId ? `/brands/${brandId}` : '/brands')}\n className=\"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 Cancel\n </button>\n <button\n type=\"submit\"\n disabled={mutation.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {mutation.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {isEdit ? 'Save changes' : 'Create brand'}\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrandsListPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandsListPage.tsx"],"sourcesContent":["/**\n * Brands List Page\n * @module coolandlovely/pages/brands\n *\n * Lists brand storefronts (cursor-paginated `cooListBrands`) with status &\n * featured filters. Each card links to the brand detail page.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Store, Plus, ChevronLeft, ChevronRight, Star, Download } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrands, useTr } from '../../hooks';\nimport { BrandCard } from '../../components/BrandCard';\nimport { cn } from '../../utils/cn';\nimport type { CooBrandStatus } from '../../types';\n\nconst PAGE_SIZE = 24;\n\nconst STATUS_ORDER: CooBrandStatus[] = [\n 'PUBLISHED',\n 'DRAFT',\n 'PENDING_REVIEW',\n 'SUSPENDED',\n 'ARCHIVED',\n];\n\nexport function BrandsListPage() {\n const { navigateTo } = useCoolAndLovely();\n const tr = useTr();\n const [status, setStatus] = useState<CooBrandStatus | 'ALL'>('ALL');\n const [featuredOnly, setFeaturedOnly] = useState(false);\n const [offset, setOffset] = useState(0);\n\n const statusOptions = useMemo(\n () => [\n { value: 'ALL' as const, label: tr('coolandlovely.filters.all', 'All') },\n ...STATUS_ORDER.map((value) => ({\n value,\n label: tr(\n `coolandlovely.status.${value.toLowerCase()}`,\n value.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase())\n ),\n })),\n ],\n [tr]\n );\n\n const query = useBrands({\n status: status === 'ALL' ? undefined : status,\n featured: featuredOnly ? true : undefined,\n pagination: { limit: PAGE_SIZE, offset },\n });\n\n const brands = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title={tr('coolandlovely.page.brandsTitle', 'Brands')}\n description={tr(\n 'coolandlovely.page.brandsSubtitle',\n 'Storefronts on the platform with a real publish lifecycle'\n )}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands/import')}\n className=\"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 <Download className=\"h-4 w-4\" />{' '}\n {tr('coolandlovely.actions.importCatalog', 'Import catalog')}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('coolandlovely.actions.newBrand', 'New brand')}\n </button>\n </div>\n }\n >\n {/* Filters */}\n <div className=\"mb-6 flex flex-wrap items-center gap-2\">\n {statusOptions.map((opt) => (\n <button\n key={opt.value}\n type=\"button\"\n onClick={() => {\n setStatus(opt.value);\n setOffset(0);\n }}\n className={cn(\n 'rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n status === opt.value\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {opt.label}\n </button>\n ))}\n <button\n type=\"button\"\n onClick={() => {\n setFeaturedOnly((v) => !v);\n setOffset(0);\n }}\n className={cn(\n 'ml-auto inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n featuredOnly\n ? 'bg-status-warning-bg-subtle text-status-warning-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n <Star className={cn('h-3.5 w-3.5', featuredOnly && 'fill-status-warning-text')} />{' '}\n {tr('coolandlovely.filters.featured', 'Featured')}\n </button>\n </div>\n\n {query.isLoading ? (\n <LoadingView label={tr('coolandlovely.loading.brands', 'Loading brands…')} />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : brands.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Store className=\"h-10 w-10\" />}\n title={tr('coolandlovely.empty.brandsTitle', 'No brands yet')}\n description={tr(\n 'coolandlovely.empty.brandsDescription',\n 'Launch the first storefront to start building the catalogue.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('coolandlovely.actions.newBrand', 'New brand')}\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {brands.map((brand) => (\n <BrandCard\n key={brand.id}\n brand={brand}\n onClick={() => navigateTo(`/brands/${brand.id}`)}\n />\n ))}\n </div>\n\n {/* Pagination */}\n {(offset > 0 || hasMore) && (\n <div className=\"mt-6 flex items-center justify-center gap-2\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n <ChevronLeft className=\"h-4 w-4\" />{' '}\n {tr('coolandlovely.pagination.previous', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.pagination.page', 'Page {{page}}', {\n page: offset / PAGE_SIZE + 1,\n })}\n </span>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset((o) => o + PAGE_SIZE)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n {tr('coolandlovely.pagination.next', 'Next')} <ChevronRight className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BrandsListPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandsListPage.tsx"],"sourcesContent":["/**\n * Brands List Page\n * @module coolandlovely/pages/brands\n *\n * Lists brand storefronts (cursor-paginated `cooListBrands`) with status &\n * featured filters. Each card links to the brand detail page.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Store, Plus, ChevronLeft, ChevronRight, Star, Download } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrands, useTr } from '../../hooks';\nimport { BrandCard } from '../../components/BrandCard';\nimport { cn } from '../../utils/cn';\nimport type { CooBrandStatus } from '../../types';\n\nconst PAGE_SIZE = 24;\n\nconst STATUS_ORDER: CooBrandStatus[] = [\n 'PUBLISHED',\n 'DRAFT',\n 'PENDING_REVIEW',\n 'SUSPENDED',\n 'ARCHIVED',\n];\n\nexport function BrandsListPage() {\n const { navigateTo } = useCoolAndLovely();\n const tr = useTr();\n const [status, setStatus] = useState<CooBrandStatus | 'ALL'>('ALL');\n const [featuredOnly, setFeaturedOnly] = useState(false);\n const [offset, setOffset] = useState(0);\n\n const statusOptions = useMemo(\n () => [\n { value: 'ALL' as const, label: tr('coolandlovely.filters.all', 'All') },\n ...STATUS_ORDER.map((value) => ({\n value,\n label: tr(\n `coolandlovely.status.${value.toLowerCase()}`,\n value.replace(/_/g, ' ').replace(/\\b\\w/g, (c) => c.toUpperCase())\n ),\n })),\n ],\n [tr]\n );\n\n const query = useBrands({\n status: status === 'ALL' ? undefined : status,\n featured: featuredOnly ? true : undefined,\n pagination: { limit: PAGE_SIZE, offset },\n });\n\n const brands = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title={tr('coolandlovely.page.brandsTitle', 'Brands')}\n description={tr(\n 'coolandlovely.page.brandsSubtitle',\n 'Storefronts on the platform with a real publish lifecycle'\n )}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands/import')}\n className=\"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 <Download className=\"h-4 w-4\" />{' '}\n {tr('coolandlovely.actions.importCatalog', 'Import catalog')}\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('coolandlovely.actions.newBrand', 'New brand')}\n </button>\n </div>\n }\n >\n {/* Filters */}\n <div className=\"mb-6 flex flex-wrap items-center gap-2\">\n {statusOptions.map((opt) => (\n <button\n key={opt.value}\n type=\"button\"\n onClick={() => {\n setStatus(opt.value);\n setOffset(0);\n }}\n className={cn(\n 'rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n status === opt.value\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {opt.label}\n </button>\n ))}\n <button\n type=\"button\"\n onClick={() => {\n setFeaturedOnly((v) => !v);\n setOffset(0);\n }}\n className={cn(\n 'ml-auto inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n featuredOnly\n ? 'bg-status-warning-bg-subtle text-status-warning-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n <Star className={cn('h-3.5 w-3.5', featuredOnly && 'fill-status-warning-text')} />{' '}\n {tr('coolandlovely.filters.featured', 'Featured')}\n </button>\n </div>\n\n {query.isLoading ? (\n <LoadingView label={tr('coolandlovely.loading.brands', 'Loading brands…')} />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : brands.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Store className=\"h-10 w-10\" />}\n title={tr('coolandlovely.empty.brandsTitle', 'No brands yet')}\n description={tr(\n 'coolandlovely.empty.brandsDescription',\n 'Launch the first storefront to start building the catalogue.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands/new')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('coolandlovely.actions.newBrand', 'New brand')}\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {brands.map((brand) => (\n <BrandCard\n key={brand.id}\n brand={brand}\n onClick={() => navigateTo(`/brands/${brand.id}`)}\n />\n ))}\n </div>\n\n {/* Pagination */}\n {(offset > 0 || hasMore) && (\n <div className=\"mt-6 flex items-center justify-center gap-2\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n <ChevronLeft className=\"h-4 w-4\" />{' '}\n {tr('coolandlovely.pagination.previous', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.pagination.page', 'Page {{page}}', {\n page: offset / PAGE_SIZE + 1,\n })}\n </span>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset((o) => o + PAGE_SIZE)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n {tr('coolandlovely.pagination.next', 'Next')} <ChevronRight className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImportCatalogPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/ImportCatalogPage.tsx"],"sourcesContent":["/**\n * Import Catalog Page (BOFF-3384)\n * @module coolandlovely/pages/brands\n *\n * Lets a brand bring its existing catalog over from Shopify, WooCommerce or\n * BigCommerce in three steps:\n *\n * 1. Connect — pick the platform and paste read-only API credentials. They\n * are sent once with `cooPreviewExternalCatalog` and never stored.\n * 2. Review — the catalog comes back mapped onto a brand, collections and\n * style items. The merchant confirms the brand name/handle and unticks\n * anything they do not want.\n * 3. Import — product images are copied into wspace-files and everything is\n * created in one transaction with `cooBulkImportCatalog`.\n */\n\nimport { type FormEvent, useMemo, useState } from 'react';\nimport {\n ArrowLeft,\n CheckCircle2,\n Download,\n ImageOff,\n Loader2,\n PackageOpen,\n Sparkles,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { SelectField, TextField, ToggleField } from '../../components/Field';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useTr } from '../../hooks';\nimport {\n type CatalogImportProgress,\n type ImportExternalCatalogResult,\n useImportExternalCatalog,\n usePreviewExternalCatalog,\n} from '../../hooks/useCatalogImport';\nimport type { CooCatalogProvider, CooExternalCatalogPreview } from '../../types';\nimport { cn } from '../../utils/cn';\nimport { formatPrice, titleCase } from '../../utils/formatters';\n\nconst SHOPIFY_SANDBOX = 'mock.shop';\nconst DEFAULT_MAX_ITEMS = '100';\nconst HANDLE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\ninterface ProviderCopy {\n label: string;\n storeLabel: string;\n storePlaceholder: string;\n storeHint: string;\n tokenLabel: string;\n tokenHint: string;\n secretLabel?: string;\n}\n\nfunction useProviderCopy(): Record<CooCatalogProvider, ProviderCopy> {\n const tr = useTr();\n return useMemo(\n () => ({\n SHOPIFY: {\n label: 'Shopify',\n storeLabel: tr('coolandlovely.import.shopify.store', 'Shop domain'),\n storePlaceholder: 'your-brand.myshopify.com',\n storeHint: tr(\n 'coolandlovely.import.shopify.storeHint',\n 'Your permanent myshopify.com domain. Use mock.shop to try the Shopify sample store.'\n ),\n tokenLabel: tr('coolandlovely.import.shopify.token', 'Storefront API access token'),\n tokenHint: tr(\n 'coolandlovely.import.shopify.tokenHint',\n 'Shopify admin → Settings → Apps → Develop apps → Storefront API. Not needed for mock.shop.'\n ),\n },\n WOOCOMMERCE: {\n label: 'WooCommerce',\n storeLabel: tr('coolandlovely.import.woo.store', 'Store address'),\n storePlaceholder: 'https://shop.your-brand.com',\n storeHint: tr(\n 'coolandlovely.import.woo.storeHint',\n 'The HTTPS address of your WordPress store.'\n ),\n tokenLabel: tr('coolandlovely.import.woo.token', 'Consumer key'),\n tokenHint: tr(\n 'coolandlovely.import.woo.tokenHint',\n 'WooCommerce → Settings → Advanced → REST API → Add key, with Read permission.'\n ),\n secretLabel: tr('coolandlovely.import.woo.secret', 'Consumer secret'),\n },\n BIGCOMMERCE: {\n label: 'BigCommerce',\n storeLabel: tr('coolandlovely.import.bc.store', 'Store hash'),\n storePlaceholder: 'abc123def',\n storeHint: tr(\n 'coolandlovely.import.bc.storeHint',\n 'The code after /stores/ in your API path (or paste the API path itself).'\n ),\n tokenLabel: tr('coolandlovely.import.bc.token', 'API account access token'),\n tokenHint: tr(\n 'coolandlovely.import.bc.tokenHint',\n 'Settings → Store-level API accounts → Create, with read-only Products and Information scopes.'\n ),\n },\n }),\n [tr]\n );\n}\n\ninterface SourceForm {\n provider: CooCatalogProvider;\n storeIdentifier: string;\n accessToken: string;\n accessSecret: string;\n maxItems: string;\n}\n\nconst EMPTY_SOURCE: SourceForm = {\n provider: 'SHOPIFY',\n storeIdentifier: '',\n accessToken: '',\n accessSecret: '',\n maxItems: DEFAULT_MAX_ITEMS,\n};\n\nexport function ImportCatalogPage() {\n const tr = useTr();\n const { navigateTo } = useCoolAndLovely();\n const copy = useProviderCopy();\n const previewMutation = usePreviewExternalCatalog();\n const importMutation = useImportExternalCatalog();\n\n const [source, setSource] = useState<SourceForm>(EMPTY_SOURCE);\n const [preview, setPreview] = useState<CooExternalCatalogPreview | null>(null);\n const [brandName, setBrandName] = useState('');\n const [brandHandle, setBrandHandle] = useState('');\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [copyImages, setCopyImages] = useState(true);\n const [progress, setProgress] = useState<CatalogImportProgress | null>(null);\n const [result, setResult] = useState<ImportExternalCatalogResult | null>(null);\n\n const providerCopy = copy[source.provider];\n const isSandbox =\n source.provider === 'SHOPIFY' &&\n source.storeIdentifier.trim().toLowerCase() === SHOPIFY_SANDBOX;\n const handleError =\n brandHandle && !HANDLE_PATTERN.test(brandHandle)\n ? tr(\n 'coolandlovely.import.handleInvalid',\n 'Use lowercase letters, numbers and single hyphens'\n )\n : undefined;\n\n const setSourceField = <K extends keyof SourceForm>(key: K, value: SourceForm[K]) =>\n setSource((current) => ({ ...current, [key]: value }));\n\n const fetchPreview = (form: SourceForm) => {\n const maxItems = Number.parseInt(form.maxItems, 10);\n previewMutation.mutate(\n {\n provider: form.provider,\n storeIdentifier: form.storeIdentifier.trim(),\n ...(form.accessToken.trim() ? { accessToken: form.accessToken.trim() } : {}),\n ...(form.accessSecret.trim() ? { accessSecret: form.accessSecret.trim() } : {}),\n ...(Number.isFinite(maxItems) ? { maxItems } : {}),\n },\n {\n onSuccess: (data) => {\n setPreview(data);\n setBrandName(data.brand.name);\n setBrandHandle(data.brand.handle);\n setSelected(new Set(data.items.map((item) => item.externalId)));\n // Credentials are only needed for the preview: drop them from memory.\n setSource((current) => ({ ...current, accessToken: '', accessSecret: '' }));\n },\n }\n );\n };\n\n const handleConnect = (event: FormEvent) => {\n event.preventDefault();\n fetchPreview(source);\n };\n\n const trySandbox = () => {\n const sandbox: SourceForm = {\n ...EMPTY_SOURCE,\n storeIdentifier: SHOPIFY_SANDBOX,\n maxItems: '20',\n };\n setSource(sandbox);\n fetchPreview(sandbox);\n };\n\n const toggleItem = (id: string) =>\n setSelected((current) => {\n const next = new Set(current);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n\n const startImport = () => {\n if (!preview || selected.size === 0 || !brandName.trim() || !brandHandle || handleError) return;\n importMutation.mutate(\n {\n preview,\n brandName: brandName.trim(),\n brandHandle,\n selectedIds: selected,\n copyImages,\n onProgress: setProgress,\n },\n { onSuccess: setResult, onSettled: () => setProgress(null) }\n );\n };\n\n const reset = () => {\n setPreview(null);\n setResult(null);\n setSelected(new Set());\n previewMutation.reset();\n importMutation.reset();\n };\n\n return (\n <PageLayout\n title={tr('coolandlovely.import.title', 'Import catalog')}\n description={tr(\n 'coolandlovely.import.subtitle',\n 'Bring your existing storefront over from Shopify, WooCommerce or BigCommerce'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands')}\n className=\"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 <ArrowLeft className=\"h-4 w-4\" /> {tr('coolandlovely.actions.backToBrands', 'Brands')}\n </button>\n }\n >\n {result ? (\n <div className=\"mx-auto max-w-2xl\" data-testid=\"catalog-import-result\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"flex items-start gap-3\">\n <CheckCircle2 className=\"mt-0.5 h-6 w-6 text-status-success-text\" />\n <div className=\"flex-1\">\n <h2 className=\"text-base font-semibold text-text-primary\">\n {tr('coolandlovely.import.doneTitle', 'Imported {{name}}', {\n name: result.brand.name,\n })}\n </h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.import.doneSummary',\n '{{items}} style items in {{collections}} collections. The storefront starts as a draft.',\n { items: result.itemsCreated, collections: result.collectionsCreated }\n )}\n </p>\n {copyImages && (\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.import.doneImages',\n '{{copied}} images copied to Files, {{failed}} could not be copied.',\n {\n copied: result.imagesCopied,\n failed: result.imagesFailed,\n }\n )}\n </p>\n )}\n <div className=\"mt-4 flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo(`/brands/${result.brand.id}`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n {tr('coolandlovely.import.openBrand', 'Open storefront')}\n </button>\n <button\n type=\"button\"\n onClick={reset}\n className=\"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 {tr('coolandlovely.import.another', 'Import another catalog')}\n </button>\n </div>\n </div>\n </div>\n </GlassCard>\n </div>\n ) : !preview ? (\n <form\n onSubmit={handleConnect}\n className=\"mx-auto max-w-2xl\"\n data-testid=\"catalog-import-connect\"\n >\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-accent-purple-subtle text-text-primary\">\n <PackageOpen className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">\n {tr('coolandlovely.import.connectTitle', 'Connect your store')}\n </h2>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'coolandlovely.import.connectHint',\n 'Credentials are used once to read your catalog and are never stored.'\n )}\n </p>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <SelectField\n id=\"provider\"\n label={tr('coolandlovely.import.platform', 'Platform')}\n value={source.provider}\n onChange={(value) => setSourceField('provider', value as CooCatalogProvider)}\n options={(Object.keys(copy) as CooCatalogProvider[]).map((value) => ({\n value,\n label: copy[value].label,\n }))}\n />\n <TextField\n id=\"maxItems\"\n label={tr('coolandlovely.import.maxItems', 'Most products to fetch')}\n type=\"number\"\n value={source.maxItems}\n onChange={(value) => setSourceField('maxItems', value)}\n hint={tr('coolandlovely.import.maxItemsHint', 'Up to 500 per import')}\n />\n </div>\n\n <TextField\n id=\"storeIdentifier\"\n label={providerCopy.storeLabel}\n value={source.storeIdentifier}\n onChange={(value) => setSourceField('storeIdentifier', value)}\n placeholder={providerCopy.storePlaceholder}\n hint={providerCopy.storeHint}\n required\n className=\"mt-4\"\n />\n <TextField\n id=\"accessToken\"\n label={providerCopy.tokenLabel}\n type=\"password\"\n autoComplete=\"off\"\n value={source.accessToken}\n onChange={(value) => setSourceField('accessToken', value)}\n hint={providerCopy.tokenHint}\n required={!isSandbox}\n className=\"mt-4\"\n />\n {providerCopy.secretLabel && (\n <TextField\n id=\"accessSecret\"\n label={providerCopy.secretLabel}\n type=\"password\"\n autoComplete=\"off\"\n value={source.accessSecret}\n onChange={(value) => setSourceField('accessSecret', value)}\n required\n className=\"mt-4\"\n />\n )}\n\n {previewMutation.isError && (\n <p\n role=\"alert\"\n className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\n {previewMutation.error instanceof Error\n ? previewMutation.error.message\n : tr('coolandlovely.errors.generic', 'Something went wrong')}\n </p>\n )}\n\n <div className=\"mt-6 flex flex-wrap justify-between gap-2\">\n <button\n type=\"button\"\n onClick={trySandbox}\n disabled={previewMutation.isPending}\n className=\"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 disabled:opacity-50\"\n >\n <Sparkles className=\"h-4 w-4\" />{' '}\n {tr('coolandlovely.import.trySandbox', 'Try the Shopify sample store')}\n </button>\n <button\n type=\"submit\"\n disabled={previewMutation.isPending || !source.storeIdentifier.trim()}\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 {previewMutation.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Download className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.import.fetch', 'Fetch catalog')}\n </button>\n </div>\n </GlassCard>\n </form>\n ) : (\n <div className=\"mx-auto max-w-5xl space-y-4\" data-testid=\"catalog-import-review\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">\n {tr('coolandlovely.import.reviewTitle', 'Review the {{provider}} catalog', {\n provider: copy[preview.provider].label,\n })}\n </h2>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'coolandlovely.import.reviewSummary',\n '{{items}} products in {{collections}} collections from {{source}}',\n {\n items: preview.items.length,\n collections: preview.collections.length,\n source: preview.sourceUrl ?? copy[preview.provider].label,\n }\n )}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={reset}\n disabled={importMutation.isPending}\n className=\"text-sm font-medium text-text-secondary hover:text-text-primary disabled:opacity-50\"\n >\n {tr('coolandlovely.import.changeSource', 'Change source')}\n </button>\n </div>\n\n {(preview.truncated || preview.warnings.length > 0) && (\n <ul className=\"mt-4 space-y-1 rounded-lg bg-status-warning-bg-subtle px-3 py-2 text-sm text-status-warning-text\">\n {preview.truncated && (\n <li>\n {tr(\n 'coolandlovely.import.truncated',\n 'The store has more products than were fetched. Import these, then run another import for the rest.'\n )}\n </li>\n )}\n {preview.warnings.map((warning) => (\n <li key={warning}>{warning}</li>\n ))}\n </ul>\n )}\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"brandName\"\n label={tr('coolandlovely.import.brandName', 'Brand name')}\n value={brandName}\n onChange={setBrandName}\n required\n />\n <TextField\n id=\"brandHandle\"\n label={tr('coolandlovely.import.brandHandle', 'Handle')}\n value={brandHandle}\n onChange={(value) => setBrandHandle(value.toLowerCase())}\n hint={tr('coolandlovely.import.brandHandleHint', 'Unique per workspace')}\n error={handleError}\n required\n />\n </div>\n <ToggleField\n id=\"copyImages\"\n label={tr('coolandlovely.import.copyImages', 'Copy product images into Files')}\n checked={copyImages}\n onChange={setCopyImages}\n hint={tr(\n 'coolandlovely.import.copyImagesHint',\n 'Images are stored in your workspace Files. Images the platform will not share are skipped.'\n )}\n className=\"mt-4\"\n />\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-0\">\n <div className=\"flex items-center justify-between border-b border-border-seam px-4 py-3 text-sm\">\n <span className=\"text-text-secondary\">\n {tr('coolandlovely.import.selected', '{{count}} of {{total}} selected', {\n count: selected.size,\n total: preview.items.length,\n })}\n </span>\n <div className=\"flex gap-3\">\n <button\n type=\"button\"\n onClick={() => setSelected(new Set(preview.items.map((item) => item.externalId)))}\n className=\"font-medium text-text-secondary hover:text-text-primary\"\n >\n {tr('coolandlovely.import.selectAll', 'Select all')}\n </button>\n <button\n type=\"button\"\n onClick={() => setSelected(new Set())}\n className=\"font-medium text-text-secondary hover:text-text-primary\"\n >\n {tr('coolandlovely.import.selectNone', 'Select none')}\n </button>\n </div>\n </div>\n <ul className=\"max-h-[28rem] divide-y divide-border-seam overflow-y-auto\">\n {preview.items.map((item) => {\n const checked = selected.has(item.externalId);\n return (\n <li key={item.externalId}>\n <label\n className={cn(\n 'flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-bg-sunken',\n !checked && 'opacity-60'\n )}\n >\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={() => toggleItem(item.externalId)}\n className=\"h-4 w-4 accent-[var(--color-action-primary-bg)]\"\n aria-label={item.name}\n />\n {item.imageUrl ? (\n <img\n src={item.imageUrl}\n alt=\"\"\n loading=\"lazy\"\n referrerPolicy=\"no-referrer\"\n className=\"h-12 w-12 flex-none rounded-md bg-bg-sunken object-cover\"\n />\n ) : (\n <span className=\"flex h-12 w-12 flex-none items-center justify-center rounded-md bg-bg-sunken text-text-secondary\">\n <ImageOff className=\"h-4 w-4\" />\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">\n {item.name}\n </span>\n <span className=\"block truncate text-xs text-text-secondary\">\n {[\n titleCase(item.category),\n item.collectionName,\n item.sizes.length ? item.sizes.join(' / ') : null,\n ]\n .filter(Boolean)\n .join(' · ')}\n </span>\n </span>\n <span className=\"flex-none text-sm font-medium text-text-primary\">\n {formatPrice(item.price, item.currency)}\n </span>\n </label>\n </li>\n );\n })}\n </ul>\n </GlassCard>\n\n {importMutation.isError && (\n <p\n role=\"alert\"\n className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\n {importMutation.error instanceof Error\n ? importMutation.error.message\n : tr('coolandlovely.errors.generic', 'Something went wrong')}\n </p>\n )}\n\n <div className=\"flex flex-wrap items-center justify-end gap-3\">\n {progress && (\n <span className=\"text-sm text-text-secondary\" aria-live=\"polite\">\n {progress.phase === 'images'\n ? tr(\n 'coolandlovely.import.progressImages',\n 'Copying images {{done}} / {{total}}…',\n {\n done: progress.done,\n total: progress.total,\n }\n )\n : tr('coolandlovely.import.progressSaving', 'Creating your storefront…')}\n </span>\n )}\n <button\n type=\"button\"\n onClick={startImport}\n disabled={\n importMutation.isPending ||\n selected.size === 0 ||\n !brandName.trim() ||\n !brandHandle ||\n Boolean(handleError)\n }\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 {importMutation.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Download className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.import.importCount', 'Import {{count}} items', {\n count: selected.size,\n })}\n </button>\n </div>\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ImportCatalogPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/ImportCatalogPage.tsx"],"sourcesContent":["/**\n * Import Catalog Page (BOFF-3384)\n * @module coolandlovely/pages/brands\n *\n * Lets a brand bring its existing catalog over from Shopify, WooCommerce or\n * BigCommerce in three steps:\n *\n * 1. Connect — pick the platform and paste read-only API credentials. They\n * are sent once with `cooPreviewExternalCatalog` and never stored.\n * 2. Review — the catalog comes back mapped onto a brand, collections and\n * style items. The merchant confirms the brand name/handle and unticks\n * anything they do not want.\n * 3. Import — product images are copied into wspace-files and everything is\n * created in one transaction with `cooBulkImportCatalog`.\n */\n\nimport { type FormEvent, useMemo, useState } from 'react';\nimport {\n ArrowLeft,\n CheckCircle2,\n Download,\n ImageOff,\n Loader2,\n PackageOpen,\n Sparkles,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { SelectField, TextField, ToggleField } from '../../components/Field';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useTr } from '../../hooks';\nimport {\n type CatalogImportProgress,\n type ImportExternalCatalogResult,\n useImportExternalCatalog,\n usePreviewExternalCatalog,\n} from '../../hooks/useCatalogImport';\nimport type { CooCatalogProvider, CooExternalCatalogPreview } from '../../types';\nimport { cn } from '../../utils/cn';\nimport { formatPrice, titleCase } from '../../utils/formatters';\n\nconst SHOPIFY_SANDBOX = 'mock.shop';\nconst DEFAULT_MAX_ITEMS = '100';\nconst HANDLE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;\n\ninterface ProviderCopy {\n label: string;\n storeLabel: string;\n storePlaceholder: string;\n storeHint: string;\n tokenLabel: string;\n tokenHint: string;\n secretLabel?: string;\n}\n\nfunction useProviderCopy(): Record<CooCatalogProvider, ProviderCopy> {\n const tr = useTr();\n return useMemo(\n () => ({\n SHOPIFY: {\n label: 'Shopify',\n storeLabel: tr('coolandlovely.import.shopify.store', 'Shop domain'),\n storePlaceholder: 'your-brand.myshopify.com',\n storeHint: tr(\n 'coolandlovely.import.shopify.storeHint',\n 'Your permanent myshopify.com domain. Use mock.shop to try the Shopify sample store.'\n ),\n tokenLabel: tr('coolandlovely.import.shopify.token', 'Storefront API access token'),\n tokenHint: tr(\n 'coolandlovely.import.shopify.tokenHint',\n 'Shopify admin → Settings → Apps → Develop apps → Storefront API. Not needed for mock.shop.'\n ),\n },\n WOOCOMMERCE: {\n label: 'WooCommerce',\n storeLabel: tr('coolandlovely.import.woo.store', 'Store address'),\n storePlaceholder: 'https://shop.your-brand.com',\n storeHint: tr(\n 'coolandlovely.import.woo.storeHint',\n 'The HTTPS address of your WordPress store.'\n ),\n tokenLabel: tr('coolandlovely.import.woo.token', 'Consumer key'),\n tokenHint: tr(\n 'coolandlovely.import.woo.tokenHint',\n 'WooCommerce → Settings → Advanced → REST API → Add key, with Read permission.'\n ),\n secretLabel: tr('coolandlovely.import.woo.secret', 'Consumer secret'),\n },\n BIGCOMMERCE: {\n label: 'BigCommerce',\n storeLabel: tr('coolandlovely.import.bc.store', 'Store hash'),\n storePlaceholder: 'abc123def',\n storeHint: tr(\n 'coolandlovely.import.bc.storeHint',\n 'The code after /stores/ in your API path (or paste the API path itself).'\n ),\n tokenLabel: tr('coolandlovely.import.bc.token', 'API account access token'),\n tokenHint: tr(\n 'coolandlovely.import.bc.tokenHint',\n 'Settings → Store-level API accounts → Create, with read-only Products and Information scopes.'\n ),\n },\n }),\n [tr]\n );\n}\n\ninterface SourceForm {\n provider: CooCatalogProvider;\n storeIdentifier: string;\n accessToken: string;\n accessSecret: string;\n maxItems: string;\n}\n\nconst EMPTY_SOURCE: SourceForm = {\n provider: 'SHOPIFY',\n storeIdentifier: '',\n accessToken: '',\n accessSecret: '',\n maxItems: DEFAULT_MAX_ITEMS,\n};\n\nexport function ImportCatalogPage() {\n const tr = useTr();\n const { navigateTo } = useCoolAndLovely();\n const copy = useProviderCopy();\n const previewMutation = usePreviewExternalCatalog();\n const importMutation = useImportExternalCatalog();\n\n const [source, setSource] = useState<SourceForm>(EMPTY_SOURCE);\n const [preview, setPreview] = useState<CooExternalCatalogPreview | null>(null);\n const [brandName, setBrandName] = useState('');\n const [brandHandle, setBrandHandle] = useState('');\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [copyImages, setCopyImages] = useState(true);\n const [progress, setProgress] = useState<CatalogImportProgress | null>(null);\n const [result, setResult] = useState<ImportExternalCatalogResult | null>(null);\n\n const providerCopy = copy[source.provider];\n const isSandbox =\n source.provider === 'SHOPIFY' &&\n source.storeIdentifier.trim().toLowerCase() === SHOPIFY_SANDBOX;\n const handleError =\n brandHandle && !HANDLE_PATTERN.test(brandHandle)\n ? tr(\n 'coolandlovely.import.handleInvalid',\n 'Use lowercase letters, numbers and single hyphens'\n )\n : undefined;\n\n const setSourceField = <K extends keyof SourceForm>(key: K, value: SourceForm[K]) =>\n setSource((current) => ({ ...current, [key]: value }));\n\n const fetchPreview = (form: SourceForm) => {\n const maxItems = Number.parseInt(form.maxItems, 10);\n previewMutation.mutate(\n {\n provider: form.provider,\n storeIdentifier: form.storeIdentifier.trim(),\n ...(form.accessToken.trim() ? { accessToken: form.accessToken.trim() } : {}),\n ...(form.accessSecret.trim() ? { accessSecret: form.accessSecret.trim() } : {}),\n ...(Number.isFinite(maxItems) ? { maxItems } : {}),\n },\n {\n onSuccess: (data) => {\n setPreview(data);\n setBrandName(data.brand.name);\n setBrandHandle(data.brand.handle);\n setSelected(new Set(data.items.map((item) => item.externalId)));\n // Credentials are only needed for the preview: drop them from memory.\n setSource((current) => ({ ...current, accessToken: '', accessSecret: '' }));\n },\n }\n );\n };\n\n const handleConnect = (event: FormEvent) => {\n event.preventDefault();\n fetchPreview(source);\n };\n\n const trySandbox = () => {\n const sandbox: SourceForm = {\n ...EMPTY_SOURCE,\n storeIdentifier: SHOPIFY_SANDBOX,\n maxItems: '20',\n };\n setSource(sandbox);\n fetchPreview(sandbox);\n };\n\n const toggleItem = (id: string) =>\n setSelected((current) => {\n const next = new Set(current);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n\n const startImport = () => {\n if (!preview || selected.size === 0 || !brandName.trim() || !brandHandle || handleError) return;\n importMutation.mutate(\n {\n preview,\n brandName: brandName.trim(),\n brandHandle,\n selectedIds: selected,\n copyImages,\n onProgress: setProgress,\n },\n { onSuccess: setResult, onSettled: () => setProgress(null) }\n );\n };\n\n const reset = () => {\n setPreview(null);\n setResult(null);\n setSelected(new Set());\n previewMutation.reset();\n importMutation.reset();\n };\n\n return (\n <PageLayout\n title={tr('coolandlovely.import.title', 'Import catalog')}\n description={tr(\n 'coolandlovely.import.subtitle',\n 'Bring your existing storefront over from Shopify, WooCommerce or BigCommerce'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands')}\n className=\"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 <ArrowLeft className=\"h-4 w-4\" /> {tr('coolandlovely.actions.backToBrands', 'Brands')}\n </button>\n }\n >\n {result ? (\n <div className=\"mx-auto max-w-2xl\" data-testid=\"catalog-import-result\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"flex items-start gap-3\">\n <CheckCircle2 className=\"mt-0.5 h-6 w-6 text-status-success-text\" />\n <div className=\"flex-1\">\n <h2 className=\"text-base font-semibold text-text-primary\">\n {tr('coolandlovely.import.doneTitle', 'Imported {{name}}', {\n name: result.brand.name,\n })}\n </h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.import.doneSummary',\n '{{items}} style items in {{collections}} collections. The storefront starts as a draft.',\n { items: result.itemsCreated, collections: result.collectionsCreated }\n )}\n </p>\n {copyImages && (\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.import.doneImages',\n '{{copied}} images copied to Files, {{failed}} could not be copied.',\n {\n copied: result.imagesCopied,\n failed: result.imagesFailed,\n }\n )}\n </p>\n )}\n <div className=\"mt-4 flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo(`/brands/${result.brand.id}`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n {tr('coolandlovely.import.openBrand', 'Open storefront')}\n </button>\n <button\n type=\"button\"\n onClick={reset}\n className=\"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 {tr('coolandlovely.import.another', 'Import another catalog')}\n </button>\n </div>\n </div>\n </div>\n </GlassCard>\n </div>\n ) : !preview ? (\n <form\n onSubmit={handleConnect}\n className=\"mx-auto max-w-2xl\"\n data-testid=\"catalog-import-connect\"\n >\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-accent-purple-subtle text-text-primary\">\n <PackageOpen className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">\n {tr('coolandlovely.import.connectTitle', 'Connect your store')}\n </h2>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'coolandlovely.import.connectHint',\n 'Credentials are used once to read your catalog and are never stored.'\n )}\n </p>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <SelectField\n id=\"provider\"\n label={tr('coolandlovely.import.platform', 'Platform')}\n value={source.provider}\n onChange={(value) => setSourceField('provider', value as CooCatalogProvider)}\n options={(Object.keys(copy) as CooCatalogProvider[]).map((value) => ({\n value,\n label: copy[value].label,\n }))}\n />\n <TextField\n id=\"maxItems\"\n label={tr('coolandlovely.import.maxItems', 'Most products to fetch')}\n type=\"number\"\n value={source.maxItems}\n onChange={(value) => setSourceField('maxItems', value)}\n hint={tr('coolandlovely.import.maxItemsHint', 'Up to 500 per import')}\n />\n </div>\n\n <TextField\n id=\"storeIdentifier\"\n label={providerCopy.storeLabel}\n value={source.storeIdentifier}\n onChange={(value) => setSourceField('storeIdentifier', value)}\n placeholder={providerCopy.storePlaceholder}\n hint={providerCopy.storeHint}\n required\n className=\"mt-4\"\n />\n <TextField\n id=\"accessToken\"\n label={providerCopy.tokenLabel}\n type=\"password\"\n autoComplete=\"off\"\n value={source.accessToken}\n onChange={(value) => setSourceField('accessToken', value)}\n hint={providerCopy.tokenHint}\n required={!isSandbox}\n className=\"mt-4\"\n />\n {providerCopy.secretLabel && (\n <TextField\n id=\"accessSecret\"\n label={providerCopy.secretLabel}\n type=\"password\"\n autoComplete=\"off\"\n value={source.accessSecret}\n onChange={(value) => setSourceField('accessSecret', value)}\n required\n className=\"mt-4\"\n />\n )}\n\n {previewMutation.isError && (\n <p\n role=\"alert\"\n className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\n {previewMutation.error instanceof Error\n ? previewMutation.error.message\n : tr('coolandlovely.errors.generic', 'Something went wrong')}\n </p>\n )}\n\n <div className=\"mt-6 flex flex-wrap justify-between gap-2\">\n <button\n type=\"button\"\n onClick={trySandbox}\n disabled={previewMutation.isPending}\n className=\"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 disabled:opacity-50\"\n >\n <Sparkles className=\"h-4 w-4\" />{' '}\n {tr('coolandlovely.import.trySandbox', 'Try the Shopify sample store')}\n </button>\n <button\n type=\"submit\"\n disabled={previewMutation.isPending || !source.storeIdentifier.trim()}\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 {previewMutation.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Download className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.import.fetch', 'Fetch catalog')}\n </button>\n </div>\n </GlassCard>\n </form>\n ) : (\n <div className=\"mx-auto max-w-5xl space-y-4\" data-testid=\"catalog-import-review\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">\n {tr('coolandlovely.import.reviewTitle', 'Review the {{provider}} catalog', {\n provider: copy[preview.provider].label,\n })}\n </h2>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'coolandlovely.import.reviewSummary',\n '{{items}} products in {{collections}} collections from {{source}}',\n {\n items: preview.items.length,\n collections: preview.collections.length,\n source: preview.sourceUrl ?? copy[preview.provider].label,\n }\n )}\n </p>\n </div>\n <button\n type=\"button\"\n onClick={reset}\n disabled={importMutation.isPending}\n className=\"text-sm font-medium text-text-secondary hover:text-text-primary disabled:opacity-50\"\n >\n {tr('coolandlovely.import.changeSource', 'Change source')}\n </button>\n </div>\n\n {(preview.truncated || preview.warnings.length > 0) && (\n <ul className=\"mt-4 space-y-1 rounded-lg bg-status-warning-bg-subtle px-3 py-2 text-sm text-status-warning-text\">\n {preview.truncated && (\n <li>\n {tr(\n 'coolandlovely.import.truncated',\n 'The store has more products than were fetched. Import these, then run another import for the rest.'\n )}\n </li>\n )}\n {preview.warnings.map((warning) => (\n <li key={warning}>{warning}</li>\n ))}\n </ul>\n )}\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"brandName\"\n label={tr('coolandlovely.import.brandName', 'Brand name')}\n value={brandName}\n onChange={setBrandName}\n required\n />\n <TextField\n id=\"brandHandle\"\n label={tr('coolandlovely.import.brandHandle', 'Handle')}\n value={brandHandle}\n onChange={(value) => setBrandHandle(value.toLowerCase())}\n hint={tr('coolandlovely.import.brandHandleHint', 'Unique per workspace')}\n error={handleError}\n required\n />\n </div>\n <ToggleField\n id=\"copyImages\"\n label={tr('coolandlovely.import.copyImages', 'Copy product images into Files')}\n checked={copyImages}\n onChange={setCopyImages}\n hint={tr(\n 'coolandlovely.import.copyImagesHint',\n 'Images are stored in your workspace Files. Images the platform will not share are skipped.'\n )}\n className=\"mt-4\"\n />\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-0\">\n <div className=\"flex items-center justify-between border-b border-border-seam px-4 py-3 text-sm\">\n <span className=\"text-text-secondary\">\n {tr('coolandlovely.import.selected', '{{count}} of {{total}} selected', {\n count: selected.size,\n total: preview.items.length,\n })}\n </span>\n <div className=\"flex gap-3\">\n <button\n type=\"button\"\n onClick={() => setSelected(new Set(preview.items.map((item) => item.externalId)))}\n className=\"font-medium text-text-secondary hover:text-text-primary\"\n >\n {tr('coolandlovely.import.selectAll', 'Select all')}\n </button>\n <button\n type=\"button\"\n onClick={() => setSelected(new Set())}\n className=\"font-medium text-text-secondary hover:text-text-primary\"\n >\n {tr('coolandlovely.import.selectNone', 'Select none')}\n </button>\n </div>\n </div>\n <ul className=\"max-h-[28rem] divide-y divide-border-seam overflow-y-auto\">\n {preview.items.map((item) => {\n const checked = selected.has(item.externalId);\n return (\n <li key={item.externalId}>\n <label\n className={cn(\n 'flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-bg-sunken',\n !checked && 'opacity-60'\n )}\n >\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={() => toggleItem(item.externalId)}\n className=\"h-4 w-4 accent-[var(--color-action-primary-bg)]\"\n aria-label={item.name}\n />\n {item.imageUrl ? (\n <img\n src={item.imageUrl}\n alt=\"\"\n loading=\"lazy\"\n referrerPolicy=\"no-referrer\"\n className=\"h-12 w-12 flex-none rounded-md bg-bg-sunken object-cover\"\n />\n ) : (\n <span className=\"flex h-12 w-12 flex-none items-center justify-center rounded-md bg-bg-sunken text-text-secondary\">\n <ImageOff className=\"h-4 w-4\" />\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">\n {item.name}\n </span>\n <span className=\"block truncate text-xs text-text-secondary\">\n {[\n titleCase(item.category),\n item.collectionName,\n item.sizes.length ? item.sizes.join(' / ') : null,\n ]\n .filter(Boolean)\n .join(' · ')}\n </span>\n </span>\n <span className=\"flex-none text-sm font-medium text-text-primary\">\n {formatPrice(item.price, item.currency)}\n </span>\n </label>\n </li>\n );\n })}\n </ul>\n </GlassCard>\n\n {importMutation.isError && (\n <p\n role=\"alert\"\n className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\"\n >\n {importMutation.error instanceof Error\n ? importMutation.error.message\n : tr('coolandlovely.errors.generic', 'Something went wrong')}\n </p>\n )}\n\n <div className=\"flex flex-wrap items-center justify-end gap-3\">\n {progress && (\n <span className=\"text-sm text-text-secondary\" aria-live=\"polite\">\n {progress.phase === 'images'\n ? tr(\n 'coolandlovely.import.progressImages',\n 'Copying images {{done}} / {{total}}…',\n {\n done: progress.done,\n total: progress.total,\n }\n )\n : tr('coolandlovely.import.progressSaving', 'Creating your storefront…')}\n </span>\n )}\n <button\n type=\"button\"\n onClick={startImport}\n disabled={\n importMutation.isPending ||\n selected.size === 0 ||\n !brandName.trim() ||\n !brandHandle ||\n Boolean(handleError)\n }\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 {importMutation.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Download className=\"h-4 w-4\" />\n )}\n {tr('coolandlovely.import.importCount', 'Import {{count}} items', {\n count: selected.size,\n })}\n </button>\n </div>\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|