@burdenoff/microfe-coolandlovely 2026.706.1 → 2026.706.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coolandlovely/CoolAndLovelyRoot.js +62 -46
- package/dist/coolandlovely/CoolAndLovelyRoot.js.map +1 -1
- package/dist/coolandlovely/CoolAndLovelyRoutes.js +14 -6
- package/dist/coolandlovely/CoolAndLovelyRoutes.js.map +1 -1
- package/dist/coolandlovely/components/ActorName.js +29 -0
- package/dist/coolandlovely/components/ActorName.js.map +1 -0
- package/dist/coolandlovely/components/AddToLookbookButton.js +81 -0
- package/dist/coolandlovely/components/AddToLookbookButton.js.map +1 -0
- 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 +109 -0
- package/dist/coolandlovely/components/Comments.js.map +1 -0
- package/dist/coolandlovely/components/Field.js +22 -2
- package/dist/coolandlovely/components/Field.js.map +1 -1
- package/dist/coolandlovely/components/FileImage.js +17 -0
- package/dist/coolandlovely/components/FileImage.js.map +1 -0
- package/dist/coolandlovely/components/FollowButton.js +24 -0
- package/dist/coolandlovely/components/FollowButton.js.map +1 -0
- package/dist/coolandlovely/components/ImageUploadField.js +153 -0
- package/dist/coolandlovely/components/ImageUploadField.js.map +1 -0
- 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 +25 -14
- 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/index.js +2 -0
- package/dist/coolandlovely/hooks/useComments.js +68 -0
- package/dist/coolandlovely/hooks/useComments.js.map +1 -0
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js +155 -60
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js.map +1 -1
- package/dist/coolandlovely/hooks/useFiles.js +111 -0
- package/dist/coolandlovely/hooks/useFiles.js.map +1 -0
- package/dist/coolandlovely/pages/HomePage.js +130 -133
- package/dist/coolandlovely/pages/HomePage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandDetailPage.js +261 -228
- package/dist/coolandlovely/pages/brands/BrandDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandFormPage.js +146 -104
- package/dist/coolandlovely/pages/brands/BrandFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandsListPage.js +13 -11
- 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 +134 -107
- package/dist/coolandlovely/pages/collections/CollectionDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionFormPage.js +117 -102
- package/dist/coolandlovely/pages/collections/CollectionFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionsListPage.js +59 -54
- package/dist/coolandlovely/pages/collections/CollectionsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/discover/DiscoverPage.js +223 -0
- package/dist/coolandlovely/pages/discover/DiscoverPage.js.map +1 -0
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js +176 -144
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemFormPage.js +144 -130
- package/dist/coolandlovely/pages/items/StyleItemFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemsListPage.js +73 -62
- package/dist/coolandlovely/pages/items/StyleItemsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js +215 -102
- package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js +115 -86
- package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookDetailPage.js +217 -155
- package/dist/coolandlovely/pages/looks/LookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookFormPage.js +147 -96
- package/dist/coolandlovely/pages/looks/LookFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LooksListPage.js +60 -95
- package/dist/coolandlovely/pages/looks/LooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js +376 -146
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js.map +1 -1
- package/dist/coolandlovely/pages/trends/TrendsPage.js +249 -0
- package/dist/coolandlovely/pages/trends/TrendsPage.js.map +1 -0
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js +136 -88
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js +75 -74
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { jsx as t, jsxs as n } from "react/jsx-runtime";
|
|
3
|
+
//#region src/coolandlovely/components/ColourSwatchRow.tsx
|
|
4
|
+
var r = {
|
|
5
|
+
black: "#111111",
|
|
6
|
+
white: "#ffffff",
|
|
7
|
+
ivory: "#fffff0",
|
|
8
|
+
cream: "#f5f1e3",
|
|
9
|
+
ecru: "#c2b280",
|
|
10
|
+
beige: "#f0e6d2",
|
|
11
|
+
sand: "#d8c3a5",
|
|
12
|
+
stone: "#a8a29e",
|
|
13
|
+
taupe: "#b38b6d",
|
|
14
|
+
grey: "#9ca3af",
|
|
15
|
+
gray: "#9ca3af",
|
|
16
|
+
charcoal: "#36454f",
|
|
17
|
+
silver: "#c0c0c0",
|
|
18
|
+
navy: "#1f2a44",
|
|
19
|
+
blue: "#2563eb",
|
|
20
|
+
cobalt: "#0047ab",
|
|
21
|
+
denim: "#1560bd",
|
|
22
|
+
indigo: "#4f46e5",
|
|
23
|
+
teal: "#0d9488",
|
|
24
|
+
turquoise: "#40e0d0",
|
|
25
|
+
green: "#16a34a",
|
|
26
|
+
olive: "#808000",
|
|
27
|
+
sage: "#9caf88",
|
|
28
|
+
mint: "#a7e8bd",
|
|
29
|
+
emerald: "#10b981",
|
|
30
|
+
khaki: "#c3b091",
|
|
31
|
+
mustard: "#e1ad01",
|
|
32
|
+
yellow: "#eab308",
|
|
33
|
+
gold: "#d4af37",
|
|
34
|
+
orange: "#ea580c",
|
|
35
|
+
rust: "#b7410e",
|
|
36
|
+
terracotta: "#e2725b",
|
|
37
|
+
coral: "#ff7f50",
|
|
38
|
+
peach: "#ffcba4",
|
|
39
|
+
camel: "#c19a6b",
|
|
40
|
+
tan: "#d2b48c",
|
|
41
|
+
brown: "#7c4a2d",
|
|
42
|
+
chocolate: "#5c3a21",
|
|
43
|
+
wine: "#722f37",
|
|
44
|
+
burgundy: "#800020",
|
|
45
|
+
maroon: "#800000",
|
|
46
|
+
red: "#dc2626",
|
|
47
|
+
pink: "#f9a8d4",
|
|
48
|
+
blush: "#e5a4a4",
|
|
49
|
+
rose: "#f43f5e",
|
|
50
|
+
lavender: "#b57edc",
|
|
51
|
+
lilac: "#c8a2c8",
|
|
52
|
+
purple: "#9333ea",
|
|
53
|
+
plum: "#8e4585"
|
|
54
|
+
}, i = Object.keys(r).sort((e, t) => t.length - e.length);
|
|
55
|
+
function a(e) {
|
|
56
|
+
let t = e.trim().toLowerCase();
|
|
57
|
+
if (!t) return null;
|
|
58
|
+
let n = r[t];
|
|
59
|
+
if (n) return n;
|
|
60
|
+
let a = i.find((e) => t.includes(e));
|
|
61
|
+
return a ? r[a] : null;
|
|
62
|
+
}
|
|
63
|
+
var o = ({ colours: r, max: i = 6, className: o }) => {
|
|
64
|
+
let s = r.filter((e) => e && e.trim().length > 0);
|
|
65
|
+
if (s.length === 0) return null;
|
|
66
|
+
let c = s.slice(0, i), l = s.length - c.length;
|
|
67
|
+
return /* @__PURE__ */ n("div", {
|
|
68
|
+
className: e("flex flex-wrap items-center gap-1.5", o),
|
|
69
|
+
children: [c.map((n, r) => {
|
|
70
|
+
let i = a(n);
|
|
71
|
+
return /* @__PURE__ */ t("span", {
|
|
72
|
+
title: n,
|
|
73
|
+
"aria-label": n,
|
|
74
|
+
style: i ? { backgroundColor: i } : void 0,
|
|
75
|
+
className: e("inline-block h-4 w-4 rounded-full ring-1 ring-inset ring-border-seam", i ? void 0 : "bg-bg-sunken")
|
|
76
|
+
}, `${n}-${r}`);
|
|
77
|
+
}), l > 0 ? /* @__PURE__ */ n("span", {
|
|
78
|
+
className: "text-xs font-medium text-text-secondary tabular-nums",
|
|
79
|
+
children: ["+", l]
|
|
80
|
+
}) : null]
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
export { o as ColourSwatchRow };
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=ColourSwatchRow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ColourSwatchRow.js","names":[],"sources":["../../../src/coolandlovely/components/ColourSwatchRow.tsx"],"sourcesContent":["/**\n * ColourSwatchRow — render a set of colour names as small swatch dots.\n * @module coolandlovely/components\n *\n * CoolAndLovely style items / wardrobe / palettes carry free-text colour names\n * (e.g. \"navy\", \"rust\", \"camel\"). We map the common ones to a representative\n * CSS colour so the catalogue reads like a real fashion product rather than a\n * row of grey chips. The swatch fill is CONTENT data (the colour the garment\n * actually is), not a theme decision, so an inline `backgroundColor` is used for\n * mapped names; unknown names fall back to a neutral token-styled dot that keeps\n * its label in a tooltip. Everything structural/text uses semantic tokens.\n */\n\nimport type { CSSProperties, FC } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface ColourSwatchRowProps {\n colours: string[];\n /** Max swatches to show before collapsing into a \"+N\" chip. */\n max?: number;\n className?: string;\n}\n\n/**\n * Common fashion colour names → a representative CSS colour. Keys are matched\n * case-insensitively; multi-word names (e.g. \"dusty rose\", \"forest green\") fall\n * back to a keyword scan so \"… green\" still resolves to green.\n */\nconst COLOUR_MAP: Record<string, string> = {\n black: '#111111',\n white: '#ffffff',\n ivory: '#fffff0',\n cream: '#f5f1e3',\n ecru: '#c2b280',\n beige: '#f0e6d2',\n sand: '#d8c3a5',\n stone: '#a8a29e',\n taupe: '#b38b6d',\n grey: '#9ca3af',\n gray: '#9ca3af',\n charcoal: '#36454f',\n silver: '#c0c0c0',\n navy: '#1f2a44',\n blue: '#2563eb',\n cobalt: '#0047ab',\n denim: '#1560bd',\n indigo: '#4f46e5',\n teal: '#0d9488',\n turquoise: '#40e0d0',\n green: '#16a34a',\n olive: '#808000',\n sage: '#9caf88',\n mint: '#a7e8bd',\n emerald: '#10b981',\n khaki: '#c3b091',\n mustard: '#e1ad01',\n yellow: '#eab308',\n gold: '#d4af37',\n orange: '#ea580c',\n rust: '#b7410e',\n terracotta: '#e2725b',\n coral: '#ff7f50',\n peach: '#ffcba4',\n camel: '#c19a6b',\n tan: '#d2b48c',\n brown: '#7c4a2d',\n chocolate: '#5c3a21',\n wine: '#722f37',\n burgundy: '#800020',\n maroon: '#800000',\n red: '#dc2626',\n pink: '#f9a8d4',\n blush: '#e5a4a4',\n rose: '#f43f5e',\n lavender: '#b57edc',\n lilac: '#c8a2c8',\n purple: '#9333ea',\n plum: '#8e4585',\n};\n\n/** Ordered keyword list for the \"contains\" fallback (longer keys first). */\nconst COLOUR_KEYWORDS = Object.keys(COLOUR_MAP).sort((a, b) => b.length - a.length);\n\nfunction resolveColour(name: string): string | null {\n const normalized = name.trim().toLowerCase();\n if (!normalized) return null;\n const direct = COLOUR_MAP[normalized];\n if (direct) return direct;\n const keyword = COLOUR_KEYWORDS.find((key) => normalized.includes(key));\n return keyword ? COLOUR_MAP[keyword] : null;\n}\n\nexport const ColourSwatchRow: FC<ColourSwatchRowProps> = ({ colours, max = 6, className }) => {\n const cleaned = colours.filter((c) => c && c.trim().length > 0);\n if (cleaned.length === 0) return null;\n\n const shown = cleaned.slice(0, max);\n const overflow = cleaned.length - shown.length;\n\n return (\n <div className={cn('flex flex-wrap items-center gap-1.5', className)}>\n {shown.map((colour, index) => {\n const css = resolveColour(colour);\n const style: CSSProperties | undefined = css ? { backgroundColor: css } : undefined;\n return (\n <span\n key={`${colour}-${index}`}\n title={colour}\n aria-label={colour}\n style={style}\n className={cn(\n 'inline-block h-4 w-4 rounded-full ring-1 ring-inset ring-border-seam',\n css ? undefined : 'bg-bg-sunken'\n )}\n />\n );\n })}\n {overflow > 0 ? (\n <span className=\"text-xs font-medium text-text-secondary tabular-nums\">+{overflow}</span>\n ) : null}\n </div>\n );\n};\n"],"mappings":";;;AA4BA,IAAM,IAAqC;CACzC,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,MAAM;CACN,OAAO;CACP,MAAM;CACN,OAAO;CACP,OAAO;CACP,MAAM;CACN,MAAM;CACN,UAAU;CACV,QAAQ;CACR,MAAM;CACN,MAAM;CACN,QAAQ;CACR,OAAO;CACP,QAAQ;CACR,MAAM;CACN,WAAW;CACX,OAAO;CACP,OAAO;CACP,MAAM;CACN,MAAM;CACN,SAAS;CACT,OAAO;CACP,SAAS;CACT,QAAQ;CACR,MAAM;CACN,QAAQ;CACR,MAAM;CACN,YAAY;CACZ,OAAO;CACP,OAAO;CACP,OAAO;CACP,KAAK;CACL,OAAO;CACP,WAAW;CACX,MAAM;CACN,UAAU;CACV,QAAQ;CACR,KAAK;CACL,MAAM;CACN,OAAO;CACP,MAAM;CACN,UAAU;CACV,OAAO;CACP,QAAQ;CACR,MAAM;CACP,EAGK,IAAkB,OAAO,KAAK,EAAW,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO;AAEnF,SAAS,EAAc,GAA6B;CAClD,IAAM,IAAa,EAAK,MAAM,CAAC,aAAa;AAC5C,KAAI,CAAC,EAAY,QAAO;CACxB,IAAM,IAAS,EAAW;AAC1B,KAAI,EAAQ,QAAO;CACnB,IAAM,IAAU,EAAgB,MAAM,MAAQ,EAAW,SAAS,EAAI,CAAC;AACvE,QAAO,IAAU,EAAW,KAAW;;AAGzC,IAAa,KAA6C,EAAE,YAAS,SAAM,GAAG,mBAAgB;CAC5F,IAAM,IAAU,EAAQ,QAAQ,MAAM,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE;AAC/D,KAAI,EAAQ,WAAW,EAAG,QAAO;CAEjC,IAAM,IAAQ,EAAQ,MAAM,GAAG,EAAI,EAC7B,IAAW,EAAQ,SAAS,EAAM;AAExC,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,uCAAuC,EAAU;YAApE,CACG,EAAM,KAAK,GAAQ,MAAU;GAC5B,IAAM,IAAM,EAAc,EAAO;AAEjC,UACE,kBAAC,QAAD;IAEE,OAAO;IACP,cAAY;IACL,OAN8B,IAAM,EAAE,iBAAiB,GAAK,GAAG,KAAA;IAOtE,WAAW,EACT,wEACA,IAAM,KAAA,IAAY,eACnB;IACD,EARK,GAAG,EAAO,GAAG,IAQlB;IAEJ,EACD,IAAW,IACV,kBAAC,QAAD;GAAM,WAAU;aAAhB,CAAuE,KAAE,EAAgB;OACvF,KACA"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { formatRelativeTime as t } from "../utils/formatters.js";
|
|
3
|
+
import { useAddComment as n, useEntityComments as r } from "../hooks/useComments.js";
|
|
4
|
+
import { ActorName as i } from "./ActorName.js";
|
|
5
|
+
import { useState as a } from "react";
|
|
6
|
+
import { Loader2 as o, MessageSquare as s, Send as c } from "lucide-react";
|
|
7
|
+
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
8
|
+
import { GlassCard as d } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
//#region src/coolandlovely/components/Comments.tsx
|
|
10
|
+
var f = ({ entityType: f, entityId: p, title: m = "Reviews & feedback", className: h }) => {
|
|
11
|
+
let g = r(f, p), _ = n(), [v, y] = a(""), b = g.data?.comments ?? [], x = g.data?.total ?? b.length;
|
|
12
|
+
return /* @__PURE__ */ u("section", {
|
|
13
|
+
className: e("mt-6", h),
|
|
14
|
+
children: [
|
|
15
|
+
/* @__PURE__ */ u("h3", {
|
|
16
|
+
className: "mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary",
|
|
17
|
+
children: [
|
|
18
|
+
/* @__PURE__ */ l(s, { className: "h-5 w-5" }),
|
|
19
|
+
" ",
|
|
20
|
+
m,
|
|
21
|
+
" (",
|
|
22
|
+
x,
|
|
23
|
+
")"
|
|
24
|
+
]
|
|
25
|
+
}),
|
|
26
|
+
/* @__PURE__ */ u(d, {
|
|
27
|
+
treatment: "plain",
|
|
28
|
+
className: "p-4",
|
|
29
|
+
children: [/* @__PURE__ */ u("form", {
|
|
30
|
+
onSubmit: (e) => {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
let t = v.trim();
|
|
33
|
+
!t || !p || _.mutate({
|
|
34
|
+
entityType: f,
|
|
35
|
+
entityId: p,
|
|
36
|
+
text: t
|
|
37
|
+
}, { onSuccess: () => y("") });
|
|
38
|
+
},
|
|
39
|
+
className: "flex flex-col gap-2 sm:flex-row sm:items-start",
|
|
40
|
+
children: [
|
|
41
|
+
/* @__PURE__ */ l("label", {
|
|
42
|
+
htmlFor: "coo-comment-input",
|
|
43
|
+
className: "sr-only",
|
|
44
|
+
children: "Write a review or leave feedback"
|
|
45
|
+
}),
|
|
46
|
+
/* @__PURE__ */ l("textarea", {
|
|
47
|
+
id: "coo-comment-input",
|
|
48
|
+
value: v,
|
|
49
|
+
onChange: (e) => y(e.target.value),
|
|
50
|
+
rows: 2,
|
|
51
|
+
placeholder: "Share a review or leave feedback…",
|
|
52
|
+
className: "w-full flex-1 resize-y rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]"
|
|
53
|
+
}),
|
|
54
|
+
/* @__PURE__ */ u("button", {
|
|
55
|
+
type: "submit",
|
|
56
|
+
disabled: !v.trim() || !p || _.isPending,
|
|
57
|
+
className: "inline-flex shrink-0 items-center justify-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50",
|
|
58
|
+
children: [_.isPending ? /* @__PURE__ */ l(o, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ l(c, { className: "h-4 w-4" }), "Post"]
|
|
59
|
+
})
|
|
60
|
+
]
|
|
61
|
+
}), _.isError && /* @__PURE__ */ l("p", {
|
|
62
|
+
className: "mt-2 text-xs text-status-error-text",
|
|
63
|
+
children: _.error instanceof Error ? _.error.message : "Could not post"
|
|
64
|
+
})]
|
|
65
|
+
}),
|
|
66
|
+
/* @__PURE__ */ l("div", {
|
|
67
|
+
className: "mt-4",
|
|
68
|
+
children: g.isLoading ? /* @__PURE__ */ u("div", {
|
|
69
|
+
className: "flex items-center justify-center gap-2 py-6 text-sm text-text-secondary",
|
|
70
|
+
children: [/* @__PURE__ */ l(o, { className: "h-4 w-4 animate-spin" }), " Loading reviews…"]
|
|
71
|
+
}) : g.isError ? /* @__PURE__ */ u("div", {
|
|
72
|
+
className: "rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary",
|
|
73
|
+
children: ["Reviews could not be loaded right now.", /* @__PURE__ */ l("button", {
|
|
74
|
+
type: "button",
|
|
75
|
+
onClick: () => void g.refetch(),
|
|
76
|
+
className: "ml-2 font-medium text-action-primary-bg hover:underline",
|
|
77
|
+
children: "Retry"
|
|
78
|
+
})]
|
|
79
|
+
}) : b.length === 0 ? /* @__PURE__ */ l("p", {
|
|
80
|
+
className: "rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary",
|
|
81
|
+
children: "No reviews yet. Be the first to share feedback."
|
|
82
|
+
}) : /* @__PURE__ */ l("ul", {
|
|
83
|
+
className: "divide-y divide-border-seam rounded-lg border border-border-seam bg-bg-surface",
|
|
84
|
+
children: b.map((e) => /* @__PURE__ */ u("li", {
|
|
85
|
+
className: "px-4 py-3",
|
|
86
|
+
children: [/* @__PURE__ */ u("div", {
|
|
87
|
+
className: "flex items-center justify-between gap-2",
|
|
88
|
+
children: [/* @__PURE__ */ l(i, {
|
|
89
|
+
id: e.authorId,
|
|
90
|
+
actorType: e.authorType,
|
|
91
|
+
className: "truncate text-sm font-medium text-text-primary"
|
|
92
|
+
}), /* @__PURE__ */ u("span", {
|
|
93
|
+
className: "shrink-0 text-xs text-text-secondary",
|
|
94
|
+
children: [t(e.createdAt), e.edited ? " · edited" : ""]
|
|
95
|
+
})]
|
|
96
|
+
}), /* @__PURE__ */ l("p", {
|
|
97
|
+
className: "mt-1 whitespace-pre-wrap break-words text-sm text-text-secondary",
|
|
98
|
+
children: e.text
|
|
99
|
+
})]
|
|
100
|
+
}, e.id))
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
]
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
//#endregion
|
|
107
|
+
export { f as Comments };
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=Comments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Comments.js","names":[],"sources":["../../../src/coolandlovely/components/Comments.tsx"],"sourcesContent":["/**\n * Comments — reviews & feedback thread for an entity (looks, brands).\n * @module coolandlovely/components\n *\n * Reuses wspace-conversations-svc `EntityComment` (NO coolandlovely comment\n * table). Lists + posts comments for (entityType, entityId). Commenter names\n * resolve via fe-libs useActorProfiles (GLOBAL gateway) — see ActorName.\n */\n\nimport type { FC } from 'react';\nimport { useState } from 'react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { Loader2, MessageSquare, Send } from 'lucide-react';\nimport { ActorName } from './ActorName';\nimport { useAddComment, useEntityComments } from '../hooks/useComments';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { cn } from '../utils/cn';\n\ninterface CommentsProps {\n /** Conversations entity type, e.g. \"CooLook\" / \"CooBrand\". */\n entityType: string;\n /** The entity id (skips rendering until present). */\n entityId?: string;\n /** Section heading (default \"Reviews & feedback\"). */\n title?: string;\n className?: string;\n}\n\nexport const Comments: FC<CommentsProps> = ({\n entityType,\n entityId,\n title = 'Reviews & feedback',\n className,\n}) => {\n const query = useEntityComments(entityType, entityId);\n const addComment = useAddComment();\n const [draft, setDraft] = useState('');\n\n const comments = query.data?.comments ?? [];\n const total = query.data?.total ?? comments.length;\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n const text = draft.trim();\n if (!text || !entityId) return;\n addComment.mutate({ entityType, entityId, text }, { onSuccess: () => setDraft('') });\n };\n\n return (\n <section className={cn('mt-6', className)}>\n <h3 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <MessageSquare className=\"h-5 w-5\" /> {title} ({total})\n </h3>\n\n <GlassCard treatment=\"plain\" className=\"p-4\">\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-2 sm:flex-row sm:items-start\">\n <label htmlFor=\"coo-comment-input\" className=\"sr-only\">\n Write a review or leave feedback\n </label>\n <textarea\n id=\"coo-comment-input\"\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n rows={2}\n placeholder=\"Share a review or leave feedback…\"\n className=\"w-full flex-1 resize-y rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <button\n type=\"submit\"\n disabled={!draft.trim() || !entityId || addComment.isPending}\n className=\"inline-flex shrink-0 items-center justify-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {addComment.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Send className=\"h-4 w-4\" />\n )}\n Post\n </button>\n </form>\n {addComment.isError && (\n <p className=\"mt-2 text-xs text-status-error-text\">\n {addComment.error instanceof Error ? addComment.error.message : 'Could not post'}\n </p>\n )}\n </GlassCard>\n\n <div className=\"mt-4\">\n {query.isLoading ? (\n <div className=\"flex items-center justify-center gap-2 py-6 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" /> Loading reviews…\n </div>\n ) : query.isError ? (\n <div className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n Reviews could not be loaded right now.\n <button\n type=\"button\"\n onClick={() => void query.refetch()}\n className=\"ml-2 font-medium text-action-primary-bg hover:underline\"\n >\n Retry\n </button>\n </div>\n ) : comments.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No reviews yet. Be the first to share feedback.\n </p>\n ) : (\n <ul className=\"divide-y divide-border-seam rounded-lg border border-border-seam bg-bg-surface\">\n {comments.map((c) => (\n <li key={c.id} className=\"px-4 py-3\">\n <div className=\"flex items-center justify-between gap-2\">\n <ActorName\n id={c.authorId}\n actorType={c.authorType}\n className=\"truncate text-sm font-medium text-text-primary\"\n />\n <span className=\"shrink-0 text-xs text-text-secondary\">\n {formatRelativeTime(c.createdAt)}\n {c.edited ? ' · edited' : ''}\n </span>\n </div>\n <p className=\"mt-1 whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {c.text}\n </p>\n </li>\n ))}\n </ul>\n )}\n </div>\n </section>\n );\n};\n"],"mappings":";;;;;;;;;AA4BA,IAAa,KAA+B,EAC1C,eACA,aACA,WAAQ,sBACR,mBACI;CACJ,IAAM,IAAQ,EAAkB,GAAY,EAAS,EAC/C,IAAa,GAAe,EAC5B,CAAC,GAAO,KAAY,EAAS,GAAG,EAEhC,IAAW,EAAM,MAAM,YAAY,EAAE,EACrC,IAAQ,EAAM,MAAM,SAAS,EAAS;AAS5C,QACE,kBAAC,WAAD;EAAS,WAAW,EAAG,QAAQ,EAAU;YAAzC;GACE,kBAAC,MAAD;IAAI,WAAU;cAAd;KACE,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;;KAAE;KAAM;KAAG;KAAM;KACnD;;GAEL,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cAAvC,CACE,kBAAC,QAAD;KAAM,WAdU,MAAuB;AAC3C,QAAE,gBAAgB;MAClB,IAAM,IAAO,EAAM,MAAM;AACrB,OAAC,KAAQ,CAAC,KACd,EAAW,OAAO;OAAE;OAAY;OAAU;OAAM,EAAE,EAAE,iBAAiB,EAAS,GAAG,EAAE,CAAC;;KAUlD,WAAU;eAAxC;MACE,kBAAC,SAAD;OAAO,SAAQ;OAAoB,WAAU;iBAAU;OAE/C,CAAA;MACR,kBAAC,YAAD;OACE,IAAG;OACH,OAAO;OACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;OACzC,MAAM;OACN,aAAY;OACZ,WAAU;OACV,CAAA;MACF,kBAAC,UAAD;OACE,MAAK;OACL,UAAU,CAAC,EAAM,MAAM,IAAI,CAAC,KAAY,EAAW;OACnD,WAAU;iBAHZ,CAKG,EAAW,YACV,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAC5B,OAEK;;MACJ;QACN,EAAW,WACV,kBAAC,KAAD;KAAG,WAAU;eACV,EAAW,iBAAiB,QAAQ,EAAW,MAAM,UAAU;KAC9D,CAAA,CAEI;;GAEZ,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAM,YACL,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,EAAA,oBACxC;SACJ,EAAM,UACR,kBAAC,OAAD;KAAK,WAAU;eAAf,CAAoG,0CAElG,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,KAAK,EAAM,SAAS;MACnC,WAAU;gBACX;MAEQ,CAAA,CACL;SACJ,EAAS,WAAW,IACtB,kBAAC,KAAD;KAAG,WAAU;eAAqF;KAE9F,CAAA,GAEJ,kBAAC,MAAD;KAAI,WAAU;eACX,EAAS,KAAK,MACb,kBAAC,MAAD;MAAe,WAAU;gBAAzB,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QACE,IAAI,EAAE;QACN,WAAW,EAAE;QACb,WAAU;QACV,CAAA,EACF,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACG,EAAmB,EAAE,UAAU,EAC/B,EAAE,SAAS,cAAc,GACrB;UACH;UACN,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAE;OACD,CAAA,CACD;QAfI,EAAE,GAeN,CACL;KACC,CAAA;IAEH,CAAA;GACE"}
|
|
@@ -53,7 +53,27 @@ var r = ({ label: r, htmlFor: i, required: a, hint: o, error: s, children: c, cl
|
|
|
53
53
|
onChange: (e) => s(e.target.value),
|
|
54
54
|
className: e(i, "resize-y", d && "border-status-error-text")
|
|
55
55
|
})
|
|
56
|
-
}), s = ({ id:
|
|
56
|
+
}), s = ({ id: r, label: i, checked: a, onChange: o, hint: s, disabled: c, className: l }) => /* @__PURE__ */ n("label", {
|
|
57
|
+
htmlFor: r,
|
|
58
|
+
className: e("flex cursor-pointer items-start gap-3 rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5", c && "cursor-not-allowed opacity-60", l),
|
|
59
|
+
children: [/* @__PURE__ */ t("input", {
|
|
60
|
+
id: r,
|
|
61
|
+
type: "checkbox",
|
|
62
|
+
checked: a,
|
|
63
|
+
disabled: c,
|
|
64
|
+
onChange: (e) => o(e.target.checked),
|
|
65
|
+
className: "mt-0.5 h-4 w-4 shrink-0 rounded border-border-seam text-action-primary-bg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]"
|
|
66
|
+
}), /* @__PURE__ */ n("span", {
|
|
67
|
+
className: "min-w-0",
|
|
68
|
+
children: [/* @__PURE__ */ t("span", {
|
|
69
|
+
className: "block text-sm font-medium text-text-primary",
|
|
70
|
+
children: i
|
|
71
|
+
}), s && /* @__PURE__ */ t("span", {
|
|
72
|
+
className: "mt-0.5 block text-xs text-text-secondary",
|
|
73
|
+
children: s
|
|
74
|
+
})]
|
|
75
|
+
})]
|
|
76
|
+
}), c = ({ id: a, label: o, value: s, onChange: c, options: l, required: u, hint: d, error: f, placeholder: p, className: m }) => /* @__PURE__ */ t(r, {
|
|
57
77
|
label: o,
|
|
58
78
|
htmlFor: a,
|
|
59
79
|
required: u,
|
|
@@ -76,6 +96,6 @@ var r = ({ label: r, htmlFor: i, required: a, hint: o, error: s, children: c, cl
|
|
|
76
96
|
})
|
|
77
97
|
});
|
|
78
98
|
//#endregion
|
|
79
|
-
export {
|
|
99
|
+
export { r as FieldShell, c as SelectField, o as TextAreaField, a as TextField, s as ToggleField };
|
|
80
100
|
|
|
81
101
|
//# sourceMappingURL=Field.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Field.js","names":[],"sources":["../../../src/coolandlovely/components/Field.tsx"],"sourcesContent":["/**\n * Form field primitives\n * @module coolandlovely/components\n *\n * Small, strictly-typed, token-only form controls used across the create/edit\n * forms (no third-party form lib — these wrap native inputs with consistent\n * labels, hints and error styling).\n */\n\nimport type { FC, ReactNode, ChangeEvent } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface FieldShellProps {\n label: string;\n htmlFor?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n children: ReactNode;\n className?: string;\n}\n\nexport const FieldShell: FC<FieldShellProps> = ({\n label,\n htmlFor,\n required,\n hint,\n error,\n children,\n className,\n}) => (\n <div className={cn('flex flex-col gap-1.5', className)}>\n <label htmlFor={htmlFor} className=\"text-sm font-medium text-text-primary\">\n {label}\n {required && <span className=\"ml-0.5 text-status-error-text\">*</span>}\n </label>\n {children}\n {error ? (\n <p className=\"text-xs text-status-error-text\">{error}</p>\n ) : hint ? (\n <p className=\"text-xs text-text-secondary\">{hint}</p>\n ) : null}\n </div>\n);\n\nconst baseControl =\n 'w-full rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\n\ninterface TextFieldProps {\n id?: string;\n label: string;\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n type?: 'text' | 'url' | 'number' | 'date';\n className?: string;\n}\n\nexport const TextField: FC<TextFieldProps> = ({\n id,\n label,\n value,\n onChange,\n placeholder,\n required,\n hint,\n error,\n type = 'text',\n className,\n}) => (\n <FieldShell\n label={label}\n htmlFor={id}\n required={required}\n hint={hint}\n error={error}\n className={className}\n >\n <input\n id={id}\n type={type}\n value={value}\n placeholder={placeholder}\n required={required}\n onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}\n className={cn(baseControl, error && 'border-status-error-text')}\n />\n </FieldShell>\n);\n\ninterface TextAreaFieldProps {\n id?: string;\n label: string;\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n rows?: number;\n className?: string;\n}\n\nexport const TextAreaField: FC<TextAreaFieldProps> = ({\n id,\n label,\n value,\n onChange,\n placeholder,\n required,\n hint,\n error,\n rows = 3,\n className,\n}) => (\n <FieldShell\n label={label}\n htmlFor={id}\n required={required}\n hint={hint}\n error={error}\n className={className}\n >\n <textarea\n id={id}\n value={value}\n placeholder={placeholder}\n required={required}\n rows={rows}\n onChange={(e: ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}\n className={cn(baseControl, 'resize-y', error && 'border-status-error-text')}\n />\n </FieldShell>\n);\n\ninterface SelectOption {\n value: string;\n label: string;\n}\n\ninterface SelectFieldProps {\n id?: string;\n label: string;\n value: string;\n onChange: (value: string) => void;\n options: SelectOption[];\n required?: boolean;\n hint?: string;\n error?: string;\n placeholder?: string;\n className?: string;\n}\n\nexport const SelectField: FC<SelectFieldProps> = ({\n id,\n label,\n value,\n onChange,\n options,\n required,\n hint,\n error,\n placeholder,\n className,\n}) => (\n <FieldShell\n label={label}\n htmlFor={id}\n required={required}\n hint={hint}\n error={error}\n className={className}\n >\n <select\n id={id}\n value={value}\n required={required}\n onChange={(e: ChangeEvent<HTMLSelectElement>) => onChange(e.target.value)}\n className={cn(baseControl, error && 'border-status-error-text')}\n >\n {placeholder && <option value=\"\">{placeholder}</option>}\n {options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </FieldShell>\n);\n"],"mappings":";;;AAsBA,IAAa,KAAmC,EAC9C,UACA,YACA,aACA,SACA,UACA,aACA,mBAEA,kBAAC,OAAD;CAAK,WAAW,EAAG,yBAAyB,EAAU;WAAtD;EACE,kBAAC,SAAD;GAAgB;GAAS,WAAU;aAAnC,CACG,GACA,KAAY,kBAAC,QAAD;IAAM,WAAU;cAAgC;IAAQ,CAAA,CAC/D;;EACP;EACA,IACC,kBAAC,KAAD;GAAG,WAAU;aAAkC;GAAU,CAAA,GACvD,IACF,kBAAC,KAAD;GAAG,WAAU;aAA+B;GAAS,CAAA,GACnD;EACA;IAGF,IACJ,sMAeW,KAAiC,EAC5C,OACA,UACA,UACA,aACA,gBACA,aACA,SACA,UACA,UAAO,QACP,mBAEA,kBAAC,GAAD;CACS;CACP,SAAS;CACC;CACJ;CACC;CACI;WAEX,kBAAC,SAAD;EACM;EACE;EACC;EACM;EACH;EACV,WAAW,MAAqC,EAAS,EAAE,OAAO,MAAM;EACxE,WAAW,EAAG,GAAa,KAAS,2BAA2B;EAC/D,CAAA;CACS,CAAA,EAgBF,KAAyC,EACpD,OACA,UACA,UACA,aACA,gBACA,aACA,SACA,UACA,UAAO,GACP,mBAEA,kBAAC,GAAD;CACS;CACP,SAAS;CACC;CACJ;CACC;CACI;WAEX,kBAAC,YAAD;EACM;EACG;EACM;EACH;EACJ;EACN,WAAW,MAAwC,EAAS,EAAE,OAAO,MAAM;EAC3E,WAAW,EAAG,GAAa,YAAY,KAAS,2BAA2B;EAC3E,CAAA;CACS,CAAA,
|
|
1
|
+
{"version":3,"file":"Field.js","names":[],"sources":["../../../src/coolandlovely/components/Field.tsx"],"sourcesContent":["/**\n * Form field primitives\n * @module coolandlovely/components\n *\n * Small, strictly-typed, token-only form controls used across the create/edit\n * forms (no third-party form lib — these wrap native inputs with consistent\n * labels, hints and error styling).\n */\n\nimport type { FC, ReactNode, ChangeEvent } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface FieldShellProps {\n label: string;\n htmlFor?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n children: ReactNode;\n className?: string;\n}\n\nexport const FieldShell: FC<FieldShellProps> = ({\n label,\n htmlFor,\n required,\n hint,\n error,\n children,\n className,\n}) => (\n <div className={cn('flex flex-col gap-1.5', className)}>\n <label htmlFor={htmlFor} className=\"text-sm font-medium text-text-primary\">\n {label}\n {required && <span className=\"ml-0.5 text-status-error-text\">*</span>}\n </label>\n {children}\n {error ? (\n <p className=\"text-xs text-status-error-text\">{error}</p>\n ) : hint ? (\n <p className=\"text-xs text-text-secondary\">{hint}</p>\n ) : null}\n </div>\n);\n\nconst baseControl =\n 'w-full rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\n\ninterface TextFieldProps {\n id?: string;\n label: string;\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n type?: 'text' | 'url' | 'number' | 'date';\n className?: string;\n}\n\nexport const TextField: FC<TextFieldProps> = ({\n id,\n label,\n value,\n onChange,\n placeholder,\n required,\n hint,\n error,\n type = 'text',\n className,\n}) => (\n <FieldShell\n label={label}\n htmlFor={id}\n required={required}\n hint={hint}\n error={error}\n className={className}\n >\n <input\n id={id}\n type={type}\n value={value}\n placeholder={placeholder}\n required={required}\n onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}\n className={cn(baseControl, error && 'border-status-error-text')}\n />\n </FieldShell>\n);\n\ninterface TextAreaFieldProps {\n id?: string;\n label: string;\n value: string;\n onChange: (value: string) => void;\n placeholder?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n rows?: number;\n className?: string;\n}\n\nexport const TextAreaField: FC<TextAreaFieldProps> = ({\n id,\n label,\n value,\n onChange,\n placeholder,\n required,\n hint,\n error,\n rows = 3,\n className,\n}) => (\n <FieldShell\n label={label}\n htmlFor={id}\n required={required}\n hint={hint}\n error={error}\n className={className}\n >\n <textarea\n id={id}\n value={value}\n placeholder={placeholder}\n required={required}\n rows={rows}\n onChange={(e: ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}\n className={cn(baseControl, 'resize-y', error && 'border-status-error-text')}\n />\n </FieldShell>\n);\n\ninterface ToggleFieldProps {\n id?: string;\n label: string;\n checked: boolean;\n onChange: (checked: boolean) => void;\n hint?: string;\n disabled?: boolean;\n className?: string;\n}\n\n/** A token-only checkbox row (used for boolean flags e.g. Featured). */\nexport const ToggleField: FC<ToggleFieldProps> = ({\n id,\n label,\n checked,\n onChange,\n hint,\n disabled,\n className,\n}) => (\n <label\n htmlFor={id}\n className={cn(\n 'flex cursor-pointer items-start gap-3 rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5',\n disabled && 'cursor-not-allowed opacity-60',\n className\n )}\n >\n <input\n id={id}\n type=\"checkbox\"\n checked={checked}\n disabled={disabled}\n onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.checked)}\n className=\"mt-0.5 h-4 w-4 shrink-0 rounded border-border-seam text-action-primary-bg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n />\n <span className=\"min-w-0\">\n <span className=\"block text-sm font-medium text-text-primary\">{label}</span>\n {hint && <span className=\"mt-0.5 block text-xs text-text-secondary\">{hint}</span>}\n </span>\n </label>\n);\n\ninterface SelectOption {\n value: string;\n label: string;\n}\n\ninterface SelectFieldProps {\n id?: string;\n label: string;\n value: string;\n onChange: (value: string) => void;\n options: SelectOption[];\n required?: boolean;\n hint?: string;\n error?: string;\n placeholder?: string;\n className?: string;\n}\n\nexport const SelectField: FC<SelectFieldProps> = ({\n id,\n label,\n value,\n onChange,\n options,\n required,\n hint,\n error,\n placeholder,\n className,\n}) => (\n <FieldShell\n label={label}\n htmlFor={id}\n required={required}\n hint={hint}\n error={error}\n className={className}\n >\n <select\n id={id}\n value={value}\n required={required}\n onChange={(e: ChangeEvent<HTMLSelectElement>) => onChange(e.target.value)}\n className={cn(baseControl, error && 'border-status-error-text')}\n >\n {placeholder && <option value=\"\">{placeholder}</option>}\n {options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </FieldShell>\n);\n"],"mappings":";;;AAsBA,IAAa,KAAmC,EAC9C,UACA,YACA,aACA,SACA,UACA,aACA,mBAEA,kBAAC,OAAD;CAAK,WAAW,EAAG,yBAAyB,EAAU;WAAtD;EACE,kBAAC,SAAD;GAAgB;GAAS,WAAU;aAAnC,CACG,GACA,KAAY,kBAAC,QAAD;IAAM,WAAU;cAAgC;IAAQ,CAAA,CAC/D;;EACP;EACA,IACC,kBAAC,KAAD;GAAG,WAAU;aAAkC;GAAU,CAAA,GACvD,IACF,kBAAC,KAAD;GAAG,WAAU;aAA+B;GAAS,CAAA,GACnD;EACA;IAGF,IACJ,sMAeW,KAAiC,EAC5C,OACA,UACA,UACA,aACA,gBACA,aACA,SACA,UACA,UAAO,QACP,mBAEA,kBAAC,GAAD;CACS;CACP,SAAS;CACC;CACJ;CACC;CACI;WAEX,kBAAC,SAAD;EACM;EACE;EACC;EACM;EACH;EACV,WAAW,MAAqC,EAAS,EAAE,OAAO,MAAM;EACxE,WAAW,EAAG,GAAa,KAAS,2BAA2B;EAC/D,CAAA;CACS,CAAA,EAgBF,KAAyC,EACpD,OACA,UACA,UACA,aACA,gBACA,aACA,SACA,UACA,UAAO,GACP,mBAEA,kBAAC,GAAD;CACS;CACP,SAAS;CACC;CACJ;CACC;CACI;WAEX,kBAAC,YAAD;EACM;EACG;EACM;EACH;EACJ;EACN,WAAW,MAAwC,EAAS,EAAE,OAAO,MAAM;EAC3E,WAAW,EAAG,GAAa,YAAY,KAAS,2BAA2B;EAC3E,CAAA;CACS,CAAA,EAcF,KAAqC,EAChD,OACA,UACA,YACA,aACA,SACA,aACA,mBAEA,kBAAC,SAAD;CACE,SAAS;CACT,WAAW,EACT,wGACA,KAAY,iCACZ,EACD;WANH,CAQE,kBAAC,SAAD;EACM;EACJ,MAAK;EACI;EACC;EACV,WAAW,MAAqC,EAAS,EAAE,OAAO,QAAQ;EAC1E,WAAU;EACV,CAAA,EACF,kBAAC,QAAD;EAAM,WAAU;YAAhB,CACE,kBAAC,QAAD;GAAM,WAAU;aAA+C;GAAa,CAAA,EAC3E,KAAQ,kBAAC,QAAD;GAAM,WAAU;aAA4C;GAAY,CAAA,CAC5E;IACD;IAqBG,KAAqC,EAChD,OACA,UACA,UACA,aACA,YACA,aACA,SACA,UACA,gBACA,mBAEA,kBAAC,GAAD;CACS;CACP,SAAS;CACC;CACJ;CACC;CACI;WAEX,kBAAC,UAAD;EACM;EACG;EACG;EACV,WAAW,MAAsC,EAAS,EAAE,OAAO,MAAM;EACzE,WAAW,EAAG,GAAa,KAAS,2BAA2B;YALjE,CAOG,KAAe,kBAAC,UAAD;GAAQ,OAAM;aAAI;GAAqB,CAAA,EACtD,EAAQ,KAAK,MACZ,kBAAC,UAAD;GAAwB,OAAO,EAAI;aAChC,EAAI;GACE,EAFI,EAAI,MAER,CACT,CACK;;CACE,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { useFileUrl as t } from "../hooks/useFiles.js";
|
|
3
|
+
import { jsx as n } from "react/jsx-runtime";
|
|
4
|
+
//#region src/coolandlovely/components/FileImage.tsx
|
|
5
|
+
var r = ({ fileId: r, alt: i = "", className: a }) => {
|
|
6
|
+
let { data: o } = t(r);
|
|
7
|
+
return !r || !o ? null : /* @__PURE__ */ n("img", {
|
|
8
|
+
src: o,
|
|
9
|
+
alt: i,
|
|
10
|
+
loading: "lazy",
|
|
11
|
+
className: e("h-full w-full object-cover", a)
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { r as FileImage };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=FileImage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileImage.js","names":[],"sources":["../../../src/coolandlovely/components/FileImage.tsx"],"sourcesContent":["/**\n * FileImage — render a wspace-files File id as an <img>.\n * @module coolandlovely/components\n *\n * Resolves the id to a short-lived presigned URL (useFileUrl, 6h TTL). Renders\n * nothing until resolved so callers can lay a glyph fallback underneath (no\n * broken <img>, no layout shift).\n */\n\nimport type { FC } from 'react';\nimport { useFileUrl } from '../hooks/useFiles';\nimport { cn } from '../utils/cn';\n\ninterface FileImageProps {\n fileId?: string | null;\n alt?: string;\n className?: string;\n}\n\nexport const FileImage: FC<FileImageProps> = ({ fileId, alt = '', className }) => {\n const { data: url } = useFileUrl(fileId);\n if (!fileId || !url) return null;\n return (\n <img\n src={url}\n alt={alt}\n loading=\"lazy\"\n className={cn('h-full w-full object-cover', className)}\n />\n );\n};\n"],"mappings":";;;;AAmBA,IAAa,KAAiC,EAAE,WAAQ,SAAM,IAAI,mBAAgB;CAChF,IAAM,EAAE,MAAM,MAAQ,EAAW,EAAO;AAExC,QADI,CAAC,KAAU,CAAC,IAAY,OAE1B,kBAAC,OAAD;EACE,KAAK;EACA;EACL,SAAQ;EACR,WAAW,EAAG,8BAA8B,EAAU;EACtD,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { useFollow as t, useIsFollowing as n, useUnfollow as r } from "../hooks/useCoolAndLovelyApi.js";
|
|
3
|
+
import { Loader2 as i, UserCheck as a, UserPlus as o } from "lucide-react";
|
|
4
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
5
|
+
//#region src/coolandlovely/components/FollowButton.tsx
|
|
6
|
+
var l = ({ followeeId: l, className: u, size: d = "md" }) => {
|
|
7
|
+
let f = n(l ?? void 0), p = t(), m = r();
|
|
8
|
+
if (!l) return null;
|
|
9
|
+
let h = f.data ?? !1, g = p.isPending || m.isPending || f.isLoading;
|
|
10
|
+
return /* @__PURE__ */ c("button", {
|
|
11
|
+
type: "button",
|
|
12
|
+
onClick: () => {
|
|
13
|
+
l && (h ? m.mutate(l) : p.mutate(l));
|
|
14
|
+
},
|
|
15
|
+
disabled: g,
|
|
16
|
+
"aria-pressed": h,
|
|
17
|
+
className: e("inline-flex items-center gap-1.5 rounded-lg font-medium transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]", d === "sm" ? "px-2.5 py-1 text-xs" : "px-3 py-2 text-sm", h ? "border border-border-seam text-text-primary hover:bg-bg-sunken" : "bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90", u),
|
|
18
|
+
children: [g ? /* @__PURE__ */ s(i, { className: e("animate-spin", d === "sm" ? "h-3.5 w-3.5" : "h-4 w-4") }) : s(h ? a : o, { className: d === "sm" ? "h-3.5 w-3.5" : "h-4 w-4" }), h ? "Following" : "Follow"]
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { l as FollowButton };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=FollowButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FollowButton.js","names":[],"sources":["../../../src/coolandlovely/components/FollowButton.tsx"],"sourcesContent":["/**\n * FollowButton — follow / unfollow a user actor (the follow graph).\n * @module coolandlovely/components\n *\n * Backed by cooFollow / cooUnfollow / cooIsFollowing (added this release). The\n * follow target is an actor id (a look's authorId, a style-profile owner, …).\n * Self-follow is impossible on the server; callers should not render this for\n * their own id.\n */\n\nimport type { FC } from 'react';\nimport { Loader2, UserCheck, UserPlus } from 'lucide-react';\nimport { useFollow, useIsFollowing, useUnfollow } from '../hooks/useCoolAndLovelyApi';\nimport { cn } from '../utils/cn';\n\ninterface FollowButtonProps {\n /** Actor id to follow/unfollow. */\n followeeId?: string | null;\n className?: string;\n size?: 'sm' | 'md';\n}\n\nexport const FollowButton: FC<FollowButtonProps> = ({ followeeId, className, size = 'md' }) => {\n const following = useIsFollowing(followeeId ?? undefined);\n const follow = useFollow();\n const unfollow = useUnfollow();\n\n if (!followeeId) return null;\n\n const isFollowing = following.data ?? false;\n const pending = follow.isPending || unfollow.isPending || following.isLoading;\n\n const handleClick = () => {\n if (!followeeId) return;\n if (isFollowing) {\n unfollow.mutate(followeeId);\n } else {\n follow.mutate(followeeId);\n }\n };\n\n const sizing = size === 'sm' ? 'px-2.5 py-1 text-xs' : 'px-3 py-2 text-sm';\n\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n disabled={pending}\n aria-pressed={isFollowing}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-lg font-medium transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n sizing,\n isFollowing\n ? 'border border-border-seam text-text-primary hover:bg-bg-sunken'\n : 'bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg/90',\n className\n )}\n >\n {pending ? (\n <Loader2 className={cn('animate-spin', size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4')} />\n ) : isFollowing ? (\n <UserCheck className={size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'} />\n ) : (\n <UserPlus className={size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'} />\n )}\n {isFollowing ? 'Following' : 'Follow'}\n </button>\n );\n};\n"],"mappings":";;;;;AAsBA,IAAa,KAAuC,EAAE,eAAY,cAAW,UAAO,WAAW;CAC7F,IAAM,IAAY,EAAe,KAAc,KAAA,EAAU,EACnD,IAAS,GAAW,EACpB,IAAW,GAAa;AAE9B,KAAI,CAAC,EAAY,QAAO;CAExB,IAAM,IAAc,EAAU,QAAQ,IAChC,IAAU,EAAO,aAAa,EAAS,aAAa,EAAU;AAapE,QACE,kBAAC,UAAD;EACE,MAAK;EACL,eAdsB;AACnB,SACD,IACF,EAAS,OAAO,EAAW,GAE3B,EAAO,OAAO,EAAW;;EAUzB,UAAU;EACV,gBAAc;EACd,WAAW,EACT,8LATS,MAAS,OAAO,wBAAwB,qBAWjD,IACI,mEACA,+EACJ,EACD;YAZH,CAcG,IACC,kBAAC,GAAD,EAAS,WAAW,EAAG,gBAAgB,MAAS,OAAO,gBAAgB,UAAU,EAAI,CAAA,GAErF,EADE,IACD,IAEA,GAFD,EAAW,WAAW,MAAS,OAAO,gBAAgB,WAAa,CAED,EAEnE,IAAc,cAAc,SACtB"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { useCoolAndLovely as e } from "../context/CoolAndLovelyContext.js";
|
|
2
|
+
import { cn as t } from "../utils/cn.js";
|
|
3
|
+
import { uploadImageToFilesWithProgress as n, useFileUrl as r } from "../hooks/useFiles.js";
|
|
4
|
+
import { FieldShell as i } from "./Field.js";
|
|
5
|
+
import { useEffect as a, useId as o, useRef as s, useState as c } from "react";
|
|
6
|
+
import { ImagePlus as l, Loader2 as u, UploadCloud as d, X as f } from "lucide-react";
|
|
7
|
+
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
8
|
+
//#region src/coolandlovely/components/ImageUploadField.tsx
|
|
9
|
+
var h = 10 * 1024 * 1024;
|
|
10
|
+
function g(e) {
|
|
11
|
+
if (e < 1024) return `${e} B`;
|
|
12
|
+
let t = e / 1024;
|
|
13
|
+
return t < 1024 ? `${t < 10 ? t.toFixed(1) : Math.round(t)} KB` : `${(t / 1024).toFixed(1)} MB`;
|
|
14
|
+
}
|
|
15
|
+
var _ = ({ label: _, value: v, onChange: y, folder: b, hint: x, accept: S = "image/*", disabled: C = !1, className: w, aspect: T = "square" }) => {
|
|
16
|
+
let { apiGatewayUrl: E } = e(), D = s(null), O = o(), [k, A] = c(!1), [j, M] = c(null), [N, P] = c(null), [F, I] = c(null), [L, R] = c(!1), [z, B] = c();
|
|
17
|
+
a(() => () => {
|
|
18
|
+
F && URL.revokeObjectURL(F);
|
|
19
|
+
}, [F]);
|
|
20
|
+
let V = r(k ? void 0 : v || void 0).data ?? null, H = C || k, U = async (e) => {
|
|
21
|
+
if (!e.type.startsWith("image/")) {
|
|
22
|
+
B("Please choose an image file (JPG or PNG).");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (e.size > h) {
|
|
26
|
+
B("That image is over 10 MB — please choose a smaller file.");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
B(void 0), P(e.name), I(URL.createObjectURL(e)), M({
|
|
30
|
+
loaded: 0,
|
|
31
|
+
total: e.size,
|
|
32
|
+
pct: 0
|
|
33
|
+
}), A(!0);
|
|
34
|
+
try {
|
|
35
|
+
y((await n(e, E, b, (e) => M(e))).id);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
B(e instanceof Error ? e.message : "Upload failed — please try again.");
|
|
38
|
+
} finally {
|
|
39
|
+
A(!1), M(null), P(null), I(null);
|
|
40
|
+
}
|
|
41
|
+
}, W = (e) => {
|
|
42
|
+
let t = e.target.files?.[0];
|
|
43
|
+
e.target.value = "", t && U(t);
|
|
44
|
+
}, G = (e) => {
|
|
45
|
+
if (e.preventDefault(), R(!1), H) return;
|
|
46
|
+
let t = e.dataTransfer.files?.[0];
|
|
47
|
+
t && U(t);
|
|
48
|
+
}, K = (e) => {
|
|
49
|
+
e.preventDefault(), H || R(!0);
|
|
50
|
+
}, q = (e) => {
|
|
51
|
+
e.preventDefault(), R(!1);
|
|
52
|
+
}, J = j?.pct ?? 0;
|
|
53
|
+
return /* @__PURE__ */ m(i, {
|
|
54
|
+
label: _,
|
|
55
|
+
htmlFor: O,
|
|
56
|
+
hint: x,
|
|
57
|
+
error: z,
|
|
58
|
+
className: w,
|
|
59
|
+
children: [/* @__PURE__ */ p("div", {
|
|
60
|
+
onDragEnter: K,
|
|
61
|
+
onDragOver: K,
|
|
62
|
+
onDragLeave: q,
|
|
63
|
+
onDrop: G,
|
|
64
|
+
children: k ? /* @__PURE__ */ m("div", {
|
|
65
|
+
className: "flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3",
|
|
66
|
+
children: [/* @__PURE__ */ p("div", {
|
|
67
|
+
className: "flex h-14 w-14 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-bg-sunken",
|
|
68
|
+
children: F ? /* @__PURE__ */ p("img", {
|
|
69
|
+
src: F,
|
|
70
|
+
alt: "",
|
|
71
|
+
className: "h-full w-full object-cover"
|
|
72
|
+
}) : /* @__PURE__ */ p(u, { className: "h-5 w-5 animate-spin text-text-secondary" })
|
|
73
|
+
}), /* @__PURE__ */ m("div", {
|
|
74
|
+
className: "min-w-0 flex-1",
|
|
75
|
+
children: [
|
|
76
|
+
/* @__PURE__ */ p("p", {
|
|
77
|
+
className: "truncate text-sm font-medium text-text-primary",
|
|
78
|
+
children: N ?? "Uploading…"
|
|
79
|
+
}),
|
|
80
|
+
/* @__PURE__ */ p("div", {
|
|
81
|
+
className: "mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken",
|
|
82
|
+
children: /* @__PURE__ */ p("div", {
|
|
83
|
+
className: "h-full rounded-full bg-accent-primary transition-[width] duration-200",
|
|
84
|
+
style: { width: `${J}%` }
|
|
85
|
+
})
|
|
86
|
+
}),
|
|
87
|
+
/* @__PURE__ */ m("p", {
|
|
88
|
+
className: "mt-1 text-xs text-text-secondary tabular-nums",
|
|
89
|
+
children: [
|
|
90
|
+
J,
|
|
91
|
+
"%",
|
|
92
|
+
j ? ` · ${g(j.loaded)} of ${g(j.total)}` : ""
|
|
93
|
+
]
|
|
94
|
+
})
|
|
95
|
+
]
|
|
96
|
+
})]
|
|
97
|
+
}) : V ? /* @__PURE__ */ m("div", {
|
|
98
|
+
className: "relative overflow-hidden rounded-lg border border-border-seam bg-bg-sunken",
|
|
99
|
+
children: [/* @__PURE__ */ p("img", {
|
|
100
|
+
src: V,
|
|
101
|
+
alt: "",
|
|
102
|
+
className: t("w-full object-cover", T === "wide" ? "aspect-video" : "aspect-square max-h-64")
|
|
103
|
+
}), /* @__PURE__ */ m("div", {
|
|
104
|
+
className: "absolute inset-x-0 bottom-0 flex justify-end gap-2 bg-gradient-to-t from-bg-canvas/85 to-transparent p-2",
|
|
105
|
+
children: [/* @__PURE__ */ m("button", {
|
|
106
|
+
type: "button",
|
|
107
|
+
disabled: H,
|
|
108
|
+
onClick: () => D.current?.click(),
|
|
109
|
+
className: "inline-flex items-center gap-1.5 rounded-lg bg-bg-surface/90 px-3 py-1.5 text-sm font-medium text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
|
|
110
|
+
children: [/* @__PURE__ */ p(l, { className: "h-4 w-4" }), " Replace"]
|
|
111
|
+
}), /* @__PURE__ */ m("button", {
|
|
112
|
+
type: "button",
|
|
113
|
+
disabled: C,
|
|
114
|
+
onClick: () => y(""),
|
|
115
|
+
className: "inline-flex items-center gap-1 rounded-lg bg-bg-surface/90 px-3 py-1.5 text-sm text-text-secondary shadow-sm backdrop-blur transition-colors hover:bg-status-error-bg-subtle hover:text-status-error-text disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
|
|
116
|
+
children: [/* @__PURE__ */ p(f, { className: "h-4 w-4" }), " Remove"]
|
|
117
|
+
})]
|
|
118
|
+
})]
|
|
119
|
+
}) : /* @__PURE__ */ m("button", {
|
|
120
|
+
type: "button",
|
|
121
|
+
disabled: H,
|
|
122
|
+
onClick: () => D.current?.click(),
|
|
123
|
+
className: t("flex min-h-40 w-full flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 text-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-50", L ? "border-accent-primary bg-[var(--color-accent-soft)]" : "border-border-seam hover:border-accent-primary/60 hover:bg-bg-sunken"),
|
|
124
|
+
children: [
|
|
125
|
+
/* @__PURE__ */ p(d, {
|
|
126
|
+
className: "h-8 w-8 text-text-secondary",
|
|
127
|
+
"aria-hidden": "true"
|
|
128
|
+
}),
|
|
129
|
+
/* @__PURE__ */ p("span", {
|
|
130
|
+
className: "text-sm font-medium text-text-primary",
|
|
131
|
+
children: "Drag & drop an image here, or click to browse"
|
|
132
|
+
}),
|
|
133
|
+
/* @__PURE__ */ p("span", {
|
|
134
|
+
className: "text-xs text-text-secondary",
|
|
135
|
+
children: "JPG · PNG · up to 10 MB"
|
|
136
|
+
})
|
|
137
|
+
]
|
|
138
|
+
})
|
|
139
|
+
}), /* @__PURE__ */ p("input", {
|
|
140
|
+
ref: D,
|
|
141
|
+
id: O,
|
|
142
|
+
type: "file",
|
|
143
|
+
accept: S,
|
|
144
|
+
className: "sr-only",
|
|
145
|
+
"aria-label": _,
|
|
146
|
+
onChange: W
|
|
147
|
+
})]
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
//#endregion
|
|
151
|
+
export { _ as ImageUploadField };
|
|
152
|
+
|
|
153
|
+
//# sourceMappingURL=ImageUploadField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageUploadField.js","names":[],"sources":["../../../src/coolandlovely/components/ImageUploadField.tsx"],"sourcesContent":["/**\n * ImageUploadField — image upload for a coo* entity, backed by wspace-files-svc.\n * @module coolandlovely/components\n *\n * Controlled by the current File id (`value`). Resting state is a real\n * drag-and-drop dropzone (dashed border, cloud-upload glyph, size/type hint);\n * dropping or browsing runs the shared presigned upload (initiateUpload -> PUT\n * -> completeUpload) via `uploadImageToFilesWithProgress`, showing a live rose\n * progress bar. On success the new File id is handed back via `onChange`; the\n * caller persists it (e.g. logoFileId). Clearing sends onChange('').\n *\n * Inline (not a modal), token-only, no Apollo, no toast dependency — surfaces\n * its own errors inline to match the existing form error style.\n */\n\nimport type { DragEvent, FC } from 'react';\nimport { useEffect, useId, useRef, useState } from 'react';\nimport { ImagePlus, Loader2, UploadCloud, X } from 'lucide-react';\nimport { FieldShell } from './Field';\nimport { useCoolAndLovely } from '../context/CoolAndLovelyContext';\nimport { uploadImageToFilesWithProgress, useFileUrl, type UploadProgress } from '../hooks/useFiles';\nimport { cn } from '../utils/cn';\n\ninterface ImageUploadFieldProps {\n label: string;\n /** Current wspace-files File id, or '' / undefined when none. */\n value?: string | null;\n /** Called with the new File id on success, or '' when cleared. */\n onChange: (fileId: string) => void;\n /** Destination folder in wspace-files (see COO_FILE_FOLDERS). */\n folder?: string;\n hint?: string;\n /** `accept` for the input; images only by default. */\n accept?: string;\n disabled?: boolean;\n className?: string;\n /** Aspect of the resolved-image preview tile. */\n aspect?: 'square' | 'wide';\n}\n\n/** 10 MB — the mock's stated ceiling and a sensible cap for garment photos. */\nconst MAX_BYTES = 10 * 1024 * 1024;\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n const kb = bytes / 1024;\n if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`;\n const mb = kb / 1024;\n return `${mb.toFixed(1)} MB`;\n}\n\nexport const ImageUploadField: FC<ImageUploadFieldProps> = ({\n label,\n value,\n onChange,\n folder,\n hint,\n accept = 'image/*',\n disabled = false,\n className,\n aspect = 'square',\n}) => {\n const { apiGatewayUrl } = useCoolAndLovely();\n const inputRef = useRef<HTMLInputElement | null>(null);\n const inputId = useId();\n const [uploading, setUploading] = useState(false);\n const [progress, setProgress] = useState<UploadProgress | null>(null);\n const [pendingName, setPendingName] = useState<string | null>(null);\n const [pendingPreview, setPendingPreview] = useState<string | null>(null);\n const [dragActive, setDragActive] = useState(false);\n const [error, setError] = useState<string | undefined>();\n\n // Revoke the local object-URL preview when it changes / on unmount.\n useEffect(() => {\n return () => {\n if (pendingPreview) URL.revokeObjectURL(pendingPreview);\n };\n }, [pendingPreview]);\n\n // While uploading we skip resolution; the preview shows after onChange lands.\n const resolved = useFileUrl(uploading ? undefined : value || undefined);\n const previewUrl = resolved.data ?? null;\n const locked = disabled || uploading;\n\n const upload = async (file: File) => {\n // Validate before opening a session so bad files never hit the network.\n if (!file.type.startsWith('image/')) {\n setError('Please choose an image file (JPG or PNG).');\n return;\n }\n if (file.size > MAX_BYTES) {\n setError('That image is over 10 MB — please choose a smaller file.');\n return;\n }\n\n setError(undefined);\n setPendingName(file.name);\n const objectUrl = URL.createObjectURL(file);\n setPendingPreview(objectUrl);\n setProgress({ loaded: 0, total: file.size, pct: 0 });\n setUploading(true);\n try {\n const uploaded = await uploadImageToFilesWithProgress(file, apiGatewayUrl, folder, (p) =>\n setProgress(p)\n );\n onChange(uploaded.id);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Upload failed — please try again.');\n } finally {\n setUploading(false);\n setProgress(null);\n setPendingName(null);\n setPendingPreview(null);\n }\n };\n\n const handleSelect = (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n event.target.value = ''; // allow re-selecting the same file\n if (file) void upload(file);\n };\n\n const handleDrop = (event: DragEvent<HTMLDivElement>) => {\n event.preventDefault();\n setDragActive(false);\n if (locked) return;\n const file = event.dataTransfer.files?.[0];\n if (file) void upload(file);\n };\n\n const handleDragOver = (event: DragEvent<HTMLDivElement>) => {\n event.preventDefault();\n if (!locked) setDragActive(true);\n };\n\n const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {\n event.preventDefault();\n setDragActive(false);\n };\n\n const pct = progress?.pct ?? 0;\n\n return (\n <FieldShell label={label} htmlFor={inputId} hint={hint} error={error} className={className}>\n <div\n onDragEnter={handleDragOver}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n >\n {uploading ? (\n /* Uploading — file row with preview, filename and a rose progress bar. */\n <div className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3\">\n <div className=\"flex h-14 w-14 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-bg-sunken\">\n {pendingPreview ? (\n <img src={pendingPreview} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : (\n <Loader2 className=\"h-5 w-5 animate-spin text-text-secondary\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {pendingName ?? 'Uploading…'}\n </p>\n <div className=\"mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-accent-primary transition-[width] duration-200\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary tabular-nums\">\n {pct}%\n {progress\n ? ` · ${formatBytes(progress.loaded)} of ${formatBytes(progress.total)}`\n : ''}\n </p>\n </div>\n </div>\n ) : previewUrl ? (\n /* Resolved image — preview with Replace / Remove actions. */\n <div className=\"relative overflow-hidden rounded-lg border border-border-seam bg-bg-sunken\">\n <img\n src={previewUrl}\n alt=\"\"\n className={cn(\n 'w-full object-cover',\n aspect === 'wide' ? 'aspect-video' : 'aspect-square max-h-64'\n )}\n />\n <div className=\"absolute inset-x-0 bottom-0 flex justify-end gap-2 bg-gradient-to-t from-bg-canvas/85 to-transparent p-2\">\n <button\n type=\"button\"\n disabled={locked}\n onClick={() => inputRef.current?.click()}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-bg-surface/90 px-3 py-1.5 text-sm font-medium text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <ImagePlus className=\"h-4 w-4\" /> Replace\n </button>\n <button\n type=\"button\"\n disabled={disabled}\n onClick={() => onChange('')}\n className=\"inline-flex items-center gap-1 rounded-lg bg-bg-surface/90 px-3 py-1.5 text-sm text-text-secondary shadow-sm backdrop-blur transition-colors hover:bg-status-error-bg-subtle hover:text-status-error-text disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <X className=\"h-4 w-4\" /> Remove\n </button>\n </div>\n </div>\n ) : (\n /* Empty — a real dropzone. */\n <button\n type=\"button\"\n disabled={locked}\n onClick={() => inputRef.current?.click()}\n className={cn(\n 'flex min-h-40 w-full flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 text-center transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-50',\n dragActive\n ? 'border-accent-primary bg-[var(--color-accent-soft)]'\n : 'border-border-seam hover:border-accent-primary/60 hover:bg-bg-sunken'\n )}\n >\n <UploadCloud className=\"h-8 w-8 text-text-secondary\" aria-hidden=\"true\" />\n <span className=\"text-sm font-medium text-text-primary\">\n Drag & drop an image here, or click to browse\n </span>\n <span className=\"text-xs text-text-secondary\">JPG · PNG · up to 10 MB</span>\n </button>\n )}\n </div>\n <input\n ref={inputRef}\n id={inputId}\n type=\"file\"\n accept={accept}\n className=\"sr-only\"\n aria-label={label}\n onChange={handleSelect}\n />\n </FieldShell>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { Thumbnail as t } from "./Thumbnail.js";
|
|
3
|
+
import { useLikeLook as n } from "../hooks/useCoolAndLovelyApi.js";
|
|
4
|
+
import { AddToLookbookButton as r } from "./AddToLookbookButton.js";
|
|
5
|
+
import { titleCase as i } from "../utils/formatters.js";
|
|
6
|
+
import { useEffect as a, useState as o } from "react";
|
|
7
|
+
import { Heart as s, Sparkles as c } from "lucide-react";
|
|
8
|
+
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
9
|
+
//#region src/coolandlovely/components/LookCard.tsx
|
|
10
|
+
var d = ({ lookId: t, likeCount: r }) => {
|
|
11
|
+
let i = n(), [c, d] = o(!1), [f, p] = o(r);
|
|
12
|
+
return a(() => {
|
|
13
|
+
c || p(r);
|
|
14
|
+
}, [r, c]), /* @__PURE__ */ u("button", {
|
|
15
|
+
type: "button",
|
|
16
|
+
onClick: () => {
|
|
17
|
+
c || i.isPending || (d(!0), p((e) => e + 1), i.mutate(t, {
|
|
18
|
+
onSuccess: (e) => p(e.likeCount),
|
|
19
|
+
onError: () => {
|
|
20
|
+
d(!1), p(r);
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
},
|
|
24
|
+
disabled: i.isPending,
|
|
25
|
+
"aria-pressed": c,
|
|
26
|
+
"aria-label": c ? "Liked" : "Like this look",
|
|
27
|
+
className: "inline-flex items-center gap-1 rounded-full bg-bg-surface/90 px-2 py-1 text-xs font-medium text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface disabled:opacity-60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
|
|
28
|
+
children: [/* @__PURE__ */ l(s, { className: e("h-3.5 w-3.5", c ? "fill-current text-accent-primary" : "text-text-secondary") }), /* @__PURE__ */ l("span", {
|
|
29
|
+
className: "tabular-nums",
|
|
30
|
+
children: f
|
|
31
|
+
})]
|
|
32
|
+
});
|
|
33
|
+
}, f = ({ id: n, title: a, occasion: o, imageFileId: f, likeCount: p, onOpen: m, showActions: h = !1, className: g }) => /* @__PURE__ */ u("div", {
|
|
34
|
+
className: e("group relative overflow-hidden rounded-xl border border-border-seam bg-bg-surface transition-shadow hover:shadow-lg", g),
|
|
35
|
+
children: [/* @__PURE__ */ l("button", {
|
|
36
|
+
type: "button",
|
|
37
|
+
onClick: m,
|
|
38
|
+
"aria-label": `Open look ${a}`,
|
|
39
|
+
className: "block w-full text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--color-focus-ring)]",
|
|
40
|
+
children: /* @__PURE__ */ l(t, {
|
|
41
|
+
icon: /* @__PURE__ */ l(c, { className: "h-8 w-8" }),
|
|
42
|
+
seed: n,
|
|
43
|
+
fileId: f,
|
|
44
|
+
alt: a,
|
|
45
|
+
className: "aspect-[3/4] w-full rounded-none",
|
|
46
|
+
overlay: /* @__PURE__ */ u("div", {
|
|
47
|
+
className: "pointer-events-none absolute inset-x-0 bottom-0 flex flex-col gap-1.5 bg-gradient-to-t from-bg-canvas/90 via-bg-canvas/45 to-transparent p-3 pt-10",
|
|
48
|
+
children: [/* @__PURE__ */ l("p", {
|
|
49
|
+
className: "truncate text-sm font-semibold text-text-primary",
|
|
50
|
+
children: a
|
|
51
|
+
}), /* @__PURE__ */ u("div", {
|
|
52
|
+
className: "flex items-center justify-between gap-2",
|
|
53
|
+
children: [/* @__PURE__ */ l("span", {
|
|
54
|
+
className: "inline-flex items-center rounded-full bg-bg-surface/85 px-2 py-0.5 text-[11px] font-medium text-text-primary backdrop-blur",
|
|
55
|
+
children: i(o)
|
|
56
|
+
}), !h && typeof p == "number" && p > 0 ? /* @__PURE__ */ u("span", {
|
|
57
|
+
className: "inline-flex items-center gap-1 text-xs text-text-secondary",
|
|
58
|
+
children: [/* @__PURE__ */ l(s, { className: "h-3.5 w-3.5 fill-current text-accent-primary" }), /* @__PURE__ */ l("span", {
|
|
59
|
+
className: "tabular-nums",
|
|
60
|
+
children: p
|
|
61
|
+
})]
|
|
62
|
+
}) : null]
|
|
63
|
+
})]
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
}), h ? /* @__PURE__ */ u("div", {
|
|
67
|
+
className: "absolute right-2 top-2 flex items-center gap-1.5 opacity-100 transition-opacity duration-200 sm:opacity-0 sm:focus-within:opacity-100 sm:group-hover:opacity-100",
|
|
68
|
+
children: [/* @__PURE__ */ l(d, {
|
|
69
|
+
lookId: n,
|
|
70
|
+
likeCount: p ?? 0
|
|
71
|
+
}), /* @__PURE__ */ l(r, { lookId: n })]
|
|
72
|
+
}) : null]
|
|
73
|
+
});
|
|
74
|
+
//#endregion
|
|
75
|
+
export { f as LookCard };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=LookCard.js.map
|