@burdenoff/microfe-coolandlovely 2026.805.1 → 2026.820.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 (66) hide show
  1. package/dist/coolandlovely/CoolAndLovelyRoot.d.ts.map +1 -1
  2. package/dist/coolandlovely/CoolAndLovelyRoot.js +60 -58
  3. package/dist/coolandlovely/CoolAndLovelyRoot.js.map +1 -1
  4. package/dist/coolandlovely/components/AddToLookbookButton.d.ts.map +1 -1
  5. package/dist/coolandlovely/components/AddToLookbookButton.js +1 -0
  6. package/dist/coolandlovely/components/AddToLookbookButton.js.map +1 -1
  7. package/dist/coolandlovely/components/BrandCard.d.ts +11 -0
  8. package/dist/coolandlovely/components/BrandCard.d.ts.map +1 -0
  9. package/dist/coolandlovely/components/BrandCard.js +55 -0
  10. package/dist/coolandlovely/components/BrandCard.js.map +1 -0
  11. package/dist/coolandlovely/components/CollectionCard.d.ts +11 -0
  12. package/dist/coolandlovely/components/CollectionCard.d.ts.map +1 -0
  13. package/dist/coolandlovely/components/CollectionCard.js +60 -0
  14. package/dist/coolandlovely/components/CollectionCard.js.map +1 -0
  15. package/dist/coolandlovely/components/FeedbackWidget.d.ts +4 -0
  16. package/dist/coolandlovely/components/FeedbackWidget.d.ts.map +1 -0
  17. package/dist/coolandlovely/components/FeedbackWidget.js +134 -0
  18. package/dist/coolandlovely/components/FeedbackWidget.js.map +1 -0
  19. package/dist/coolandlovely/components/LookbookCard.d.ts +11 -0
  20. package/dist/coolandlovely/components/LookbookCard.d.ts.map +1 -0
  21. package/dist/coolandlovely/components/LookbookCard.js +60 -0
  22. package/dist/coolandlovely/components/LookbookCard.js.map +1 -0
  23. package/dist/coolandlovely/components/WardrobeItemCard.d.ts +14 -0
  24. package/dist/coolandlovely/components/WardrobeItemCard.d.ts.map +1 -0
  25. package/dist/coolandlovely/components/WardrobeItemCard.js +92 -0
  26. package/dist/coolandlovely/components/WardrobeItemCard.js.map +1 -0
  27. package/dist/coolandlovely/components/index.d.ts +5 -0
  28. package/dist/coolandlovely/components/index.d.ts.map +1 -1
  29. package/dist/coolandlovely/components/index.js +26 -0
  30. package/dist/coolandlovely/pages/HomePage.d.ts.map +1 -1
  31. package/dist/coolandlovely/pages/HomePage.js +10 -4
  32. package/dist/coolandlovely/pages/HomePage.js.map +1 -1
  33. package/dist/coolandlovely/pages/brands/BrandDetailPage.js +7 -7
  34. package/dist/coolandlovely/pages/brands/BrandsListPage.d.ts.map +1 -1
  35. package/dist/coolandlovely/pages/brands/BrandsListPage.js +48 -89
  36. package/dist/coolandlovely/pages/brands/BrandsListPage.js.map +1 -1
  37. package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js +4 -4
  38. package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js +13 -13
  39. package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js +4 -4
  40. package/dist/coolandlovely/pages/collections/CollectionsListPage.d.ts.map +1 -1
  41. package/dist/coolandlovely/pages/collections/CollectionsListPage.js +28 -74
  42. package/dist/coolandlovely/pages/collections/CollectionsListPage.js.map +1 -1
  43. package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js +5 -5
  44. package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.d.ts.map +1 -1
  45. package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js +66 -112
  46. package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js.map +1 -1
  47. package/dist/coolandlovely/pages/profile/StyleProfilePage.js +124 -124
  48. package/dist/coolandlovely/pages/profile/StyleProfilePage.js.map +1 -1
  49. package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.d.ts.map +1 -1
  50. package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js +46 -120
  51. package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js.map +1 -1
  52. package/dist/stories/BrandCard.stories.d.ts +10 -0
  53. package/dist/stories/BrandCard.stories.d.ts.map +1 -0
  54. package/dist/stories/CollectionCard.stories.d.ts +10 -0
  55. package/dist/stories/CollectionCard.stories.d.ts.map +1 -0
  56. package/dist/stories/FeedbackWidget.stories.d.ts +9 -0
  57. package/dist/stories/FeedbackWidget.stories.d.ts.map +1 -0
  58. package/dist/stories/LookCard.stories.d.ts +10 -0
  59. package/dist/stories/LookCard.stories.d.ts.map +1 -0
  60. package/dist/stories/LookbookCard.stories.d.ts +10 -0
  61. package/dist/stories/LookbookCard.stories.d.ts.map +1 -0
  62. package/dist/stories/WardrobeItemCard.stories.d.ts +11 -0
  63. package/dist/stories/WardrobeItemCard.stories.d.ts.map +1 -0
  64. package/dist/stories/__fixtures__/coolandlovely.d.ts +13 -0
  65. package/dist/stories/__fixtures__/coolandlovely.d.ts.map +1 -0
  66. package/package.json +4 -1
