Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"fileupload",
"Foxbiz",
"gapis",
"googleplay",
"hangingpiece",
"hideloading",
"hilightjs",
Expand Down
3 changes: 1 addition & 2 deletions client/jsconfig.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"*": [
"*"
"./src/*"
],
}
}
Expand Down
88 changes: 88 additions & 0 deletions client/pages/admin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default async function Admin() {
<h1>Admin Panel</h1>
<Dashboard />
<Users />
<EmailUsers />
</section>
);
}
Expand Down Expand Up @@ -194,3 +195,90 @@ async function deleteUser(id) {
alert('Success', 'User deleted successfully');
}
}

function EmailUsers() {
const recipientCount = Reactive(0);
const sendBtn = Ref();
let filter = 'all';
let subject = '';
let message = '';

const fetchCount = async (selectedFilter) => {
const res = await fetch(`api/admin/email-recipients-count?filter=${selectedFilter}`);
Comment thread
deadlyjack marked this conversation as resolved.
Outdated
const json = await res.json();
recipientCount.value = json.count;
};
Comment thread
deadlyjack marked this conversation as resolved.

fetchCount(filter);

const onFilterChange = (e) => {
filter = e.target.value;
fetchCount(filter);
};

const onSend = async () => {
if (!subject.trim() || !message.trim()) {
alert('ERROR', 'Subject and message are required');
return;
}
const confirmation = await confirm('Confirm', `Send email to ${recipientCount.value} recipient(s)?`);
if (!confirmation) return;
sendBtn.disabled = true;
sendBtn.textContent = 'Sending...';
try {
const res = await fetch('api/admin/send-email', {
Comment thread
deadlyjack marked this conversation as resolved.
Outdated
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filter, subject, message }),
});
const json = await res.json();
if (json.error) {
alert('ERROR', json.error);
} else {
alert('Success', `Email sent to ${json.sent} user(s)`);
}
} catch {
alert('ERROR', 'Failed to send emails');
} finally {
sendBtn.disabled = false;
sendBtn.textContent = 'Send Email';
}
};

