Skip to content

Commit a167f06

Browse files
authored
Merge pull request #36 from Acode-Foundation/fix-promo-feat-email
Add email functionality for user management in admin panel
2 parents e6044c5 + 30445e4 commit a167f06

6 files changed

Lines changed: 248 additions & 4 deletions

File tree

client/jsconfig.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
{
22
"compilerOptions": {
3-
"baseUrl": "./",
43
"paths": {
54
"*": [
6-
"*"
5+
"./src/*"
76
],
87
}
98
}

client/pages/admin/index.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default async function Admin() {
1919
<h1>Admin Panel</h1>
2020
<Dashboard />
2121
<Users />
22+
<EmailUsers />
2223
</section>
2324
);
2425
}
@@ -194,3 +195,90 @@ async function deleteUser(id) {
194195
alert('Success', 'User deleted successfully');
195196
}
196197
}
198+
199+
function EmailUsers() {
200+
const recipientCount = Reactive(0);
201+
const sendBtn = Ref();
202+
let filter = 'all';
203+
let subject = '';
204+
let message = '';
205+
206+
const fetchCount = async (selectedFilter) => {
207+
const res = await fetch(`api/admin/email-recipients-count?filter=${selectedFilter}`);
208+
const json = await res.json();
209+
recipientCount.value = json.count;
210+
};
211+
212+
fetchCount(filter);
213+
214+
const onFilterChange = (e) => {
215+
filter = e.target.value;
216+
fetchCount(filter);
217+
};
218+
219+
const onSend = async () => {
220+
if (!subject.trim() || !message.trim()) {
221+
alert('ERROR', 'Subject and message are required');
222+
return;
223+
}
224+
const confirmation = await confirm('Confirm', `Send email to ${recipientCount.value} recipient(s)?`);
225+
if (!confirmation) return;
226+
sendBtn.disabled = true;
227+
sendBtn.textContent = 'Sending...';
228+
try {
229+
const res = await fetch('api/admin/send-email', {
230+
method: 'POST',
231+
headers: { 'Content-Type': 'application/json' },
232+
body: JSON.stringify({ filter, subject, message }),
233+
});
234+
const json = await res.json();
235+
if (json.error) {
236+
alert('ERROR', json.error);
237+
} else {
238+
alert('Success', `Email sent to ${json.sent} user(s)`);
239+
}
240+
} catch {
241+
alert('ERROR', 'Failed to send emails');
242+
} finally {
243+
sendBtn.disabled = false;
244+
sendBtn.textContent = 'Send Email';
245+
}
246+
};
247+
248+
return (
249+
<div className='email-users'>
250+
<h2>Email Users</h2>
251+
<div className='email-form'>
252+
<div className='form-group'>
253+
<label>Recipients</label>
254+
<select onchange={onFilterChange}>
255+
<option value='all'>All Users</option>
256+
<option value='with_plugins'>Users with Plugins</option>
257+
<option value='with_paid_plugins'>Users with Paid Plugins</option>
258+
<option value='with_payment'>Users who Received Payment</option>
259+
</select>
260+
<small>{recipientCount} recipient(s) will receive this email</small>
261+
</div>
262+
<Input
263+
label='Subject'
264+
placeholder='Email subject'
265+
oninput={(e) => {
266+
subject = e.target.value;
267+
}}
268+
/>
269+
<div className='form-group'>
270+
<label>Message</label>
271+
<textarea
272+
placeholder='Email message...'
273+
oninput={(e) => {
274+
message = e.target.value;
275+
}}
276+
/>
277+
</div>
278+
<button ref={sendBtn} type='button' onclick={onSend} className='send-btn'>
279+
Send Email
280+
</button>
281+
</div>
282+
</div>
283+
);
284+
}

