@burdenoff/microfe-comradecircle 2026.819.1 → 2026.827.1

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.
Files changed (25) hide show
  1. package/dist/comradecircle/components/DraftsPanel.d.ts +32 -0
  2. package/dist/comradecircle/components/DraftsPanel.d.ts.map +1 -0
  3. package/dist/comradecircle/components/DraftsPanel.js +176 -0
  4. package/dist/comradecircle/components/DraftsPanel.js.map +1 -0
  5. package/dist/comradecircle/components/PostComposer.d.ts +11 -5
  6. package/dist/comradecircle/components/PostComposer.d.ts.map +1 -1
  7. package/dist/comradecircle/components/PostComposer.js +58 -46
  8. package/dist/comradecircle/components/PostComposer.js.map +1 -1
  9. package/dist/comradecircle/components/UpcomingEvents.d.ts +26 -0
  10. package/dist/comradecircle/components/UpcomingEvents.d.ts.map +1 -0
  11. package/dist/comradecircle/components/UpcomingEvents.js +126 -0
  12. package/dist/comradecircle/components/UpcomingEvents.js.map +1 -0
  13. package/dist/comradecircle/components/index.d.ts +2 -0
  14. package/dist/comradecircle/components/index.d.ts.map +1 -1
  15. package/dist/comradecircle/pages/FeedPage.d.ts +5 -0
  16. package/dist/comradecircle/pages/FeedPage.d.ts.map +1 -1
  17. package/dist/comradecircle/pages/FeedPage.js +54 -53
  18. package/dist/comradecircle/pages/FeedPage.js.map +1 -1
  19. package/dist/comradecircle/pages/circles/CircleDetailPage.d.ts.map +1 -1
  20. package/dist/comradecircle/pages/circles/CircleDetailPage.js +244 -237
  21. package/dist/comradecircle/pages/circles/CircleDetailPage.js.map +1 -1
  22. package/dist/comradecircle/utils/pageContext.d.ts.map +1 -1
  23. package/dist/comradecircle/utils/pageContext.js +3 -1
  24. package/dist/comradecircle/utils/pageContext.js.map +1 -1
  25. package/package.json +1 -1
