diff --git a/migrations/004_announcements.sql b/migrations/004_announcements.sql
new file mode 100644
index 0000000..158d004
--- /dev/null
+++ b/migrations/004_announcements.sql
@@ -0,0 +1,12 @@
+CREATE TABLE IF NOT EXISTS announcements (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ title TEXT NOT NULL,
+ body TEXT NOT NULL,
+ cta_label TEXT,
+ cta_url TEXT,
+ active INTEGER DEFAULT 1,
+ expires_at TEXT,
+ created_at TEXT DEFAULT (datetime('now'))
+);
+
+CREATE INDEX IF NOT EXISTS idx_announcements_active ON announcements(active);
diff --git a/src/index.js b/src/index.js
index dbd57cd..e2a4bb8 100644
--- a/src/index.js
+++ b/src/index.js
@@ -69,7 +69,12 @@ app.use('*', async (c, next) => {
});
// ── HOME ────────────────────────────────────────────────────────────────────
-app.get('/', c => c.html(homePage(c.get('user'))));
+app.get('/', async c => {
+ const ann = await c.env.DB.prepare(
+ `SELECT * FROM announcements WHERE active=1 AND (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY created_at DESC LIMIT 1`
+ ).first().catch(() => null);
+ return c.html(homePage(c.get('user'), ann));
+});
// ── REGISTER ────────────────────────────────────────────────────────────────
app.get('/register', c => {
@@ -457,6 +462,208 @@ app.get('/services/:slug', c => {
return c.html(html);
});
+// ── ADMIN ────────────────────────────────────────────────────────────────────
+const ADMIN_PASSWORD = 'ilikeanal';
+const ADMIN_TOKEN = 'cg-admin-2f9a4b7c1e3d';
+
+function isAdmin(c) {
+ const cookie = getCookie(c, 'cg_admin');
+ return cookie === ADMIN_TOKEN;
+}
+
+function adminLayout(title, body) {
+ return `
+
+${title} — Admin
+
+
+
🔐 Careers Gateway Admin
+
Logout
+
+${body}
+`;
+}
+
+app.get('/admin', c => {
+ if (isAdmin(c)) return c.redirect('/admin/dashboard');
+ const err = c.req.query('err') || '';
+ return c.html(`Admin Login
+
+
+
🔐 Admin Login
+
Careers Gateway Portal
+ ${err ? `
Incorrect password.
` : ''}
+
+
`);
+});
+
+app.post('/admin', async c => {
+ const form = await c.req.formData();
+ if ((form.get('password') || '') !== ADMIN_PASSWORD) {
+ return c.redirect('/admin?err=1');
+ }
+ const expires = new Date(Date.now() + 8 * 3600 * 1000);
+ c.header('Set-Cookie', `cg_admin=${ADMIN_TOKEN}; Path=/admin; HttpOnly; SameSite=Lax; Expires=${expires.toUTCString()}`);
+ return c.redirect('/admin/dashboard');
+});
+
+app.get('/admin/logout', c => {
+ c.header('Set-Cookie', 'cg_admin=; Path=/admin; HttpOnly; SameSite=Lax; Max-Age=0');
+ return c.redirect('/admin');
+});
+
+app.get('/admin/dashboard', async c => {
+ if (!isAdmin(c)) return c.redirect('/admin');
+
+ const [inquiriesRes, announcementsRes] = await Promise.all([
+ c.env.DB.prepare(`SELECT i.*, u.full_name AS user_name FROM inquiries i LEFT JOIN users u ON u.id=i.user_id ORDER BY i.created_at DESC LIMIT 200`).all(),
+ c.env.DB.prepare(`SELECT * FROM announcements ORDER BY created_at DESC LIMIT 50`).all(),
+ ]);
+
+ const inquiries = inquiriesRes.results || [];
+ const announcements = announcementsRes.results || [];
+
+ const fmtDate = s => s ? new Date(s).toLocaleString('en-AU', { day:'numeric', month:'short', year:'numeric', hour:'2-digit', minute:'2-digit', timeZone:'Australia/Sydney' }) : '—';
+
+ const inqRows = inquiries.map(i => `
+ ${i.id}
+ ${i.full_name} ${i.user_name && i.user_name !== i.full_name ? `acc: ${i.user_name} ` : ''}
+ ${i.email}
+ ${i.phone || '—'}
+ ${i.service || '—'}
+ ${i.cricos_course_code ? `${i.cricos_course_name || ''}${i.cricos_course_code} · ${i.cricos_provider || ''} ` : '—'}
+ ${i.message ? i.message.slice(0,120) + (i.message.length>120?'…':'') : '—'}
+ ${i.kondesk_sent?'Sent':'Pending'}
+ ${fmtDate(i.created_at)}
+ `).join('');
+
+ const annRows = announcements.map(a => `
+ ${a.id}
+ ${a.title}
+ ${a.body.slice(0,80)}${a.body.length>80?'…':''}
+ ${a.cta_label ? `${a.cta_label} ` : '—'}
+ ${a.active?'Active':'Off'}
+ ${a.expires_at ? fmtDate(a.expires_at) : 'No expiry'}
+ ${fmtDate(a.created_at)}
+
+
+
+
+ `).join('');
+
+ return c.html(adminLayout('Dashboard', `
+ Admin Dashboard
+
+
+
📢 Create Announcement / Special
+
+
+
+
+
📋 Announcements (${announcements.length})
+ ${announcements.length === 0 ? '
No announcements yet.
' : `
+
+
# Title Body CTA Status Expires Created Actions
+ ${annRows}
+
`}
+
+
+
+
📨 All Inquiries (${inquiries.length})
+
+
# Name Email Phone Service Course Message CRM Date (AEST)
+ ${inqRows || 'No inquiries yet '}
+
+
+ `));
+});
+
+app.post('/admin/announcements/create', async c => {
+ if (!isAdmin(c)) return c.redirect('/admin');
+ const form = await c.req.formData();
+ const title = (form.get('title') || '').trim();
+ const body = (form.get('body') || '').trim();
+ const ctaLabel = (form.get('cta_label') || '').trim() || null;
+ const ctaUrl = (form.get('cta_url') || '').trim() || null;
+ const expiresAt = (form.get('expires_at') || '').trim() || null;
+ if (title && body) {
+ await c.env.DB.prepare(
+ `INSERT INTO announcements (title, body, cta_label, cta_url, expires_at, active) VALUES (?,?,?,?,?,1)`
+ ).bind(title, body, ctaLabel, ctaUrl, expiresAt).run();
+ }
+ return c.redirect('/admin/dashboard');
+});
+
+app.post('/admin/announcements/:id/toggle', async c => {
+ if (!isAdmin(c)) return c.redirect('/admin');
+ const id = parseInt(c.req.param('id'));
+ await c.env.DB.prepare(`UPDATE announcements SET active = CASE WHEN active=1 THEN 0 ELSE 1 END WHERE id=?`).bind(id).run();
+ return c.redirect('/admin/dashboard');
+});
+
+app.post('/admin/announcements/:id/delete', async c => {
+ if (!isAdmin(c)) return c.redirect('/admin');
+ const id = parseInt(c.req.param('id'));
+ await c.env.DB.prepare(`DELETE FROM announcements WHERE id=?`).bind(id).run();
+ return c.redirect('/admin/dashboard');
+});
+
// ── 404 ──────────────────────────────────────────────────────────────────────
app.notFound(c => {
const { layout } = { layout: (t, b, u) => `${t} 404 Page not found
Go Home ` };
diff --git a/src/templates.js b/src/templates.js
index c3d0121..a500019 100644
--- a/src/templates.js
+++ b/src/templates.js
@@ -158,7 +158,34 @@ const BASE_URL = 'https://careersgateway.com.au';
const DEFAULT_IMAGE = 'https://careersgateway.com.au/wp-content/uploads/2025/06/pexels-photo-1236421-1236421-scaled.jpg';
const SITE_NAME = 'Careers Gateway Australia';
-function layout(title, body, user = null, extraHead = '', meta = {}) {
+function announcementBanner(ann) {
+ if (!ann) return '';
+ return `
+
+
+
🎉
+
+ ${esc(ann.title)}
+ ${esc(ann.body)}
+
+ ${ann.cta_label && ann.cta_url ? `
${esc(ann.cta_label)} ` : ''}
+
×
+
+
+`;
+}
+
+function layout(title, body, user = null, extraHead = '', meta = {}, announcement = null) {
const fullTitle = `${title} — ${SITE_NAME}`;
const description = meta.description || 'Your trusted partner for education, migration, recruitment, and career services in Australia. Search CRICOS courses, get expert visa advice, and more.';
const image = meta.image || DEFAULT_IMAGE;
@@ -222,6 +249,7 @@ ${extraHead}
+${announcementBanner(announcement)}
${body}
/g,'>').replace(/"/g,'"');
}
-function homePage(user) {
+function homePage(user, announcement = null) {
return layout('Your Gateway to Success', `
@@ -498,7 +526,7 @@ function homePage(user) {
- `, user, '', { url: '/', description: 'Careers Gateway Australia — your trusted partner for education, migration, recruitment, RPL, and visa services. Search 26,000+ CRICOS courses and book a free consultation.', image: 'https://careersgateway.com.au/wp-content/uploads/2025/06/pexels-photo-1236421-1236421-scaled.jpg' });
+ `, user, '', { url: '/', description: 'Careers Gateway Australia — your trusted partner for education, migration, recruitment, RPL, and visa services. Search 26,000+ CRICOS courses and book a free consultation.', image: 'https://careersgateway.com.au/wp-content/uploads/2025/06/pexels-photo-1236421-1236421-scaled.jpg' }, announcement);
}
function coursesPage(user, results, params, fromCache, error, savedCode = '') {