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.

' : ` +
+ + ${annRows}
#TitleBodyCTAStatusExpiresCreatedActions
+
`} +
+ +
+

📨 All Inquiries (${inquiries.length})

+
+ + ${inqRows || ''}
#NameEmailPhoneServiceCourseMessageCRMDate (AEST)
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 = '') {