@burdenoff/microfe-coolandlovely 2026.1031.2 → 2026.1101.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/coolandlovely/CoolAndLovelyRoot.d.ts.map +1 -1
  2. package/dist/coolandlovely/CoolAndLovelyRoot.js +16 -4
  3. package/dist/coolandlovely/CoolAndLovelyRoot.js.map +1 -1
  4. package/dist/coolandlovely/CoolAndLovelyRoutes.d.ts.map +1 -1
  5. package/dist/coolandlovely/CoolAndLovelyRoutes.js +19 -15
  6. package/dist/coolandlovely/CoolAndLovelyRoutes.js.map +1 -1
  7. package/dist/coolandlovely/components/ActivityTrail.js +1 -1
  8. package/dist/coolandlovely/components/ActivityTrail.js.map +1 -1
  9. package/dist/coolandlovely/components/ActorName.js.map +1 -1
  10. package/dist/coolandlovely/components/AddToCartPanel.js.map +1 -1
  11. package/dist/coolandlovely/components/AddToLookbookButton.js.map +1 -1
  12. package/dist/coolandlovely/components/BrandCard.js.map +1 -1
  13. package/dist/coolandlovely/components/BrandCover.js.map +1 -1
  14. package/dist/coolandlovely/components/BrandCreatorAvatars.js.map +1 -1
  15. package/dist/coolandlovely/components/CollaborationFeeCard.js.map +1 -1
  16. package/dist/coolandlovely/components/CollectionCard.js.map +1 -1
  17. package/dist/coolandlovely/components/ColourSwatchRow.js.map +1 -1
  18. package/dist/coolandlovely/components/Comments.js.map +1 -1
  19. package/dist/coolandlovely/components/EmptyState.js.map +1 -1
  20. package/dist/coolandlovely/components/FeedbackWidget.js.map +1 -1
  21. package/dist/coolandlovely/components/Field.js.map +1 -1
  22. package/dist/coolandlovely/components/FileImage.js.map +1 -1
  23. package/dist/coolandlovely/components/FollowButton.js.map +1 -1
  24. package/dist/coolandlovely/components/FulfilmentTimeline.js.map +1 -1
  25. package/dist/coolandlovely/components/ImageUploadField.js +3 -1
  26. package/dist/coolandlovely/components/ImageUploadField.js.map +1 -1
  27. package/dist/coolandlovely/components/LookCard.js.map +1 -1
  28. package/dist/coolandlovely/components/LookbookCard.js.map +1 -1
  29. package/dist/coolandlovely/components/MetricsCard.js.map +1 -1
  30. package/dist/coolandlovely/components/PageLayout.js.map +1 -1
  31. package/dist/coolandlovely/components/PayForOrder.js.map +1 -1
  32. package/dist/coolandlovely/components/RefundPanel.js +1 -1
  33. package/dist/coolandlovely/components/RefundPanel.js.map +1 -1
  34. package/dist/coolandlovely/components/ReturnRequests.js +1 -1
  35. package/dist/coolandlovely/components/ReturnRequests.js.map +1 -1
  36. package/dist/coolandlovely/components/ShopItemCard.js.map +1 -1
  37. package/dist/coolandlovely/components/StateViews.js.map +1 -1
  38. package/dist/coolandlovely/components/StatusBadge.js +1 -1
  39. package/dist/coolandlovely/components/StatusBadge.js.map +1 -1
  40. package/dist/coolandlovely/components/StepTracker.js.map +1 -1
  41. package/dist/coolandlovely/components/StockEditor.js.map +1 -1
  42. package/dist/coolandlovely/components/StoreListingCard.js.map +1 -1
  43. package/dist/coolandlovely/components/TagList.js.map +1 -1
  44. package/dist/coolandlovely/components/Thumbnail.js +1 -1
  45. package/dist/coolandlovely/components/Thumbnail.js.map +1 -1
  46. package/dist/coolandlovely/components/VisibilityChip.js.map +1 -1
  47. package/dist/coolandlovely/components/WardrobeItemCard.js.map +1 -1
  48. package/dist/coolandlovely/components/index.js +2 -1
  49. package/dist/coolandlovely/context/CoolAndLovelyContext.js +2 -1
  50. package/dist/coolandlovely/context/CoolAndLovelyContext.js.map +1 -1
  51. package/dist/coolandlovely/context/index.js +2 -1
  52. package/dist/coolandlovely/hooks/index.js +4 -3
  53. package/dist/coolandlovely/hooks/useActivity.d.ts +1 -1
  54. package/dist/coolandlovely/hooks/useActivity.d.ts.map +1 -1
  55. package/dist/coolandlovely/hooks/useActivity.js.map +1 -1
  56. package/dist/coolandlovely/hooks/useAssistantEntity.js.map +1 -1
  57. package/dist/coolandlovely/hooks/useBrandOrdersApi.d.ts +4 -4
  58. package/dist/coolandlovely/hooks/useBrandOrdersApi.d.ts.map +1 -1
  59. package/dist/coolandlovely/hooks/useBrandOrdersApi.js.map +1 -1
  60. package/dist/coolandlovely/hooks/useCatalogImport.js.map +1 -1
  61. package/dist/coolandlovely/hooks/useComments.d.ts +1 -1
  62. package/dist/coolandlovely/hooks/useComments.d.ts.map +1 -1
  63. package/dist/coolandlovely/hooks/useComments.js.map +1 -1
  64. package/dist/coolandlovely/hooks/useCommerceApi.d.ts +7 -7
  65. package/dist/coolandlovely/hooks/useCommerceApi.d.ts.map +1 -1
  66. package/dist/coolandlovely/hooks/useCommerceApi.js.map +1 -1
  67. package/dist/coolandlovely/hooks/useCoolAndLovelyApi.d.ts +24 -24
  68. package/dist/coolandlovely/hooks/useCoolAndLovelyApi.d.ts.map +1 -1
  69. package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js.map +1 -1
  70. package/dist/coolandlovely/hooks/useEditorialApi.d.ts +95 -0
  71. package/dist/coolandlovely/hooks/useEditorialApi.d.ts.map +1 -0
  72. package/dist/coolandlovely/hooks/useEditorialApi.js +107 -0
  73. package/dist/coolandlovely/hooks/useEditorialApi.js.map +1 -0
  74. package/dist/coolandlovely/hooks/useFiles.js.map +1 -1
  75. package/dist/coolandlovely/hooks/useInventoryApi.d.ts +1 -1
  76. package/dist/coolandlovely/hooks/useInventoryApi.d.ts.map +1 -1
  77. package/dist/coolandlovely/hooks/useInventoryApi.js.map +1 -1
  78. package/dist/coolandlovely/hooks/useTr.js.map +1 -1
  79. package/dist/coolandlovely/index.js +6 -5
  80. package/dist/coolandlovely/pages/HomePage.js.map +1 -1
  81. package/dist/coolandlovely/pages/admin/AdminBrandsPage.js.map +1 -1
  82. package/dist/coolandlovely/pages/admin/AdminCollaborationsPage.js.map +1 -1
  83. package/dist/coolandlovely/pages/admin/AdminFeaturedItemsPage.js.map +1 -1
  84. package/dist/coolandlovely/pages/brands/BrandDetailPage.js.map +1 -1
  85. package/dist/coolandlovely/pages/brands/BrandFormPage.js.map +1 -1
  86. package/dist/coolandlovely/pages/brands/BrandsListPage.js.map +1 -1
  87. package/dist/coolandlovely/pages/brands/ImportCatalogPage.js.map +1 -1
  88. package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js.map +1 -1
  89. package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js +1 -1
  90. package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js.map +1 -1
  91. package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js.map +1 -1
  92. package/dist/coolandlovely/pages/collections/CollectionDetailPage.js.map +1 -1
  93. package/dist/coolandlovely/pages/collections/CollectionFormPage.js.map +1 -1
  94. package/dist/coolandlovely/pages/collections/CollectionsListPage.js.map +1 -1
  95. package/dist/coolandlovely/pages/commerce/BrandOrdersPage.js.map +1 -1
  96. package/dist/coolandlovely/pages/commerce/CartPage.js.map +1 -1
  97. package/dist/coolandlovely/pages/commerce/CreatorEarningsPage.js.map +1 -1
  98. package/dist/coolandlovely/pages/commerce/OrdersPage.js.map +1 -1
  99. package/dist/coolandlovely/pages/discover/DiscoverPage.js +1 -1
  100. package/dist/coolandlovely/pages/discover/DiscoverPage.js.map +1 -1
  101. package/dist/coolandlovely/pages/editorial/EditorialPage.d.ts +2 -0
  102. package/dist/coolandlovely/pages/editorial/EditorialPage.d.ts.map +1 -0
  103. package/dist/coolandlovely/pages/editorial/EditorialPage.js +486 -0
  104. package/dist/coolandlovely/pages/editorial/EditorialPage.js.map +1 -0
  105. package/dist/coolandlovely/pages/items/StoreListedPieceView.js.map +1 -1
  106. package/dist/coolandlovely/pages/items/StyleItemDetailPage.js.map +1 -1
  107. package/dist/coolandlovely/pages/items/StyleItemFormPage.js.map +1 -1
  108. package/dist/coolandlovely/pages/items/StyleItemsListPage.js.map +1 -1
  109. package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js.map +1 -1
  110. package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js.map +1 -1
  111. package/dist/coolandlovely/pages/looks/LookDetailPage.js.map +1 -1
  112. package/dist/coolandlovely/pages/looks/LookFormPage.js.map +1 -1
  113. package/dist/coolandlovely/pages/looks/LooksListPage.js.map +1 -1
  114. package/dist/coolandlovely/pages/profile/StyleProfilePage.js +1 -1
  115. package/dist/coolandlovely/pages/profile/StyleProfilePage.js.map +1 -1
  116. package/dist/coolandlovely/pages/trends/TrendsPage.js.map +1 -1
  117. package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js.map +1 -1
  118. package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js.map +1 -1
  119. package/dist/coolandlovely/store/coolandlovelyStore.js.map +1 -1
  120. package/dist/coolandlovely/store/index.js +2 -1
  121. package/dist/coolandlovely/types/commerce.d.ts.map +1 -1
  122. package/dist/coolandlovely/types/index.d.ts.map +1 -1
  123. package/dist/coolandlovely/utils/cn.js +1 -1
  124. package/dist/coolandlovely/utils/cn.js.map +1 -1
  125. package/dist/coolandlovely/utils/formatters.js.map +1 -1
  126. package/dist/coolandlovely/utils/icons.js.map +1 -1
  127. package/dist/coolandlovely/utils/index.js +2 -1
  128. package/dist/coolandlovely/utils/navigation.js.map +1 -1
  129. package/dist/coolandlovely/utils/paymentProviders.js +22 -22
  130. package/dist/coolandlovely/utils/paymentProviders.js.map +1 -1
  131. package/dist/coolandlovely/utils/sparkline.js +13 -15
  132. package/dist/coolandlovely/utils/sparkline.js.map +1 -1
  133. package/dist/node_modules/clsx/dist/clsx.js +1 -1
  134. package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
  135. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +225 -146
  136. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
  137. package/dist/services/createSandboxAssistantTransport.js.map +1 -1
  138. package/dist/stories/DiscoverPage.stories.d.ts.map +1 -1
  139. package/dist/utils/pageContext.js +1 -1
  140. package/dist/utils/pageContext.js.map +1 -1
  141. package/package.json +6 -4