@@ -0,0 +1,32 @@
1
+ /**
2
+ * DraftsPanel — the caller's own unpublished posts in one channel.
3
+ * @module comradecircle/components
4
+ *
5
+ * The composer can store a post as `status: DRAFT` (`ComCreatePostInput.status`);
6
+ * this is where those drafts come back. It reads
7
+ * `comListPosts(channelId, status: DRAFT)` and writes
8
+ * `comUpdatePost(id, { body })` (edit) / `comUpdatePost(id, { status: PUBLISHED })`
9
+ * (publish) / `comRemovePost(id)` (discard) — every one an existing `com*`
10
+ * capability, and `comUpdatePost`'s server-side gate already accepts the post's
11
+ * own author (`requirePostModeration` returns early on `authorId === actorId`).
12
+ *
13
+ * Scoping note: `comListPosts` filters by channel + status only — it does NOT
14
+ * scope drafts to their author — so the returned page can contain other
15
+ * members' drafts. This panel therefore renders ONLY rows whose `authorId`
16
+ * matches the calling member, and renders nothing at all when the shell has not
17
+ * supplied a current user (no identity ⇒ no way to tell whose draft is whose).
18
+ * The same bound means the panel is a "your recent drafts" view, not a
19
+ * guaranteed-complete one: it looks at one page of the channel's drafts, so it
20
+ * never claims a count it hasn't verified beyond that window.
21
+ */
22
+ interface DraftsPanelProps {
23
+ channelId: string;
24
+ /** Whether the drafts list is expanded (controlled by the channel feed). */
25
+ open: boolean;
26
+ onOpenChange: (open: boolean) => void;
27
+ /** Announcement channels label the publish action "Broadcast". */
28
+ variant?: 'default' | 'announcement';
29
+ }
30
+ export declare function DraftsPanel({ channelId, open, onOpenChange, variant, }: DraftsPanelProps): import("react/jsx-runtime").JSX.Element | null;
31
+ export {};
32
+ //# sourceMappingURL=DraftsPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DraftsPanel.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/components/DraftsPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAmBH,UAAU,gBAAgB;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,kEAAkE;IAClE,OAAO,CAAC,EAAE,SAAS,GAAG,cAAc,CAAC;CACtC;AAED,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,IAAI,EACJ,YAAY,EACZ,OAAmB,GACpB,EAAE,gBAAgB,kDAuElB"}
@@ -0,0 +1,176 @@
1
+ import { useComradeCircle as e } from "../context/ComradeCircleContext.js";
2
+ import "../context/index.js";
3
+ import { cn as t } from "../utils/cn.js";
4
+ import { useComPosts as n, useComRemovePost as r, useComUpdatePost as i } from "../hooks/useComradeCircleApi.js";
5
+ import "../hooks/index.js";
6
+ import { describeError as a, notify as o } from "../lib/toast.js";
7
+ import { formatNumber as s, formatRelativeTime as c } from "../utils/formatters.js";
8
+ import { useMemo as l, useState as u } from "react";
9
+ import { ChevronDown as d, FileText as f, Megaphone as p, Paperclip as m, Send as h, Trash2 as g } from "lucide-react";
10
+ import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
11
+ import { Button as b, Textarea as x } from "@burdenoff/fe-libs/ui";
12
+ //#region src/comradecircle/components/DraftsPanel.tsx
13
+ var S = 50;
14
+ function C({ channelId: r, open: i, onOpenChange: a, variant: o = "default" }) {
15
+ let { currentUser: c } = e(), u = c?.id, { data: p, isLoading: m, isError: h, refetch: g } = n({
16
+ channelId: r,
17
+ status: "DRAFT",
18
+ pagination: { limit: S }
19
+ }), _ = l(() => u ? (p?.items ?? []).filter((e) => e.authorId === u) : [], [p, u]);
20
+ return u ? h ? /* @__PURE__ */ y("div", {
21
+ className: "flex flex-wrap items-center justify-between gap-2 rounded-xl border border-dashed border-border-seam px-4 py-2.5 text-sm text-text-secondary",
22
+ children: [/* @__PURE__ */ y("span", {
23
+ className: "inline-flex items-center gap-2",
24
+ children: [/* @__PURE__ */ v(f, { className: "h-4 w-4 text-text-tertiary" }), " We couldn't load your drafts."]
25
+ }), /* @__PURE__ */ v(b, {
26
+ size: "sm",
27
+ variant: "outline",
28
+ onClick: () => void g(),
29
+ children: "Retry"
30
+ })]
31
+ }) : m || _.length === 0 ? null : /* @__PURE__ */ y("div", {
32
+ className: "overflow-hidden rounded-xl border border-border-seam bg-bg-surface",
33
+ children: [/* @__PURE__ */ y("button", {
34
+ type: "button",
35
+ onClick: () => a(!i),
36
+ "aria-expanded": i,
37
+ className: "flex w-full items-center justify-between gap-3 px-4 py-2.5 text-left transition-colors hover:bg-bg-sunken",
38
+ children: [/* @__PURE__ */ y("span", {
39
+ className: "inline-flex items-center gap-2 text-sm font-medium text-text-primary",
40
+ children: [
41
+ /* @__PURE__ */ v(f, { className: "h-4 w-4 text-text-tertiary" }),
42
+ "Your drafts",
43
+ /* @__PURE__ */ v("span", {
44
+ className: "rounded-full bg-bg-sunken px-2 py-0.5 text-xs tabular-nums text-text-secondary",
45
+ children: s(_.length)
46
+ })
47
+ ]
48
+ }), /* @__PURE__ */ v(d, {
49
+ className: t("h-4 w-4 shrink-0 text-text-tertiary transition-transform", i && "rotate-180"),
50
+ "aria-hidden": "true"
51
+ })]
52
+ }), i && /* @__PURE__ */ v("ul", {
53
+ className: "border-t border-border-seam",
54
+ children: _.map((e) => /* @__PURE__ */ v("li", {
55
+ className: "border-b border-border-seam last:border-b-0",
56
+ children: /* @__PURE__ */ v(w, {
57
+ draft: e,
58
+ variant: o
59
+ })
60
+ }, e.id))
61
+ })]
62
+ }) : null;
63
+ }
64
+ function w({ draft: e, variant: t }) {
65
+ let n = i(), l = r(), [d, f] = u(!1), [S, C] = u(e.body), w = t === "announcement", T = n.isPending || l.isPending, E = async () => {
66
+ let t = S.trim();
67
+ if (!t || t === e.body) {
68
+ C(e.body), f(!1);
69
+ return;
70
+ }
71
+ try {
72
+ await n.mutateAsync({
73
+ id: e.id,
74
+ input: { body: t }
75
+ }), f(!1), o.success("Draft updated");
76
+ } catch (e) {
77
+ o.error("Could not update draft", a(e));
78
+ }
79
+ }, D = async () => {
80
+ try {
81
+ await n.mutateAsync({
82
+ id: e.id,
83
+ input: { status: "PUBLISHED" }
84
+ }), o.success(w ? "Announcement published" : "Draft published");
85
+ } catch (e) {
86
+ o.error("Could not publish draft", a(e));
87
+ }
88
+ }, O = async () => {
89
+ try {
90
+ await l.mutateAsync({
91
+ id: e.id,
92
+ channelId: e.channelId
93
+ }), o.success("Draft discarded");
94
+ } catch (e) {
95
+ o.error("Could not discard draft", a(e));
96
+ }
97
+ };
98
+ return /* @__PURE__ */ v("div", {
99
+ className: "px-4 py-3",
100
+ children: d ? /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ v(x, {
101
+ value: S,
102
+ onChange: (e) => C(e.target.value),
103
+ rows: 3,
104
+ className: "resize-none",
105
+ "aria-label": "Draft body"
106
+ }), /* @__PURE__ */ y("div", {
107
+ className: "mt-2 flex justify-end gap-2",
108
+ children: [/* @__PURE__ */ v(b, {
109
+ size: "sm",
110
+ variant: "outline",
111
+ onClick: () => {
112
+ C(e.body), f(!1);
113
+ },
114
+ children: "Cancel"
115
+ }), /* @__PURE__ */ v(b, {
116
+ size: "sm",
117
+ onClick: () => void E(),
118
+ disabled: n.isPending,
119
+ children: "Save"
120
+ })]
121
+ })] }) : /* @__PURE__ */ y(_, { children: [
122
+ /* @__PURE__ */ v("p", {
123
+ className: "line-clamp-3 whitespace-pre-wrap break-words text-sm text-text-primary",
124
+ children: e.body || "Empty draft"
125
+ }),
126
+ /* @__PURE__ */ y("div", {
127
+ className: "mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-tertiary",
128
+ children: [
129
+ /* @__PURE__ */ y("span", { children: ["Saved ", c(e.updatedAt)] }),
130
+ e.attachmentFileIds.length > 0 && /* @__PURE__ */ y("span", {
131
+ className: "inline-flex items-center gap-1",
132
+ children: [
133
+ /* @__PURE__ */ v(m, { className: "h-3 w-3" }),
134
+ s(e.attachmentFileIds.length),
135
+ e.attachmentFileIds.length === 1 ? " attachment" : " attachments"
136
+ ]
137
+ }),
138
+ e.tags.map((e) => /* @__PURE__ */ y("span", {
139
+ className: "text-text-secondary",
140
+ children: ["#", e]
141
+ }, e))
142
+ ]
143
+ }),
144
+ /* @__PURE__ */ y("div", {
145
+ className: "mt-2.5 flex flex-wrap items-center justify-end gap-2",
146
+ children: [
147
+ /* @__PURE__ */ y(b, {
148
+ size: "sm",
149
+ variant: "ghost",
150
+ onClick: () => void O(),
151
+ disabled: T,
152
+ className: "text-status-error-text hover:text-status-error-text",
153
+ children: [/* @__PURE__ */ v(g, { className: "mr-1.5 h-3.5 w-3.5" }), " Discard"]
154
+ }),
155
+ /* @__PURE__ */ v(b, {
156
+ size: "sm",
157
+ variant: "outline",
158
+ onClick: () => f(!0),
159
+ disabled: T,
160
+ children: "Edit"
161
+ }),
162
+ /* @__PURE__ */ v(b, {
163
+ size: "sm",
164
+ onClick: () => void D(),
165
+ disabled: T,
166
+ children: w ? /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ v(p, { className: "mr-1.5 h-3.5 w-3.5" }), " Broadcast"] }) : /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ v(h, { className: "mr-1.5 h-3.5 w-3.5" }), " Publish"] })
167
+ })
168
+ ]
169
+ })
170
+ ] })
171
+ });
172
+ }
173
+ //#endregion
174
+ export { C as DraftsPanel };
175
+
176
+ //# sourceMappingURL=DraftsPanel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DraftsPanel.js","names":[],"sources":["../../../src/comradecircle/components/DraftsPanel.tsx"],"sourcesContent":["/**\n * DraftsPanel — the caller's own unpublished posts in one channel.\n * @module comradecircle/components\n *\n * The composer can store a post as `status: DRAFT` (`ComCreatePostInput.status`);\n * this is where those drafts come back. It reads\n * `comListPosts(channelId, status: DRAFT)` and writes\n * `comUpdatePost(id, { body })` (edit) / `comUpdatePost(id, { status: PUBLISHED })`\n * (publish) / `comRemovePost(id)` (discard) — every one an existing `com*`\n * capability, and `comUpdatePost`'s server-side gate already accepts the post's\n * own author (`requirePostModeration` returns early on `authorId === actorId`).\n *\n * Scoping note: `comListPosts` filters by channel + status only — it does NOT\n * scope drafts to their author — so the returned page can contain other\n * members' drafts. This panel therefore renders ONLY rows whose `authorId`\n * matches the calling member, and renders nothing at all when the shell has not\n * supplied a current user (no identity ⇒ no way to tell whose draft is whose).\n * The same bound means the panel is a \"your recent drafts\" view, not a\n * guaranteed-complete one: it looks at one page of the channel's drafts, so it\n * never claims a count it hasn't verified beyond that window.\n */\n\nimport { useMemo, useState } from 'react';\nimport { ChevronDown, FileText, Megaphone, Paperclip, Send, Trash2 } from 'lucide-react';\nimport { Button, Textarea } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport { useComPosts, useComRemovePost, useComUpdatePost } from '../hooks';\nimport { useComradeCircle } from '../context';\nimport { formatNumber, formatRelativeTime } from '../utils/formatters';\nimport { notify, describeError } from '../lib/toast';\nimport type { ComPost } from '../types';\n\n/**\n * One page of the channel's drafts. Deliberately larger than a visible list\n * needs to be: the page is shared with every other member's drafts and only the\n * caller's survive the filter below.\n */\nconst DRAFT_WINDOW = 50;\n\ninterface DraftsPanelProps {\n channelId: string;\n /** Whether the drafts list is expanded (controlled by the channel feed). */\n open: boolean;\n onOpenChange: (open: boolean) => void;\n /** Announcement channels label the publish action \"Broadcast\". */\n variant?: 'default' | 'announcement';\n}\n\nexport function DraftsPanel({\n channelId,\n open,\n onOpenChange,\n variant = 'default',\n}: DraftsPanelProps) {\n const { currentUser } = useComradeCircle();\n const meId = currentUser?.id;\n\n const { data, isLoading, isError, refetch } = useComPosts({\n channelId,\n status: 'DRAFT',\n pagination: { limit: DRAFT_WINDOW },\n });\n\n const drafts = useMemo(\n () => (meId ? (data?.items ?? []).filter((post) => post.authorId === meId) : []),\n [data, meId]\n );\n\n // No identity → the author filter above cannot run, so show nothing rather\n // than risk surfacing someone else's unpublished post.\n if (!meId) return null;\n\n if (isError) {\n return (\n <div className=\"flex flex-wrap items-center justify-between gap-2 rounded-xl border border-dashed border-border-seam px-4 py-2.5 text-sm text-text-secondary\">\n <span className=\"inline-flex items-center gap-2\">\n <FileText className=\"h-4 w-4 text-text-tertiary\" /> We couldn&apos;t load your drafts.\n </span>\n <Button size=\"sm\" variant=\"outline\" onClick={() => void refetch()}>\n Retry\n </Button>\n </div>\n );\n }\n\n // While the first page is in flight there is nothing truthful to announce —\n // the count is unknown — so the panel stays out of the layout entirely.\n if (isLoading || drafts.length === 0) return null;\n\n return (\n <div className=\"overflow-hidden rounded-xl border border-border-seam bg-bg-surface\">\n <button\n type=\"button\"\n onClick={() => onOpenChange(!open)}\n aria-expanded={open}\n className=\"flex w-full items-center justify-between gap-3 px-4 py-2.5 text-left transition-colors hover:bg-bg-sunken\"\n >\n <span className=\"inline-flex items-center gap-2 text-sm font-medium text-text-primary\">\n <FileText className=\"h-4 w-4 text-text-tertiary\" />\n Your drafts\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs tabular-nums text-text-secondary\">\n {formatNumber(drafts.length)}\n </span>\n </span>\n <ChevronDown\n className={cn(\n 'h-4 w-4 shrink-0 text-text-tertiary transition-transform',\n open && 'rotate-180'\n )}\n aria-hidden=\"true\"\n />\n </button>\n\n {open && (\n <ul className=\"border-t border-border-seam\">\n {drafts.map((draft) => (\n <li key={draft.id} className=\"border-b border-border-seam last:border-b-0\">\n <DraftRow draft={draft} variant={variant} />\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction DraftRow({ draft, variant }: { draft: ComPost; variant: 'default' | 'announcement' }) {\n const updatePost = useComUpdatePost();\n const removePost = useComRemovePost();\n const [editing, setEditing] = useState(false);\n const [body, setBody] = useState(draft.body);\n\n const isAnnouncement = variant === 'announcement';\n const busy = updatePost.isPending || removePost.isPending;\n\n const saveEdit = async () => {\n const text = body.trim();\n if (!text || text === draft.body) {\n setBody(draft.body);\n setEditing(false);\n return;\n }\n try {\n await updatePost.mutateAsync({ id: draft.id, input: { body: text } });\n setEditing(false);\n notify.success('Draft updated');\n } catch (err) {\n notify.error('Could not update draft', describeError(err));\n }\n };\n\n const publish = async () => {\n try {\n await updatePost.mutateAsync({ id: draft.id, input: { status: 'PUBLISHED' } });\n notify.success(isAnnouncement ? 'Announcement published' : 'Draft published');\n } catch (err) {\n notify.error('Could not publish draft', describeError(err));\n }\n };\n\n const discard = async () => {\n try {\n await removePost.mutateAsync({ id: draft.id, channelId: draft.channelId });\n notify.success('Draft discarded');\n } catch (err) {\n notify.error('Could not discard draft', describeError(err));\n }\n };\n\n return (\n <div className=\"px-4 py-3\">\n {editing ? (\n <>\n <Textarea\n value={body}\n onChange={(e) => setBody(e.target.value)}\n rows={3}\n className=\"resize-none\"\n aria-label=\"Draft body\"\n />\n <div className=\"mt-2 flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setBody(draft.body);\n setEditing(false);\n }}\n >\n Cancel\n </Button>\n <Button size=\"sm\" onClick={() => void saveEdit()} disabled={updatePost.isPending}>\n Save\n </Button>\n </div>\n </>\n ) : (\n <>\n <p className=\"line-clamp-3 whitespace-pre-wrap break-words text-sm text-text-primary\">\n {draft.body || 'Empty draft'}\n </p>\n\n <div className=\"mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-tertiary\">\n <span>Saved {formatRelativeTime(draft.updatedAt)}</span>\n {draft.attachmentFileIds.length > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Paperclip className=\"h-3 w-3\" />\n {formatNumber(draft.attachmentFileIds.length)}\n {draft.attachmentFileIds.length === 1 ? ' attachment' : ' attachments'}\n </span>\n )}\n {draft.tags.map((tag) => (\n <span key={tag} className=\"text-text-secondary\">\n #{tag}\n </span>\n ))}\n </div>\n\n <div className=\"mt-2.5 flex flex-wrap items-center justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => void discard()}\n disabled={busy}\n className=\"text-status-error-text hover:text-status-error-text\"\n >\n <Trash2 className=\"mr-1.5 h-3.5 w-3.5\" /> Discard\n </Button>\n <Button size=\"sm\" variant=\"outline\" onClick={() => setEditing(true)} disabled={busy}>\n Edit\n </Button>\n <Button size=\"sm\" onClick={() => void publish()} disabled={busy}>\n {isAnnouncement ? (\n <>\n <Megaphone className=\"mr-1.5 h-3.5 w-3.5\" /> Broadcast\n </>\n ) : (\n <>\n <Send className=\"mr-1.5 h-3.5 w-3.5\" /> Publish\n </>\n )}\n </Button>\n </div>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,15 +1,21 @@
1
1
  /**
2
- * PostComposer — compose and publish a post to a channel.
2
+ * PostComposer — compose, save, and publish a post to a channel.
3
3
  * @module comradecircle/components
4
4
  *
5
- * Writes `comCreatePost({ channelId, body, tags })`. Text + tags only (file
6
- * attachments are a future enhancement — the svc accepts attachmentFileIds but
7
- * file upload/preview is out of scope for v1).
5
+ * Writes `comCreatePost({ channelId, body, tags, attachmentFileIds, status })`.
6
+ *
7
+ * `ComCreatePostInput.status` (`ComPostStatus`, server default `PUBLISHED`) is
8
+ * what backs the "Save draft" action: a draft is a real `ComPost` row with
9
+ * `status: DRAFT`, invisible to the channel feed (which reads
10
+ * `comListPosts(status: PUBLISHED)`) until its author publishes it through
11
+ * `comUpdatePost(id, { status: PUBLISHED })` from {@link DraftsPanel}.
8
12
  */