@@ -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, 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} 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 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 = VISIBILITIES.map((v) => ({ value: v, label: titleCase(v) }));\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=\"Style Profile\">\n <LoadingView label=\"Loading style profile…\" />\n </PageLayout>\n );\n }\n\n if (query.isError) {\n return (\n <PageLayout title=\"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=\"Style Profile\">\n <EmptyState\n title=\"Profile not available\"\n description=\"This style profile doesn't exist, or its owner hasn't shared it with you.\"\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={isOwnProfile ? 'Style Profile' : headerName || 'Style Profile'}\n description={\n isOwnProfile\n ? 'Your taste, palette and sizing — the spine of personalised discovery'\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\" /> 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 ? 'follower' : '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 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=\"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 ? 'No preferences yet — add a few in Edit.'\n : 'No preferences shared yet.'}\n </p>\n )}\n </BentoCard>\n\n <BentoCard title=\"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\">No palette yet.</p>\n )}\n </BentoCard>\n\n <div className=\"grid gap-6 sm:grid-cols-2\">\n <BentoCard title=\"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\">No brands yet.</p>\n )}\n </BentoCard>\n\n {isOwnProfile && (\n <BentoCard title=\"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\">{titleCase(key)}</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\">No sizes recorded yet.</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=\"Her 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\" /> 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\" /> Add pieces to your wardrobe\n </button>\n )}\n </BentoCard>\n )}\n\n <BentoCard title=\"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\" /> 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 ? 'You have not published any looks yet.'\n : '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\" /> People you follow ({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\" /> 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 You are not following anyone yet. Follow creators from their looks to see their\n latest in Discover.\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(new Set([...SIZE_KEYS, ...Object.keys(form.sizes)]));\n\n return (\n <PageLayout\n title={hasProfile ? 'Edit style profile' : 'Create your style profile'}\n description=\"This powers your personalised discovery feed\"\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\" /> 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=\"Display name\"\n value={form.displayName}\n onChange={(v) => setField('displayName', v)}\n placeholder=\"How you want to appear\"\n />\n <TextAreaField\n id=\"bio\"\n label=\"Bio\"\n value={form.bio}\n onChange={(v) => setField('bio', v)}\n placeholder=\"A sentence about your style…\"\n rows={3}\n className=\"mt-4\"\n />\n <SelectField\n id=\"visibility\"\n label=\"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=\"Style preferences\"\n value={form.stylePreferences}\n onChange={(v) => setField('stylePreferences', v)}\n placeholder=\"minimal, tailored, vintage\"\n hint=\"Comma-separated\"\n className=\"mt-4\"\n />\n <TextField\n id=\"palette\"\n label=\"Palette\"\n value={form.palette}\n onChange={(v) => setField('palette', v)}\n placeholder=\"neutrals, navy, rust\"\n hint=\"Comma-separated colour names — shown as swatches\"\n className=\"mt-4\"\n />\n <TextField\n id=\"favouriteBrands\"\n label=\"Favourite brands\"\n value={form.favouriteBrands}\n onChange={(v) => setField('favouriteBrands', v)}\n placeholder=\"Aurora Atelier, …\"\n hint=\"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\">Sizes</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={titleCase(key)}\n value={form.sizes[key] ?? ''}\n onChange={(v) => setSize(key, v)}\n placeholder=\"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 ? upsert.error.message : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"submit\"\n disabled={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 ? 'Save changes' : '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, 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} 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 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 = VISIBILITIES.map((v) => ({ value: v, label: titleCase(v) }));\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=\"Style Profile\">\n <LoadingView label=\"Loading style profile…\" />\n </PageLayout>\n );\n }\n\n if (query.isError) {\n return (\n <PageLayout title=\"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=\"Style Profile\">\n <EmptyState\n title=\"Profile not available\"\n description=\"This style profile doesn't exist, or its owner hasn't shared it with you.\"\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={isOwnProfile ? 'Style Profile' : headerName || 'Style Profile'}\n description={\n isOwnProfile\n ? 'Your taste, palette and sizing — the spine of personalised discovery'\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\" /> 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 ? 'follower' : '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 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=\"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 ? 'No preferences yet — add a few in Edit.'\n : 'No preferences shared yet.'}\n </p>\n )}\n </BentoCard>\n\n <BentoCard title=\"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\">No palette yet.</p>\n )}\n </BentoCard>\n\n <div className=\"grid gap-6 sm:grid-cols-2\">\n <BentoCard title=\"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\">No brands yet.</p>\n )}\n </BentoCard>\n\n {isOwnProfile && (\n <BentoCard title=\"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\">{titleCase(key)}</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\">No sizes recorded yet.</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=\"Her 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\" /> 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\" /> Add pieces to your wardrobe\n </button>\n )}\n </BentoCard>\n )}\n\n <BentoCard title=\"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\" /> 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 ? 'You have not published any looks yet.'\n : '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\" /> People you follow ({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\" /> 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 You are not following anyone yet. Follow creators from their looks to see their\n latest in Discover.\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(new Set([...SIZE_KEYS, ...Object.keys(form.sizes)]));\n\n return (\n <PageLayout\n title={hasProfile ? 'Edit style profile' : 'Create your style profile'}\n description=\"This powers your personalised discovery feed\"\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\" /> 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=\"Display name\"\n value={form.displayName}\n onChange={(v) => setField('displayName', v)}\n placeholder=\"How you want to appear\"\n />\n <TextAreaField\n id=\"bio\"\n label=\"Bio\"\n value={form.bio}\n onChange={(v) => setField('bio', v)}\n placeholder=\"A sentence about your style…\"\n rows={3}\n className=\"mt-4\"\n />\n <SelectField\n id=\"visibility\"\n label=\"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=\"Style preferences\"\n value={form.stylePreferences}\n onChange={(v) => setField('stylePreferences', v)}\n placeholder=\"minimal, tailored, vintage\"\n hint=\"Comma-separated\"\n className=\"mt-4\"\n />\n <TextField\n id=\"palette\"\n label=\"Palette\"\n value={form.palette}\n onChange={(v) => setField('palette', v)}\n placeholder=\"neutrals, navy, rust\"\n hint=\"Comma-separated colour names — shown as swatches\"\n className=\"mt-4\"\n />\n <TextField\n id=\"favouriteBrands\"\n label=\"Favourite brands\"\n value={form.favouriteBrands}\n onChange={(v) => setField('favouriteBrands', v)}\n placeholder=\"Aurora Atelier, …\"\n hint=\"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\">Sizes</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={titleCase(key)}\n value={form.sizes[key] ?? ''}\n onChange={(v) => setSize(key, v)}\n placeholder=\"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 ? upsert.error.message : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"submit\"\n disabled={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 ? 'Save changes' : '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":"WardrobeListPage.d.ts","sourceRoot":"","sources":["../../../../src/coolandlovely/pages/wardrobe/WardrobeListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAwBH,wBAAgB,gBAAgB,gCA2M/B"}
1
+ {"version":3,"file":"WardrobeListPage.d.ts","sourceRoot":"","sources":["../../../../src/coolandlovely/pages/wardrobe/WardrobeListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAuBH,wBAAgB,gBAAgB,gCAkH/B"}
@@ -1,19 +1,18 @@
1
1
  import { useCoolAndLovely as e } from "../../context/CoolAndLovelyContext.js";
2
2
  import { cn as t } from "../../utils/cn.js";
3
3
  import { PageLayout as n } from "../../components/PageLayout.js";
4
- import { StatusBadge as r } from "../../components/StatusBadge.js";
5
- import { ErrorView as i, LoadingView as a } from "../../components/StateViews.js";
6
- import { Thumbnail as o } from "../../components/Thumbnail.js";
7
- import { useLogWardrobeWear as s, useRemoveWardrobeItem as c, useWardrobeItems as l } from "../../hooks/useCoolAndLovelyApi.js";
8
- import { formatPrice as u, formatRelativeTime as d, titleCase as f } from "../../utils/formatters.js";
4
+ import { ErrorView as r, LoadingView as i } from "../../components/StateViews.js";
5
+ import { useLogWardrobeWear as a, useRemoveWardrobeItem as o, useWardrobeItems as s } from "../../hooks/useCoolAndLovelyApi.js";
6
+ import { titleCase as c } from "../../utils/formatters.js";
9
7
  import "../../hooks/index.js";
10
- import { STYLE_CATEGORIES as p, categoryIcon as m } from "../../utils/icons.js";
11
- import { EmptyState as h } from "../../components/EmptyState.js";
12
- import { useState as g } from "react";
13
- import { CheckCircle2 as _, Heart as v, Loader2 as y, Plus as b, Trash2 as x } from "lucide-react";
14
- import { jsx as S, jsxs as C } from "react/jsx-runtime";
8
+ import { STYLE_CATEGORIES as l, categoryIcon as u } from "../../utils/icons.js";
9
+ import { EmptyState as d } from "../../components/EmptyState.js";
10
+ import { WardrobeItemCard as f } from "../../components/WardrobeItemCard.js";
11
+ import { useState as p } from "react";
12
+ import { Heart as m, Plus as h } from "lucide-react";
13
+ import { jsx as g, jsxs as _ } from "react/jsx-runtime";
15
14
  //#region src/coolandlovely/pages/wardrobe/WardrobeListPage.tsx
16
- var w = [
15
+ var v = [
17
16
  {
18
17
  value: "ALL",
19
18
  label: "All"
@@ -35,146 +34,73 @@ var w = [
35
34
  label: "Gifted"
36
35
  }
37
36
  ];
38
- function T() {
39
- let { navigateTo: T } = e(), [E, D] = g("ALL"), [O, k] = g("ALL"), A = l({
40
- category: E === "ALL" ? void 0 : E,
41
- source: O === "ALL" ? void 0 : O
42
- }), j = s(), M = c(), N = A.data ?? [];
43
- return /* @__PURE__ */ C(n, {
37
+ function y() {
38
+ let { navigateTo: y } = e(), [b, x] = p("ALL"), [S, C] = p("ALL"), w = s({
39
+ category: b === "ALL" ? void 0 : b,
40
+ source: S === "ALL" ? void 0 : S
41
+ }), T = a(), E = o(), D = w.data ?? [];
42
+ return /* @__PURE__ */ _(n, {
44
43
  title: "My Wardrobe",
45
44
  description: "A private catalogue of what you own — with a wear log",
46
- actions: /* @__PURE__ */ C("button", {
45
+ actions: /* @__PURE__ */ _("button", {
47
46
  type: "button",
48
- onClick: () => T("/wardrobe/new"),
47
+ onClick: () => y("/wardrobe/new"),
49
48
  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",
50
- children: [/* @__PURE__ */ S(b, { className: "h-4 w-4" }), " Add piece"]
49
+ children: [/* @__PURE__ */ g(h, { className: "h-4 w-4" }), " Add piece"]
51
50
  }),
52
51
  children: [
53
- /* @__PURE__ */ S("div", {
52
+ /* @__PURE__ */ g("div", {
54
53
  className: "mb-3 flex flex-wrap items-center gap-2",
55
- children: w.map((e) => /* @__PURE__ */ S("button", {
54
+ children: v.map((e) => /* @__PURE__ */ g("button", {
56
55
  type: "button",
57
- onClick: () => k(e.value),
58
- className: t("rounded-full px-3 py-1.5 text-sm font-medium transition-colors", O === e.value ? "bg-action-primary-bg text-action-primary-text" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
56
+ onClick: () => C(e.value),
57
+ className: t("rounded-full px-3 py-1.5 text-sm font-medium transition-colors", S === e.value ? "bg-action-primary-bg text-action-primary-text" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
59
58
  children: e.label
60
59
  }, e.value))
61
60
  }),
62
- /* @__PURE__ */ C("div", {
61
+ /* @__PURE__ */ _("div", {
63
62
  className: "mb-6 flex flex-wrap items-center gap-2",
64
- children: [/* @__PURE__ */ S("button", {
63
+ children: [/* @__PURE__ */ g("button", {
65
64
  type: "button",
66
- onClick: () => D("ALL"),
67
- className: t("rounded-full px-3 py-1.5 text-xs font-medium transition-colors", E === "ALL" ? "bg-accent-purple-subtle text-text-primary" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
65
+ onClick: () => x("ALL"),
66
+ className: t("rounded-full px-3 py-1.5 text-xs font-medium transition-colors", b === "ALL" ? "bg-accent-purple-subtle text-text-primary" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
68
67
  children: "All categories"
69
- }), p.map((e) => /* @__PURE__ */ C("button", {
68
+ }), l.map((e) => /* @__PURE__ */ _("button", {
70
69
  type: "button",
71
- onClick: () => D(e),
72
- className: t("inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors", E === e ? "bg-accent-purple-subtle text-text-primary" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
70
+ onClick: () => x(e),
71
+ className: t("inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors", b === e ? "bg-accent-purple-subtle text-text-primary" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
73
72
  children: [
74
- m(e, 14),
73
+ u(e, 14),
75
74
  " ",
76
- f(e)
75
+ c(e)
77
76
  ]
78
77
  }, e))]
79
78
  }),
80
- A.isLoading ? /* @__PURE__ */ S(a, { label: "Loading your wardrobe…" }) : A.isError ? /* @__PURE__ */ S(i, { onRetry: () => void A.refetch() }) : N.length === 0 ? /* @__PURE__ */ S(h, {
79
+ w.isLoading ? /* @__PURE__ */ g(i, { label: "Loading your wardrobe…" }) : w.isError ? /* @__PURE__ */ g(r, { onRetry: () => void w.refetch() }) : D.length === 0 ? /* @__PURE__ */ g(d, {
81
80
  illustration: "empty-data",
82
- icon: /* @__PURE__ */ S(v, { className: "h-10 w-10" }),
81
+ icon: /* @__PURE__ */ g(m, { className: "h-10 w-10" }),
83
82
  title: "Your wardrobe is empty",
84
83
  description: "Add the pieces you already own (or want) to start your digital closet.",
85
- action: /* @__PURE__ */ C("button", {
84
+ action: /* @__PURE__ */ _("button", {
86
85
  type: "button",
87
- onClick: () => T("/wardrobe/new"),
86
+ onClick: () => y("/wardrobe/new"),
88
87
  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",
89
- children: [/* @__PURE__ */ S(b, { className: "h-4 w-4" }), " Add piece"]
88
+ children: [/* @__PURE__ */ g(h, { className: "h-4 w-4" }), " Add piece"]
90
89
  })
91
- }) : /* @__PURE__ */ S("div", {
90
+ }) : /* @__PURE__ */ g("div", {
92
91
  className: "grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
93
- children: N.map((e) => {
94
- let t = e.brandName || e.colour || f(e.category), n = j.isPending && j.variables === e.id, i = M.isPending && M.variables === e.id;
95
- return /* @__PURE__ */ C("div", {
96
- className: "group flex flex-col overflow-hidden rounded-xl border border-border-seam bg-bg-surface shadow-sm transition-shadow hover:shadow-lg",
97
- children: [/* @__PURE__ */ C("div", {
98
- className: "relative",
99
- children: [
100
- /* @__PURE__ */ S(o, {
101
- icon: m(e.category, 40),
102
- seed: e.id,
103
- fileId: e.image?.id,
104
- alt: e.name,
105
- className: "aspect-[4/5] w-full rounded-none"
106
- }),
107
- /* @__PURE__ */ C("div", {
108
- className: "absolute right-2.5 top-2.5 flex h-12 w-12 flex-col items-center justify-center rounded-full bg-bg-surface/90 text-center leading-none shadow-sm ring-1 ring-[var(--color-accent-soft)] backdrop-blur",
109
- children: [/* @__PURE__ */ S("span", {
110
- className: "text-[9px] font-medium uppercase tracking-wide text-text-secondary",
111
- children: "worn"
112
- }), /* @__PURE__ */ C("span", {
113
- className: "text-sm font-bold tabular-nums text-text-primary",
114
- children: [e.timesWorn, "×"]
115
- })]
116
- }),
117
- /* @__PURE__ */ C("div", {
118
- className: "absolute inset-x-0 bottom-0 flex items-center gap-1.5 bg-gradient-to-t from-bg-canvas/90 to-transparent p-2.5 pt-8 opacity-100 transition-opacity duration-200 sm:opacity-0 sm:focus-within:opacity-100 sm:group-hover:opacity-100",
119
- children: [/* @__PURE__ */ C("button", {
120
- type: "button",
121
- disabled: j.isPending,
122
- onClick: () => j.mutate(e.id),
123
- "aria-label": `Log a wear for ${e.name}`,
124
- className: "inline-flex items-center gap-1 rounded-lg bg-bg-surface/90 px-2.5 py-1 text-xs font-medium text-status-success-text shadow-sm backdrop-blur transition-colors hover:bg-bg-surface disabled:opacity-50",
125
- children: [n ? /* @__PURE__ */ S(y, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ S(_, { className: "h-3.5 w-3.5" }), "Wore it"]
126
- }), /* @__PURE__ */ S("button", {
127
- type: "button",
128
- disabled: M.isPending,
129
- onClick: () => M.mutate(e.id),
130
- "aria-label": `Remove ${e.name}`,
131
- className: "ml-auto inline-flex h-7 w-7 items-center justify-center rounded-lg bg-bg-surface/90 text-text-secondary shadow-sm backdrop-blur transition-colors hover:text-status-error-text disabled:opacity-50",
132
- children: i ? /* @__PURE__ */ S(y, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ S(x, { className: "h-3.5 w-3.5" })
133
- })]
134
- })
135
- ]
136
- }), /* @__PURE__ */ C("div", {
137
- className: "flex flex-1 flex-col p-4",
138
- children: [
139
- /* @__PURE__ */ S("p", {
140
- className: "truncate text-sm font-semibold text-text-primary",
141
- children: e.name
142
- }),
143
- /* @__PURE__ */ S("p", {
144
- className: "truncate text-xs text-text-secondary",
145
- children: t
146
- }),
147
- /* @__PURE__ */ C("div", {
148
- className: "mt-2 flex flex-wrap items-center gap-1.5",
149
- children: [/* @__PURE__ */ S(r, { status: e.source }), e.size && /* @__PURE__ */ S("span", {
150
- className: "rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary",
151
- children: e.size
152
- })]
153
- }),
154
- e.notes && /* @__PURE__ */ S("p", {
155
- className: "mt-2 line-clamp-2 text-xs text-text-secondary",
156
- children: e.notes
157
- }),
158
- /* @__PURE__ */ C("div", {
159
- className: "mt-3 flex items-center justify-between gap-2 border-t border-border-seam pt-2 text-xs text-text-secondary",
160
- children: [/* @__PURE__ */ S("span", { children: e.lastWornAt ? `Last worn ${d(e.lastWornAt)}` : "Not worn yet" }), e.costPerWear == null ? e.purchasePrice == null ? null : /* @__PURE__ */ C("span", {
161
- className: "shrink-0 text-text-secondary",
162
- children: [u(e.purchasePrice, e.currency ?? "USD"), " paid"]
163
- }) : /* @__PURE__ */ C("span", {
164
- title: `${u(e.purchasePrice ?? 0, e.currency ?? "USD")} ÷ ${e.timesWorn} wears`,
165
- className: "shrink-0 font-medium text-text-primary",
166
- children: [u(e.costPerWear, e.currency ?? "USD"), "/wear"]
167
- })]
168
- })
169
- ]
170
- })]
171
- }, e.id);
172
- })
92
+ children: D.map((e) => /* @__PURE__ */ g(f, {
93
+ item: e,
94
+ onLogWear: () => T.mutate(e.id),
95
+ onRemove: () => E.mutate(e.id),
96
+ isLoggingWear: T.isPending && T.variables === e.id,
97
+ isRemoving: E.isPending && E.variables === e.id
98
+ }, e.id))
173
99
  })
174
100
  ]
175
101
  });
176
102
  }
177
103
  //#endregion
178
- export { T as WardrobeListPage };
104
+ export { y as WardrobeListPage };
179
105
 
180
106
  //# sourceMappingURL=WardrobeListPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"WardrobeListPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/wardrobe/WardrobeListPage.tsx"],"sourcesContent":["/**\n * Wardrobe List Page (self-scoped)\n * @module coolandlovely/pages/wardrobe\n *\n * The shopper's digital wardrobe (`cooListWardrobeItems`, self-scoped) rendered\n * as an image-forward gallery — each card is a large piece photo with a circular\n * \"Worn N×\" badge and a hover-revealed action bar (wear log via\n * `cooLogWardrobeWear`, removal via `cooRemoveWardrobeItem`), with category &\n * source filters.\n */\n\nimport { useState } from 'react';\nimport { Plus, Heart, CheckCircle2, Trash2, Loader2 } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useWardrobeItems, useLogWardrobeWear, useRemoveWardrobeItem } from '../../hooks';\nimport { categoryIcon, STYLE_CATEGORIES } from '../../utils/icons';\nimport { titleCase, formatRelativeTime, formatPrice } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooStyleCategory, CooWardrobeSource } from '../../types';\n\nconst SOURCES: Array<{ value: CooWardrobeSource | 'ALL'; label: string }> = [\n { value: 'ALL', label: 'All' },\n { value: 'OWNED', label: 'Owned' },\n { value: 'WISHLIST', label: 'Wishlist' },\n { value: 'PURCHASED_ON_PLATFORM', label: 'Purchased' },\n { value: 'GIFTED', label: 'Gifted' },\n];\n\nexport function WardrobeListPage() {\n const { navigateTo } = useCoolAndLovely();\n const [category, setCategory] = useState<CooStyleCategory | 'ALL'>('ALL');\n const [source, setSource] = useState<CooWardrobeSource | 'ALL'>('ALL');\n\n const query = useWardrobeItems({\n category: category === 'ALL' ? undefined : category,\n source: source === 'ALL' ? undefined : source,\n });\n const logWear = useLogWardrobeWear();\n const remove = useRemoveWardrobeItem();\n\n const items = query.data ?? [];\n\n return (\n <PageLayout\n title=\"My Wardrobe\"\n description=\"A private catalogue of what you own — with a wear log\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/wardrobe/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\" /> Add piece\n </button>\n }\n >\n {/* Source filter */}\n <div className=\"mb-3 flex flex-wrap items-center gap-2\">\n {SOURCES.map((opt) => (\n <button\n key={opt.value}\n type=\"button\"\n onClick={() => setSource(opt.value)}\n className={cn(\n 'rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n source === opt.value\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {opt.label}\n </button>\n ))}\n </div>\n\n {/* Category filter */}\n <div className=\"mb-6 flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setCategory('ALL')}\n className={cn(\n 'rounded-full px-3 py-1.5 text-xs font-medium transition-colors',\n category === 'ALL'\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n All categories\n </button>\n {STYLE_CATEGORIES.map((c) => (\n <button\n key={c}\n type=\"button\"\n onClick={() => setCategory(c)}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors',\n category === c\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {categoryIcon(c, 14)} {titleCase(c)}\n </button>\n ))}\n </div>\n\n {query.isLoading ? (\n <LoadingView label=\"Loading your wardrobe…\" />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Heart className=\"h-10 w-10\" />}\n title=\"Your wardrobe is empty\"\n description=\"Add the pieces you already own (or want) to start your digital closet.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/wardrobe/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\" /> Add piece\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((item) => {\n const subtitle = item.brandName || item.colour || titleCase(item.category);\n const wearing = logWear.isPending && logWear.variables === item.id;\n const removing = remove.isPending && remove.variables === item.id;\n return (\n <div\n key={item.id}\n className=\"group flex flex-col overflow-hidden rounded-xl border border-border-seam bg-bg-surface shadow-sm transition-shadow hover:shadow-lg\"\n >\n {/* Image-forward hero */}\n <div className=\"relative\">\n <Thumbnail\n icon={categoryIcon(item.category, 40)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"aspect-[4/5] w-full rounded-none\"\n />\n\n {/* Worn-count badge */}\n <div className=\"absolute right-2.5 top-2.5 flex h-12 w-12 flex-col items-center justify-center rounded-full bg-bg-surface/90 text-center leading-none shadow-sm ring-1 ring-[var(--color-accent-soft)] backdrop-blur\">\n <span className=\"text-[9px] font-medium uppercase tracking-wide text-text-secondary\">\n worn\n </span>\n <span className=\"text-sm font-bold tabular-nums text-text-primary\">\n {item.timesWorn}×\n </span>\n </div>\n\n {/* Hover action bar (keeps the resting card pure imagery) */}\n <div className=\"absolute inset-x-0 bottom-0 flex items-center gap-1.5 bg-gradient-to-t from-bg-canvas/90 to-transparent p-2.5 pt-8 opacity-100 transition-opacity duration-200 sm:opacity-0 sm:focus-within:opacity-100 sm:group-hover:opacity-100\">\n <button\n type=\"button\"\n disabled={logWear.isPending}\n onClick={() => logWear.mutate(item.id)}\n aria-label={`Log a wear for ${item.name}`}\n className=\"inline-flex items-center gap-1 rounded-lg bg-bg-surface/90 px-2.5 py-1 text-xs font-medium text-status-success-text shadow-sm backdrop-blur transition-colors hover:bg-bg-surface disabled:opacity-50\"\n >\n {wearing ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"h-3.5 w-3.5\" />\n )}\n Wore it\n </button>\n <button\n type=\"button\"\n disabled={remove.isPending}\n onClick={() => remove.mutate(item.id)}\n aria-label={`Remove ${item.name}`}\n className=\"ml-auto inline-flex h-7 w-7 items-center justify-center rounded-lg bg-bg-surface/90 text-text-secondary shadow-sm backdrop-blur transition-colors hover:text-status-error-text disabled:opacity-50\"\n >\n {removing ? (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n ) : (\n <Trash2 className=\"h-3.5 w-3.5\" />\n )}\n </button>\n </div>\n </div>\n\n {/* Details */}\n <div className=\"flex flex-1 flex-col p-4\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">{item.name}</p>\n <p className=\"truncate text-xs text-text-secondary\">{subtitle}</p>\n <div className=\"mt-2 flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={item.source} />\n {item.size && (\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\">\n {item.size}\n </span>\n )}\n </div>\n {item.notes && (\n <p className=\"mt-2 line-clamp-2 text-xs text-text-secondary\">{item.notes}</p>\n )}\n <div className=\"mt-3 flex items-center justify-between gap-2 border-t border-border-seam pt-2 text-xs text-text-secondary\">\n <span>\n {item.lastWornAt\n ? `Last worn ${formatRelativeTime(item.lastWornAt)}`\n : 'Not worn yet'}\n </span>\n {item.costPerWear != null ? (\n <span\n title={`${formatPrice(item.purchasePrice ?? 0, item.currency ?? 'USD')} ÷ ${item.timesWorn} wears`}\n className=\"shrink-0 font-medium text-text-primary\"\n >\n {formatPrice(item.costPerWear, item.currency ?? 'USD')}/wear\n </span>\n ) : item.purchasePrice != null ? (\n <span className=\"shrink-0 text-text-secondary\">\n {formatPrice(item.purchasePrice, item.currency ?? 'USD')} paid\n </span>\n ) : null}\n </div>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"WardrobeListPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/wardrobe/WardrobeListPage.tsx"],"sourcesContent":["/**\n * Wardrobe List Page (self-scoped)\n * @module coolandlovely/pages/wardrobe\n *\n * The shopper's digital wardrobe (`cooListWardrobeItems`, self-scoped) rendered\n * as an image-forward gallery — each card is a large piece photo with a circular\n * \"Worn N×\" badge and a hover-revealed action bar (wear log via\n * `cooLogWardrobeWear`, removal via `cooRemoveWardrobeItem`), with category &\n * source filters.\n */\n\nimport { useState } from 'react';\nimport { Plus, Heart } from 'lucide-react';\nimport { PageLayout } from '../../components/PageLayout';\nimport { WardrobeItemCard } from '../../components/WardrobeItemCard';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useWardrobeItems, useLogWardrobeWear, useRemoveWardrobeItem } from '../../hooks';\nimport { categoryIcon, STYLE_CATEGORIES } from '../../utils/icons';\nimport { titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooStyleCategory, CooWardrobeSource } from '../../types';\n\nconst SOURCES: Array<{ value: CooWardrobeSource | 'ALL'; label: string }> = [\n { value: 'ALL', label: 'All' },\n { value: 'OWNED', label: 'Owned' },\n { value: 'WISHLIST', label: 'Wishlist' },\n { value: 'PURCHASED_ON_PLATFORM', label: 'Purchased' },\n { value: 'GIFTED', label: 'Gifted' },\n];\n\nexport function WardrobeListPage() {\n const { navigateTo } = useCoolAndLovely();\n const [category, setCategory] = useState<CooStyleCategory | 'ALL'>('ALL');\n const [source, setSource] = useState<CooWardrobeSource | 'ALL'>('ALL');\n\n const query = useWardrobeItems({\n category: category === 'ALL' ? undefined : category,\n source: source === 'ALL' ? undefined : source,\n });\n const logWear = useLogWardrobeWear();\n const remove = useRemoveWardrobeItem();\n\n const items = query.data ?? [];\n\n return (\n <PageLayout\n title=\"My Wardrobe\"\n description=\"A private catalogue of what you own — with a wear log\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/wardrobe/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\" /> Add piece\n </button>\n }\n >\n {/* Source filter */}\n <div className=\"mb-3 flex flex-wrap items-center gap-2\">\n {SOURCES.map((opt) => (\n <button\n key={opt.value}\n type=\"button\"\n onClick={() => setSource(opt.value)}\n className={cn(\n 'rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n source === opt.value\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {opt.label}\n </button>\n ))}\n </div>\n\n {/* Category filter */}\n <div className=\"mb-6 flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setCategory('ALL')}\n className={cn(\n 'rounded-full px-3 py-1.5 text-xs font-medium transition-colors',\n category === 'ALL'\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n All categories\n </button>\n {STYLE_CATEGORIES.map((c) => (\n <button\n key={c}\n type=\"button\"\n onClick={() => setCategory(c)}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors',\n category === c\n ? 'bg-accent-purple-subtle text-text-primary'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {categoryIcon(c, 14)} {titleCase(c)}\n </button>\n ))}\n </div>\n\n {query.isLoading ? (\n <LoadingView label=\"Loading your wardrobe…\" />\n ) : query.isError ? (\n <ErrorView onRetry={() => void query.refetch()} />\n ) : items.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Heart className=\"h-10 w-10\" />}\n title=\"Your wardrobe is empty\"\n description=\"Add the pieces you already own (or want) to start your digital closet.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/wardrobe/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\" /> Add piece\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((item) => (\n <WardrobeItemCard\n key={item.id}\n item={item}\n onLogWear={() => logWear.mutate(item.id)}\n onRemove={() => remove.mutate(item.id)}\n isLoggingWear={logWear.isPending && logWear.variables === item.id}\n isRemoving={remove.isPending && remove.variables === item.id}\n />\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,IAAM,IAAsE;CAC1E;EAAE,OAAO;EAAO,OAAO;EAAO;CAC9B;EAAE,OAAO;EAAS,OAAO;EAAS;CAClC;EAAE,OAAO;EAAY,OAAO;EAAY;CACxC;EAAE,OAAO;EAAyB,OAAO;EAAa;CACtD;EAAE,OAAO;EAAU,OAAO;EAAU;CACrC;AAED,SAAgB,IAAmB;CACjC,IAAM,EAAE,kBAAe,GAAkB,EACnC,CAAC,GAAU,KAAe,EAAmC,MAAM,EACnE,CAAC,GAAQ,KAAa,EAAoC,MAAM,EAEhE,IAAQ,EAAiB;EAC7B,UAAU,MAAa,QAAQ,KAAA,IAAY;EAC3C,QAAQ,MAAW,QAAQ,KAAA,IAAY;EACxC,CAAC,EACI,IAAU,GAAoB,EAC9B,IAAS,GAAuB,EAEhC,IAAQ,EAAM,QAAQ,EAAE;AAE9B,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAW,gBAAgB;GAC1C,WAAU;aAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,aACrB;;YAVb;GAcE,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAQ,KAAK,MACZ,kBAAC,UAAD;KAEE,MAAK;KACL,eAAe,EAAU,EAAI,MAAM;KACnC,WAAW,EACT,kEACA,MAAW,EAAI,QACX,kDACA,2DACL;eAEA,EAAI;KACE,EAXF,EAAI,MAWF,CACT;IACE,CAAA;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAY,MAAM;KACjC,WAAW,EACT,kEACA,MAAa,QACT,8CACA,2DACL;eACF;KAEQ,CAAA,EACR,EAAiB,KAAK,MACrB,kBAAC,UAAD;KAEE,MAAK;KACL,eAAe,EAAY,EAAE;KAC7B,WAAW,EACT,mGACA,MAAa,IACT,8CACA,2DACL;eATH;MAWG,EAAa,GAAG,GAAG;MAAC;MAAE,EAAU,EAAE;MAC5B;OAXF,EAWE,CACT,CACE;;GAEL,EAAM,YACL,kBAAC,GAAD,EAAa,OAAM,0BAA2B,CAAA,GAC5C,EAAM,UACR,kBAAC,GAAD,EAAW,eAAe,KAAK,EAAM,SAAS,EAAI,CAAA,GAChD,EAAM,WAAW,IACnB,kBAAC,GAAD;IACE,cAAa;IACb,MAAM,kBAAC,GAAD,EAAO,WAAU,aAAc,CAAA;IACrC,OAAM;IACN,aAAY;IACZ,QACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,gBAAgB;KAC1C,WAAU;eAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,aACrB;;IAEX,CAAA,GAEF,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAM,KAAK,MACV,kBAAC,GAAD;KAEQ;KACN,iBAAiB,EAAQ,OAAO,EAAK,GAAG;KACxC,gBAAgB,EAAO,OAAO,EAAK,GAAG;KACtC,eAAe,EAAQ,aAAa,EAAQ,cAAc,EAAK;KAC/D,YAAY,EAAO,aAAa,EAAO,cAAc,EAAK;KAC1D,EANK,EAAK,GAMV,CACF;IACE,CAAA;GAEG"}
@@ -0,0 +1,10 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { BrandCard } from '../coolandlovely/components/BrandCard';
3
+
4
+ declare const meta: Meta<typeof BrandCard>;
5
+ export default meta;
6
+ type Story = StoryObj<typeof meta>;
7
+ export declare const Default: Story;
8
+ export declare const Featured: Story;
9
+ export declare const Draft: Story;
10
+ //# sourceMappingURL=BrandCard.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BrandCard.stories.d.ts","sourceRoot":"","sources":["../../src/stories/BrandCard.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EAAE,SAAS,EAAE,MAAM,uCAAuC,CAAC;AAGlE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,SAAS,CAShC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAIrB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KActB,CAAC;AAEF,eAAO,MAAM,KAAK,EAAE,KASnB,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { CollectionCard } from '../coolandlovely/components/CollectionCard';
3
+
4
+ declare const meta: Meta<typeof CollectionCard>;
5
+ export default meta;
6
+ type Story = StoryObj<typeof meta>;
7
+ export declare const Default: Story;
8
+ export declare const LiveCollection: Story;
9
+ export declare const Scheduled: Story;
10
+ //# sourceMappingURL=CollectionCard.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CollectionCard.stories.d.ts","sourceRoot":"","sources":["../../src/stories/CollectionCard.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EAAE,cAAc,EAAE,MAAM,4CAA4C,CAAC;AAG5E,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,cAAc,CASrC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAIrB,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,KAe5B,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KASvB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { FeedbackWidget } from '../coolandlovely/components/FeedbackWidget';
3
+
4
+ declare const meta: Meta<typeof FeedbackWidget>;
5
+ export default meta;
6
+ type Story = StoryObj<typeof meta>;
7
+ export declare const Default: Story;
8
+ export declare const OpensAndSubmits: Story;
9
+ //# sourceMappingURL=FeedbackWidget.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeedbackWidget.stories.d.ts","sourceRoot":"","sources":["../../src/stories/FeedbackWidget.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,cAAc,EAAE,MAAM,4CAA4C,CAAC;AAE5E,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,cAAc,CAMrC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAAU,CAAC;AAEjC,eAAO,MAAM,eAAe,EAAE,KAoB7B,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { LookCard } from '../coolandlovely/components/LookCard';
3
+
4
+ declare const meta: Meta<typeof LookCard>;
5
+ export default meta;
6
+ type Story = StoryObj<typeof meta>;
7
+ export declare const Default: Story;
8
+ export declare const WithActions: Story;
9
+ export declare const LikedAndSaved: Story;
10
+ //# sourceMappingURL=LookCard.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LookCard.stories.d.ts","sourceRoot":"","sources":["../../src/stories/LookCard.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EAAE,QAAQ,EAAE,MAAM,sCAAsC,CAAC;AAGhE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,QAAQ,CAU/B,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAWrB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KA4BzB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,KAiB3B,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { LookbookCard } from '../coolandlovely/components/LookbookCard';
3
+
4
+ declare const meta: Meta<typeof LookbookCard>;
5
+ export default meta;
6
+ type Story = StoryObj<typeof meta>;
7
+ export declare const Default: Story;
8
+ export declare const PublicLookbook: Story;
9
+ export declare const PrivateEmpty: Story;
10
+ //# sourceMappingURL=LookbookCard.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LookbookCard.stories.d.ts","sourceRoot":"","sources":["../../src/stories/LookbookCard.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,0CAA0C,CAAC;AAGxE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,YAAY,CASnC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAIrB,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,KAc5B,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,KAU1B,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { WardrobeItemCard } from '../coolandlovely/components/WardrobeItemCard';
3
+
4
+ declare const meta: Meta<typeof WardrobeItemCard>;
5
+ export default meta;
6
+ type Story = StoryObj<typeof meta>;
7
+ export declare const Default: Story;
8
+ export declare const OwnedPiece: Story;
9
+ export declare const Wishlist: Story;
10
+ export declare const LoggingWear: Story;
11
+ //# sourceMappingURL=WardrobeItemCard.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WardrobeItemCard.stories.d.ts","sourceRoot":"","sources":["../../src/stories/WardrobeItemCard.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,8CAA8C,CAAC;AAGhF,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,gBAAgB,CAWvC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAIrB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAsBxB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAUtB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAUzB,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { CooBrand, CooCollection, CooLook, CooLookbook, CooWardrobeItem } from '../../coolandlovely/types';
2
+
3
+ export declare const mockBrand: CooBrand;
4
+ export declare const mockDraftBrand: CooBrand;
5
+ export declare const mockCollection: CooCollection;
6
+ export declare const mockScheduledCollection: CooCollection;
7
+ export declare const mockLook: CooLook;
8
+ export declare const mockLikedLook: CooLook;
9
+ export declare const mockLookbook: CooLookbook;
10
+ export declare const mockPrivateLookbook: CooLookbook;
11
+ export declare const mockWardrobeItem: CooWardrobeItem;
12
+ export declare const mockWishlistItem: CooWardrobeItem;
13
+ //# sourceMappingURL=coolandlovely.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coolandlovely.d.ts","sourceRoot":"","sources":["../../../src/stories/__fixtures__/coolandlovely.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,QAAQ,EACR,aAAa,EACb,OAAO,EACP,WAAW,EACX,eAAe,EAChB,MAAM,2BAA2B,CAAC;AAEnC,eAAO,MAAM,SAAS,EAAE,QAgBvB,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,QAQ5B,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,aAe5B,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,aAOrC,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,OAmBtB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,OAQ3B,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,WAoC1B,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,WAQjC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,eAqB9B,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,eAe9B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-coolandlovely",
3
- "version": "2026.805.1",
3
+ "version": "2026.820.2",
4
4
  "description": "Burdenoff CoolAndLovely Microfrontend",
5
5
  "type": "module",
6
6
  "files": [
@@ -38,6 +38,7 @@
38
38
  "storybook": "storybook dev -p 6006",
39
39
  "build-storybook": "storybook build",
40
40
  "test:storybook": "test-storybook",
41
+ "test:storybook:ci": "bun run build-storybook -- --output-dir storybook-static && python3 -m http.server 6006 --directory storybook-static > /dev/null 2>&1 & SERVER_PID=$! && sleep 3 && bun run test:storybook; EXIT_CODE=$?; kill $SERVER_PID 2>/dev/null; exit $EXIT_CODE",
41
42
  "switch:status": "bun scripts/deps-mode.ts status",
42
43
  "switch:remote": "bun scripts/deps-mode.ts remote",
43
44
  "switch:local": "bun scripts/deps-mode.ts local",
@@ -82,6 +83,7 @@
82
83
  "typescript-eslint": "^8.46.0",
83
84
  "vite": "8.0.8",
84
85
  "vite-plugin-dts": "3.9.1",
86
+ "@storybook/addon-a11y": "^8.6.0",
85
87
  "@storybook/addon-essentials": "^8.6.0",
86
88
  "@storybook/addon-interactions": "^8.6.0",
87
89
  "@storybook/react": "^8.6.0",
@@ -89,6 +91,7 @@
89
91
  "@storybook/test": "^8.6.0",
90
92
  "@storybook/test-runner": "0.22.0",
91
93
  "@tailwindcss/vite": "^4.3.2",
94
+ "playwright": "^1.50.0",
92
95
  "storybook": "^8.6.0"
93
96
  },
94
97
  "peerDependencies": {