@burdenoff/microfe-comradecircle 2026.704.1 → 2026.704.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/comradecircle/components/Avatar.js +52 -17
- package/dist/comradecircle/components/Avatar.js.map +1 -1
- package/dist/comradecircle/components/AvatarUploadField.js +91 -0
- package/dist/comradecircle/components/AvatarUploadField.js.map +1 -0
- package/dist/comradecircle/components/CircleCard.js +55 -52
- package/dist/comradecircle/components/CircleCard.js.map +1 -1
- package/dist/comradecircle/components/CommentThread.js +80 -64
- package/dist/comradecircle/components/CommentThread.js.map +1 -1
- package/dist/comradecircle/components/CreateCircleDialog.js +74 -63
- package/dist/comradecircle/components/CreateCircleDialog.js.map +1 -1
- package/dist/comradecircle/components/EventCard.js +84 -58
- package/dist/comradecircle/components/EventCard.js.map +1 -1
- package/dist/comradecircle/components/FileImage.js +20 -0
- package/dist/comradecircle/components/FileImage.js.map +1 -0
- package/dist/comradecircle/components/ImageAttachments.js +97 -0
- package/dist/comradecircle/components/ImageAttachments.js.map +1 -0
- package/dist/comradecircle/components/MemberIdentity.js +4 -2
- package/dist/comradecircle/components/MemberIdentity.js.map +1 -1
- package/dist/comradecircle/components/PollCard.js +65 -44
- package/dist/comradecircle/components/PollCard.js.map +1 -1
- package/dist/comradecircle/components/PostCard.js +8 -15
- package/dist/comradecircle/components/PostCard.js.map +1 -1
- package/dist/comradecircle/components/PostComposer.js +45 -35
- package/dist/comradecircle/components/PostComposer.js.map +1 -1
- package/dist/comradecircle/components/ReactionBar.js +1 -1
- package/dist/comradecircle/components/ReactionBar.js.map +1 -1
- package/dist/comradecircle/components/StateViews.js +1 -16
- package/dist/comradecircle/components/StateViews.js.map +1 -1
- package/dist/comradecircle/components/StatusBadge.js +1 -1
- package/dist/comradecircle/components/StatusBadge.js.map +1 -1
- package/dist/comradecircle/hooks/index.js +1 -0
- package/dist/comradecircle/hooks/useComradeCircleApi.js +244 -241
- package/dist/comradecircle/hooks/useComradeCircleApi.js.map +1 -1
- package/dist/comradecircle/hooks/useFiles.js +78 -0
- package/dist/comradecircle/hooks/useFiles.js.map +1 -0
- package/dist/comradecircle/lib/graphql-cache.js +9 -0
- package/dist/comradecircle/lib/graphql-cache.js.map +1 -0
- package/dist/comradecircle/pages/FeedPage.js +79 -33
- package/dist/comradecircle/pages/FeedPage.js.map +1 -1
- package/dist/comradecircle/pages/circles/CircleDetailPage.js +354 -242
- package/dist/comradecircle/pages/circles/CircleDetailPage.js.map +1 -1
- package/dist/comradecircle/pages/circles/CirclesPage.js +97 -47
- package/dist/comradecircle/pages/circles/CirclesPage.js.map +1 -1
- package/dist/comradecircle/pages/events/EventDetailPage.js +193 -126
- package/dist/comradecircle/pages/events/EventDetailPage.js.map +1 -1
- package/dist/comradecircle/pages/events/EventsPage.js +74 -34
- package/dist/comradecircle/pages/events/EventsPage.js.map +1 -1
- package/dist/comradecircle/pages/members/MemberProfilePage.js +154 -81
- package/dist/comradecircle/pages/members/MemberProfilePage.js.map +1 -1
- package/dist/comradecircle/pages/members/MembersPage.js +141 -71
- package/dist/comradecircle/pages/members/MembersPage.js.map +1 -1
- package/dist/comradecircle/pages/profile/MyProfilePage.js +298 -191
- package/dist/comradecircle/pages/profile/MyProfilePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,35 +1,70 @@
|
|
|
1
1
|
import { cn as e } from "../utils/cn.js";
|
|
2
|
-
import {
|
|
2
|
+
import { FileImage as t } from "./FileImage.js";
|
|
3
|
+
import { jsx as n, jsxs as r } from "react/jsx-runtime";
|
|
3
4
|
//#region src/comradecircle/components/Avatar.tsx
|
|
4
|
-
var
|
|
5
|
+
var i = {
|
|
5
6
|
xs: "h-6 w-6 text-[0.6rem]",
|
|
6
7
|
sm: "h-8 w-8 text-xs",
|
|
7
8
|
md: "h-10 w-10 text-sm",
|
|
8
9
|
lg: "h-14 w-14 text-lg",
|
|
9
|
-
xl: "h-20 w-20 text-2xl"
|
|
10
|
-
|
|
10
|
+
xl: "h-20 w-20 text-2xl",
|
|
11
|
+
"2xl": "h-28 w-28 text-4xl"
|
|
12
|
+
}, a = {
|
|
11
13
|
ONLINE: "bg-status-success-text",
|
|
12
14
|
AWAY: "bg-status-warning-text",
|
|
13
15
|
BUSY: "bg-status-error-text",
|
|
14
16
|
OFFLINE: "bg-text-tertiary"
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
+
}, o = {
|
|
18
|
+
xs: "h-2 w-2",
|
|
19
|
+
sm: "h-2.5 w-2.5",
|
|
20
|
+
md: "h-2.5 w-2.5",
|
|
21
|
+
lg: "h-3 w-3",
|
|
22
|
+
xl: "h-3.5 w-3.5",
|
|
23
|
+
"2xl": "h-4 w-4"
|
|
24
|
+
}, s = [
|
|
25
|
+
"bg-blue-500/20 text-blue-700 dark:text-blue-200",
|
|
26
|
+
"bg-violet-500/20 text-violet-700 dark:text-violet-200",
|
|
27
|
+
"bg-emerald-500/20 text-emerald-700 dark:text-emerald-200",
|
|
28
|
+
"bg-amber-500/20 text-amber-700 dark:text-amber-100",
|
|
29
|
+
"bg-rose-500/20 text-rose-700 dark:text-rose-200",
|
|
30
|
+
"bg-cyan-500/20 text-cyan-700 dark:text-cyan-200",
|
|
31
|
+
"bg-fuchsia-500/20 text-fuchsia-700 dark:text-fuchsia-200",
|
|
32
|
+
"bg-teal-500/20 text-teal-700 dark:text-teal-200"
|
|
33
|
+
];
|
|
34
|
+
function c(e) {
|
|
35
|
+
if (!e) return s[0];
|
|
36
|
+
let t = 0;
|
|
37
|
+
for (let n = 0; n < e.length; n += 1) t = t * 31 + e.charCodeAt(n) | 0;
|
|
38
|
+
return s[Math.abs(t) % s.length];
|
|
39
|
+
}
|
|
40
|
+
function l(e) {
|
|
17
41
|
if (!e) return "?";
|
|
18
42
|
let t = e.trim().split(/\s+/).filter(Boolean);
|
|
19
43
|
return t.length === 0 ? "?" : t.length === 1 ? t[0].slice(0, 2).toUpperCase() : (t[0][0] + t[t.length - 1][0]).toUpperCase();
|
|
20
44
|
}
|
|
21
|
-
var
|
|
22
|
-
|
|
23
|
-
children: [/* @__PURE__ */ t("span", {
|
|
45
|
+
var u = ({ name: s, size: u = "md", presence: d, fileId: f, seed: p, ring: m = !1, className: h }) => {
|
|
46
|
+
let g = /* @__PURE__ */ n("span", {
|
|
24
47
|
"aria-hidden": "true",
|
|
25
|
-
className: e("
|
|
26
|
-
children:
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
className: e("flex h-full w-full items-center justify-center rounded-full font-semibold", c(p ?? s), i[u]),
|
|
49
|
+
children: l(s)
|
|
50
|
+
});
|
|
51
|
+
return /* @__PURE__ */ r("span", {
|
|
52
|
+
className: e("relative inline-flex shrink-0", h),
|
|
53
|
+
children: [/* @__PURE__ */ n("span", {
|
|
54
|
+
className: e("inline-flex items-center justify-center overflow-hidden rounded-full", m ? "ring-2 ring-[var(--color-accent-soft)] shadow-[0_0_0_4px_var(--color-accent-soft)]" : "ring-1 ring-border-seam", i[u]),
|
|
55
|
+
children: f ? /* @__PURE__ */ n(t, {
|
|
56
|
+
fileId: f,
|
|
57
|
+
alt: s ?? "Avatar",
|
|
58
|
+
className: "h-full w-full rounded-full",
|
|
59
|
+
fallback: g
|
|
60
|
+
}) : g
|
|
61
|
+
}), d && /* @__PURE__ */ n("span", {
|
|
62
|
+
className: e("absolute -bottom-0.5 -right-0.5 rounded-full ring-2 ring-bg-surface", o[u], a[d], d === "ONLINE" && "motion-safe:animate-vc-glow-pulse"),
|
|
63
|
+
"aria-label": `Presence: ${d.toLowerCase()}`
|
|
64
|
+
})]
|
|
65
|
+
});
|
|
66
|
+
};
|
|
32
67
|
//#endregion
|
|
33
|
-
export {
|
|
68
|
+
export { u as Avatar };
|
|
34
69
|
|
|
35
70
|
//# sourceMappingURL=Avatar.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Avatar.js","names":[],"sources":["../../../src/comradecircle/components/Avatar.tsx"],"sourcesContent":["/**\n * Avatar — initials
|
|
1
|
+
{"version":3,"file":"Avatar.js","names":[],"sources":["../../../src/comradecircle/components/Avatar.tsx"],"sourcesContent":["/**\n * Avatar — initials or image avatar with a deterministic tint + presence dot.\n * @module comradecircle/components\n *\n * When a wspace-files-svc `fileId` is present the image is rendered; otherwise\n * we show initials on a per-identity tint (hashed from the name) so grids of\n * avatars read as distinct, colourful, and alive — while staying theme-aware\n * (each tint ships a light + dark text pairing). An optional presence dot and a\n * soft accent ring round out the treatment.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport { FileImage } from './FileImage';\nimport type { ComPresence } from '../types';\n\ntype AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';\n\nconst sizeClass: Record<AvatarSize, string> = {\n xs: 'h-6 w-6 text-[0.6rem]',\n sm: 'h-8 w-8 text-xs',\n md: 'h-10 w-10 text-sm',\n lg: 'h-14 w-14 text-lg',\n xl: 'h-20 w-20 text-2xl',\n '2xl': 'h-28 w-28 text-4xl',\n};\n\nconst presenceDot: Record<ComPresence, string> = {\n ONLINE: 'bg-status-success-text',\n AWAY: 'bg-status-warning-text',\n BUSY: 'bg-status-error-text',\n OFFLINE: 'bg-text-tertiary',\n};\n\nconst presenceDotSize: Record<AvatarSize, string> = {\n xs: 'h-2 w-2',\n sm: 'h-2.5 w-2.5',\n md: 'h-2.5 w-2.5',\n lg: 'h-3 w-3',\n xl: 'h-3.5 w-3.5',\n '2xl': 'h-4 w-4',\n};\n\n// Decorative per-identity tints. Not semantic status — just a stable, colourful\n// identity cue. Each ships a dark-mode and light-mode readable text pairing so\n// initials stay legible under either theme.\nconst TINTS = [\n 'bg-blue-500/20 text-blue-700 dark:text-blue-200',\n 'bg-violet-500/20 text-violet-700 dark:text-violet-200',\n 'bg-emerald-500/20 text-emerald-700 dark:text-emerald-200',\n 'bg-amber-500/20 text-amber-700 dark:text-amber-100',\n 'bg-rose-500/20 text-rose-700 dark:text-rose-200',\n 'bg-cyan-500/20 text-cyan-700 dark:text-cyan-200',\n 'bg-fuchsia-500/20 text-fuchsia-700 dark:text-fuchsia-200',\n 'bg-teal-500/20 text-teal-700 dark:text-teal-200',\n] as const;\n\nfunction tintFor(seed: string | null | undefined): string {\n if (!seed) return TINTS[0]!;\n let hash = 0;\n for (let i = 0; i < seed.length; i += 1) hash = (hash * 31 + seed.charCodeAt(i)) | 0;\n return TINTS[Math.abs(hash) % TINTS.length]!;\n}\n\nexport function initialsOf(name: string | null | undefined): string {\n if (!name) return '?';\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return '?';\n if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase();\n return (parts[0]![0]! + parts[parts.length - 1]![0]!).toUpperCase();\n}\n\ninterface AvatarProps {\n name?: string | null;\n size?: AvatarSize;\n presence?: ComPresence | null;\n /** wspace-files-svc file id — when set + resolvable, renders the image. */\n fileId?: string | null;\n /** Stable seed for the tint (defaults to name). Pass an id for stability. */\n seed?: string | null;\n /** Add a soft accent ring + glow (hero/profile treatment). */\n ring?: boolean;\n className?: string;\n}\n\nexport const Avatar: FC<AvatarProps> = ({\n name,\n size = 'md',\n presence,\n fileId,\n seed,\n ring = false,\n className,\n}) => {\n const initials = (\n <span\n aria-hidden=\"true\"\n className={cn(\n 'flex h-full w-full items-center justify-center rounded-full font-semibold',\n tintFor(seed ?? name),\n sizeClass[size]\n )}\n >\n {initialsOf(name)}\n </span>\n );\n\n return (\n <span className={cn('relative inline-flex shrink-0', className)}>\n <span\n className={cn(\n 'inline-flex items-center justify-center overflow-hidden rounded-full',\n ring\n ? 'ring-2 ring-[var(--color-accent-soft)] shadow-[0_0_0_4px_var(--color-accent-soft)]'\n : 'ring-1 ring-border-seam',\n sizeClass[size]\n )}\n >\n {fileId ? (\n <FileImage\n fileId={fileId}\n alt={name ?? 'Avatar'}\n className=\"h-full w-full rounded-full\"\n fallback={initials}\n />\n ) : (\n initials\n )}\n </span>\n {presence && (\n <span\n className={cn(\n 'absolute -bottom-0.5 -right-0.5 rounded-full ring-2 ring-bg-surface',\n presenceDotSize[size],\n presenceDot[presence],\n presence === 'ONLINE' && 'motion-safe:animate-vc-glow-pulse'\n )}\n aria-label={`Presence: ${presence.toLowerCase()}`}\n />\n )}\n </span>\n );\n};\n"],"mappings":";;;;AAkBA,IAAM,IAAwC;CAC5C,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,OAAO;CACR,EAEK,IAA2C;CAC/C,QAAQ;CACR,MAAM;CACN,MAAM;CACN,SAAS;CACV,EAEK,IAA8C;CAClD,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,OAAO;CACR,EAKK,IAAQ;CACZ;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,SAAS,EAAQ,GAAyC;AACxD,KAAI,CAAC,EAAM,QAAO,EAAM;CACxB,IAAI,IAAO;AACX,MAAK,IAAI,IAAI,GAAG,IAAI,EAAK,QAAQ,KAAK,EAAG,KAAQ,IAAO,KAAK,EAAK,WAAW,EAAE,GAAI;AACnF,QAAO,EAAM,KAAK,IAAI,EAAK,GAAG,EAAM;;AAGtC,SAAgB,EAAW,GAAyC;AAClE,KAAI,CAAC,EAAM,QAAO;CAClB,IAAM,IAAQ,EAAK,MAAM,CAAC,MAAM,MAAM,CAAC,OAAO,QAAQ;AAGtD,QAFI,EAAM,WAAW,IAAU,MAC3B,EAAM,WAAW,IAAU,EAAM,GAAI,MAAM,GAAG,EAAE,CAAC,aAAa,IAC1D,EAAM,GAAI,KAAM,EAAM,EAAM,SAAS,GAAI,IAAK,aAAa;;AAgBrE,IAAa,KAA2B,EACtC,SACA,UAAO,MACP,aACA,WACA,SACA,UAAO,IACP,mBACI;CACJ,IAAM,IACJ,kBAAC,QAAD;EACE,eAAY;EACZ,WAAW,EACT,6EACA,EAAQ,KAAQ,EAAK,EACrB,EAAU,GACX;YAEA,EAAW,EAAK;EACZ,CAAA;AAGT,QACE,kBAAC,QAAD;EAAM,WAAW,EAAG,iCAAiC,EAAU;YAA/D,CACE,kBAAC,QAAD;GACE,WAAW,EACT,wEACA,IACI,uFACA,2BACJ,EAAU,GACX;aAEA,IACC,kBAAC,GAAD;IACU;IACR,KAAK,KAAQ;IACb,WAAU;IACV,UAAU;IACV,CAAA,GAEF;GAEG,CAAA,EACN,KACC,kBAAC,QAAD;GACE,WAAW,EACT,uEACA,EAAgB,IAChB,EAAY,IACZ,MAAa,YAAY,oCAC1B;GACD,cAAY,aAAa,EAAS,aAAa;GAC/C,CAAA,CAEC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { FILE_FOLDERS as t, uploadImageToFiles as n } from "../hooks/useFiles.js";
|
|
3
|
+
import { Avatar as r } from "./Avatar.js";
|
|
4
|
+
import { describeError as i, notify as a } from "../lib/toast.js";
|
|
5
|
+
import { useId as o, useRef as s, useState as c } from "react";
|
|
6
|
+
import { Camera as l, Loader2 as u } from "lucide-react";
|
|
7
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
8
|
+
//#region src/comradecircle/components/AvatarUploadField.tsx
|
|
9
|
+
var p = 5 * 1024 * 1024;
|
|
10
|
+
function m({ fileId: m, name: h, onUploaded: g, label: _ = "Upload avatar", disabled: v = !1 }) {
|
|
11
|
+
let y = s(null), b = o(), [x, S] = c(!1), [C, w] = c(null);
|
|
12
|
+
return /* @__PURE__ */ f("div", {
|
|
13
|
+
className: "flex items-center gap-4",
|
|
14
|
+
children: [/* @__PURE__ */ f("button", {
|
|
15
|
+
type: "button",
|
|
16
|
+
onClick: () => y.current?.click(),
|
|
17
|
+
disabled: v || x,
|
|
18
|
+
"aria-label": _,
|
|
19
|
+
className: "group relative h-16 w-16 shrink-0 rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-accent disabled:cursor-not-allowed",
|
|
20
|
+
children: [
|
|
21
|
+
C ? /* @__PURE__ */ d("img", {
|
|
22
|
+
src: C,
|
|
23
|
+
alt: h ?? "Avatar",
|
|
24
|
+
className: "h-16 w-16 rounded-full object-cover ring-1 ring-border-seam"
|
|
25
|
+
}) : /* @__PURE__ */ d(r, {
|
|
26
|
+
name: h,
|
|
27
|
+
fileId: m,
|
|
28
|
+
size: "lg"
|
|
29
|
+
}),
|
|
30
|
+
/* @__PURE__ */ d("span", {
|
|
31
|
+
className: "absolute inset-0 flex items-center justify-center rounded-full bg-bg-canvas/60 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100",
|
|
32
|
+
children: /* @__PURE__ */ d(l, { className: "h-5 w-5 text-text-primary" })
|
|
33
|
+
}),
|
|
34
|
+
x && /* @__PURE__ */ d("span", {
|
|
35
|
+
className: "absolute inset-0 flex items-center justify-center rounded-full bg-bg-canvas/70",
|
|
36
|
+
children: /* @__PURE__ */ d(u, { className: "h-5 w-5 animate-spin text-text-primary" })
|
|
37
|
+
})
|
|
38
|
+
]
|
|
39
|
+
}), /* @__PURE__ */ f("div", { children: [
|
|
40
|
+
/* @__PURE__ */ d("input", {
|
|
41
|
+
ref: y,
|
|
42
|
+
id: b,
|
|
43
|
+
type: "file",
|
|
44
|
+
accept: "image/*",
|
|
45
|
+
className: "sr-only",
|
|
46
|
+
onChange: async (e) => {
|
|
47
|
+
let r = e.target.files?.[0];
|
|
48
|
+
if (e.target.value = "", r) {
|
|
49
|
+
if (!r.type.startsWith("image/")) {
|
|
50
|
+
a.error("Unsupported file", "Please choose an image.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (r.size > p) {
|
|
54
|
+
a.error("Image too large", "Please choose an image under 5 MB.");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
S(!0);
|
|
58
|
+
try {
|
|
59
|
+
let e = await n(r, t.avatars);
|
|
60
|
+
w(e.url), g(e.id), a.success("Avatar updated");
|
|
61
|
+
} catch (e) {
|
|
62
|
+
a.error("Upload failed", i(e));
|
|
63
|
+
} finally {
|
|
64
|
+
S(!1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
disabled: v || x
|
|
69
|
+
}),
|
|
70
|
+
/* @__PURE__ */ f("button", {
|
|
71
|
+
type: "button",
|
|
72
|
+
onClick: () => y.current?.click(),
|
|
73
|
+
disabled: v || x,
|
|
74
|
+
className: e("inline-flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken", (v || x) && "cursor-not-allowed opacity-60"),
|
|
75
|
+
children: [
|
|
76
|
+
/* @__PURE__ */ d(l, { className: "h-4 w-4" }),
|
|
77
|
+
" ",
|
|
78
|
+
_
|
|
79
|
+
]
|
|
80
|
+
}),
|
|
81
|
+
/* @__PURE__ */ d("p", {
|
|
82
|
+
className: "mt-1 text-xs text-text-tertiary",
|
|
83
|
+
children: "PNG or JPG, up to 5 MB."
|
|
84
|
+
})
|
|
85
|
+
] })]
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { m as AvatarUploadField };
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=AvatarUploadField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvatarUploadField.js","names":[],"sources":["../../../src/comradecircle/components/AvatarUploadField.tsx"],"sourcesContent":["/**\n * AvatarUploadField — upload a circular avatar image to wspace-files-svc.\n * @module comradecircle/components\n *\n * On select, uploads via {@link uploadImageToFiles} and hands the resulting file\n * id back to the caller (persisted as avatarFileId). Shows the current avatar\n * (by file id) or the freshly-uploaded preview; falls back to initials.\n */\n\nimport { useId, useRef, useState } from 'react';\nimport { Camera, Loader2 } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { Avatar } from './Avatar';\nimport { uploadImageToFiles, FILE_FOLDERS } from '../hooks/useFiles';\nimport { notify, describeError } from '../lib/toast';\n\ninterface AvatarUploadFieldProps {\n /** Current avatar file id (persisted). */\n fileId?: string | null;\n /** Name used for the initials fallback. */\n name?: string | null;\n /** Called with the new file id after a successful upload. */\n onUploaded: (fileId: string) => void;\n label?: string;\n disabled?: boolean;\n}\n\nconst MAX_BYTES = 5 * 1024 * 1024;\n\nexport function AvatarUploadField({\n fileId,\n name,\n onUploaded,\n label = 'Upload avatar',\n disabled = false,\n}: AvatarUploadFieldProps) {\n const inputRef = useRef<HTMLInputElement | null>(null);\n const inputId = useId();\n const [uploading, setUploading] = useState(false);\n const [previewUrl, setPreviewUrl] = useState<string | null>(null);\n\n const handleSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n event.target.value = '';\n if (!file) return;\n if (!file.type.startsWith('image/')) {\n notify.error('Unsupported file', 'Please choose an image.');\n return;\n }\n if (file.size > MAX_BYTES) {\n notify.error('Image too large', 'Please choose an image under 5 MB.');\n return;\n }\n setUploading(true);\n try {\n const uploaded = await uploadImageToFiles(file, FILE_FOLDERS.avatars);\n setPreviewUrl(uploaded.url);\n onUploaded(uploaded.id);\n notify.success('Avatar updated');\n } catch (err) {\n notify.error('Upload failed', describeError(err));\n } finally {\n setUploading(false);\n }\n };\n\n return (\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => inputRef.current?.click()}\n disabled={disabled || uploading}\n aria-label={label}\n className=\"group relative h-16 w-16 shrink-0 rounded-full focus:outline-none focus-visible:ring-2 focus-visible:ring-accent disabled:cursor-not-allowed\"\n >\n {previewUrl ? (\n <img\n src={previewUrl}\n alt={name ?? 'Avatar'}\n className=\"h-16 w-16 rounded-full object-cover ring-1 ring-border-seam\"\n />\n ) : (\n <Avatar name={name} fileId={fileId} size=\"lg\" />\n )}\n {/* Hover overlay: click the image directly to change it. */}\n <span className=\"absolute inset-0 flex items-center justify-center rounded-full bg-bg-canvas/60 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100\">\n <Camera className=\"h-5 w-5 text-text-primary\" />\n </span>\n {uploading && (\n <span className=\"absolute inset-0 flex items-center justify-center rounded-full bg-bg-canvas/70\">\n <Loader2 className=\"h-5 w-5 animate-spin text-text-primary\" />\n </span>\n )}\n </button>\n <div>\n <input\n ref={inputRef}\n id={inputId}\n type=\"file\"\n accept=\"image/*\"\n className=\"sr-only\"\n onChange={handleSelect}\n disabled={disabled || uploading}\n />\n <button\n type=\"button\"\n onClick={() => inputRef.current?.click()}\n disabled={disabled || uploading}\n className={cn(\n 'inline-flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken',\n (disabled || uploading) && 'cursor-not-allowed opacity-60'\n )}\n >\n <Camera className=\"h-4 w-4\" /> {label}\n </button>\n <p className=\"mt-1 text-xs text-text-tertiary\">PNG or JPG, up to 5 MB.</p>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;AA2BA,IAAM,IAAY,IAAI,OAAO;AAE7B,SAAgB,EAAkB,EAChC,WACA,SACA,eACA,WAAQ,iBACR,cAAW,MACc;CACzB,IAAM,IAAW,EAAgC,KAAK,EAChD,IAAU,GAAO,EACjB,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,CAAC,GAAY,KAAiB,EAAwB,KAAK;AA2BjE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAS,SAAS,OAAO;GACxC,UAAU,KAAY;GACtB,cAAY;GACZ,WAAU;aALZ;IAOG,IACC,kBAAC,OAAD;KACE,KAAK;KACL,KAAK,KAAQ;KACb,WAAU;KACV,CAAA,GAEF,kBAAC,GAAD;KAAc;KAAc;KAAQ,MAAK;KAAO,CAAA;IAGlD,kBAAC,QAAD;KAAM,WAAU;eACd,kBAAC,GAAD,EAAQ,WAAU,6BAA8B,CAAA;KAC3C,CAAA;IACN,KACC,kBAAC,QAAD;KAAM,WAAU;eACd,kBAAC,GAAD,EAAS,WAAU,0CAA2C,CAAA;KACzD,CAAA;IAEF;MACT,kBAAC,OAAD,EAAA,UAAA;GACE,kBAAC,SAAD;IACE,KAAK;IACL,IAAI;IACJ,MAAK;IACL,QAAO;IACP,WAAU;IACV,UA5Da,OAAO,MAA+C;KACzE,IAAM,IAAO,EAAM,OAAO,QAAQ;AAClC,WAAM,OAAO,QAAQ,IAChB,GACL;UAAI,CAAC,EAAK,KAAK,WAAW,SAAS,EAAE;AACnC,SAAO,MAAM,oBAAoB,0BAA0B;AAC3D;;AAEF,UAAI,EAAK,OAAO,GAAW;AACzB,SAAO,MAAM,mBAAmB,qCAAqC;AACrE;;AAEF,QAAa,GAAK;AAClB,UAAI;OACF,IAAM,IAAW,MAAM,EAAmB,GAAM,EAAa,QAAQ;AAGrE,OAFA,EAAc,EAAS,IAAI,EAC3B,EAAW,EAAS,GAAG,EACvB,EAAO,QAAQ,iBAAiB;eACzB,GAAK;AACZ,SAAO,MAAM,iBAAiB,EAAc,EAAI,CAAC;gBACzC;AACR,SAAa,GAAM;;;;IAwCf,UAAU,KAAY;IACtB,CAAA;GACF,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAS,SAAS,OAAO;IACxC,UAAU,KAAY;IACtB,WAAW,EACT,6KACC,KAAY,MAAc,gCAC5B;cAPH;KASE,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;;KAAE;KACzB;;GACT,kBAAC,KAAD;IAAG,WAAU;cAAkC;IAA2B,CAAA;GACtE,EAAA,CAAA,CACF"}
|
|
@@ -1,110 +1,113 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { useComJoinCircle as t, useComLeaveCircle as n } from "../hooks/useComradeCircleApi.js";
|
|
2
3
|
import "../hooks/index.js";
|
|
3
|
-
import { Avatar as
|
|
4
|
-
import { describeError as
|
|
5
|
-
import { formatEnumLabel as
|
|
6
|
-
import { StatusBadge as
|
|
7
|
-
import { Hash as
|
|
8
|
-
import { Fragment as
|
|
9
|
-
import { Button as
|
|
4
|
+
import { Avatar as r } from "./Avatar.js";
|
|
5
|
+
import { describeError as i, notify as a } from "../lib/toast.js";
|
|
6
|
+
import { formatEnumLabel as o, formatNumber as s } from "../utils/formatters.js";
|
|
7
|
+
import { StatusBadge as c } from "./StatusBadge.js";
|
|
8
|
+
import { Hash as l, Users as u } from "lucide-react";
|
|
9
|
+
import { Fragment as d, jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
10
|
+
import { Button as m, GlassCard as h } from "@burdenoff/fe-libs/ui";
|
|
10
11
|
//#region src/comradecircle/components/CircleCard.tsx
|
|
11
|
-
function
|
|
12
|
-
let
|
|
13
|
-
return /* @__PURE__ */
|
|
12
|
+
function g({ circle: g, membership: _, onOpen: v }) {
|
|
13
|
+
let y = t(), b = n(), x = _?.status === "ACTIVE";
|
|
14
|
+
return /* @__PURE__ */ p(h, {
|
|
14
15
|
treatment: "plain",
|
|
15
|
-
className: "flex flex-col p-5",
|
|
16
|
+
className: e("flex flex-col p-5 transition-all duration-200 motion-safe:hover:-translate-y-0.5 hover:border-[var(--color-accent-soft)]", (_?.role === "OWNER" || _?.role === "ADMIN") && "ring-1 ring-[var(--color-accent-soft)]"),
|
|
16
17
|
children: [
|
|
17
|
-
/* @__PURE__ */
|
|
18
|
+
/* @__PURE__ */ p("div", {
|
|
18
19
|
className: "flex items-start gap-3",
|
|
19
|
-
children: [/* @__PURE__ */
|
|
20
|
-
name:
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
children: [/* @__PURE__ */ f(r, {
|
|
21
|
+
name: g.name,
|
|
22
|
+
seed: g.id,
|
|
23
|
+
size: "lg",
|
|
24
|
+
fileId: g.avatar?.id
|
|
25
|
+
}), /* @__PURE__ */ p("div", {
|
|
23
26
|
className: "min-w-0 flex-1",
|
|
24
27
|
children: [
|
|
25
|
-
/* @__PURE__ */
|
|
28
|
+
/* @__PURE__ */ f("button", {
|
|
26
29
|
type: "button",
|
|
27
|
-
onClick:
|
|
30
|
+
onClick: v,
|
|
28
31
|
className: "truncate text-left text-base font-semibold text-text-primary hover:underline",
|
|
29
|
-
children:
|
|
32
|
+
children: g.name
|
|
30
33
|
}),
|
|
31
|
-
|
|
34
|
+
g.topic && /* @__PURE__ */ f("p", {
|
|
32
35
|
className: "truncate text-sm text-text-secondary",
|
|
33
|
-
children:
|
|
36
|
+
children: g.topic
|
|
34
37
|
}),
|
|
35
|
-
/* @__PURE__ */
|
|
38
|
+
/* @__PURE__ */ p("div", {
|
|
36
39
|
className: "mt-1.5 flex flex-wrap items-center gap-1.5",
|
|
37
40
|
children: [
|
|
38
|
-
/* @__PURE__ */
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
/* @__PURE__ */ f(c, { status: g.visibility }),
|
|
42
|
+
g.status !== "ACTIVE" && /* @__PURE__ */ f(c, { status: g.status }),
|
|
43
|
+
x && _ && /* @__PURE__ */ f(c, { status: _.role })
|
|
41
44
|
]
|
|
42
45
|
})
|
|
43
46
|
]
|
|
44
47
|
})]
|
|
45
48
|
}),
|
|
46
|
-
|
|
49
|
+
g.description && /* @__PURE__ */ f("p", {
|
|
47
50
|
className: "mt-3 line-clamp-2 text-sm text-text-secondary",
|
|
48
|
-
children:
|
|
51
|
+
children: g.description
|
|
49
52
|
}),
|
|
50
|
-
|
|
53
|
+
g.tags.length > 0 && /* @__PURE__ */ f("div", {
|
|
51
54
|
className: "mt-3 flex flex-wrap gap-1.5",
|
|
52
|
-
children:
|
|
55
|
+
children: g.tags.slice(0, 4).map((e) => /* @__PURE__ */ p("span", {
|
|
53
56
|
className: "rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary",
|
|
54
57
|
children: ["#", e]
|
|
55
58
|
}, e))
|
|
56
59
|
}),
|
|
57
|
-
/* @__PURE__ */
|
|
60
|
+
/* @__PURE__ */ p("div", {
|
|
58
61
|
className: "mt-4 flex items-center justify-between gap-3 border-t border-border-seam pt-3",
|
|
59
|
-
children: [/* @__PURE__ */
|
|
62
|
+
children: [/* @__PURE__ */ p("div", {
|
|
60
63
|
className: "flex items-center gap-3 text-xs text-text-tertiary",
|
|
61
|
-
children: [/* @__PURE__ */
|
|
64
|
+
children: [/* @__PURE__ */ p("span", {
|
|
62
65
|
className: "inline-flex items-center gap-1",
|
|
63
66
|
children: [
|
|
64
|
-
/* @__PURE__ */
|
|
67
|
+
/* @__PURE__ */ f(u, { className: "h-3.5 w-3.5" }),
|
|
65
68
|
" ",
|
|
66
|
-
|
|
69
|
+
s(g.memberCount)
|
|
67
70
|
]
|
|
68
|
-
}), /* @__PURE__ */
|
|
71
|
+
}), /* @__PURE__ */ p("span", {
|
|
69
72
|
className: "inline-flex items-center gap-1",
|
|
70
73
|
children: [
|
|
71
|
-
/* @__PURE__ */
|
|
74
|
+
/* @__PURE__ */ f(l, { className: "h-3.5 w-3.5" }),
|
|
72
75
|
" ",
|
|
73
|
-
|
|
76
|
+
g.channels?.length ?? 0
|
|
74
77
|
]
|
|
75
78
|
})]
|
|
76
|
-
}), /* @__PURE__ */
|
|
79
|
+
}), /* @__PURE__ */ f("div", {
|
|
77
80
|
className: "flex items-center gap-2",
|
|
78
|
-
children:
|
|
81
|
+
children: x ? /* @__PURE__ */ p(d, { children: [/* @__PURE__ */ f(m, {
|
|
79
82
|
size: "sm",
|
|
80
83
|
variant: "outline",
|
|
81
84
|
onClick: async () => {
|
|
82
85
|
try {
|
|
83
|
-
await
|
|
86
|
+
await b.mutateAsync(g.id), a.success("Left circle", `You have left ${g.name}.`);
|
|
84
87
|
} catch (e) {
|
|
85
|
-
|
|
88
|
+
a.error("Could not leave", i(e));
|
|
86
89
|
}
|
|
87
90
|
},
|
|
88
|
-
disabled:
|
|
91
|
+
disabled: b.isPending,
|
|
89
92
|
children: "Leave"
|
|
90
|
-
}), /* @__PURE__ */
|
|
93
|
+
}), /* @__PURE__ */ f(m, {
|
|
91
94
|
size: "sm",
|
|
92
|
-
onClick:
|
|
95
|
+
onClick: v,
|
|
93
96
|
children: "Open"
|
|
94
|
-
})] }) :
|
|
97
|
+
})] }) : g.status === "ACTIVE" ? /* @__PURE__ */ f(m, {
|
|
95
98
|
size: "sm",
|
|
96
99
|
onClick: async () => {
|
|
97
100
|
try {
|
|
98
|
-
await
|
|
101
|
+
await y.mutateAsync(g.id), a.success("Joined circle", `You are now a member of ${g.name}.`);
|
|
99
102
|
} catch (e) {
|
|
100
|
-
|
|
103
|
+
a.error("Could not join", i(e));
|
|
101
104
|
}
|
|
102
105
|
},
|
|
103
|
-
disabled:
|
|
106
|
+
disabled: y.isPending,
|
|
104
107
|
children: "Join"
|
|
105
|
-
}) : /* @__PURE__ */
|
|
108
|
+
}) : /* @__PURE__ */ f("span", {
|
|
106
109
|
className: "text-xs text-text-tertiary",
|
|
107
|
-
children:
|
|
110
|
+
children: o(g.status)
|
|
108
111
|
})
|
|
109
112
|
})]
|
|
110
113
|
})
|
|
@@ -112,6 +115,6 @@ function h({ circle: h, membership: g, onOpen: _ }) {
|
|
|
112
115
|
});
|
|
113
116
|
}
|
|
114
117
|
//#endregion
|
|
115
|
-
export {
|
|
118
|
+
export { g as CircleCard };
|
|
116
119
|
|
|
117
120
|
//# sourceMappingURL=CircleCard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CircleCard.js","names":[],"sources":["../../../src/comradecircle/components/CircleCard.tsx"],"sourcesContent":["/**\n * CircleCard — a discovery/overview tile for a community circle.\n * @module comradecircle/components\n *\n * Writes `comJoinCircle` / `comLeaveCircle`. When the caller already belongs to\n * the circle the card shows their role and an Open action; otherwise a Join\n * action. Membership is supplied by the parent (which knows the caller's set).\n */\n\nimport { Users, Hash } from 'lucide-react';\nimport { GlassCard, Button } from '@burdenoff/fe-libs/ui';\nimport { Avatar } from './Avatar';\nimport { StatusBadge } from './StatusBadge';\nimport { useComJoinCircle, useComLeaveCircle } from '../hooks';\nimport { formatEnumLabel, formatNumber } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComCircle, ComMembership } from '../types';\n\ninterface CircleCardProps {\n circle: ComCircle;\n membership?: ComMembership | null;\n onOpen: () => void;\n}\n\nexport function CircleCard({ circle, membership, onOpen }: CircleCardProps) {\n const join = useComJoinCircle();\n const leave = useComLeaveCircle();\n const isMember = membership?.status === 'ACTIVE';\n\n const handleJoin = async () => {\n try {\n await join.mutateAsync(circle.id);\n notify.success('Joined circle', `You are now a member of ${circle.name}.`);\n } catch (err) {\n notify.error('Could not join', describeError(err));\n }\n };\n\n const handleLeave = async () => {\n try {\n await leave.mutateAsync(circle.id);\n notify.success('Left circle', `You have left ${circle.name}.`);\n } catch (err) {\n notify.error('Could not leave', describeError(err));\n }\n };\n\n return (\n <GlassCard
|
|
1
|
+
{"version":3,"file":"CircleCard.js","names":[],"sources":["../../../src/comradecircle/components/CircleCard.tsx"],"sourcesContent":["/**\n * CircleCard — a discovery/overview tile for a community circle.\n * @module comradecircle/components\n *\n * Writes `comJoinCircle` / `comLeaveCircle`. When the caller already belongs to\n * the circle the card shows their role and an Open action; otherwise a Join\n * action. Membership is supplied by the parent (which knows the caller's set).\n */\n\nimport { Users, Hash } from 'lucide-react';\nimport { GlassCard, Button } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { Avatar } from './Avatar';\nimport { StatusBadge } from './StatusBadge';\nimport { useComJoinCircle, useComLeaveCircle } from '../hooks';\nimport { formatEnumLabel, formatNumber } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComCircle, ComMembership } from '../types';\n\ninterface CircleCardProps {\n circle: ComCircle;\n membership?: ComMembership | null;\n onOpen: () => void;\n}\n\nexport function CircleCard({ circle, membership, onOpen }: CircleCardProps) {\n const join = useComJoinCircle();\n const leave = useComLeaveCircle();\n const isMember = membership?.status === 'ACTIVE';\n\n const handleJoin = async () => {\n try {\n await join.mutateAsync(circle.id);\n notify.success('Joined circle', `You are now a member of ${circle.name}.`);\n } catch (err) {\n notify.error('Could not join', describeError(err));\n }\n };\n\n const handleLeave = async () => {\n try {\n await leave.mutateAsync(circle.id);\n notify.success('Left circle', `You have left ${circle.name}.`);\n } catch (err) {\n notify.error('Could not leave', describeError(err));\n }\n };\n\n const isOwner = membership?.role === 'OWNER' || membership?.role === 'ADMIN';\n\n return (\n <GlassCard\n treatment=\"plain\"\n className={cn(\n 'flex flex-col p-5 transition-all duration-200 motion-safe:hover:-translate-y-0.5 hover:border-[var(--color-accent-soft)]',\n isOwner && 'ring-1 ring-[var(--color-accent-soft)]'\n )}\n >\n <div className=\"flex items-start gap-3\">\n <Avatar name={circle.name} seed={circle.id} size=\"lg\" fileId={circle.avatar?.id} />\n <div className=\"min-w-0 flex-1\">\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"truncate text-left text-base font-semibold text-text-primary hover:underline\"\n >\n {circle.name}\n </button>\n {circle.topic && <p className=\"truncate text-sm text-text-secondary\">{circle.topic}</p>}\n <div className=\"mt-1.5 flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={circle.visibility} />\n {circle.status !== 'ACTIVE' && <StatusBadge status={circle.status} />}\n {isMember && membership && <StatusBadge status={membership.role} />}\n </div>\n </div>\n </div>\n\n {circle.description && (\n <p className=\"mt-3 line-clamp-2 text-sm text-text-secondary\">{circle.description}</p>\n )}\n\n {circle.tags.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1.5\">\n {circle.tags.slice(0, 4).map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\"\n >\n #{tag}\n </span>\n ))}\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between gap-3 border-t border-border-seam pt-3\">\n <div className=\"flex items-center gap-3 text-xs text-text-tertiary\">\n <span className=\"inline-flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" /> {formatNumber(circle.memberCount)}\n </span>\n <span className=\"inline-flex items-center gap-1\">\n <Hash className=\"h-3.5 w-3.5\" /> {circle.channels?.length ?? 0}\n </span>\n </div>\n <div className=\"flex items-center gap-2\">\n {isMember ? (\n <>\n <Button size=\"sm\" variant=\"outline\" onClick={handleLeave} disabled={leave.isPending}>\n Leave\n </Button>\n <Button size=\"sm\" onClick={onOpen}>\n Open\n </Button>\n </>\n ) : circle.status === 'ACTIVE' ? (\n <Button size=\"sm\" onClick={handleJoin} disabled={join.isPending}>\n Join\n </Button>\n ) : (\n <span className=\"text-xs text-text-tertiary\">{formatEnumLabel(circle.status)}</span>\n )}\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;;;;AAyBA,SAAgB,EAAW,EAAE,WAAQ,eAAY,aAA2B;CAC1E,IAAM,IAAO,GAAkB,EACzB,IAAQ,GAAmB,EAC3B,IAAW,GAAY,WAAW;AAsBxC,QACE,kBAAC,GAAD;EACE,WAAU;EACV,WAAW,EACT,6HANU,GAAY,SAAS,WAAW,GAAY,SAAS,YAOpD,yCACZ;YALH;GAOE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAQ,MAAM,EAAO;KAAM,MAAM,EAAO;KAAI,MAAK;KAAK,QAAQ,EAAO,QAAQ;KAAM,CAAA,EACnF,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,WAAU;iBAET,EAAO;OACD,CAAA;MACR,EAAO,SAAS,kBAAC,KAAD;OAAG,WAAU;iBAAwC,EAAO;OAAU,CAAA;MACvF,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD,EAAa,QAAQ,EAAO,YAAc,CAAA;QACzC,EAAO,WAAW,YAAY,kBAAC,GAAD,EAAa,QAAQ,EAAO,QAAU,CAAA;QACpE,KAAY,KAAc,kBAAC,GAAD,EAAa,QAAQ,EAAW,MAAQ,CAAA;QAC/D;;MACF;OACF;;GAEL,EAAO,eACN,kBAAC,KAAD;IAAG,WAAU;cAAiD,EAAO;IAAgB,CAAA;GAGtF,EAAO,KAAK,SAAS,KACpB,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAO,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,MAC5B,kBAAC,QAAD;KAEE,WAAU;eAFZ,CAGC,KACG,EACG;OAJA,EAIA,CACP;IACE,CAAA;GAGR,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA;;OAAE,EAAa,EAAO,YAAY;OAC9D;SACP,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD,EAAM,WAAU,eAAgB,CAAA;;OAAE,EAAO,UAAU,UAAU;OACxD;QACH;QACN,kBAAC,OAAD;KAAK,WAAU;eACZ,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;MAAQ,MAAK;MAAK,SAAQ;MAAU,SAnE5B,YAAY;AAC9B,WAAI;AAEF,QADA,MAAM,EAAM,YAAY,EAAO,GAAG,EAClC,EAAO,QAAQ,eAAe,iBAAiB,EAAO,KAAK,GAAG;gBACvD,GAAK;AACZ,UAAO,MAAM,mBAAmB,EAAc,EAAI,CAAC;;;MA8De,UAAU,EAAM;gBAAW;MAE5E,CAAA,EACT,kBAAC,GAAD;MAAQ,MAAK;MAAK,SAAS;gBAAQ;MAE1B,CAAA,CACR,EAAA,CAAA,GACD,EAAO,WAAW,WACpB,kBAAC,GAAD;MAAQ,MAAK;MAAK,SApFT,YAAY;AAC7B,WAAI;AAEF,QADA,MAAM,EAAK,YAAY,EAAO,GAAG,EACjC,EAAO,QAAQ,iBAAiB,2BAA2B,EAAO,KAAK,GAAG;gBACnE,GAAK;AACZ,UAAO,MAAM,kBAAkB,EAAc,EAAI,CAAC;;;MA+EL,UAAU,EAAK;gBAAW;MAExD,CAAA,GAET,kBAAC,QAAD;MAAM,WAAU;gBAA8B,EAAgB,EAAO,OAAO;MAAQ,CAAA;KAElF,CAAA,CACF;;GACI"}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useComradeCircle as e } from "../context/ComradeCircleContext.js";
|
|
2
|
+
import { cn as t } from "../utils/cn.js";
|
|
3
|
+
import "../context/index.js";
|
|
4
|
+
import { useComAddComment as n, useComComments as r } from "../hooks/useComradeCircleApi.js";
|
|
3
5
|
import "../hooks/index.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
6
|
+
import { Avatar as i } from "./Avatar.js";
|
|
7
|
+
import { MemberIdentity as a } from "./MemberIdentity.js";
|
|
8
|
+
import { describeError as o, notify as s } from "../lib/toast.js";
|
|
9
|
+
import { formatRelativeTime as c } from "../utils/formatters.js";
|
|
10
|
+
import { useMemo as l, useState as u } from "react";
|
|
11
|
+
import { CornerDownRight as d, MessageCircle as f, Send as p } from "lucide-react";
|
|
12
|
+
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
13
|
+
import { Button as g, Spinner as _, Textarea as v } from "@burdenoff/fe-libs/ui";
|
|
11
14
|
//#region src/comradecircle/components/CommentThread.tsx
|
|
12
|
-
function
|
|
13
|
-
let { data:
|
|
14
|
-
let e =
|
|
15
|
+
function y({ postId: y }) {
|
|
16
|
+
let { currentUser: b } = e(), { data: x, isLoading: S } = r(y), C = n(), [w, T] = u(""), [E, D] = u(null), { roots: O, repliesByParent: k } = l(() => {
|
|
17
|
+
let e = x ?? [], t = /* @__PURE__ */ new Map(), n = [];
|
|
15
18
|
for (let r of e) if (r.parentId) {
|
|
16
19
|
let e = t.get(r.parentId) ?? [];
|
|
17
20
|
e.push(r), t.set(r.parentId, e);
|
|
@@ -20,86 +23,99 @@ function g({ postId: g }) {
|
|
|
20
23
|
roots: n,
|
|
21
24
|
repliesByParent: t
|
|
22
25
|
};
|
|
23
|
-
}, [
|
|
24
|
-
let e =
|
|
26
|
+
}, [x]), A = async () => {
|
|
27
|
+
let e = w.trim();
|
|
25
28
|
if (e) try {
|
|
26
|
-
await
|
|
27
|
-
postId:
|
|
29
|
+
await C.mutateAsync({
|
|
30
|
+
postId: y,
|
|
28
31
|
body: e,
|
|
29
|
-
parentId:
|
|
30
|
-
}),
|
|
32
|
+
parentId: E?.id ?? null
|
|
33
|
+
}), T(""), D(null);
|
|
31
34
|
} catch (e) {
|
|
32
|
-
|
|
35
|
+
s.error("Could not add comment", o(e));
|
|
33
36
|
}
|
|
34
|
-
},
|
|
35
|
-
className:
|
|
36
|
-
children: [/* @__PURE__ */
|
|
37
|
+
}, j = (e, n = !1) => /* @__PURE__ */ h("div", {
|
|
38
|
+
className: t("flex flex-col gap-1", n && "ml-8"),
|
|
39
|
+
children: [/* @__PURE__ */ h("div", {
|
|
37
40
|
className: "rounded-xl bg-bg-sunken/60 px-3 py-2",
|
|
38
|
-
children: [/* @__PURE__ */
|
|
41
|
+
children: [/* @__PURE__ */ h("div", {
|
|
39
42
|
className: "flex items-center justify-between gap-2",
|
|
40
|
-
children: [/* @__PURE__ */
|
|
41
|
-
memberId:
|
|
43
|
+
children: [/* @__PURE__ */ m(a, {
|
|
44
|
+
memberId: e.authorId,
|
|
42
45
|
size: "xs",
|
|
43
|
-
suffix:
|
|
44
|
-
}), !n && /* @__PURE__ */
|
|
46
|
+
suffix: c(e.createdAt)
|
|
47
|
+
}), !n && /* @__PURE__ */ h("button", {
|
|
45
48
|
type: "button",
|
|
46
|
-
onClick: () =>
|
|
49
|
+
onClick: () => D(e),
|
|
47
50
|
className: "inline-flex items-center gap-1 text-xs text-text-tertiary transition-colors hover:text-text-primary",
|
|
48
|
-
children: [/* @__PURE__ */ d
|
|
51
|
+
children: [/* @__PURE__ */ m(d, { className: "h-3 w-3" }), " Reply"]
|
|
49
52
|
})]
|
|
50
|
-
}), /* @__PURE__ */
|
|
53
|
+
}), /* @__PURE__ */ m("p", {
|
|
51
54
|
className: "mt-1 whitespace-pre-wrap break-words text-sm text-text-primary",
|
|
52
|
-
children:
|
|
55
|
+
children: e.body
|
|
53
56
|
})]
|
|
54
|
-
}), (
|
|
55
|
-
},
|
|
56
|
-
return /* @__PURE__ */
|
|
57
|
+
}), (k.get(e.id) ?? []).map((e) => j(e, !0))]
|
|
58
|
+
}, e.id);
|
|
59
|
+
return /* @__PURE__ */ h("div", {
|
|
57
60
|
className: "mt-3 border-t border-border-seam pt-3",
|
|
58
|
-
children: [
|
|
61
|
+
children: [S ? /* @__PURE__ */ m("div", {
|
|
59
62
|
className: "flex justify-center py-4",
|
|
60
|
-
children: /* @__PURE__ */
|
|
61
|
-
}) :
|
|
62
|
-
className: "
|
|
63
|
-
children: "
|
|
64
|
-
|
|
63
|
+
children: /* @__PURE__ */ m(_, { size: "sm" })
|
|
64
|
+
}) : O.length === 0 ? /* @__PURE__ */ h("div", {
|
|
65
|
+
className: "flex flex-col items-center gap-1.5 py-4 text-center",
|
|
66
|
+
children: [/* @__PURE__ */ m(f, { className: "h-6 w-6 text-text-tertiary opacity-60" }), /* @__PURE__ */ m("p", {
|
|
67
|
+
className: "text-sm text-text-tertiary",
|
|
68
|
+
children: "No comments yet — start the conversation."
|
|
69
|
+
})]
|
|
70
|
+
}) : /* @__PURE__ */ m("div", {
|
|
65
71
|
className: "flex flex-col gap-3",
|
|
66
|
-
children:
|
|
67
|
-
}), /* @__PURE__ */
|
|
72
|
+
children: O.map((e) => j(e))
|
|
73
|
+
}), /* @__PURE__ */ h("div", {
|
|
68
74
|
className: "mt-3 flex flex-col gap-2",
|
|
69
|
-
children: [
|
|
75
|
+
children: [E && /* @__PURE__ */ h("div", {
|
|
70
76
|
className: "flex items-center justify-between rounded-lg bg-bg-sunken px-3 py-1.5 text-xs text-text-secondary",
|
|
71
|
-
children: [/* @__PURE__ */
|
|
77
|
+
children: [/* @__PURE__ */ m("span", {
|
|
72
78
|
className: "truncate",
|
|
73
79
|
children: "Replying to a comment"
|
|
74
|
-
}), /* @__PURE__ */
|
|
80
|
+
}), /* @__PURE__ */ m("button", {
|
|
75
81
|
type: "button",
|
|
76
|
-
onClick: () =>
|
|
82
|
+
onClick: () => D(null),
|
|
77
83
|
className: "text-text-tertiary hover:text-text-primary",
|
|
78
84
|
children: "Cancel"
|
|
79
85
|
})]
|
|
80
|
-
}), /* @__PURE__ */
|
|
86
|
+
}), /* @__PURE__ */ h("div", {
|
|
81
87
|
className: "flex items-end gap-2",
|
|
82
|
-
children: [
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
children: [
|
|
89
|
+
/* @__PURE__ */ m(i, {
|
|
90
|
+
name: b?.name ?? b?.email ?? "You",
|
|
91
|
+
seed: b?.id,
|
|
92
|
+
size: "sm",
|
|
93
|
+
className: "mb-1"
|
|
94
|
+
}),
|
|
95
|
+
/* @__PURE__ */ m(v, {
|
|
96
|
+
value: w,
|
|
97
|
+
onChange: (e) => T(e.target.value),
|
|
98
|
+
placeholder: "Add a comment…",
|
|
99
|
+
rows: 1,
|
|
100
|
+
className: "min-h-[40px] resize-none",
|
|
101
|
+
onKeyDown: (e) => {
|
|
102
|
+
e.key === "Enter" && (e.metaKey || e.ctrlKey) && (e.preventDefault(), A());
|
|
103
|
+
}
|
|
104
|
+
}),
|
|
105
|
+
/* @__PURE__ */ m(g, {
|
|
106
|
+
size: "icon",
|
|
107
|
+
onClick: A,
|
|
108
|
+
disabled: !w.trim() || C.isPending,
|
|
109
|
+
"aria-label": "Post comment",
|
|
110
|
+
className: "shrink-0 rounded-full",
|
|
111
|
+
children: /* @__PURE__ */ m(p, { className: "h-4 w-4" })
|
|
112
|
+
})
|
|
113
|
+
]
|
|
98
114
|
})]
|
|
99
115
|
})]
|
|
100
116
|
});
|
|
101
117
|
}
|
|
102
118
|
//#endregion
|
|
103
|
-
export {
|
|
119
|
+
export { y as CommentThread };
|
|
104
120
|
|
|
105
121
|
//# sourceMappingURL=CommentThread.js.map
|