return (
<div className='email-users'>
<h2>Email Users</h2>
<div className='email-form'>
<div className='form-group'>
<label>Recipients</label>
<select onchange={onFilterChange}>
<option value='all'>All Users</option>
<option value='with_plugins'>Users with Plugins</option>
<option value='with_paid_plugins'>Users with Paid Plugins</option>
<option value='with_payment'>Users who Received Payment</option>
</select>
<small>{recipientCount} recipient(s) will receive this email</small>
</div>
<Input
label='Subject'
placeholder='Email subject'
oninput={(e) => {
subject = e.target.value;
}}
/>
<div className='form-group'>
<label>Message</label>
<textarea
placeholder='Email message...'
oninput={(e) => {
message = e.target.value;
}}
/>
</div>
<button ref={sendBtn} type='button' onclick={onSend} className='send-btn'>
Send Email
</button>
</div>
</div>
);
}
81 changes: 80 additions & 1 deletion client/pages/admin/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
&:empty {
align-items: center;
justify-content: center;

&::before {
content: '';
height: 60px;
Expand All @@ -34,6 +35,7 @@
0% {
transform: rotate(0deg);
}

100% {
transform: rotate(360deg);
}
Expand Down Expand Up @@ -74,6 +76,7 @@

table {
max-width: unset;

td {
width: fit-content;
}
Expand All @@ -82,4 +85,80 @@
.table-container {
overflow: auto;
}
}

.email-users {
margin-top: 32px;

h2 {
margin: 0 0 16px;
}

.email-form {
display: flex;
flex-direction: column;
gap: 16px;
max-width: 600px;
}

.form-group {
display: flex;
flex-direction: column;
gap: 6px;

label {
font-weight: 600;
}

select {
padding: 8px 12px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: var(--secondary-color);
color: inherit;
font-size: 1em;
line-height: 1.5;
height: auto;
appearance: auto;

option {
line-height: 1.5;
padding: 4px 0;
background-color: var(--secondary-color);
color: inherit;
}
}

small {
opacity: 0.7;
}

textarea {
padding: 10px 12px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: var(--secondary-color);
color: inherit;
font-size: 1em;
min-height: 120px;
resize: vertical;
font-family: inherit;
}
}

.send-btn {
align-self: flex-start;
padding: 10px 24px;
border-radius: 8px;
background-color: var(--primary-color);
color: #fff;
border: none;
font-size: 1em;
cursor: pointer;

&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
}
}
2 changes: 1 addition & 1 deletion client/pages/home/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default async function home() {
logo='https://academy.acode.app/icon.png'
alt='Acode Academy'
title='Acode Academy'
subtitle='Learn to build Acode plugins and master the editor — interactive courses by the creator'
subtitle='Best-crafted courses, hands-on exercises, and progress tracking — right inside Acode. Earn a certificate when you finish.'
url='https://academy.acode.app'
cta='Explore Courses'
/>
Expand Down
24 changes: 24 additions & 0 deletions client/pages/plugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export default async function Plugin({ id: pluginId, section = 'description' })
};
const $orders = <Order />;
const shouldShowOrders = user && (user.id === userId || user.isAdmin) && !!plugin.price;
const isSelf = user && user.id === userId;

let canInstall = /android/i.test(navigator.userAgent);

Expand Down Expand Up @@ -177,6 +178,18 @@ export default async function Plugin({ id: pluginId, section = 'description' })
</div>
</div>
</div>
{isSelf && status === 'deleted' && price > 0 && (
<div className='paid-plugin-notice'>
<div className='notice-header'>
<span className='icon warning' />
<strong>Paid plugins discontinued</strong>
</div>
<p>This plugin was deactivated because paid plugins are no longer supported. Make it free to restore it to the store.</p>
<button type='button' className='make-free-btn' onclick={makeFree}>
Make Free &amp; Restore
</button>
</div>
)}
<div className='detailed'>
<div
className='options'
Expand Down Expand Up @@ -206,6 +219,17 @@ export default async function Plugin({ id: pluginId, section = 'description' })
</section>
);

async function makeFree() {
const confirmed = await confirm('Make Plugin Free', 'This will set your plugin price to \u20B90 (free) and restore it to the store. Continue?');
if (!confirmed) return;
const res = await fetch(`/api/plugin/${id}/make-free`, { method: 'PATCH' }).then((r) => r.json());
if (res.error) {
alert('Error', res.error);
return;
}
alert('Success', 'Your plugin is now free and has been restored to the store.', () => Router.loadUrl(`/plugin/${id}`), true);
}

/**
*
* @param {'comments' | 'description' | 'changelogs' | 'orders'} sectionName
Expand Down
56 changes: 56 additions & 0 deletions client/pages/plugin/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,62 @@
height: 100%;
}

.paid-plugin-notice {
margin: 16px 0;
padding: 16px 20px;
background: rgba(245, 158, 11, 0.12);
border: 1px solid rgba(245, 158, 11, 0.5);
border-left: 4px solid #f59e0b;
border-radius: 10px;

.notice-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;

.icon {
color: #f59e0b;
font-size: 20px;
}

strong {
font-size: 15px;
color: #f59e0b;
}
}

p {
margin: 0 0 14px;
font-size: 14px;
line-height: 1.5;
color: var(--secondary-text-color);
}

.make-free-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 20px;
background: #f59e0b;
color: #000;
font-weight: 600;
font-size: 14px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s ease;

&:hover {
background: #d97706;
}

&:active {
background: #b45309;
}
}
}

.verified {
color: #39f;
}
Expand Down
19 changes: 7 additions & 12 deletions client/pages/publishPlugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export default async function PublishPlugin({ mode = 'publish', id }) {
const pluginName = Reactive(plugin?.name);
const license = Reactive(plugin?.license);
const pluginVersion = Reactive(plugin?.version);
const pluginPrice = Reactive(+plugin?.price ? `INR ${plugin.price}` : 'Free');
const keywords = Reactive(plugin?.keywords && json(plugin.keywords)?.join(', '));
const contributors = Reactive(
plugin?.contributors &&
Expand Down Expand Up @@ -66,6 +65,13 @@ export default async function PublishPlugin({ mode = 'publish', id }) {
<section id='publish-plugin'>
<h1 style={{ textAlign: 'center' }}>{capitalize(mode)} plugin</h1>

<div className='paid-plugin-notice update-banner'>
<div className='icon-wrapper'>
<span className='icon warning' />
<span>Paid plugin support has been discontinued. Plugins submitted with a price will be rejected.</span>
</div>
</div>

{mode === 'update' && (
<div className='update-banner'>
<div className='icon-wrapper'>
Expand Down Expand Up @@ -156,10 +162,6 @@ export default async function PublishPlugin({ mode = 'publish', id }) {
<th>Min Version Code</th>
<td>{minVersionCode}</td>
</tr>
<tr>
<th>Price</th>
<td>{pluginPrice}</td>
</tr>
<tr>
<th>Icon</th>
<td>{pluginIcon}</td>
Expand Down Expand Up @@ -208,7 +210,6 @@ export default async function PublishPlugin({ mode = 'publish', id }) {
pluginName.value = '';
pluginVersion.value = '';
pluginAuthor.value = '';
pluginPrice.value = '';
pluginIcon.src = '#';
minVersionCode.value = '';
submitButton.el.disabled = true;
Expand Down Expand Up @@ -274,12 +275,6 @@ export default async function PublishPlugin({ mode = 'publish', id }) {
updateType.value = `(${getUpdateType(manifest.version, plugin.version)} from ${plugin.version})`;
}

if (+manifest.price) {
pluginPrice.value = `INR ${manifest.price || 0}`;
} else {
pluginPrice.value = 'Free';
}

pluginId.value = manifest.id;
pluginName.value = manifest.name;
pluginVersion.value = manifest.version;
Expand Down
Loading