@burdenoff/microfe-coolandlovely 2026.706.1 → 2026.706.3

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 (89) hide show
  1. package/dist/coolandlovely/CoolAndLovelyRoot.js +62 -46
  2. package/dist/coolandlovely/CoolAndLovelyRoot.js.map +1 -1
  3. package/dist/coolandlovely/CoolAndLovelyRoutes.js +14 -6
  4. package/dist/coolandlovely/CoolAndLovelyRoutes.js.map +1 -1
  5. package/dist/coolandlovely/components/ActorName.js +29 -0
  6. package/dist/coolandlovely/components/ActorName.js.map +1 -0
  7. package/dist/coolandlovely/components/AddToLookbookButton.js +81 -0
  8. package/dist/coolandlovely/components/AddToLookbookButton.js.map +1 -0
  9. package/dist/coolandlovely/components/BrandCover.js +39 -0
  10. package/dist/coolandlovely/components/BrandCover.js.map +1 -0
  11. package/dist/coolandlovely/components/BrandCreatorAvatars.js +57 -0
  12. package/dist/coolandlovely/components/BrandCreatorAvatars.js.map +1 -0
  13. package/dist/coolandlovely/components/ColourSwatchRow.js +86 -0
  14. package/dist/coolandlovely/components/ColourSwatchRow.js.map +1 -0
  15. package/dist/coolandlovely/components/Comments.js +109 -0
  16. package/dist/coolandlovely/components/Comments.js.map +1 -0
  17. package/dist/coolandlovely/components/Field.js +22 -2
  18. package/dist/coolandlovely/components/Field.js.map +1 -1
  19. package/dist/coolandlovely/components/FileImage.js +17 -0
  20. package/dist/coolandlovely/components/FileImage.js.map +1 -0
  21. package/dist/coolandlovely/components/FollowButton.js +24 -0
  22. package/dist/coolandlovely/components/FollowButton.js.map +1 -0
  23. package/dist/coolandlovely/components/ImageUploadField.js +153 -0
  24. package/dist/coolandlovely/components/ImageUploadField.js.map +1 -0
  25. package/dist/coolandlovely/components/LookCard.js +77 -0
  26. package/dist/coolandlovely/components/LookCard.js.map +1 -0
  27. package/dist/coolandlovely/components/MetricsCard.js +1 -1
  28. package/dist/coolandlovely/components/MetricsCard.js.map +1 -1
  29. package/dist/coolandlovely/components/ShopItemCard.js +45 -0
  30. package/dist/coolandlovely/components/ShopItemCard.js.map +1 -0
  31. package/dist/coolandlovely/components/StepTracker.js +35 -0
  32. package/dist/coolandlovely/components/StepTracker.js.map +1 -0
  33. package/dist/coolandlovely/components/Thumbnail.js +25 -14
  34. package/dist/coolandlovely/components/Thumbnail.js.map +1 -1
  35. package/dist/coolandlovely/components/VisibilityChip.js +32 -0
  36. package/dist/coolandlovely/components/VisibilityChip.js.map +1 -0
  37. package/dist/coolandlovely/hooks/index.js +2 -0
  38. package/dist/coolandlovely/hooks/useComments.js +68 -0
  39. package/dist/coolandlovely/hooks/useComments.js.map +1 -0
  40. package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js +155 -60
  41. package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js.map +1 -1
  42. package/dist/coolandlovely/hooks/useFiles.js +111 -0
  43. package/dist/coolandlovely/hooks/useFiles.js.map +1 -0
  44. package/dist/coolandlovely/pages/HomePage.js +130 -133
  45. package/dist/coolandlovely/pages/HomePage.js.map +1 -1
  46. package/dist/coolandlovely/pages/brands/BrandDetailPage.js +261 -228
  47. package/dist/coolandlovely/pages/brands/BrandDetailPage.js.map +1 -1
  48. package/dist/coolandlovely/pages/brands/BrandFormPage.js +146 -104
  49. package/dist/coolandlovely/pages/brands/BrandFormPage.js.map +1 -1
  50. package/dist/coolandlovely/pages/brands/BrandsListPage.js +13 -11
  51. package/dist/coolandlovely/pages/brands/BrandsListPage.js.map +1 -1
  52. package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js +117 -95
  53. package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js.map +1 -1
  54. package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js +1 -1
  55. package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js +50 -43
  56. package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js.map +1 -1
  57. package/dist/coolandlovely/pages/collections/CollectionDetailPage.js +134 -107
  58. package/dist/coolandlovely/pages/collections/CollectionDetailPage.js.map +1 -1
  59. package/dist/coolandlovely/pages/collections/CollectionFormPage.js +117 -102
  60. package/dist/coolandlovely/pages/collections/CollectionFormPage.js.map +1 -1
  61. package/dist/coolandlovely/pages/collections/CollectionsListPage.js +59 -54
  62. package/dist/coolandlovely/pages/collections/CollectionsListPage.js.map +1 -1
  63. package/dist/coolandlovely/pages/discover/DiscoverPage.js +223 -0
  64. package/dist/coolandlovely/pages/discover/DiscoverPage.js.map +1 -0
  65. package/dist/coolandlovely/pages/items/StyleItemDetailPage.js +176 -144
  66. package/dist/coolandlovely/pages/items/StyleItemDetailPage.js.map +1 -1
  67. package/dist/coolandlovely/pages/items/StyleItemFormPage.js +144 -130
  68. package/dist/coolandlovely/pages/items/StyleItemFormPage.js.map +1 -1
  69. package/dist/coolandlovely/pages/items/StyleItemsListPage.js +73 -62
  70. package/dist/coolandlovely/pages/items/StyleItemsListPage.js.map +1 -1
  71. package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js +215 -102
  72. package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js.map +1 -1
  73. package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js +115 -86
  74. package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js.map +1 -1
  75. package/dist/coolandlovely/pages/looks/LookDetailPage.js +217 -155
  76. package/dist/coolandlovely/pages/looks/LookDetailPage.js.map +1 -1
  77. package/dist/coolandlovely/pages/looks/LookFormPage.js +147 -96
  78. package/dist/coolandlovely/pages/looks/LookFormPage.js.map +1 -1
  79. package/dist/coolandlovely/pages/looks/LooksListPage.js +60 -95
  80. package/dist/coolandlovely/pages/looks/LooksListPage.js.map +1 -1
  81. package/dist/coolandlovely/pages/profile/StyleProfilePage.js +376 -146
  82. package/dist/coolandlovely/pages/profile/StyleProfilePage.js.map +1 -1
  83. package/dist/coolandlovely/pages/trends/TrendsPage.js +249 -0
  84. package/dist/coolandlovely/pages/trends/TrendsPage.js.map +1 -0
  85. package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js +136 -88
  86. package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js.map +1 -1
  87. package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js +75 -74
  88. package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js.map +1 -1
  89. package/package.json +1 -1
