@burdenoff/microfe-coolandlovely 2026.706.2 → 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.
- package/dist/coolandlovely/CoolAndLovelyRoot.js +62 -46
- package/dist/coolandlovely/CoolAndLovelyRoot.js.map +1 -1
- package/dist/coolandlovely/components/AddToLookbookButton.js +22 -22
- package/dist/coolandlovely/components/AddToLookbookButton.js.map +1 -1
- package/dist/coolandlovely/components/BrandCover.js +39 -0
- package/dist/coolandlovely/components/BrandCover.js.map +1 -0
- package/dist/coolandlovely/components/BrandCreatorAvatars.js +57 -0
- package/dist/coolandlovely/components/BrandCreatorAvatars.js.map +1 -0
- package/dist/coolandlovely/components/ColourSwatchRow.js +86 -0
- package/dist/coolandlovely/components/ColourSwatchRow.js.map +1 -0
- package/dist/coolandlovely/components/Comments.js +4 -4
- package/dist/coolandlovely/components/Comments.js.map +1 -1
- package/dist/coolandlovely/components/FileImage.js.map +1 -1
- package/dist/coolandlovely/components/ImageUploadField.js +139 -57
- package/dist/coolandlovely/components/ImageUploadField.js.map +1 -1
- package/dist/coolandlovely/components/LookCard.js +77 -0
- package/dist/coolandlovely/components/LookCard.js.map +1 -0
- package/dist/coolandlovely/components/MetricsCard.js +1 -1
- package/dist/coolandlovely/components/MetricsCard.js.map +1 -1
- package/dist/coolandlovely/components/ShopItemCard.js +45 -0
- package/dist/coolandlovely/components/ShopItemCard.js.map +1 -0
- package/dist/coolandlovely/components/StepTracker.js +35 -0
- package/dist/coolandlovely/components/StepTracker.js.map +1 -0
- package/dist/coolandlovely/components/Thumbnail.js +15 -11
- package/dist/coolandlovely/components/Thumbnail.js.map +1 -1
- package/dist/coolandlovely/components/VisibilityChip.js +32 -0
- package/dist/coolandlovely/components/VisibilityChip.js.map +1 -0
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js +94 -69
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js.map +1 -1
- package/dist/coolandlovely/hooks/useFiles.js +49 -32
- package/dist/coolandlovely/hooks/useFiles.js.map +1 -1
- package/dist/coolandlovely/pages/HomePage.js +112 -133
- package/dist/coolandlovely/pages/HomePage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandDetailPage.js +253 -242
- package/dist/coolandlovely/pages/brands/BrandDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandFormPage.js +116 -112
- package/dist/coolandlovely/pages/brands/BrandFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandsListPage.js +13 -13
- package/dist/coolandlovely/pages/brands/BrandsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js +117 -95
- package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js +50 -43
- package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionDetailPage.js +131 -108
- package/dist/coolandlovely/pages/collections/CollectionDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionFormPage.js +1 -1
- package/dist/coolandlovely/pages/collections/CollectionsListPage.js +59 -56
- package/dist/coolandlovely/pages/collections/CollectionsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/discover/DiscoverPage.js +1 -1
- package/dist/coolandlovely/pages/discover/DiscoverPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js +175 -147
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemFormPage.js +1 -1
- package/dist/coolandlovely/pages/items/StyleItemsListPage.js +71 -62
- package/dist/coolandlovely/pages/items/StyleItemsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js +149 -127
- package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js +69 -53
- package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookDetailPage.js +135 -113
- package/dist/coolandlovely/pages/looks/LookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookFormPage.js +122 -110
- package/dist/coolandlovely/pages/looks/LookFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LooksListPage.js +60 -97
- package/dist/coolandlovely/pages/looks/LooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js +372 -210
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js.map +1 -1
- package/dist/coolandlovely/pages/trends/TrendsPage.js +1 -1
- package/dist/coolandlovely/pages/trends/TrendsPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js +130 -94
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js +75 -76
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StyleProfilePage.js","names":[],"sources":["../../../../src/coolandlovely/pages/profile/StyleProfilePage.tsx"],"sourcesContent":["/**\n * Style Profile Page (self-scoped)\n * @module coolandlovely/pages/profile\n *\n * Views and edits the calling shopper's style profile (`cooMyStyleProfile` +\n * `cooUpsertStyleProfile`). One profile per workspace per shopper.\n */\n\nimport { useEffect, useState } from 'react';\nimport { Loader2, Save, UserCircle, Pencil, X, Users } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { TagList } from '../../components/TagList';\nimport { TextField, TextAreaField, SelectField } from '../../components/Field';\nimport { ActorName } from '../../components/ActorName';\nimport { FollowButton } from '../../components/FollowButton';\nimport { useMyStyleProfile, useUpsertStyleProfile, useMyFollowing } from '../../hooks';\nimport { parseCsv, titleCase } from '../../utils/formatters';\nimport type { CooVisibility } from '../../types';\n\nconst VISIBILITIES: CooVisibility[] = ['PUBLIC', 'FOLLOWERS', 'PRIVATE'];\n\ninterface FormState {\n displayName: string;\n bio: string;\n visibility: CooVisibility;\n stylePreferences: string;\n palette: string;\n favouriteBrands: string;\n}\n\nconst EMPTY: FormState = {\n displayName: '',\n bio: '',\n visibility: 'PRIVATE',\n stylePreferences: '',\n palette: '',\n favouriteBrands: '',\n};\n\nexport function StyleProfilePage() {\n const query = useMyStyleProfile();\n const upsert = useUpsertStyleProfile();\n const following = useMyFollowing();\n\n const [editing, setEditing] = useState(false);\n const [form, setForm] = useState<FormState>(EMPTY);\n\n const profile = query.data;\n const hasProfile = Boolean(profile);\n const followingList = following.data ?? [];\n\n useEffect(() => {\n if (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 });\n }\n }, [profile]);\n\n // If there is no profile yet, drop straight into edit mode.\n useEffect(() => {\n if (!query.isLoading && !hasProfile) {\n setEditing(true);\n }\n }, [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 visibilityOptions = VISIBILITIES.map((v) => ({ value: v, label: titleCase(v) }));\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\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 },\n { onSuccess: () => setEditing(false) }\n );\n };\n\n if (query.isLoading) {\n return (\n <PageLayout title=\"Style Profile\">\n <LoadingView label=\"Loading your 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 // ----- View mode -----\n if (!editing && profile) {\n return (\n <PageLayout\n title=\"Style Profile\"\n description=\"Your taste, palette and sizing — the spine of personalised discovery\"\n actions={\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 >\n <div className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"glass\" className=\"p-6\">\n <div className=\"flex items-center gap-4\">\n <div className=\"flex h-14 w-14 items-center justify-center rounded-full bg-accent-purple-subtle text-text-primary\">\n <UserCircle className=\"h-7 w-7\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <h2 className=\"truncate text-xl font-semibold text-text-primary\">\n {profile.displayName || 'Your style profile'}\n </h2>\n <StatusBadge status={profile.visibility} className=\"mt-1\" />\n </div>\n </div>\n {profile.bio && <p className=\"mt-4 text-sm text-text-secondary\">{profile.bio}</p>}\n\n <div className=\"mt-4 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\n <div className=\"mt-6 space-y-4\">\n <div>\n <p className=\"mb-1.5 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Style preferences\n </p>\n {profile.stylePreferences.length ? (\n <TagList tags={profile.stylePreferences} tone=\"accent\" max={20} />\n ) : (\n <p className=\"text-sm text-text-secondary\">—</p>\n )}\n </div>\n <div>\n <p className=\"mb-1.5 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Palette\n </p>\n {profile.palette.length ? (\n <TagList tags={profile.palette} max={20} />\n ) : (\n <p className=\"text-sm text-text-secondary\">—</p>\n )}\n </div>\n <div>\n <p className=\"mb-1.5 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Favourite brands\n </p>\n {profile.favouriteBrands.length ? (\n <TagList tags={profile.favouriteBrands} max={20} />\n ) : (\n <p className=\"text-sm text-text-secondary\">—</p>\n )}\n </div>\n </div>\n </GlassCard>\n\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 <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\">\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 <FollowButton followeeId={p.ownerId} size=\"sm\" />\n </li>\n ))}\n </ul>\n )}\n </section>\n </div>\n </PageLayout>\n );\n }\n\n // ----- Edit / create mode -----\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\"\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 {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 (self-scoped)\n * @module coolandlovely/pages/profile\n *\n * Views and edits the calling shopper's style profile (`cooMyStyleProfile` +\n * `cooUpsertStyleProfile`) as a two-column bento — preferences, palette\n * swatches, favourite-brand monograms and a sizes table on the left; the\n * shopper's wardrobe (`cooListWardrobeItems`) and recent looks\n * (`cooListLooks` by author) on the right. One profile per workspace per shopper.\n */\n\nimport { useEffect, useState } from 'react';\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 { 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 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 { navigateTo, currentUser } = useCoolAndLovely();\n const query = useMyStyleProfile();\n const upsert = useUpsertStyleProfile();\n const following = useMyFollowing();\n const wardrobe = useWardrobeItems();\n const recentLooks = useLooks({\n authorId: query.data?.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 profile = query.data;\n const hasProfile = Boolean(profile);\n const followingList = following.data ?? [];\n\n useEffect(() => {\n if (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 }, [profile]);\n\n // If there is no profile yet, drop straight into edit mode.\n useEffect(() => {\n if (!query.isLoading && !hasProfile) {\n setEditing(true);\n }\n }, [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 your 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 // ----- View mode -----\n if (!editing && profile) {\n const avatarUrl = currentUser?.profilePicture || currentUser?.avatar;\n const wardrobeItems = (wardrobe.data ?? []).slice(0, 6);\n const looksList = recentLooks.data?.items ?? [];\n const sizeEntries = Object.entries(profile.sizes ?? {});\n\n return (\n <PageLayout\n title=\"Style Profile\"\n description=\"Your taste, palette and sizing — the spine of personalised discovery\"\n actions={\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 >\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={profile.displayName || 'Your 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 {profile.displayName || currentUser?.name || 'Your 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 No preferences yet — add a few in Edit.\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 <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 </div>\n </div>\n\n {/* Right: wardrobe + recent looks */}\n <div className=\"space-y-6 lg:col-span-2\">\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 <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 You have not published any looks yet.\n </p>\n )}\n </BentoCard>\n </div>\n </div>\n\n {/* People you follow */}\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 <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\">\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 <FollowButton followeeId={p.ownerId} size=\"sm\" />\n </li>\n ))}\n </ul>\n )}\n </section>\n </div>\n </PageLayout>\n );\n }\n\n // ----- Edit / create mode -----\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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"}
|
|
@@ -4,8 +4,8 @@ import { PageLayout as n } from "../../components/PageLayout.js";
|
|
|
4
4
|
import { MetricsCard as r } from "../../components/MetricsCard.js";
|
|
5
5
|
import { ErrorView as i, LoadingView as a } from "../../components/StateViews.js";
|
|
6
6
|
import { useBrands as o, useLooks as s, useStyleItems as c } from "../../hooks/useCoolAndLovelyApi.js";
|
|
7
|
-
import "../../hooks/index.js";
|
|
8
7
|
import { titleCase as l } from "../../utils/formatters.js";
|
|
8
|
+
import "../../hooks/index.js";
|
|
9
9
|
import { useMemo as u } from "react";
|
|
10
10
|
import { CalendarHeart as d, Heart as f, Palette as p, Shapes as m, Shirt as h, Sparkles as g, Store as _, TrendingUp as v } from "lucide-react";
|
|
11
11
|
import { jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TrendsPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/trends/TrendsPage.tsx"],"sourcesContent":["/**\n * Trends Page\n * @module coolandlovely/pages/trends\n *\n * Client-side analytics computed over the real catalogue + social lists\n * (cooListBrands / cooListStyleItems / cooListLooks). Honest by construction —\n * it only counts and ranks what is actually on the platform. No fabricated\n * metrics. Header labels it \"Signals from platform activity.\"\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useMemo } from 'react';\nimport {\n TrendingUp,\n Palette,\n Shapes,\n Sparkles,\n Store,\n Shirt,\n Heart,\n CalendarHeart,\n} from 'lucide-react';\nimport { EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrands, useStyleItems, useLooks } from '../../hooks';\nimport { titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\n\ninterface Ranked {\n label: string;\n value: number;\n sublabel?: string;\n}\n\nfunction rank(map: Map<string, number>, top = 8): Ranked[] {\n return [...map.entries()]\n .map(([label, value]) => ({ label, value }))\n .sort((a, b) => b.value - a.value)\n .slice(0, top);\n}\n\ninterface StatBarsProps {\n title: string;\n icon: ReactNode;\n items: Ranked[];\n unit: string;\n emptyLabel: string;\n className?: string;\n}\n\nconst StatBars: FC<StatBarsProps> = ({ title, icon, items, unit, emptyLabel, className }) => {\n const max = items.reduce((m, i) => Math.max(m, i.value), 0) || 1;\n return (\n <GlassCard treatment=\"plain\" className={cn('p-5', className)}>\n <h3 className=\"mb-4 flex items-center gap-2 text-base font-semibold text-text-primary\">\n {icon} {title}\n </h3>\n {items.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">{emptyLabel}</p>\n ) : (\n <ul className=\"space-y-3\">\n {items.map((item) => (\n <li key={item.label}>\n <div className=\"mb-1 flex items-center justify-between gap-2 text-sm\">\n <span className=\"truncate text-text-primary\">{item.label}</span>\n <span className=\"shrink-0 tabular-nums text-text-secondary\">\n {item.value.toLocaleString()} {unit}\n </span>\n </div>\n <div\n className=\"h-2 w-full overflow-hidden rounded-full bg-bg-sunken\"\n role=\"img\"\n aria-label={`${item.label}: ${item.value} ${unit}`}\n >\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${Math.max(4, Math.round((item.value / max) * 100))}%` }}\n />\n </div>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n );\n};\n\nexport function TrendsPage() {\n useCoolAndLovely();\n const brands = useBrands({ pagination: { limit: 100, offset: 0 } });\n const items = useStyleItems({ pagination: { limit: 100, offset: 0 } });\n const looks = useLooks({ pagination: { limit: 100, offset: 0 } });\n\n const isLoading = brands.isLoading || items.isLoading || looks.isLoading;\n const isError = brands.isError && items.isError && looks.isError;\n\n const analysis = useMemo(() => {\n const brandRows = brands.data?.items ?? [];\n const itemRows = items.data?.items ?? [];\n const lookRows = looks.data?.items ?? [];\n\n const categoryMap = new Map<string, number>();\n const colourMap = new Map<string, number>();\n for (const it of itemRows) {\n categoryMap.set(titleCase(it.category), (categoryMap.get(titleCase(it.category)) ?? 0) + 1);\n for (const c of it.colours) {\n const key = titleCase(c);\n colourMap.set(key, (colourMap.get(key) ?? 0) + 1);\n }\n }\n\n const aestheticMap = new Map<string, number>();\n for (const b of brandRows) {\n for (const a of b.aesthetics) {\n const key = titleCase(a);\n aestheticMap.set(key, (aestheticMap.get(key) ?? 0) + 1);\n }\n }\n\n const occasionEngagement = new Map<string, number>();\n let totalLikes = 0;\n let totalSaves = 0;\n for (const l of lookRows) {\n const eng = l.likeCount + l.saveCount;\n const key = titleCase(l.occasion);\n occasionEngagement.set(key, (occasionEngagement.get(key) ?? 0) + eng);\n totalLikes += l.likeCount;\n totalSaves += l.saveCount;\n }\n\n const topLooks = [...lookRows]\n .sort((a, b) => b.likeCount + b.saveCount - (a.likeCount + a.saveCount))\n .slice(0, 5);\n\n return {\n brandCount: brandRows.length,\n itemCount: itemRows.length,\n lookCount: lookRows.length,\n totalLikes,\n totalSaves,\n topCategories: rank(categoryMap, 8),\n topColours: rank(colourMap, 8),\n topAesthetics: rank(aestheticMap, 8),\n risingOccasions: rank(occasionEngagement, 8),\n topLooks,\n };\n }, [brands.data, items.data, looks.data]);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Trends\">\n <LoadingView label=\"Reading platform signals…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title=\"Trends\">\n <ErrorView\n onRetry={() => {\n void brands.refetch();\n void items.refetch();\n void looks.refetch();\n }}\n />\n </PageLayout>\n );\n }\n\n const metrics = [\n { icon: <Store className=\"h-5 w-5\" />, value: analysis.brandCount, label: 'Brands sampled' },\n { icon: <Shirt className=\"h-5 w-5\" />, value: analysis.itemCount, label: 'Items sampled' },\n { icon: <Sparkles className=\"h-5 w-5\" />, value: analysis.lookCount, label: 'Looks sampled' },\n {\n icon: <Heart className=\"h-5 w-5\" />,\n value: analysis.totalLikes + analysis.totalSaves,\n label: 'Total engagement',\n },\n ];\n\n return (\n <PageLayout\n title=\"Trends\"\n description=\"Signals from platform activity\"\n hero\n >\n <PagePurpose className=\"mb-6\">\n These are honest, client-side signals computed from the pieces, brands and looks currently\n on CoolAndLovely — the most-listed categories and colours, the aesthetics brands describe\n themselves with, and which occasions are drawing the most likes and saves. Nothing here is\n fabricated; it is a live read of the catalogue and social activity.\n </PagePurpose>\n\n <EmphasisPanel className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">At a glance</h2>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n {metrics.map((m) => (\n <MetricsCard\n key={m.label}\n icon={m.icon}\n value={m.value.toLocaleString()}\n label={m.label}\n />\n ))}\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <StatBars\n title=\"Top categories\"\n icon={<Shapes className=\"h-5 w-5\" />}\n items={analysis.topCategories}\n unit=\"items\"\n emptyLabel=\"No style items yet.\"\n />\n <StatBars\n title=\"Most-listed colours\"\n icon={<Palette className=\"h-5 w-5\" />}\n items={analysis.topColours}\n unit=\"items\"\n emptyLabel=\"No colours recorded on items yet.\"\n />\n <StatBars\n title=\"Brand aesthetics\"\n icon={<Store className=\"h-5 w-5\" />}\n items={analysis.topAesthetics}\n unit=\"brands\"\n emptyLabel=\"No brand aesthetics recorded yet.\"\n />\n <StatBars\n title=\"Rising occasions\"\n icon={<CalendarHeart className=\"h-5 w-5\" />}\n items={analysis.risingOccasions}\n unit=\"engagement\"\n emptyLabel=\"No looks published yet.\"\n />\n </div>\n\n <section className=\"mt-8\">\n <h2 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <TrendingUp className=\"h-5 w-5\" /> Most-loved looks\n </h2>\n {analysis.topLooks.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No looks published yet.\n </p>\n ) : (\n <div className=\"divide-y divide-border-seam rounded-lg border border-border-seam bg-bg-surface\">\n {analysis.topLooks.map((look, idx) => (\n <div key={look.id} className=\"flex items-center gap-4 px-4 py-3\">\n <span className=\"w-6 shrink-0 text-center text-sm font-semibold tabular-nums text-text-secondary\">\n {idx + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{look.title}</p>\n <p className=\"text-xs text-text-secondary\">{titleCase(look.occasion)}</p>\n </div>\n <span className=\"flex shrink-0 items-center gap-3 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1\">\n <Heart className=\"h-3.5 w-3.5\" /> {look.likeCount}\n </span>\n <span className=\"tabular-nums\">{look.saveCount} saves</span>\n </span>\n </div>\n ))}\n </div>\n )}\n </section>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TrendsPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/trends/TrendsPage.tsx"],"sourcesContent":["/**\n * Trends Page\n * @module coolandlovely/pages/trends\n *\n * Client-side analytics computed over the real catalogue + social lists\n * (cooListBrands / cooListStyleItems / cooListLooks). Honest by construction —\n * it only counts and ranks what is actually on the platform. No fabricated\n * metrics. Header labels it \"Signals from platform activity.\"\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useMemo } from 'react';\nimport {\n TrendingUp,\n Palette,\n Shapes,\n Sparkles,\n Store,\n Shirt,\n Heart,\n CalendarHeart,\n} from 'lucide-react';\nimport { EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { MetricsCard } from '../../components/MetricsCard';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrands, useStyleItems, useLooks } from '../../hooks';\nimport { titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\n\ninterface Ranked {\n label: string;\n value: number;\n sublabel?: string;\n}\n\nfunction rank(map: Map<string, number>, top = 8): Ranked[] {\n return [...map.entries()]\n .map(([label, value]) => ({ label, value }))\n .sort((a, b) => b.value - a.value)\n .slice(0, top);\n}\n\ninterface StatBarsProps {\n title: string;\n icon: ReactNode;\n items: Ranked[];\n unit: string;\n emptyLabel: string;\n className?: string;\n}\n\nconst StatBars: FC<StatBarsProps> = ({ title, icon, items, unit, emptyLabel, className }) => {\n const max = items.reduce((m, i) => Math.max(m, i.value), 0) || 1;\n return (\n <GlassCard treatment=\"plain\" className={cn('p-5', className)}>\n <h3 className=\"mb-4 flex items-center gap-2 text-base font-semibold text-text-primary\">\n {icon} {title}\n </h3>\n {items.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">{emptyLabel}</p>\n ) : (\n <ul className=\"space-y-3\">\n {items.map((item) => (\n <li key={item.label}>\n <div className=\"mb-1 flex items-center justify-between gap-2 text-sm\">\n <span className=\"truncate text-text-primary\">{item.label}</span>\n <span className=\"shrink-0 tabular-nums text-text-secondary\">\n {item.value.toLocaleString()} {unit}\n </span>\n </div>\n <div\n className=\"h-2 w-full overflow-hidden rounded-full bg-bg-sunken\"\n role=\"img\"\n aria-label={`${item.label}: ${item.value} ${unit}`}\n >\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${Math.max(4, Math.round((item.value / max) * 100))}%` }}\n />\n </div>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n );\n};\n\nexport function TrendsPage() {\n useCoolAndLovely();\n const brands = useBrands({ pagination: { limit: 100, offset: 0 } });\n const items = useStyleItems({ pagination: { limit: 100, offset: 0 } });\n const looks = useLooks({ pagination: { limit: 100, offset: 0 } });\n\n const isLoading = brands.isLoading || items.isLoading || looks.isLoading;\n const isError = brands.isError && items.isError && looks.isError;\n\n const analysis = useMemo(() => {\n const brandRows = brands.data?.items ?? [];\n const itemRows = items.data?.items ?? [];\n const lookRows = looks.data?.items ?? [];\n\n const categoryMap = new Map<string, number>();\n const colourMap = new Map<string, number>();\n for (const it of itemRows) {\n categoryMap.set(titleCase(it.category), (categoryMap.get(titleCase(it.category)) ?? 0) + 1);\n for (const c of it.colours) {\n const key = titleCase(c);\n colourMap.set(key, (colourMap.get(key) ?? 0) + 1);\n }\n }\n\n const aestheticMap = new Map<string, number>();\n for (const b of brandRows) {\n for (const a of b.aesthetics) {\n const key = titleCase(a);\n aestheticMap.set(key, (aestheticMap.get(key) ?? 0) + 1);\n }\n }\n\n const occasionEngagement = new Map<string, number>();\n let totalLikes = 0;\n let totalSaves = 0;\n for (const l of lookRows) {\n const eng = l.likeCount + l.saveCount;\n const key = titleCase(l.occasion);\n occasionEngagement.set(key, (occasionEngagement.get(key) ?? 0) + eng);\n totalLikes += l.likeCount;\n totalSaves += l.saveCount;\n }\n\n const topLooks = [...lookRows]\n .sort((a, b) => b.likeCount + b.saveCount - (a.likeCount + a.saveCount))\n .slice(0, 5);\n\n return {\n brandCount: brandRows.length,\n itemCount: itemRows.length,\n lookCount: lookRows.length,\n totalLikes,\n totalSaves,\n topCategories: rank(categoryMap, 8),\n topColours: rank(colourMap, 8),\n topAesthetics: rank(aestheticMap, 8),\n risingOccasions: rank(occasionEngagement, 8),\n topLooks,\n };\n }, [brands.data, items.data, looks.data]);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Trends\">\n <LoadingView label=\"Reading platform signals…\" />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title=\"Trends\">\n <ErrorView\n onRetry={() => {\n void brands.refetch();\n void items.refetch();\n void looks.refetch();\n }}\n />\n </PageLayout>\n );\n }\n\n const metrics = [\n { icon: <Store className=\"h-5 w-5\" />, value: analysis.brandCount, label: 'Brands sampled' },\n { icon: <Shirt className=\"h-5 w-5\" />, value: analysis.itemCount, label: 'Items sampled' },\n { icon: <Sparkles className=\"h-5 w-5\" />, value: analysis.lookCount, label: 'Looks sampled' },\n {\n icon: <Heart className=\"h-5 w-5\" />,\n value: analysis.totalLikes + analysis.totalSaves,\n label: 'Total engagement',\n },\n ];\n\n return (\n <PageLayout title=\"Trends\" description=\"Signals from platform activity\" hero>\n <PagePurpose className=\"mb-6\">\n These are honest, client-side signals computed from the pieces, brands and looks currently\n on CoolAndLovely — the most-listed categories and colours, the aesthetics brands describe\n themselves with, and which occasions are drawing the most likes and saves. Nothing here is\n fabricated; it is a live read of the catalogue and social activity.\n </PagePurpose>\n\n <EmphasisPanel className=\"mb-8\">\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">At a glance</h2>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n {metrics.map((m) => (\n <MetricsCard\n key={m.label}\n icon={m.icon}\n value={m.value.toLocaleString()}\n label={m.label}\n />\n ))}\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <StatBars\n title=\"Top categories\"\n icon={<Shapes className=\"h-5 w-5\" />}\n items={analysis.topCategories}\n unit=\"items\"\n emptyLabel=\"No style items yet.\"\n />\n <StatBars\n title=\"Most-listed colours\"\n icon={<Palette className=\"h-5 w-5\" />}\n items={analysis.topColours}\n unit=\"items\"\n emptyLabel=\"No colours recorded on items yet.\"\n />\n <StatBars\n title=\"Brand aesthetics\"\n icon={<Store className=\"h-5 w-5\" />}\n items={analysis.topAesthetics}\n unit=\"brands\"\n emptyLabel=\"No brand aesthetics recorded yet.\"\n />\n <StatBars\n title=\"Rising occasions\"\n icon={<CalendarHeart className=\"h-5 w-5\" />}\n items={analysis.risingOccasions}\n unit=\"engagement\"\n emptyLabel=\"No looks published yet.\"\n />\n </div>\n\n <section className=\"mt-8\">\n <h2 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <TrendingUp className=\"h-5 w-5\" /> Most-loved looks\n </h2>\n {analysis.topLooks.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No looks published yet.\n </p>\n ) : (\n <div className=\"divide-y divide-border-seam rounded-lg border border-border-seam bg-bg-surface\">\n {analysis.topLooks.map((look, idx) => (\n <div key={look.id} className=\"flex items-center gap-4 px-4 py-3\">\n <span className=\"w-6 shrink-0 text-center text-sm font-semibold tabular-nums text-text-secondary\">\n {idx + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{look.title}</p>\n <p className=\"text-xs text-text-secondary\">{titleCase(look.occasion)}</p>\n </div>\n <span className=\"flex shrink-0 items-center gap-3 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1\">\n <Heart className=\"h-3.5 w-3.5\" /> {look.likeCount}\n </span>\n <span className=\"tabular-nums\">{look.saveCount} saves</span>\n </span>\n </div>\n ))}\n </div>\n )}\n </section>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAqCA,SAAS,EAAK,GAA0B,IAAM,GAAa;AACzD,QAAO,CAAC,GAAG,EAAI,SAAS,CAAC,CACtB,KAAK,CAAC,GAAO,QAAY;EAAE;EAAO;EAAO,EAAE,CAC3C,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,CACjC,MAAM,GAAG,EAAI;;AAYlB,IAAM,KAA+B,EAAE,UAAO,SAAM,UAAO,SAAM,eAAY,mBAAgB;CAC3F,IAAM,IAAM,EAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI;AAC/D,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAW,EAAG,OAAO,EAAU;YAA5D,CACE,kBAAC,MAAD;GAAI,WAAU;aAAd;IACG;IAAK;IAAE;IACL;MACJ,EAAM,WAAW,IAChB,kBAAC,KAAD;GAAG,WAAU;aAA+B;GAAe,CAAA,GAE3D,kBAAC,MAAD;GAAI,WAAU;aACX,EAAM,KAAK,MACV,kBAAC,MAAD,EAAA,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAA8B,EAAK;KAAa,CAAA,EAChE,kBAAC,QAAD;KAAM,WAAU;eAAhB;MACG,EAAK,MAAM,gBAAgB;MAAC;MAAE;MAC1B;OACH;OACN,kBAAC,OAAD;IACE,WAAU;IACV,MAAK;IACL,cAAY,GAAG,EAAK,MAAM,IAAI,EAAK,MAAM,GAAG;cAE5C,kBAAC,OAAD;KACE,WAAU;KACV,OAAO,EAAE,OAAO,GAAG,KAAK,IAAI,GAAG,KAAK,MAAO,EAAK,QAAQ,IAAO,IAAI,CAAC,CAAC,IAAI;KACzE,CAAA;IACE,CAAA,CACH,EAAA,EAjBI,EAAK,MAiBT,CACL;GACC,CAAA,CAEG;;;AAIhB,SAAgB,IAAa;AAC3B,IAAkB;CAClB,IAAM,IAAS,EAAU,EAAE,YAAY;EAAE,OAAO;EAAK,QAAQ;EAAG,EAAE,CAAC,EAC7D,IAAQ,EAAc,EAAE,YAAY;EAAE,OAAO;EAAK,QAAQ;EAAG,EAAE,CAAC,EAChE,IAAQ,EAAS,EAAE,YAAY;EAAE,OAAO;EAAK,QAAQ;EAAG,EAAE,CAAC,EAE3D,IAAY,EAAO,aAAa,EAAM,aAAa,EAAM,WACzD,IAAU,EAAO,WAAW,EAAM,WAAW,EAAM,SAEnD,IAAW,QAAc;EAC7B,IAAM,IAAY,EAAO,MAAM,SAAS,EAAE,EACpC,IAAW,EAAM,MAAM,SAAS,EAAE,EAClC,IAAW,EAAM,MAAM,SAAS,EAAE,EAElC,oBAAc,IAAI,KAAqB,EACvC,oBAAY,IAAI,KAAqB;AAC3C,OAAK,IAAM,KAAM,GAAU;AACzB,KAAY,IAAI,EAAU,EAAG,SAAS,GAAG,EAAY,IAAI,EAAU,EAAG,SAAS,CAAC,IAAI,KAAK,EAAE;AAC3F,QAAK,IAAM,KAAK,EAAG,SAAS;IAC1B,IAAM,IAAM,EAAU,EAAE;AACxB,MAAU,IAAI,IAAM,EAAU,IAAI,EAAI,IAAI,KAAK,EAAE;;;EAIrD,IAAM,oBAAe,IAAI,KAAqB;AAC9C,OAAK,IAAM,KAAK,EACd,MAAK,IAAM,KAAK,EAAE,YAAY;GAC5B,IAAM,IAAM,EAAU,EAAE;AACxB,KAAa,IAAI,IAAM,EAAa,IAAI,EAAI,IAAI,KAAK,EAAE;;EAI3D,IAAM,oBAAqB,IAAI,KAAqB,EAChD,IAAa,GACb,IAAa;AACjB,OAAK,IAAM,KAAK,GAAU;GACxB,IAAM,IAAM,EAAE,YAAY,EAAE,WACtB,IAAM,EAAU,EAAE,SAAS;AAGjC,GAFA,EAAmB,IAAI,IAAM,EAAmB,IAAI,EAAI,IAAI,KAAK,EAAI,EACrE,KAAc,EAAE,WAChB,KAAc,EAAE;;EAGlB,IAAM,IAAW,CAAC,GAAG,EAAS,CAC3B,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,CACvE,MAAM,GAAG,EAAE;AAEd,SAAO;GACL,YAAY,EAAU;GACtB,WAAW,EAAS;GACpB,WAAW,EAAS;GACpB;GACA;GACA,eAAe,EAAK,GAAa,EAAE;GACnC,YAAY,EAAK,GAAW,EAAE;GAC9B,eAAe,EAAK,GAAc,EAAE;GACpC,iBAAiB,EAAK,GAAoB,EAAE;GAC5C;GACD;IACA;EAAC,EAAO;EAAM,EAAM;EAAM,EAAM;EAAK,CAAC;AAEzC,KAAI,EACF,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD,EAAa,OAAM,6BAA8B,CAAA;EACtC,CAAA;AAIjB,KAAI,EACF,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD,EACE,eAAe;AAGR,GAFA,EAAO,SAAS,EAChB,EAAM,SAAS,EACf,EAAM,SAAS;KAEtB,CAAA;EACS,CAAA;CAIjB,IAAM,IAAU;EACd;GAAE,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;GAAE,OAAO,EAAS;GAAY,OAAO;GAAkB;EAC5F;GAAE,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;GAAE,OAAO,EAAS;GAAW,OAAO;GAAiB;EAC1F;GAAE,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;GAAE,OAAO,EAAS;GAAW,OAAO;GAAiB;EAC7F;GACE,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;GACnC,OAAO,EAAS,aAAa,EAAS;GACtC,OAAO;GACR;EACF;AAED,QACE,kBAAC,GAAD;EAAY,OAAM;EAAS,aAAY;EAAiC,MAAA;YAAxE;GACE,kBAAC,GAAD;IAAa,WAAU;cAAO;IAKhB,CAAA;GAEd,kBAAC,GAAD;IAAe,WAAU;cAAzB,CACE,kBAAC,MAAD;KAAI,WAAU;eAA+C;KAAgB,CAAA,EAC7E,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAQ,KAAK,MACZ,kBAAC,GAAD;MAEE,MAAM,EAAE;MACR,OAAO,EAAE,MAAM,gBAAgB;MAC/B,OAAO,EAAE;MACT,EAJK,EAAE,MAIP,CACF;KACE,CAAA,CACQ;;GAEhB,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,OAAM;MACN,MAAM,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;MACpC,OAAO,EAAS;MAChB,MAAK;MACL,YAAW;MACX,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,MAAM,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;MACrC,OAAO,EAAS;MAChB,MAAK;MACL,YAAW;MACX,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;MACnC,OAAO,EAAS;MAChB,MAAK;MACL,YAAW;MACX,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,MAAM,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;MAC3C,OAAO,EAAS;MAChB,MAAK;MACL,YAAW;MACX,CAAA;KACE;;GAEN,kBAAC,WAAD;IAAS,WAAU;cAAnB,CACE,kBAAC,MAAD;KAAI,WAAU;eAAd,CACE,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA,EAAA,oBAC/B;QACJ,EAAS,SAAS,WAAW,IAC5B,kBAAC,KAAD;KAAG,WAAU;eAAqF;KAE9F,CAAA,GAEJ,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAS,SAAS,KAAK,GAAM,MAC5B,kBAAC,OAAD;MAAmB,WAAU;gBAA7B;OACE,kBAAC,QAAD;QAAM,WAAU;kBACb,IAAM;QACF,CAAA;OACP,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAkD,EAAK;SAAU,CAAA,EAC9E,kBAAC,KAAD;SAAG,WAAU;mBAA+B,EAAU,EAAK,SAAS;SAAK,CAAA,CACrE;;OACN,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UACE,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA;;UAAE,EAAK;UACnC;YACP,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CAAgC,EAAK,WAAU,SAAa;WACvD;;OACH;QAdI,EAAK,GAcT,CACN;KACE,CAAA,CAEA;;GACC"}
|
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
import { useCoolAndLovely as e } from "../../context/CoolAndLovelyContext.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { cn as t } from "../../utils/cn.js";
|
|
3
|
+
import { PageLayout as n } from "../../components/PageLayout.js";
|
|
4
|
+
import { COO_FILE_FOLDERS as r } from "../../hooks/useFiles.js";
|
|
5
|
+
import { useCreateWardrobeItem as i, useStyleItems as a } from "../../hooks/useCoolAndLovelyApi.js";
|
|
6
|
+
import { parseCsv as o, titleCase as s } from "../../utils/formatters.js";
|
|
5
7
|
import "../../hooks/index.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { GlassCard as v } from "@burdenoff/fe-libs/ui";
|
|
8
|
+
import { STYLE_CATEGORIES as c } from "../../utils/icons.js";
|
|
9
|
+
import { SelectField as l, TextAreaField as u, TextField as d } from "../../components/Field.js";
|
|
10
|
+
import { ImageUploadField as f } from "../../components/ImageUploadField.js";
|
|
11
|
+
import { useMemo as p, useState as m } from "react";
|
|
12
|
+
import { ArrowLeft as h, Heart as g, Loader2 as _, Save as v } from "lucide-react";
|
|
13
|
+
import { jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
14
|
+
import { GlassCard as x } from "@burdenoff/fe-libs/ui";
|
|
14
15
|
//#region src/coolandlovely/pages/wardrobe/WardrobeFormPage.tsx
|
|
15
|
-
var
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
var S = [
|
|
17
|
+
{
|
|
18
|
+
value: "OWNED",
|
|
19
|
+
label: "Owned"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
value: "WISHLIST",
|
|
23
|
+
label: "Wishlist"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
value: "PURCHASED_ON_PLATFORM",
|
|
27
|
+
label: "Purchased"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
value: "GIFTED",
|
|
31
|
+
label: "Gifted"
|
|
32
|
+
}
|
|
33
|
+
], C = {
|
|
21
34
|
name: "",
|
|
22
35
|
category: "OTHER",
|
|
23
36
|
source: "OWNED",
|
|
@@ -26,152 +39,175 @@ var y = [
|
|
|
26
39
|
size: "",
|
|
27
40
|
notes: "",
|
|
28
41
|
tags: "",
|
|
42
|
+
styleItemId: "",
|
|
29
43
|
imageFileId: ""
|
|
30
44
|
};
|
|
31
|
-
function
|
|
32
|
-
let { navigateTo:
|
|
45
|
+
function w() {
|
|
46
|
+
let { navigateTo: w } = e(), T = i(), E = a({ pagination: {
|
|
47
|
+
limit: 100,
|
|
48
|
+
offset: 0
|
|
49
|
+
} }), [D, O] = m(C), [k, A] = m(), j = (e, t) => O((n) => ({
|
|
33
50
|
...n,
|
|
34
51
|
[e]: t
|
|
35
|
-
})),
|
|
52
|
+
})), M = c.map((e) => ({
|
|
36
53
|
value: e,
|
|
37
|
-
label:
|
|
38
|
-
})),
|
|
39
|
-
value: e,
|
|
40
|
-
label:
|
|
41
|
-
}));
|
|
42
|
-
return /* @__PURE__ */
|
|
54
|
+
label: s(e)
|
|
55
|
+
})), N = p(() => (E.data?.items ?? []).filter((e) => e.status !== "RETIRED"), [E.data]), P = p(() => N.map((e) => ({
|
|
56
|
+
value: e.id,
|
|
57
|
+
label: `${e.name}${e.brand?.name ? ` · ${e.brand.name}` : ""}`
|
|
58
|
+
})), [N]);
|
|
59
|
+
return /* @__PURE__ */ y(n, {
|
|
43
60
|
title: "Add a wardrobe piece",
|
|
44
61
|
description: "Catalogue something you own or want",
|
|
45
|
-
actions: /* @__PURE__ */
|
|
62
|
+
actions: /* @__PURE__ */ b("button", {
|
|
46
63
|
type: "button",
|
|
47
|
-
onClick: () =>
|
|
64
|
+
onClick: () => w("/wardrobe"),
|
|
48
65
|
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",
|
|
49
|
-
children: [/* @__PURE__ */
|
|
66
|
+
children: [/* @__PURE__ */ y(h, { className: "h-4 w-4" }), " Cancel"]
|
|
50
67
|
}),
|
|
51
|
-
children: /* @__PURE__ */
|
|
68
|
+
children: /* @__PURE__ */ y("form", {
|
|
52
69
|
onSubmit: (e) => {
|
|
53
|
-
if (e.preventDefault(), !
|
|
54
|
-
|
|
70
|
+
if (e.preventDefault(), !D.name.trim()) {
|
|
71
|
+
A("Name is required");
|
|
55
72
|
return;
|
|
56
73
|
}
|
|
57
|
-
|
|
58
|
-
name:
|
|
59
|
-
category:
|
|
60
|
-
source:
|
|
61
|
-
brandName:
|
|
62
|
-
colour:
|
|
63
|
-
size:
|
|
64
|
-
notes:
|
|
65
|
-
tags:
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
A(void 0), T.mutate({
|
|
75
|
+
name: D.name.trim(),
|
|
76
|
+
category: D.category,
|
|
77
|
+
source: D.source,
|
|
78
|
+
brandName: D.brandName.trim() || void 0,
|
|
79
|
+
colour: D.colour.trim() || void 0,
|
|
80
|
+
size: D.size.trim() || void 0,
|
|
81
|
+
notes: D.notes.trim() || void 0,
|
|
82
|
+
tags: o(D.tags),
|
|
83
|
+
styleItemId: D.styleItemId || void 0,
|
|
84
|
+
imageFileId: D.imageFileId || void 0
|
|
85
|
+
}, { onSuccess: () => w("/wardrobe") });
|
|
68
86
|
},
|
|
69
87
|
className: "mx-auto max-w-2xl",
|
|
70
|
-
children: /* @__PURE__ */
|
|
88
|
+
children: /* @__PURE__ */ b(x, {
|
|
71
89
|
treatment: "plain",
|
|
72
90
|
className: "p-6",
|
|
73
91
|
children: [
|
|
74
|
-
/* @__PURE__ */
|
|
92
|
+
/* @__PURE__ */ b("div", {
|
|
75
93
|
className: "mb-6 flex items-center gap-3",
|
|
76
|
-
children: [/* @__PURE__ */
|
|
94
|
+
children: [/* @__PURE__ */ y("div", {
|
|
77
95
|
className: "flex h-11 w-11 items-center justify-center rounded-lg bg-status-error-bg-subtle text-status-error-text",
|
|
78
|
-
children: /* @__PURE__ */ g
|
|
79
|
-
}), /* @__PURE__ */
|
|
96
|
+
children: /* @__PURE__ */ y(g, { className: "h-5 w-5" })
|
|
97
|
+
}), /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("h2", {
|
|
80
98
|
className: "text-base font-semibold text-text-primary",
|
|
81
99
|
children: "Piece details"
|
|
82
|
-
}), /* @__PURE__ */
|
|
100
|
+
}), /* @__PURE__ */ y("p", {
|
|
83
101
|
className: "text-xs text-text-secondary",
|
|
84
102
|
children: "Only you can see your wardrobe."
|
|
85
103
|
})] })]
|
|
86
104
|
}),
|
|
87
|
-
/* @__PURE__ */
|
|
105
|
+
/* @__PURE__ */ y(d, {
|
|
88
106
|
id: "name",
|
|
89
107
|
label: "Name",
|
|
90
|
-
value:
|
|
91
|
-
onChange: (e) =>
|
|
108
|
+
value: D.name,
|
|
109
|
+
onChange: (e) => j("name", e),
|
|
92
110
|
placeholder: "Vintage denim jacket",
|
|
93
111
|
required: !0,
|
|
94
|
-
error:
|
|
112
|
+
error: k
|
|
113
|
+
}),
|
|
114
|
+
/* @__PURE__ */ y(l, {
|
|
115
|
+
id: "category",
|
|
116
|
+
label: "Category",
|
|
117
|
+
value: D.category,
|
|
118
|
+
onChange: (e) => j("category", e),
|
|
119
|
+
options: M,
|
|
120
|
+
className: "mt-4"
|
|
95
121
|
}),
|
|
96
|
-
/* @__PURE__ */
|
|
97
|
-
className: "mt-4
|
|
98
|
-
children: [/* @__PURE__ */
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
/* @__PURE__ */ b("div", {
|
|
123
|
+
className: "mt-4",
|
|
124
|
+
children: [/* @__PURE__ */ y("span", {
|
|
125
|
+
className: "mb-1.5 block text-sm font-medium text-text-primary",
|
|
126
|
+
children: "Source"
|
|
127
|
+
}), /* @__PURE__ */ y("div", {
|
|
128
|
+
className: "flex flex-wrap gap-2",
|
|
129
|
+
children: S.map((e) => /* @__PURE__ */ y("button", {
|
|
130
|
+
type: "button",
|
|
131
|
+
onClick: () => j("source", e.value),
|
|
132
|
+
"aria-pressed": D.source === e.value,
|
|
133
|
+
className: t("rounded-full px-3 py-1.5 text-sm font-medium transition-colors", D.source === e.value ? "bg-action-primary-bg text-action-primary-text" : "bg-bg-sunken text-text-secondary hover:text-text-primary"),
|
|
134
|
+
children: e.label
|
|
135
|
+
}, e.value))
|
|
110
136
|
})]
|
|
111
137
|
}),
|
|
112
|
-
/* @__PURE__ */
|
|
138
|
+
/* @__PURE__ */ b("div", {
|
|
113
139
|
className: "mt-4 grid grid-cols-1 gap-4 sm:grid-cols-3",
|
|
114
140
|
children: [
|
|
115
|
-
/* @__PURE__ */
|
|
141
|
+
/* @__PURE__ */ y(d, {
|
|
116
142
|
id: "brandName",
|
|
117
143
|
label: "Brand",
|
|
118
|
-
value:
|
|
119
|
-
onChange: (e) =>
|
|
144
|
+
value: D.brandName,
|
|
145
|
+
onChange: (e) => j("brandName", e),
|
|
120
146
|
placeholder: "Levi's"
|
|
121
147
|
}),
|
|
122
|
-
/* @__PURE__ */
|
|
148
|
+
/* @__PURE__ */ y(d, {
|
|
123
149
|
id: "colour",
|
|
124
150
|
label: "Colour",
|
|
125
|
-
value:
|
|
126
|
-
onChange: (e) =>
|
|
151
|
+
value: D.colour,
|
|
152
|
+
onChange: (e) => j("colour", e),
|
|
127
153
|
placeholder: "Indigo"
|
|
128
154
|
}),
|
|
129
|
-
/* @__PURE__ */
|
|
155
|
+
/* @__PURE__ */ y(d, {
|
|
130
156
|
id: "size",
|
|
131
157
|
label: "Size",
|
|
132
|
-
value:
|
|
133
|
-
onChange: (e) =>
|
|
158
|
+
value: D.size,
|
|
159
|
+
onChange: (e) => j("size", e),
|
|
134
160
|
placeholder: "M"
|
|
135
161
|
})
|
|
136
162
|
]
|
|
137
163
|
}),
|
|
138
|
-
/* @__PURE__ */
|
|
164
|
+
/* @__PURE__ */ y(l, {
|
|
165
|
+
id: "styleItemId",
|
|
166
|
+
label: "Link to a style item (optional)",
|
|
167
|
+
value: D.styleItemId,
|
|
168
|
+
onChange: (e) => j("styleItemId", e),
|
|
169
|
+
options: P,
|
|
170
|
+
placeholder: E.isLoading ? "Loading catalogue…" : "None — describe it yourself above",
|
|
171
|
+
hint: "Connect this piece to a catalogue item to keep its brand and imagery in sync.",
|
|
172
|
+
className: "mt-4"
|
|
173
|
+
}),
|
|
174
|
+
/* @__PURE__ */ y(u, {
|
|
139
175
|
id: "notes",
|
|
140
176
|
label: "Notes",
|
|
141
|
-
value:
|
|
142
|
-
onChange: (e) =>
|
|
177
|
+
value: D.notes,
|
|
178
|
+
onChange: (e) => j("notes", e),
|
|
143
179
|
placeholder: "Where you got it, how you wear it…",
|
|
144
180
|
rows: 3,
|
|
145
181
|
className: "mt-4"
|
|
146
182
|
}),
|
|
147
|
-
/* @__PURE__ */
|
|
183
|
+
/* @__PURE__ */ y(d, {
|
|
148
184
|
id: "tags",
|
|
149
185
|
label: "Tags",
|
|
150
|
-
value:
|
|
151
|
-
onChange: (e) =>
|
|
186
|
+
value: D.tags,
|
|
187
|
+
onChange: (e) => j("tags", e),
|
|
152
188
|
placeholder: "denim, layering",
|
|
153
189
|
hint: "Comma-separated",
|
|
154
190
|
className: "mt-4"
|
|
155
191
|
}),
|
|
156
|
-
/* @__PURE__ */
|
|
192
|
+
/* @__PURE__ */ y(f, {
|
|
157
193
|
label: "Photo",
|
|
158
|
-
value:
|
|
159
|
-
onChange: (e) =>
|
|
160
|
-
folder:
|
|
194
|
+
value: D.imageFileId,
|
|
195
|
+
onChange: (e) => j("imageFileId", e),
|
|
196
|
+
folder: r.wardrobe,
|
|
161
197
|
hint: "Your photo of the piece (private to you)",
|
|
162
198
|
className: "mt-4"
|
|
163
199
|
}),
|
|
164
|
-
|
|
200
|
+
T.isError && /* @__PURE__ */ y("p", {
|
|
165
201
|
className: "mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
|
|
166
|
-
children:
|
|
202
|
+
children: T.error instanceof Error ? T.error.message : "Something went wrong"
|
|
167
203
|
}),
|
|
168
|
-
/* @__PURE__ */
|
|
204
|
+
/* @__PURE__ */ y("div", {
|
|
169
205
|
className: "mt-6 flex justify-end gap-2",
|
|
170
|
-
children: /* @__PURE__ */
|
|
206
|
+
children: /* @__PURE__ */ b("button", {
|
|
171
207
|
type: "submit",
|
|
172
|
-
disabled:
|
|
208
|
+
disabled: T.isPending,
|
|
173
209
|
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",
|
|
174
|
-
children: [
|
|
210
|
+
children: [T.isPending ? /* @__PURE__ */ y(_, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ y(v, { className: "h-4 w-4" }), "Add to wardrobe"]
|
|
175
211
|
})
|
|
176
212
|
})
|
|
177
213
|
]
|
|
@@ -180,6 +216,6 @@ function x() {
|
|
|
180
216
|
});
|
|
181
217
|
}
|
|
182
218
|
//#endregion
|
|
183
|
-
export {
|
|
219
|
+
export { w as WardrobeFormPage };
|
|
184
220
|
|
|
185
221
|
//# sourceMappingURL=WardrobeFormPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WardrobeFormPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/wardrobe/WardrobeFormPage.tsx"],"sourcesContent":["/**\n * Wardrobe Item Create Page (self-scoped)\n * @module coolandlovely/pages/wardrobe\n *\n * Drives `cooCreateWardrobeItem`. Belongs to the calling shopper's profile.\n */\n\nimport { useState } from 'react';\nimport { ArrowLeft, Loader2, Save, Heart } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { TextField, TextAreaField, SelectField } from '../../components/Field';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useCreateWardrobeItem } from '../../hooks';\nimport { COO_FILE_FOLDERS } from '../../hooks/useFiles';\nimport { STYLE_CATEGORIES } from '../../utils/icons';\nimport { parseCsv, titleCase } from '../../utils/formatters';\nimport type { CooStyleCategory, CooWardrobeSource } from '../../types';\n\nconst SOURCES: CooWardrobeSource[] = ['OWNED', 'WISHLIST', 'PURCHASED_ON_PLATFORM', 'GIFTED'];\n\ninterface FormState {\n name: string;\n category: CooStyleCategory;\n source: CooWardrobeSource;\n brandName: string;\n colour: string;\n size: string;\n notes: string;\n tags: string;\n imageFileId: string;\n}\n\nconst EMPTY: FormState = {\n name: '',\n category: 'OTHER',\n source: 'OWNED',\n brandName: '',\n colour: '',\n size: '',\n notes: '',\n tags: '',\n imageFileId: '',\n};\n\nexport function WardrobeFormPage() {\n const { navigateTo } = useCoolAndLovely();\n const createItem = useCreateWardrobeItem();\n\n const [form, setForm] = useState<FormState>(EMPTY);\n const [nameError, setNameError] = useState<string | undefined>();\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((f) => ({ ...f, [key]: value }));\n\n const categoryOptions = STYLE_CATEGORIES.map((c) => ({ value: c, label: titleCase(c) }));\n const sourceOptions = SOURCES.map((s) => ({ value: s, label: titleCase(s) }));\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (!form.name.trim()) {\n setNameError('Name is required');\n return;\n }\n setNameError(undefined);\n\n createItem.mutate(\n {\n name: form.name.trim(),\n category: form.category,\n source: form.source,\n brandName: form.brandName.trim() || undefined,\n colour: form.colour.trim() || undefined,\n size: form.size.trim() || undefined,\n notes: form.notes.trim() || undefined,\n tags: parseCsv(form.tags),\n imageFileId: form.imageFileId || undefined,\n },\n { onSuccess: () => navigateTo('/wardrobe') }\n );\n };\n\n return (\n <PageLayout\n title=\"Add a wardrobe piece\"\n description=\"Catalogue something you own or want\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/wardrobe')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-status-error-bg-subtle text-status-error-text\">\n <Heart className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Piece details</h2>\n <p className=\"text-xs text-text-secondary\">Only you can see your wardrobe.</p>\n </div>\n </div>\n\n <TextField\n id=\"name\"\n label=\"Name\"\n value={form.name}\n onChange={(v) => setField('name', v)}\n placeholder=\"Vintage denim jacket\"\n required\n error={nameError}\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <SelectField\n id=\"category\"\n label=\"Category\"\n value={form.category}\n onChange={(v) => setField('category', v as CooStyleCategory)}\n options={categoryOptions}\n />\n <SelectField\n id=\"source\"\n label=\"Source\"\n value={form.source}\n onChange={(v) => setField('source', v as CooWardrobeSource)}\n options={sourceOptions}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <TextField\n id=\"brandName\"\n label=\"Brand\"\n value={form.brandName}\n onChange={(v) => setField('brandName', v)}\n placeholder=\"Levi's\"\n />\n <TextField\n id=\"colour\"\n label=\"Colour\"\n value={form.colour}\n onChange={(v) => setField('colour', v)}\n placeholder=\"Indigo\"\n />\n <TextField\n id=\"size\"\n label=\"Size\"\n value={form.size}\n onChange={(v) => setField('size', v)}\n placeholder=\"M\"\n />\n </div>\n\n <TextAreaField\n id=\"notes\"\n label=\"Notes\"\n value={form.notes}\n onChange={(v) => setField('notes', v)}\n placeholder=\"Where you got it, how you wear it…\"\n rows={3}\n className=\"mt-4\"\n />\n\n <TextField\n id=\"tags\"\n label=\"Tags\"\n value={form.tags}\n onChange={(v) => setField('tags', v)}\n placeholder=\"denim, layering\"\n hint=\"Comma-separated\"\n className=\"mt-4\"\n />\n\n <ImageUploadField\n label=\"Photo\"\n value={form.imageFileId}\n onChange={(id) => setField('imageFileId', id)}\n folder={COO_FILE_FOLDERS.wardrobe}\n hint=\"Your photo of the piece (private to you)\"\n className=\"mt-4\"\n />\n\n {createItem.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {createItem.error instanceof Error\n ? createItem.error.message\n : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"submit\"\n disabled={createItem.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 {createItem.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Add to wardrobe\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WardrobeFormPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/wardrobe/WardrobeFormPage.tsx"],"sourcesContent":["/**\n * Wardrobe Item Create Page (self-scoped)\n * @module coolandlovely/pages/wardrobe\n *\n * Drives `cooCreateWardrobeItem`. Belongs to the calling shopper's profile. A\n * piece can optionally be linked to a catalogue style item (`styleItemId`), and\n * its source is chosen with a segmented control.\n */\n\nimport { useMemo, useState } from 'react';\nimport { ArrowLeft, Loader2, Save, Heart } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { TextField, TextAreaField, SelectField } from '../../components/Field';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useCreateWardrobeItem, useStyleItems } from '../../hooks';\nimport { COO_FILE_FOLDERS } from '../../hooks/useFiles';\nimport { STYLE_CATEGORIES } from '../../utils/icons';\nimport { parseCsv, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooStyleCategory, CooWardrobeSource } from '../../types';\n\nconst SOURCES: Array<{ value: CooWardrobeSource; label: string }> = [\n { value: 'OWNED', label: 'Owned' },\n { value: 'WISHLIST', label: 'Wishlist' },\n { value: 'PURCHASED_ON_PLATFORM', label: 'Purchased' },\n { value: 'GIFTED', label: 'Gifted' },\n];\n\ninterface FormState {\n name: string;\n category: CooStyleCategory;\n source: CooWardrobeSource;\n brandName: string;\n colour: string;\n size: string;\n notes: string;\n tags: string;\n styleItemId: string;\n imageFileId: string;\n}\n\nconst EMPTY: FormState = {\n name: '',\n category: 'OTHER',\n source: 'OWNED',\n brandName: '',\n colour: '',\n size: '',\n notes: '',\n tags: '',\n styleItemId: '',\n imageFileId: '',\n};\n\nexport function WardrobeFormPage() {\n const { navigateTo } = useCoolAndLovely();\n const createItem = useCreateWardrobeItem();\n const styleItems = useStyleItems({ pagination: { limit: 100, offset: 0 } });\n\n const [form, setForm] = useState<FormState>(EMPTY);\n const [nameError, setNameError] = useState<string | undefined>();\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((f) => ({ ...f, [key]: value }));\n\n const categoryOptions = STYLE_CATEGORIES.map((c) => ({ value: c, label: titleCase(c) }));\n\n const usableStyleItems = useMemo(\n () => (styleItems.data?.items ?? []).filter((i) => i.status !== 'RETIRED'),\n [styleItems.data]\n );\n\n const styleItemOptions = useMemo(\n () =>\n usableStyleItems.map((i) => ({\n value: i.id,\n label: `${i.name}${i.brand?.name ? ` · ${i.brand.name}` : ''}`,\n })),\n [usableStyleItems]\n );\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (!form.name.trim()) {\n setNameError('Name is required');\n return;\n }\n setNameError(undefined);\n\n createItem.mutate(\n {\n name: form.name.trim(),\n category: form.category,\n source: form.source,\n brandName: form.brandName.trim() || undefined,\n colour: form.colour.trim() || undefined,\n size: form.size.trim() || undefined,\n notes: form.notes.trim() || undefined,\n tags: parseCsv(form.tags),\n styleItemId: form.styleItemId || undefined,\n imageFileId: form.imageFileId || undefined,\n },\n { onSuccess: () => navigateTo('/wardrobe') }\n );\n };\n\n return (\n <PageLayout\n title=\"Add a wardrobe piece\"\n description=\"Catalogue something you own or want\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/wardrobe')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-status-error-bg-subtle text-status-error-text\">\n <Heart className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Piece details</h2>\n <p className=\"text-xs text-text-secondary\">Only you can see your wardrobe.</p>\n </div>\n </div>\n\n <TextField\n id=\"name\"\n label=\"Name\"\n value={form.name}\n onChange={(v) => setField('name', v)}\n placeholder=\"Vintage denim jacket\"\n required\n error={nameError}\n />\n\n <SelectField\n id=\"category\"\n label=\"Category\"\n value={form.category}\n onChange={(v) => setField('category', v as CooStyleCategory)}\n options={categoryOptions}\n className=\"mt-4\"\n />\n\n {/* Segmented source control */}\n <div className=\"mt-4\">\n <span className=\"mb-1.5 block text-sm font-medium text-text-primary\">Source</span>\n <div className=\"flex flex-wrap gap-2\">\n {SOURCES.map((opt) => (\n <button\n key={opt.value}\n type=\"button\"\n onClick={() => setField('source', opt.value)}\n aria-pressed={form.source === opt.value}\n className={cn(\n 'rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n form.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 </div>\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <TextField\n id=\"brandName\"\n label=\"Brand\"\n value={form.brandName}\n onChange={(v) => setField('brandName', v)}\n placeholder=\"Levi's\"\n />\n <TextField\n id=\"colour\"\n label=\"Colour\"\n value={form.colour}\n onChange={(v) => setField('colour', v)}\n placeholder=\"Indigo\"\n />\n <TextField\n id=\"size\"\n label=\"Size\"\n value={form.size}\n onChange={(v) => setField('size', v)}\n placeholder=\"M\"\n />\n </div>\n\n <SelectField\n id=\"styleItemId\"\n label=\"Link to a style item (optional)\"\n value={form.styleItemId}\n onChange={(v) => setField('styleItemId', v)}\n options={styleItemOptions}\n placeholder={\n styleItems.isLoading ? 'Loading catalogue…' : 'None — describe it yourself above'\n }\n hint=\"Connect this piece to a catalogue item to keep its brand and imagery in sync.\"\n className=\"mt-4\"\n />\n\n <TextAreaField\n id=\"notes\"\n label=\"Notes\"\n value={form.notes}\n onChange={(v) => setField('notes', v)}\n placeholder=\"Where you got it, how you wear it…\"\n rows={3}\n className=\"mt-4\"\n />\n\n <TextField\n id=\"tags\"\n label=\"Tags\"\n value={form.tags}\n onChange={(v) => setField('tags', v)}\n placeholder=\"denim, layering\"\n hint=\"Comma-separated\"\n className=\"mt-4\"\n />\n\n <ImageUploadField\n label=\"Photo\"\n value={form.imageFileId}\n onChange={(id) => setField('imageFileId', id)}\n folder={COO_FILE_FOLDERS.wardrobe}\n hint=\"Your photo of the piece (private to you)\"\n className=\"mt-4\"\n />\n\n {createItem.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {createItem.error instanceof Error\n ? createItem.error.message\n : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"submit\"\n disabled={createItem.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 {createItem.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Add to wardrobe\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|