Skip to content

Commit 0a9452f

Browse files
authored
Merge pull request #87 from indrazm/broadcast
Broadcast Feature
2 parents 30f68a2 + daf523e commit 0a9452f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2207
-65
lines changed

.github/workflows/release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ jobs:
4444
echo "=== GIT STATUS ==="
4545
git log --oneline -5 || echo "No git history available"
4646
47-
# Try to get previous version from git history
47+
# Try to get previous version from git history (use HEAD^ to get the parent commit)
4848
PREVIOUS_VERSION=""
4949
echo "=== CHECKING PREVIOUS PACKAGE.JSON ==="
50-
if git show origin/main:package.json >/dev/null 2>&1; then
51-
PREVIOUS_VERSION=$(git show origin/main:package.json | node -p "JSON.parse(require('fs').readFileSync(0)).version")
50+
if git show HEAD^:package.json >/dev/null 2>&1; then
51+
PREVIOUS_VERSION=$(git show HEAD^:package.json | node -p "JSON.parse(require('fs').readFileSync(0)).version")
5252
echo "Previous version: $PREVIOUS_VERSION"
5353
else
5454
echo "No previous version found in git history"

apps/admin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "admin",
33
"private": true,
4-
"version": "0.0.14",
4+
"version": "0.0.15",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

