Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
92 changes: 92 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,94 @@ 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) => {
try {
const res = await fetch(`/api/admin/email-recipients-count?filter=${selectedFilter}`);
const json = await res.json();
recipientCount.value = json.count;
} catch {
// silently ignore; count remains at last known value
}
};

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', {
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