From f0dc1657b18ed8ed1e099a68080924899b02aebd Mon Sep 17 00:00:00 2001 From: yashbothra-kointrack Date: Sat, 17 Jan 2026 18:39:56 +0530 Subject: [PATCH 1/2] Added Broadcast feature --- client/src/app.tsx | 4 + client/src/components/Layout.tsx | 11 +- client/src/pages/Broadcasts.tsx | 306 ++++++++++++++++++++++++++++ client/src/pages/Contacts.tsx | 269 ++++++++++++++++++++++++ client/src/pages/common.css | 288 ++++++++++++++++++++++++++ client/src/services/api.ts | 66 ++++++ shared/types.ts | 46 ++++- src/api/broadcasts.ts | 183 +++++++++++++++++ src/api/contacts.ts | 106 ++++++++++ src/db/migrations.ts | 214 +++++++++++++------ src/server.ts | 6 + src/services/whatsapp.ts | 181 +++++++++++++++- src/validations/broadcastSchemas.ts | 13 ++ src/validations/contactSchemas.ts | 10 + 14 files changed, 1634 insertions(+), 69 deletions(-) create mode 100644 client/src/pages/Broadcasts.tsx create mode 100644 client/src/pages/Contacts.tsx create mode 100644 client/src/pages/common.css create mode 100644 src/api/broadcasts.ts create mode 100644 src/api/contacts.ts create mode 100644 src/validations/broadcastSchemas.ts create mode 100644 src/validations/contactSchemas.ts diff --git a/client/src/app.tsx b/client/src/app.tsx index ba9a5a5..9733584 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -4,6 +4,8 @@ import { Layout } from "./components/Layout"; import { ReloadPrompt } from "./components/ReloadPrompt"; import { ConnectWhatsApp } from "./pages/ConnectWhatsApp"; import { CreateSchedule } from "./pages/CreateSchedule"; +import { Contacts } from "./pages/Contacts"; +import { Broadcasts } from "./pages/Broadcasts"; import { Feedbacks } from "./pages/Feedbacks"; import { Schedules } from "./pages/Schedules"; import { Settings } from "./pages/Settings"; @@ -50,6 +52,8 @@ export function App() { + + diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index e0cf19c..4e8743f 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -1,4 +1,11 @@ -import { Calendar, MessageSquare, Plus, Settings } from "lucide-preact"; +import { + Calendar, + MessageSquare, + Megaphone, + Plus, + Settings, + Users, +} from "lucide-preact"; import { Link, useLocation } from "wouter-preact"; export function Layout({ children }: { children: any }) { @@ -7,6 +14,8 @@ export function Layout({ children }: { children: any }) { const navItems = [ { href: "/", icon: Calendar, label: "Schedules" }, { href: "/create", icon: Plus, label: "Create" }, + { href: "/contacts", icon: Users, label: "Contacts" }, + { href: "/broadcasts", icon: Megaphone, label: "Broadcasts" }, { href: "/feedbacks", icon: MessageSquare, label: "Feedbacks" }, { href: "/settings", icon: Settings, label: "Settings" }, ]; diff --git a/client/src/pages/Broadcasts.tsx b/client/src/pages/Broadcasts.tsx new file mode 100644 index 0000000..314be7e --- /dev/null +++ b/client/src/pages/Broadcasts.tsx @@ -0,0 +1,306 @@ +import type { Broadcast, Contact, CreateBroadcastDto } from "@shared/types"; +import { Megaphone, Plus, Trash2 } from "lucide-preact"; +import { useEffect, useState } from "preact/hooks"; +import { api } from "../services/api"; +import "./common.css"; + +export function Broadcasts() { + const [broadcasts, setBroadcasts] = useState([]); + const [contacts, setContacts] = useState([]); + const [loading, setLoading] = useState(true); + const [showCreate, setShowCreate] = useState(false); + + const [formData, setFormData] = useState({ + name: "", + message: "", + contactIds: [], + scheduledTime: undefined, + type: "instant", + intervalValue: 1, + intervalUnit: "day", + }); + + const loadData = async () => { + try { + const [bData, cData] = await Promise.all([ + api.getBroadcasts(), + api.getContacts(), + ]); + setBroadcasts(bData); + setContacts(cData); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadData(); + }, []); + + const handleDelete = async (id: number) => { + if (confirm("Delete this broadcast?")) { + await api.deleteBroadcast(id); + loadData(); + } + }; + + const handleCreate = async (e: Event) => { + e.preventDefault(); + try { + if (formData.contactIds.length === 0) { + alert("Please select at least one contact"); + return; + } + // Prepare payload + const payload = { ...formData }; + if (payload.type === "instant") { + payload.scheduledTime = undefined; + payload.intervalValue = undefined; + payload.intervalUnit = undefined; + } else if (payload.type === "once") { + if (!payload.scheduledTime) { + alert("Please select a time"); + return; + } + payload.intervalValue = undefined; + payload.intervalUnit = undefined; + } else if (payload.type === "recurring") { + if (!payload.scheduledTime) { + alert("Please select a start time"); + return; + } + } + await api.createBroadcast(payload); + setShowCreate(false); + setFormData({ + name: "", + message: "", + contactIds: [], + scheduledTime: undefined, + type: "instant", + intervalValue: 1, + intervalUnit: "day", + }); + loadData(); + } catch (err: any) { + alert(err.message); + } + }; + + const toggleContact = (id: number) => { + const current = formData.contactIds; + if (current.includes(id)) { + setFormData({ + ...formData, + contactIds: current.filter((cid) => cid !== id), + }); + } else { + setFormData({ ...formData, contactIds: [...current, id] }); + } + }; + + const toggleSelectAll = () => { + if (formData.contactIds.length === contacts.length) { + setFormData({ ...formData, contactIds: [] }); + } else { + setFormData({ ...formData, contactIds: contacts.map((c) => c.id) }); + } + }; + + if (loading) return
Loading...
; + + return ( +
+ + + {showCreate && ( +
+

New Broadcast

+
+ + + setFormData({ ...formData, name: e.currentTarget.value }) + } + required + /> +
+ +
+ +
+ {(["instant", "once", "recurring"] as const).map((t) => ( + + ))} +
+
+ + {formData.type !== "instant" && ( +
+ + + setFormData({ + ...formData, + scheduledTime: e.currentTarget.value, + }) + } + required + /> +
+ )} + + {formData.type === "recurring" && ( +
+
+ + + setFormData({ + ...formData, + intervalValue: parseInt(e.currentTarget.value), + }) + } + required + /> +
+
+ + +
+
+ )} + +
+ +