client/pages/admin/style.scss

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
&:empty {
2222
align-items: center;
2323
justify-content: center;
24+
2425
&::before {
2526
content: '';
2627
height: 60px;
@@ -34,6 +35,7 @@
3435
0% {
3536
transform: rotate(0deg);
3637
}
38+
3739
100% {
3840
transform: rotate(360deg);
3941
}
@@ -74,6 +76,7 @@
7476

7577
table {
7678
max-width: unset;
79+
7780
td {
7881
width: fit-content;
7982
}
@@ -82,4 +85,80 @@
8285
.table-container {
8386
overflow: auto;
8487
}
85-
}
88+
89+
.email-users {
90+
margin-top: 32px;
91+
92+
h2 {
93+
margin: 0 0 16px;
94+
}
95+
96+
.email-form {
97+
display: flex;
98+
flex-direction: column;
99+
gap: 16px;
100+
max-width: 600px;
101+
}
102+
103+
.form-group {
104+
display: flex;
105+
flex-direction: column;
106+
gap: 6px;
107+
108+
label {
109+
font-weight: 600;
110+
}
111+
112+
select {
113+
padding: 8px 12px;
114+
border-radius: 8px;
115+
border: 1px solid rgba(255, 255, 255, 0.2);
116+
background-color: var(--secondary-color);
117+
color: inherit;
118+
font-size: 1em;
119+
line-height: 1.5;
120+
height: auto;
121+
appearance: auto;
122+
123+
option {
124+
line-height: 1.5;
125+
padding: 4px 0;
126+
background-color: var(--secondary-color);
127+
color: inherit;
128+
}
129+
}
130+
131+
small {
132+
opacity: 0.7;
133+
}
134+
135+
textarea {
136+
padding: 10px 12px;
137+
border-radius: 8px;
138+
border: 1px solid rgba(255, 255, 255, 0.2);
139+
background-color: var(--secondary-color);
140+
color: inherit;
141+
font-size: 1em;
142+
min-height: 120px;
143+
resize: vertical;
144+
font-family: inherit;
145+
}
146+
}
147+
148+
.send-btn {
149+
align-self: flex-start;
150+
padding: 10px 24px;
151+
border-radius: 8px;
152+
background-color: var(--primary-color);
153+
color: #fff;
154+
border: none;
155+
font-size: 1em;
156+
cursor: pointer;
157+
158+
&:disabled {
159+
opacity: 0.6;
160+
cursor: not-allowed;
161+
}
162+
}
163+
}
164+
}

client/pages/home/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export default async function home() {
107107
logo='https://academy.acode.app/icon.png'
108108
alt='Acode Academy'
109109
title='Acode Academy'
110-
subtitle='Learn to build Acode plugins and master the editor — interactive courses by the creator'
110+
subtitle='Best-crafted courses, hands-on exercises, and progress tracking — right inside Acode. Earn a certificate when you finish.'
111111
url='https://academy.acode.app'
112112
cta='Explore Courses'
113113
/>

server/apis/admin.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,33 @@ router.delete('/user/:id', async (req, res) => {
116116
res.send(user);
117117
});
118118

119+
const ALLOWED_FILTERS = ['all', 'with_plugins', 'with_paid_plugins', 'with_payment'];
120+
121+
router.get('/email-recipients-count', async (req, res) => {
122+
const { filter = 'all' } = req.query;
123+
if (!ALLOWED_FILTERS.includes(filter)) {
124+
res.status(400).send({ error: 'Invalid filter' });
125+
return;
126+
}
127+
const count = await User.countUsersByFilter(filter);
128+
res.send({ count });
129+
});
130+
131+
router.post('/send-email', async (req, res) => {
132+
const { filter = 'all', subject, message } = req.body;
133+
if (!ALLOWED_FILTERS.includes(filter)) {
134+
res.status(400).send({ error: 'Invalid filter' });
135+
return;
136+
}
137+
if (!subject?.trim() || !message?.trim()) {
138+
res.status(400).send({ error: 'Subject and message are required' });
139+
return;
140+
}
141+
const users = await User.getUsersByFilter(filter);
142+
for (const user of users) {
143+
await sendEmail(user.email, user.name, subject.trim(), message.trim());
144+
}
145+
res.send({ sent: users.length });
146+
});
147+
119148
module.exports = router;

server/entities/user.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,55 @@ class User extends Entity {
6767
return super.delete(where, operator);
6868
}
6969

70+
getUsersByFilter(filter) {
71+
let sql;
72+
switch (filter) {
73+
case 'with_plugins':
74+
sql = `SELECT DISTINCT u.name, u.email FROM user u
75+
INNER JOIN plugin p ON u.id = p.user_id
76+
WHERE u.role != 'admin' AND p.status != 3`;
77+
break;
78+
case 'with_paid_plugins':
79+
sql = `SELECT DISTINCT u.name, u.email FROM user u
80+
INNER JOIN plugin p ON u.id = p.user_id
81+
WHERE u.role != 'admin' AND p.price > 0 AND p.status != 3`;
82+
break;
83+
case 'with_payment':
84+
sql = `SELECT DISTINCT u.name, u.email FROM user u
85+
INNER JOIN payment pay ON u.id = pay.user_id
86+
WHERE u.role != 'admin' AND pay.status = 1`;
87+
break;
88+
default:
89+
sql = `SELECT name, email FROM user WHERE role != 'admin'`;
90+
}
91+
return Entity.execSql(sql, [], this);
92+
}
93+
94+
async countUsersByFilter(filter) {
95+
let sql;
96+
switch (filter) {
97+
case 'with_plugins':
98+
sql = `SELECT COUNT(DISTINCT u.id) as count FROM user u
99+
INNER JOIN plugin p ON u.id = p.user_id
100+
WHERE u.role != 'admin' AND p.status != 3`;
101+
break;
102+
case 'with_paid_plugins':
103+
sql = `SELECT COUNT(DISTINCT u.id) as count FROM user u
104+
INNER JOIN plugin p ON u.id = p.user_id
105+
WHERE u.role != 'admin' AND p.price > 0 AND p.status != 3`;
106+
break;
107+
case 'with_payment':
108+
sql = `SELECT COUNT(DISTINCT u.id) as count FROM user u
109+
INNER JOIN payment pay ON u.id = pay.user_id
110+
WHERE u.role != 'admin' AND pay.status = 1`;
111+
break;
112+
default:
113+
sql = `SELECT COUNT(*) as count FROM user WHERE role != 'admin'`;
114+
}
115+
const [{ count }] = await Entity.execSql(sql, [], this);
116+
return count;
117+
}
118+
70119
get columns() {
71120
return [
72121
this.ID,

0 commit comments

Comments
 (0)