bizlin-chat 0.0.1-a1
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/backend/README.md +54 -0
- package/backend/package.json +19 -0
- package/backend/src/db.js +44 -0
- package/backend/src/index.js +176 -0
- package/backend/src/whatsapp.js +67 -0
- package/frontend/README.md +11 -0
- package/frontend/package.json +16 -0
- package/frontend/pages/index.js +98 -0
- package/package.json +32 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Backend (Node/Express)
|
|
2
|
+
|
|
3
|
+
Run locally:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
cd backend
|
|
7
|
+
npm install
|
|
8
|
+
npm start
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
API endpoints:
|
|
12
|
+
|
|
13
|
+
- `GET /api/messages` — list messages
|
|
14
|
+
- `POST /api/messages` — send message, body `{ text, sender? }`
|
|
15
|
+
|
|
16
|
+
Realtime: Socket.IO server on the same port (emits `message` events).
|
|
17
|
+
|
|
18
|
+
Example: http://localhost:4000/api/hello
|
|
19
|
+
|
|
20
|
+
Database (PostgreSQL)
|
|
21
|
+
|
|
22
|
+
The backend supports PostgreSQL. By default it connects to the `DATABASE_URL` env var or `postgresql://postgres:postgres@localhost:5432/bizchat`.
|
|
23
|
+
|
|
24
|
+
Quick start with Docker Compose:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
docker compose up -d
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This starts a Postgres instance on port 5432. Then run the backend as usual and it will auto-create the `messages` table.
|
|
31
|
+
|
|
32
|
+
WhatsApp integration
|
|
33
|
+
|
|
34
|
+
Set these environment variables for WhatsApp Cloud API integration:
|
|
35
|
+
|
|
36
|
+
- `WHATSAPP_PHONE_NUMBER_ID` — your phone number ID from the Meta App Dashboard
|
|
37
|
+
- `WHATSAPP_TOKEN` — system user permanent access token (keep secret)
|
|
38
|
+
- `WHATSAPP_VERIFY_TOKEN` — a token you set to verify webhook subscription
|
|
39
|
+
- `WHATSAPP_API_VERSION` — optional, defaults to `v15.0`
|
|
40
|
+
|
|
41
|
+
Endpoints:
|
|
42
|
+
|
|
43
|
+
- `GET /webhook` — verification endpoint used when subscribing to webhooks (uses `WHATSAPP_VERIFY_TOKEN`).
|
|
44
|
+
- `POST /webhook` — receives webhook events (incoming messages and statuses).
|
|
45
|
+
- `POST /api/send` — send a text message via WhatsApp Cloud API. Body: `{ "to": "<whatsapp_number>", "text": "Hello" }`.
|
|
46
|
+
- `POST /api/send` — send a text message via WhatsApp Cloud API. Body: `{ "to": "<whatsapp_number>", "text": "Hello" }`.
|
|
47
|
+
- `POST /api/sendMedia` — send media by URL. Body: `{ "to": "<whatsapp_number>", "mediaUrl": "https://...", "mediaType": "image|audio|video|document" }`.
|
|
48
|
+
- `POST /api/sendTemplate` — send a template message. Body: `{ "to": "<whatsapp_number>", "templateName": "template_name", "language": "en_US", "components": [...] }`.
|
|
49
|
+
|
|
50
|
+
The backend persists incoming messages to the `messages` table and emits realtime `message` events via Socket.IO.
|
|
51
|
+
|
|
52
|
+
Security
|
|
53
|
+
- Webhook requests are verified using `X-Hub-Signature-256` when `WHATSAPP_APP_SECRET` is set.
|
|
54
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node src/index.js",
|
|
8
|
+
"dev": "nodemon src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"axios": "1.5.0",
|
|
12
|
+
"cors": "2.8.5",
|
|
13
|
+
"express": "4.18.2",
|
|
14
|
+
"socket.io": "4.7.2",
|
|
15
|
+
"uuid": "9.0.0",
|
|
16
|
+
"pg": "8.11.0"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const { Pool } = require('pg')
|
|
2
|
+
|
|
3
|
+
const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/bizchat'
|
|
4
|
+
|
|
5
|
+
const pool = new Pool({ connectionString: DATABASE_URL })
|
|
6
|
+
|
|
7
|
+
async function init() {
|
|
8
|
+
// Create messages table if it doesn't exist
|
|
9
|
+
const sql = `
|
|
10
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
text TEXT NOT NULL,
|
|
13
|
+
sender TEXT,
|
|
14
|
+
ts BIGINT NOT NULL,
|
|
15
|
+
wa_id TEXT,
|
|
16
|
+
direction TEXT,
|
|
17
|
+
type TEXT,
|
|
18
|
+
media_url TEXT,
|
|
19
|
+
status TEXT
|
|
20
|
+
);
|
|
21
|
+
`
|
|
22
|
+
await pool.query(sql)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getMessages() {
|
|
26
|
+
const res = await pool.query('SELECT id, text, sender, ts, wa_id, direction, type, media_url FROM messages ORDER BY ts ASC')
|
|
27
|
+
return res.rows
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function addMessage(message) {
|
|
31
|
+
const { id, text, sender, ts, wa_id, direction, type, media_url, status } = message
|
|
32
|
+
const sql = 'INSERT INTO messages(id, text, sender, ts, wa_id, direction, type, media_url, status) VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9)'
|
|
33
|
+
await pool.query(sql, [id, text, sender, ts, wa_id || null, direction || null, type || null, media_url || null, status || null])
|
|
34
|
+
return message
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function updateMessageStatus(id, status) {
|
|
38
|
+
const sql = 'UPDATE messages SET status = $1 WHERE id = $2'
|
|
39
|
+
await pool.query(sql, [status, id])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { init, getMessages, addMessage, updateMessageStatus, pool }
|
|
43
|
+
|
|
44
|
+
module.exports = { init, getMessages, addMessage, pool }
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const express = require('express')
|
|
2
|
+
const cors = require('cors')
|
|
3
|
+
const http = require('http')
|
|
4
|
+
const { Server } = require('socket.io')
|
|
5
|
+
const { v4: uuidv4 } = require('uuid')
|
|
6
|
+
const db = require('./db')
|
|
7
|
+
const whatsapp = require('./whatsapp')
|
|
8
|
+
const crypto = require('crypto')
|
|
9
|
+
|
|
10
|
+
const app = express()
|
|
11
|
+
app.use(cors())
|
|
12
|
+
app.use(express.json())
|
|
13
|
+
|
|
14
|
+
app.get('/api/hello', (req, res) => {
|
|
15
|
+
res.json({ message: 'Hello from backend' })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
app.get('/api/messages', async (req, res) => {
|
|
19
|
+
try {
|
|
20
|
+
const rows = await db.getMessages()
|
|
21
|
+
res.json(rows)
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.error(err)
|
|
24
|
+
res.status(500).json({ error: 'failed to load messages' })
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
app.post('/api/messages', async (req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
const { text, sender } = req.body
|
|
31
|
+
if (!text) return res.status(400).json({ error: 'text is required' })
|
|
32
|
+
const message = { id: uuidv4(), text, sender: sender || 'agent', ts: Date.now() }
|
|
33
|
+
await db.addMessage(message)
|
|
34
|
+
io.emit('message', message)
|
|
35
|
+
res.status(201).json(message)
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error(err)
|
|
38
|
+
res.status(500).json({ error: 'failed to save message' })
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Send outbound message via WhatsApp Cloud API
|
|
43
|
+
app.post('/api/send', async (req, res) => {
|
|
44
|
+
const { to, text } = req.body
|
|
45
|
+
if (!to || !text) return res.status(400).json({ error: 'to and text are required' })
|
|
46
|
+
try {
|
|
47
|
+
const resp = await whatsapp.sendText(to, text)
|
|
48
|
+
const waMessageId = resp && resp.messages && resp.messages[0] && resp.messages[0].id
|
|
49
|
+
const message = { id: waMessageId || uuidv4(), text, sender: 'us', ts: Date.now(), wa_id: to, direction: 'outbound', type: 'text' }
|
|
50
|
+
await db.addMessage(message)
|
|
51
|
+
io.emit('message', message)
|
|
52
|
+
res.status(200).json(message)
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error('send failed', err?.response?.data || err.message || err)
|
|
55
|
+
res.status(500).json({ error: 'failed to send message' })
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Webhook verification endpoint (GET)
|
|
60
|
+
app.get('/webhook', (req, res) => {
|
|
61
|
+
const mode = req.query['hub.mode']
|
|
62
|
+
const token = req.query['hub.verify_token']
|
|
63
|
+
const challenge = req.query['hub.challenge']
|
|
64
|
+
if (mode && token) {
|
|
65
|
+
if (mode === 'subscribe' && token === process.env.WHATSAPP_VERIFY_TOKEN) {
|
|
66
|
+
console.log('WEBHOOK_VERIFIED')
|
|
67
|
+
res.status(200).send(challenge)
|
|
68
|
+
} else {
|
|
69
|
+
res.sendStatus(403)
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
res.sendStatus(400)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Webhook receiver (POST)
|
|
77
|
+
// Use raw body parsing for webhook to allow signature verification
|
|
78
|
+
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
79
|
+
const raw = req.body
|
|
80
|
+
const sig = req.headers['x-hub-signature-256']
|
|
81
|
+
const appSecret = process.env.WHATSAPP_APP_SECRET
|
|
82
|
+
if (appSecret && sig) {
|
|
83
|
+
const hmac = crypto.createHmac('sha256', appSecret).update(raw).digest('hex')
|
|
84
|
+
const expected = `sha256=${hmac}`
|
|
85
|
+
if (sig !== expected) {
|
|
86
|
+
console.warn('Invalid webhook signature')
|
|
87
|
+
return res.sendStatus(403)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let body
|
|
91
|
+
try {
|
|
92
|
+
body = JSON.parse(raw.toString())
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error('invalid json', err)
|
|
95
|
+
return res.sendStatus(400)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (body.object === 'whatsapp_business_account') {
|
|
99
|
+
try {
|
|
100
|
+
for (const entry of body.entry || []) {
|
|
101
|
+
for (const change of entry.changes || []) {
|
|
102
|
+
const value = change.value || {}
|
|
103
|
+
// Incoming messages
|
|
104
|
+
if (value.messages) {
|
|
105
|
+
for (const msg of value.messages) {
|
|
106
|
+
const wa_id = msg.from || (value.contacts && value.contacts[0] && value.contacts[0].wa_id)
|
|
107
|
+
const senderName = (value.contacts && value.contacts[0] && value.contacts[0].profile && value.contacts[0].profile.name) || wa_id
|
|
108
|
+
let text = ''
|
|
109
|
+
let media_url = null
|
|
110
|
+
if (msg.type === 'text') text = msg.text && msg.text.body
|
|
111
|
+
else if (['image', 'audio', 'video', 'document'].includes(msg.type)) {
|
|
112
|
+
const media = msg[msg.type]
|
|
113
|
+
// media.id is present — get downloadable url
|
|
114
|
+
if (media && media.id) {
|
|
115
|
+
try {
|
|
116
|
+
const url = await whatsapp.getMediaUrl(media.id)
|
|
117
|
+
media_url = url
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.warn('failed to fetch media url', e?.message || e)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else if (msg.type === 'interactive') {
|
|
123
|
+
// buttons or list replies
|
|
124
|
+
if (msg.interactive && msg.interactive.type === 'button_reply') {
|
|
125
|
+
text = msg.interactive.button_reply && msg.interactive.button_reply.title
|
|
126
|
+
} else if (msg.interactive && msg.interactive.type === 'list_reply') {
|
|
127
|
+
text = msg.interactive.list_reply && msg.interactive.list_reply.title
|
|
128
|
+
}
|
|
129
|
+
} else if (msg.type === 'contacts') {
|
|
130
|
+
text = JSON.stringify(msg.contacts)
|
|
131
|
+
} else if (msg.type === 'location') {
|
|
132
|
+
text = JSON.stringify(msg.location)
|
|
133
|
+
}
|
|
134
|
+
const message = { id: msg.id || uuidv4(), text, sender: senderName, ts: Date.now(), wa_id, direction: 'inbound', type: msg.type, media_url }
|
|
135
|
+
await db.addMessage(message)
|
|
136
|
+
io.emit('message', message)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Status updates
|
|
140
|
+
if (value.statuses) {
|
|
141
|
+
for (const st of value.statuses) {
|
|
142
|
+
const msgId = st.id
|
|
143
|
+
const status = st.status
|
|
144
|
+
if (msgId) await db.updateMessageStatus(msgId, status)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
res.sendStatus(200)
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error('webhook processing error', err)
|
|
152
|
+
res.sendStatus(500)
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
res.sendStatus(404)
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const port = process.env.PORT || 4000
|
|
160
|
+
const server = http.createServer(app)
|
|
161
|
+
const io = new Server(server, { cors: { origin: '*' } })
|
|
162
|
+
|
|
163
|
+
io.on('connection', (socket) => {
|
|
164
|
+
console.log('socket connected', socket.id)
|
|
165
|
+
socket.on('disconnect', () => console.log('socket disconnected', socket.id))
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
;(async () => {
|
|
169
|
+
try {
|
|
170
|
+
await db.init()
|
|
171
|
+
server.listen(port, () => console.log(`Backend listening on ${port}`))
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error('Failed to initialize database', err)
|
|
174
|
+
process.exit(1)
|
|
175
|
+
}
|
|
176
|
+
})()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const axios = require('axios')
|
|
2
|
+
|
|
3
|
+
const PHONE_ID = process.env.WHATSAPP_PHONE_NUMBER_ID
|
|
4
|
+
const TOKEN = process.env.WHATSAPP_TOKEN
|
|
5
|
+
const API_VERSION = process.env.WHATSAPP_API_VERSION || 'v15.0'
|
|
6
|
+
|
|
7
|
+
if (!PHONE_ID || !TOKEN) {
|
|
8
|
+
// Not throwing here; some envs (local dev) may not have these set.
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function sendText(to, text) {
|
|
12
|
+
if (!PHONE_ID || !TOKEN) throw new Error('WHATSAPP_PHONE_NUMBER_ID or WHATSAPP_TOKEN not configured')
|
|
13
|
+
const url = `https://graph.facebook.com/${API_VERSION}/${PHONE_ID}/messages`
|
|
14
|
+
const body = {
|
|
15
|
+
messaging_product: 'whatsapp',
|
|
16
|
+
to,
|
|
17
|
+
type: 'text',
|
|
18
|
+
text: { body: text }
|
|
19
|
+
}
|
|
20
|
+
const res = await axios.post(url, body, {
|
|
21
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' }
|
|
22
|
+
})
|
|
23
|
+
return res.data
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function sendMedia(to, mediaUrl, mediaType = 'image') {
|
|
27
|
+
if (!PHONE_ID || !TOKEN) throw new Error('WHATSAPP_PHONE_NUMBER_ID or WHATSAPP_TOKEN not configured')
|
|
28
|
+
const url = `https://graph.facebook.com/${API_VERSION}/${PHONE_ID}/messages`
|
|
29
|
+
const body = {
|
|
30
|
+
messaging_product: 'whatsapp',
|
|
31
|
+
to,
|
|
32
|
+
type: mediaType,
|
|
33
|
+
[mediaType]: { link: mediaUrl }
|
|
34
|
+
}
|
|
35
|
+
const res = await axios.post(url, body, {
|
|
36
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' }
|
|
37
|
+
})
|
|
38
|
+
return res.data
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function sendTemplate(to, templateName, language = 'en_US', components = []) {
|
|
42
|
+
if (!PHONE_ID || !TOKEN) throw new Error('WHATSAPP_PHONE_NUMBER_ID or WHATSAPP_TOKEN not configured')
|
|
43
|
+
const url = `https://graph.facebook.com/${API_VERSION}/${PHONE_ID}/messages`
|
|
44
|
+
const body = {
|
|
45
|
+
messaging_product: 'whatsapp',
|
|
46
|
+
to,
|
|
47
|
+
type: 'template',
|
|
48
|
+
template: {
|
|
49
|
+
name: templateName,
|
|
50
|
+
language: { code: language },
|
|
51
|
+
components
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const res = await axios.post(url, body, {
|
|
55
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json' }
|
|
56
|
+
})
|
|
57
|
+
return res.data
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function getMediaUrl(mediaId) {
|
|
61
|
+
if (!TOKEN) throw new Error('WHATSAPP_TOKEN not configured')
|
|
62
|
+
const url = `https://graph.facebook.com/${API_VERSION}/${mediaId}`
|
|
63
|
+
const res = await axios.get(url, { headers: { Authorization: `Bearer ${TOKEN}` } })
|
|
64
|
+
return res.data && res.data.url
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { sendText }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev -p 3000",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start -p 3000"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"next": "13.4.7",
|
|
12
|
+
"react": "18.2.0",
|
|
13
|
+
"react-dom": "18.2.0",
|
|
14
|
+
"socket.io-client": "4.7.2"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useEffect, useState, useRef } from 'react'
|
|
2
|
+
import { io } from 'socket.io-client'
|
|
3
|
+
|
|
4
|
+
export default function Home() {
|
|
5
|
+
const [messages, setMessages] = useState([])
|
|
6
|
+
const [text, setText] = useState('')
|
|
7
|
+
const [to, setTo] = useState('')
|
|
8
|
+
const [mediaUrl, setMediaUrl] = useState('')
|
|
9
|
+
const [templateName, setTemplateName] = useState('')
|
|
10
|
+
const [templateLang, setTemplateLang] = useState('en_US')
|
|
11
|
+
const socketRef = useRef(null)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
fetch('http://localhost:4000/api/messages')
|
|
15
|
+
.then((r) => r.json())
|
|
16
|
+
.then((d) => setMessages(d))
|
|
17
|
+
.catch(() => setMessages([]))
|
|
18
|
+
|
|
19
|
+
socketRef.current = io('http://localhost:4000')
|
|
20
|
+
socketRef.current.on('message', (msg) => {
|
|
21
|
+
setMessages((m) => [...m, msg])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return () => {
|
|
25
|
+
socketRef.current.disconnect()
|
|
26
|
+
}
|
|
27
|
+
}, [])
|
|
28
|
+
|
|
29
|
+
const send = async () => {
|
|
30
|
+
if (!text) return
|
|
31
|
+
// If `to` is provided, send via WhatsApp Cloud API through backend
|
|
32
|
+
if (to) {
|
|
33
|
+
await fetch('http://localhost:4000/api/send', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({ to, text })
|
|
37
|
+
})
|
|
38
|
+
} else {
|
|
39
|
+
await fetch('http://localhost:4000/api/messages', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({ text, sender: 'web' })
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
setText('')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const sendMedia = async () => {
|
|
49
|
+
if (!to || !mediaUrl) return
|
|
50
|
+
await fetch('http://localhost:4000/api/sendMedia', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ to, mediaUrl, mediaType: 'image' })
|
|
54
|
+
})
|
|
55
|
+
setMediaUrl('')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sendTemplate = async () => {
|
|
59
|
+
if (!to || !templateName) return
|
|
60
|
+
await fetch('http://localhost:4000/api/sendTemplate', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ to, templateName, language: templateLang, components: [] })
|
|
64
|
+
})
|
|
65
|
+
setTemplateName('')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<main style={{ fontFamily: 'system-ui, sans-serif', padding: 24 }}>
|
|
70
|
+
<h1>WhatsApp Team Inbox (Frontend)</h1>
|
|
71
|
+
<div style={{ maxWidth: 720 }}>
|
|
72
|
+
<div style={{ border: '1px solid #ddd', padding: 12, minHeight: 200 }}>
|
|
73
|
+
{messages.map((m) => (
|
|
74
|
+
<div key={m.id} style={{ padding: 8, borderBottom: '1px solid #eee' }}>
|
|
75
|
+
<div style={{ fontSize: 12, color: '#666' }}>{m.sender} • {new Date(m.ts).toLocaleTimeString()}</div>
|
|
76
|
+
<div>{m.text}</div>
|
|
77
|
+
</div>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div style={{ display: 'flex', marginTop: 12, gap: 8 }}>
|
|
82
|
+
<input placeholder="To (WhatsApp number, optional)" value={to} onChange={(e) => setTo(e.target.value)} style={{ width: 220, padding: 8 }} />
|
|
83
|
+
<input value={text} onChange={(e) => setText(e.target.value)} style={{ flex: 1, padding: 8 }} />
|
|
84
|
+
<button onClick={send} style={{ padding: '8px 12px' }}>Send</button>
|
|
85
|
+
</div>
|
|
86
|
+
<div style={{ display: 'flex', marginTop: 8, gap: 8 }}>
|
|
87
|
+
<input placeholder="Media URL (optional)" value={mediaUrl} onChange={(e) => setMediaUrl(e.target.value)} style={{ flex: 1, padding: 8 }} />
|
|
88
|
+
<button onClick={sendMedia} style={{ padding: '8px 12px' }}>Send Media</button>
|
|
89
|
+
</div>
|
|
90
|
+
<div style={{ display: 'flex', marginTop: 8, gap: 8 }}>
|
|
91
|
+
<input placeholder="Template name" value={templateName} onChange={(e) => setTemplateName(e.target.value)} style={{ width: 240, padding: 8 }} />
|
|
92
|
+
<input placeholder="lang" value={templateLang} onChange={(e) => setTemplateLang(e.target.value)} style={{ width: 100, padding: 8 }} />
|
|
93
|
+
<button onClick={sendTemplate} style={{ padding: '8px 12px' }}>Send Template</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</main>
|
|
97
|
+
)
|
|
98
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bizlin-chat",
|
|
3
|
+
"version": "0.0.1-a1",
|
|
4
|
+
"description": "zchat npm plugin (integrates backend and frontend)",
|
|
5
|
+
"main": "backend/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"backend",
|
|
8
|
+
"frontend"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node backend/index.js",
|
|
12
|
+
"start:frontend": "node frontend/index.js",
|
|
13
|
+
"test": "echo \"No tests specified\" && exit 0"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"zchat",
|
|
17
|
+
"plugin",
|
|
18
|
+
"chat"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/bizlinsolutions/zchat.git"
|
|
26
|
+
},
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=14"
|
|
31
|
+
}
|
|
32
|
+
}
|