@@ -1 +1 @@
1
- {"version":3,"file":"LookDetailPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/looks/LookDetailPage.tsx"],"sourcesContent":["/**\n * Look Detail Page\n * @module coolandlovely/pages/looks\n *\n * Single look (`cooLook`) with its look items, a true-toggle like\n * (`cooLikeLook` / `cooUnlikeLook`), a true-toggle save (`cooSaveLook` /\n * `cooUnsaveLook`), publish (`cooPublishLook`) and delete (`cooDeleteLook`)\n * actions.\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { ArrowLeft, Heart, Bookmark, Rocket, Trash2, Loader2, Sparkles, Star } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useActorProfiles, type ActorInput } from '@burdenoff/fe-libs/shared';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { TagList } from '../../components/TagList';\nimport { Comments } from '../../components/Comments';\nimport { FollowButton } from '../../components/FollowButton';\nimport { AddToLookbookButton } from '../../components/AddToLookbookButton';\nimport { ShopItemCard } from '../../components/ShopItemCard';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport {\n useLook,\n useLikeLook,\n useUnlikeLook,\n useSaveLook,\n useUnsaveLook,\n usePublishLook,\n useDeleteLook,\n useUpdateLook,\n useAssistantEntityName,\n} from '../../hooks';\nimport { formatDate, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\n\n/** Compute up-to-two-letter initials for the avatar fallback. */\nfunction initials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return '?';\n if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n}\n\n/**\n * Author chip — avatar + display name resolved via fe-libs `useActorProfiles`\n * on the GLOBAL gateway (never select firstName on a federated wspace User).\n * Clicking it opens the author's public style profile (`/profile/:userId`).\n */\nfunction LookAuthor({ authorId }: { authorId: string }) {\n const { apiGatewayUrl, authToken, workspaceId, navigateTo } = useCoolAndLovely();\n const [avatarFailed, setAvatarFailed] = useState(false);\n const actors = useMemo<ActorInput[]>(\n () => [{ actorId: authorId, actorType: 'user' }],\n [authorId]\n );\n const profiles = useActorProfiles(actors, { apiGatewayUrl, authToken, workspaceId });\n const profile = profiles[authorId];\n const name = profile?.displayName ?? 'Creator';\n const avatarUrl = profile?.avatarUrl;\n\n return (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/profile/${authorId}`)}\n className=\"inline-flex items-center gap-2 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <span\n title={name}\n className=\"relative flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-full bg-bg-sunken ring-1 ring-border-seam\"\n >\n {avatarUrl && !avatarFailed ? (\n <img\n src={avatarUrl}\n alt={name}\n loading=\"lazy\"\n onError={() => setAvatarFailed(true)}\n className=\"h-full w-full object-cover\"\n />\n ) : (\n <span className=\"text-[10px] font-semibold text-text-secondary\">{initials(name)}</span>\n )}\n </span>\n <span\n className=\"font-medium text-text-primary hover:underline\"\n title={profile?.email ?? authorId}\n >\n {name}\n </span>\n </button>\n );\n}\n\nexport function LookDetailPage() {\n const { lookId } = useParams<{ lookId: string }>();\n const { navigateTo, currentUser } = useCoolAndLovely();\n const query = useLook(lookId);\n const like = useLikeLook();\n const unlike = useUnlikeLook();\n const save = useSaveLook();\n const unsave = useUnsaveLook();\n const publish = usePublishLook();\n const remove = useDeleteLook();\n const update = useUpdateLook();\n // Name the look for the assistant's screen context.\n useAssistantEntityName(lookId, query.data?.title);\n\n if (query.isLoading) {\n return (\n <PageLayout title=\"Look\">\n <LoadingView label=\"Loading look…\" />\n </PageLayout>\n );\n }\n\n if (query.isError || !query.data) {\n return (\n <PageLayout title=\"Look\">\n <ErrorView title=\"Look not found\" onRetry={() => void query.refetch()} />\n </PageLayout>\n );\n }\n\n const look = query.data;\n const items = look.items ?? [];\n const isPublished = look.publishedAt !== null;\n const isOwnLook = Boolean(currentUser?.id && currentUser.id === look.authorId);\n\n return (\n <PageLayout\n title={look.title}\n description={titleCase(look.occasion)}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/looks')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <AddToLookbookButton lookId={look.id} />\n <button\n type=\"button\"\n disabled={update.isPending}\n onClick={() => update.mutate({ id: look.id, input: { isFeatured: !look.isFeatured } })}\n aria-pressed={look.isFeatured}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-lg border px-3 py-2 text-sm font-medium transition-colors disabled:opacity-50',\n look.isFeatured\n ? 'border-status-warning-text/40 bg-status-warning-bg-subtle text-status-warning-text'\n : 'border-border-seam text-text-primary hover:bg-bg-sunken'\n )}\n >\n {update.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Star className={cn('h-4 w-4', look.isFeatured && 'fill-status-warning-text')} />\n )}\n {look.isFeatured ? 'Featured' : 'Feature'}\n </button>\n {!isPublished && (\n <button\n type=\"button\"\n disabled={publish.isPending}\n onClick={() => publish.mutate(look.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 disabled:opacity-50\"\n >\n {publish.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Rocket className=\"h-4 w-4\" />\n )}\n Publish\n </button>\n )}\n <button\n type=\"button\"\n disabled={remove.isPending}\n onClick={() => remove.mutate(look.id, { onSuccess: () => navigateTo('/looks') })}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-50\"\n >\n {remove.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Trash2 className=\"h-4 w-4\" />\n )}\n Delete\n </button>\n </div>\n }\n >\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">\n <GlassCard treatment=\"glass\" className=\"group overflow-hidden p-0\">\n <Thumbnail\n icon={<Sparkles className=\"h-14 w-14\" />}\n seed={look.id}\n fileId={look.image?.id}\n alt={look.title}\n className=\"aspect-[4/5] max-h-[38rem] w-full rounded-none\"\n />\n <div className=\"p-6\">\n <div className=\"flex items-center gap-2\">\n <h2 className=\"text-xl font-semibold text-text-primary\">{look.title}</h2>\n {look.isFeatured && (\n <Star className=\"h-4 w-4 fill-status-warning-text text-status-warning-text\" />\n )}\n <StatusBadge status={isPublished ? 'published' : 'draft'} className=\"ml-auto\" />\n </div>\n {look.caption && <p className=\"mt-3 text-sm text-text-secondary\">{look.caption}</p>}\n {look.tags.length > 0 && <TagList tags={look.tags} className=\"mt-4\" tone=\"accent\" />}\n\n <div className=\"mt-4 flex flex-wrap items-center gap-3 text-sm text-text-secondary\">\n <LookAuthor authorId={look.authorId} />\n {!isOwnLook && <FollowButton followeeId={look.authorId} size=\"sm\" />}\n </div>\n\n <div className=\"mt-5 flex items-center gap-3\">\n <button\n type=\"button\"\n disabled={like.isPending || unlike.isPending}\n aria-pressed={Boolean(look.isLikedByMe)}\n onClick={() => (look.isLikedByMe ? unlike : like).mutate(look.id)}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-lg border px-3 py-2 text-sm font-medium transition-colors disabled:opacity-50',\n look.isLikedByMe\n ? 'border-status-error-text/40 bg-status-error-bg-subtle text-status-error-text'\n : 'border-border-seam text-text-primary hover:bg-status-error-bg-subtle'\n )}\n >\n {like.isPending || unlike.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Heart className={cn('h-4 w-4', look.isLikedByMe && 'fill-current')} />\n )}\n {look.isLikedByMe ? 'Liked' : 'Like'} · {look.likeCount}\n </button>\n <button\n type=\"button\"\n disabled={save.isPending || unsave.isPending}\n aria-pressed={Boolean(look.isSavedByMe)}\n onClick={() => (look.isSavedByMe ? unsave : save).mutate(look.id)}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-lg border px-3 py-2 text-sm font-medium transition-colors disabled:opacity-50',\n look.isSavedByMe\n ? 'border-accent-primary/40 bg-accent-primary/10 text-accent-primary'\n : 'border-border-seam text-text-secondary hover:bg-bg-sunken'\n )}\n >\n {save.isPending || unsave.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Bookmark className={cn('h-4 w-4', look.isSavedByMe && 'fill-current')} />\n )}\n {look.isSavedByMe ? 'Saved' : 'Save'} · {look.saveCount}\n </button>\n </div>\n </div>\n </GlassCard>\n\n {/* Shop the look */}\n <section className=\"mt-6\">\n <h3 className=\"mb-3 text-lg font-semibold text-text-primary\">\n Shop the look ({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 tagged in this look yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {[...items]\n .sort((a, b) => a.position - b.position)\n .map((li) =>\n li.styleItem ? (\n <ShopItemCard\n key={li.id}\n id={li.styleItem.id}\n name={li.styleItem.name}\n brandName={li.styleItem.brand?.name}\n price={li.styleItem.price}\n currency={li.styleItem.currency}\n imageFileId={li.styleItem.image?.id}\n onOpen={() => navigateTo(`/items/${li.styleItem?.id}?look=${look.id}`)}\n />\n ) : (\n <div\n key={li.id}\n className=\"flex flex-col justify-center rounded-xl border border-border-seam bg-bg-surface p-4\"\n >\n <p className=\"truncate text-sm font-medium text-text-primary\">\n Catalogue piece\n </p>\n {li.styling && (\n <p className=\"mt-0.5 truncate text-xs text-text-secondary\">\n {li.styling}\n </p>\n )}\n </div>\n )\n )}\n </div>\n )}\n </section>\n\n <Comments entityType=\"CooLook\" entityId={look.id} />\n </div>\n\n <div className=\"space-y-4\">\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\">Occasion</dt>\n <dd className=\"text-text-primary\">{titleCase(look.occasion)}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Visibility</dt>\n <dd className=\"text-text-primary\">{titleCase(look.visibility)}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Published</dt>\n <dd className=\"text-text-primary\">\n {look.publishedAt\n ? formatDate(look.publishedAt, { hour: undefined, minute: undefined })\n : '—'}\n </dd>\n </div>\n </dl>\n </GlassCard>\n\n {look.collaborationId && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/collaborations/${look.collaborationId}`)}\n className=\"flex w-full items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-4 text-left transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-status-warning-bg-subtle text-status-warning-text\">\n <Sparkles className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n Part of a collaboration\n </p>\n <p className=\"truncate text-xs text-text-secondary\">View collaboration</p>\n </div>\n </button>\n )}\n\n {(publish.isError ||\n remove.isError ||\n like.isError ||\n unlike.isError ||\n save.isError ||\n unsave.isError) && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-xs text-status-error-text\">\n {(publish.error ??\n remove.error ??\n like.error ??\n unlike.error ??\n save.error ??\n unsave.error) instanceof Error\n ? (\n publish.error ??\n remove.error ??\n like.error ??\n unlike.error ??\n save.error ??\n unsave.error\n )?.message\n : 'Action failed'}\n </p>\n )}\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LookDetailPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/looks/LookDetailPage.tsx"],"sourcesContent":["/**\n * Look Detail Page\n * @module coolandlovely/pages/looks\n *\n * Single look (`cooLook`) with its look items, a true-toggle like\n * (`cooLikeLook` / `cooUnlikeLook`), a true-toggle save (`cooSaveLook` /\n * `cooUnsaveLook`), publish (`cooPublishLook`) and delete (`cooDeleteLook`)\n * actions.\n */\n\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { ArrowLeft, Heart, Bookmark, Rocket, Trash2, Loader2, Sparkles, Star } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useActorProfiles, type ActorInput } from '@burdenoff/fe-libs/shared';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { TagList } from '../../components/TagList';\nimport { Comments } from '../../components/Comments';\nimport { FollowButton } from '../../components/FollowButton';\nimport { AddToLookbookButton } from '../../components/AddToLookbookButton';\nimport { ShopItemCard } from '../../components/ShopItemCard';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport {\n useLook,\n useLikeLook,\n useUnlikeLook,\n useSaveLook,\n useUnsaveLook,\n usePublishLook,\n useDeleteLook,\n useUpdateLook,\n useAssistantEntityName,\n} from '../../hooks';\nimport { formatDate, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\n\n/** Compute up-to-two-letter initials for the avatar fallback. */\nfunction initials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return '?';\n if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n}\n\n/**\n * Author chip — avatar + display name resolved via fe-libs `useActorProfiles`\n * on the GLOBAL gateway (never select firstName on a federated wspace User).\n * Clicking it opens the author's public style profile (`/profile/:userId`).\n */\nfunction LookAuthor({ authorId }: { authorId: string }) {\n const { apiGatewayUrl, authToken, workspaceId, navigateTo } = useCoolAndLovely();\n const [avatarFailed, setAvatarFailed] = useState(false);\n const actors = useMemo<ActorInput[]>(\n () => [{ actorId: authorId, actorType: 'user' }],\n [authorId]\n );\n const profiles = useActorProfiles(actors, { apiGatewayUrl, authToken, workspaceId });\n const profile = profiles[authorId];\n const name = profile?.displayName ?? 'Creator';\n const avatarUrl = profile?.avatarUrl;\n\n return (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/profile/${authorId}`)}\n className=\"inline-flex items-center gap-2 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <span\n title={name}\n className=\"relative flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-full bg-bg-sunken ring-1 ring-border-seam\"\n >\n {avatarUrl && !avatarFailed ? (\n <img\n src={avatarUrl}\n alt={name}\n loading=\"lazy\"\n onError={() => setAvatarFailed(true)}\n className=\"h-full w-full object-cover\"\n />\n ) : (\n <span className=\"text-[10px] font-semibold text-text-secondary\">{initials(name)}</span>\n )}\n </span>\n <span\n className=\"font-medium text-text-primary hover:underline\"\n title={profile?.email ?? authorId}\n >\n {name}\n </span>\n </button>\n );\n}\n\nexport function LookDetailPage() {\n const { lookId } = useParams<{ lookId: string }>();\n const { navigateTo, currentUser } = useCoolAndLovely();\n const query = useLook(lookId);\n const like = useLikeLook();\n const unlike = useUnlikeLook();\n const save = useSaveLook();\n const unsave = useUnsaveLook();\n const publish = usePublishLook();\n const remove = useDeleteLook();\n const update = useUpdateLook();\n // Name the look for the assistant's screen context.\n useAssistantEntityName(lookId, query.data?.title);\n\n if (query.isLoading) {\n return (\n <PageLayout title=\"Look\">\n <LoadingView label=\"Loading look…\" />\n </PageLayout>\n );\n }\n\n if (query.isError || !query.data) {\n return (\n <PageLayout title=\"Look\">\n <ErrorView title=\"Look not found\" onRetry={() => void query.refetch()} />\n </PageLayout>\n );\n }\n\n const look = query.data;\n const items = look.items ?? [];\n const isPublished = look.publishedAt !== null;\n const isOwnLook = Boolean(currentUser?.id && currentUser.id === look.authorId);\n\n return (\n <PageLayout\n title={look.title}\n description={titleCase(look.occasion)}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/looks')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <AddToLookbookButton lookId={look.id} />\n <button\n type=\"button\"\n disabled={update.isPending}\n onClick={() => update.mutate({ id: look.id, input: { isFeatured: !look.isFeatured } })}\n aria-pressed={look.isFeatured}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-lg border px-3 py-2 text-sm font-medium transition-colors disabled:opacity-50',\n look.isFeatured\n ? 'border-status-warning-text/40 bg-status-warning-bg-subtle text-status-warning-text'\n : 'border-border-seam text-text-primary hover:bg-bg-sunken'\n )}\n >\n {update.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Star className={cn('h-4 w-4', look.isFeatured && 'fill-status-warning-text')} />\n )}\n {look.isFeatured ? 'Featured' : 'Feature'}\n </button>\n {!isPublished && (\n <button\n type=\"button\"\n disabled={publish.isPending}\n onClick={() => publish.mutate(look.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 disabled:opacity-50\"\n >\n {publish.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Rocket className=\"h-4 w-4\" />\n )}\n Publish\n </button>\n )}\n <button\n type=\"button\"\n disabled={remove.isPending}\n onClick={() => remove.mutate(look.id, { onSuccess: () => navigateTo('/looks') })}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-status-error-text transition-colors hover:bg-status-error-bg-subtle disabled:opacity-50\"\n >\n {remove.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Trash2 className=\"h-4 w-4\" />\n )}\n Delete\n </button>\n </div>\n }\n >\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">\n <GlassCard treatment=\"glass\" className=\"group overflow-hidden p-0\">\n <Thumbnail\n icon={<Sparkles className=\"h-14 w-14\" />}\n seed={look.id}\n fileId={look.image?.id}\n alt={look.title}\n className=\"aspect-[4/5] max-h-[38rem] w-full rounded-none\"\n />\n <div className=\"p-6\">\n <div className=\"flex items-center gap-2\">\n <h2 className=\"text-xl font-semibold text-text-primary\">{look.title}</h2>\n {look.isFeatured && (\n <Star className=\"h-4 w-4 fill-status-warning-text text-status-warning-text\" />\n )}\n <StatusBadge status={isPublished ? 'published' : 'draft'} className=\"ml-auto\" />\n </div>\n {look.caption && <p className=\"mt-3 text-sm text-text-secondary\">{look.caption}</p>}\n {look.tags.length > 0 && <TagList tags={look.tags} className=\"mt-4\" tone=\"accent\" />}\n\n <div className=\"mt-4 flex flex-wrap items-center gap-3 text-sm text-text-secondary\">\n <LookAuthor authorId={look.authorId} />\n {!isOwnLook && <FollowButton followeeId={look.authorId} size=\"sm\" />}\n </div>\n\n <div className=\"mt-5 flex items-center gap-3\">\n <button\n type=\"button\"\n disabled={like.isPending || unlike.isPending}\n aria-pressed={Boolean(look.isLikedByMe)}\n onClick={() => (look.isLikedByMe ? unlike : like).mutate(look.id)}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-lg border px-3 py-2 text-sm font-medium transition-colors disabled:opacity-50',\n look.isLikedByMe\n ? 'border-status-error-text/40 bg-status-error-bg-subtle text-status-error-text'\n : 'border-border-seam text-text-primary hover:bg-status-error-bg-subtle'\n )}\n >\n {like.isPending || unlike.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Heart className={cn('h-4 w-4', look.isLikedByMe && 'fill-current')} />\n )}\n {look.isLikedByMe ? 'Liked' : 'Like'} · {look.likeCount}\n </button>\n <button\n type=\"button\"\n disabled={save.isPending || unsave.isPending}\n aria-pressed={Boolean(look.isSavedByMe)}\n onClick={() => (look.isSavedByMe ? unsave : save).mutate(look.id)}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-lg border px-3 py-2 text-sm font-medium transition-colors disabled:opacity-50',\n look.isSavedByMe\n ? 'border-accent-primary/40 bg-accent-primary/10 text-accent-primary'\n : 'border-border-seam text-text-secondary hover:bg-bg-sunken'\n )}\n >\n {save.isPending || unsave.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Bookmark className={cn('h-4 w-4', look.isSavedByMe && 'fill-current')} />\n )}\n {look.isSavedByMe ? 'Saved' : 'Save'} · {look.saveCount}\n </button>\n </div>\n </div>\n </GlassCard>\n\n {/* Shop the look */}\n <section className=\"mt-6\">\n <h3 className=\"mb-3 text-lg font-semibold text-text-primary\">\n Shop the look ({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 tagged in this look yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {[...items]\n .sort((a, b) => a.position - b.position)\n .map((li) =>\n li.styleItem ? (\n <ShopItemCard\n key={li.id}\n id={li.styleItem.id}\n name={li.styleItem.name}\n brandName={li.styleItem.brand?.name}\n price={li.styleItem.price}\n currency={li.styleItem.currency}\n imageFileId={li.styleItem.image?.id}\n onOpen={() => navigateTo(`/items/${li.styleItem?.id}?look=${look.id}`)}\n />\n ) : (\n <div\n key={li.id}\n className=\"flex flex-col justify-center rounded-xl border border-border-seam bg-bg-surface p-4\"\n >\n <p className=\"truncate text-sm font-medium text-text-primary\">\n Catalogue piece\n </p>\n {li.styling && (\n <p className=\"mt-0.5 truncate text-xs text-text-secondary\">\n {li.styling}\n </p>\n )}\n </div>\n )\n )}\n </div>\n )}\n </section>\n\n <Comments entityType=\"CooLook\" entityId={look.id} />\n </div>\n\n <div className=\"space-y-4\">\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\">Occasion</dt>\n <dd className=\"text-text-primary\">{titleCase(look.occasion)}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Visibility</dt>\n <dd className=\"text-text-primary\">{titleCase(look.visibility)}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Published</dt>\n <dd className=\"text-text-primary\">\n {look.publishedAt\n ? formatDate(look.publishedAt, { hour: undefined, minute: undefined })\n : '—'}\n </dd>\n </div>\n </dl>\n </GlassCard>\n\n {look.collaborationId && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/collaborations/${look.collaborationId}`)}\n className=\"flex w-full items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-4 text-left transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-status-warning-bg-subtle text-status-warning-text\">\n <Sparkles className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n Part of a collaboration\n </p>\n <p className=\"truncate text-xs text-text-secondary\">View collaboration</p>\n </div>\n </button>\n )}\n\n {(publish.isError ||\n remove.isError ||\n like.isError ||\n unlike.isError ||\n save.isError ||\n unsave.isError) && (\n <p className=\"rounded-lg bg-status-error-bg-subtle px-3 py-2 text-xs text-status-error-text\">\n {(publish.error ??\n remove.error ??\n like.error ??\n unlike.error ??\n save.error ??\n unsave.error) instanceof Error\n ? (\n publish.error ??\n remove.error ??\n like.error ??\n unlike.error ??\n save.error ??\n unsave.error\n )?.message\n : 'Action failed'}\n </p>\n )}\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"LookFormPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/looks/LookFormPage.tsx"],"sourcesContent":["/**\n * Look Create Page\n * @module coolandlovely/pages/looks\n *\n * Drives `cooCreateLook`. A look is created with its title/occasion/visibility\n * and an inline list of look items (catalogue pieces + styling notes), matching\n * the `CooCreateLookInput` shape exactly.\n */\n\nimport { useMemo, useState } from 'react';\nimport { ArrowLeft, Loader2, Plus, Save, Sparkles, X } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { TextField, TextAreaField, SelectField, ToggleField } from '../../components/Field';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useCreateLook, useUpdateLook, useStyleItems, useCollaborations } from '../../hooks';\nimport { COO_FILE_FOLDERS } from '../../hooks/useFiles';\nimport { parseCsv, titleCase, formatPrice } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooLookOccasion, CooVisibility, CooLookItemInput } from '../../types';\n\nconst OCCASIONS: CooLookOccasion[] = [\n 'EVERYDAY',\n 'WORK',\n 'EVENING',\n 'WEDDING_GUEST',\n 'VACATION',\n 'PARTY',\n 'ACTIVE',\n 'OTHER',\n];\n\nconst VISIBILITIES: CooVisibility[] = ['PUBLIC', 'FOLLOWERS', 'PRIVATE'];\n\ninterface DraftItem {\n styleItemId: string;\n styling: string;\n}\n\nexport function LookFormPage() {\n const { navigateTo, currentUser } = useCoolAndLovely();\n const createLook = useCreateLook();\n const updateLook = useUpdateLook();\n const styleItems = useStyleItems({ pagination: { limit: 100, offset: 0 } });\n // The caller's own collaborations — a look can be attributed to one.\n const collaborations = useCollaborations(currentUser?.id ? { creatorId: currentUser.id } : {});\n\n const [title, setTitle] = useState('');\n const [caption, setCaption] = useState('');\n const [occasion, setOccasion] = useState<CooLookOccasion>('EVERYDAY');\n const [visibility, setVisibility] = useState<CooVisibility>('PUBLIC');\n const [tags, setTags] = useState('');\n const [imageFileId, setImageFileId] = useState('');\n const [collaborationId, setCollaborationId] = useState('');\n const [isFeatured, setIsFeatured] = useState(false);\n const [draftItems, setDraftItems] = useState<DraftItem[]>([]);\n const [addValue, setAddValue] = useState('');\n const [titleError, setTitleError] = useState<string | undefined>();\n\n const usableItems = useMemo(\n () => (styleItems.data?.items ?? []).filter((i) => i.status !== 'RETIRED'),\n [styleItems.data]\n );\n\n const itemOptions = useMemo(\n () =>\n usableItems.map((i) => ({\n value: i.id,\n label: `${i.name}${i.brand?.name ? ` · ${i.brand.name}` : ''} — ${formatPrice(i.price, i.currency)}`,\n })),\n [usableItems]\n );\n\n // Pieces not yet added — the right-rail \"add\" picker only offers these.\n const availableOptions = useMemo(() => {\n const chosen = new Set(draftItems.map((d) => d.styleItemId));\n return itemOptions.filter((o) => !chosen.has(o.value));\n }, [itemOptions, draftItems]);\n\n const collaborationOptions = useMemo(\n () => (collaborations.data ?? []).map((c) => ({ value: c.id, label: c.title })),\n [collaborations.data]\n );\n\n const occasionOptions = OCCASIONS.map((o) => ({ value: o, label: titleCase(o) }));\n const visibilityOptions = VISIBILITIES.map((v) => ({ value: v, label: titleCase(v) }));\n\n const addDraftItem = (styleItemId: string) => {\n if (!styleItemId) return;\n setDraftItems((d) =>\n d.some((it) => it.styleItemId === styleItemId) ? d : [...d, { styleItemId, styling: '' }]\n );\n };\n const removeDraftItem = (idx: number) => setDraftItems((d) => d.filter((_, i) => i !== idx));\n const setDraftItem = (idx: number, patch: Partial<DraftItem>) =>\n setDraftItems((d) => d.map((it, i) => (i === idx ? { ...it, ...patch } : it)));\n\n const submitting = createLook.isPending || updateLook.isPending;\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (!title.trim()) {\n setTitleError('Title is required');\n return;\n }\n setTitleError(undefined);\n\n const items: CooLookItemInput[] = draftItems\n .filter((d) => d.styleItemId)\n .map((d, idx) => ({\n styleItemId: d.styleItemId,\n position: idx,\n styling: d.styling.trim() || undefined,\n }));\n\n createLook.mutate(\n {\n title: title.trim(),\n caption: caption.trim() || undefined,\n occasion,\n visibility,\n collaborationId: collaborationId || undefined,\n imageFileId: imageFileId || undefined,\n tags: parseCsv(tags),\n items: items.length > 0 ? items : undefined,\n },\n {\n onSuccess: (look) => {\n // isFeatured is only settable via update — feature right after create.\n if (isFeatured) {\n updateLook.mutate(\n { id: look.id, input: { isFeatured: true } },\n { onSuccess: () => navigateTo(`/looks/${look.id}`) }\n );\n } else {\n navigateTo(`/looks/${look.id}`);\n }\n },\n }\n );\n };\n\n return (\n <PageLayout\n title=\"New look\"\n description=\"Compose a shop-the-look outfit from catalogue pieces\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/looks')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-status-success-bg-subtle text-status-success-text\">\n <Sparkles className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Look details</h2>\n <p className=\"text-xs text-text-secondary\">\n Created as a draft — publish it from the look page.\n </p>\n </div>\n </div>\n\n <TextField\n id=\"title\"\n label=\"Title\"\n value={title}\n onChange={setTitle}\n placeholder=\"Coastal grandma, refined\"\n required\n error={titleError}\n />\n\n <TextAreaField\n id=\"caption\"\n label=\"Caption\"\n value={caption}\n onChange={setCaption}\n placeholder=\"How to style it…\"\n rows={3}\n className=\"mt-4\"\n />\n\n <ImageUploadField\n label=\"Look photo\"\n aspect=\"wide\"\n value={imageFileId}\n onChange={setImageFileId}\n folder={COO_FILE_FOLDERS.looks}\n hint=\"The styled outfit shot\"\n className=\"mt-4\"\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <SelectField\n id=\"occasion\"\n label=\"Occasion\"\n value={occasion}\n onChange={(v) => setOccasion(v as CooLookOccasion)}\n options={occasionOptions}\n />\n <SelectField\n id=\"visibility\"\n label=\"Visibility\"\n value={visibility}\n onChange={(v) => setVisibility(v as CooVisibility)}\n options={visibilityOptions}\n />\n </div>\n\n <SelectField\n id=\"collaborationId\"\n label=\"Collaboration (optional)\"\n value={collaborationId}\n onChange={setCollaborationId}\n options={collaborationOptions}\n placeholder={\n collaborations.isLoading\n ? 'Loading collaborations…'\n : collaborationOptions.length === 0\n ? 'No collaborations yet'\n : 'Not part of a collaboration'\n }\n hint=\"Attribute this look to one of your collaborations\"\n className=\"mt-4\"\n />\n\n <TextField\n id=\"tags\"\n label=\"Tags\"\n value={tags}\n onChange={setTags}\n placeholder=\"linen, neutral\"\n hint=\"Comma-separated\"\n className=\"mt-4\"\n />\n\n <ToggleField\n id=\"isFeatured\"\n label=\"Feature this look\"\n checked={isFeatured}\n onChange={setIsFeatured}\n hint=\"Featured looks surface in the Featured filter and carry a star badge.\"\n className=\"mt-4\"\n />\n\n {/* Look items */}\n <div className=\"mt-6\">\n <div className=\"mb-3 flex flex-col gap-3 sm:flex-row sm:items-end\">\n <div className=\"flex-1\">\n <SelectField\n id=\"add-style-item\"\n label=\"Pieces in this look\"\n value={addValue}\n onChange={setAddValue}\n options={availableOptions}\n placeholder={\n styleItems.isLoading\n ? 'Loading catalogue…'\n : availableOptions.length === 0\n ? 'All available pieces are already added'\n : 'Select a catalogue piece'\n }\n />\n </div>\n <button\n type=\"button\"\n disabled={!addValue}\n onClick={() => {\n addDraftItem(addValue);\n setAddValue('');\n }}\n className=\"inline-flex items-center justify-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-50\"\n >\n <Plus className=\"h-4 w-4\" /> Add piece\n </button>\n </div>\n {draftItems.length === 0 ? (\n <p className=\"rounded-lg border border-dashed border-border-seam p-4 text-center text-xs text-text-secondary\">\n No pieces added yet. A look can be published without pieces and styled later.\n </p>\n ) : (\n <div className=\"space-y-3\">\n {draftItems.map((di, idx) => (\n <div key={idx} className=\"rounded-lg border border-border-seam bg-bg-surface p-3\">\n <div className=\"flex items-start gap-2\">\n <div className=\"flex-1 space-y-2\">\n <SelectField\n label={`Piece ${idx + 1}`}\n value={di.styleItemId}\n onChange={(v) => setDraftItem(idx, { styleItemId: v })}\n options={itemOptions}\n placeholder={\n styleItems.isLoading ? 'Loading items…' : 'Select a style item'\n }\n />\n <TextField\n label=\"Styling note\"\n value={di.styling}\n onChange={(v) => setDraftItem(idx, { styling: v })}\n placeholder=\"Cuff the sleeves, tuck the front…\"\n />\n </div>\n <button\n type=\"button\"\n onClick={() => removeDraftItem(idx)}\n aria-label=\"Remove piece\"\n className={cn(\n 'mt-7 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',\n 'text-text-secondary transition-colors hover:bg-status-error-bg-subtle hover:text-status-error-text'\n )}\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n\n {createLook.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {createLook.error instanceof Error\n ? createLook.error.message\n : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"submit\"\n disabled={submitting}\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 {submitting ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Create look\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LookFormPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/looks/LookFormPage.tsx"],"sourcesContent":["/**\n * Look Create Page\n * @module coolandlovely/pages/looks\n *\n * Drives `cooCreateLook`. A look is created with its title/occasion/visibility\n * and an inline list of look items (catalogue pieces + styling notes), matching\n * the `CooCreateLookInput` shape exactly.\n */\n\nimport { useMemo, useState } from 'react';\nimport { ArrowLeft, Loader2, Plus, Save, Sparkles, X } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { TextField, TextAreaField, SelectField, ToggleField } from '../../components/Field';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useCreateLook, useUpdateLook, useStyleItems, useCollaborations } from '../../hooks';\nimport { COO_FILE_FOLDERS } from '../../hooks/useFiles';\nimport { parseCsv, titleCase, formatPrice } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooLookOccasion, CooVisibility, CooLookItemInput } from '../../types';\n\nconst OCCASIONS: CooLookOccasion[] = [\n 'EVERYDAY',\n 'WORK',\n 'EVENING',\n 'WEDDING_GUEST',\n 'VACATION',\n 'PARTY',\n 'ACTIVE',\n 'OTHER',\n];\n\nconst VISIBILITIES: CooVisibility[] = ['PUBLIC', 'FOLLOWERS', 'PRIVATE'];\n\ninterface DraftItem {\n styleItemId: string;\n styling: string;\n}\n\nexport function LookFormPage() {\n const { navigateTo, currentUser } = useCoolAndLovely();\n const createLook = useCreateLook();\n const updateLook = useUpdateLook();\n const styleItems = useStyleItems({ pagination: { limit: 100, offset: 0 } });\n // The caller's own collaborations — a look can be attributed to one.\n const collaborations = useCollaborations(currentUser?.id ? { creatorId: currentUser.id } : {});\n\n const [title, setTitle] = useState('');\n const [caption, setCaption] = useState('');\n const [occasion, setOccasion] = useState<CooLookOccasion>('EVERYDAY');\n const [visibility, setVisibility] = useState<CooVisibility>('PUBLIC');\n const [tags, setTags] = useState('');\n const [imageFileId, setImageFileId] = useState('');\n const [collaborationId, setCollaborationId] = useState('');\n const [isFeatured, setIsFeatured] = useState(false);\n const [draftItems, setDraftItems] = useState<DraftItem[]>([]);\n const [addValue, setAddValue] = useState('');\n const [titleError, setTitleError] = useState<string | undefined>();\n\n const usableItems = useMemo(\n () => (styleItems.data?.items ?? []).filter((i) => i.status !== 'RETIRED'),\n [styleItems.data]\n );\n\n const itemOptions = useMemo(\n () =>\n usableItems.map((i) => ({\n value: i.id,\n label: `${i.name}${i.brand?.name ? ` · ${i.brand.name}` : ''} — ${formatPrice(i.price, i.currency)}`,\n })),\n [usableItems]\n );\n\n // Pieces not yet added — the right-rail \"add\" picker only offers these.\n const availableOptions = useMemo(() => {\n const chosen = new Set(draftItems.map((d) => d.styleItemId));\n return itemOptions.filter((o) => !chosen.has(o.value));\n }, [itemOptions, draftItems]);\n\n const collaborationOptions = useMemo(\n () => (collaborations.data ?? []).map((c) => ({ value: c.id, label: c.title })),\n [collaborations.data]\n );\n\n const occasionOptions = OCCASIONS.map((o) => ({ value: o, label: titleCase(o) }));\n const visibilityOptions = VISIBILITIES.map((v) => ({ value: v, label: titleCase(v) }));\n\n const addDraftItem = (styleItemId: string) => {\n if (!styleItemId) return;\n setDraftItems((d) =>\n d.some((it) => it.styleItemId === styleItemId) ? d : [...d, { styleItemId, styling: '' }]\n );\n };\n const removeDraftItem = (idx: number) => setDraftItems((d) => d.filter((_, i) => i !== idx));\n const setDraftItem = (idx: number, patch: Partial<DraftItem>) =>\n setDraftItems((d) => d.map((it, i) => (i === idx ? { ...it, ...patch } : it)));\n\n const submitting = createLook.isPending || updateLook.isPending;\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (!title.trim()) {\n setTitleError('Title is required');\n return;\n }\n setTitleError(undefined);\n\n const items: CooLookItemInput[] = draftItems\n .filter((d) => d.styleItemId)\n .map((d, idx) => ({\n styleItemId: d.styleItemId,\n position: idx,\n styling: d.styling.trim() || undefined,\n }));\n\n createLook.mutate(\n {\n title: title.trim(),\n caption: caption.trim() || undefined,\n occasion,\n visibility,\n collaborationId: collaborationId || undefined,\n imageFileId: imageFileId || undefined,\n tags: parseCsv(tags),\n items: items.length > 0 ? items : undefined,\n },\n {\n onSuccess: (look) => {\n // isFeatured is only settable via update — feature right after create.\n if (isFeatured) {\n updateLook.mutate(\n { id: look.id, input: { isFeatured: true } },\n { onSuccess: () => navigateTo(`/looks/${look.id}`) }\n );\n } else {\n navigateTo(`/looks/${look.id}`);\n }\n },\n }\n );\n };\n\n return (\n <PageLayout\n title=\"New look\"\n description=\"Compose a shop-the-look outfit from catalogue pieces\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/looks')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-status-success-bg-subtle text-status-success-text\">\n <Sparkles className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Look details</h2>\n <p className=\"text-xs text-text-secondary\">\n Created as a draft — publish it from the look page.\n </p>\n </div>\n </div>\n\n <TextField\n id=\"title\"\n label=\"Title\"\n value={title}\n onChange={setTitle}\n placeholder=\"Coastal grandma, refined\"\n required\n error={titleError}\n />\n\n <TextAreaField\n id=\"caption\"\n label=\"Caption\"\n value={caption}\n onChange={setCaption}\n placeholder=\"How to style it…\"\n rows={3}\n className=\"mt-4\"\n />\n\n <ImageUploadField\n label=\"Look photo\"\n aspect=\"wide\"\n value={imageFileId}\n onChange={setImageFileId}\n folder={COO_FILE_FOLDERS.looks}\n hint=\"The styled outfit shot\"\n className=\"mt-4\"\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <SelectField\n id=\"occasion\"\n label=\"Occasion\"\n value={occasion}\n onChange={(v) => setOccasion(v as CooLookOccasion)}\n options={occasionOptions}\n />\n <SelectField\n id=\"visibility\"\n label=\"Visibility\"\n value={visibility}\n onChange={(v) => setVisibility(v as CooVisibility)}\n options={visibilityOptions}\n />\n </div>\n\n <SelectField\n id=\"collaborationId\"\n label=\"Collaboration (optional)\"\n value={collaborationId}\n onChange={setCollaborationId}\n options={collaborationOptions}\n placeholder={\n collaborations.isLoading\n ? 'Loading collaborations…'\n : collaborationOptions.length === 0\n ? 'No collaborations yet'\n : 'Not part of a collaboration'\n }\n hint=\"Attribute this look to one of your collaborations\"\n className=\"mt-4\"\n />\n\n <TextField\n id=\"tags\"\n label=\"Tags\"\n value={tags}\n onChange={setTags}\n placeholder=\"linen, neutral\"\n hint=\"Comma-separated\"\n className=\"mt-4\"\n />\n\n <ToggleField\n id=\"isFeatured\"\n label=\"Feature this look\"\n checked={isFeatured}\n onChange={setIsFeatured}\n hint=\"Featured looks surface in the Featured filter and carry a star badge.\"\n className=\"mt-4\"\n />\n\n {/* Look items */}\n <div className=\"mt-6\">\n <div className=\"mb-3 flex flex-col gap-3 sm:flex-row sm:items-end\">\n <div className=\"flex-1\">\n <SelectField\n id=\"add-style-item\"\n label=\"Pieces in this look\"\n value={addValue}\n onChange={setAddValue}\n options={availableOptions}\n placeholder={\n styleItems.isLoading\n ? 'Loading catalogue…'\n : availableOptions.length === 0\n ? 'All available pieces are already added'\n : 'Select a catalogue piece'\n }\n />\n </div>\n <button\n type=\"button\"\n disabled={!addValue}\n onClick={() => {\n addDraftItem(addValue);\n setAddValue('');\n }}\n className=\"inline-flex items-center justify-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-50\"\n >\n <Plus className=\"h-4 w-4\" /> Add piece\n </button>\n </div>\n {draftItems.length === 0 ? (\n <p className=\"rounded-lg border border-dashed border-border-seam p-4 text-center text-xs text-text-secondary\">\n No pieces added yet. A look can be published without pieces and styled later.\n </p>\n ) : (\n <div className=\"space-y-3\">\n {draftItems.map((di, idx) => (\n <div key={idx} className=\"rounded-lg border border-border-seam bg-bg-surface p-3\">\n <div className=\"flex items-start gap-2\">\n <div className=\"flex-1 space-y-2\">\n <SelectField\n label={`Piece ${idx + 1}`}\n value={di.styleItemId}\n onChange={(v) => setDraftItem(idx, { styleItemId: v })}\n options={itemOptions}\n placeholder={\n styleItems.isLoading ? 'Loading items…' : 'Select a style item'\n }\n />\n <TextField\n label=\"Styling note\"\n value={di.styling}\n onChange={(v) => setDraftItem(idx, { styling: v })}\n placeholder=\"Cuff the sleeves, tuck the front…\"\n />\n </div>\n <button\n type=\"button\"\n onClick={() => removeDraftItem(idx)}\n aria-label=\"Remove piece\"\n className={cn(\n 'mt-7 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',\n 'text-text-secondary transition-colors hover:bg-status-error-bg-subtle hover:text-status-error-text'\n )}\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n\n {createLook.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {createLook.error instanceof Error\n ? createLook.error.message\n : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"submit\"\n disabled={submitting}\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 {submitting ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Create look\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"LooksListPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/looks/LooksListPage.tsx"],"sourcesContent":["/**\n * Looks List Page\n * @module coolandlovely/pages/looks\n *\n * Cursor-paginated feed of published looks (`cooListLooks`) with occasion &\n * featured filters.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, Sparkles, ChevronLeft, ChevronRight, Star } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { LookCard } from '../../components/LookCard';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useLooks, useTr } from '../../hooks';\nimport { titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooLookOccasion } from '../../types';\n\nconst PAGE_SIZE = 24;\nconst OCCASION_ORDER: CooLookOccasion[] = [\n 'EVERYDAY',\n 'WORK',\n 'EVENING',\n 'WEDDING_GUEST',\n 'VACATION',\n 'PARTY',\n 'ACTIVE',\n 'OTHER',\n];\n\nexport function LooksListPage() {\n const { navigateTo } = useCoolAndLovely();\n const tr = useTr();\n const [occasion, setOccasion] = useState<CooLookOccasion | 'ALL'>('ALL');\n const [featuredOnly, setFeaturedOnly] = useState(false);\n const [offset, setOffset] = useState(0);\n\n const occasionOptions = useMemo(\n () => [\n { value: 'ALL' as const, label: tr('coolandlovely.filters.all', 'All') },\n ...OCCASION_ORDER.map((value) => ({\n value,\n label: tr(`coolandlovely.occasion.${value.toLowerCase()}`, titleCase(value)),\n })),\n ],\n [tr]\n );\n\n const query = useLooks({\n occasion: occasion === 'ALL' ? undefined : occasion,\n featured: featuredOnly ? true : undefined,\n pagination: { limit: PAGE_SIZE, offset },\n });\n\n const looks = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title={tr('coolandlovely.page.looksTitle', 'Looks')}\n description={tr(\n 'coolandlovely.page.looksSubtitle',\n 'Shop-the-look outfits built from real catalogue pieces'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/looks/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.newLook', 'New look')}\n </button>\n }\n >\n <div className=\"mb-6 flex flex-wrap items-center gap-2\">\n {occasionOptions.map((opt) => (\n <button\n key={opt.value}\n type=\"button\"\n onClick={() => {\n setOccasion(opt.value);\n setOffset(0);\n }}\n className={cn(\n 'rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n occasion === 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.looks', 'Loading looks…')} />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : looks.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Sparkles className=\"h-10 w-10\" />}\n title={tr('coolandlovely.empty.looksTitle', 'No looks yet')}\n description={tr(\n 'coolandlovely.empty.looksDescription',\n 'Publish a shop-the-look outfit from catalogue pieces.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/looks/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.newLook', 'New look')}\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4\">\n {looks.map((look) => (\n <div key={look.id} className=\"relative\">\n <LookCard\n id={look.id}\n title={look.title}\n occasion={look.occasion}\n imageFileId={look.image?.id}\n likeCount={look.likeCount}\n isLikedByMe={look.isLikedByMe}\n isSavedByMe={look.isSavedByMe}\n onOpen={() => navigateTo(`/looks/${look.id}`)}\n showActions\n />\n {(look.isFeatured || look.publishedAt === null) && (\n <div className=\"pointer-events-none absolute left-2 top-2 z-10 flex flex-col items-start gap-1\">\n {look.isFeatured && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-xs font-medium text-status-warning-text shadow-sm backdrop-blur\">\n <Star className=\"h-3 w-3 fill-status-warning-text\" />{' '}\n {tr('coolandlovely.filters.featured', 'Featured')}\n </span>\n )}\n {look.publishedAt === null && <StatusBadge status=\"draft\" />}\n </div>\n )}\n </div>\n ))}\n </div>\n\n {(offset > 0 || hasMore) && (\n <div className=\"mt-6 flex items-center justify-center gap-2\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n <ChevronLeft className=\"h-4 w-4\" />{' '}\n {tr('coolandlovely.pagination.previous', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.pagination.page', 'Page {{page}}', {\n page: offset / PAGE_SIZE + 1,\n })}\n </span>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset((o) => o + PAGE_SIZE)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n {tr('coolandlovely.pagination.next', 'Next')} <ChevronRight className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LooksListPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/looks/LooksListPage.tsx"],"sourcesContent":["/**\n * Looks List Page\n * @module coolandlovely/pages/looks\n *\n * Cursor-paginated feed of published looks (`cooListLooks`) with occasion &\n * featured filters.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Plus, Sparkles, ChevronLeft, ChevronRight, Star } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { LookCard } from '../../components/LookCard';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useLooks, useTr } from '../../hooks';\nimport { titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooLookOccasion } from '../../types';\n\nconst PAGE_SIZE = 24;\nconst OCCASION_ORDER: CooLookOccasion[] = [\n 'EVERYDAY',\n 'WORK',\n 'EVENING',\n 'WEDDING_GUEST',\n 'VACATION',\n 'PARTY',\n 'ACTIVE',\n 'OTHER',\n];\n\nexport function LooksListPage() {\n const { navigateTo } = useCoolAndLovely();\n const tr = useTr();\n const [occasion, setOccasion] = useState<CooLookOccasion | 'ALL'>('ALL');\n const [featuredOnly, setFeaturedOnly] = useState(false);\n const [offset, setOffset] = useState(0);\n\n const occasionOptions = useMemo(\n () => [\n { value: 'ALL' as const, label: tr('coolandlovely.filters.all', 'All') },\n ...OCCASION_ORDER.map((value) => ({\n value,\n label: tr(`coolandlovely.occasion.${value.toLowerCase()}`, titleCase(value)),\n })),\n ],\n [tr]\n );\n\n const query = useLooks({\n occasion: occasion === 'ALL' ? undefined : occasion,\n featured: featuredOnly ? true : undefined,\n pagination: { limit: PAGE_SIZE, offset },\n });\n\n const looks = query.data?.items ?? [];\n const hasMore = query.data?.pageInfo.hasMore ?? false;\n\n return (\n <PageLayout\n title={tr('coolandlovely.page.looksTitle', 'Looks')}\n description={tr(\n 'coolandlovely.page.looksSubtitle',\n 'Shop-the-look outfits built from real catalogue pieces'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/looks/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.newLook', 'New look')}\n </button>\n }\n >\n <div className=\"mb-6 flex flex-wrap items-center gap-2\">\n {occasionOptions.map((opt) => (\n <button\n key={opt.value}\n type=\"button\"\n onClick={() => {\n setOccasion(opt.value);\n setOffset(0);\n }}\n className={cn(\n 'rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n occasion === 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.looks', 'Loading looks…')} />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : looks.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Sparkles className=\"h-10 w-10\" />}\n title={tr('coolandlovely.empty.looksTitle', 'No looks yet')}\n description={tr(\n 'coolandlovely.empty.looksDescription',\n 'Publish a shop-the-look outfit from catalogue pieces.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/looks/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.newLook', 'New look')}\n </button>\n }\n />\n ) : (\n <>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4\">\n {looks.map((look) => (\n <div key={look.id} className=\"relative\">\n <LookCard\n id={look.id}\n title={look.title}\n occasion={look.occasion}\n imageFileId={look.image?.id}\n likeCount={look.likeCount}\n isLikedByMe={look.isLikedByMe}\n isSavedByMe={look.isSavedByMe}\n onOpen={() => navigateTo(`/looks/${look.id}`)}\n showActions\n />\n {(look.isFeatured || look.publishedAt === null) && (\n <div className=\"pointer-events-none absolute left-2 top-2 z-10 flex flex-col items-start gap-1\">\n {look.isFeatured && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-warning-bg-subtle px-2 py-0.5 text-xs font-medium text-status-warning-text shadow-sm backdrop-blur\">\n <Star className=\"h-3 w-3 fill-status-warning-text\" />{' '}\n {tr('coolandlovely.filters.featured', 'Featured')}\n </span>\n )}\n {look.publishedAt === null && <StatusBadge status=\"draft\" />}\n </div>\n )}\n </div>\n ))}\n </div>\n\n {(offset > 0 || hasMore) && (\n <div className=\"mt-6 flex items-center justify-center gap-2\">\n <button\n type=\"button\"\n disabled={offset === 0}\n onClick={() => setOffset((o) => Math.max(0, o - PAGE_SIZE))}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n <ChevronLeft className=\"h-4 w-4\" />{' '}\n {tr('coolandlovely.pagination.previous', 'Previous')}\n </button>\n <span className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.pagination.page', 'Page {{page}}', {\n page: offset / PAGE_SIZE + 1,\n })}\n </span>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setOffset((o) => o + PAGE_SIZE)}\n className=\"inline-flex items-center gap-1 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:cursor-not-allowed disabled:opacity-40\"\n >\n {tr('coolandlovely.pagination.next', 'Next')} <ChevronRight className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -388,7 +388,7 @@ function P() {
388
388
  })
