@burdenoff/microfe-coolandlovely 2026.716.1 → 2026.720.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coolandlovely/components/LookCard.d.ts +3 -1
- package/dist/coolandlovely/components/LookCard.d.ts.map +1 -1
- package/dist/coolandlovely/components/LookCard.js +80 -44
- package/dist/coolandlovely/components/LookCard.js.map +1 -1
- package/dist/coolandlovely/hooks/index.d.ts +1 -1
- package/dist/coolandlovely/hooks/index.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.d.ts +3 -0
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.d.ts.map +1 -1
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js +58 -22
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js.map +1 -1
- package/dist/coolandlovely/pages/HomePage.d.ts.map +1 -1
- package/dist/coolandlovely/pages/HomePage.js +2 -0
- package/dist/coolandlovely/pages/HomePage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.d.ts.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js +2 -0
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookDetailPage.d.ts +4 -2
- package/dist/coolandlovely/pages/looks/LookDetailPage.d.ts.map +1 -1
- package/dist/coolandlovely/pages/looks/LookDetailPage.js +134 -128
- package/dist/coolandlovely/pages/looks/LookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LooksListPage.d.ts.map +1 -1
- package/dist/coolandlovely/pages/looks/LooksListPage.js +2 -0
- package/dist/coolandlovely/pages/looks/LooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.d.ts.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js +46 -23
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.d.ts.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js +70 -63
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js.map +1 -1
- package/dist/coolandlovely/types/index.d.ts +11 -0
- package/dist/coolandlovely/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WardrobeFormPage.d.ts","sourceRoot":"","sources":["../../../../src/coolandlovely/pages/wardrobe/WardrobeFormPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"WardrobeFormPage.d.ts","sourceRoot":"","sources":["../../../../src/coolandlovely/pages/wardrobe/WardrobeFormPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAqDH,wBAAgB,gBAAgB,gCAsP/B"}
|
|
@@ -40,22 +40,24 @@ var S = [
|
|
|
40
40
|
notes: "",
|
|
41
41
|
tags: "",
|
|
42
42
|
styleItemId: "",
|
|
43
|
-
imageFileId: ""
|
|
43
|
+
imageFileId: "",
|
|
44
|
+
purchasePrice: "",
|
|
45
|
+
currency: "USD"
|
|
44
46
|
};
|
|
45
47
|
function w() {
|
|
46
48
|
let { navigateTo: w } = e(), T = i(), E = a({ pagination: {
|
|
47
49
|
limit: 100,
|
|
48
50
|
offset: 0
|
|
49
|
-
} }), [D, O] = m(C), [k, A] = m(), j = (e, t) => O((n) => ({
|
|
51
|
+
} }), [D, O] = m(C), [k, A] = m(), [j, M] = m(), N = (e, t) => O((n) => ({
|
|
50
52
|
...n,
|
|
51
53
|
[e]: t
|
|
52
|
-
})),
|
|
54
|
+
})), P = c.map((e) => ({
|
|
53
55
|
value: e,
|
|
54
56
|
label: s(e)
|
|
55
|
-
})),
|
|
57
|
+
})), F = p(() => (E.data?.items ?? []).filter((e) => e.status !== "RETIRED"), [E.data]), I = p(() => F.map((e) => ({
|
|
56
58
|
value: e.id,
|
|
57
59
|
label: `${e.name}${e.brand?.name ? ` · ${e.brand.name}` : ""}`
|
|
58
|
-
})), [
|
|
60
|
+
})), [F]);
|
|
59
61
|
return /* @__PURE__ */ y(n, {
|
|
60
62
|
title: "Add a wardrobe piece",
|
|
61
63
|
description: "Catalogue something you own or want",
|
|
@@ -67,11 +69,11 @@ function w() {
|
|
|
67
69
|
}),
|
|
68
70
|
children: /* @__PURE__ */ y("form", {
|
|
69
71
|
onSubmit: (e) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
let t = !1;
|
|
74
|
+
if (D.name.trim() ? A(void 0) : (A("Name is required"), t = !0), D.purchasePrice.trim() && Number.isNaN(Number(D.purchasePrice)) ? (M("Price must be a number"), t = !0) : M(void 0), t) return;
|
|
75
|
+
let n = D.purchasePrice.trim() ? Number(D.purchasePrice) : void 0;
|
|
76
|
+
T.mutate({
|
|
75
77
|
name: D.name.trim(),
|
|
76
78
|
category: D.category,
|
|
77
79
|
source: D.source,
|
|
@@ -81,7 +83,9 @@ function w() {
|
|
|
81
83
|
notes: D.notes.trim() || void 0,
|
|
82
84
|
tags: o(D.tags),
|
|
83
85
|
styleItemId: D.styleItemId || void 0,
|
|
84
|
-
imageFileId: D.imageFileId || void 0
|
|
86
|
+
imageFileId: D.imageFileId || void 0,
|
|
87
|
+
purchasePrice: n,
|
|
88
|
+
currency: n == null ? void 0 : D.currency.trim() || void 0
|
|
85
89
|
}, { onSuccess: () => w("/wardrobe") });
|
|
86
90
|
},
|
|
87
91
|
className: "mx-auto max-w-2xl",
|
|
@@ -106,7 +110,7 @@ function w() {
|
|
|
106
110
|
id: "name",
|
|
107
111
|
label: "Name",
|
|
108
112
|
value: D.name,
|
|
109
|
-
onChange: (e) =>
|
|
113
|
+
onChange: (e) => N("name", e),
|
|
110
114
|
placeholder: "Vintage denim jacket",
|
|
111
115
|
required: !0,
|
|
112
116
|
error: k
|
|
@@ -115,8 +119,8 @@ function w() {
|
|
|
115
119
|
id: "category",
|
|
116
120
|
label: "Category",
|
|
117
121
|
value: D.category,
|
|
118
|
-
onChange: (e) =>
|
|
119
|
-
options:
|
|
122
|
+
onChange: (e) => N("category", e),
|
|
123
|
+
options: P,
|
|
120
124
|
className: "mt-4"
|
|
121
125
|
}),
|
|
122
126
|
/* @__PURE__ */ b("div", {
|
|
@@ -128,7 +132,7 @@ function w() {
|
|
|
128
132
|
className: "flex flex-wrap gap-2",
|
|
129
133
|
children: S.map((e) => /* @__PURE__ */ y("button", {
|
|
130
134
|
type: "button",
|
|
131
|
-
onClick: () =>
|
|
135
|
+
onClick: () => N("source", e.value),
|
|
132
136
|
"aria-pressed": D.source === e.value,
|
|
133
137
|
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
138
|
children: e.label
|
|
@@ -142,21 +146,21 @@ function w() {
|
|
|
142
146
|
id: "brandName",
|
|
143
147
|
label: "Brand",
|
|
144
148
|
value: D.brandName,
|
|
145
|
-
onChange: (e) =>
|
|
149
|
+
onChange: (e) => N("brandName", e),
|
|
146
150
|
placeholder: "Levi's"
|
|
147
151
|
}),
|
|
148
152
|
/* @__PURE__ */ y(d, {
|
|
149
153
|
id: "colour",
|
|
150
154
|
label: "Colour",
|
|
151
155
|
value: D.colour,
|
|
152
|
-
onChange: (e) =>
|
|
156
|
+
onChange: (e) => N("colour", e),
|
|
153
157
|
placeholder: "Indigo"
|
|
154
158
|
}),
|
|
155
159
|
/* @__PURE__ */ y(d, {
|
|
156
160
|
id: "size",
|
|
157
161
|
label: "Size",
|
|
158
162
|
value: D.size,
|
|
159
|
-
onChange: (e) =>
|
|
163
|
+
onChange: (e) => N("size", e),
|
|
160
164
|
placeholder: "M"
|
|
161
165
|
})
|
|
162
166
|
]
|
|
@@ -165,17 +169,36 @@ function w() {
|
|
|
165
169
|
id: "styleItemId",
|
|
166
170
|
label: "Link to a style item (optional)",
|
|
167
171
|
value: D.styleItemId,
|
|
168
|
-
onChange: (e) =>
|
|
169
|
-
options:
|
|
172
|
+
onChange: (e) => N("styleItemId", e),
|
|
173
|
+
options: I,
|
|
170
174
|
placeholder: E.isLoading ? "Loading catalogue…" : "None — describe it yourself above",
|
|
171
175
|
hint: "Connect this piece to a catalogue item to keep its brand and imagery in sync.",
|
|
172
176
|
className: "mt-4"
|
|
173
177
|
}),
|
|
178
|
+
/* @__PURE__ */ b("div", {
|
|
179
|
+
className: "mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
180
|
+
children: [/* @__PURE__ */ y(d, {
|
|
181
|
+
id: "purchasePrice",
|
|
182
|
+
label: "Purchase price (optional)",
|
|
183
|
+
type: "number",
|
|
184
|
+
value: D.purchasePrice,
|
|
185
|
+
onChange: (e) => N("purchasePrice", e),
|
|
186
|
+
placeholder: "0.00",
|
|
187
|
+
hint: "Powers cost-per-wear once you log wears",
|
|
188
|
+
error: j
|
|
189
|
+
}), /* @__PURE__ */ y(d, {
|
|
190
|
+
id: "currency",
|
|
191
|
+
label: "Currency",
|
|
192
|
+
value: D.currency,
|
|
193
|
+
onChange: (e) => N("currency", e),
|
|
194
|
+
placeholder: "USD"
|
|
195
|
+
})]
|
|
196
|
+
}),
|
|
174
197
|
/* @__PURE__ */ y(u, {
|
|
175
198
|
id: "notes",
|
|
176
199
|
label: "Notes",
|
|
177
200
|
value: D.notes,
|
|
178
|
-
onChange: (e) =>
|
|
201
|
+
onChange: (e) => N("notes", e),
|
|
179
202
|
placeholder: "Where you got it, how you wear it…",
|
|
180
203
|
rows: 3,
|
|
181
204
|
className: "mt-4"
|
|
@@ -184,7 +207,7 @@ function w() {
|
|
|
184
207
|
id: "tags",
|
|
185
208
|
label: "Tags",
|
|
186
209
|
value: D.tags,
|
|
187
|
-
onChange: (e) =>
|
|
210
|
+
onChange: (e) => N("tags", e),
|
|
188
211
|
placeholder: "denim, layering",
|
|
189
212
|
hint: "Comma-separated",
|
|
190
213
|
className: "mt-4"
|
|
@@ -192,7 +215,7 @@ function w() {
|
|
|
192
215
|
/* @__PURE__ */ y(f, {
|
|
193
216
|
label: "Photo",
|
|
194
217
|
value: D.imageFileId,
|
|
195
|
-
onChange: (e) =>
|
|
218
|
+
onChange: (e) => N("imageFileId", e),
|
|
196
219
|
folder: r.wardrobe,
|
|
197
220
|
hint: "Your photo of the piece (private to you)",
|
|
198
221
|
className: "mt-4"
|
|
@@ -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. 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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"}
|
|
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 purchasePrice: string;\n currency: 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 purchasePrice: '',\n currency: 'USD',\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 const [priceError, setPriceError] = 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 let hasError = false;\n if (!form.name.trim()) {\n setNameError('Name is required');\n hasError = true;\n } else {\n setNameError(undefined);\n }\n if (form.purchasePrice.trim() && Number.isNaN(Number(form.purchasePrice))) {\n setPriceError('Price must be a number');\n hasError = true;\n } else {\n setPriceError(undefined);\n }\n if (hasError) return;\n\n const purchasePrice = form.purchasePrice.trim() ? Number(form.purchasePrice) : 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 purchasePrice,\n currency: purchasePrice != null ? form.currency.trim() || undefined : 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 <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"purchasePrice\"\n label=\"Purchase price (optional)\"\n type=\"number\"\n value={form.purchasePrice}\n onChange={(v) => setField('purchasePrice', v)}\n placeholder=\"0.00\"\n hint=\"Powers cost-per-wear once you log wears\"\n error={priceError}\n />\n <TextField\n id=\"currency\"\n label=\"Currency\"\n value={form.currency}\n onChange={(v) => setField('currency', v)}\n placeholder=\"USD\"\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":";;;;;;;;;;;;;;;AAuBA,IAAM,IAA8D;CAClE;EAAE,OAAO;EAAS,OAAO;EAAS;CAClC;EAAE,OAAO;EAAY,OAAO;EAAY;CACxC;EAAE,OAAO;EAAyB,OAAO;EAAa;CACtD;EAAE,OAAO;EAAU,OAAO;EAAU;CACrC,EAiBK,IAAmB;CACvB,MAAM;CACN,UAAU;CACV,QAAQ;CACR,WAAW;CACX,QAAQ;CACR,MAAM;CACN,OAAO;CACP,MAAM;CACN,aAAa;CACb,aAAa;CACb,eAAe;CACf,UAAU;CACX;AAED,SAAgB,IAAmB;CACjC,IAAM,EAAE,kBAAe,GAAkB,EACnC,IAAa,GAAuB,EACpC,IAAa,EAAc,EAAE,YAAY;EAAE,OAAO;EAAK,QAAQ;EAAG,EAAE,CAAC,EAErE,CAAC,GAAM,KAAW,EAAoB,EAAM,EAC5C,CAAC,GAAW,KAAgB,GAA8B,EAC1D,CAAC,GAAY,KAAiB,GAA8B,EAE5D,KAAuC,GAAQ,MACnD,GAAS,OAAO;EAAE,GAAG;GAAI,IAAM;EAAO,EAAE,EAEpC,IAAkB,EAAiB,KAAK,OAAO;EAAE,OAAO;EAAG,OAAO,EAAU,EAAE;EAAE,EAAE,EAElF,IAAmB,SAChB,EAAW,MAAM,SAAS,EAAE,EAAE,QAAQ,MAAM,EAAE,WAAW,UAAU,EAC1E,CAAC,EAAW,KAAK,CAClB,EAEK,IAAmB,QAErB,EAAiB,KAAK,OAAO;EAC3B,OAAO,EAAE;EACT,OAAO,GAAG,EAAE,OAAO,EAAE,OAAO,OAAO,MAAM,EAAE,MAAM,SAAS;EAC3D,EAAE,EACL,CAAC,EAAiB,CACnB;AAwCD,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAW,YAAY;GACtC,WAAU;aAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAAA,UAC1B;;YAGX,kBAAC,QAAD;GAAM,WApDY,MAAuB;AAC3C,MAAE,gBAAgB;IAClB,IAAI,IAAW;AAaf,QAZK,EAAK,KAAK,MAAM,GAInB,EAAa,KAAA,EAAU,IAHvB,EAAa,mBAAmB,EAChC,IAAW,KAIT,EAAK,cAAc,MAAM,IAAI,OAAO,MAAM,OAAO,EAAK,cAAc,CAAC,IACvE,EAAc,yBAAyB,EACvC,IAAW,MAEX,EAAc,KAAA,EAAU,EAEtB,EAAU;IAEd,IAAM,IAAgB,EAAK,cAAc,MAAM,GAAG,OAAO,EAAK,cAAc,GAAG,KAAA;AAE/E,MAAW,OACT;KACE,MAAM,EAAK,KAAK,MAAM;KACtB,UAAU,EAAK;KACf,QAAQ,EAAK;KACb,WAAW,EAAK,UAAU,MAAM,IAAI,KAAA;KACpC,QAAQ,EAAK,OAAO,MAAM,IAAI,KAAA;KAC9B,MAAM,EAAK,KAAK,MAAM,IAAI,KAAA;KAC1B,OAAO,EAAK,MAAM,MAAM,IAAI,KAAA;KAC5B,MAAM,EAAS,EAAK,KAAK;KACzB,aAAa,EAAK,eAAe,KAAA;KACjC,aAAa,EAAK,eAAe,KAAA;KACjC;KACA,UAAU,KAAiB,OAA2C,KAAA,IAApC,EAAK,SAAS,MAAM,IAAI,KAAA;KAC3D,EACD,EAAE,iBAAiB,EAAW,YAAY,EAAE,CAC7C;;GAiB+B,WAAU;aACtC,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cAAvC;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;OACzB,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;OAAI,WAAU;iBAA4C;OAAkB,CAAA,EAC5E,kBAAC,KAAD;OAAG,WAAU;iBAA8B;OAAmC,CAAA,CAC1E,EAAA,CAAA,CACF;;KAEN,kBAAC,GAAD;MACE,IAAG;MACH,OAAM;MACN,OAAO,EAAK;MACZ,WAAW,MAAM,EAAS,QAAQ,EAAE;MACpC,aAAY;MACZ,UAAA;MACA,OAAO;MACP,CAAA;KAEF,kBAAC,GAAD;MACE,IAAG;MACH,OAAM;MACN,OAAO,EAAK;MACZ,WAAW,MAAM,EAAS,YAAY,EAAsB;MAC5D,SAAS;MACT,WAAU;MACV,CAAA;KAGF,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAqD;OAAa,CAAA,EAClF,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAQ,KAAK,MACZ,kBAAC,UAAD;QAEE,MAAK;QACL,eAAe,EAAS,UAAU,EAAI,MAAM;QAC5C,gBAAc,EAAK,WAAW,EAAI;QAClC,WAAW,EACT,kEACA,EAAK,WAAW,EAAI,QAChB,kDACA,2DACL;kBAEA,EAAI;QACE,EAZF,EAAI,MAYF,CACT;OACE,CAAA,CACF;;KAEN,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,GAAD;QACE,IAAG;QACH,OAAM;QACN,OAAO,EAAK;QACZ,WAAW,MAAM,EAAS,aAAa,EAAE;QACzC,aAAY;QACZ,CAAA;OACF,kBAAC,GAAD;QACE,IAAG;QACH,OAAM;QACN,OAAO,EAAK;QACZ,WAAW,MAAM,EAAS,UAAU,EAAE;QACtC,aAAY;QACZ,CAAA;OACF,kBAAC,GAAD;QACE,IAAG;QACH,OAAM;QACN,OAAO,EAAK;QACZ,WAAW,MAAM,EAAS,QAAQ,EAAE;QACpC,aAAY;QACZ,CAAA;OACE;;KAEN,kBAAC,GAAD;MACE,IAAG;MACH,OAAM;MACN,OAAO,EAAK;MACZ,WAAW,MAAM,EAAS,eAAe,EAAE;MAC3C,SAAS;MACT,aACE,EAAW,YAAY,uBAAuB;MAEhD,MAAK;MACL,WAAU;MACV,CAAA;KAEF,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,IAAG;OACH,OAAM;OACN,MAAK;OACL,OAAO,EAAK;OACZ,WAAW,MAAM,EAAS,iBAAiB,EAAE;OAC7C,aAAY;OACZ,MAAK;OACL,OAAO;OACP,CAAA,EACF,kBAAC,GAAD;OACE,IAAG;OACH,OAAM;OACN,OAAO,EAAK;OACZ,WAAW,MAAM,EAAS,YAAY,EAAE;OACxC,aAAY;OACZ,CAAA,CACE;;KAEN,kBAAC,GAAD;MACE,IAAG;MACH,OAAM;MACN,OAAO,EAAK;MACZ,WAAW,MAAM,EAAS,SAAS,EAAE;MACrC,aAAY;MACZ,MAAM;MACN,WAAU;MACV,CAAA;KAEF,kBAAC,GAAD;MACE,IAAG;MACH,OAAM;MACN,OAAO,EAAK;MACZ,WAAW,MAAM,EAAS,QAAQ,EAAE;MACpC,aAAY;MACZ,MAAK;MACL,WAAU;MACV,CAAA;KAEF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,EAAK;MACZ,WAAW,MAAO,EAAS,eAAe,EAAG;MAC7C,QAAQ,EAAiB;MACzB,MAAK;MACL,WAAU;MACV,CAAA;KAED,EAAW,WACV,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAW,iBAAiB,QACzB,EAAW,MAAM,UACjB;MACF,CAAA;KAGN,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,UAAD;OACE,MAAK;OACL,UAAU,EAAW;OACrB,WAAU;iBAHZ,CAKG,EAAW,YACV,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAC5B,kBAEK;;MACL,CAAA;KACI;;GACP,CAAA;EACI,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WardrobeListPage.d.ts","sourceRoot":"","sources":["../../../../src/coolandlovely/pages/wardrobe/WardrobeListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAwBH,wBAAgB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"WardrobeListPage.d.ts","sourceRoot":"","sources":["../../../../src/coolandlovely/pages/wardrobe/WardrobeListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAwBH,wBAAgB,gBAAgB,gCA2M/B"}
|
|
@@ -5,15 +5,15 @@ import { StatusBadge as r } from "../../components/StatusBadge.js";
|
|
|
5
5
|
import { ErrorView as i, LoadingView as a } from "../../components/StateViews.js";
|
|
6
6
|
import { Thumbnail as o } from "../../components/Thumbnail.js";
|
|
7
7
|
import { useLogWardrobeWear as s, useRemoveWardrobeItem as c, useWardrobeItems as l } from "../../hooks/useCoolAndLovelyApi.js";
|
|
8
|
-
import {
|
|
8
|
+
import { formatPrice as u, formatRelativeTime as d, titleCase as f } from "../../utils/formatters.js";
|
|
9
9
|
import "../../hooks/index.js";
|
|
10
|
-
import { STYLE_CATEGORIES as
|
|
11
|
-
import { EmptyState as
|
|
12
|
-
import { useState as
|
|
13
|
-
import { CheckCircle2 as
|
|
14
|
-
import { jsx as
|
|
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";
|
|
15
15
|
//#region src/coolandlovely/pages/wardrobe/WardrobeListPage.tsx
|
|
16
|
-
var
|
|
16
|
+
var w = [
|
|
17
17
|
{
|
|
18
18
|
value: "ALL",
|
|
19
19
|
label: "All"
|
|
@@ -35,129 +35,136 @@ var C = [
|
|
|
35
35
|
label: "Gifted"
|
|
36
36
|
}
|
|
37
37
|
];
|
|
38
|
-
function
|
|
39
|
-
let { navigateTo:
|
|
40
|
-
category:
|
|
41
|
-
source:
|
|
42
|
-
}),
|
|
43
|
-
return /* @__PURE__ */
|
|
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, {
|
|
44
44
|
title: "My Wardrobe",
|
|
45
45
|
description: "A private catalogue of what you own — with a wear log",
|
|
46
|
-
actions: /* @__PURE__ */
|
|
46
|
+
actions: /* @__PURE__ */ C("button", {
|
|
47
47
|
type: "button",
|
|
48
|
-
onClick: () =>
|
|
48
|
+
onClick: () => T("/wardrobe/new"),
|
|
49
49
|
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__ */
|
|
50
|
+
children: [/* @__PURE__ */ S(b, { className: "h-4 w-4" }), " Add piece"]
|
|
51
51
|
}),
|
|
52
52
|
children: [
|
|
53
|
-
/* @__PURE__ */
|
|
53
|
+
/* @__PURE__ */ S("div", {
|
|
54
54
|
className: "mb-3 flex flex-wrap items-center gap-2",
|
|
55
|
-
children:
|
|
55
|
+
children: w.map((e) => /* @__PURE__ */ S("button", {
|
|
56
56
|
type: "button",
|
|
57
|
-
onClick: () =>
|
|
58
|
-
className: t("rounded-full px-3 py-1.5 text-sm font-medium transition-colors",
|
|
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"),
|
|
59
59
|
children: e.label
|
|
60
60
|
}, e.value))
|
|
61
61
|
}),
|
|
62
|
-
/* @__PURE__ */
|
|
62
|
+
/* @__PURE__ */ C("div", {
|
|
63
63
|
className: "mb-6 flex flex-wrap items-center gap-2",
|
|
64
|
-
children: [/* @__PURE__ */
|
|
64
|
+
children: [/* @__PURE__ */ S("button", {
|
|
65
65
|
type: "button",
|
|
66
|
-
onClick: () =>
|
|
67
|
-
className: t("rounded-full px-3 py-1.5 text-xs font-medium transition-colors",
|
|
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"),
|
|
68
68
|
children: "All categories"
|
|
69
|
-
}),
|
|
69
|
+
}), p.map((e) => /* @__PURE__ */ C("button", {
|
|
70
70
|
type: "button",
|
|
71
|
-
onClick: () =>
|
|
72
|
-
className: t("inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium transition-colors",
|
|
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"),
|
|
73
73
|
children: [
|
|
74
|
-
|
|
74
|
+
m(e, 14),
|
|
75
75
|
" ",
|
|
76
|
-
|
|
76
|
+
f(e)
|
|
77
77
|
]
|
|
78
78
|
}, e))]
|
|
79
79
|
}),
|
|
80
|
-
|
|
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, {
|
|
81
81
|
illustration: "empty-data",
|
|
82
|
-
icon: /* @__PURE__ */
|
|
82
|
+
icon: /* @__PURE__ */ S(v, { className: "h-10 w-10" }),
|
|
83
83
|
title: "Your wardrobe is empty",
|
|
84
84
|
description: "Add the pieces you already own (or want) to start your digital closet.",
|
|
85
|
-
action: /* @__PURE__ */
|
|
85
|
+
action: /* @__PURE__ */ C("button", {
|
|
86
86
|
type: "button",
|
|
87
|
-
onClick: () =>
|
|
87
|
+
onClick: () => T("/wardrobe/new"),
|
|
88
88
|
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__ */
|
|
89
|
+
children: [/* @__PURE__ */ S(b, { className: "h-4 w-4" }), " Add piece"]
|
|
90
90
|
})
|
|
91
|
-
}) : /* @__PURE__ */
|
|
91
|
+
}) : /* @__PURE__ */ S("div", {
|
|
92
92
|
className: "grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
|
|
93
|
-
children:
|
|
94
|
-
let t = e.brandName || e.colour ||
|
|
95
|
-
return /* @__PURE__ */
|
|
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
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__ */
|
|
97
|
+
children: [/* @__PURE__ */ C("div", {
|
|
98
98
|
className: "relative",
|
|
99
99
|
children: [
|
|
100
|
-
/* @__PURE__ */
|
|
101
|
-
icon:
|
|
100
|
+
/* @__PURE__ */ S(o, {
|
|
101
|
+
icon: m(e.category, 40),
|
|
102
102
|
seed: e.id,
|
|
103
103
|
fileId: e.image?.id,
|
|
104
104
|
alt: e.name,
|
|
105
105
|
className: "aspect-[4/5] w-full rounded-none"
|
|
106
106
|
}),
|
|
107
|
-
/* @__PURE__ */
|
|
107
|
+
/* @__PURE__ */ C("div", {
|
|
108
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__ */
|
|
109
|
+
children: [/* @__PURE__ */ S("span", {
|
|
110
110
|
className: "text-[9px] font-medium uppercase tracking-wide text-text-secondary",
|
|
111
111
|
children: "worn"
|
|
112
|
-
}), /* @__PURE__ */
|
|
112
|
+
}), /* @__PURE__ */ C("span", {
|
|
113
113
|
className: "text-sm font-bold tabular-nums text-text-primary",
|
|
114
114
|
children: [e.timesWorn, "×"]
|
|
115
115
|
})]
|
|
116
116
|
}),
|
|
117
|
-
/* @__PURE__ */
|
|
117
|
+
/* @__PURE__ */ C("div", {
|
|
118
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__ */
|
|
119
|
+
children: [/* @__PURE__ */ C("button", {
|
|
120
120
|
type: "button",
|
|
121
|
-
disabled:
|
|
122
|
-
onClick: () =>
|
|
121
|
+
disabled: j.isPending,
|
|
122
|
+
onClick: () => j.mutate(e.id),
|
|
123
123
|
"aria-label": `Log a wear for ${e.name}`,
|
|
124
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__ */
|
|
126
|
-
}), /* @__PURE__ */
|
|
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
127
|
type: "button",
|
|
128
|
-
disabled:
|
|
129
|
-
onClick: () =>
|
|
128
|
+
disabled: M.isPending,
|
|
129
|
+
onClick: () => M.mutate(e.id),
|
|
130
130
|
"aria-label": `Remove ${e.name}`,
|
|
131
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__ */
|
|
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
133
|
})]
|
|
134
134
|
})
|
|
135
135
|
]
|
|
136
|
-
}), /* @__PURE__ */
|
|
136
|
+
}), /* @__PURE__ */ C("div", {
|
|
137
137
|
className: "flex flex-1 flex-col p-4",
|
|
138
138
|
children: [
|
|
139
|
-
/* @__PURE__ */
|
|
139
|
+
/* @__PURE__ */ S("p", {
|
|
140
140
|
className: "truncate text-sm font-semibold text-text-primary",
|
|
141
141
|
children: e.name
|
|
142
142
|
}),
|
|
143
|
-
/* @__PURE__ */
|
|
143
|
+
/* @__PURE__ */ S("p", {
|
|
144
144
|
className: "truncate text-xs text-text-secondary",
|
|
145
145
|
children: t
|
|
146
146
|
}),
|
|
147
|
-
/* @__PURE__ */
|
|
147
|
+
/* @__PURE__ */ C("div", {
|
|
148
148
|
className: "mt-2 flex flex-wrap items-center gap-1.5",
|
|
149
|
-
children: [/* @__PURE__ */
|
|
149
|
+
children: [/* @__PURE__ */ S(r, { status: e.source }), e.size && /* @__PURE__ */ S("span", {
|
|
150
150
|
className: "rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary",
|
|
151
151
|
children: e.size
|
|
152
152
|
})]
|
|
153
153
|
}),
|
|
154
|
-
e.notes && /* @__PURE__ */
|
|
154
|
+
e.notes && /* @__PURE__ */ S("p", {
|
|
155
155
|
className: "mt-2 line-clamp-2 text-xs text-text-secondary",
|
|
156
156
|
children: e.notes
|
|
157
157
|
}),
|
|
158
|
-
/* @__PURE__ */
|
|
159
|
-
className: "mt-3 border-t border-border-seam pt-2 text-xs text-text-secondary",
|
|
160
|
-
children: e.lastWornAt ? `Last worn ${
|
|
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
|
+
})]
|
|
161
168
|
})
|
|
162
169
|
]
|
|
163
170
|
})]
|
|
@@ -168,6 +175,6 @@ function w() {
|
|
|
168
175
|
});
|
|
169
176
|
}
|
|
170
177
|
//#endregion
|
|
171
|
-
export {
|
|
178
|
+
export { T as WardrobeListPage };
|
|
172
179
|
|
|
173
180
|
//# 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 } 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 border-t border-border-seam pt-2 text-xs text-text-secondary\">\n {item.lastWornAt\n ? `Last worn ${formatRelativeTime(item.lastWornAt)}`\n : 'Not worn yet'}\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, 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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"}
|
|
@@ -191,6 +191,9 @@ export interface CooLook {
|
|
|
191
191
|
createdAt: string;
|
|
192
192
|
updatedAt: string;
|
|
193
193
|
items?: CooLookItem[];
|
|
194
|
+
/** Per-caller like/save state (false for an unauthenticated caller). */
|
|
195
|
+
isLikedByMe?: boolean;
|
|
196
|
+
isSavedByMe?: boolean;
|
|
194
197
|
}
|
|
195
198
|
export interface CooLookConnection {
|
|
196
199
|
items: CooLook[];
|
|
@@ -248,6 +251,10 @@ export interface CooWardrobeItem {
|
|
|
248
251
|
size: string | null;
|
|
249
252
|
notes: string | null;
|
|
250
253
|
styleItemId: string | null;
|
|
254
|
+
purchasePrice: number | null;
|
|
255
|
+
currency: string | null;
|
|
256
|
+
/** purchasePrice / timesWorn, server-computed. Null when either input is missing/zero. */
|
|
257
|
+
costPerWear: number | null;
|
|
251
258
|
timesWorn: number;
|
|
252
259
|
lastWornAt: string | null;
|
|
253
260
|
tags: string[];
|
|
@@ -405,6 +412,8 @@ export interface CooCreateWardrobeItemInput {
|
|
|
405
412
|
size?: string;
|
|
406
413
|
notes?: string;
|
|
407
414
|
styleItemId?: string;
|
|
415
|
+
purchasePrice?: number;
|
|
416
|
+
currency?: string;
|
|
408
417
|
tags?: string[];
|
|
409
418
|
imageFileId?: string;
|
|
410
419
|
}
|
|
@@ -416,6 +425,8 @@ export interface CooUpdateWardrobeItemInput {
|
|
|
416
425
|
colour?: string;
|
|
417
426
|
size?: string;
|
|
418
427
|
notes?: string;
|
|
428
|
+
purchasePrice?: number;
|
|
429
|
+
currency?: string;
|
|
419
430
|
tags?: string[];
|
|
420
431
|
imageFileId?: string;
|
|
421
432
|
}
|