@burdenoff/microfe-comradecircle 2026.704.2 → 2026.704.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/comradecircle/components/Avatar.js +40 -17
- package/dist/comradecircle/components/Avatar.js.map +1 -1
- package/dist/comradecircle/components/AvatarUploadField.js +25 -14
- package/dist/comradecircle/components/AvatarUploadField.js.map +1 -1
- package/dist/comradecircle/components/CircleCard.js +54 -52
- package/dist/comradecircle/components/CircleCard.js.map +1 -1
- package/dist/comradecircle/components/CommentThread.js +80 -64
- package/dist/comradecircle/components/CommentThread.js.map +1 -1
- package/dist/comradecircle/components/EventCard.js +84 -58
- package/dist/comradecircle/components/EventCard.js.map +1 -1
- package/dist/comradecircle/components/MemberIdentity.js +2 -1
- package/dist/comradecircle/components/MemberIdentity.js.map +1 -1
- package/dist/comradecircle/components/PollCard.js +65 -44
- package/dist/comradecircle/components/PollCard.js.map +1 -1
- package/dist/comradecircle/components/PostCard.js +1 -1
- package/dist/comradecircle/components/PostCard.js.map +1 -1
- package/dist/comradecircle/components/ReactionBar.js +1 -1
- package/dist/comradecircle/components/ReactionBar.js.map +1 -1
- package/dist/comradecircle/components/StateViews.js +1 -16
- package/dist/comradecircle/components/StateViews.js.map +1 -1
- package/dist/comradecircle/components/StatusBadge.js +1 -1
- package/dist/comradecircle/components/StatusBadge.js.map +1 -1
- package/dist/comradecircle/hooks/useComradeCircleApi.js +1 -1
- package/dist/comradecircle/hooks/useComradeCircleApi.js.map +1 -1
- package/dist/comradecircle/pages/FeedPage.js +79 -33
- package/dist/comradecircle/pages/FeedPage.js.map +1 -1
- package/dist/comradecircle/pages/circles/CircleDetailPage.js +354 -243
- package/dist/comradecircle/pages/circles/CircleDetailPage.js.map +1 -1
- package/dist/comradecircle/pages/circles/CirclesPage.js +97 -47
- package/dist/comradecircle/pages/circles/CirclesPage.js.map +1 -1
- package/dist/comradecircle/pages/events/EventDetailPage.js +193 -126
- package/dist/comradecircle/pages/events/EventDetailPage.js.map +1 -1
- package/dist/comradecircle/pages/events/EventsPage.js +74 -34
- package/dist/comradecircle/pages/events/EventsPage.js.map +1 -1
- package/dist/comradecircle/pages/members/MemberProfilePage.js +154 -82
- package/dist/comradecircle/pages/members/MemberProfilePage.js.map +1 -1
- package/dist/comradecircle/pages/members/MembersPage.js +140 -71
- package/dist/comradecircle/pages/members/MembersPage.js.map +1 -1
- package/dist/comradecircle/pages/profile/MyProfilePage.js +298 -202
- package/dist/comradecircle/pages/profile/MyProfilePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommentThread.js","names":[],"sources":["../../../src/comradecircle/components/CommentThread.tsx"],"sourcesContent":["/**\n * CommentThread — threaded comments for a post.\n * @module comradecircle/components\n *\n * Reads `comListComments(postId)`; writes `comAddComment`. Top-level comments\n * (parentId == null) render replies one level deep. Author identity comes from\n * `ComProfile` via {@link MemberIdentity}.\n */\n\nimport { useMemo, useState } from 'react';\nimport { CornerDownRight, Send } from 'lucide-react';\nimport { Button, Textarea, Spinner } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { useComComments, useComAddComment } from '../hooks';\nimport { MemberIdentity } from './MemberIdentity';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComComment } from '../types';\n\ninterface CommentThreadProps {\n postId: string;\n}\n\nexport function CommentThread({ postId }: CommentThreadProps) {\n const { data: comments, isLoading } = useComComments(postId);\n const addComment = useComAddComment();\n const [body, setBody] = useState('');\n const [replyTo, setReplyTo] = useState<ComComment | null>(null);\n\n const { roots, repliesByParent } = useMemo(() => {\n const list = comments ?? [];\n const repliesMap = new Map<string, ComComment[]>();\n const top: ComComment[] = [];\n for (const c of list) {\n if (c.parentId) {\n const arr = repliesMap.get(c.parentId) ?? [];\n arr.push(c);\n repliesMap.set(c.parentId, arr);\n } else {\n top.push(c);\n }\n }\n return { roots: top, repliesByParent: repliesMap };\n }, [comments]);\n\n const submit = async () => {\n const text = body.trim();\n if (!text) return;\n try {\n await addComment.mutateAsync({ postId, body: text, parentId: replyTo?.id ?? null });\n setBody('');\n setReplyTo(null);\n } catch (err) {\n notify.error('Could not add comment', describeError(err));\n }\n };\n\n const renderComment = (c: ComComment, isReply = false) => (\n <div key={c.id} className={cn('flex flex-col gap-1', isReply && 'ml-8')}>\n <div className=\"rounded-xl bg-bg-sunken/60 px-3 py-2\">\n <div className=\"flex items-center justify-between gap-2\">\n <MemberIdentity\n memberId={c.authorId}\n size=\"xs\"\n suffix={formatRelativeTime(c.createdAt)}\n />\n {!isReply && (\n <button\n type=\"button\"\n onClick={() => setReplyTo(c)}\n className=\"inline-flex items-center gap-1 text-xs text-text-tertiary transition-colors hover:text-text-primary\"\n >\n <CornerDownRight className=\"h-3 w-3\" /> Reply\n </button>\n )}\n </div>\n <p className=\"mt-1 whitespace-pre-wrap break-words text-sm text-text-primary\">{c.body}</p>\n </div>\n {(repliesByParent.get(c.id) ?? []).map((r) => renderComment(r, true))}\n </div>\n );\n\n return (\n <div className=\"mt-3 border-t border-border-seam pt-3\">\n {isLoading ? (\n <div className=\"flex justify-center py-4\">\n <Spinner size=\"sm\" />\n </div>\n ) : roots.length === 0 ? (\n <
|
|
1
|
+
{"version":3,"file":"CommentThread.js","names":[],"sources":["../../../src/comradecircle/components/CommentThread.tsx"],"sourcesContent":["/**\n * CommentThread — threaded comments for a post.\n * @module comradecircle/components\n *\n * Reads `comListComments(postId)`; writes `comAddComment`. Top-level comments\n * (parentId == null) render replies one level deep. Author identity comes from\n * `ComProfile` via {@link MemberIdentity}.\n */\n\nimport { useMemo, useState } from 'react';\nimport { CornerDownRight, Send, MessageCircle } from 'lucide-react';\nimport { Button, Textarea, Spinner } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { useComComments, useComAddComment } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { MemberIdentity } from './MemberIdentity';\nimport { Avatar } from './Avatar';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComComment } from '../types';\n\ninterface CommentThreadProps {\n postId: string;\n}\n\nexport function CommentThread({ postId }: CommentThreadProps) {\n const { currentUser } = useComradeCircle();\n const { data: comments, isLoading } = useComComments(postId);\n const addComment = useComAddComment();\n const [body, setBody] = useState('');\n const [replyTo, setReplyTo] = useState<ComComment | null>(null);\n\n const { roots, repliesByParent } = useMemo(() => {\n const list = comments ?? [];\n const repliesMap = new Map<string, ComComment[]>();\n const top: ComComment[] = [];\n for (const c of list) {\n if (c.parentId) {\n const arr = repliesMap.get(c.parentId) ?? [];\n arr.push(c);\n repliesMap.set(c.parentId, arr);\n } else {\n top.push(c);\n }\n }\n return { roots: top, repliesByParent: repliesMap };\n }, [comments]);\n\n const submit = async () => {\n const text = body.trim();\n if (!text) return;\n try {\n await addComment.mutateAsync({ postId, body: text, parentId: replyTo?.id ?? null });\n setBody('');\n setReplyTo(null);\n } catch (err) {\n notify.error('Could not add comment', describeError(err));\n }\n };\n\n const renderComment = (c: ComComment, isReply = false) => (\n <div key={c.id} className={cn('flex flex-col gap-1', isReply && 'ml-8')}>\n <div className=\"rounded-xl bg-bg-sunken/60 px-3 py-2\">\n <div className=\"flex items-center justify-between gap-2\">\n <MemberIdentity\n memberId={c.authorId}\n size=\"xs\"\n suffix={formatRelativeTime(c.createdAt)}\n />\n {!isReply && (\n <button\n type=\"button\"\n onClick={() => setReplyTo(c)}\n className=\"inline-flex items-center gap-1 text-xs text-text-tertiary transition-colors hover:text-text-primary\"\n >\n <CornerDownRight className=\"h-3 w-3\" /> Reply\n </button>\n )}\n </div>\n <p className=\"mt-1 whitespace-pre-wrap break-words text-sm text-text-primary\">{c.body}</p>\n </div>\n {(repliesByParent.get(c.id) ?? []).map((r) => renderComment(r, true))}\n </div>\n );\n\n return (\n <div className=\"mt-3 border-t border-border-seam pt-3\">\n {isLoading ? (\n <div className=\"flex justify-center py-4\">\n <Spinner size=\"sm\" />\n </div>\n ) : roots.length === 0 ? (\n <div className=\"flex flex-col items-center gap-1.5 py-4 text-center\">\n <MessageCircle className=\"h-6 w-6 text-text-tertiary opacity-60\" />\n <p className=\"text-sm text-text-tertiary\">No comments yet — start the conversation.</p>\n </div>\n ) : (\n <div className=\"flex flex-col gap-3\">{roots.map((c) => renderComment(c))}</div>\n )}\n\n <div className=\"mt-3 flex flex-col gap-2\">\n {replyTo && (\n <div className=\"flex items-center justify-between rounded-lg bg-bg-sunken px-3 py-1.5 text-xs text-text-secondary\">\n <span className=\"truncate\">Replying to a comment</span>\n <button\n type=\"button\"\n onClick={() => setReplyTo(null)}\n className=\"text-text-tertiary hover:text-text-primary\"\n >\n Cancel\n </button>\n </div>\n )}\n <div className=\"flex items-end gap-2\">\n <Avatar\n name={currentUser?.name ?? currentUser?.email ?? 'You'}\n seed={currentUser?.id}\n size=\"sm\"\n className=\"mb-1\"\n />\n <Textarea\n value={body}\n onChange={(e) => setBody(e.target.value)}\n placeholder=\"Add a comment…\"\n rows={1}\n className=\"min-h-[40px] resize-none\"\n onKeyDown={(e) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n void submit();\n }\n }}\n />\n <Button\n size=\"icon\"\n onClick={submit}\n disabled={!body.trim() || addComment.isPending}\n aria-label=\"Post comment\"\n className=\"shrink-0 rounded-full\"\n >\n <Send className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,93 +1,119 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { useComRsvpEvent as t } from "../hooks/useComradeCircleApi.js";
|
|
2
3
|
import "../hooks/index.js";
|
|
3
|
-
import { describeError as
|
|
4
|
-
import { formatDateTime as
|
|
5
|
-
import { StatusBadge as
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { describeError as n, notify as r } from "../lib/toast.js";
|
|
5
|
+
import { formatDateTime as i, formatNumber as a, formatRelativeTime as o } from "../utils/formatters.js";
|
|
6
|
+
import { StatusBadge as s } from "./StatusBadge.js";
|
|
7
|
+
import { MiniBar as c } from "./MiniBar.js";
|
|
8
|
+
import { useState as l } from "react";
|
|
9
|
+
import { Calendar as u, Check as d, ExternalLink as f, MapPin as p, Users as m, Video as h } from "lucide-react";
|
|
10
|
+
import { jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
11
|
+
import { Button as v, GlassCard as y } from "@burdenoff/fe-libs/ui";
|
|
9
12
|
//#region src/comradecircle/components/EventCard.tsx
|
|
10
|
-
var
|
|
13
|
+
var b = [{
|
|
11
14
|
status: "GOING",
|
|
12
15
|
label: "Going"
|
|
13
16
|
}, {
|
|
14
17
|
status: "INTERESTED",
|
|
15
18
|
label: "Interested"
|
|
16
19
|
}];
|
|
17
|
-
function
|
|
18
|
-
let
|
|
20
|
+
function x({ event: x, onOpen: S }) {
|
|
21
|
+
let C = t(), [w, T] = l(null), E = x.status === "SCHEDULED" || x.status === "LIVE", D = x.status === "LIVE", O = async (e) => {
|
|
19
22
|
try {
|
|
20
|
-
await
|
|
21
|
-
eventId:
|
|
23
|
+
await C.mutateAsync({
|
|
24
|
+
eventId: x.id,
|
|
22
25
|
status: e
|
|
23
|
-
}),
|
|
26
|
+
}), T(e), r.success(`RSVP: ${e === "GOING" ? "Going" : "Interested"}`, x.title);
|
|
24
27
|
} catch (e) {
|
|
25
|
-
|
|
28
|
+
r.error("Could not RSVP", n(e));
|
|
26
29
|
}
|
|
27
|
-
},
|
|
28
|
-
return /* @__PURE__ */
|
|
30
|
+
}, k = x.mode === "IN_PERSON" ? p : h, A = x.capacity ? Math.min(100, x.rsvpCount / x.capacity * 100) : 0, j = A >= 80;
|
|
31
|
+
return /* @__PURE__ */ _(y, {
|
|
29
32
|
treatment: "plain",
|
|
30
|
-
className: "flex flex-col p-5",
|
|
33
|
+
className: e("flex flex-col p-5 transition-all duration-200 motion-safe:hover:-translate-y-0.5 hover:border-[var(--color-accent-soft)]", D && "ring-1 ring-[var(--color-accent-soft)]"),
|
|
31
34
|
children: [
|
|
32
|
-
/* @__PURE__ */
|
|
35
|
+
/* @__PURE__ */ _("div", {
|
|
33
36
|
className: "flex items-start justify-between gap-3",
|
|
34
|
-
children: [/* @__PURE__ */
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
children: [/* @__PURE__ */ g(s, { status: x.status }), E && /* @__PURE__ */ g("span", {
|
|
38
|
+
className: "text-xs font-medium text-text-tertiary",
|
|
39
|
+
children: o(x.startsAt)
|
|
40
|
+
})]
|
|
41
|
+
}),
|
|
42
|
+
/* @__PURE__ */ g("button", {
|
|
43
|
+
type: "button",
|
|
44
|
+
onClick: S,
|
|
45
|
+
className: "mt-2 text-left text-base font-semibold text-text-primary transition-colors hover:text-accent",
|
|
46
|
+
children: x.title
|
|
40
47
|
}),
|
|
41
|
-
|
|
42
|
-
className: "mt-
|
|
43
|
-
children:
|
|
48
|
+
x.description && /* @__PURE__ */ g("p", {
|
|
49
|
+
className: "mt-1.5 line-clamp-2 text-sm text-text-secondary",
|
|
50
|
+
children: x.description
|
|
44
51
|
}),
|
|
45
|
-
/* @__PURE__ */
|
|
52
|
+
/* @__PURE__ */ _("div", {
|
|
46
53
|
className: "mt-3 flex flex-col gap-1.5 text-sm text-text-secondary",
|
|
47
|
-
children: [/* @__PURE__ */
|
|
54
|
+
children: [/* @__PURE__ */ _("span", {
|
|
48
55
|
className: "inline-flex items-center gap-2",
|
|
49
56
|
children: [
|
|
50
|
-
/* @__PURE__ */
|
|
57
|
+
/* @__PURE__ */ g(u, { className: "h-4 w-4 text-text-tertiary" }),
|
|
51
58
|
" ",
|
|
52
|
-
|
|
59
|
+
i(x.startsAt)
|
|
53
60
|
]
|
|
54
|
-
}), /* @__PURE__ */
|
|
61
|
+
}), /* @__PURE__ */ _("span", {
|
|
55
62
|
className: "inline-flex items-center gap-2",
|
|
56
|
-
children: [/* @__PURE__ */
|
|
63
|
+
children: [/* @__PURE__ */ g(k, { className: "h-4 w-4 text-text-tertiary" }), x.mode === "IN_PERSON" ? x.location || "In person" : x.mode === "HYBRID" ? `Hybrid${x.location ? ` · ${x.location}` : ""}` : "Online"]
|
|
57
64
|
})]
|
|
58
65
|
}),
|
|
59
|
-
/* @__PURE__ */
|
|
66
|
+
/* @__PURE__ */ _("div", {
|
|
67
|
+
className: "mt-3",
|
|
68
|
+
children: [/* @__PURE__ */ _("div", {
|
|
69
|
+
className: "flex items-center justify-between text-xs text-text-tertiary",
|
|
70
|
+
children: [/* @__PURE__ */ _("span", {
|
|
71
|
+
className: "inline-flex items-center gap-1",
|
|
72
|
+
children: [
|
|
73
|
+
/* @__PURE__ */ g(m, { className: "h-3.5 w-3.5" }),
|
|
74
|
+
" ",
|
|
75
|
+
a(x.rsvpCount),
|
|
76
|
+
x.capacity ? ` / ${a(x.capacity)}` : "",
|
|
77
|
+
" attending"
|
|
78
|
+
]
|
|
79
|
+
}), x.capacity && j && /* @__PURE__ */ g("span", {
|
|
80
|
+
className: e(A >= 100 ? "text-status-error-text" : "text-status-warning-text"),
|
|
81
|
+
children: A >= 100 ? "Full" : `${Math.round(A)}% full`
|
|
82
|
+
})]
|
|
83
|
+
}), x.capacity ? /* @__PURE__ */ g(c, {
|
|
84
|
+
value: A,
|
|
85
|
+
className: "mt-1.5",
|
|
86
|
+
height: 4,
|
|
87
|
+
barClassName: A >= 100 ? "bg-status-error-text" : j ? "bg-status-warning-text" : "bg-accent"
|
|
88
|
+
}) : null]
|
|
89
|
+
}),
|
|
90
|
+
/* @__PURE__ */ _("div", {
|
|
60
91
|
className: "mt-4 flex items-center justify-between gap-3 border-t border-border-seam pt-3",
|
|
61
|
-
children: [/* @__PURE__ */
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
" attending"
|
|
69
|
-
]
|
|
70
|
-
}), /* @__PURE__ */ f("div", {
|
|
92
|
+
children: [D && x.joinUrl ? /* @__PURE__ */ _("a", {
|
|
93
|
+
href: x.joinUrl,
|
|
94
|
+
target: "_blank",
|
|
95
|
+
rel: "noreferrer noopener",
|
|
96
|
+
className: "inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline",
|
|
97
|
+
children: ["Join live ", /* @__PURE__ */ g(f, { className: "h-3 w-3" })]
|
|
98
|
+
}) : /* @__PURE__ */ g("span", {}), /* @__PURE__ */ g("div", {
|
|
71
99
|
className: "flex items-center gap-2",
|
|
72
|
-
children:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
children: e.label
|
|
84
|
-
}, e.status))]
|
|
100
|
+
children: E && b.map((e) => {
|
|
101
|
+
let t = w === e.status;
|
|
102
|
+
return /* @__PURE__ */ _(v, {
|
|
103
|
+
size: "sm",
|
|
104
|
+
variant: t || e.status === "GOING" ? "default" : "outline",
|
|
105
|
+
onClick: () => O(e.status),
|
|
106
|
+
disabled: C.isPending,
|
|
107
|
+
"aria-pressed": t,
|
|
108
|
+
children: [t && /* @__PURE__ */ g(d, { className: "mr-1 h-3.5 w-3.5" }), e.label]
|
|
109
|
+
}, e.status);
|
|
110
|
+
})
|
|
85
111
|
})]
|
|
86
112
|
})
|
|
87
113
|
]
|
|
88
114
|
});
|
|
89
115
|
}
|
|
90
116
|
//#endregion
|
|
91
|
-
export {
|
|
117
|
+
export { x as EventCard };
|
|
92
118
|
|
|
93
119
|
//# sourceMappingURL=EventCard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EventCard.js","names":[],"sources":["../../../src/comradecircle/components/EventCard.tsx"],"sourcesContent":["/**\n * EventCard — a tile for a community event with a quick RSVP control.\n * @module comradecircle/components\n *\n * Writes `comRsvpEvent`. Shows mode + lifecycle status,
|
|
1
|
+
{"version":3,"file":"EventCard.js","names":[],"sources":["../../../src/comradecircle/components/EventCard.tsx"],"sourcesContent":["/**\n * EventCard — a tile for a community event with a quick RSVP control.\n * @module comradecircle/components\n *\n * Writes `comRsvpEvent`. Shows mode + lifecycle status, a relative-time cue, a\n * capacity fill bar, the caller's selected RSVP (tracked client-side after a\n * successful mutation — the list query returns no viewer RSVP), and a live-join\n * link. LIVE events get an accent ring so they pop out of the grid.\n */\n\nimport { useState } from 'react';\nimport { Calendar, MapPin, Video, Users, ExternalLink, Check } from 'lucide-react';\nimport { GlassCard, Button } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { MiniBar } from './MiniBar';\nimport { StatusBadge } from './StatusBadge';\nimport { useComRsvpEvent } from '../hooks';\nimport { formatDateTime, formatNumber, formatRelativeTime } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComEvent, ComRsvpStatus } from '../types';\n\ninterface EventCardProps {\n event: ComEvent;\n onOpen: () => void;\n}\n\nconst RSVP_ACTIONS: { status: ComRsvpStatus; label: string }[] = [\n { status: 'GOING', label: 'Going' },\n { status: 'INTERESTED', label: 'Interested' },\n];\n\nexport function EventCard({ event, onOpen }: EventCardProps) {\n const rsvp = useComRsvpEvent();\n const [myStatus, setMyStatus] = useState<ComRsvpStatus | null>(null);\n const canRsvp = event.status === 'SCHEDULED' || event.status === 'LIVE';\n const isLive = event.status === 'LIVE';\n\n const doRsvp = async (status: ComRsvpStatus) => {\n try {\n await rsvp.mutateAsync({ eventId: event.id, status });\n setMyStatus(status);\n notify.success(`RSVP: ${status === 'GOING' ? 'Going' : 'Interested'}`, event.title);\n } catch (err) {\n notify.error('Could not RSVP', describeError(err));\n }\n };\n\n const ModeIcon = event.mode === 'IN_PERSON' ? MapPin : Video;\n const fillPct = event.capacity ? Math.min(100, (event.rsvpCount / event.capacity) * 100) : 0;\n const nearFull = fillPct >= 80;\n\n return (\n <GlassCard\n treatment=\"plain\"\n className={cn(\n 'flex flex-col p-5 transition-all duration-200 motion-safe:hover:-translate-y-0.5 hover:border-[var(--color-accent-soft)]',\n isLive && 'ring-1 ring-[var(--color-accent-soft)]'\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <StatusBadge status={event.status} />\n {canRsvp && (\n <span className=\"text-xs font-medium text-text-tertiary\">\n {formatRelativeTime(event.startsAt)}\n </span>\n )}\n </div>\n\n <button\n type=\"button\"\n onClick={onOpen}\n className=\"mt-2 text-left text-base font-semibold text-text-primary transition-colors hover:text-accent\"\n >\n {event.title}\n </button>\n\n {event.description && (\n <p className=\"mt-1.5 line-clamp-2 text-sm text-text-secondary\">{event.description}</p>\n )}\n\n <div className=\"mt-3 flex flex-col gap-1.5 text-sm text-text-secondary\">\n <span className=\"inline-flex items-center gap-2\">\n <Calendar className=\"h-4 w-4 text-text-tertiary\" /> {formatDateTime(event.startsAt)}\n </span>\n <span className=\"inline-flex items-center gap-2\">\n <ModeIcon className=\"h-4 w-4 text-text-tertiary\" />\n {event.mode === 'IN_PERSON'\n ? event.location || 'In person'\n : event.mode === 'HYBRID'\n ? `Hybrid${event.location ? ` · ${event.location}` : ''}`\n : 'Online'}\n </span>\n </div>\n\n <div className=\"mt-3\">\n <div className=\"flex items-center justify-between text-xs text-text-tertiary\">\n <span className=\"inline-flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" /> {formatNumber(event.rsvpCount)}\n {event.capacity ? ` / ${formatNumber(event.capacity)}` : ''} attending\n </span>\n {event.capacity && nearFull && (\n <span\n className={cn(fillPct >= 100 ? 'text-status-error-text' : 'text-status-warning-text')}\n >\n {fillPct >= 100 ? 'Full' : `${Math.round(fillPct)}% full`}\n </span>\n )}\n </div>\n {event.capacity ? (\n <MiniBar\n value={fillPct}\n className=\"mt-1.5\"\n height={4}\n barClassName={\n fillPct >= 100\n ? 'bg-status-error-text'\n : nearFull\n ? 'bg-status-warning-text'\n : 'bg-accent'\n }\n />\n ) : null}\n </div>\n\n <div className=\"mt-4 flex items-center justify-between gap-3 border-t border-border-seam pt-3\">\n {isLive && event.joinUrl ? (\n <a\n href={event.joinUrl}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline\"\n >\n Join live <ExternalLink className=\"h-3 w-3\" />\n </a>\n ) : (\n <span />\n )}\n <div className=\"flex items-center gap-2\">\n {canRsvp &&\n RSVP_ACTIONS.map((a) => {\n const active = myStatus === a.status;\n return (\n <Button\n key={a.status}\n size=\"sm\"\n variant={active || a.status === 'GOING' ? 'default' : 'outline'}\n onClick={() => doRsvp(a.status)}\n disabled={rsvp.isPending}\n aria-pressed={active}\n >\n {active && <Check className=\"mr-1 h-3.5 w-3.5\" />}\n {a.label}\n </Button>\n );\n })}\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -17,6 +17,7 @@ var s = ({ memberId: s, variant: c = "inline", size: l = "sm", suffix: u, showPr
|
|
|
17
17
|
className: t("flex items-center gap-2.5 text-left", f && "rounded-lg transition-colors hover:opacity-80", p),
|
|
18
18
|
children: [/* @__PURE__ */ i(r, {
|
|
19
19
|
name: v,
|
|
20
|
+
seed: s,
|
|
20
21
|
size: l,
|
|
21
22
|
presence: y,
|
|
22
23
|
fileId: g?.avatar?.id
|
|
@@ -27,7 +28,7 @@ var s = ({ memberId: s, variant: c = "inline", size: l = "sm", suffix: u, showPr
|
|
|
27
28
|
children: [/* @__PURE__ */ i("span", {
|
|
28
29
|
className: "truncate text-sm font-medium text-text-primary",
|
|
29
30
|
children: _
|
|
30
|
-
}), u &&
|
|
31
|
+
}), u && /* @__PURE__ */ a("span", {
|
|
31
32
|
className: "shrink-0 text-xs text-text-tertiary",
|
|
32
33
|
children: ["· ", u]
|
|
33
34
|
})]
|
|
@@ -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
|
|
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\n name={nameForAvatar}\n seed={memberId}\n size={size}\n presence={presence}\n fileId={profile?.avatar?.id}\n />\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 && <span className=\"shrink-0 text-xs text-text-tertiary\">· {suffix}</span>}\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;GACE,MAAM;GACN,MAAM;GACA;GACI;GACV,QAAQ,GAAS,QAAQ;GACzB,CAAA,EACF,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAAkD;KAAY,CAAA,EAC7E,KAAU,kBAAC,QAAD;KAAM,WAAU;eAAhB,CAAsD,MAAG,EAAc;OAC9E;OACL,MAAY,SAAS,GAAS,YAC7B,kBAAC,KAAD;IAAG,WAAU;cAAwC,EAAQ;IAAa,CAAA,CAExE;KACE"}
|
|
@@ -5,54 +5,69 @@ import { describeError as r, notify as i } from "../lib/toast.js";
|
|
|
5
5
|
import { formatNumber as a, formatRelativeTime as o } from "../utils/formatters.js";
|
|
6
6
|
import { StatusBadge as s } from "./StatusBadge.js";
|
|
7
7
|
import { MiniBar as c } from "./MiniBar.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { useMemo as l, useState as u } from "react";
|
|
9
|
+
import { CheckCircle2 as d, Crown as f, Lock as p } from "lucide-react";
|
|
10
|
+
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
11
|
+
import { Button as g, GlassCard as _ } from "@burdenoff/fe-libs/ui";
|
|
11
12
|
//#region src/comradecircle/components/PollCard.tsx
|
|
12
|
-
function
|
|
13
|
-
let
|
|
14
|
-
if (!
|
|
15
|
-
await
|
|
16
|
-
pollId:
|
|
13
|
+
function v({ poll: v, canManage: y = !1 }) {
|
|
14
|
+
let b = n(), x = t(), [S, C] = u(/* @__PURE__ */ new Set()), w = v.options.reduce((e, t) => e + t.voteCount, 0), T = v.status === "CLOSED", E = l(() => v.options.reduce((e, t) => Math.max(e, t.voteCount), 0), [v.options]), D = S.size > 0, O = async (e) => {
|
|
15
|
+
if (!T) try {
|
|
16
|
+
await b.mutateAsync({
|
|
17
|
+
pollId: v.id,
|
|
17
18
|
optionId: e,
|
|
18
|
-
circleId:
|
|
19
|
-
}), i.success("Vote recorded");
|
|
19
|
+
circleId: v.circleId
|
|
20
|
+
}), C((t) => new Set(t).add(e)), i.success("Vote recorded");
|
|
20
21
|
} catch (e) {
|
|
21
22
|
i.error("Could not vote", r(e));
|
|
22
23
|
}
|
|
23
|
-
},
|
|
24
|
+
}, k = async () => {
|
|
24
25
|
try {
|
|
25
|
-
await
|
|
26
|
+
await x.mutateAsync(v.id), i.success("Poll closed");
|
|
26
27
|
} catch (e) {
|
|
27
28
|
i.error("Could not close poll", r(e));
|
|
28
29
|
}
|
|
29
30
|
};
|
|
30
|
-
return /* @__PURE__ */
|
|
31
|
+
return /* @__PURE__ */ h(_, {
|
|
31
32
|
treatment: "plain",
|
|
32
33
|
className: "p-5",
|
|
33
34
|
children: [
|
|
34
|
-
/* @__PURE__ */
|
|
35
|
+
/* @__PURE__ */ h("div", {
|
|
35
36
|
className: "flex items-start justify-between gap-3",
|
|
36
|
-
children: [/* @__PURE__ */
|
|
37
|
+
children: [/* @__PURE__ */ m("h3", {
|
|
37
38
|
className: "text-base font-semibold text-text-primary",
|
|
38
|
-
children:
|
|
39
|
-
}), /* @__PURE__ */
|
|
39
|
+
children: v.question
|
|
40
|
+
}), /* @__PURE__ */ m(s, { status: v.status })]
|
|
40
41
|
}),
|
|
41
|
-
/* @__PURE__ */
|
|
42
|
+
/* @__PURE__ */ m("div", {
|
|
42
43
|
className: "mt-4 flex flex-col gap-2.5",
|
|
43
|
-
children: [...
|
|
44
|
-
let n =
|
|
45
|
-
return /* @__PURE__ */
|
|
44
|
+
children: [...v.options].sort((e, t) => e.position - t.position).map((t) => {
|
|
45
|
+
let n = w > 0 ? t.voteCount / w * 100 : 0, r = w > 0 && t.voteCount === E, i = S.has(t.id);
|
|
46
|
+
return /* @__PURE__ */ h("button", {
|
|
46
47
|
type: "button",
|
|
47
|
-
onClick: () =>
|
|
48
|
-
disabled:
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
onClick: () => O(t.id),
|
|
49
|
+
disabled: T || b.isPending,
|
|
50
|
+
"aria-pressed": i,
|
|
51
|
+
className: e("group w-full rounded-xl border px-3 py-2.5 text-left transition-all duration-200", i ? "border-accent bg-[var(--color-accent-soft)]/40" : "border-border-seam", !T && !i && "hover:-translate-y-px hover:border-accent hover:bg-bg-sunken/60", T && "cursor-default"),
|
|
52
|
+
children: [/* @__PURE__ */ h("div", {
|
|
51
53
|
className: "flex items-center justify-between gap-2",
|
|
52
|
-
children: [/* @__PURE__ */
|
|
53
|
-
className: "
|
|
54
|
-
children:
|
|
55
|
-
|
|
54
|
+
children: [/* @__PURE__ */ h("span", {
|
|
55
|
+
className: "flex min-w-0 items-center gap-1.5",
|
|
56
|
+
children: [
|
|
57
|
+
i && /* @__PURE__ */ m(d, {
|
|
58
|
+
className: "h-4 w-4 shrink-0 text-accent",
|
|
59
|
+
"aria-hidden": "true"
|
|
60
|
+
}),
|
|
61
|
+
/* @__PURE__ */ m("span", {
|
|
62
|
+
className: e("truncate text-sm", r ? "font-semibold text-text-primary" : "font-medium text-text-secondary"),
|
|
63
|
+
children: t.label
|
|
64
|
+
}),
|
|
65
|
+
r && w > 1 && /* @__PURE__ */ m(f, {
|
|
66
|
+
className: "h-3.5 w-3.5 shrink-0 text-status-warning-text",
|
|
67
|
+
"aria-label": "Leading"
|
|
68
|
+
})
|
|
69
|
+
]
|
|
70
|
+
}), /* @__PURE__ */ h("span", {
|
|
56
71
|
className: "shrink-0 text-xs tabular-nums text-text-secondary",
|
|
57
72
|
children: [
|
|
58
73
|
a(t.voteCount),
|
|
@@ -61,37 +76,43 @@ function h({ poll: h, canManage: g = !1 }) {
|
|
|
61
76
|
"%"
|
|
62
77
|
]
|
|
63
78
|
})]
|
|
64
|
-
}), /* @__PURE__ */
|
|
79
|
+
}), /* @__PURE__ */ m(c, {
|
|
65
80
|
value: n,
|
|
66
|
-
className: "mt-2"
|
|
81
|
+
className: "mt-2",
|
|
82
|
+
barClassName: r ? "bg-gradient-to-r from-accent to-[var(--color-accent-soft)]" : "bg-accent/40"
|
|
67
83
|
})]
|
|
68
84
|
}, t.id);
|
|
69
85
|
})
|
|
70
86
|
}),
|
|
71
|
-
/* @__PURE__ */
|
|
87
|
+
/* @__PURE__ */ h("div", {
|
|
72
88
|
className: "mt-4 flex items-center justify-between gap-3 border-t border-border-seam pt-3 text-xs text-text-tertiary",
|
|
73
|
-
children: [/* @__PURE__ */
|
|
74
|
-
className: "inline-flex items-center gap-1.5",
|
|
89
|
+
children: [/* @__PURE__ */ h("span", {
|
|
90
|
+
className: "inline-flex flex-wrap items-center gap-1.5",
|
|
75
91
|
children: [
|
|
76
|
-
|
|
77
|
-
a(
|
|
92
|
+
v.allowMultiple ? /* @__PURE__ */ m(d, { className: "h-3.5 w-3.5" }) : null,
|
|
93
|
+
a(w),
|
|
78
94
|
" vote",
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
w === 1 ? "" : "s",
|
|
96
|
+
v.allowMultiple ? " · multiple choice" : "",
|
|
97
|
+
v.isAnonymous ? " · anonymous" : "",
|
|
98
|
+
v.closesAt ? ` · closes ${o(v.closesAt)}` : "",
|
|
99
|
+
D && /* @__PURE__ */ m("span", {
|
|
100
|
+
className: "font-medium text-accent",
|
|
101
|
+
children: "· You voted"
|
|
102
|
+
})
|
|
82
103
|
]
|
|
83
|
-
}),
|
|
104
|
+
}), y && !T && /* @__PURE__ */ h(g, {
|
|
84
105
|
size: "sm",
|
|
85
106
|
variant: "outline",
|
|
86
|
-
onClick:
|
|
87
|
-
disabled:
|
|
88
|
-
children: [/* @__PURE__ */
|
|
107
|
+
onClick: k,
|
|
108
|
+
disabled: x.isPending,
|
|
109
|
+
children: [/* @__PURE__ */ m(p, { className: "mr-1.5 h-3.5 w-3.5" }), " Close"]
|
|
89
110
|
})]
|
|
90
111
|
})
|
|
91
112
|
]
|
|
92
113
|
});
|
|
93
114
|
}
|
|
94
115
|
//#endregion
|
|
95
|
-
export {
|
|
116
|
+
export { v as PollCard };
|
|
96
117
|
|
|
97
118
|
//# sourceMappingURL=PollCard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PollCard.js","names":[],"sources":["../../../src/comradecircle/components/PollCard.tsx"],"sourcesContent":["/**\n * PollCard — a circle poll with vote + live results.\n * @module comradecircle/components\n *\n * Writes `comVotePoll(pollId, optionId)` and `comClosePoll`. Options render as\n * clickable result bars;
|
|
1
|
+
{"version":3,"file":"PollCard.js","names":[],"sources":["../../../src/comradecircle/components/PollCard.tsx"],"sourcesContent":["/**\n * PollCard — a circle poll with vote + live results.\n * @module comradecircle/components\n *\n * Writes `comVotePoll(pollId, optionId)` and `comClosePoll`. Options render as\n * clickable result bars; the leading option is emphasised with an accent\n * gradient fill and the caller's own voted option(s) are checkmarked (tracked\n * client-side — the poll query returns aggregate counts, not per-member votes).\n * A closed poll (or one past its close time) is read-only.\n */\n\nimport { useMemo, useState } from 'react';\nimport { CheckCircle2, Lock, Crown } from 'lucide-react';\nimport { GlassCard, Button } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { MiniBar } from './MiniBar';\nimport { StatusBadge } from './StatusBadge';\nimport { useComVotePoll, useComClosePoll } from '../hooks';\nimport { formatNumber, formatRelativeTime } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComPoll } from '../types';\n\ninterface PollCardProps {\n poll: ComPoll;\n /** Whether the caller may close the poll (creator / circle admin). */\n canManage?: boolean;\n}\n\nexport function PollCard({ poll, canManage = false }: PollCardProps) {\n const vote = useComVotePoll();\n const close = useComClosePoll();\n // The poll query returns aggregate voteCounts, not per-member votes, so track\n // the options this caller has voted for in this session for the \"You voted\" cue.\n const [votedIds, setVotedIds] = useState<Set<string>>(new Set());\n\n const totalVotes = poll.options.reduce((sum, o) => sum + o.voteCount, 0);\n const isClosed = poll.status === 'CLOSED';\n const leadingCount = useMemo(\n () => poll.options.reduce((max, o) => Math.max(max, o.voteCount), 0),\n [poll.options]\n );\n const hasVoted = votedIds.size > 0;\n\n const doVote = async (optionId: string) => {\n if (isClosed) return;\n try {\n await vote.mutateAsync({ pollId: poll.id, optionId, circleId: poll.circleId });\n setVotedIds((prev) => new Set(prev).add(optionId));\n notify.success('Vote recorded');\n } catch (err) {\n notify.error('Could not vote', describeError(err));\n }\n };\n\n const doClose = async () => {\n try {\n await close.mutateAsync(poll.id);\n notify.success('Poll closed');\n } catch (err) {\n notify.error('Could not close poll', describeError(err));\n }\n };\n\n return (\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <h3 className=\"text-base font-semibold text-text-primary\">{poll.question}</h3>\n <StatusBadge status={poll.status} />\n </div>\n\n <div className=\"mt-4 flex flex-col gap-2.5\">\n {[...poll.options]\n .sort((a, b) => a.position - b.position)\n .map((opt) => {\n const pct = totalVotes > 0 ? (opt.voteCount / totalVotes) * 100 : 0;\n const isLeading = totalVotes > 0 && opt.voteCount === leadingCount;\n const isMine = votedIds.has(opt.id);\n return (\n <button\n key={opt.id}\n type=\"button\"\n onClick={() => doVote(opt.id)}\n disabled={isClosed || vote.isPending}\n aria-pressed={isMine}\n className={cn(\n 'group w-full rounded-xl border px-3 py-2.5 text-left transition-all duration-200',\n isMine ? 'border-accent bg-[var(--color-accent-soft)]/40' : 'border-border-seam',\n !isClosed &&\n !isMine &&\n 'hover:-translate-y-px hover:border-accent hover:bg-bg-sunken/60',\n isClosed && 'cursor-default'\n )}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex min-w-0 items-center gap-1.5\">\n {isMine && (\n <CheckCircle2 className=\"h-4 w-4 shrink-0 text-accent\" aria-hidden=\"true\" />\n )}\n <span\n className={cn(\n 'truncate text-sm',\n isLeading\n ? 'font-semibold text-text-primary'\n : 'font-medium text-text-secondary'\n )}\n >\n {opt.label}\n </span>\n {isLeading && totalVotes > 1 && (\n <Crown\n className=\"h-3.5 w-3.5 shrink-0 text-status-warning-text\"\n aria-label=\"Leading\"\n />\n )}\n </span>\n <span className=\"shrink-0 text-xs tabular-nums text-text-secondary\">\n {formatNumber(opt.voteCount)} · {Math.round(pct)}%\n </span>\n </div>\n <MiniBar\n value={pct}\n className=\"mt-2\"\n barClassName={\n isLeading\n ? 'bg-gradient-to-r from-accent to-[var(--color-accent-soft)]'\n : 'bg-accent/40'\n }\n />\n </button>\n );\n })}\n </div>\n\n <div className=\"mt-4 flex items-center justify-between gap-3 border-t border-border-seam pt-3 text-xs text-text-tertiary\">\n <span className=\"inline-flex flex-wrap items-center gap-1.5\">\n {poll.allowMultiple ? <CheckCircle2 className=\"h-3.5 w-3.5\" /> : null}\n {formatNumber(totalVotes)} vote{totalVotes === 1 ? '' : 's'}\n {poll.allowMultiple ? ' · multiple choice' : ''}\n {poll.isAnonymous ? ' · anonymous' : ''}\n {poll.closesAt ? ` · closes ${formatRelativeTime(poll.closesAt)}` : ''}\n {hasVoted && <span className=\"font-medium text-accent\">· You voted</span>}\n </span>\n {canManage && !isClosed && (\n <Button size=\"sm\" variant=\"outline\" onClick={doClose} disabled={close.isPending}>\n <Lock className=\"mr-1.5 h-3.5 w-3.5\" /> Close\n </Button>\n )}\n </div>\n </GlassCard>\n );\n}\n"],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ function D({ post: D, defaultExpanded: O = !1 }) {
|
|
|
18
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);
|
|
19
19
|
return /* @__PURE__ */ v(T, {
|
|
20
20
|
treatment: "plain",
|
|
21
|
-
className: t("p-4", D.isPinned && "ring-1 ring-accent/40"),
|
|
21
|
+
className: t("p-4 transition-all duration-200 motion-safe:hover:-translate-y-0.5 hover:border-[var(--color-accent-soft)]", D.isPinned && "ring-1 ring-accent/40"),
|
|
22
22
|
children: [
|
|
23
23
|
/* @__PURE__ */ v("div", {
|
|
24
24
|
className: "flex items-start justify-between gap-3",
|
|
@@ -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 } 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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"}
|
|
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\n treatment=\"plain\"\n className={cn(\n 'p-4 transition-all duration-200 motion-safe:hover:-translate-y-0.5 hover:border-[var(--color-accent-soft)]',\n post.isPinned && 'ring-1 ring-accent/40'\n )}\n >\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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"}
|
|
@@ -71,7 +71,7 @@ function p({ post: p }) {
|
|
|
71
71
|
disabled: h.isPending,
|
|
72
72
|
"aria-pressed": n.mine,
|
|
73
73
|
"aria-label": `${e.label} (${n.count})`,
|
|
74
|
-
className: t("inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium transition-
|
|
74
|
+
className: t("inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium transition-transform duration-150 active:scale-90", n.mine ? "border-accent bg-[var(--color-accent-soft)] text-text-primary shadow-[0_0_0_2px_var(--color-accent-soft)]" : "border-border-seam bg-bg-surface text-text-secondary hover:-translate-y-px hover:bg-bg-sunken"),
|
|
75
75
|
children: [/* @__PURE__ */ s("span", {
|
|
76
76
|
"aria-hidden": "true",
|
|
77
77
|
children: e.emoji
|