9
13
  interface PostComposerProps {
10
14
  channelId: string;
11
15
  channelName?: string;
12
16
  onPosted?: () => void;
17
+ /** Fired after a draft is stored, so the host can reveal its drafts list. */
18
+ onDraftSaved?: () => void;
13
19
  /**
14
20
  * 'announcement' renders the manager-only broadcast composer with distinct
15
21
  * styling (accent border + "New announcement" affordance) per BOFF-3969's
@@ -17,6 +23,6 @@ interface PostComposerProps {
17
23
  */
18
24
  variant?: 'default' | 'announcement';
19
25
  }
20
- export declare function PostComposer({ channelId, channelName, onPosted, variant, }: PostComposerProps): import("react/jsx-runtime").JSX.Element;
26
+ export declare function PostComposer({ channelId, channelName, onPosted, onDraftSaved, variant, }: PostComposerProps): import("react/jsx-runtime").JSX.Element;
21
27
  export {};
22
28
  //# sourceMappingURL=PostComposer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PostComposer.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/components/PostComposer.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,cAAc,CAAC;CACtC;AAED,wBAAgB,YAAY,CAAC,EAC3B,SAAS,EACT,WAAW,EACX,QAAQ,EACR,OAAmB,GACpB,EAAE,iBAAiB,2CAyGnB"}
