@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageAttachments.js","names":[],"sources":["../../../src/comradecircle/components/ImageAttachments.tsx"],"sourcesContent":["/**\n * ImageAttachments — attach images to a post + render a post's attachments.\n * @module comradecircle/components\n *\n * Uploads go to wspace-files-svc (reuse, no blob storage here); the composer\n * keeps only file ids, which are persisted as ComPost.attachmentFileIds.\n */\n\nimport { useRef, useState } from 'react';\nimport { ImagePlus, Loader2, X } from 'lucide-react';\nimport { cn } from '../utils/cn';\nimport { FileImage } from './FileImage';\nimport { uploadImageToFiles, FILE_FOLDERS } from '../hooks/useFiles';\nimport { notify, describeError } from '../lib/toast';\n\nconst MAX_BYTES = 10 * 1024 * 1024;\nconst MAX_ATTACHMENTS = 6;\n\n// --- Composer: pick + upload images, expose file ids -----------------------\n\ninterface AttachmentPickerProps {\n fileIds: string[];\n onChange: (fileIds: string[]) => void;\n disabled?: boolean;\n}\n\nexport function AttachmentPicker({ fileIds, onChange, disabled = false }: AttachmentPickerProps) {\n const inputRef = useRef<HTMLInputElement | null>(null);\n const [uploading, setUploading] = useState(false);\n const [previews, setPreviews] = useState<Record<string, string>>({});\n\n const handleSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {\n const files = Array.from(event.target.files ?? []);\n event.target.value = '';\n if (files.length === 0) return;\n const room = MAX_ATTACHMENTS - fileIds.length;\n if (room <= 0) {\n notify.warning('Attachment limit reached', `Up to ${MAX_ATTACHMENTS} images per post.`);\n return;\n }\n setUploading(true);\n try {\n const added: string[] = [];\n const nextPreviews: Record<string, string> = {};\n for (const file of files.slice(0, room)) {\n if (!file.type.startsWith('image/')) continue;\n if (file.size > MAX_BYTES) {\n notify.error('Image too large', `${file.name} exceeds 10 MB.`);\n continue;\n }\n const uploaded = await uploadImageToFiles(file, FILE_FOLDERS.posts);\n added.push(uploaded.id);\n nextPreviews[uploaded.id] = uploaded.url;\n }\n if (added.length) {\n setPreviews((p) => ({ ...p, ...nextPreviews }));\n onChange([...fileIds, ...added]);\n }\n } catch (err) {\n notify.error('Upload failed', describeError(err));\n } finally {\n setUploading(false);\n }\n };\n\n const remove = (id: string) => onChange(fileIds.filter((f) => f !== id));\n\n return (\n <div>\n {fileIds.length > 0 && (\n <div className=\"mb-2 flex flex-wrap gap-2\">\n {fileIds.map((id) => (\n <div\n key={id}\n className=\"relative h-20 w-20 overflow-hidden rounded-lg ring-1 ring-border-seam\"\n >\n {previews[id] ? (\n <img src={previews[id]} alt=\"attachment\" className=\"h-full w-full object-cover\" />\n ) : (\n <FileImage fileId={id} alt=\"attachment\" className=\"h-full w-full\" />\n )}\n <button\n type=\"button\"\n onClick={() => remove(id)}\n aria-label=\"Remove attachment\"\n className=\"absolute right-0.5 top-0.5 rounded-full bg-bg-canvas/70 p-0.5 text-text-primary hover:bg-bg-canvas\"\n >\n <X className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n ))}\n </div>\n )}\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\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 || fileIds.length >= MAX_ATTACHMENTS}\n className=\"inline-flex items-center gap-1.5 text-xs font-medium text-text-secondary transition-colors hover:text-text-primary disabled:opacity-50\"\n >\n {uploading ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <ImagePlus className=\"h-4 w-4\" />\n )}\n {uploading ? 'Uploading…' : 'Add images'}\n </button>\n </div>\n );\n}\n\n// --- Reader: render a post's attachment grid -------------------------------\n\ninterface AttachmentGridProps {\n fileIds: string[];\n className?: string;\n}\n\nexport function AttachmentGrid({ fileIds, className }: AttachmentGridProps) {\n if (fileIds.length === 0) return null;\n return (\n <div\n className={cn(\n 'mt-3 grid gap-2',\n fileIds.length === 1 ? 'grid-cols-1' : 'grid-cols-2 sm:grid-cols-3',\n className\n )}\n >\n {fileIds.map((id) => (\n <div\n key={id}\n className={cn(\n 'overflow-hidden rounded-xl ring-1 ring-border-seam',\n fileIds.length === 1 ? 'max-h-96' : 'aspect-square'\n )}\n >\n <FileImage fileId={id} alt=\"Post attachment\" className=\"h-full w-full\" />\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAeA,IAAM,IAAY,KAAK,OAAO,MACxB,IAAkB;AAUxB,SAAgB,EAAiB,EAAE,YAAS,aAAU,cAAW,MAAgC;CAC/F,IAAM,IAAW,EAAgC,KAAK,EAChD,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,CAAC,GAAU,KAAe,EAAiC,EAAE,CAAC,EAE9D,IAAe,OAAO,MAA+C;EACzE,IAAM,IAAQ,MAAM,KAAK,EAAM,OAAO,SAAS,EAAE,CAAC;AAElD,MADA,EAAM,OAAO,QAAQ,IACjB,EAAM,WAAW,EAAG;EACxB,IAAM,IAAO,IAAkB,EAAQ;AACvC,MAAI,KAAQ,GAAG;AACb,KAAO,QAAQ,4BAA4B,SAAS,EAAgB,mBAAmB;AACvF;;AAEF,IAAa,GAAK;AAClB,MAAI;GACF,IAAM,IAAkB,EAAE,EACpB,IAAuC,EAAE;AAC/C,QAAK,IAAM,KAAQ,EAAM,MAAM,GAAG,EAAK,EAAE;AACvC,QAAI,CAAC,EAAK,KAAK,WAAW,SAAS,CAAE;AACrC,QAAI,EAAK,OAAO,GAAW;AACzB,OAAO,MAAM,mBAAmB,GAAG,EAAK,KAAK,iBAAiB;AAC9D;;IAEF,IAAM,IAAW,MAAM,EAAmB,GAAM,EAAa,MAAM;AAEnE,IADA,EAAM,KAAK,EAAS,GAAG,EACvB,EAAa,EAAS,MAAM,EAAS;;AAEvC,GAAI,EAAM,WACR,GAAa,OAAO;IAAE,GAAG;IAAG,GAAG;IAAc,EAAE,EAC/C,EAAS,CAAC,GAAG,GAAS,GAAG,EAAM,CAAC;WAE3B,GAAK;AACZ,KAAO,MAAM,iBAAiB,EAAc,EAAI,CAAC;YACzC;AACR,KAAa,GAAM;;IAIjB,KAAU,MAAe,EAAS,EAAQ,QAAQ,MAAM,MAAM,EAAG,CAAC;AAExE,QACE,kBAAC,OAAD,EAAA,UAAA;EACG,EAAQ,SAAS,KAChB,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAQ,KAAK,MACZ,kBAAC,OAAD;IAEE,WAAU;cAFZ,CAIG,EAAS,KACR,kBAAC,OAAD;KAAK,KAAK,EAAS;KAAK,KAAI;KAAa,WAAU;KAA+B,CAAA,GAElF,kBAAC,GAAD;KAAW,QAAQ;KAAI,KAAI;KAAa,WAAU;KAAkB,CAAA,EAEtE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAO,EAAG;KACzB,cAAW;KACX,WAAU;eAEV,kBAAC,GAAD,EAAG,WAAU,eAAgB,CAAA;KACtB,CAAA,CACL;MAhBC,EAgBD,CACN;GACE,CAAA;EAER,kBAAC,SAAD;GACE,KAAK;GACL,MAAK;GACL,QAAO;GACP,UAAA;GACA,WAAU;GACV,UAAU;GACV,UAAU,KAAY;GACtB,CAAA;EACF,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAS,SAAS,OAAO;GACxC,UAAU,KAAY,KAAa,EAAQ,UAAU;GACrD,WAAU;aAJZ,CAMG,IACC,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,GAE5C,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAElC,IAAY,eAAe,aACrB;;EACL,EAAA,CAAA;;AAWV,SAAgB,EAAe,EAAE,YAAS,gBAAkC;AAE1E,QADI,EAAQ,WAAW,IAAU,OAE/B,kBAAC,OAAD;EACE,WAAW,EACT,mBACA,EAAQ,WAAW,IAAI,gBAAgB,8BACvC,EACD;YAEA,EAAQ,KAAK,MACZ,kBAAC,OAAD;GAEE,WAAW,EACT,sDACA,EAAQ,WAAW,IAAI,aAAa,gBACrC;aAED,kBAAC,GAAD;IAAW,QAAQ;IAAI,KAAI;IAAkB,WAAU;IAAkB,CAAA;GACrE,EAPC,EAOD,CACN;EACE,CAAA"}
|
|
@@ -18,7 +18,8 @@ var s = ({ memberId: s, variant: c = "inline", size: l = "sm", suffix: u, showPr
|
|
|
18
18
|
children: [/* @__PURE__ */ i(r, {
|
|
19
19
|
name: v,
|
|
20
20
|
size: l,
|
|
21
|
-
presence: y
|
|
21
|
+
presence: y,
|
|
22
|
+
fileId: g?.avatar?.id
|
|
22
23
|
}), /* @__PURE__ */ a("div", {
|
|
23
24
|
className: "min-w-0",
|
|
24
25
|
children: [/* @__PURE__ */ a("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MemberIdentity.js","names":[],"sources":["../../../src/comradecircle/components/MemberIdentity.tsx"],"sourcesContent":["/**\n * MemberIdentity — resolve a `memberId` to a display name + avatar via the\n * product's own social-identity layer (`ComProfile`).\n * @module comradecircle/components\n *\n * The composed `User` entity resolves `id` only, so we never render a raw actor\n * id. Instead we look up the member's `ComProfile` (cached aggressively). The\n * calling user is labelled \"You\". If a member has not created a profile yet we\n * fall back to a neutral \"Member\" label with initials from the id.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport { useComProfile } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { Avatar } from './Avatar';\nimport type { ComPresence } from '../types';\n\ninterface MemberIdentityProps {\n memberId: string;\n /** Layout: 'inline' = avatar + name only; 'row' = avatar + name + headline. */\n variant?: 'inline' | 'row';\n size?: 'xs' | 'sm' | 'md' | 'lg';\n /** Suffix shown after the name (e.g. a timestamp) in inline variant. */\n suffix?: string;\n showPresence?: boolean;\n onClick?: () => void;\n className?: string;\n}\n\nfunction labelForMember(\n displayName: string | undefined,\n isSelf: boolean\n): { name: string; presence?: ComPresence | null } {\n if (displayName) return { name: isSelf ? `${displayName} (You)` : displayName };\n return { name: isSelf ? 'You' : 'Member' };\n}\n\nexport const MemberIdentity: FC<MemberIdentityProps> = ({\n memberId,\n variant = 'inline',\n size = 'sm',\n suffix,\n showPresence = false,\n onClick,\n className,\n}) => {\n const { currentUser } = useComradeCircle();\n const isSelf = Boolean(currentUser?.id && currentUser.id === memberId);\n const { data: profile } = useComProfile(memberId);\n\n const { name } = labelForMember(profile?.displayName ?? undefined, isSelf);\n const nameForAvatar = profile?.displayName ?? (isSelf ? (currentUser?.name ?? 'You') : memberId);\n const presence = showPresence ? (profile?.presence ?? null) : null;\n\n const Wrapper = onClick ? 'button' : 'div';\n\n return (\n <Wrapper\n type={onClick ? 'button' : undefined}\n onClick={onClick}\n className={cn(\n 'flex items-center gap-2.5 text-left',\n onClick && 'rounded-lg transition-colors hover:opacity-80',\n className\n )}\n >\n <Avatar name={nameForAvatar} size={size} presence={presence} />\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"truncate text-sm font-medium text-text-primary\">{name}</span>\n {suffix && variant === 'inline' && (\n <span className=\"shrink-0 text-xs text-text-tertiary\">· {suffix}</span>\n )}\n </div>\n {variant === 'row' && profile?.headline && (\n <p className=\"truncate text-xs text-text-secondary\">{profile.headline}</p>\n )}\n </div>\n </Wrapper>\n );\n};\n"],"mappings":";;;;;;;;AA8BA,SAAS,EACP,GACA,GACiD;AAEjD,QADI,IAAoB,EAAE,MAAM,IAAS,GAAG,EAAY,UAAU,GAAa,GACxE,EAAE,MAAM,IAAS,QAAQ,UAAU;;AAG5C,IAAa,KAA2C,EACtD,aACA,aAAU,UACV,UAAO,MACP,WACA,kBAAe,IACf,YACA,mBACI;CACJ,IAAM,EAAE,mBAAgB,GAAkB,EACpC,IAAS,GAAQ,GAAa,MAAM,EAAY,OAAO,IACvD,EAAE,MAAM,MAAY,EAAc,EAAS,EAE3C,EAAE,YAAS,EAAe,GAAS,eAAe,KAAA,GAAW,EAAO,EACpE,IAAgB,GAAS,gBAAgB,IAAU,GAAa,QAAQ,QAAS,IACjF,IAAW,IAAgB,GAAS,YAAY,OAAQ;AAI9D,QACE,kBAHc,IAAU,WAAW,OAGnC;EACE,MAAM,IAAU,WAAW,KAAA;EAClB;EACT,WAAW,EACT,uCACA,KAAW,iDACX,EACD;YAPH,CASE,kBAAC,GAAD;GAAQ,MAAM;GAAqB;GAAgB;
|
|
1
|
+
{"version":3,"file":"MemberIdentity.js","names":[],"sources":["../../../src/comradecircle/components/MemberIdentity.tsx"],"sourcesContent":["/**\n * MemberIdentity — resolve a `memberId` to a display name + avatar via the\n * product's own social-identity layer (`ComProfile`).\n * @module comradecircle/components\n *\n * The composed `User` entity resolves `id` only, so we never render a raw actor\n * id. Instead we look up the member's `ComProfile` (cached aggressively). The\n * calling user is labelled \"You\". If a member has not created a profile yet we\n * fall back to a neutral \"Member\" label with initials from the id.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\nimport { useComProfile } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { Avatar } from './Avatar';\nimport type { ComPresence } from '../types';\n\ninterface MemberIdentityProps {\n memberId: string;\n /** Layout: 'inline' = avatar + name only; 'row' = avatar + name + headline. */\n variant?: 'inline' | 'row';\n size?: 'xs' | 'sm' | 'md' | 'lg';\n /** Suffix shown after the name (e.g. a timestamp) in inline variant. */\n suffix?: string;\n showPresence?: boolean;\n onClick?: () => void;\n className?: string;\n}\n\nfunction labelForMember(\n displayName: string | undefined,\n isSelf: boolean\n): { name: string; presence?: ComPresence | null } {\n if (displayName) return { name: isSelf ? `${displayName} (You)` : displayName };\n return { name: isSelf ? 'You' : 'Member' };\n}\n\nexport const MemberIdentity: FC<MemberIdentityProps> = ({\n memberId,\n variant = 'inline',\n size = 'sm',\n suffix,\n showPresence = false,\n onClick,\n className,\n}) => {\n const { currentUser } = useComradeCircle();\n const isSelf = Boolean(currentUser?.id && currentUser.id === memberId);\n const { data: profile } = useComProfile(memberId);\n\n const { name } = labelForMember(profile?.displayName ?? undefined, isSelf);\n const nameForAvatar = profile?.displayName ?? (isSelf ? (currentUser?.name ?? 'You') : memberId);\n const presence = showPresence ? (profile?.presence ?? null) : null;\n\n const Wrapper = onClick ? 'button' : 'div';\n\n return (\n <Wrapper\n type={onClick ? 'button' : undefined}\n onClick={onClick}\n className={cn(\n 'flex items-center gap-2.5 text-left',\n onClick && 'rounded-lg transition-colors hover:opacity-80',\n className\n )}\n >\n <Avatar name={nameForAvatar} size={size} presence={presence} fileId={profile?.avatar?.id} />\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"truncate text-sm font-medium text-text-primary\">{name}</span>\n {suffix && variant === 'inline' && (\n <span className=\"shrink-0 text-xs text-text-tertiary\">· {suffix}</span>\n )}\n </div>\n {variant === 'row' && profile?.headline && (\n <p className=\"truncate text-xs text-text-secondary\">{profile.headline}</p>\n )}\n </div>\n </Wrapper>\n );\n};\n"],"mappings":";;;;;;;;AA8BA,SAAS,EACP,GACA,GACiD;AAEjD,QADI,IAAoB,EAAE,MAAM,IAAS,GAAG,EAAY,UAAU,GAAa,GACxE,EAAE,MAAM,IAAS,QAAQ,UAAU;;AAG5C,IAAa,KAA2C,EACtD,aACA,aAAU,UACV,UAAO,MACP,WACA,kBAAe,IACf,YACA,mBACI;CACJ,IAAM,EAAE,mBAAgB,GAAkB,EACpC,IAAS,GAAQ,GAAa,MAAM,EAAY,OAAO,IACvD,EAAE,MAAM,MAAY,EAAc,EAAS,EAE3C,EAAE,YAAS,EAAe,GAAS,eAAe,KAAA,GAAW,EAAO,EACpE,IAAgB,GAAS,gBAAgB,IAAU,GAAa,QAAQ,QAAS,IACjF,IAAW,IAAgB,GAAS,YAAY,OAAQ;AAI9D,QACE,kBAHc,IAAU,WAAW,OAGnC;EACE,MAAM,IAAU,WAAW,KAAA;EAClB;EACT,WAAW,EACT,uCACA,KAAW,iDACX,EACD;YAPH,CASE,kBAAC,GAAD;GAAQ,MAAM;GAAqB;GAAgB;GAAU,QAAQ,GAAS,QAAQ;GAAM,CAAA,EAC5F,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAAkD;KAAY,CAAA,EAC7E,KAAU,MAAY,YACrB,kBAAC,QAAD;KAAM,WAAU;eAAhB,CAAsD,MAAG,EAAc;OAErE;OACL,MAAY,SAAS,GAAS,YAC7B,kBAAC,KAAD;IAAG,WAAU;cAAwC,EAAQ;IAAa,CAAA,CAExE;KACE"}
|
|
@@ -8,13 +8,14 @@ import { describeError as a, notify as o } from "../lib/toast.js";
|
|
|
8
8
|
import { ReactionBar as s } from "./ReactionBar.js";
|
|
9
9
|
import { formatRelativeTime as c } from "../utils/formatters.js";
|
|
10
10
|
import { CommentThread as l } from "./CommentThread.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { AttachmentGrid as u } from "./ImageAttachments.js";
|
|
12
|
+
import { useState as d } from "react";
|
|
13
|
+
import { MessageSquare as f, MoreHorizontal as p, Pencil as m, Pin as h, Trash2 as g } from "lucide-react";
|
|
13
14
|
import { jsx as _, jsxs as v } from "react/jsx-runtime";
|
|
14
15
|
import { Button as y, DropdownMenu as b, DropdownMenuContent as x, DropdownMenuItem as S, DropdownMenuSeparator as C, DropdownMenuTrigger as w, GlassCard as T, Textarea as E } from "@burdenoff/fe-libs/ui";
|
|
15
16
|
//#region src/comradecircle/components/PostCard.tsx
|
|
16
17
|
function D({ post: D, defaultExpanded: O = !1 }) {
|
|
17
|
-
let { currentUser: k } = e(), A = r(), j = n(), [M, N] =
|
|
18
|
+
let { currentUser: k } = e(), A = r(), j = n(), [M, N] = d(O), [P, F] = d(!1), [I, L] = d(D.body), R = !!(k?.id && k.id === D.authorId);
|
|
18
19
|
return /* @__PURE__ */ v(T, {
|
|
19
20
|
treatment: "plain",
|
|
20
21
|
className: t("p-4", D.isPinned && "ring-1 ring-accent/40"),
|
|
@@ -38,7 +39,7 @@ function D({ post: D, defaultExpanded: O = !1 }) {
|
|
|
38
39
|
type: "button",
|
|
39
40
|
"aria-label": "Post actions",
|
|
40
41
|
className: "rounded-lg p-1.5 text-text-tertiary transition-colors hover:bg-bg-sunken hover:text-text-primary",
|
|
41
|
-
children: /* @__PURE__ */ _(
|
|
42
|
+
children: /* @__PURE__ */ _(p, { className: "h-4 w-4" })
|
|
42
43
|
})
|
|
43
44
|
}), /* @__PURE__ */ v(x, {
|
|
44
45
|
align: "end",
|
|
@@ -131,15 +132,7 @@ function D({ post: D, defaultExpanded: O = !1 }) {
|
|
|
131
132
|
children: ["#", e]
|
|
132
133
|
}, e))
|
|
133
134
|
}),
|
|
134
|
-
D.attachmentFileIds.length > 0 && /* @__PURE__ */
|
|
135
|
-
className: "mt-2 inline-flex items-center gap-1.5 text-xs text-text-tertiary",
|
|
136
|
-
children: [
|
|
137
|
-
/* @__PURE__ */ _(p, { className: "h-3.5 w-3.5" }),
|
|
138
|
-
D.attachmentFileIds.length,
|
|
139
|
-
" attachment",
|
|
140
|
-
D.attachmentFileIds.length === 1 ? "" : "s"
|
|
141
|
-
]
|
|
142
|
-
}),
|
|
135
|
+
D.attachmentFileIds.length > 0 && /* @__PURE__ */ _(u, { fileIds: D.attachmentFileIds }),
|
|
143
136
|
/* @__PURE__ */ v("div", {
|
|
144
137
|
className: "mt-3 flex items-center justify-between gap-3 border-t border-border-seam pt-3",
|
|
145
138
|
children: [/* @__PURE__ */ _(s, { post: D }), /* @__PURE__ */ v("button", {
|
|
@@ -148,7 +141,7 @@ function D({ post: D, defaultExpanded: O = !1 }) {
|
|
|
148
141
|
"aria-expanded": M,
|
|
149
142
|
className: t("inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium transition-colors", M ? "bg-[var(--color-accent-soft)] text-text-primary" : "text-text-secondary hover:bg-bg-sunken hover:text-text-primary"),
|
|
150
143
|
children: [
|
|
151
|
-
/* @__PURE__ */ _(
|
|
144
|
+
/* @__PURE__ */ _(f, { className: "h-3.5 w-3.5" }),
|
|
152
145
|
/* @__PURE__ */ _("span", {
|
|
153
146
|
className: "tabular-nums",
|
|
154
147
|
children: D.commentCount
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostCard.js","names":[],"sources":["../../../src/comradecircle/components/PostCard.tsx"],"sourcesContent":["/**\n * PostCard — a single post with reactions and a collapsible comment thread.\n * @module comradecircle/components\n *\n * Reads its data from a `ComPost`; writes via `comUpdatePost` (edit / pin) and\n * `comRemovePost` (delete). Reactions + comments are delegated to\n * {@link ReactionBar} and {@link CommentThread}. Author-only actions (edit, pin,\n * delete) are gated on `authorId === currentUser.id`.\n */\n\nimport { useState } from 'react';\nimport { MessageSquare, MoreHorizontal, Pin, Pencil, Trash2, Paperclip } from 'lucide-react';\nimport {\n GlassCard,\n Button,\n Textarea,\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n} from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { MemberIdentity } from './MemberIdentity';\nimport { ReactionBar } from './ReactionBar';\nimport { CommentThread } from './CommentThread';\nimport { useComUpdatePost, useComRemovePost } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComPost } from '../types';\n\ninterface PostCardProps {\n post: ComPost;\n /** Start with the comment thread open. */\n defaultExpanded?: boolean;\n}\n\nexport function PostCard({ post, defaultExpanded = false }: PostCardProps) {\n const { currentUser } = useComradeCircle();\n const updatePost = useComUpdatePost();\n const removePost = useComRemovePost();\n const [expanded, setExpanded] = useState(defaultExpanded);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(post.body);\n\n const isAuthor = Boolean(currentUser?.id && currentUser.id === post.authorId);\n\n const saveEdit = async () => {\n const text = draft.trim();\n if (!text || text === post.body) {\n setEditing(false);\n return;\n }\n try {\n await updatePost.mutateAsync({ id: post.id, input: { body: text } });\n setEditing(false);\n notify.success('Post updated');\n } catch (err) {\n notify.error('Could not update post', describeError(err));\n }\n };\n\n const togglePin = async () => {\n try {\n await updatePost.mutateAsync({ id: post.id, input: { isPinned: !post.isPinned } });\n notify.success(post.isPinned ? 'Post unpinned' : 'Post pinned');\n } catch (err) {\n notify.error('Could not update post', describeError(err));\n }\n };\n\n const remove = async () => {\n try {\n await removePost.mutateAsync({ id: post.id, channelId: post.channelId });\n notify.success('Post removed');\n } catch (err) {\n notify.error('Could not remove post', describeError(err));\n }\n };\n\n return (\n <GlassCard treatment=\"plain\" className={cn('p-4', post.isPinned && 'ring-1 ring-accent/40')}>\n <div className=\"flex items-start justify-between gap-3\">\n <MemberIdentity\n memberId={post.authorId}\n variant=\"row\"\n size=\"md\"\n suffix={formatRelativeTime(post.createdAt)}\n showPresence\n />\n <div className=\"flex items-center gap-1\">\n {post.isPinned && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n <Pin className=\"h-3 w-3\" /> Pinned\n </span>\n )}\n {isAuthor && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n aria-label=\"Post actions\"\n className=\"rounded-lg p-1.5 text-text-tertiary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => setEditing(true)}>\n <Pencil className=\"mr-2 h-4 w-4\" /> Edit\n </DropdownMenuItem>\n <DropdownMenuItem onClick={togglePin}>\n <Pin className=\"mr-2 h-4 w-4\" /> {post.isPinned ? 'Unpin' : 'Pin'}\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onClick={remove}\n className=\"text-status-error-text focus:text-status-error-text\"\n >\n <Trash2 className=\"mr-2 h-4 w-4\" /> Delete\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n\n {editing ? (\n <div className=\"mt-3\">\n <Textarea\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n rows={3}\n className=\"resize-none\"\n />\n <div className=\"mt-2 flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n setDraft(post.body);\n setEditing(false);\n }}\n >\n Cancel\n </Button>\n <Button size=\"sm\" onClick={saveEdit} disabled={updatePost.isPending}>\n Save\n </Button>\n </div>\n </div>\n ) : (\n <p className=\"mt-3 whitespace-pre-wrap break-words text-sm text-text-primary\">\n {post.body}\n </p>\n )}\n\n {post.tags.length > 0 && (\n <div className=\"mt-2 flex flex-wrap gap-1.5\">\n {post.tags.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 {post.attachmentFileIds.length > 0 && (\n <div className=\"mt-2 inline-flex items-center gap-1.5 text-xs text-text-tertiary\">\n <Paperclip className=\"h-3.5 w-3.5\" />\n {post.attachmentFileIds.length} attachment\n {post.attachmentFileIds.length === 1 ? '' : 's'}\n </div>\n )}\n\n <div className=\"mt-3 flex items-center justify-between gap-3 border-t border-border-seam pt-3\">\n <ReactionBar post={post} />\n <button\n type=\"button\"\n onClick={() => setExpanded((s) => !s)}\n aria-expanded={expanded}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium transition-colors',\n expanded\n ? 'bg-[var(--color-accent-soft)] text-text-primary'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n <MessageSquare className=\"h-3.5 w-3.5\" />\n <span className=\"tabular-nums\">{post.commentCount}</span>\n <span className=\"hidden sm:inline\">\n {post.commentCount === 1 ? 'comment' : 'comments'}\n </span>\n </button>\n </div>\n\n {expanded && <CommentThread postId={post.id} />}\n </GlassCard>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PostCard.js","names":[],"sources":["../../../src/comradecircle/components/PostCard.tsx"],"sourcesContent":["/**\n * PostCard — a single post with reactions and a collapsible comment thread.\n * @module comradecircle/components\n *\n * Reads its data from a `ComPost`; writes via `comUpdatePost` (edit / pin) and\n * `comRemovePost` (delete). Reactions + comments are delegated to\n * {@link ReactionBar} and {@link CommentThread}. Author-only actions (edit, pin,\n * delete) are gated on `authorId === currentUser.id`.\n */\n\nimport { useState } from 'react';\nimport { MessageSquare, MoreHorizontal, Pin, Pencil, Trash2 } from 'lucide-react';\nimport {\n GlassCard,\n Button,\n Textarea,\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n} from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { MemberIdentity } from './MemberIdentity';\nimport { ReactionBar } from './ReactionBar';\nimport { CommentThread } from './CommentThread';\nimport { AttachmentGrid } from './ImageAttachments';\nimport { useComUpdatePost, useComRemovePost } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComPost } from '../types';\n\ninterface PostCardProps {\n post: ComPost;\n /** Start with the comment thread open. */\n defaultExpanded?: boolean;\n}\n\nexport function PostCard({ post, defaultExpanded = false }: PostCardProps) {\n const { currentUser } = useComradeCircle();\n const updatePost = useComUpdatePost();\n const removePost = useComRemovePost();\n const [expanded, setExpanded] = useState(defaultExpanded);\n const [editing, setEditing] = useState(false);\n const [draft, setDraft] = useState(post.body);\n\n const isAuthor = Boolean(currentUser?.id && currentUser.id === post.authorId);\n\n const saveEdit = async () => {\n const text = draft.trim();\n if (!text || text === post.body) {\n setEditing(false);\n return;\n }\n try {\n await updatePost.mutateAsync({ id: post.id, input: { body: text } });\n setEditing(false);\n notify.success('Post updated');\n } catch (err) {\n notify.error('Could not update post', describeError(err));\n }\n };\n\n const togglePin = async () => {\n try {\n await updatePost.mutateAsync({ id: post.id, input: { isPinned: !post.isPinned } });\n notify.success(post.isPinned ? 'Post unpinned' : 'Post pinned');\n } catch (err) {\n notify.error('Could not update post', describeError(err));\n }\n };\n\n const remove = async () => {\n try {\n await removePost.mutateAsync({ id: post.id, channelId: post.channelId });\n notify.success('Post removed');\n } catch (err) {\n notify.error('Could not remove post', describeError(err));\n }\n };\n\n return (\n <GlassCard treatment=\"plain\" className={cn('p-4', post.isPinned && 'ring-1 ring-accent/40')}>\n <div className=\"flex items-start justify-between gap-3\">\n <MemberIdentity\n memberId={post.authorId}\n variant=\"row\"\n size=\"md\"\n suffix={formatRelativeTime(post.createdAt)}\n showPresence\n />\n <div className=\"flex items-center gap-1\">\n {post.isPinned && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n <Pin className=\"h-3 w-3\" /> Pinned\n </span>\n )}\n {isAuthor && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n aria-label=\"Post actions\"\n className=\"rounded-lg p-1.5 text-text-tertiary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onClick={() => setEditing(true)}>\n <Pencil className=\"mr-2 h-4 w-4\" /> Edit\n </DropdownMenuItem>\n <DropdownMenuItem onClick={togglePin}>\n <Pin className=\"mr-2 h-4 w-4\" /> {post.isPinned ? 'Unpin' : 'Pin'}\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onClick={remove}\n className=\"text-status-error-text focus:text-status-error-text\"\n >\n <Trash2 className=\"mr-2 h-4 w-4\" /> Delete\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </div>\n\n {editing ? (\n <div className=\"mt-3\">\n <Textarea\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n rows={3}\n className=\"resize-none\"\n />\n <div className=\"mt-2 flex justify-end gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n setDraft(post.body);\n setEditing(false);\n }}\n >\n Cancel\n </Button>\n <Button size=\"sm\" onClick={saveEdit} disabled={updatePost.isPending}>\n Save\n </Button>\n </div>\n </div>\n ) : (\n <p className=\"mt-3 whitespace-pre-wrap break-words text-sm text-text-primary\">\n {post.body}\n </p>\n )}\n\n {post.tags.length > 0 && (\n <div className=\"mt-2 flex flex-wrap gap-1.5\">\n {post.tags.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 {post.attachmentFileIds.length > 0 && <AttachmentGrid fileIds={post.attachmentFileIds} />}\n\n <div className=\"mt-3 flex items-center justify-between gap-3 border-t border-border-seam pt-3\">\n <ReactionBar post={post} />\n <button\n type=\"button\"\n onClick={() => setExpanded((s) => !s)}\n aria-expanded={expanded}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium transition-colors',\n expanded\n ? 'bg-[var(--color-accent-soft)] text-text-primary'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n <MessageSquare className=\"h-3.5 w-3.5\" />\n <span className=\"tabular-nums\">{post.commentCount}</span>\n <span className=\"hidden sm:inline\">\n {post.commentCount === 1 ? 'comment' : 'comments'}\n </span>\n </button>\n </div>\n\n {expanded && <CommentThread postId={post.id} />}\n </GlassCard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -4,66 +4,76 @@ import { useComCreatePost as t } from "../hooks/useComradeCircleApi.js";
|
|
|
4
4
|
import "../hooks/index.js";
|
|
5
5
|
import { Avatar as n } from "./Avatar.js";
|
|
6
6
|
import { describeError as r, notify as i } from "../lib/toast.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
7
|
+
import { AttachmentPicker as a } from "./ImageAttachments.js";
|
|
8
|
+
import { TagInput as o } from "./TagInput.js";
|
|
9
|
+
import { useState as s } from "react";
|
|
10
|
+
import { Send as c } from "lucide-react";
|
|
11
|
+
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
12
|
+
import { Button as d, GlassCard as f, Textarea as p } from "@burdenoff/fe-libs/ui";
|
|
12
13
|
//#region src/comradecircle/components/PostComposer.tsx
|
|
13
|
-
function
|
|
14
|
-
let { currentUser:
|
|
15
|
-
let e =
|
|
16
|
-
if (e) try {
|
|
17
|
-
await
|
|
18
|
-
channelId:
|
|
14
|
+
function m({ channelId: m, channelName: h, onPosted: g }) {
|
|
15
|
+
let { currentUser: _ } = e(), v = t(), [y, b] = s(""), [x, S] = s([]), [C, w] = s(!1), [T, E] = s([]), D = async () => {
|
|
16
|
+
let e = y.trim();
|
|
17
|
+
if (!(!e && T.length === 0)) try {
|
|
18
|
+
await v.mutateAsync({
|
|
19
|
+
channelId: m,
|
|
19
20
|
body: e,
|
|
20
|
-
tags:
|
|
21
|
-
|
|
21
|
+
tags: x.length ? x : null,
|
|
22
|
+
attachmentFileIds: T.length ? T : null
|
|
23
|
+
}), b(""), S([]), E([]), w(!1), i.success("Posted", h ? `Shared to #${h}` : void 0), g?.();
|
|
22
24
|
} catch (e) {
|
|
23
25
|
i.error("Could not publish post", r(e));
|
|
24
26
|
}
|
|
25
27
|
};
|
|
26
|
-
return /* @__PURE__ */
|
|
28
|
+
return /* @__PURE__ */ l(f, {
|
|
27
29
|
treatment: "plain",
|
|
28
30
|
className: "p-4",
|
|
29
|
-
children: /* @__PURE__ */
|
|
31
|
+
children: /* @__PURE__ */ u("div", {
|
|
30
32
|
className: "flex gap-3",
|
|
31
|
-
children: [/* @__PURE__ */
|
|
32
|
-
name:
|
|
33
|
+
children: [/* @__PURE__ */ l(n, {
|
|
34
|
+
name: _?.name ?? _?.email ?? "You",
|
|
33
35
|
size: "md"
|
|
34
|
-
}), /* @__PURE__ */
|
|
36
|
+
}), /* @__PURE__ */ u("div", {
|
|
35
37
|
className: "flex-1",
|
|
36
38
|
children: [
|
|
37
|
-
/* @__PURE__ */
|
|
38
|
-
value:
|
|
39
|
-
onChange: (e) =>
|
|
40
|
-
placeholder:
|
|
39
|
+
/* @__PURE__ */ l(p, {
|
|
40
|
+
value: y,
|
|
41
|
+
onChange: (e) => b(e.target.value),
|
|
42
|
+
placeholder: h ? `Share something in #${h}…` : "Share something…",
|
|
41
43
|
rows: 2,
|
|
42
44
|
className: "resize-none border-0 bg-transparent px-0 shadow-none focus-visible:ring-0",
|
|
43
45
|
onKeyDown: (e) => {
|
|
44
|
-
e.key === "Enter" && (e.metaKey || e.ctrlKey) && (e.preventDefault(),
|
|
46
|
+
e.key === "Enter" && (e.metaKey || e.ctrlKey) && (e.preventDefault(), D());
|
|
45
47
|
}
|
|
46
48
|
}),
|
|
47
|
-
|
|
49
|
+
C && /* @__PURE__ */ l("div", {
|
|
48
50
|
className: "mt-2",
|
|
49
|
-
children: /* @__PURE__ */
|
|
50
|
-
value:
|
|
51
|
-
onChange:
|
|
51
|
+
children: /* @__PURE__ */ l(o, {
|
|
52
|
+
value: x,
|
|
53
|
+
onChange: S,
|
|
52
54
|
placeholder: "Add topics…"
|
|
53
55
|
})
|
|
54
56
|
}),
|
|
55
57
|
/* @__PURE__ */ l("div", {
|
|
58
|
+
className: "mt-2",
|
|
59
|
+
children: /* @__PURE__ */ l(a, {
|
|
60
|
+
fileIds: T,
|
|
61
|
+
onChange: E,
|
|
62
|
+
disabled: v.isPending
|
|
63
|
+
})
|
|
64
|
+
}),
|
|
65
|
+
/* @__PURE__ */ u("div", {
|
|
56
66
|
className: "mt-3 flex items-center justify-between",
|
|
57
|
-
children: [/* @__PURE__ */
|
|
67
|
+
children: [/* @__PURE__ */ l("button", {
|
|
58
68
|
type: "button",
|
|
59
|
-
onClick: () =>
|
|
69
|
+
onClick: () => w((e) => !e),
|
|
60
70
|
className: "text-xs font-medium text-text-secondary transition-colors hover:text-text-primary",
|
|
61
|
-
children:
|
|
62
|
-
}), /* @__PURE__ */
|
|
71
|
+
children: C ? "Hide topics" : "# Add topics"
|
|
72
|
+
}), /* @__PURE__ */ u(d, {
|
|
63
73
|
size: "sm",
|
|
64
|
-
onClick:
|
|
65
|
-
disabled: !
|
|
66
|
-
children: [/* @__PURE__ */ c
|
|
74
|
+
onClick: D,
|
|
75
|
+
disabled: !y.trim() && T.length === 0 || v.isPending,
|
|
76
|
+
children: [/* @__PURE__ */ l(c, { className: "mr-1.5 h-4 w-4" }), " Post"]
|
|
67
77
|
})]
|
|
68
78
|
})
|
|
69
79
|
]
|
|
@@ -72,6 +82,6 @@ function p({ channelId: p, channelName: m, onPosted: h }) {
|
|
|
72
82
|
});
|
|
73
83
|
}
|
|
74
84
|
//#endregion
|
|
75
|
-
export {
|
|
85
|
+
export { m as PostComposer };
|
|
76
86
|
|
|
77
87
|
//# sourceMappingURL=PostComposer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostComposer.js","names":[],"sources":["../../../src/comradecircle/components/PostComposer.tsx"],"sourcesContent":["/**\n * PostComposer — compose and publish a post to a channel.\n * @module comradecircle/components\n *\n * Writes `comCreatePost({ channelId, body, tags })`. Text + tags only (file\n * attachments are a future enhancement — the svc accepts attachmentFileIds but\n * file upload/preview is out of scope for v1).\n */\n\nimport { useState } from 'react';\nimport { Send } from 'lucide-react';\nimport { Button, Textarea, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { TagInput } from './TagInput';\nimport { Avatar } from './Avatar';\nimport { useComCreatePost } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { notify, describeError } from '../lib/toast';\n\ninterface PostComposerProps {\n channelId: string;\n channelName?: string;\n onPosted?: () => void;\n}\n\nexport function PostComposer({ channelId, channelName, onPosted }: PostComposerProps) {\n const { currentUser } = useComradeCircle();\n const createPost = useComCreatePost();\n const [body, setBody] = useState('');\n const [tags, setTags] = useState<string[]>([]);\n const [showTags, setShowTags] = useState(false);\n\n const submit = async () => {\n const text = body.trim();\n if (!text) return;\n try {\n await createPost.mutateAsync({
|
|
1
|
+
{"version":3,"file":"PostComposer.js","names":[],"sources":["../../../src/comradecircle/components/PostComposer.tsx"],"sourcesContent":["/**\n * PostComposer — compose and publish a post to a channel.\n * @module comradecircle/components\n *\n * Writes `comCreatePost({ channelId, body, tags })`. Text + tags only (file\n * attachments are a future enhancement — the svc accepts attachmentFileIds but\n * file upload/preview is out of scope for v1).\n */\n\nimport { useState } from 'react';\nimport { Send } from 'lucide-react';\nimport { Button, Textarea, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { TagInput } from './TagInput';\nimport { Avatar } from './Avatar';\nimport { AttachmentPicker } from './ImageAttachments';\nimport { useComCreatePost } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { notify, describeError } from '../lib/toast';\n\ninterface PostComposerProps {\n channelId: string;\n channelName?: string;\n onPosted?: () => void;\n}\n\nexport function PostComposer({ channelId, channelName, onPosted }: PostComposerProps) {\n const { currentUser } = useComradeCircle();\n const createPost = useComCreatePost();\n const [body, setBody] = useState('');\n const [tags, setTags] = useState<string[]>([]);\n const [showTags, setShowTags] = useState(false);\n const [attachmentFileIds, setAttachmentFileIds] = useState<string[]>([]);\n\n const submit = async () => {\n const text = body.trim();\n if (!text && attachmentFileIds.length === 0) return;\n try {\n await createPost.mutateAsync({\n channelId,\n body: text,\n tags: tags.length ? tags : null,\n attachmentFileIds: attachmentFileIds.length ? attachmentFileIds : null,\n });\n setBody('');\n setTags([]);\n setAttachmentFileIds([]);\n setShowTags(false);\n notify.success('Posted', channelName ? `Shared to #${channelName}` : undefined);\n onPosted?.();\n } catch (err) {\n notify.error('Could not publish post', describeError(err));\n }\n };\n\n return (\n <GlassCard treatment=\"plain\" className=\"p-4\">\n <div className=\"flex gap-3\">\n <Avatar name={currentUser?.name ?? currentUser?.email ?? 'You'} size=\"md\" />\n <div className=\"flex-1\">\n <Textarea\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder={channelName ? `Share something in #${channelName}…` : 'Share something…'}\n rows={2}\n className=\"resize-none border-0 bg-transparent px-0 shadow-none focus-visible:ring-0\"\n onKeyDown={(e) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n void submit();\n }\n }}\n />\n {showTags && (\n <div className=\"mt-2\">\n <TagInput value={tags} onChange={setTags} placeholder=\"Add topics…\" />\n </div>\n )}\n <div className=\"mt-2\">\n <AttachmentPicker\n fileIds={attachmentFileIds}\n onChange={setAttachmentFileIds}\n disabled={createPost.isPending}\n />\n </div>\n <div className=\"mt-3 flex items-center justify-between\">\n <button\n type=\"button\"\n onClick={() => setShowTags((s) => !s)}\n className=\"text-xs font-medium text-text-secondary transition-colors hover:text-text-primary\"\n >\n {showTags ? 'Hide topics' : '# Add topics'}\n </button>\n <Button\n size=\"sm\"\n onClick={submit}\n disabled={(!body.trim() && attachmentFileIds.length === 0) || createPost.isPending}\n >\n <Send className=\"mr-1.5 h-4 w-4\" /> Post\n </Button>\n </div>\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAyBA,SAAgB,EAAa,EAAE,cAAW,gBAAa,eAA+B;CACpF,IAAM,EAAE,mBAAgB,GAAkB,EACpC,IAAa,GAAkB,EAC/B,CAAC,GAAM,KAAW,EAAS,GAAG,EAC9B,CAAC,GAAM,KAAW,EAAmB,EAAE,CAAC,EACxC,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAmB,KAAwB,EAAmB,EAAE,CAAC,EAElE,IAAS,YAAY;EACzB,IAAM,IAAO,EAAK,MAAM;AACpB,SAAC,KAAQ,EAAkB,WAAW,GAC1C,KAAI;AAYF,GAXA,MAAM,EAAW,YAAY;IAC3B;IACA,MAAM;IACN,MAAM,EAAK,SAAS,IAAO;IAC3B,mBAAmB,EAAkB,SAAS,IAAoB;IACnE,CAAC,EACF,EAAQ,GAAG,EACX,EAAQ,EAAE,CAAC,EACX,EAAqB,EAAE,CAAC,EACxB,EAAY,GAAM,EAClB,EAAO,QAAQ,UAAU,IAAc,cAAc,MAAgB,KAAA,EAAU,EAC/E,KAAY;WACL,GAAK;AACZ,KAAO,MAAM,0BAA0B,EAAc,EAAI,CAAC;;;AAI9D,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YACrC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAQ,MAAM,GAAa,QAAQ,GAAa,SAAS;IAAO,MAAK;IAAO,CAAA,EAC5E,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,OAAO;MACP,WAAW,MAAM,EAAQ,EAAE,OAAO,MAAM;MACxC,aAAa,IAAc,uBAAuB,EAAY,KAAK;MACnE,MAAM;MACN,WAAU;MACV,YAAY,MAAM;AAChB,OAAI,EAAE,QAAQ,YAAY,EAAE,WAAW,EAAE,aACvC,EAAE,gBAAgB,EACb,GAAQ;;MAGjB,CAAA;KACD,KACC,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAU,OAAO;OAAM,UAAU;OAAS,aAAY;OAAgB,CAAA;MAClE,CAAA;KAER,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OACE,SAAS;OACT,UAAU;OACV,UAAU,EAAW;OACrB,CAAA;MACE,CAAA;KACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,GAAa,MAAM,CAAC,EAAE;OACrC,WAAU;iBAET,IAAW,gBAAgB;OACrB,CAAA,EACT,kBAAC,GAAD;OACE,MAAK;OACL,SAAS;OACT,UAAW,CAAC,EAAK,MAAM,IAAI,EAAkB,WAAW,KAAM,EAAW;iBAH3E,CAKE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,QAC5B;SACL;;KACF;MACF;;EACI,CAAA"}
|