apps/admin/src/components/sidebar.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Key,
1111
Link as LinkIcon,
1212
LogOut,
13+
Mail,
1314
Settings,
1415
UserCheck,
1516
Users,
@@ -104,6 +105,11 @@ export const Sidebar = () => {
104105
label="Invite Codes"
105106
to="/invite-codes"
106107
/>
108+
<MenuItem
109+
icon={<Mail size={18} />}
110+
label="Broadcast"
111+
to="/broadcast"
112+
/>
107113
</MenuGroup>
108114
<MenuGroup value="content" label="Content">
109115
<MenuItem
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { Button, Input } from "@opencircle/ui";
2+
import { useQuery } from "@tanstack/react-query";
3+
import MDEditor from "@uiw/react-md-editor";
4+
import { Eye, EyeOff, Save } from "lucide-react";
5+
import { useId, useState } from "react";
6+
import { api } from "../../../utils/api";
7+
import type {
8+
BroadcastCreate,
9+
BroadcastRecipientType,
10+
BroadcastUpdate,
11+
} from "../utils/types";
12+
13+
interface BroadcastEditorProps {
14+
broadcast?: Partial<BroadcastCreate> & { id?: string };
15+
onSave: (data: BroadcastCreate | BroadcastUpdate) => Promise<void>;
16+
onCancel?: () => void;
17+
loading?: boolean;
18+
isEdit?: boolean;
19+
}
20+
21+
export const BroadcastEditor = ({
22+
broadcast,
23+
onSave,
24+
onCancel,
25+
loading,
26+
isEdit = false,
27+
}: BroadcastEditorProps) => {
28+
const [subject, setSubject] = useState(broadcast?.subject || "");
29+
const [content, setContent] = useState(broadcast?.content || "");
30+
const [recipientType, setRecipientType] = useState<BroadcastRecipientType>(
31+
broadcast?.recipient_type || "all_users",
32+
);
33+
const [channelId, setChannelId] = useState(broadcast?.channel_id || "");
34+
const [showPreview, setShowPreview] = useState(false);
35+
const recipientTypeId = useId();
36+
const channelSelectId = useId();
37+
38+
const { data: channels } = useQuery({
39+
queryKey: ["channels"],
40+
queryFn: () => api.channels.getAll(),
41+
});
42+
43+
const handleSubmit = async (e: React.FormEvent) => {
44+
e.preventDefault();
45+
46+
if (!subject.trim() || !content.trim()) {
47+
return;
48+
}
49+
50+
if (recipientType === "channel_members" && !channelId) {
51+
return;
52+
}
53+
54+
if (isEdit && broadcast?.id) {
55+
const updateData: BroadcastUpdate = {
56+
subject: subject.trim(),
57+
content: content.trim(),
58+
recipient_type: recipientType,
59+
channel_id: recipientType === "channel_members" ? channelId : undefined,
60+
};
61+
await onSave(updateData);
62+
} else {
63+
const createData: BroadcastCreate = {
64+
subject: subject.trim(),
65+
content: content.trim(),
66+
recipient_type: recipientType,
67+
channel_id: recipientType === "channel_members" ? channelId : undefined,
68+
};
69+
await onSave(createData);
70+
}
71+
};
72+
73+
return (
74+
<form onSubmit={handleSubmit} className="space-y-6">
75+
<div className="flex items-center justify-between">
76+
<h1 className="font-bold text-3xl">
77+
{isEdit ? "Edit Broadcast" : "Create New Broadcast"}
78+
</h1>
79+
<div className="flex gap-2">
80+
{onCancel && (
81+
<Button type="button" onClick={onCancel}>
82+
Cancel
83+
</Button>
84+
)}
85+
<Button
86+
type="submit"
87+
disabled={loading || !subject.trim() || !content.trim()}
88+
>
89+
<Save size={16} className="mr-2" />
90+
{loading ? "Saving..." : "Save Draft"}
91+
</Button>
92+
</div>
93+
</div>
94+
95+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
96+
<div className="space-y-6 lg:col-span-2">
97+
<div>
98+
<label htmlFor="subject" className="mb-2 block font-medium text-sm">
99+
Subject *
100+
</label>
101+
<Input
102+
value={subject}
103+
onChange={(e) => setSubject(e.target.value)}
104+
placeholder="Enter email subject..."
105+
required
106+
/>
107+
</div>
108+
109+
<div>
110+
<div className="mb-2 flex items-center justify-between">
111+
<label htmlFor="content" className="block font-medium text-sm">
112+
Content (Markdown/HTML) *
113+
</label>
114+
<Button
115+
type="button"
116+
size="sm"
117+
onClick={() => setShowPreview(!showPreview)}
118+
>
119+
{showPreview ? (
120+
<EyeOff size={14} className="mr-1" />
121+
) : (
122+
<Eye size={14} className="mr-1" />
123+
)}
124+
{showPreview ? "Edit" : "Preview"}
125+
</Button>
126+
</div>
127+
{showPreview ? (
128+
<div className="prose prose-invert min-h-[400px] max-w-none rounded-lg border border-border p-4">
129+
<MDEditor.Markdown source={content} />
130+
</div>
131+
) : (
132+
<div data-color-mode="dark">
133+
<MDEditor
134+
value={content}
135+
onChange={(val) => setContent(val || "")}
136+
height={400}
137+
preview="edit"
138+
hideToolbar={false}
139+
visibleDragbar={false}
140+
/>
141+
</div>
142+
)}
143+
</div>
144+
</div>
145+
146+
<div className="space-y-4">
147+
<div className="rounded-lg border border-border bg-background-secondary/30 p-4">
148+
<h3 className="mb-3 font-semibold text-sm">Recipients</h3>
149+
<div className="space-y-3">
150+
<div>
151+
<label
152+
htmlFor={recipientTypeId}
153+
className="mb-1 block text-foreground/60 text-xs"
154+
>
155+
Send to
156+
</label>
157+
<select
158+
id={recipientTypeId}
159+
value={recipientType}
160+
onChange={(e) =>
161+
setRecipientType(e.target.value as BroadcastRecipientType)
162+
}
163+
className="w-full rounded-lg border border-border bg-background p-2 text-sm"
164+
>
165+
<option value="all_users">All Users</option>
166+
<option value="channel_members">Channel Members</option>
167+
</select>
168+
</div>
169+
170+
{recipientType === "channel_members" && (
171+
<div>
172+
<label
173+
htmlFor={channelSelectId}
174+
className="mb-1 block text-foreground/60 text-xs"
175+
>
176+
Select Channel
177+
</label>
178+
<select
179+
id={channelSelectId}
180+
value={channelId}
181+
onChange={(e) => setChannelId(e.target.value)}
182+
className="w-full rounded-lg border border-border bg-background p-2 text-sm"
183+
>
184+
<option value="">Select a channel...</option>
185+
{channels?.map((channel) => (
186+
<option key={channel.id} value={channel.id}>
187+
{channel.emoji} {channel.name}
188+
</option>
189+
))}
190+
</select>
191+
</div>
192+
)}
193+
</div>
194+
</div>
195+
196+
<div className="rounded-lg border border-border bg-background-secondary/30 p-4">
197+
<h3 className="mb-2 font-semibold text-sm">Tips</h3>
198+
<ul className="space-y-1 text-foreground/60 text-sm">
199+
<li>- Use Markdown for formatting</li>
200+
<li>- Preview before sending</li>
201+
<li>- Test with a single email first</li>
202+
</ul>
203+
</div>
204+
</div>
205+
</div>
206+
</form>
207+
);
208+
};

0 commit comments

Comments
 (0)