1
+ {"version":3,"file":"PostComposer.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/components/PostComposer.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAcH,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,GAAG,cAAc,CAAC;CACtC;AAED,wBAAgB,YAAY,CAAC,EAC3B,SAAS,EACT,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,OAAmB,GACpB,EAAE,iBAAiB,2CA+HnB"}
@@ -8,76 +8,88 @@ import { describeError as i, notify as a } from "../lib/toast.js";
8
8
  import { AttachmentPicker as o } from "./ImageAttachments.js";
9
9
  import { TagInput as s } from "./TagInput.js";
10
10
  import { useState as c } from "react";
11
- import { Megaphone as l, Send as u } from "lucide-react";
12
- import { Fragment as d, jsx as f, jsxs as p } from "react/jsx-runtime";
13
- import { Button as m, GlassCard as h, Textarea as g } from "@burdenoff/fe-libs/ui";
11
+ import { FileText as l, Megaphone as u, Send as d } from "lucide-react";
12
+ import { Fragment as f, jsx as p, jsxs as m } from "react/jsx-runtime";
13
+ import { Button as h, GlassCard as g, Textarea as _ } from "@burdenoff/fe-libs/ui";
14
14
  //#region src/comradecircle/components/PostComposer.tsx
15
- function _({ channelId: _, channelName: v, onPosted: y, variant: b = "default" }) {
16
- let x = b === "announcement", { currentUser: S } = e(), C = n(), [w, T] = c(""), [E, D] = c([]), [O, k] = c(!1), [A, j] = c([]), M = async () => {
17
- let e = w.trim();
18
- if (!(!e && A.length === 0)) try {
19
- await C.mutateAsync({
20
- channelId: _,
21
- body: e,
22
- tags: E.length ? E : null,
23
- attachmentFileIds: A.length ? A : null
24
- }), T(""), D([]), j([]), k(!1), a.success(x ? "Announcement published" : "Posted", v ? `Shared to #${v}` : void 0), y?.();
15
+ function v({ channelId: v, channelName: y, onPosted: b, onDraftSaved: x, variant: S = "default" }) {
16
+ let C = S === "announcement", { currentUser: w } = e(), T = n(), [E, D] = c(""), [O, k] = c([]), [A, j] = c(!1), [M, N] = c([]), P = !E.trim() && M.length === 0, F = async (e) => {
17
+ let t = E.trim();
18
+ if (!t && M.length === 0) return;
19
+ let n = e === "DRAFT";
20
+ try {
21
+ await T.mutateAsync({
22
+ channelId: v,
23
+ body: t,
24
+ tags: O.length ? O : null,
25
+ attachmentFileIds: M.length ? M : null,
26
+ status: e
27
+ }), D(""), k([]), N([]), j(!1), n ? (a.success("Draft saved", y ? `Only you can see it until you publish it to #${y}.` : void 0), x?.()) : (a.success(C ? "Announcement published" : "Posted", y ? `Shared to #${y}` : void 0), b?.());
25
28
  } catch (e) {
26
- a.error("Could not publish post", i(e));
29
+ a.error(n ? "Could not save draft" : "Could not publish post", i(e));
27
30
  }
28
31
  };
29
- return /* @__PURE__ */ p(h, {
32
+ return /* @__PURE__ */ m(g, {
30
33
  treatment: "plain",
31
- className: t("p-4", x && "border-2 border-[color:var(--color-accent)]"),
32
- children: [x && /* @__PURE__ */ p("div", {
34
+ className: t("p-4", C && "border-2 border-[color:var(--color-accent)]"),
35
+ children: [C && /* @__PURE__ */ m("div", {
33
36
  className: "mb-3 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-accent",
34
- children: [/* @__PURE__ */ f(l, { className: "h-3.5 w-3.5" }), " New announcement"]
35
- }), /* @__PURE__ */ p("div", {
37
+ children: [/* @__PURE__ */ p(u, { className: "h-3.5 w-3.5" }), " New announcement"]
38
+ }), /* @__PURE__ */ m("div", {
36
39
  className: "flex gap-3",
37
- children: [/* @__PURE__ */ f(r, {
38
- name: S?.name ?? S?.email ?? "You",
40
+ children: [/* @__PURE__ */ p(r, {
41
+ name: w?.name ?? w?.email ?? "You",
39
42
  size: "md"
40
- }), /* @__PURE__ */ p("div", {
43
+ }), /* @__PURE__ */ m("div", {
41
44
  className: "flex-1",
42
45
  children: [
43
- /* @__PURE__ */ f(g, {
44
- value: w,
45
- onChange: (e) => T(e.target.value),
46
- placeholder: x ? `Broadcast an announcement to #${v ?? "this channel"}…` : v ? `Share something in #${v}…` : "Share something…",
46
+ /* @__PURE__ */ p(_, {
47
+ value: E,
48
+ onChange: (e) => D(e.target.value),
49
+ placeholder: C ? `Broadcast an announcement to #${y ?? "this channel"}…` : y ? `Share something in #${y}…` : "Share something…",
47
50
  rows: 2,
48
51
  className: "resize-none border-0 bg-transparent px-0 shadow-none focus-visible:ring-0",
49
52
  onKeyDown: (e) => {
50
- e.key === "Enter" && (e.metaKey || e.ctrlKey) && (e.preventDefault(), M());
53
+ e.key === "Enter" && (e.metaKey || e.ctrlKey) && (e.preventDefault(), F("PUBLISHED"));
51
54
  }
52
55
  }),
53
- O && /* @__PURE__ */ f("div", {
56
+ A && /* @__PURE__ */ p("div", {
54
57
  className: "mt-2",
55
- children: /* @__PURE__ */ f(s, {
56
- value: E,
57
- onChange: D,
58
+ children: /* @__PURE__ */ p(s, {
59
+ value: O,
60
+ onChange: k,
58
61
  placeholder: "Add topics…"
59
62
  })
60
63
  }),
61
- /* @__PURE__ */ f("div", {
64
+ /* @__PURE__ */ p("div", {
62
65
  className: "mt-2",
63
- children: /* @__PURE__ */ f(o, {
64
- fileIds: A,
65
- onChange: j,
66
- disabled: C.isPending
66
+ children: /* @__PURE__ */ p(o, {
67
+ fileIds: M,
68
+ onChange: N,
69
+ disabled: T.isPending
67
70
  })
68
71
  }),
69
- /* @__PURE__ */ p("div", {
72
+ /* @__PURE__ */ m("div", {
70
73
  className: "mt-3 flex items-center justify-between",
71
- children: [/* @__PURE__ */ f("button", {
74
+ children: [/* @__PURE__ */ p("button", {
72
75
  type: "button",
73
- onClick: () => k((e) => !e),
76
+ onClick: () => j((e) => !e),
74
77
  className: "text-xs font-medium text-text-secondary transition-colors hover:text-text-primary",
75
- children: O ? "Hide topics" : "# Add topics"
76
- }), /* @__PURE__ */ f(m, {
77
- size: "sm",
78
- onClick: M,
79
- disabled: !w.trim() && A.length === 0 || C.isPending,
80
- children: x ? /* @__PURE__ */ p(d, { children: [/* @__PURE__ */ f(l, { className: "mr-1.5 h-4 w-4" }), " Broadcast"] }) : /* @__PURE__ */ p(d, { children: [/* @__PURE__ */ f(u, { className: "mr-1.5 h-4 w-4" }), " Post"] })
78
+ children: A ? "Hide topics" : "# Add topics"
79
+ }), /* @__PURE__ */ m("div", {
80
+ className: "flex items-center gap-2",
81
+ children: [/* @__PURE__ */ m(h, {
82
+ size: "sm",
83
+ variant: "outline",
84
+ onClick: () => void F("DRAFT"),
85
+ disabled: P || T.isPending,
86
+ children: [/* @__PURE__ */ p(l, { className: "mr-1.5 h-4 w-4" }), " Save draft"]
87
+ }), /* @__PURE__ */ p(h, {
88
+ size: "sm",
89
+ onClick: () => void F("PUBLISHED"),
90
+ disabled: P || T.isPending,
91
+ children: C ? /* @__PURE__ */ m(f, { children: [/* @__PURE__ */ p(u, { className: "mr-1.5 h-4 w-4" }), " Broadcast"] }) : /* @__PURE__ */ m(f, { children: [/* @__PURE__ */ p(d, { className: "mr-1.5 h-4 w-4" }), " Post"] })
92
+ })]
81
93
  })]
82
94
  })
83
95
  ]
@@ -86,6 +98,6 @@ function _({ channelId: _, channelName: v, onPosted: y, variant: b = "default" }
86
98
  });
87
99
  }
88
100
  //#endregion
89
- export { _ as PostComposer };
101
+ export { v as PostComposer };
90
102
 
91
103
  //# 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, Megaphone } from 'lucide-react';\nimport { Button, Textarea, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\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 * 'announcement' renders the manager-only broadcast composer with distinct\n * styling (accent border + \"New announcement\" affordance) per BOFF-3969's\n * mock; 'default' is the regular post composer used on TEXT/FORUM channels.\n */\n variant?: 'default' | 'announcement';\n}\n\nexport function PostComposer({\n channelId,\n channelName,\n onPosted,\n variant = 'default',\n}: PostComposerProps) {\n const isAnnouncement = variant === 'announcement';\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(\n isAnnouncement ? 'Announcement published' : 'Posted',\n channelName ? `Shared to #${channelName}` : undefined\n );\n onPosted?.();\n } catch (err) {\n notify.error('Could not publish post', describeError(err));\n }\n };\n\n return (\n <GlassCard\n treatment=\"plain\"\n className={cn('p-4', isAnnouncement && 'border-2 border-[color:var(--color-accent)]')}\n >\n {isAnnouncement && (\n <div className=\"mb-3 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-accent\">\n <Megaphone className=\"h-3.5 w-3.5\" /> New announcement\n </div>\n )}\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={\n isAnnouncement\n ? `Broadcast an announcement to #${channelName ?? 'this channel'}…`\n : channelName\n ? `Share something in #${channelName}…`\n : 'Share something…'\n }\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 {isAnnouncement ? (\n <>\n <Megaphone className=\"mr-1.5 h-4 w-4\" /> Broadcast\n </>\n ) : (\n <>\n <Send className=\"mr-1.5 h-4 w-4\" /> Post\n </>\n )}\n </Button>\n </div>\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAgCA,SAAgB,EAAa,EAC3B,cACA,gBACA,aACA,aAAU,aACU;CACpB,IAAM,IAAiB,MAAY,gBAC7B,EAAE,mBAAgB,EAAiB,GACnC,IAAa,EAAiB,GAC9B,CAAC,GAAM,KAAW,EAAS,EAAE,GAC7B,CAAC,GAAM,KAAW,EAAmB,CAAC,CAAC,GACvC,CAAC,GAAU,KAAe,EAAS,EAAK,GACxC,CAAC,GAAmB,KAAwB,EAAmB,CAAC,CAAC,GAEjE,IAAS,YAAY;EACzB,IAAM,IAAO,EAAK,KAAK;EACnB,OAAC,KAAQ,EAAkB,WAAW,IAC1C,IAAI;GAeF,AAdA,MAAM,EAAW,YAAY;IAC3B;IACA,MAAM;IACN,MAAM,EAAK,SAAS,IAAO;IAC3B,mBAAmB,EAAkB,SAAS,IAAoB;GACpE,CAAC,GACD,EAAQ,EAAE,GACV,EAAQ,CAAC,CAAC,GACV,EAAqB,CAAC,CAAC,GACvB,EAAY,EAAK,GACjB,EAAO,QACL,IAAiB,2BAA2B,UAC5C,IAAc,cAAc,MAAgB,KAAA,CAC9C,GACA,IAAW;EACb,SAAS,GAAK;GACZ,EAAO,MAAM,0BAA0B,EAAc,CAAG,CAAC;EAC3D;CACF;CAEA,OACE,kBAAC,GAAD;EACE,WAAU;EACV,WAAW,EAAG,OAAO,KAAkB,6CAA6C;YAFtF,CAIG,KACC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAW,WAAU,cAAe,CAAA,GAAC,mBAClC;MAEP,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IAAQ,MAAM,GAAa,QAAQ,GAAa,SAAS;IAAO,MAAK;GAAM,CAAA,GAC3E,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,OAAO;MACP,WAAW,MAAM,EAAQ,EAAE,OAAO,KAAK;MACvC,aACE,IACI,iCAAiC,KAAe,eAAe,KAC/D,IACE,uBAAuB,EAAY,KACnC;MAER,MAAM;MACN,WAAU;MACV,YAAY,MAAM;OAChB,AAAI,EAAE,QAAQ,YAAY,EAAE,WAAW,EAAE,aACvC,EAAE,eAAe,GACjB,EAAY;MAEhB;KACD,CAAA;KACA,KACC,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAU,OAAO;OAAM,UAAU;OAAS,aAAY;MAAe,CAAA;KAClE,CAAA;KAEP,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OACE,SAAS;OACT,UAAU;OACV,UAAU,EAAW;MACtB,CAAA;KACE,CAAA;KACL,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,GAAa,MAAM,CAAC,CAAC;OACpC,WAAU;iBAET,IAAW,gBAAgB;MACtB,CAAA,GACR,kBAAC,GAAD;OACE,MAAK;OACL,SAAS;OACT,UAAW,CAAC,EAAK,KAAK,KAAK,EAAkB,WAAW,KAAM,EAAW;iBAExE,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAW,WAAU,iBAAkB,CAAA,GAAC,YACxC,EAAA,CAAA,IAEF,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAM,WAAU,iBAAkB,CAAA,GAAC,OACnC,EAAA,CAAA;MAEE,CAAA,CACL;;IACF;KACF;IACI;;AAEf"}
1
+ {"version":3,"file":"PostComposer.js","names":[],"sources":["../../../src/comradecircle/components/PostComposer.tsx"],"sourcesContent":["/**\n * PostComposer — compose, save, and publish a post to a channel.\n * @module comradecircle/components\n *\n * Writes `comCreatePost({ channelId, body, tags, attachmentFileIds, status })`.\n *\n * `ComCreatePostInput.status` (`ComPostStatus`, server default `PUBLISHED`) is\n * what backs the \"Save draft\" action: a draft is a real `ComPost` row with\n * `status: DRAFT`, invisible to the channel feed (which reads\n * `comListPosts(status: PUBLISHED)`) until its author publishes it through\n * `comUpdatePost(id, { status: PUBLISHED })` from {@link DraftsPanel}.\n */\n\nimport { useState } from 'react';\nimport { Send, Megaphone, FileText } from 'lucide-react';\nimport { Button, Textarea, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\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';\nimport type { ComPostStatus } from '../types';\n\ninterface PostComposerProps {\n channelId: string;\n channelName?: string;\n onPosted?: () => void;\n /** Fired after a draft is stored, so the host can reveal its drafts list. */\n onDraftSaved?: () => void;\n /**\n * 'announcement' renders the manager-only broadcast composer with distinct\n * styling (accent border + \"New announcement\" affordance) per BOFF-3969's\n * mock; 'default' is the regular post composer used on TEXT/FORUM channels.\n */\n variant?: 'default' | 'announcement';\n}\n\nexport function PostComposer({\n channelId,\n channelName,\n onPosted,\n onDraftSaved,\n variant = 'default',\n}: PostComposerProps) {\n const isAnnouncement = variant === 'announcement';\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 isEmpty = !body.trim() && attachmentFileIds.length === 0;\n\n const submit = async (status: ComPostStatus) => {\n const text = body.trim();\n if (!text && attachmentFileIds.length === 0) return;\n const draft = status === 'DRAFT';\n try {\n await createPost.mutateAsync({\n channelId,\n body: text,\n tags: tags.length ? tags : null,\n attachmentFileIds: attachmentFileIds.length ? attachmentFileIds : null,\n status,\n });\n setBody('');\n setTags([]);\n setAttachmentFileIds([]);\n setShowTags(false);\n if (draft) {\n notify.success(\n 'Draft saved',\n channelName ? `Only you can see it until you publish it to #${channelName}.` : undefined\n );\n onDraftSaved?.();\n } else {\n notify.success(\n isAnnouncement ? 'Announcement published' : 'Posted',\n channelName ? `Shared to #${channelName}` : undefined\n );\n onPosted?.();\n }\n } catch (err) {\n notify.error(draft ? 'Could not save draft' : 'Could not publish post', describeError(err));\n }\n };\n\n return (\n <GlassCard\n treatment=\"plain\"\n className={cn('p-4', isAnnouncement && 'border-2 border-[color:var(--color-accent)]')}\n >\n {isAnnouncement && (\n <div className=\"mb-3 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-accent\">\n <Megaphone className=\"h-3.5 w-3.5\" /> New announcement\n </div>\n )}\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={\n isAnnouncement\n ? `Broadcast an announcement to #${channelName ?? 'this channel'}…`\n : channelName\n ? `Share something in #${channelName}…`\n : 'Share something…'\n }\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('PUBLISHED');\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 <div className=\"flex items-center gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => void submit('DRAFT')}\n disabled={isEmpty || createPost.isPending}\n >\n <FileText className=\"mr-1.5 h-4 w-4\" /> Save draft\n </Button>\n <Button\n size=\"sm\"\n onClick={() => void submit('PUBLISHED')}\n disabled={isEmpty || createPost.isPending}\n >\n {isAnnouncement ? (\n <>\n <Megaphone className=\"mr-1.5 h-4 w-4\" /> Broadcast\n </>\n ) : (\n <>\n <Send className=\"mr-1.5 h-4 w-4\" /> Post\n </>\n )}\n </Button>\n </div>\n </div>\n </div>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * UpcomingEvents — a compact "what's next" strip for the home feed.
3
+ * @module comradecircle/components
4
+ *
5
+ * Reads `comListEvents(status: SCHEDULED)` — the same root field the Events page
6
+ * already uses — and writes `comRsvpEvent(eventId, status)` straight from the
7
+ * row, so the member can commit to the next gathering without leaving home.
8
+ * Every value shown (`title`, `startsAt`, `mode`, `location`, `rsvpCount`,
9
+ * `capacity`) comes from that query's own selection set.
10
+ *
11
+ * Why the client-side date filter: the svc orders `comListEvents` by
12
+ * `startsAt: 'asc'` and does NOT drop events whose start time has passed, so an
13
+ * event left in `SCHEDULED` after its slot sorts *ahead* of genuinely upcoming
14
+ * ones. The rail therefore reads a window of the ordered list and keeps only
15
+ * rows still in the future. If that window is entirely historical the strip
16
+ * renders nothing at all — it never claims "no upcoming events", because one
17
+ * page of a list is not evidence that none exist.
18
+ */
19
+ interface UpcomingEventsProps {
20
+ /** Rows to render once past events are filtered out. */
21
+ limit?: number;
22
+ className?: string;
23
+ }
24
+ export declare function UpcomingEvents({ limit, className }: UpcomingEventsProps): import("react/jsx-runtime").JSX.Element | null;
25
+ export {};
26
+ //# sourceMappingURL=UpcomingEvents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UpcomingEvents.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/components/UpcomingEvents.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAgCH,UAAU,mBAAmB;IAC3B,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,cAAc,CAAC,EAAE,KAAS,EAAE,SAAS,EAAE,EAAE,mBAAmB,kDAgD3E"}
@@ -0,0 +1,126 @@
1
+ import { useComradeCircle as e } from "../context/ComradeCircleContext.js";
2
+ import "../context/index.js";
3
+ import { cn as t } from "../utils/cn.js";
4
+ import { useComEvents as n, useComRsvpEvent as r } from "../hooks/useComradeCircleApi.js";
5
+ import "../hooks/index.js";
6
+ import { describeError as i, notify as a } from "../lib/toast.js";
7
+ import { formatDateTime as o, formatNumber as s } from "../utils/formatters.js";
8
+ import { useEffect as c, useMemo as l, useState as u } from "react";
9
+ import { ArrowRight as d, CalendarDays as f, Check as p, MapPin as m, Users as h, Video as g } from "lucide-react";
10
+ import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
11
+ import { Button as b } from "@burdenoff/fe-libs/ui";
12
+ //#region src/comradecircle/components/UpcomingEvents.tsx
13
+ var x = 50, S = 6e4;
14
+ function C(e = S) {
15
+ let [t, n] = u(() => Date.now());
16
+ return c(() => {
17
+ let t = setInterval(() => n(Date.now()), e);
18
+ return () => clearInterval(t);
19
+ }, [e]), t;
20
+ }
21
+ function w({ limit: r = 3, className: i }) {
22
+ let { navigateTo: a } = e(), o = C(), { data: s, isLoading: c, isError: u } = n({
23
+ status: "SCHEDULED",
24
+ pagination: { limit: x }
25
+ }), p = l(() => (s?.items ?? []).filter((e) => new Date(e.startsAt).getTime() >= o).slice(0, r), [
26
+ s,
27
+ o,
28
+ r
29
+ ]);
30
+ return c || u || p.length === 0 ? null : /* @__PURE__ */ y("section", {
31
+ className: t("mx-auto w-full max-w-2xl", i),
32
+ "aria-labelledby": "cc-upcoming",
33
+ children: [/* @__PURE__ */ y("div", {
34
+ className: "mb-2 flex items-center justify-between gap-3",
35
+ children: [/* @__PURE__ */ y("h2", {
36
+ id: "cc-upcoming",
37
+ className: "inline-flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-text-tertiary",
38
+ children: [/* @__PURE__ */ v(f, { className: "h-3.5 w-3.5" }), " Upcoming events"]
39
+ }), /* @__PURE__ */ y("button", {
40
+ type: "button",
41
+ onClick: () => a("events"),
42
+ className: "inline-flex items-center gap-1 text-xs font-medium text-text-secondary transition-colors hover:text-text-primary",
43
+ children: ["View all ", /* @__PURE__ */ v(d, { className: "h-3 w-3" })]
44
+ })]
45
+ }), /* @__PURE__ */ v("ul", {
46
+ className: "overflow-hidden rounded-2xl border border-border-seam bg-bg-surface",
47
+ children: p.map((e) => /* @__PURE__ */ v("li", {
48
+ className: "border-b border-border-seam last:border-b-0",
49
+ children: /* @__PURE__ */ v(T, { event: e })
50
+ }, e.id))
51
+ })]
52
+ });
53
+ }
54
+ function T({ event: n }) {
55
+ let { navigateTo: c } = e(), l = r(), [d, x] = u(null), S = d === "GOING", C = n.mode === "IN_PERSON" ? m : g, w = n.mode === "IN_PERSON" ? n.location || "In person" : n.mode === "HYBRID" ? `Hybrid${n.location ? ` · ${n.location}` : ""}` : "Online", T = async () => {
56
+ try {
57
+ await l.mutateAsync({
58
+ eventId: n.id,
59
+ status: "GOING"
60
+ }), x("GOING"), a.success("RSVP: Going", n.title);
61
+ } catch (e) {
62
+ a.error("Could not RSVP", i(e));
63
+ }
64
+ };
65
+ return /* @__PURE__ */ y("div", {
66
+ className: "flex items-center gap-3 px-4 py-3",
67
+ children: [
68
+ /* @__PURE__ */ v("span", {
69
+ "aria-hidden": "true",
70
+ className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-accent",
71
+ children: /* @__PURE__ */ v(f, { className: "h-5 w-5" })
72
+ }),
73
+ /* @__PURE__ */ y("div", {
74
+ className: "min-w-0 flex-1",
75
+ children: [/* @__PURE__ */ v("button", {
76
+ type: "button",
77
+ onClick: () => c(`events/${n.id}`),
78
+ className: "block max-w-full truncate text-left text-sm font-semibold text-text-primary transition-colors hover:text-accent",
79
+ children: n.title
80
+ }), /* @__PURE__ */ y("div", {
81
+ className: "mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-text-tertiary",
82
+ children: [
83
+ /* @__PURE__ */ v("span", { children: o(n.startsAt) }),
84
+ /* @__PURE__ */ v("span", {
85
+ "aria-hidden": "true",
86
+ children: "·"
87
+ }),
88
+ /* @__PURE__ */ y("span", {
89
+ className: "inline-flex items-center gap-1",
90
+ children: [
91
+ /* @__PURE__ */ v(C, { className: "h-3 w-3" }),
92
+ " ",
93
+ w
94
+ ]
95
+ }),
96
+ /* @__PURE__ */ v("span", {
97
+ "aria-hidden": "true",
98
+ children: "·"
99
+ }),
100
+ /* @__PURE__ */ y("span", {
101
+ className: "inline-flex items-center gap-1",
102
+ children: [
103
+ /* @__PURE__ */ v(h, { className: "h-3 w-3" }),
104
+ s(n.rsvpCount),
105
+ n.capacity ? ` / ${s(n.capacity)}` : ""
106
+ ]
107
+ })
108
+ ]
109
+ })]
110
+ }),
111
+ /* @__PURE__ */ v(b, {
112
+ size: "sm",
113
+ variant: S ? "outline" : "default",
114
+ onClick: () => void T(),
115
+ disabled: l.isPending,
116
+ "aria-pressed": S,
117
+ className: t("shrink-0", S && "text-accent"),
118
+ children: S ? /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ v(p, { className: "mr-1 h-3.5 w-3.5" }), " Going"] }) : "RSVP"
119
+ })
120
+ ]
121
+ });
122
+ }
123
+ //#endregion
124
+ export { w as UpcomingEvents };
125
+
126
+ //# sourceMappingURL=UpcomingEvents.js.map