389
389
  });
390
390
  }
391
- let me = Array.from(new Set([...le, ...Object.keys(J.sizes).map((e) => e.toLowerCase())]));
391
+ let me = Array.from(/* @__PURE__ */ new Set([...le, ...Object.keys(J.sizes).map((e) => e.toLowerCase())]));
392
392
  return /* @__PURE__ */ O(n, {
393
393
  title: X ? L("coolandlovely.profile.editTitle", "Edit style profile") : L("coolandlovely.profile.createTitle", "Create your style profile"),
394
394
  description: L("coolandlovely.profile.editDescription", "This powers your personalised discovery feed"),
@@ -1 +1 @@
1
- {"version":3,"file":"StyleProfilePage.js","names":[],"sources":["../../../../src/coolandlovely/pages/profile/StyleProfilePage.tsx"],"sourcesContent":["/**\n * Style Profile Page\n * @module coolandlovely/pages/profile\n *\n * Two routes share this component:\n * - `/profile` — the calling shopper's own profile (`cooMyStyleProfile` +\n * `cooUpsertStyleProfile`), editable, with a self-scoped wardrobe preview\n * and \"people you follow\" list.\n * - `/profile/:userId` — any other shopper's PUBLIC profile\n * (`cooStyleProfileByOwner`), reached by clicking an author's name/avatar\n * on a Look/Discover/Collaboration card. Read-only: no edit affordance, no\n * wardrobe preview (wardrobe stays self-scoped server-side), and shows a\n * Follow button instead. Server-side CooVisibility (PRIVATE/FOLLOWERS)\n * gates whether the profile resolves at all.\n */\n\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { Loader2, Save, UserCircle, Pencil, X, Users, Sparkles, Shirt } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { TextField, TextAreaField, SelectField } from '../../components/Field';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { ColourSwatchRow } from '../../components/ColourSwatchRow';\nimport { VisibilityChip } from '../../components/VisibilityChip';\nimport { ActorName } from '../../components/ActorName';\nimport { FollowButton } from '../../components/FollowButton';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport {\n useMyStyleProfile,\n useStyleProfileByOwner,\n useUpsertStyleProfile,\n useMyFollowing,\n useWardrobeItems,\n useLooks,\n useTr,\n} from '../../hooks';\nimport { categoryIcon } from '../../utils/icons';\nimport { parseCsv, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooVisibility } from '../../types';\n\nconst VISIBILITIES: CooVisibility[] = ['PUBLIC', 'FOLLOWERS', 'PRIVATE'];\n\n/** Common size rows offered in the editor (existing custom keys are preserved). */\nconst SIZE_KEYS = ['tops', 'bottoms', 'dresses', 'shoes', 'outerwear'];\n\ninterface FormState {\n displayName: string;\n bio: string;\n visibility: CooVisibility;\n stylePreferences: string;\n palette: string;\n favouriteBrands: string;\n sizes: Record<string, string>;\n}\n\nconst EMPTY: FormState = {\n displayName: '',\n bio: '',\n visibility: 'PRIVATE',\n stylePreferences: '',\n palette: '',\n favouriteBrands: '',\n sizes: {},\n};\n\n/** Monogram from a free-text brand name (real brand logos are unavailable). */\nfunction brandInitials(name: string): string {\n const words = name.trim().split(/\\s+/).filter(Boolean);\n if (words.length === 0) return '?';\n if (words.length === 1) return words[0].slice(0, 2).toUpperCase();\n return (words[0][0] + words[1][0]).toUpperCase();\n}\n\nfunction BentoCard({\n title,\n children,\n className,\n}: {\n title: string;\n children: React.ReactNode;\n className?: string;\n}) {\n return (\n <GlassCard treatment=\"plain\" className={cn('p-5', className)}>\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">{title}</h3>\n {children}\n </GlassCard>\n );\n}\n\nexport function StyleProfilePage() {\n const { userId } = useParams<{ userId?: string }>();\n const { navigateTo, currentUser } = useCoolAndLovely();\n const tr = useTr();\n const isOwnProfile = !userId || userId === currentUser?.id;\n\n const myQuery = useMyStyleProfile();\n const otherQuery = useStyleProfileByOwner(isOwnProfile ? undefined : userId);\n const query = isOwnProfile ? myQuery : otherQuery;\n\n const upsert = useUpsertStyleProfile();\n // Both self-scoped — meaningful only on the caller's own profile page, but\n // called unconditionally (rules of hooks) and simply not rendered otherwise.\n const following = useMyFollowing();\n const wardrobe = useWardrobeItems();\n\n const profile = query.data;\n const recentLooks = useLooks({\n authorId: profile?.ownerId,\n pagination: { limit: 6, offset: 0 },\n });\n\n const [editing, setEditing] = useState(false);\n const [form, setForm] = useState<FormState>(EMPTY);\n\n const hasProfile = Boolean(profile);\n const followingList = following.data ?? [];\n\n useEffect(() => {\n if (isOwnProfile && profile) {\n setForm({\n displayName: profile.displayName ?? '',\n bio: profile.bio ?? '',\n visibility: profile.visibility,\n stylePreferences: profile.stylePreferences.join(', '),\n palette: profile.palette.join(', '),\n favouriteBrands: profile.favouriteBrands.join(', '),\n sizes: Object.fromEntries(\n Object.entries(profile.sizes ?? {}).map(([k, v]) => [k, String(v ?? '')])\n ),\n });\n }\n }, [isOwnProfile, profile]);\n\n // If there is no profile yet, drop straight into edit mode — own profile only.\n useEffect(() => {\n if (isOwnProfile && !query.isLoading && !hasProfile) {\n setEditing(true);\n }\n }, [isOwnProfile, query.isLoading, hasProfile]);\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((f) => ({ ...f, [key]: value }));\n\n const setSize = (key: string, value: string) =>\n setForm((f) => ({ ...f, sizes: { ...f.sizes, [key]: value } }));\n\n const visibilityOptions = useMemo(\n () =>\n VISIBILITIES.map((v) => ({\n value: v,\n label: tr(`coolandlovely.visibility.${v.toLowerCase()}`, titleCase(v)),\n })),\n [tr]\n );\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n const cleanedSizes = Object.fromEntries(\n Object.entries(form.sizes)\n .map(([k, v]) => [k, v.trim()] as const)\n .filter(([, v]) => v.length > 0)\n );\n upsert.mutate(\n {\n displayName: form.displayName.trim() || undefined,\n bio: form.bio.trim() || undefined,\n visibility: form.visibility,\n stylePreferences: parseCsv(form.stylePreferences),\n palette: parseCsv(form.palette),\n favouriteBrands: parseCsv(form.favouriteBrands),\n sizes: cleanedSizes,\n },\n { onSuccess: () => setEditing(false) }\n );\n };\n\n if (query.isLoading) {\n return (\n <PageLayout title={tr('coolandlovely.page.profileTitle', 'Style Profile')}>\n <LoadingView label={tr('coolandlovely.loading.profile', 'Loading style profile…')} />\n </PageLayout>\n );\n }\n\n if (query.isError) {\n return (\n <PageLayout title={tr('coolandlovely.page.profileTitle', 'Style Profile')}>\n <ErrorView onRetry={() => void query.refetch()} />\n </PageLayout>\n );\n }\n\n // Public view of someone else's profile that does not exist, or whose\n // CooVisibility (PRIVATE / FOLLOWERS-not-following) hides it from us — the\n // svc returns null in both cases so a caller cannot tell them apart.\n if (!isOwnProfile && !profile) {\n return (\n <PageLayout title={tr('coolandlovely.page.profileTitle', 'Style Profile')}>\n <EmptyState\n title={tr('coolandlovely.profile.notAvailableTitle', 'Profile not available')}\n description={tr(\n 'coolandlovely.profile.notAvailableDescription',\n \"This style profile doesn't exist, or its owner hasn't shared it with you.\"\n )}\n icon={<UserCircle className=\"h-8 w-8\" />}\n />\n </PageLayout>\n );\n }\n\n const canEdit = isOwnProfile;\n\n // ----- View mode (own profile in view state, or any visible public profile) -----\n if ((!editing || !canEdit) && profile) {\n const avatarUrl = isOwnProfile ? currentUser?.profilePicture || currentUser?.avatar : undefined;\n const wardrobeItems = isOwnProfile ? (wardrobe.data ?? []).slice(0, 6) : [];\n const looksList = recentLooks.data?.items ?? [];\n const sizeEntries = Object.entries(profile.sizes ?? {});\n const headerName = profile.displayName || (isOwnProfile ? currentUser?.name : undefined);\n\n return (\n <PageLayout\n title={\n isOwnProfile\n ? tr('coolandlovely.page.profileTitle', 'Style Profile')\n : headerName || tr('coolandlovely.page.profileTitle', 'Style Profile')\n }\n description={\n isOwnProfile\n ? tr(\n 'coolandlovely.page.profileSubtitle',\n 'Your taste, palette and sizing — the spine of personalised discovery'\n )\n : undefined\n }\n actions={\n canEdit ? (\n <button\n type=\"button\"\n onClick={() => setEditing(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Pencil className=\"h-4 w-4\" /> {tr('coolandlovely.actions.edit', 'Edit')}\n </button>\n ) : (\n <FollowButton followeeId={profile.ownerId} />\n )\n }\n >\n <div className=\"mx-auto max-w-5xl\">\n {/* Header */}\n <GlassCard treatment=\"glass\" className=\"p-6\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center\">\n {avatarUrl ? (\n <img\n src={avatarUrl}\n alt={headerName || 'Avatar'}\n className=\"h-16 w-16 shrink-0 rounded-full object-cover ring-2 ring-bg-surface\"\n />\n ) : (\n <div className=\"flex h-16 w-16 shrink-0 items-center justify-center rounded-full bg-accent-purple-subtle text-text-primary\">\n <UserCircle className=\"h-8 w-8\" />\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h2 className=\"truncate text-xl font-semibold text-text-primary\">\n {headerName || <ActorName id={profile.ownerId} fallback=\"Style profile\" />}\n </h2>\n <VisibilityChip visibility={profile.visibility} />\n </div>\n {profile.bio && (\n <p className=\"mt-1 line-clamp-2 text-sm text-text-secondary\">{profile.bio}</p>\n )}\n <div className=\"mt-2 flex items-center gap-6\">\n <span className=\"text-sm text-text-secondary\">\n <span className=\"font-semibold text-text-primary tabular-nums\">\n {profile.followerCount ?? 0}\n </span>{' '}\n {profile.followerCount === 1\n ? tr('coolandlovely.profile.followerSingular', 'follower')\n : tr('coolandlovely.profile.followerPlural', 'followers')}\n </span>\n <span className=\"text-sm text-text-secondary\">\n <span className=\"font-semibold text-text-primary tabular-nums\">\n {profile.followingCount ?? 0}\n </span>{' '}\n {tr('coolandlovely.profile.following', 'following')}\n </span>\n </div>\n </div>\n </div>\n </GlassCard>\n\n {/* Bento */}\n <div className=\"mt-6 grid gap-6 lg:grid-cols-5\">\n {/* Left: taste detail */}\n <div className=\"space-y-6 lg:col-span-3\">\n <BentoCard title={tr('coolandlovely.profile.stylePreferences', 'Style preferences')}>\n {profile.stylePreferences.length ? (\n <div className=\"flex flex-wrap gap-2\">\n {profile.stylePreferences.map((pref, i) => (\n <span\n key={`${pref}-${i}`}\n className={cn(\n 'rounded-full px-3 py-1 text-sm font-medium',\n i === 0\n ? 'bg-accent-purple-subtle text-text-primary ring-1 ring-[var(--color-accent-soft)]'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n {pref}\n </span>\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {isOwnProfile\n ? tr(\n 'coolandlovely.profile.noPreferencesOwn',\n 'No preferences yet — add a few in Edit.'\n )\n : tr(\n 'coolandlovely.profile.noPreferencesOther',\n 'No preferences shared yet.'\n )}\n </p>\n )}\n </BentoCard>\n\n <BentoCard title={tr('coolandlovely.profile.palette', 'Palette')}>\n {profile.palette.length ? (\n <ColourSwatchRow colours={profile.palette} max={12} className=\"gap-2.5\" />\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.profile.noPalette', 'No palette yet.')}\n </p>\n )}\n </BentoCard>\n\n <div className=\"grid gap-6 sm:grid-cols-2\">\n <BentoCard title={tr('coolandlovely.profile.favouriteBrands', 'Favourite brands')}>\n {profile.favouriteBrands.length ? (\n <div className=\"grid grid-cols-1 gap-2\">\n {profile.favouriteBrands.map((brand, i) => (\n <div\n key={`${brand}-${i}`}\n className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2\"\n >\n <span className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-accent-purple-subtle text-xs font-bold text-text-primary\">\n {brandInitials(brand)}\n </span>\n <span className=\"truncate text-sm font-medium text-text-primary\">\n {brand}\n </span>\n </div>\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.profile.noBrands', 'No brands yet.')}\n </p>\n )}\n </BentoCard>\n\n {isOwnProfile && (\n <BentoCard title={tr('coolandlovely.profile.sizes', 'Sizes')}>\n {sizeEntries.length ? (\n <dl className=\"space-y-0\">\n {sizeEntries.map(([key, value]) => (\n <div\n key={key}\n className=\"flex items-center justify-between border-b border-border-seam/60 py-1.5 last:border-0\"\n >\n <dt className=\"text-sm text-text-secondary\">\n {tr(`coolandlovely.size.${key.toLowerCase()}`, titleCase(key))}\n </dt>\n <dd className=\"text-sm font-medium text-text-primary tabular-nums\">\n {String(value)}\n </dd>\n </div>\n ))}\n </dl>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.profile.noSizes', 'No sizes recorded yet.')}\n </p>\n )}\n </BentoCard>\n )}\n </div>\n </div>\n\n {/* Right: wardrobe (own profile only — self-scoped server-side) + recent looks */}\n <div className=\"space-y-6 lg:col-span-2\">\n {isOwnProfile && (\n <BentoCard title={tr('coolandlovely.profile.myWardrobe', 'My wardrobe')}>\n {wardrobe.isLoading ? (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />{' '}\n {tr('coolandlovely.loading.short', 'Loading…')}\n </div>\n ) : wardrobeItems.length ? (\n <div className=\"grid grid-cols-3 gap-2\">\n {wardrobeItems.map((w) => (\n <button\n key={w.id}\n type=\"button\"\n onClick={() => navigateTo('/wardrobe')}\n aria-label={w.name}\n className=\"group overflow-hidden rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Thumbnail\n icon={categoryIcon(w.category, 18)}\n seed={w.id}\n fileId={w.image?.id}\n alt={w.name}\n className=\"aspect-square w-full rounded-lg\"\n />\n </button>\n ))}\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => navigateTo('/wardrobe/new')}\n className=\"flex w-full items-center gap-2 rounded-lg border border-dashed border-border-seam px-3 py-3 text-left text-sm text-text-secondary transition-colors hover:text-text-primary\"\n >\n <Shirt className=\"h-4 w-4\" />{' '}\n {tr('coolandlovely.profile.addPieces', 'Add pieces to your wardrobe')}\n </button>\n )}\n </BentoCard>\n )}\n\n <BentoCard title={tr('coolandlovely.profile.recentLooks', 'Recent looks')}>\n {recentLooks.isLoading ? (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />{' '}\n {tr('coolandlovely.loading.short', 'Loading…')}\n </div>\n ) : looksList.length ? (\n <div className=\"-mx-1 flex gap-3 overflow-x-auto px-1 pb-1\">\n {looksList.map((look) => (\n <button\n key={look.id}\n type=\"button\"\n onClick={() => navigateTo(`/looks/${look.id}`)}\n className=\"group w-24 shrink-0 text-left focus-visible:outline-none\"\n >\n <Thumbnail\n icon={<Sparkles className=\"h-5 w-5\" />}\n seed={look.id}\n fileId={look.image?.id}\n alt={look.title}\n className=\"aspect-[3/4] w-full rounded-lg\"\n />\n <p className=\"mt-1 truncate text-xs text-text-secondary\">{look.title}</p>\n </button>\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {isOwnProfile\n ? tr(\n 'coolandlovely.profile.noLooksOwn',\n 'You have not published any looks yet.'\n )\n : tr('coolandlovely.profile.noLooksOther', 'No looks published yet.')}\n </p>\n )}\n </BentoCard>\n </div>\n </div>\n\n {/* People you follow (own profile only) */}\n {isOwnProfile && (\n <section className=\"mt-6\">\n <h3 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Users className=\"h-5 w-5\" />{' '}\n {tr('coolandlovely.profile.peopleYouFollow', 'People you follow')} (\n {followingList.length})\n </h3>\n {following.isLoading ? (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />{' '}\n {tr('coolandlovely.loading.short', 'Loading…')}\n </div>\n ) : followingList.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.profile.noFollowing',\n 'You are not following anyone yet. Follow creators from their looks to see their latest in Discover.'\n )}\n </p>\n ) : (\n <ul className=\"divide-y divide-border-seam rounded-lg border border-border-seam bg-bg-surface\">\n {followingList.map((p) => (\n <li key={p.id} className=\"flex items-center gap-3 px-4 py-3\">\n <button\n type=\"button\"\n onClick={() => navigateTo(`/profile/${p.ownerId}`)}\n className=\"flex min-w-0 flex-1 items-center gap-3 text-left focus-visible:outline-none\"\n >\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent-purple-subtle text-text-primary\">\n <UserCircle className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary hover:underline\">\n {p.displayName || <ActorName id={p.ownerId} />}\n </p>\n {p.bio && <p className=\"truncate text-xs text-text-secondary\">{p.bio}</p>}\n </div>\n </button>\n <FollowButton followeeId={p.ownerId} size=\"sm\" />\n </li>\n ))}\n </ul>\n )}\n </section>\n )}\n </div>\n </PageLayout>\n );\n }\n\n // ----- Edit / create mode (own profile only) -----\n const sizeKeys = Array.from(\n new Set([...SIZE_KEYS, ...Object.keys(form.sizes).map((k) => k.toLowerCase())])\n );\n\n return (\n <PageLayout\n title={\n hasProfile\n ? tr('coolandlovely.profile.editTitle', 'Edit style profile')\n : tr('coolandlovely.profile.createTitle', 'Create your style profile')\n }\n description={tr(\n 'coolandlovely.profile.editDescription',\n 'This powers your personalised discovery feed'\n )}\n actions={\n hasProfile ? (\n <button\n type=\"button\"\n onClick={() => setEditing(false)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <X className=\"h-4 w-4\" /> {tr('coolandlovely.actions.cancel', 'Cancel')}\n </button>\n ) : undefined\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <TextField\n id=\"displayName\"\n label={tr('coolandlovely.field.displayName', 'Display name')}\n value={form.displayName}\n onChange={(v) => setField('displayName', v)}\n placeholder={tr('coolandlovely.placeholder.displayName', 'How you want to appear')}\n />\n <TextAreaField\n id=\"bio\"\n label={tr('coolandlovely.field.bio', 'Bio')}\n value={form.bio}\n onChange={(v) => setField('bio', v)}\n placeholder={tr('coolandlovely.placeholder.bio', 'A sentence about your style…')}\n rows={3}\n className=\"mt-4\"\n />\n <SelectField\n id=\"visibility\"\n label={tr('coolandlovely.field.visibility', 'Visibility')}\n value={form.visibility}\n onChange={(v) => setField('visibility', v as CooVisibility)}\n options={visibilityOptions}\n className=\"mt-4\"\n />\n <TextField\n id=\"stylePreferences\"\n label={tr('coolandlovely.field.stylePreferences', 'Style preferences')}\n value={form.stylePreferences}\n onChange={(v) => setField('stylePreferences', v)}\n placeholder={tr(\n 'coolandlovely.placeholder.stylePreferences',\n 'minimal, tailored, vintage'\n )}\n hint={tr('coolandlovely.hint.commaSeparated', 'Comma-separated')}\n className=\"mt-4\"\n />\n <TextField\n id=\"palette\"\n label={tr('coolandlovely.field.palette', 'Palette')}\n value={form.palette}\n onChange={(v) => setField('palette', v)}\n placeholder={tr('coolandlovely.placeholder.palette', 'neutrals, navy, rust')}\n hint={tr(\n 'coolandlovely.hint.palette',\n 'Comma-separated colour names — shown as swatches'\n )}\n className=\"mt-4\"\n />\n <TextField\n id=\"favouriteBrands\"\n label={tr('coolandlovely.field.favouriteBrands', 'Favourite brands')}\n value={form.favouriteBrands}\n onChange={(v) => setField('favouriteBrands', v)}\n placeholder={tr('coolandlovely.placeholder.favouriteBrands', 'Aurora Atelier, …')}\n hint={tr('coolandlovely.hint.commaSeparated', 'Comma-separated')}\n className=\"mt-4\"\n />\n\n <div className=\"mt-4\">\n <span className=\"mb-2 block text-sm font-medium text-text-primary\">\n {tr('coolandlovely.profile.sizes', 'Sizes')}\n </span>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {sizeKeys.map((key) => (\n <TextField\n key={key}\n id={`size-${key}`}\n label={tr(`coolandlovely.size.${key}`, titleCase(key))}\n value={form.sizes[key] ?? form.sizes[titleCase(key)] ?? ''}\n onChange={(v) => setSize(key, v)}\n placeholder={tr('coolandlovely.placeholder.size', 'e.g. M · 26 · 39')}\n />\n ))}\n </div>\n </div>\n\n {upsert.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {upsert.error instanceof Error\n ? upsert.error.message\n : tr('coolandlovely.error.generic', 'Something went wrong')}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"submit\"\n disabled={upsert.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 {upsert.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {hasProfile\n ? tr('coolandlovely.actions.saveChanges', 'Save changes')\n : tr('coolandlovely.actions.createProfile', 'Create profile')}\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"StyleProfilePage.js","names":[],"sources":["../../../../src/coolandlovely/pages/profile/StyleProfilePage.tsx"],"sourcesContent":["/**\n * Style Profile Page\n * @module coolandlovely/pages/profile\n *\n * Two routes share this component:\n * - `/profile` — the calling shopper's own profile (`cooMyStyleProfile` +\n * `cooUpsertStyleProfile`), editable, with a self-scoped wardrobe preview\n * and \"people you follow\" list.\n * - `/profile/:userId` — any other shopper's PUBLIC profile\n * (`cooStyleProfileByOwner`), reached by clicking an author's name/avatar\n * on a Look/Discover/Collaboration card. Read-only: no edit affordance, no\n * wardrobe preview (wardrobe stays self-scoped server-side), and shows a\n * Follow button instead. Server-side CooVisibility (PRIVATE/FOLLOWERS)\n * gates whether the profile resolves at all.\n */\n\nimport { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { Loader2, Save, UserCircle, Pencil, X, Users, Sparkles, Shirt } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { EmptyState } from '../../components/EmptyState';\nimport { TextField, TextAreaField, SelectField } from '../../components/Field';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { ColourSwatchRow } from '../../components/ColourSwatchRow';\nimport { VisibilityChip } from '../../components/VisibilityChip';\nimport { ActorName } from '../../components/ActorName';\nimport { FollowButton } from '../../components/FollowButton';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport {\n useMyStyleProfile,\n useStyleProfileByOwner,\n useUpsertStyleProfile,\n useMyFollowing,\n useWardrobeItems,\n useLooks,\n useTr,\n} from '../../hooks';\nimport { categoryIcon } from '../../utils/icons';\nimport { parseCsv, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooVisibility } from '../../types';\n\nconst VISIBILITIES: CooVisibility[] = ['PUBLIC', 'FOLLOWERS', 'PRIVATE'];\n\n/** Common size rows offered in the editor (existing custom keys are preserved). */\nconst SIZE_KEYS = ['tops', 'bottoms', 'dresses', 'shoes', 'outerwear'];\n\ninterface FormState {\n displayName: string;\n bio: string;\n visibility: CooVisibility;\n stylePreferences: string;\n palette: string;\n favouriteBrands: string;\n sizes: Record<string, string>;\n}\n\nconst EMPTY: FormState = {\n displayName: '',\n bio: '',\n visibility: 'PRIVATE',\n stylePreferences: '',\n palette: '',\n favouriteBrands: '',\n sizes: {},\n};\n\n/** Monogram from a free-text brand name (real brand logos are unavailable). */\nfunction brandInitials(name: string): string {\n const words = name.trim().split(/\\s+/).filter(Boolean);\n if (words.length === 0) return '?';\n if (words.length === 1) return words[0].slice(0, 2).toUpperCase();\n return (words[0][0] + words[1][0]).toUpperCase();\n}\n\nfunction BentoCard({\n title,\n children,\n className,\n}: {\n title: string;\n children: React.ReactNode;\n className?: string;\n}) {\n return (\n <GlassCard treatment=\"plain\" className={cn('p-5', className)}>\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">{title}</h3>\n {children}\n </GlassCard>\n );\n}\n\nexport function StyleProfilePage() {\n const { userId } = useParams<{ userId?: string }>();\n const { navigateTo, currentUser } = useCoolAndLovely();\n const tr = useTr();\n const isOwnProfile = !userId || userId === currentUser?.id;\n\n const myQuery = useMyStyleProfile();\n const otherQuery = useStyleProfileByOwner(isOwnProfile ? undefined : userId);\n const query = isOwnProfile ? myQuery : otherQuery;\n\n const upsert = useUpsertStyleProfile();\n // Both self-scoped — meaningful only on the caller's own profile page, but\n // called unconditionally (rules of hooks) and simply not rendered otherwise.\n const following = useMyFollowing();\n const wardrobe = useWardrobeItems();\n\n const profile = query.data;\n const recentLooks = useLooks({\n authorId: profile?.ownerId,\n pagination: { limit: 6, offset: 0 },\n });\n\n const [editing, setEditing] = useState(false);\n const [form, setForm] = useState<FormState>(EMPTY);\n\n const hasProfile = Boolean(profile);\n const followingList = following.data ?? [];\n\n useEffect(() => {\n if (isOwnProfile && profile) {\n setForm({\n displayName: profile.displayName ?? '',\n bio: profile.bio ?? '',\n visibility: profile.visibility,\n stylePreferences: profile.stylePreferences.join(', '),\n palette: profile.palette.join(', '),\n favouriteBrands: profile.favouriteBrands.join(', '),\n sizes: Object.fromEntries(\n Object.entries(profile.sizes ?? {}).map(([k, v]) => [k, String(v ?? '')])\n ),\n });\n }\n }, [isOwnProfile, profile]);\n\n // If there is no profile yet, drop straight into edit mode — own profile only.\n useEffect(() => {\n if (isOwnProfile && !query.isLoading && !hasProfile) {\n setEditing(true);\n }\n }, [isOwnProfile, query.isLoading, hasProfile]);\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((f) => ({ ...f, [key]: value }));\n\n const setSize = (key: string, value: string) =>\n setForm((f) => ({ ...f, sizes: { ...f.sizes, [key]: value } }));\n\n const visibilityOptions = useMemo(\n () =>\n VISIBILITIES.map((v) => ({\n value: v,\n label: tr(`coolandlovely.visibility.${v.toLowerCase()}`, titleCase(v)),\n })),\n [tr]\n );\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n const cleanedSizes = Object.fromEntries(\n Object.entries(form.sizes)\n .map(([k, v]) => [k, v.trim()] as const)\n .filter(([, v]) => v.length > 0)\n );\n upsert.mutate(\n {\n displayName: form.displayName.trim() || undefined,\n bio: form.bio.trim() || undefined,\n visibility: form.visibility,\n stylePreferences: parseCsv(form.stylePreferences),\n palette: parseCsv(form.palette),\n favouriteBrands: parseCsv(form.favouriteBrands),\n sizes: cleanedSizes,\n },\n { onSuccess: () => setEditing(false) }\n );\n };\n\n if (query.isLoading) {\n return (\n <PageLayout title={tr('coolandlovely.page.profileTitle', 'Style Profile')}>\n <LoadingView label={tr('coolandlovely.loading.profile', 'Loading style profile…')} />\n </PageLayout>\n );\n }\n\n if (query.isError) {\n return (\n <PageLayout title={tr('coolandlovely.page.profileTitle', 'Style Profile')}>\n <ErrorView onRetry={() => void query.refetch()} />\n </PageLayout>\n );\n }\n\n // Public view of someone else's profile that does not exist, or whose\n // CooVisibility (PRIVATE / FOLLOWERS-not-following) hides it from us — the\n // svc returns null in both cases so a caller cannot tell them apart.\n if (!isOwnProfile && !profile) {\n return (\n <PageLayout title={tr('coolandlovely.page.profileTitle', 'Style Profile')}>\n <EmptyState\n title={tr('coolandlovely.profile.notAvailableTitle', 'Profile not available')}\n description={tr(\n 'coolandlovely.profile.notAvailableDescription',\n \"This style profile doesn't exist, or its owner hasn't shared it with you.\"\n )}\n icon={<UserCircle className=\"h-8 w-8\" />}\n />\n </PageLayout>\n );\n }\n\n const canEdit = isOwnProfile;\n\n // ----- View mode (own profile in view state, or any visible public profile) -----\n if ((!editing || !canEdit) && profile) {\n const avatarUrl = isOwnProfile ? currentUser?.profilePicture || currentUser?.avatar : undefined;\n const wardrobeItems = isOwnProfile ? (wardrobe.data ?? []).slice(0, 6) : [];\n const looksList = recentLooks.data?.items ?? [];\n const sizeEntries = Object.entries(profile.sizes ?? {});\n const headerName = profile.displayName || (isOwnProfile ? currentUser?.name : undefined);\n\n return (\n <PageLayout\n title={\n isOwnProfile\n ? tr('coolandlovely.page.profileTitle', 'Style Profile')\n : headerName || tr('coolandlovely.page.profileTitle', 'Style Profile')\n }\n description={\n isOwnProfile\n ? tr(\n 'coolandlovely.page.profileSubtitle',\n 'Your taste, palette and sizing — the spine of personalised discovery'\n )\n : undefined\n }\n actions={\n canEdit ? (\n <button\n type=\"button\"\n onClick={() => setEditing(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Pencil className=\"h-4 w-4\" /> {tr('coolandlovely.actions.edit', 'Edit')}\n </button>\n ) : (\n <FollowButton followeeId={profile.ownerId} />\n )\n }\n >\n <div className=\"mx-auto max-w-5xl\">\n {/* Header */}\n <GlassCard treatment=\"glass\" className=\"p-6\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center\">\n {avatarUrl ? (\n <img\n src={avatarUrl}\n alt={headerName || 'Avatar'}\n className=\"h-16 w-16 shrink-0 rounded-full object-cover ring-2 ring-bg-surface\"\n />\n ) : (\n <div className=\"flex h-16 w-16 shrink-0 items-center justify-center rounded-full bg-accent-purple-subtle text-text-primary\">\n <UserCircle className=\"h-8 w-8\" />\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h2 className=\"truncate text-xl font-semibold text-text-primary\">\n {headerName || <ActorName id={profile.ownerId} fallback=\"Style profile\" />}\n </h2>\n <VisibilityChip visibility={profile.visibility} />\n </div>\n {profile.bio && (\n <p className=\"mt-1 line-clamp-2 text-sm text-text-secondary\">{profile.bio}</p>\n )}\n <div className=\"mt-2 flex items-center gap-6\">\n <span className=\"text-sm text-text-secondary\">\n <span className=\"font-semibold text-text-primary tabular-nums\">\n {profile.followerCount ?? 0}\n </span>{' '}\n {profile.followerCount === 1\n ? tr('coolandlovely.profile.followerSingular', 'follower')\n : tr('coolandlovely.profile.followerPlural', 'followers')}\n </span>\n <span className=\"text-sm text-text-secondary\">\n <span className=\"font-semibold text-text-primary tabular-nums\">\n {profile.followingCount ?? 0}\n </span>{' '}\n {tr('coolandlovely.profile.following', 'following')}\n </span>\n </div>\n </div>\n </div>\n </GlassCard>\n\n {/* Bento */}\n <div className=\"mt-6 grid gap-6 lg:grid-cols-5\">\n {/* Left: taste detail */}\n <div className=\"space-y-6 lg:col-span-3\">\n <BentoCard title={tr('coolandlovely.profile.stylePreferences', 'Style preferences')}>\n {profile.stylePreferences.length ? (\n <div className=\"flex flex-wrap gap-2\">\n {profile.stylePreferences.map((pref, i) => (\n <span\n key={`${pref}-${i}`}\n className={cn(\n 'rounded-full px-3 py-1 text-sm font-medium',\n i === 0\n ? 'bg-accent-purple-subtle text-text-primary ring-1 ring-[var(--color-accent-soft)]'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n {pref}\n </span>\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {isOwnProfile\n ? tr(\n 'coolandlovely.profile.noPreferencesOwn',\n 'No preferences yet — add a few in Edit.'\n )\n : tr(\n 'coolandlovely.profile.noPreferencesOther',\n 'No preferences shared yet.'\n )}\n </p>\n )}\n </BentoCard>\n\n <BentoCard title={tr('coolandlovely.profile.palette', 'Palette')}>\n {profile.palette.length ? (\n <ColourSwatchRow colours={profile.palette} max={12} className=\"gap-2.5\" />\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.profile.noPalette', 'No palette yet.')}\n </p>\n )}\n </BentoCard>\n\n <div className=\"grid gap-6 sm:grid-cols-2\">\n <BentoCard title={tr('coolandlovely.profile.favouriteBrands', 'Favourite brands')}>\n {profile.favouriteBrands.length ? (\n <div className=\"grid grid-cols-1 gap-2\">\n {profile.favouriteBrands.map((brand, i) => (\n <div\n key={`${brand}-${i}`}\n className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2\"\n >\n <span className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-accent-purple-subtle text-xs font-bold text-text-primary\">\n {brandInitials(brand)}\n </span>\n <span className=\"truncate text-sm font-medium text-text-primary\">\n {brand}\n </span>\n </div>\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.profile.noBrands', 'No brands yet.')}\n </p>\n )}\n </BentoCard>\n\n {isOwnProfile && (\n <BentoCard title={tr('coolandlovely.profile.sizes', 'Sizes')}>\n {sizeEntries.length ? (\n <dl className=\"space-y-0\">\n {sizeEntries.map(([key, value]) => (\n <div\n key={key}\n className=\"flex items-center justify-between border-b border-border-seam/60 py-1.5 last:border-0\"\n >\n <dt className=\"text-sm text-text-secondary\">\n {tr(`coolandlovely.size.${key.toLowerCase()}`, titleCase(key))}\n </dt>\n <dd className=\"text-sm font-medium text-text-primary tabular-nums\">\n {String(value)}\n </dd>\n </div>\n ))}\n </dl>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {tr('coolandlovely.profile.noSizes', 'No sizes recorded yet.')}\n </p>\n )}\n </BentoCard>\n )}\n </div>\n </div>\n\n {/* Right: wardrobe (own profile only — self-scoped server-side) + recent looks */}\n <div className=\"space-y-6 lg:col-span-2\">\n {isOwnProfile && (\n <BentoCard title={tr('coolandlovely.profile.myWardrobe', 'My wardrobe')}>\n {wardrobe.isLoading ? (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />{' '}\n {tr('coolandlovely.loading.short', 'Loading…')}\n </div>\n ) : wardrobeItems.length ? (\n <div className=\"grid grid-cols-3 gap-2\">\n {wardrobeItems.map((w) => (\n <button\n key={w.id}\n type=\"button\"\n onClick={() => navigateTo('/wardrobe')}\n aria-label={w.name}\n className=\"group overflow-hidden rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Thumbnail\n icon={categoryIcon(w.category, 18)}\n seed={w.id}\n fileId={w.image?.id}\n alt={w.name}\n className=\"aspect-square w-full rounded-lg\"\n />\n </button>\n ))}\n </div>\n ) : (\n <button\n type=\"button\"\n onClick={() => navigateTo('/wardrobe/new')}\n className=\"flex w-full items-center gap-2 rounded-lg border border-dashed border-border-seam px-3 py-3 text-left text-sm text-text-secondary transition-colors hover:text-text-primary\"\n >\n <Shirt className=\"h-4 w-4\" />{' '}\n {tr('coolandlovely.profile.addPieces', 'Add pieces to your wardrobe')}\n </button>\n )}\n </BentoCard>\n )}\n\n <BentoCard title={tr('coolandlovely.profile.recentLooks', 'Recent looks')}>\n {recentLooks.isLoading ? (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />{' '}\n {tr('coolandlovely.loading.short', 'Loading…')}\n </div>\n ) : looksList.length ? (\n <div className=\"-mx-1 flex gap-3 overflow-x-auto px-1 pb-1\">\n {looksList.map((look) => (\n <button\n key={look.id}\n type=\"button\"\n onClick={() => navigateTo(`/looks/${look.id}`)}\n className=\"group w-24 shrink-0 text-left focus-visible:outline-none\"\n >\n <Thumbnail\n icon={<Sparkles className=\"h-5 w-5\" />}\n seed={look.id}\n fileId={look.image?.id}\n alt={look.title}\n className=\"aspect-[3/4] w-full rounded-lg\"\n />\n <p className=\"mt-1 truncate text-xs text-text-secondary\">{look.title}</p>\n </button>\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {isOwnProfile\n ? tr(\n 'coolandlovely.profile.noLooksOwn',\n 'You have not published any looks yet.'\n )\n : tr('coolandlovely.profile.noLooksOther', 'No looks published yet.')}\n </p>\n )}\n </BentoCard>\n </div>\n </div>\n\n {/* People you follow (own profile only) */}\n {isOwnProfile && (\n <section className=\"mt-6\">\n <h3 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Users className=\"h-5 w-5\" />{' '}\n {tr('coolandlovely.profile.peopleYouFollow', 'People you follow')} (\n {followingList.length})\n </h3>\n {following.isLoading ? (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />{' '}\n {tr('coolandlovely.loading.short', 'Loading…')}\n </div>\n ) : followingList.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n {tr(\n 'coolandlovely.profile.noFollowing',\n 'You are not following anyone yet. Follow creators from their looks to see their latest in Discover.'\n )}\n </p>\n ) : (\n <ul className=\"divide-y divide-border-seam rounded-lg border border-border-seam bg-bg-surface\">\n {followingList.map((p) => (\n <li key={p.id} className=\"flex items-center gap-3 px-4 py-3\">\n <button\n type=\"button\"\n onClick={() => navigateTo(`/profile/${p.ownerId}`)}\n className=\"flex min-w-0 flex-1 items-center gap-3 text-left focus-visible:outline-none\"\n >\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent-purple-subtle text-text-primary\">\n <UserCircle className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary hover:underline\">\n {p.displayName || <ActorName id={p.ownerId} />}\n </p>\n {p.bio && <p className=\"truncate text-xs text-text-secondary\">{p.bio}</p>}\n </div>\n </button>\n <FollowButton followeeId={p.ownerId} size=\"sm\" />\n </li>\n ))}\n </ul>\n )}\n </section>\n )}\n </div>\n </PageLayout>\n );\n }\n\n // ----- Edit / create mode (own profile only) -----\n const sizeKeys = Array.from(\n new Set([...SIZE_KEYS, ...Object.keys(form.sizes).map((k) => k.toLowerCase())])\n );\n\n return (\n <PageLayout\n title={\n hasProfile\n ? tr('coolandlovely.profile.editTitle', 'Edit style profile')\n : tr('coolandlovely.profile.createTitle', 'Create your style profile')\n }\n description={tr(\n 'coolandlovely.profile.editDescription',\n 'This powers your personalised discovery feed'\n )}\n actions={\n hasProfile ? (\n <button\n type=\"button\"\n onClick={() => setEditing(false)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <X className=\"h-4 w-4\" /> {tr('coolandlovely.actions.cancel', 'Cancel')}\n </button>\n ) : undefined\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <TextField\n id=\"displayName\"\n label={tr('coolandlovely.field.displayName', 'Display name')}\n value={form.displayName}\n onChange={(v) => setField('displayName', v)}\n placeholder={tr('coolandlovely.placeholder.displayName', 'How you want to appear')}\n />\n <TextAreaField\n id=\"bio\"\n label={tr('coolandlovely.field.bio', 'Bio')}\n value={form.bio}\n onChange={(v) => setField('bio', v)}\n placeholder={tr('coolandlovely.placeholder.bio', 'A sentence about your style…')}\n rows={3}\n className=\"mt-4\"\n />\n <SelectField\n id=\"visibility\"\n label={tr('coolandlovely.field.visibility', 'Visibility')}\n value={form.visibility}\n onChange={(v) => setField('visibility', v as CooVisibility)}\n options={visibilityOptions}\n className=\"mt-4\"\n />\n <TextField\n id=\"stylePreferences\"\n label={tr('coolandlovely.field.stylePreferences', 'Style preferences')}\n value={form.stylePreferences}\n onChange={(v) => setField('stylePreferences', v)}\n placeholder={tr(\n 'coolandlovely.placeholder.stylePreferences',\n 'minimal, tailored, vintage'\n )}\n hint={tr('coolandlovely.hint.commaSeparated', 'Comma-separated')}\n className=\"mt-4\"\n />\n <TextField\n id=\"palette\"\n label={tr('coolandlovely.field.palette', 'Palette')}\n value={form.palette}\n onChange={(v) => setField('palette', v)}\n placeholder={tr('coolandlovely.placeholder.palette', 'neutrals, navy, rust')}\n hint={tr(\n 'coolandlovely.hint.palette',\n 'Comma-separated colour names — shown as swatches'\n )}\n className=\"mt-4\"\n />\n <TextField\n id=\"favouriteBrands\"\n label={tr('coolandlovely.field.favouriteBrands', 'Favourite brands')}\n value={form.favouriteBrands}\n onChange={(v) => setField('favouriteBrands', v)}\n placeholder={tr('coolandlovely.placeholder.favouriteBrands', 'Aurora Atelier, …')}\n hint={tr('coolandlovely.hint.commaSeparated', 'Comma-separated')}\n className=\"mt-4\"\n />\n\n <div className=\"mt-4\">\n <span className=\"mb-2 block text-sm font-medium text-text-primary\">\n {tr('coolandlovely.profile.sizes', 'Sizes')}\n </span>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {sizeKeys.map((key) => (\n <TextField\n key={key}\n id={`size-${key}`}\n label={tr(`coolandlovely.size.${key}`, titleCase(key))}\n value={form.sizes[key] ?? form.sizes[titleCase(key)] ?? ''}\n onChange={(v) => setSize(key, v)}\n placeholder={tr('coolandlovely.placeholder.size', 'e.g. M · 26 · 39')}\n />\n ))}\n </div>\n </div>\n\n {upsert.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {upsert.error instanceof Error\n ? upsert.error.message\n : tr('coolandlovely.error.generic', 'Something went wrong')}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"submit\"\n disabled={upsert.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 {upsert.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {hasProfile\n ? tr('coolandlovely.actions.saveChanges', 'Save changes')\n : tr('coolandlovely.actions.createProfile', 'Create profile')}\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TrendsPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/trends/TrendsPage.tsx"],"sourcesContent":["/**\n * Trends Page\n * @module coolandlovely/pages/trends\n *\n * Client-side analytics computed over the real catalogue + social lists\n * (cooListBrands / cooListStyleItems / cooListLooks). Honest by construction —\n * it only counts and ranks what is actually on the platform. No fabricated\n * metrics. Header labels it \"Signals from platform activity.\"\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useMemo } from 'react';\nimport {\n TrendingUp,\n Palette,\n Shapes,\n Sparkles,\n Store,\n Shirt,\n Heart,\n CalendarHeart,\n} from 'lucide-react';\nimport { EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrands, useStyleItems, useLooks } from '../../hooks';\nimport { titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\n\ninterface Ranked {\n label: string;\n value: number;\n sublabel?: string;\n}\n\nfunction rank(map: Map<string, number>, top = 8): Ranked[] {\n return [...map.entries()]\n .map(([label, value]) => ({ label, value }))\n .sort((a, b) => b.value - a.value)\n .slice(0, top);\n}\n\ninterface StatBarsProps {\n title: string;\n icon: ReactNode;\n items: Ranked[];\n unit: string;\n emptyLabel: string;\n className?: string;\n}\n\nconst StatBars: FC<StatBarsProps> = ({ title, icon, items, unit, emptyLabel, className }) => {\n const max = items.reduce((m, i) => Math.max(m, i.value), 0) || 1;\n return (\n <GlassCard treatment=\"plain\" className={cn('p-5', className)}>\n <h3 className=\"mb-4 flex items-center gap-2 text-base font-semibold text-text-primary\">\n {icon} {title}\n </h3>\n {items.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">{emptyLabel}</p>\n ) : (\n <ul className=\"space-y-3\">\n {items.map((item) => (\n <li key={item.label}>\n <div className=\"mb-1 flex items-center justify-between gap-2 text-sm\">\n <span className=\"truncate text-text-primary\">{item.label}</span>\n <span className=\"shrink-0 tabular-nums text-text-secondary\">\n {item.value.toLocaleString()} {unit}\n </span>\n </div>\n <div\n className=\"h-2 w-full overflow-hidden rounded-full bg-bg-sunken\"\n role=\"img\"\n aria-label={`${item.label}: ${item.value} ${unit}`}\n >\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${Math.max(4, Math.round((item.value / max) * 100))}%` }}\n />\n </div>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n );\n};\n\nexport function TrendsPage() {\n useCoolAndLovely();\n const brands = useBrands({ pagination: { limit: 100, offset: 0 } });\n const items = useStyleItems({ pagination: { limit: 100, offset: 0 } });\n const looks = useLooks({ pagination: { limit: 100, offset: 0 } });\n\n const isLoading = brands.isLoading || items.isLoading || looks.isLoading;\n const isError = brands.isError && items.isError && looks.isError;\n\n const analysis = useMemo(() => {\n const brandRows = brands.data?.items ?? [];\n const itemRows = items.data?.items ?? [];\n const lookRows = looks.data?.items ?? [];\n\n const categoryMap = new Map<string, number>();\n const colourMap = new Map<string, number>();\n for (const it of itemRows) {\n categoryMap.set(titleCase(it.category), (categoryMap.get(titleCase(it.category)) ?? 0) + 1);\n for (const c of it.colours) {\n const key = titleCase(c);\n colourMap.set(key, (colourMap.get(key) ?? 0) + 1);\n }\n }\n\n const aestheticMap = new Map<string, number>();\n for (const b of brandRows) {\n for (const a of b.aesthetics) {\n const key = titleCase(a);\n aestheticMap.set(key, (aestheticMap.get(key) ?? 0) + 1);\n }\n }\n\n const occasionEngagement = new Map<string, number>();\n let totalLikes = 0;\n let totalSaves = 0;\n for (const l of lookRows) {\n const eng = l.likeCount + l.saveCount;\n const key = titleCase(l.occasion);\n occasionEngagement.set(key, (occasionEngagement.get(key) ?? 0) + eng);\n totalLikes += l.likeCount;\n totalSaves += l.saveCount;\n }\n\n const topLooks = [...lookRows]\n .sort((a, b) => b.likeCount + b.saveCount - (a.likeCount + a.saveCount))\n .slice(0, 5);\n\n return {\n brandCount: brandRows.length,\n itemCount: itemRows.length,\n lookCount: lookRows.length,\n totalLikes,\n totalSaves,\n topCategories: rank(categoryMap, 8),\n topColours: rank(colourMap, 8),\n topAesthetics: rank(aestheticMap, 8),\n risingOccasions: rank(occasionEngagement, 8),\n topLooks,\n };\n }, [brands.data, items.data, looks.data]);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Trends\">\n <LoadingView label=\"Reading platform signals…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title=\"Trends\">\n <ErrorView\n onRetry={() => {\n void brands.refetch();\n void items.refetch();\n void looks.refetch();\n }}\n />\n </PageLayout>\n );\n }\n\n const metrics = [\n { icon: <Store className=\"h-5 w-5\" />, value: analysis.brandCount, label: 'Brands sampled' },\n { icon: <Shirt className=\"h-5 w-5\" />, value: analysis.itemCount, label: 'Items sampled' },\n { icon: <Sparkles className=\"h-5 w-5\" />, value: analysis.lookCount, label: 'Looks sampled' },\n {\n icon: <Heart className=\"h-5 w-5\" />,\n value: analysis.totalLikes + analysis.totalSaves,\n label: 'Total engagement',\n },\n ];\n\n return (\n <PageLayout title=\"Trends\" description=\"Signals from platform activity\" hero>\n <PagePurpose className=\"mb-6\">\n These are honest, client-side signals computed from the pieces, brands and looks currently\n on CoolAndLovely — the most-listed categories and colours, the aesthetics brands describe\n themselves with, and which occasions are drawing the most likes and saves. Nothing here is\n fabricated; it is a live read of the catalogue and social activity.\n </PagePurpose>\n\n <EmphasisPanel className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">At a glance</h2>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n {metrics.map((m) => (\n <MetricsCard\n key={m.label}\n icon={m.icon}\n value={m.value.toLocaleString()}\n label={m.label}\n />\n ))}\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <StatBars\n title=\"Top categories\"\n icon={<Shapes className=\"h-5 w-5\" />}\n items={analysis.topCategories}\n unit=\"items\"\n emptyLabel=\"No style items yet.\"\n />\n <StatBars\n title=\"Most-listed colours\"\n icon={<Palette className=\"h-5 w-5\" />}\n items={analysis.topColours}\n unit=\"items\"\n emptyLabel=\"No colours recorded on items yet.\"\n />\n <StatBars\n title=\"Brand aesthetics\"\n icon={<Store className=\"h-5 w-5\" />}\n items={analysis.topAesthetics}\n unit=\"brands\"\n emptyLabel=\"No brand aesthetics recorded yet.\"\n />\n <StatBars\n title=\"Rising occasions\"\n icon={<CalendarHeart className=\"h-5 w-5\" />}\n items={analysis.risingOccasions}\n unit=\"engagement\"\n emptyLabel=\"No looks published yet.\"\n />\n </div>\n\n <section className=\"mt-8\">\n <h2 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <TrendingUp className=\"h-5 w-5\" /> Most-loved looks\n </h2>\n {analysis.topLooks.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No looks published yet.\n </p>\n ) : (\n <div className=\"divide-y divide-border-seam rounded-lg border border-border-seam bg-bg-surface\">\n {analysis.topLooks.map((look, idx) => (\n <div key={look.id} className=\"flex items-center gap-4 px-4 py-3\">\n <span className=\"w-6 shrink-0 text-center text-sm font-semibold tabular-nums text-text-secondary\">\n {idx + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{look.title}</p>\n <p className=\"text-xs text-text-secondary\">{titleCase(look.occasion)}</p>\n </div>\n <span className=\"flex shrink-0 items-center gap-3 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1\">\n <Heart className=\"h-3.5 w-3.5\" /> {look.likeCount}\n </span>\n <span className=\"tabular-nums\">{look.saveCount} saves</span>\n </span>\n </div>\n ))}\n </div>\n )}\n </section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TrendsPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/trends/TrendsPage.tsx"],"sourcesContent":["/**\n * Trends Page\n * @module coolandlovely/pages/trends\n *\n * Client-side analytics computed over the real catalogue + social lists\n * (cooListBrands / cooListStyleItems / cooListLooks). Honest by construction —\n * it only counts and ranks what is actually on the platform. No fabricated\n * metrics. Header labels it \"Signals from platform activity.\"\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useMemo } from 'react';\nimport {\n TrendingUp,\n Palette,\n Shapes,\n Sparkles,\n Store,\n Shirt,\n Heart,\n CalendarHeart,\n} from 'lucide-react';\nimport { EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrands, useStyleItems, useLooks } from '../../hooks';\nimport { titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\n\ninterface Ranked {\n label: string;\n value: number;\n sublabel?: string;\n}\n\nfunction rank(map: Map<string, number>, top = 8): Ranked[] {\n return [...map.entries()]\n .map(([label, value]) => ({ label, value }))\n .sort((a, b) => b.value - a.value)\n .slice(0, top);\n}\n\ninterface StatBarsProps {\n title: string;\n icon: ReactNode;\n items: Ranked[];\n unit: string;\n emptyLabel: string;\n className?: string;\n}\n\nconst StatBars: FC<StatBarsProps> = ({ title, icon, items, unit, emptyLabel, className }) => {\n const max = items.reduce((m, i) => Math.max(m, i.value), 0) || 1;\n return (\n <GlassCard treatment=\"plain\" className={cn('p-5', className)}>\n <h3 className=\"mb-4 flex items-center gap-2 text-base font-semibold text-text-primary\">\n {icon} {title}\n </h3>\n {items.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">{emptyLabel}</p>\n ) : (\n <ul className=\"space-y-3\">\n {items.map((item) => (\n <li key={item.label}>\n <div className=\"mb-1 flex items-center justify-between gap-2 text-sm\">\n <span className=\"truncate text-text-primary\">{item.label}</span>\n <span className=\"shrink-0 tabular-nums text-text-secondary\">\n {item.value.toLocaleString()} {unit}\n </span>\n </div>\n <div\n className=\"h-2 w-full overflow-hidden rounded-full bg-bg-sunken\"\n role=\"img\"\n aria-label={`${item.label}: ${item.value} ${unit}`}\n >\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${Math.max(4, Math.round((item.value / max) * 100))}%` }}\n />\n </div>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n );\n};\n\nexport function TrendsPage() {\n useCoolAndLovely();\n const brands = useBrands({ pagination: { limit: 100, offset: 0 } });\n const items = useStyleItems({ pagination: { limit: 100, offset: 0 } });\n const looks = useLooks({ pagination: { limit: 100, offset: 0 } });\n\n const isLoading = brands.isLoading || items.isLoading || looks.isLoading;\n const isError = brands.isError && items.isError && looks.isError;\n\n const analysis = useMemo(() => {\n const brandRows = brands.data?.items ?? [];\n const itemRows = items.data?.items ?? [];\n const lookRows = looks.data?.items ?? [];\n\n const categoryMap = new Map<string, number>();\n const colourMap = new Map<string, number>();\n for (const it of itemRows) {\n categoryMap.set(titleCase(it.category), (categoryMap.get(titleCase(it.category)) ?? 0) + 1);\n for (const c of it.colours) {\n const key = titleCase(c);\n colourMap.set(key, (colourMap.get(key) ?? 0) + 1);\n }\n }\n\n const aestheticMap = new Map<string, number>();\n for (const b of brandRows) {\n for (const a of b.aesthetics) {\n const key = titleCase(a);\n aestheticMap.set(key, (aestheticMap.get(key) ?? 0) + 1);\n }\n }\n\n const occasionEngagement = new Map<string, number>();\n let totalLikes = 0;\n let totalSaves = 0;\n for (const l of lookRows) {\n const eng = l.likeCount + l.saveCount;\n const key = titleCase(l.occasion);\n occasionEngagement.set(key, (occasionEngagement.get(key) ?? 0) + eng);\n totalLikes += l.likeCount;\n totalSaves += l.saveCount;\n }\n\n const topLooks = [...lookRows]\n .sort((a, b) => b.likeCount + b.saveCount - (a.likeCount + a.saveCount))\n .slice(0, 5);\n\n return {\n brandCount: brandRows.length,\n itemCount: itemRows.length,\n lookCount: lookRows.length,\n totalLikes,\n totalSaves,\n topCategories: rank(categoryMap, 8),\n topColours: rank(colourMap, 8),\n topAesthetics: rank(aestheticMap, 8),\n risingOccasions: rank(occasionEngagement, 8),\n topLooks,\n };\n }, [brands.data, items.data, looks.data]);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Trends\">\n <LoadingView label=\"Reading platform signals…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title=\"Trends\">\n <ErrorView\n onRetry={() => {\n void brands.refetch();\n void items.refetch();\n void looks.refetch();\n }}\n />\n </PageLayout>\n );\n }\n\n const metrics = [\n { icon: <Store className=\"h-5 w-5\" />, value: analysis.brandCount, label: 'Brands sampled' },\n { icon: <Shirt className=\"h-5 w-5\" />, value: analysis.itemCount, label: 'Items sampled' },\n { icon: <Sparkles className=\"h-5 w-5\" />, value: analysis.lookCount, label: 'Looks sampled' },\n {\n icon: <Heart className=\"h-5 w-5\" />,\n value: analysis.totalLikes + analysis.totalSaves,\n label: 'Total engagement',\n },\n ];\n\n return (\n <PageLayout title=\"Trends\" description=\"Signals from platform activity\" hero>\n <PagePurpose className=\"mb-6\">\n These are honest, client-side signals computed from the pieces, brands and looks currently\n on CoolAndLovely — the most-listed categories and colours, the aesthetics brands describe\n themselves with, and which occasions are drawing the most likes and saves. Nothing here is\n fabricated; it is a live read of the catalogue and social activity.\n </PagePurpose>\n\n <EmphasisPanel className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">At a glance</h2>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n {metrics.map((m) => (\n <MetricsCard\n key={m.label}\n icon={m.icon}\n value={m.value.toLocaleString()}\n label={m.label}\n />\n ))}\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <StatBars\n title=\"Top categories\"\n icon={<Shapes className=\"h-5 w-5\" />}\n items={analysis.topCategories}\n unit=\"items\"\n emptyLabel=\"No style items yet.\"\n />\n <StatBars\n title=\"Most-listed colours\"\n icon={<Palette className=\"h-5 w-5\" />}\n items={analysis.topColours}\n unit=\"items\"\n emptyLabel=\"No colours recorded on items yet.\"\n />\n <StatBars\n title=\"Brand aesthetics\"\n icon={<Store className=\"h-5 w-5\" />}\n items={analysis.topAesthetics}\n unit=\"brands\"\n emptyLabel=\"No brand aesthetics recorded yet.\"\n />\n <StatBars\n title=\"Rising occasions\"\n icon={<CalendarHeart className=\"h-5 w-5\" />}\n items={analysis.risingOccasions}\n unit=\"engagement\"\n emptyLabel=\"No looks published yet.\"\n />\n </div>\n\n <section className=\"mt-8\">\n <h2 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <TrendingUp className=\"h-5 w-5\" /> Most-loved looks\n </h2>\n {analysis.topLooks.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No looks published yet.\n </p>\n ) : (\n <div className=\"divide-y divide-border-seam rounded-lg border border-border-seam bg-bg-surface\">\n {analysis.topLooks.map((look, idx) => (\n <div key={look.id} className=\"flex items-center gap-4 px-4 py-3\">\n <span className=\"w-6 shrink-0 text-center text-sm font-semibold tabular-nums text-text-secondary\">\n {idx + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{look.title}</p>\n <p className=\"text-xs text-text-secondary\">{titleCase(look.occasion)}</p>\n </div>\n <span className=\"flex shrink-0 items-center gap-3 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1\">\n <Heart className=\"h-3.5 w-3.5\" /> {look.likeCount}\n </span>\n <span className=\"tabular-nums\">{look.saveCount} saves</span>\n </span>\n </div>\n ))}\n </div>\n )}\n </section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}