@@ -0,0 +1,223 @@
1
+ import { useCoolAndLovely as e } from "../../context/CoolAndLovelyContext.js";
2
+ import { PageLayout as t } from "../../components/PageLayout.js";
3
+ import { ErrorView as n, LoadingView as r } from "../../components/StateViews.js";
4
+ import { Thumbnail as i } from "../../components/Thumbnail.js";
5
+ import { useLooks as a, useMyFollowing as o, useMyStyleProfile as s, useStyleItems as c } from "../../hooks/useCoolAndLovelyApi.js";
6
+ import { titleCase as l } from "../../utils/formatters.js";
7
+ import "../../hooks/index.js";
8
+ import { TagList as u } from "../../components/TagList.js";
9
+ import { EmptyState as d } from "../../components/EmptyState.js";
10
+ import { useMemo as f } from "react";
11
+ import { Compass as p, Heart as m, ShoppingBag as h, Sparkles as g, UserCheck as _, Users as v } from "lucide-react";
12
+ import { jsx as y, jsxs as b } from "react/jsx-runtime";
13
+ import { EmphasisPanel as x, PagePurpose as S } from "@burdenoff/fe-libs/ui";
14
+ //#region src/coolandlovely/pages/discover/DiscoverPage.tsx
15
+ function C(e) {
16
+ return new Set(e.map((e) => e.trim().toLowerCase()).filter(Boolean));
17
+ }
18
+ function w(e, t) {
19
+ let n = 0;
20
+ for (let r of e) t.has(r.trim().toLowerCase()) && (n += 1);
21
+ return n;
22
+ }
23
+ var T = ({ title: e, icon: t, sublabel: n, children: r }) => /* @__PURE__ */ b("section", {
24
+ className: "mt-8",
25
+ children: [/* @__PURE__ */ b("div", {
26
+ className: "mb-3",
27
+ children: [/* @__PURE__ */ b("h2", {
28
+ className: "flex items-center gap-2 text-lg font-semibold text-text-primary",
29
+ children: [
30
+ t,
31
+ " ",
32
+ e
33
+ ]
34
+ }), n ? /* @__PURE__ */ y("p", {
35
+ className: "mt-0.5 text-sm text-text-secondary",
36
+ children: n
37
+ }) : null]
38
+ }), r]
39
+ });
40
+ function E() {
41
+ let { navigateTo: E } = e(), D = s(), O = a({ pagination: {
42
+ limit: 100,
43
+ offset: 0
44
+ } }), k = c({ pagination: {
45
+ limit: 100,
46
+ offset: 0
47
+ } }), A = o(), j = D.isLoading || O.isLoading || k.isLoading, M = O.isError && k.isError, N = f(() => {
48
+ let e = D.data, t = O.data?.items ?? [], n = k.data?.items ?? [], r = new Set((A.data ?? []).map((e) => e.ownerId)), i = C([
49
+ ...e?.stylePreferences ?? [],
50
+ ...e?.palette ?? [],
51
+ ...e?.favouriteBrands ?? []
52
+ ]), a = C(e?.palette ?? []), o = C(e?.favouriteBrands ?? []), s = (e) => w(e.tags, i) * 2 + +!!i.has(e.occasion.toLowerCase()) + e.likeCount * .05 + e.saveCount * .05, c = (e) => w(e.tags, i) * 2 + w(e.colours, a) * 2 + +!!i.has(e.category.toLowerCase()) + (o.size && e.tags.some((e) => o.has(e.toLowerCase())) ? 1 : 0), l = i.size > 0, u = [...t].sort((e, t) => l ? s(t) - s(e) : t.likeCount + t.saveCount - (e.likeCount + e.saveCount)).slice(0, 8), d = [...n].filter((e) => l ? c(e) > 0 : !0).sort((e, t) => c(t) - c(e)).slice(0, 8), f = t.filter((e) => r.has(e.authorId)).sort((e, t) => t.likeCount + t.saveCount - (e.likeCount + e.saveCount)).slice(0, 8);
53
+ return {
54
+ hasProfile: !!e,
55
+ hasTaste: l,
56
+ madeForYou: u,
57
+ inYourPalette: d,
58
+ fromFollowing: f,
59
+ followingProfiles: A.data ?? []
60
+ };
61
+ }, [
62
+ D.data,
63
+ O.data,
64
+ k.data,
65
+ A.data
66
+ ]);
67
+ return j ? /* @__PURE__ */ y(t, {
68
+ title: "Discover",
69
+ children: /* @__PURE__ */ y(r, { label: "Curating your feed…" })
70
+ }) : M ? /* @__PURE__ */ y(t, {
71
+ title: "Discover",
72
+ children: /* @__PURE__ */ y(n, { onRetry: () => {
73
+ O.refetch(), k.refetch();
74
+ } })
75
+ }) : /* @__PURE__ */ b(t, {
76
+ title: "Discover",
77
+ description: "Looks and pieces tuned to your taste",
78
+ hero: !0,
79
+ children: [
80
+ /* @__PURE__ */ y(S, {
81
+ className: "mb-6",
82
+ children: "Your personalised feed, computed live from your style profile against the pieces and looks on the platform — the closer a piece matches the aesthetics, palette and brands you follow, the higher it ranks. Nothing here is fabricated."
83
+ }),
84
+ N.hasProfile ? null : /* @__PURE__ */ y(x, {
85
+ className: "mb-8",
86
+ children: /* @__PURE__ */ b("div", {
87
+ className: "flex flex-col items-start gap-3 sm:flex-row sm:items-center sm:justify-between",
88
+ children: [/* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("h2", {
89
+ className: "text-lg font-semibold text-text-primary",
90
+ children: "Build your style profile"
91
+ }), /* @__PURE__ */ y("p", {
92
+ className: "mt-1 text-sm text-text-secondary",
93
+ children: "Tell us your aesthetics, palette and favourite brands and this feed tunes itself to you."
94
+ })] }), /* @__PURE__ */ b("button", {
95
+ type: "button",
96
+ onClick: () => E("/profile"),
97
+ className: "inline-flex shrink-0 items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-opacity hover:opacity-90",
98
+ children: [/* @__PURE__ */ y(g, { className: "h-4 w-4" }), " Create profile"]
99
+ })]
100
+ })
101
+ }),
102
+ /* @__PURE__ */ y(T, {
103
+ title: "Made for you",
104
+ icon: /* @__PURE__ */ y(p, { className: "h-5 w-5" }),
105
+ sublabel: N.hasTaste ? "Ranked to match your style profile" : "Most-loved looks on the platform",
106
+ children: N.madeForYou.length === 0 ? /* @__PURE__ */ y(d, {
107
+ title: "No looks yet",
108
+ description: "Once looks are published they will appear here, tuned to your taste."
109
+ }) : /* @__PURE__ */ y("div", {
110
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4",
111
+ children: N.madeForYou.map((e) => /* @__PURE__ */ b("button", {
112
+ type: "button",
113
+ onClick: () => E(`/looks/${e.id}`),
114
+ className: "group flex flex-col overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left transition-colors hover:border-accent-foreground/20 hover:shadow-md",
115
+ children: [/* @__PURE__ */ y(i, {
116
+ icon: /* @__PURE__ */ y(g, { className: "h-6 w-6" }),
117
+ seed: e.id,
118
+ fileId: e.image?.id,
119
+ alt: e.title,
120
+ className: "aspect-[4/5] w-full rounded-none"
121
+ }), /* @__PURE__ */ b("div", {
122
+ className: "min-w-0 p-3",
123
+ children: [
124
+ /* @__PURE__ */ y("p", {
125
+ className: "truncate text-sm font-medium text-text-primary",
126
+ children: e.title
127
+ }),
128
+ /* @__PURE__ */ y("p", {
129
+ className: "mt-0.5 text-xs text-text-secondary",
130
+ children: l(e.occasion)
131
+ }),
132
+ /* @__PURE__ */ b("span", {
133
+ className: "mt-2 inline-flex items-center gap-1 text-xs text-text-secondary",
134
+ children: [
135
+ /* @__PURE__ */ y(m, { className: "h-3.5 w-3.5" }),
136
+ " ",
137
+ e.likeCount
138
+ ]
139
+ })
140
+ ]
141
+ })]
142
+ }, e.id))
143
+ })
144
+ }),
145
+ N.inYourPalette.length > 0 ? /* @__PURE__ */ y(T, {
146
+ title: "In your palette",
147
+ icon: /* @__PURE__ */ y(h, { className: "h-5 w-5" }),
148
+ sublabel: "Pieces that match your colours, categories and tags",
149
+ children: /* @__PURE__ */ y("div", {
150
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4",
151
+ children: N.inYourPalette.map((e) => /* @__PURE__ */ b("button", {
152
+ type: "button",
153
+ onClick: () => E(`/items/${e.id}`),
154
+ className: "group flex flex-col overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left transition-colors hover:border-accent-foreground/20 hover:shadow-md",
155
+ children: [/* @__PURE__ */ y(i, {
156
+ icon: /* @__PURE__ */ y(h, { className: "h-6 w-6" }),
157
+ seed: e.id,
158
+ fileId: e.image?.id,
159
+ alt: e.name,
160
+ className: "aspect-[4/5] w-full rounded-none"
161
+ }), /* @__PURE__ */ b("div", {
162
+ className: "min-w-0 p-3",
163
+ children: [
164
+ /* @__PURE__ */ y("p", {
165
+ className: "truncate text-sm font-medium text-text-primary",
166
+ children: e.name
167
+ }),
168
+ /* @__PURE__ */ y("p", {
169
+ className: "mt-0.5 text-xs text-text-secondary",
170
+ children: l(e.category)
171
+ }),
172
+ /* @__PURE__ */ y(u, {
173
+ tags: e.colours.slice(0, 3),
174
+ className: "mt-2"
175
+ })
176
+ ]
177
+ })]
178
+ }, e.id))
179
+ })
180
+ }) : null,
181
+ /* @__PURE__ */ y(T, {
182
+ title: "From people you follow",
183
+ icon: /* @__PURE__ */ y(_, { className: "h-5 w-5" }),
184
+ sublabel: N.followingProfiles.length === 0 ? "Follow creators and shoppers to see their looks here" : `You follow ${N.followingProfiles.length} ${N.followingProfiles.length === 1 ? "person" : "people"}`,
185
+ children: N.followingProfiles.length === 0 ? /* @__PURE__ */ y(d, {
186
+ title: "You are not following anyone yet",
187
+ description: "Open a look or a style profile and tap Follow to build a feed from people whose taste you love.",
188
+ icon: /* @__PURE__ */ y(v, { className: "h-8 w-8" })
189
+ }) : N.fromFollowing.length === 0 ? /* @__PURE__ */ y("div", {
190
+ className: "rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary",
191
+ children: "The people you follow have not published looks visible to you yet."
192
+ }) : /* @__PURE__ */ y("div", {
193
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4",
194
+ children: N.fromFollowing.map((e) => /* @__PURE__ */ b("button", {
195
+ type: "button",
196
+ onClick: () => E(`/looks/${e.id}`),
197
+ className: "group flex flex-col overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left transition-colors hover:border-accent-foreground/20 hover:shadow-md",
198
+ children: [/* @__PURE__ */ y(i, {
199
+ icon: /* @__PURE__ */ y(g, { className: "h-6 w-6" }),
200
+ seed: e.id,
201
+ fileId: e.image?.id,
202
+ alt: e.title,
203
+ className: "aspect-[4/5] w-full rounded-none"
204
+ }), /* @__PURE__ */ b("div", {
205
+ className: "min-w-0 p-3",
206
+ children: [/* @__PURE__ */ y("p", {
207
+ className: "truncate text-sm font-medium text-text-primary",
208
+ children: e.title
209
+ }), /* @__PURE__ */ y("p", {
210
+ className: "mt-0.5 text-xs text-text-secondary",
211
+ children: l(e.occasion)
212
+ })]
213
+ })]
214
+ }, e.id))
215
+ })
216
+ })
217
+ ]
218
+ });
219
+ }
220
+ //#endregion
221
+ export { E as DiscoverPage };
222
+
223
+ //# sourceMappingURL=DiscoverPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DiscoverPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/discover/DiscoverPage.tsx"],"sourcesContent":["/**\n * Discover Page\n * @module coolandlovely/pages/discover\n *\n * A profile-tuned discovery feed computed entirely client-side over the REAL\n * catalogue + social lists (cooListLooks / cooListStyleItems) and the caller's\n * own style profile (cooMyStyleProfile) + follow graph (cooMyFollowing). It only\n * ranks what is actually on the platform against the taste the shopper declared\n * — no fabricated recommendations, no black-box \"AI\" (that is roadmap). When the\n * shopper has no profile yet, it invites them to build one.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useMemo } from 'react';\nimport { Compass, Sparkles, Heart, ShoppingBag, UserCheck, Users } from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { TagList } from '../../components/TagList';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useMyStyleProfile, useLooks, useStyleItems, useMyFollowing } from '../../hooks';\nimport type { CooLook, CooStyleItem } from '../../types';\nimport { titleCase } from '../../utils/formatters';\n\n/** Lower-cased token set used for overlap scoring. */\nfunction tokenSet(values: string[]): Set<string> {\n return new Set(values.map((v) => v.trim().toLowerCase()).filter(Boolean));\n}\n\n/** Count how many of `candidate`'s tokens appear in the taste set. */\nfunction overlap(candidate: string[], taste: Set<string>): number {\n let n = 0;\n for (const c of candidate) {\n if (taste.has(c.trim().toLowerCase())) n += 1;\n }\n return n;\n}\n\ninterface SectionProps {\n title: string;\n icon: ReactNode;\n children: ReactNode;\n sublabel?: string;\n}\n\nconst Section: FC<SectionProps> = ({ title, icon, sublabel, children }) => (\n <section className=\"mt-8\">\n <div className=\"mb-3\">\n <h2 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n {icon} {title}\n </h2>\n {sublabel ? <p className=\"mt-0.5 text-sm text-text-secondary\">{sublabel}</p> : null}\n </div>\n {children}\n </section>\n);\n\nexport function DiscoverPage() {\n const { navigateTo } = useCoolAndLovely();\n const profile = useMyStyleProfile();\n const looks = useLooks({ pagination: { limit: 100, offset: 0 } });\n const items = useStyleItems({ pagination: { limit: 100, offset: 0 } });\n const following = useMyFollowing();\n\n const isLoading = profile.isLoading || looks.isLoading || items.isLoading;\n const isError = looks.isError && items.isError;\n\n const feed = useMemo(() => {\n const p = profile.data;\n const lookRows = looks.data?.items ?? [];\n const itemRows = items.data?.items ?? [];\n const followingOwners = new Set((following.data ?? []).map((f) => f.ownerId));\n\n // Taste = declared style preferences ∪ palette ∪ favourite brands.\n const taste = tokenSet([\n ...(p?.stylePreferences ?? []),\n ...(p?.palette ?? []),\n ...(p?.favouriteBrands ?? []),\n ]);\n const paletteTokens = tokenSet(p?.palette ?? []);\n const brandTokens = tokenSet(p?.favouriteBrands ?? []);\n\n const scoreLook = (l: CooLook): number =>\n overlap(l.tags, taste) * 2 +\n (taste.has(l.occasion.toLowerCase()) ? 1 : 0) +\n l.likeCount * 0.05 +\n l.saveCount * 0.05;\n\n const scoreItem = (it: CooStyleItem): number =>\n overlap(it.tags, taste) * 2 +\n overlap(it.colours, paletteTokens) * 2 +\n (taste.has(it.category.toLowerCase()) ? 1 : 0) +\n (brandTokens.size && it.tags.some((t) => brandTokens.has(t.toLowerCase())) ? 1 : 0);\n\n const hasTaste = taste.size > 0;\n\n // \"Made for you\": when the shopper has taste, rank by match then engagement;\n // otherwise fall back to most-loved looks so the feed is never empty.\n const madeForYou = [...lookRows]\n .sort((a, b) =>\n hasTaste\n ? scoreLook(b) - scoreLook(a)\n : b.likeCount + b.saveCount - (a.likeCount + a.saveCount)\n )\n .slice(0, 8);\n\n const inYourPalette = [...itemRows]\n .filter((it) => (hasTaste ? scoreItem(it) > 0 : true))\n .sort((a, b) => scoreItem(b) - scoreItem(a))\n .slice(0, 8);\n\n const fromFollowing = lookRows\n .filter((l) => followingOwners.has(l.authorId))\n .sort((a, b) => b.likeCount + b.saveCount - (a.likeCount + a.saveCount))\n .slice(0, 8);\n\n return {\n hasProfile: Boolean(p),\n hasTaste,\n madeForYou,\n inYourPalette,\n fromFollowing,\n followingProfiles: following.data ?? [],\n };\n }, [profile.data, looks.data, items.data, following.data]);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Discover\">\n <LoadingView label=\"Curating your feed…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title=\"Discover\">\n <ErrorView\n onRetry={() => {\n void looks.refetch();\n void items.refetch();\n }}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout title=\"Discover\" description=\"Looks and pieces tuned to your taste\" hero>\n <PagePurpose className=\"mb-6\">\n Your personalised feed, computed live from your style profile against the pieces and looks\n on the platform — the closer a piece matches the aesthetics, palette and brands you follow,\n the higher it ranks. Nothing here is fabricated.\n </PagePurpose>\n\n {!feed.hasProfile ? (\n <EmphasisPanel className=\"mb-8\">\n <div className=\"flex flex-col items-start gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Build your style profile</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Tell us your aesthetics, palette and favourite brands and this feed tunes itself to\n you.\n </p>\n </div>\n <button\n type=\"button\"\n onClick={() => navigateTo('/profile')}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-opacity hover:opacity-90\"\n >\n <Sparkles className=\"h-4 w-4\" /> Create profile\n </button>\n </div>\n </EmphasisPanel>\n ) : null}\n\n <Section\n title=\"Made for you\"\n icon={<Compass className=\"h-5 w-5\" />}\n sublabel={\n feed.hasTaste ? 'Ranked to match your style profile' : 'Most-loved looks on the platform'\n }\n >\n {feed.madeForYou.length === 0 ? (\n <EmptyState\n title=\"No looks yet\"\n description=\"Once looks are published they will appear here, tuned to your taste.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {feed.madeForYou.map((look) => (\n <button\n key={look.id}\n type=\"button\"\n onClick={() => navigateTo(`/looks/${look.id}`)}\n className=\"group flex flex-col overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left transition-colors hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <Thumbnail\n icon={<Sparkles className=\"h-6 w-6\" />}\n seed={look.id}\n fileId={look.image?.id}\n alt={look.title}\n className=\"aspect-[4/5] w-full rounded-none\"\n />\n <div className=\"min-w-0 p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{look.title}</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">{titleCase(look.occasion)}</p>\n <span className=\"mt-2 inline-flex items-center gap-1 text-xs text-text-secondary\">\n <Heart className=\"h-3.5 w-3.5\" /> {look.likeCount}\n </span>\n </div>\n </button>\n ))}\n </div>\n )}\n </Section>\n\n {feed.inYourPalette.length > 0 ? (\n <Section\n title=\"In your palette\"\n icon={<ShoppingBag className=\"h-5 w-5\" />}\n sublabel=\"Pieces that match your colours, categories and tags\"\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {feed.inYourPalette.map((item) => (\n <button\n key={item.id}\n type=\"button\"\n onClick={() => navigateTo(`/items/${item.id}`)}\n className=\"group flex flex-col overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left transition-colors hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <Thumbnail\n icon={<ShoppingBag className=\"h-6 w-6\" />}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"aspect-[4/5] w-full rounded-none\"\n />\n <div className=\"min-w-0 p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{item.name}</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">{titleCase(item.category)}</p>\n <TagList tags={item.colours.slice(0, 3)} className=\"mt-2\" />\n </div>\n </button>\n ))}\n </div>\n </Section>\n ) : null}\n\n <Section\n title=\"From people you follow\"\n icon={<UserCheck className=\"h-5 w-5\" />}\n sublabel={\n feed.followingProfiles.length === 0\n ? 'Follow creators and shoppers to see their looks here'\n : `You follow ${feed.followingProfiles.length} ${feed.followingProfiles.length === 1 ? 'person' : 'people'}`\n }\n >\n {feed.followingProfiles.length === 0 ? (\n <EmptyState\n title=\"You are not following anyone yet\"\n description=\"Open a look or a style profile and tap Follow to build a feed from people whose taste you love.\"\n icon={<Users className=\"h-8 w-8\" />}\n />\n ) : feed.fromFollowing.length === 0 ? (\n <div className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n The people you follow have not published looks visible to you yet.\n </div>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n {feed.fromFollowing.map((look) => (\n <button\n key={look.id}\n type=\"button\"\n onClick={() => navigateTo(`/looks/${look.id}`)}\n className=\"group flex flex-col overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left transition-colors hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <Thumbnail\n icon={<Sparkles className=\"h-6 w-6\" />}\n seed={look.id}\n fileId={look.image?.id}\n alt={look.title}\n className=\"aspect-[4/5] w-full rounded-none\"\n />\n <div className=\"min-w-0 p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{look.title}</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">{titleCase(look.occasion)}</p>\n </div>\n </button>\n ))}\n </div>\n )}\n </Section>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AA2BA,SAAS,EAAS,GAA+B;AAC/C,QAAO,IAAI,IAAI,EAAO,KAAK,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC,OAAO,QAAQ,CAAC;;AAI3E,SAAS,EAAQ,GAAqB,GAA4B;CAChE,IAAI,IAAI;AACR,MAAK,IAAM,KAAK,EACd,CAAI,EAAM,IAAI,EAAE,MAAM,CAAC,aAAa,CAAC,KAAE,KAAK;AAE9C,QAAO;;AAUT,IAAM,KAA6B,EAAE,UAAO,SAAM,aAAU,kBAC1D,kBAAC,WAAD;CAAS,WAAU;WAAnB,CACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,MAAD;GAAI,WAAU;aAAd;IACG;IAAK;IAAE;IACL;MACJ,IAAW,kBAAC,KAAD;GAAG,WAAU;aAAsC;GAAa,CAAA,GAAG,KAC3E;KACL,EACO;;AAGZ,SAAgB,IAAe;CAC7B,IAAM,EAAE,kBAAe,GAAkB,EACnC,IAAU,GAAmB,EAC7B,IAAQ,EAAS,EAAE,YAAY;EAAE,OAAO;EAAK,QAAQ;EAAG,EAAE,CAAC,EAC3D,IAAQ,EAAc,EAAE,YAAY;EAAE,OAAO;EAAK,QAAQ;EAAG,EAAE,CAAC,EAChE,IAAY,GAAgB,EAE5B,IAAY,EAAQ,aAAa,EAAM,aAAa,EAAM,WAC1D,IAAU,EAAM,WAAW,EAAM,SAEjC,IAAO,QAAc;EACzB,IAAM,IAAI,EAAQ,MACZ,IAAW,EAAM,MAAM,SAAS,EAAE,EAClC,IAAW,EAAM,MAAM,SAAS,EAAE,EAClC,IAAkB,IAAI,KAAK,EAAU,QAAQ,EAAE,EAAE,KAAK,MAAM,EAAE,QAAQ,CAAC,EAGvE,IAAQ,EAAS;GACrB,GAAI,GAAG,oBAAoB,EAAE;GAC7B,GAAI,GAAG,WAAW,EAAE;GACpB,GAAI,GAAG,mBAAmB,EAAE;GAC7B,CAAC,EACI,IAAgB,EAAS,GAAG,WAAW,EAAE,CAAC,EAC1C,IAAc,EAAS,GAAG,mBAAmB,EAAE,CAAC,EAEhD,KAAa,MACjB,EAAQ,EAAE,MAAM,EAAM,GAAG,IACxB,KAAM,IAAI,EAAE,SAAS,aAAa,CAAC,GACpC,EAAE,YAAY,MACd,EAAE,YAAY,KAEV,KAAa,MACjB,EAAQ,EAAG,MAAM,EAAM,GAAG,IAC1B,EAAQ,EAAG,SAAS,EAAc,GAAG,IACpC,KAAM,IAAI,EAAG,SAAS,aAAa,CAAC,IACpC,EAAY,QAAQ,EAAG,KAAK,MAAM,MAAM,EAAY,IAAI,EAAE,aAAa,CAAC,CAAC,GAAG,IAAI,IAE7E,IAAW,EAAM,OAAO,GAIxB,IAAa,CAAC,GAAG,EAAS,CAC7B,MAAM,GAAG,MACR,IACI,EAAU,EAAE,GAAG,EAAU,EAAE,GAC3B,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,WAClD,CACA,MAAM,GAAG,EAAE,EAER,IAAgB,CAAC,GAAG,EAAS,CAChC,QAAQ,MAAQ,IAAW,EAAU,EAAG,GAAG,IAAI,GAAM,CACrD,MAAM,GAAG,MAAM,EAAU,EAAE,GAAG,EAAU,EAAE,CAAC,CAC3C,MAAM,GAAG,EAAE,EAER,IAAgB,EACnB,QAAQ,MAAM,EAAgB,IAAI,EAAE,SAAS,CAAC,CAC9C,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,CACvE,MAAM,GAAG,EAAE;AAEd,SAAO;GACL,YAAY,EAAQ;GACpB;GACA;GACA;GACA;GACA,mBAAmB,EAAU,QAAQ,EAAE;GACxC;IACA;EAAC,EAAQ;EAAM,EAAM;EAAM,EAAM;EAAM,EAAU;EAAK,CAAC;AAuB1D,QArBI,IAEA,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD,EAAa,OAAM,uBAAwB,CAAA;EAChC,CAAA,GAIb,IAEA,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD,EACE,eAAe;AAER,GADA,EAAM,SAAS,EACf,EAAM,SAAS;KAEtB,CAAA;EACS,CAAA,GAKf,kBAAC,GAAD;EAAY,OAAM;EAAW,aAAY;EAAuC,MAAA;YAAhF;GACE,kBAAC,GAAD;IAAa,WAAU;cAAO;IAIhB,CAAA;GAEZ,EAAK,aAmBH,OAlBF,kBAAC,GAAD;IAAe,WAAU;cACvB,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;MAAI,WAAU;gBAA0C;MAA6B,CAAA,EACrF,kBAAC,KAAD;MAAG,WAAU;gBAAmC;MAG5C,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAW,WAAW;MACrC,WAAU;gBAHZ,CAKE,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA,EAAA,kBACzB;QACL;;IACQ,CAAA;GAGlB,kBAAC,GAAD;IACE,OAAM;IACN,MAAM,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;IACrC,UACE,EAAK,WAAW,uCAAuC;cAGxD,EAAK,WAAW,WAAW,IAC1B,kBAAC,GAAD;KACE,OAAM;KACN,aAAY;KACZ,CAAA,GAEF,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAK,WAAW,KAAK,MACpB,kBAAC,UAAD;MAEE,MAAK;MACL,eAAe,EAAW,UAAU,EAAK,KAAK;MAC9C,WAAU;gBAJZ,CAME,kBAAC,GAAD;OACE,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;OACtC,MAAM,EAAK;OACX,QAAQ,EAAK,OAAO;OACpB,KAAK,EAAK;OACV,WAAU;OACV,CAAA,EACF,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,KAAD;SAAG,WAAU;mBAAkD,EAAK;SAAU,CAAA;QAC9E,kBAAC,KAAD;SAAG,WAAU;mBAAsC,EAAU,EAAK,SAAS;SAAK,CAAA;QAChF,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UACE,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA;;UAAE,EAAK;UACnC;;QACH;SACC;QAnBF,EAAK,GAmBH,CACT;KACE,CAAA;IAEA,CAAA;GAET,EAAK,cAAc,SAAS,IAC3B,kBAAC,GAAD;IACE,OAAM;IACN,MAAM,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;IACzC,UAAS;cAET,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAK,cAAc,KAAK,MACvB,kBAAC,UAAD;MAEE,MAAK;MACL,eAAe,EAAW,UAAU,EAAK,KAAK;MAC9C,WAAU;gBAJZ,CAME,kBAAC,GAAD;OACE,MAAM,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;OACzC,MAAM,EAAK;OACX,QAAQ,EAAK,OAAO;OACpB,KAAK,EAAK;OACV,WAAU;OACV,CAAA,EACF,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,KAAD;SAAG,WAAU;mBAAkD,EAAK;SAAS,CAAA;QAC7E,kBAAC,KAAD;SAAG,WAAU;mBAAsC,EAAU,EAAK,SAAS;SAAK,CAAA;QAChF,kBAAC,GAAD;SAAS,MAAM,EAAK,QAAQ,MAAM,GAAG,EAAE;SAAE,WAAU;SAAS,CAAA;QACxD;SACC;QAjBF,EAAK,GAiBH,CACT;KACE,CAAA;IACE,CAAA,GACR;GAEJ,kBAAC,GAAD;IACE,OAAM;IACN,MAAM,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA;IACvC,UACE,EAAK,kBAAkB,WAAW,IAC9B,yDACA,cAAc,EAAK,kBAAkB,OAAO,GAAG,EAAK,kBAAkB,WAAW,IAAI,WAAW;cAGrG,EAAK,kBAAkB,WAAW,IACjC,kBAAC,GAAD;KACE,OAAM;KACN,aAAY;KACZ,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;KACnC,CAAA,GACA,EAAK,cAAc,WAAW,IAChC,kBAAC,OAAD;KAAK,WAAU;eAAqF;KAE9F,CAAA,GAEN,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAK,cAAc,KAAK,MACvB,kBAAC,UAAD;MAEE,MAAK;MACL,eAAe,EAAW,UAAU,EAAK,KAAK;MAC9C,WAAU;gBAJZ,CAME,kBAAC,GAAD;OACE,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;OACtC,MAAM,EAAK;OACX,QAAQ,EAAK,OAAO;OACpB,KAAK,EAAK;OACV,WAAU;OACV,CAAA,EACF,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAkD,EAAK;QAAU,CAAA,EAC9E,kBAAC,KAAD;QAAG,WAAU;kBAAsC,EAAU,EAAK,SAAS;QAAK,CAAA,CAC5E;SACC;QAhBF,EAAK,GAgBH,CACT;KACE,CAAA;IAEA,CAAA;GACC"}