@burdenoff/microfe-comradecircle 2026.704.1 → 2026.704.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/comradecircle/components/Avatar.js +27 -15
- package/dist/comradecircle/components/Avatar.js.map +1 -1
- package/dist/comradecircle/components/AvatarUploadField.js +80 -0
- package/dist/comradecircle/components/AvatarUploadField.js.map +1 -0
- package/dist/comradecircle/components/CircleCard.js +2 -1
- package/dist/comradecircle/components/CircleCard.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/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 +2 -1
- package/dist/comradecircle/components/MemberIdentity.js.map +1 -1
- package/dist/comradecircle/components/PostCard.js +7 -14
- 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/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/circles/CircleDetailPage.js +2 -1
- package/dist/comradecircle/pages/circles/CircleDetailPage.js.map +1 -1
- package/dist/comradecircle/pages/members/MemberProfilePage.js +2 -1
- package/dist/comradecircle/pages/members/MemberProfilePage.js.map +1 -1
- package/dist/comradecircle/pages/members/MembersPage.js +2 -1
- package/dist/comradecircle/pages/members/MembersPage.js.map +1 -1
- package/dist/comradecircle/pages/profile/MyProfilePage.js +100 -89
- package/dist/comradecircle/pages/profile/MyProfilePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,35 +1,47 @@
|
|
|
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
10
|
xl: "h-20 w-20 text-2xl"
|
|
10
|
-
},
|
|
11
|
+
}, a = {
|
|
11
12
|
ONLINE: "bg-status-success-text",
|
|
12
13
|
AWAY: "bg-status-warning-text",
|
|
13
14
|
BUSY: "bg-status-error-text",
|
|
14
15
|
OFFLINE: "bg-text-tertiary"
|
|
15
16
|
};
|
|
16
|
-
function
|
|
17
|
+
function o(e) {
|
|
17
18
|
if (!e) return "?";
|
|
18
19
|
let t = e.trim().split(/\s+/).filter(Boolean);
|
|
19
20
|
return t.length === 0 ? "?" : t.length === 1 ? t[0].slice(0, 2).toUpperCase() : (t[0][0] + t[t.length - 1][0]).toUpperCase();
|
|
20
21
|
}
|
|
21
|
-
var
|
|
22
|
-
|
|
23
|
-
children: [/* @__PURE__ */ t("span", {
|
|
22
|
+
var s = ({ name: s, size: c = "md", presence: l, fileId: u, className: d }) => {
|
|
23
|
+
let f = /* @__PURE__ */ n("span", {
|
|
24
24
|
"aria-hidden": "true",
|
|
25
|
-
className: e("
|
|
26
|
-
children:
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
className: e("flex h-full w-full items-center justify-center rounded-full bg-[var(--color-accent-soft)] font-semibold text-text-primary", i[c]),
|
|
26
|
+
children: o(s)
|
|
27
|
+
});
|
|
28
|
+
return /* @__PURE__ */ r("span", {
|
|
29
|
+
className: e("relative inline-flex shrink-0", d),
|
|
30
|
+
children: [/* @__PURE__ */ n("span", {
|
|
31
|
+
className: e("inline-flex items-center justify-center overflow-hidden rounded-full ring-1 ring-border-seam", i[c]),
|
|
32
|
+
children: u ? /* @__PURE__ */ n(t, {
|
|
33
|
+
fileId: u,
|
|
34
|
+
alt: s ?? "Avatar",
|
|
35
|
+
className: "h-full w-full rounded-full",
|
|
36
|
+
fallback: f
|
|
37
|
+
}) : f
|
|
38
|
+
}), l && /* @__PURE__ */ n("span", {
|
|
39
|
+
className: e("absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full ring-2 ring-bg-surface", a[l], l === "ONLINE" && "motion-safe:animate-vc-glow-pulse"),
|
|
40
|
+
"aria-label": `Presence: ${l.toLowerCase()}`
|
|
41
|
+
})]
|
|
42
|
+
});
|
|
43
|
+
};
|
|
32
44
|
//#endregion
|
|
33
|
-
export {
|
|
45
|
+
export { s as Avatar };
|
|
34
46
|
|
|
35
47
|
//# sourceMappingURL=Avatar.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Avatar.js","names":[],"sources":["../../../src/comradecircle/components/Avatar.tsx"],"sourcesContent":["/**\n * Avatar — initials-based, token-only avatar with an optional presence dot.\n * @module comradecircle/components\n *\n * File-backed avatars are not rendered here: in this subgraph `File` resolves\n * `id` only (no URL), so we render deterministic initials on the brand\n * accent-soft tint. Clean, theme-aware, and never a broken image.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport type { ComPresence } from '../types';\n\ntype AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\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};\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\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 className?: string;\n}\n\nexport const Avatar: FC<AvatarProps> = ({ name, size = 'md', presence, className }) => {\n
|
|
1
|
+
{"version":3,"file":"Avatar.js","names":[],"sources":["../../../src/comradecircle/components/Avatar.tsx"],"sourcesContent":["/**\n * Avatar — initials-based, token-only avatar with an optional presence dot.\n * @module comradecircle/components\n *\n * File-backed avatars are not rendered here: in this subgraph `File` resolves\n * `id` only (no URL), so we render deterministic initials on the brand\n * accent-soft tint. Clean, theme-aware, and never a broken image.\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';\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};\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\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 className?: string;\n}\n\nexport const Avatar: FC<AvatarProps> = ({ name, size = 'md', presence, fileId, className }) => {\n const initials = (\n <span\n aria-hidden=\"true\"\n className={cn(\n 'flex h-full w-full items-center justify-center rounded-full bg-[var(--color-accent-soft)] font-semibold text-text-primary',\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 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 h-2.5 w-2.5 rounded-full ring-2 ring-bg-surface',\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":";;;;AAgBA,IAAM,IAAwC;CAC5C,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACL,EAEK,IAA2C;CAC/C,QAAQ;CACR,MAAM;CACN,MAAM;CACN,SAAS;CACV;AAED,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;;AAYrE,IAAa,KAA2B,EAAE,SAAM,UAAO,MAAM,aAAU,WAAQ,mBAAgB;CAC7F,IAAM,IACJ,kBAAC,QAAD;EACE,eAAY;EACZ,WAAW,EACT,6HACA,EAAU,GACX;YAEA,EAAW,EAAK;EACZ,CAAA;AAGT,QACE,kBAAC,QAAD;EAAM,WAAW,EAAG,iCAAiC,EAAU;YAA/D,CACE,kBAAC,QAAD;GACE,WAAW,EACT,gGACA,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,mFACA,EAAY,IACZ,MAAa,YAAY,oCAC1B;GACD,cAAY,aAAa,EAAS,aAAa;GAC/C,CAAA,CAEC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
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("div", {
|
|
15
|
+
className: "relative",
|
|
16
|
+
children: [C ? /* @__PURE__ */ d("img", {
|
|
17
|
+
src: C,
|
|
18
|
+
alt: h ?? "Avatar",
|
|
19
|
+
className: "h-16 w-16 rounded-full object-cover ring-1 ring-border-seam"
|
|
20
|
+
}) : /* @__PURE__ */ d(r, {
|
|
21
|
+
name: h,
|
|
22
|
+
fileId: m,
|
|
23
|
+
size: "lg"
|
|
24
|
+
}), x && /* @__PURE__ */ d("span", {
|
|
25
|
+
className: "absolute inset-0 flex items-center justify-center rounded-full bg-bg-canvas/60",
|
|
26
|
+
children: /* @__PURE__ */ d(u, { className: "h-5 w-5 animate-spin text-text-primary" })
|
|
27
|
+
})]
|
|
28
|
+
}), /* @__PURE__ */ f("div", { children: [
|
|
29
|
+
/* @__PURE__ */ d("input", {
|
|
30
|
+
ref: y,
|
|
31
|
+
id: b,
|
|
32
|
+
type: "file",
|
|
33
|
+
accept: "image/*",
|
|
34
|
+
className: "sr-only",
|
|
35
|
+
onChange: async (e) => {
|
|
36
|
+
let r = e.target.files?.[0];
|
|
37
|
+
if (e.target.value = "", r) {
|
|
38
|
+
if (!r.type.startsWith("image/")) {
|
|
39
|
+
a.error("Unsupported file", "Please choose an image.");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (r.size > p) {
|
|
43
|
+
a.error("Image too large", "Please choose an image under 5 MB.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
S(!0);
|
|
47
|
+
try {
|
|
48
|
+
let e = await n(r, t.avatars);
|
|
49
|
+
w(e.url), g(e.id), a.success("Avatar updated");
|
|
50
|
+
} catch (e) {
|
|
51
|
+
a.error("Upload failed", i(e));
|
|
52
|
+
} finally {
|
|
53
|
+
S(!1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
disabled: v || x
|
|
58
|
+
}),
|
|
59
|
+
/* @__PURE__ */ f("button", {
|
|
60
|
+
type: "button",
|
|
61
|
+
onClick: () => y.current?.click(),
|
|
62
|
+
disabled: v || x,
|
|
63
|
+
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"),
|
|
64
|
+
children: [
|
|
65
|
+
/* @__PURE__ */ d(l, { className: "h-4 w-4" }),
|
|
66
|
+
" ",
|
|
67
|
+
_
|
|
68
|
+
]
|
|
69
|
+
}),
|
|
70
|
+
/* @__PURE__ */ d("p", {
|
|
71
|
+
className: "mt-1 text-xs text-text-tertiary",
|
|
72
|
+
children: "PNG or JPG, up to 5 MB."
|
|
73
|
+
})
|
|
74
|
+
] })]
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { m as AvatarUploadField };
|
|
79
|
+
|
|
80
|
+
//# 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 <div className=\"relative\">\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 {uploading && (\n <span className=\"absolute inset-0 flex items-center justify-center rounded-full bg-bg-canvas/60\">\n <Loader2 className=\"h-5 w-5 animate-spin text-text-primary\" />\n </span>\n )}\n </div>\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,OAAD;GAAK,WAAU;aAAf,CACG,IACC,kBAAC,OAAD;IACE,KAAK;IACL,KAAK,KAAQ;IACb,WAAU;IACV,CAAA,GAEF,kBAAC,GAAD;IAAc;IAAc;IAAQ,MAAK;IAAO,CAAA,EAEjD,KACC,kBAAC,QAAD;IAAM,WAAU;cACd,kBAAC,GAAD,EAAS,WAAU,0CAA2C,CAAA;IACzD,CAAA,CAEL;MACN,kBAAC,OAAD,EAAA,UAAA;GACE,kBAAC,SAAD;IACE,KAAK;IACL,IAAI;IACJ,MAAK;IACL,QAAO;IACP,WAAU;IACV,UAlDa,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;;;;IA8Bf,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"}
|
|
@@ -18,7 +18,8 @@ function h({ circle: h, membership: g, onOpen: _ }) {
|
|
|
18
18
|
className: "flex items-start gap-3",
|
|
19
19
|
children: [/* @__PURE__ */ d(n, {
|
|
20
20
|
name: h.name,
|
|
21
|
-
size: "lg"
|
|
21
|
+
size: "lg",
|
|
22
|
+
fileId: h.avatar?.id
|
|
22
23
|
}), /* @__PURE__ */ f("div", {
|
|
23
24
|
className: "min-w-0 flex-1",
|
|
24
25
|
children: [
|
|
@@ -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 treatment=\"plain\" className=\"flex flex-col p-5\">\n <div className=\"flex items-start gap-3\">\n <Avatar name={circle.name} size=\"lg\" />\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":";;;;;;;;;;AAwBA,SAAgB,EAAW,EAAE,WAAQ,eAAY,aAA2B;CAC1E,IAAM,IAAO,GAAkB,EACzB,IAAQ,GAAmB,EAC3B,IAAW,GAAY,WAAW;AAoBxC,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAQ,MAAM,EAAO;KAAM,MAAK;
|
|
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 treatment=\"plain\" className=\"flex flex-col p-5\">\n <div className=\"flex items-start gap-3\">\n <Avatar name={circle.name} 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":";;;;;;;;;;AAwBA,SAAgB,EAAW,EAAE,WAAQ,eAAY,aAA2B;CAC1E,IAAM,IAAO,GAAkB,EACzB,IAAQ,GAAmB,EAC3B,IAAW,GAAY,WAAW;AAoBxC,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAQ,MAAM,EAAO;KAAM,MAAK;KAAK,QAAQ,EAAO,QAAQ;KAAM,CAAA,EAClE,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,SA3D5B,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;;;MAsDe,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,SA5ET,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;;;MAuEL,UAAU,EAAK;gBAAW;MAExD,CAAA,GAET,kBAAC,QAAD;MAAM,WAAU;gBAA8B,EAAgB,EAAO,OAAO;MAAQ,CAAA;KAElF,CAAA,CACF;;GACI"}
|
|
@@ -4,138 +4,149 @@ import { describeError as t, notify as n } from "../lib/toast.js";
|
|
|
4
4
|
import { formatEnumLabel as r } from "../utils/formatters.js";
|
|
5
5
|
import { slugify as i } from "../utils/slug.js";
|
|
6
6
|
import { TagInput as a } from "./TagInput.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { AvatarUploadField as o } from "./AvatarUploadField.js";
|
|
8
|
+
import { useState as s } from "react";
|
|
9
|
+
import { jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
10
|
+
import { Button as u, Input as d, Label as f, ResponsiveDialog as p, ResponsiveDialogContent as m, ResponsiveDialogDescription as h, ResponsiveDialogFooter as g, ResponsiveDialogHeader as _, ResponsiveDialogTitle as v, Select as y, SelectContent as b, SelectItem as x, SelectTrigger as S, SelectValue as C, Textarea as w } from "@burdenoff/fe-libs/ui";
|
|
10
11
|
//#region src/comradecircle/components/CreateCircleDialog.tsx
|
|
11
|
-
var
|
|
12
|
+
var T = [
|
|
12
13
|
"PUBLIC",
|
|
13
14
|
"PRIVATE",
|
|
14
15
|
"SECRET"
|
|
15
16
|
];
|
|
16
|
-
function
|
|
17
|
-
let
|
|
18
|
-
|
|
17
|
+
function E({ open: E, onOpenChange: D, onCreated: O }) {
|
|
18
|
+
let k = e(), [A, j] = s(""), [M, N] = s(""), [P, F] = s(!1), [I, L] = s(""), [R, z] = s(""), [B, V] = s("PUBLIC"), [H, U] = s([]), [W, G] = s(null), [K, q] = s(null), J = P ? M : i(A), Y = () => {
|
|
19
|
+
j(""), N(""), F(!1), L(""), z(""), V("PUBLIC"), U([]), G(null), q(null);
|
|
19
20
|
};
|
|
20
|
-
return /* @__PURE__ */
|
|
21
|
-
open:
|
|
22
|
-
onOpenChange:
|
|
23
|
-
children: /* @__PURE__ */
|
|
21
|
+
return /* @__PURE__ */ c(p, {
|
|
22
|
+
open: E,
|
|
23
|
+
onOpenChange: D,
|
|
24
|
+
children: /* @__PURE__ */ l(m, {
|
|
24
25
|
className: "sm:max-w-lg",
|
|
25
26
|
children: [
|
|
26
|
-
/* @__PURE__ */
|
|
27
|
-
/* @__PURE__ */
|
|
27
|
+
/* @__PURE__ */ l(_, { children: [/* @__PURE__ */ c(v, { children: "Create a circle" }), /* @__PURE__ */ c(h, { children: "A circle is a community space with its own channels, members, events, and polls." })] }),
|
|
28
|
+
/* @__PURE__ */ l("div", {
|
|
28
29
|
className: "flex flex-col gap-4 py-2",
|
|
29
30
|
children: [
|
|
30
|
-
/* @__PURE__ */
|
|
31
|
+
/* @__PURE__ */ l("div", {
|
|
31
32
|
className: "grid gap-1.5",
|
|
32
|
-
children: [/* @__PURE__ */
|
|
33
|
+
children: [/* @__PURE__ */ c(f, { children: "Avatar" }), /* @__PURE__ */ c(o, {
|
|
34
|
+
fileId: W,
|
|
35
|
+
name: A || "Circle",
|
|
36
|
+
onUploaded: G,
|
|
37
|
+
label: "Upload circle avatar"
|
|
38
|
+
})]
|
|
39
|
+
}),
|
|
40
|
+
/* @__PURE__ */ l("div", {
|
|
41
|
+
className: "grid gap-1.5",
|
|
42
|
+
children: [/* @__PURE__ */ c(f, {
|
|
33
43
|
htmlFor: "circle-name",
|
|
34
44
|
children: "Name"
|
|
35
|
-
}), /* @__PURE__ */
|
|
45
|
+
}), /* @__PURE__ */ c(d, {
|
|
36
46
|
id: "circle-name",
|
|
37
|
-
value:
|
|
38
|
-
onChange: (e) =>
|
|
47
|
+
value: A,
|
|
48
|
+
onChange: (e) => j(e.target.value),
|
|
39
49
|
placeholder: "e.g. Product Guild"
|
|
40
50
|
})]
|
|
41
51
|
}),
|
|
42
|
-
/* @__PURE__ */
|
|
52
|
+
/* @__PURE__ */ l("div", {
|
|
43
53
|
className: "grid gap-1.5",
|
|
44
|
-
children: [/* @__PURE__ */
|
|
54
|
+
children: [/* @__PURE__ */ c(f, {
|
|
45
55
|
htmlFor: "circle-slug",
|
|
46
56
|
children: "URL slug"
|
|
47
|
-
}), /* @__PURE__ */
|
|
57
|
+
}), /* @__PURE__ */ c(d, {
|
|
48
58
|
id: "circle-slug",
|
|
49
|
-
value:
|
|
59
|
+
value: J,
|
|
50
60
|
onChange: (e) => {
|
|
51
|
-
|
|
61
|
+
F(!0), N(i(e.target.value));
|
|
52
62
|
},
|
|
53
63
|
placeholder: "product-guild"
|
|
54
64
|
})]
|
|
55
65
|
}),
|
|
56
|
-
/* @__PURE__ */
|
|
66
|
+
/* @__PURE__ */ l("div", {
|
|
57
67
|
className: "grid gap-1.5",
|
|
58
|
-
children: [/* @__PURE__ */
|
|
68
|
+
children: [/* @__PURE__ */ c(f, {
|
|
59
69
|
htmlFor: "circle-topic",
|
|
60
70
|
children: "Topic"
|
|
61
|
-
}), /* @__PURE__ */
|
|
71
|
+
}), /* @__PURE__ */ c(d, {
|
|
62
72
|
id: "circle-topic",
|
|
63
|
-
value:
|
|
64
|
-
onChange: (e) =>
|
|
73
|
+
value: I,
|
|
74
|
+
onChange: (e) => L(e.target.value),
|
|
65
75
|
placeholder: "A short one-line topic"
|
|
66
76
|
})]
|
|
67
77
|
}),
|
|
68
|
-
/* @__PURE__ */
|
|
78
|
+
/* @__PURE__ */ l("div", {
|
|
69
79
|
className: "grid gap-1.5",
|
|
70
|
-
children: [/* @__PURE__ */
|
|
80
|
+
children: [/* @__PURE__ */ c(f, {
|
|
71
81
|
htmlFor: "circle-desc",
|
|
72
82
|
children: "Description"
|
|
73
|
-
}), /* @__PURE__ */
|
|
83
|
+
}), /* @__PURE__ */ c(w, {
|
|
74
84
|
id: "circle-desc",
|
|
75
|
-
value:
|
|
76
|
-
onChange: (e) =>
|
|
85
|
+
value: R,
|
|
86
|
+
onChange: (e) => z(e.target.value),
|
|
77
87
|
placeholder: "What is this circle about?",
|
|
78
88
|
rows: 3
|
|
79
89
|
})]
|
|
80
90
|
}),
|
|
81
|
-
/* @__PURE__ */
|
|
91
|
+
/* @__PURE__ */ l("div", {
|
|
82
92
|
className: "grid gap-1.5",
|
|
83
|
-
children: [/* @__PURE__ */
|
|
84
|
-
value:
|
|
85
|
-
onValueChange: (e) =>
|
|
86
|
-
children: [/* @__PURE__ */
|
|
93
|
+
children: [/* @__PURE__ */ c(f, { children: "Visibility" }), /* @__PURE__ */ l(y, {
|
|
94
|
+
value: B,
|
|
95
|
+
onValueChange: (e) => V(e),
|
|
96
|
+
children: [/* @__PURE__ */ c(S, { children: /* @__PURE__ */ c(C, {}) }), /* @__PURE__ */ c(b, { children: T.map((e) => /* @__PURE__ */ c(x, {
|
|
87
97
|
value: e,
|
|
88
98
|
children: r(e)
|
|
89
99
|
}, e)) })]
|
|
90
100
|
})]
|
|
91
101
|
}),
|
|
92
|
-
/* @__PURE__ */
|
|
102
|
+
/* @__PURE__ */ l("div", {
|
|
93
103
|
className: "grid gap-1.5",
|
|
94
|
-
children: [/* @__PURE__ */
|
|
104
|
+
children: [/* @__PURE__ */ c(f, {
|
|
95
105
|
htmlFor: "circle-tags",
|
|
96
106
|
children: "Tags"
|
|
97
|
-
}), /* @__PURE__ */
|
|
107
|
+
}), /* @__PURE__ */ c(a, {
|
|
98
108
|
id: "circle-tags",
|
|
99
|
-
value:
|
|
100
|
-
onChange:
|
|
109
|
+
value: H,
|
|
110
|
+
onChange: U,
|
|
101
111
|
placeholder: "Add tags…"
|
|
102
112
|
})]
|
|
103
113
|
}),
|
|
104
|
-
|
|
114
|
+
K && /* @__PURE__ */ c("p", {
|
|
105
115
|
className: "text-sm text-status-error-text",
|
|
106
|
-
children:
|
|
116
|
+
children: K
|
|
107
117
|
})
|
|
108
118
|
]
|
|
109
119
|
}),
|
|
110
|
-
/* @__PURE__ */
|
|
120
|
+
/* @__PURE__ */ l(g, { children: [/* @__PURE__ */ c(u, {
|
|
111
121
|
variant: "outline",
|
|
112
|
-
onClick: () =>
|
|
122
|
+
onClick: () => D(!1),
|
|
113
123
|
children: "Cancel"
|
|
114
|
-
}), /* @__PURE__ */
|
|
124
|
+
}), /* @__PURE__ */ c(u, {
|
|
115
125
|
onClick: async () => {
|
|
116
|
-
if (!
|
|
117
|
-
|
|
126
|
+
if (!A.trim()) {
|
|
127
|
+
q("Circle name is required.");
|
|
118
128
|
return;
|
|
119
129
|
}
|
|
120
|
-
if (!
|
|
121
|
-
|
|
130
|
+
if (!J) {
|
|
131
|
+
q("A URL slug is required.");
|
|
122
132
|
return;
|
|
123
133
|
}
|
|
124
134
|
try {
|
|
125
|
-
let e = await
|
|
126
|
-
name:
|
|
127
|
-
slug:
|
|
128
|
-
topic:
|
|
129
|
-
description:
|
|
130
|
-
visibility:
|
|
131
|
-
|
|
135
|
+
let e = await k.mutateAsync({
|
|
136
|
+
name: A.trim(),
|
|
137
|
+
slug: J,
|
|
138
|
+
topic: I.trim() || null,
|
|
139
|
+
description: R.trim() || null,
|
|
140
|
+
visibility: B,
|
|
141
|
+
avatarFileId: W,
|
|
142
|
+
tags: H.length ? H : null
|
|
132
143
|
});
|
|
133
|
-
n.success("Circle created", `${e.name} is ready.`),
|
|
144
|
+
n.success("Circle created", `${e.name} is ready.`), D(!1), Y(), O?.(e);
|
|
134
145
|
} catch (e) {
|
|
135
|
-
|
|
146
|
+
q(t(e));
|
|
136
147
|
}
|
|
137
148
|
},
|
|
138
|
-
disabled:
|
|
149
|
+
disabled: k.isPending,
|
|
139
150
|
children: "Create circle"
|
|
140
151
|
})] })
|
|
141
152
|
]
|
|
@@ -143,6 +154,6 @@ function T({ open: T, onOpenChange: E, onCreated: D }) {
|
|
|
143
154
|
});
|
|
144
155
|
}
|
|
145
156
|
//#endregion
|
|
146
|
-
export {
|
|
157
|
+
export { E as CreateCircleDialog };
|
|
147
158
|
|
|
148
159
|
//# sourceMappingURL=CreateCircleDialog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreateCircleDialog.js","names":[],"sources":["../../../src/comradecircle/components/CreateCircleDialog.tsx"],"sourcesContent":["/**\n * CreateCircleDialog — create a new community circle.\n * @module comradecircle/components\n *\n * Writes `comCreateCircle`. Slug auto-derives from the name (editable).\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { TagInput } from './TagInput';\nimport { useComCreateCircle } from '../hooks';\nimport { slugify } from '../utils/slug';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComCircle, ComCircleVisibility } from '../types';\n\nconst VISIBILITIES: ComCircleVisibility[] = ['PUBLIC', 'PRIVATE', 'SECRET'];\n\ninterface CreateCircleDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onCreated?: (circle: ComCircle) => void;\n}\n\nexport function CreateCircleDialog({ open, onOpenChange, onCreated }: CreateCircleDialogProps) {\n const createCircle = useComCreateCircle();\n const [name, setName] = useState('');\n const [slug, setSlug] = useState('');\n const [slugTouched, setSlugTouched] = useState(false);\n const [topic, setTopic] = useState('');\n const [description, setDescription] = useState('');\n const [visibility, setVisibility] = useState<ComCircleVisibility>('PUBLIC');\n const [tags, setTags] = useState<string[]>([]);\n const [error, setError] = useState<string | null>(null);\n\n const effectiveSlug = slugTouched ? slug : slugify(name);\n\n const reset = () => {\n setName('');\n setSlug('');\n setSlugTouched(false);\n setTopic('');\n setDescription('');\n setVisibility('PUBLIC');\n setTags([]);\n setError(null);\n };\n\n const submit = async () => {\n if (!name.trim()) {\n setError('Circle name is required.');\n return;\n }\n if (!effectiveSlug) {\n setError('A URL slug is required.');\n return;\n }\n try {\n const created = await createCircle.mutateAsync({\n name: name.trim(),\n slug: effectiveSlug,\n topic: topic.trim() || null,\n description: description.trim() || null,\n visibility,\n tags: tags.length ? tags : null,\n });\n notify.success('Circle created', `${created.name} is ready.`);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Create a circle</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n A circle is a community space with its own channels, members, events, and polls.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex flex-col gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-name\">Name</Label>\n <Input\n id=\"circle-name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"e.g. Product Guild\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-slug\">URL slug</Label>\n <Input\n id=\"circle-slug\"\n value={effectiveSlug}\n onChange={(e) => {\n setSlugTouched(true);\n setSlug(slugify(e.target.value));\n }}\n placeholder=\"product-guild\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-topic\">Topic</Label>\n <Input\n id=\"circle-topic\"\n value={topic}\n onChange={(e) => setTopic(e.target.value)}\n placeholder=\"A short one-line topic\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-desc\">Description</Label>\n <Textarea\n id=\"circle-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n placeholder=\"What is this circle about?\"\n rows={3}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Visibility</Label>\n <Select\n value={visibility}\n onValueChange={(v) => setVisibility(v as ComCircleVisibility)}\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {VISIBILITIES.map((v) => (\n <SelectItem key={v} value={v}>\n {formatEnumLabel(v)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-tags\">Tags</Label>\n <TagInput id=\"circle-tags\" value={tags} onChange={setTags} placeholder=\"Add tags…\" />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={createCircle.isPending}>\n Create circle\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"CreateCircleDialog.js","names":[],"sources":["../../../src/comradecircle/components/CreateCircleDialog.tsx"],"sourcesContent":["/**\n * CreateCircleDialog — create a new community circle.\n * @module comradecircle/components\n *\n * Writes `comCreateCircle`. Slug auto-derives from the name (editable).\n */\n\nimport { useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n Textarea,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { TagInput } from './TagInput';\nimport { AvatarUploadField } from './AvatarUploadField';\nimport { useComCreateCircle } from '../hooks';\nimport { slugify } from '../utils/slug';\nimport { formatEnumLabel } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComCircle, ComCircleVisibility } from '../types';\n\nconst VISIBILITIES: ComCircleVisibility[] = ['PUBLIC', 'PRIVATE', 'SECRET'];\n\ninterface CreateCircleDialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onCreated?: (circle: ComCircle) => void;\n}\n\nexport function CreateCircleDialog({ open, onOpenChange, onCreated }: CreateCircleDialogProps) {\n const createCircle = useComCreateCircle();\n const [name, setName] = useState('');\n const [slug, setSlug] = useState('');\n const [slugTouched, setSlugTouched] = useState(false);\n const [topic, setTopic] = useState('');\n const [description, setDescription] = useState('');\n const [visibility, setVisibility] = useState<ComCircleVisibility>('PUBLIC');\n const [tags, setTags] = useState<string[]>([]);\n const [avatarFileId, setAvatarFileId] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const effectiveSlug = slugTouched ? slug : slugify(name);\n\n const reset = () => {\n setName('');\n setSlug('');\n setSlugTouched(false);\n setTopic('');\n setDescription('');\n setVisibility('PUBLIC');\n setTags([]);\n setAvatarFileId(null);\n setError(null);\n };\n\n const submit = async () => {\n if (!name.trim()) {\n setError('Circle name is required.');\n return;\n }\n if (!effectiveSlug) {\n setError('A URL slug is required.');\n return;\n }\n try {\n const created = await createCircle.mutateAsync({\n name: name.trim(),\n slug: effectiveSlug,\n topic: topic.trim() || null,\n description: description.trim() || null,\n visibility,\n avatarFileId,\n tags: tags.length ? tags : null,\n });\n notify.success('Circle created', `${created.name} is ready.`);\n onOpenChange(false);\n reset();\n onCreated?.(created);\n } catch (err) {\n setError(describeError(err));\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"sm:max-w-lg\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Create a circle</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n A circle is a community space with its own channels, members, events, and polls.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"flex flex-col gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label>Avatar</Label>\n <AvatarUploadField\n fileId={avatarFileId}\n name={name || 'Circle'}\n onUploaded={setAvatarFileId}\n label=\"Upload circle avatar\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-name\">Name</Label>\n <Input\n id=\"circle-name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"e.g. Product Guild\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-slug\">URL slug</Label>\n <Input\n id=\"circle-slug\"\n value={effectiveSlug}\n onChange={(e) => {\n setSlugTouched(true);\n setSlug(slugify(e.target.value));\n }}\n placeholder=\"product-guild\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-topic\">Topic</Label>\n <Input\n id=\"circle-topic\"\n value={topic}\n onChange={(e) => setTopic(e.target.value)}\n placeholder=\"A short one-line topic\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-desc\">Description</Label>\n <Textarea\n id=\"circle-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n placeholder=\"What is this circle about?\"\n rows={3}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label>Visibility</Label>\n <Select\n value={visibility}\n onValueChange={(v) => setVisibility(v as ComCircleVisibility)}\n >\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {VISIBILITIES.map((v) => (\n <SelectItem key={v} value={v}>\n {formatEnumLabel(v)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"circle-tags\">Tags</Label>\n <TagInput id=\"circle-tags\" value={tags} onChange={setTags} placeholder=\"Add tags…\" />\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button onClick={submit} disabled={createCircle.isPending}>\n Create circle\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { useFileUrl as t } from "../hooks/useFiles.js";
|
|
3
|
+
import { useState as n } from "react";
|
|
4
|
+
import { Fragment as r, jsx as i } from "react/jsx-runtime";
|
|
5
|
+
import { Skeleton as a } from "@burdenoff/fe-libs/ui";
|
|
6
|
+
//#region src/comradecircle/components/FileImage.tsx
|
|
7
|
+
function o({ fileId: o, alt: s, className: c, fallback: l = null }) {
|
|
8
|
+
let { data: u, isLoading: d, isError: f } = t(o), [p, m] = n(!1);
|
|
9
|
+
return o ? d ? /* @__PURE__ */ i(a, { className: e("h-full w-full", c) }) : f || p || !u ? /* @__PURE__ */ i(r, { children: l }) : /* @__PURE__ */ i("img", {
|
|
10
|
+
src: u,
|
|
11
|
+
alt: s,
|
|
12
|
+
loading: "lazy",
|
|
13
|
+
onError: () => m(!0),
|
|
14
|
+
className: e("object-cover", c)
|
|
15
|
+
}) : /* @__PURE__ */ i(r, { children: l });
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { o as FileImage };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=FileImage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileImage.js","names":[],"sources":["../../../src/comradecircle/components/FileImage.tsx"],"sourcesContent":["/**\n * FileImage — render a wspace-files-svc file id as an image.\n * @module comradecircle/components\n *\n * Resolves the id to a presigned URL via {@link useFileUrl}. Shows a skeleton\n * while resolving and renders nothing (so the caller's fallback shows) if the id\n * is absent or the URL fails to resolve.\n */\n\nimport { useState } from 'react';\nimport { Skeleton } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { useFileUrl } from '../hooks/useFiles';\n\ninterface FileImageProps {\n fileId?: string | null;\n alt: string;\n className?: string;\n /** Rendered when there is no fileId, resolution fails, or the image errors. */\n fallback?: React.ReactNode;\n}\n\nexport function FileImage({ fileId, alt, className, fallback = null }: FileImageProps) {\n const { data: url, isLoading, isError } = useFileUrl(fileId);\n const [imgError, setImgError] = useState(false);\n\n if (!fileId) return <>{fallback}</>;\n if (isLoading) return <Skeleton className={cn('h-full w-full', className)} />;\n if (isError || imgError || !url) return <>{fallback}</>;\n\n return (\n <img\n src={url}\n alt={alt}\n loading=\"lazy\"\n onError={() => setImgError(true)}\n className={cn('object-cover', className)}\n />\n );\n}\n"],"mappings":";;;;;;AAsBA,SAAgB,EAAU,EAAE,WAAQ,QAAK,cAAW,cAAW,QAAwB;CACrF,IAAM,EAAE,MAAM,GAAK,cAAW,eAAY,EAAW,EAAO,EACtD,CAAC,GAAU,KAAe,EAAS,GAAM;AAM/C,QAJK,IACD,IAAkB,kBAAC,GAAD,EAAU,WAAW,EAAG,iBAAiB,EAAU,EAAI,CAAA,GACzE,KAAW,KAAY,CAAC,IAAY,kBAAA,GAAA,EAAA,UAAG,GAAY,CAAA,GAGrD,kBAAC,OAAD;EACE,KAAK;EACA;EACL,SAAQ;EACR,eAAe,EAAY,GAAK;EAChC,WAAW,EAAG,gBAAgB,EAAU;EACxC,CAAA,GAXgB,kBAAA,GAAA,EAAA,UAAG,GAAY,CAAA"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { FILE_FOLDERS as t, uploadImageToFiles as n } from "../hooks/useFiles.js";
|
|
3
|
+
import { FileImage as r } from "./FileImage.js";
|
|
4
|
+
import { describeError as i, notify as a } from "../lib/toast.js";
|
|
5
|
+
import { useRef as o, useState as s } from "react";
|
|
6
|
+
import { ImagePlus as c, Loader2 as l, X as u } from "lucide-react";
|
|
7
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
8
|
+
//#region src/comradecircle/components/ImageAttachments.tsx
|
|
9
|
+
var p = 10 * 1024 * 1024, m = 6;
|
|
10
|
+
function h({ fileIds: e, onChange: h, disabled: g = !1 }) {
|
|
11
|
+
let _ = o(null), [v, y] = s(!1), [b, x] = s({}), S = async (r) => {
|
|
12
|
+
let o = Array.from(r.target.files ?? []);
|
|
13
|
+
if (r.target.value = "", o.length === 0) return;
|
|
14
|
+
let s = m - e.length;
|
|
15
|
+
if (s <= 0) {
|
|
16
|
+
a.warning("Attachment limit reached", `Up to ${m} images per post.`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
y(!0);
|
|
20
|
+
try {
|
|
21
|
+
let r = [], i = {};
|
|
22
|
+
for (let e of o.slice(0, s)) {
|
|
23
|
+
if (!e.type.startsWith("image/")) continue;
|
|
24
|
+
if (e.size > p) {
|
|
25
|
+
a.error("Image too large", `${e.name} exceeds 10 MB.`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
let o = await n(e, t.posts);
|
|
29
|
+
r.push(o.id), i[o.id] = o.url;
|
|
30
|
+
}
|
|
31
|
+
r.length && (x((e) => ({
|
|
32
|
+
...e,
|
|
33
|
+
...i
|
|
34
|
+
})), h([...e, ...r]));
|
|
35
|
+
} catch (e) {
|
|
36
|
+
a.error("Upload failed", i(e));
|
|
37
|
+
} finally {
|
|
38
|
+
y(!1);
|
|
39
|
+
}
|
|
40
|
+
}, C = (t) => h(e.filter((e) => e !== t));
|
|
41
|
+
return /* @__PURE__ */ f("div", { children: [
|
|
42
|
+
e.length > 0 && /* @__PURE__ */ d("div", {
|
|
43
|
+
className: "mb-2 flex flex-wrap gap-2",
|
|
44
|
+
children: e.map((e) => /* @__PURE__ */ f("div", {
|
|
45
|
+
className: "relative h-20 w-20 overflow-hidden rounded-lg ring-1 ring-border-seam",
|
|
46
|
+
children: [b[e] ? /* @__PURE__ */ d("img", {
|
|
47
|
+
src: b[e],
|
|
48
|
+
alt: "attachment",
|
|
49
|
+
className: "h-full w-full object-cover"
|
|
50
|
+
}) : /* @__PURE__ */ d(r, {
|
|
51
|
+
fileId: e,
|
|
52
|
+
alt: "attachment",
|
|
53
|
+
className: "h-full w-full"
|
|
54
|
+
}), /* @__PURE__ */ d("button", {
|
|
55
|
+
type: "button",
|
|
56
|
+
onClick: () => C(e),
|
|
57
|
+
"aria-label": "Remove attachment",
|
|
58
|
+
className: "absolute right-0.5 top-0.5 rounded-full bg-bg-canvas/70 p-0.5 text-text-primary hover:bg-bg-canvas",
|
|
59
|
+
children: /* @__PURE__ */ d(u, { className: "h-3.5 w-3.5" })
|
|
60
|
+
})]
|
|
61
|
+
}, e))
|
|
62
|
+
}),
|
|
63
|
+
/* @__PURE__ */ d("input", {
|
|
64
|
+
ref: _,
|
|
65
|
+
type: "file",
|
|
66
|
+
accept: "image/*",
|
|
67
|
+
multiple: !0,
|
|
68
|
+
className: "sr-only",
|
|
69
|
+
onChange: S,
|
|
70
|
+
disabled: g || v
|
|
71
|
+
}),
|
|
72
|
+
/* @__PURE__ */ f("button", {
|
|
73
|
+
type: "button",
|
|
74
|
+
onClick: () => _.current?.click(),
|
|
75
|
+
disabled: g || v || e.length >= m,
|
|
76
|
+
className: "inline-flex items-center gap-1.5 text-xs font-medium text-text-secondary transition-colors hover:text-text-primary disabled:opacity-50",
|
|
77
|
+
children: [v ? /* @__PURE__ */ d(l, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ d(c, { className: "h-4 w-4" }), v ? "Uploading…" : "Add images"]
|
|
78
|
+
})
|
|
79
|
+
] });
|
|
80
|
+
}
|
|
81
|
+
function g({ fileIds: t, className: n }) {
|
|
82
|
+
return t.length === 0 ? null : /* @__PURE__ */ d("div", {
|
|
83
|
+
className: e("mt-3 grid gap-2", t.length === 1 ? "grid-cols-1" : "grid-cols-2 sm:grid-cols-3", n),
|
|
84
|
+
children: t.map((n) => /* @__PURE__ */ d("div", {
|
|
85
|
+
className: e("overflow-hidden rounded-xl ring-1 ring-border-seam", t.length === 1 ? "max-h-96" : "aspect-square"),
|
|
86
|
+
children: /* @__PURE__ */ d(r, {
|
|
87
|
+
fileId: n,
|
|
88
|
+
alt: "Post attachment",
|
|
89
|
+
className: "h-full w-full"
|
|
90
|
+
})
|
|
91
|
+
}, n))
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { g as AttachmentGrid, h as AttachmentPicker };
|
|
96
|
+
|
|
97
|
+
//# sourceMappingURL=ImageAttachments.js.map
|