diff --git a/.env.example b/.env.example deleted file mode 100644 index 1db6fca..0000000 --- a/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -# Database Configuration -DB_HOST=82.25.95.117 -DB_USER=user_tamilculturewaterloo -DB_PASSWORD=Metatron@2025 -DB_NAME=user_tamilculturewaterloo - -# Server Configuration -PORT=3006 - -DB_CONNECTION_LIMIT=20 -DB_ACQUIRE_TIMEOUT=60000 -DB_TIMEOUT=60000 -DB_IDLE_TIMEOUT=300000 \ No newline at end of file diff --git a/server.js b/server.js index fbf4218..425fea5 100644 --- a/server.js +++ b/server.js @@ -157,6 +157,120 @@ async function ensureEventImagesSortOrderColumn() { throw error; } } + +async function ensureUpcomingEventsTable() { + try { + await pool.execute(` + CREATE TABLE IF NOT EXISTS upcoming_events ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255), + eventtitle VARCHAR(255), + slug VARCHAR(255), + date VARCHAR(255), + eventdate VARCHAR(255), + time VARCHAR(255), + location VARCHAR(255), + image TEXT, + eventimageurl TEXT, + link TEXT, + btn_text VARCHAR(255) DEFAULT 'Details Coming Soon', + btnText VARCHAR(255) DEFAULT 'Details Coming Soon', + description TEXT, + eventdescription TEXT, + admission VARCHAR(255), + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + + const [columns] = await pool.execute("SHOW COLUMNS FROM upcoming_events"); + const colNames = columns.map(col => col.Field); + + if (colNames.includes('year')) { + try { + await pool.execute("ALTER TABLE upcoming_events MODIFY COLUMN year INT NULL DEFAULT 2026"); + } catch (err) { + console.warn('Could not modify year column:', err.message); + } + } + + const columnsToAdd = [ + { name: 'title', type: 'VARCHAR(255)' }, + { name: 'date', type: 'VARCHAR(255)' }, + { name: 'image', type: 'TEXT' }, + { name: 'description', type: 'TEXT' }, + { name: 'btn_text', type: "VARCHAR(255) DEFAULT 'Details Coming Soon'" }, + { name: 'admission', type: 'VARCHAR(255)' }, + { name: 'sort_order', type: 'INT DEFAULT 0' } + ]; + + for (const col of columnsToAdd) { + if (!colNames.includes(col.name)) { + await pool.execute(`ALTER TABLE upcoming_events ADD COLUMN ${col.name} ${col.type}`); + } + } + + console.log('✅ Verified upcoming_events table'); + + const [existing] = await pool.execute( + 'SELECT id FROM upcoming_events WHERE title = ? OR eventtitle = ?', + ['KW Multicultural Festival', 'KW Multicultural Festival'] + ); + + if (existing.length === 0) { + const hasYearCol = colNames.includes('year'); + if (hasYearCol) { + await pool.execute( + `INSERT INTO upcoming_events (year, title, eventtitle, slug, date, eventdate, time, location, image, eventimageurl, link, btn_text, btnText, description, eventdescription) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + 2026, + 'KW Multicultural Festival', + 'KW Multicultural Festival', + 'kw-multicultural-festival-2026', + 'Jun 20, 2026 and Jun 21, 2026', + 'Jun 20, 2026 and Jun 21, 2026', + 'Details will be announced', + 'Victoria Park, Kitchener', + '/assets/img/event/upcoming-event/multicultural-festival.webp', + '/assets/img/event/upcoming-event/multicultural-festival.webp', + '/upcoming-event/kw-multicultural-festival-2026', + 'Details Coming Soon', + 'Details Coming Soon', + 'KW Multicultural Festival will take place on Jun 20-21, 2026 at Victoria Park, Kitchener. More details will be updated soon.', + 'KW Multicultural Festival will take place on Jun 20-21, 2026 at Victoria Park, Kitchener. More details will be updated soon.' + ] + ); + } else { + await pool.execute( + `INSERT INTO upcoming_events (title, eventtitle, slug, date, eventdate, time, location, image, eventimageurl, link, btn_text, btnText, description, eventdescription) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + 'KW Multicultural Festival', + 'KW Multicultural Festival', + 'kw-multicultural-festival-2026', + 'Jun 20, 2026 and Jun 21, 2026', + 'Jun 20, 2026 and Jun 21, 2026', + 'Details will be announced', + 'Victoria Park, Kitchener', + '/assets/img/event/upcoming-event/multicultural-festival.webp', + '/assets/img/event/upcoming-event/multicultural-festival.webp', + '/upcoming-event/kw-multicultural-festival-2026', + 'Details Coming Soon', + 'Details Coming Soon', + 'KW Multicultural Festival will take place on Jun 20-21, 2026 at Victoria Park, Kitchener. More details will be updated soon.', + 'KW Multicultural Festival will take place on Jun 20-21, 2026 at Victoria Park, Kitchener. More details will be updated soon.' + ] + ); + } + console.log('✅ Seeded initial upcoming event: KW Multicultural Festival'); + } + } catch (error) { + console.error('❌ Failed to ensure upcoming_events table:', error.message); + throw error; + } +} + // Test database connection on startup async function testDatabaseConnection() { try { @@ -166,6 +280,7 @@ async function testDatabaseConnection() { connection.release(); await ensureAdminUsersTable(); await ensureEventImagesSortOrderColumn(); + await ensureUpcomingEventsTable(); } catch (error) { console.error('❌ Database connection failed:', error.message); process.exit(1); @@ -548,6 +663,253 @@ app.delete('/api/events/:id', async (req, res) => { } }); +// =============================== +// UPCOMING EVENTS CRUD ROUTES +// =============================== + +// Helper function to slugify string +function slugify(text) { + if (!text) return ''; + return text + .toString() + .toLowerCase() + .trim() + .replace(/\s+/g, '-') // Replace spaces with - + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '-'); // Replace multiple - with single - +} + +// GET all upcoming events +app.get('/api/upcoming-events', async (req, res) => { + try { + const [rows] = await pool.execute('SELECT * FROM upcoming_events ORDER BY sort_order ASC, id DESC'); + res.json({ + success: true, + data: rows + }); + } catch (error) { + console.error('Error fetching upcoming events:', error); + res.status(500).json({ + success: false, + message: 'Error fetching upcoming events', + error: error.message + }); + } +}); + +// GET single upcoming event by ID +app.get('/api/upcoming-events/:id', async (req, res) => { + try { + const [rows] = await pool.execute('SELECT * FROM upcoming_events WHERE id = ?', [req.params.id]); + if (rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Upcoming event not found' + }); + } + res.json({ + success: true, + data: rows[0] + }); + } catch (error) { + console.error('Error fetching upcoming event:', error); + res.status(500).json({ + success: false, + message: 'Error fetching upcoming event', + error: error.message + }); + } +}); + +// GET single upcoming event by slug or ID +app.get('/api/upcoming-events/slug/:slug', async (req, res) => { + try { + const rawSlug = req.params.slug ? req.params.slug.trim() : ''; + const cleanedSlug = slugify(rawSlug); + + // 1. First priority: exact slug or ID match + let [rows] = await pool.execute( + 'SELECT * FROM upcoming_events WHERE slug = ? OR slug = ? OR id = ?', + [rawSlug, cleanedSlug, rawSlug] + ); + + // 2. Second priority: title slug match fallback + if (rows.length === 0 && cleanedSlug) { + [rows] = await pool.execute( + 'SELECT * FROM upcoming_events WHERE LOWER(REPLACE(title, " ", "-")) = ? OR LOWER(REPLACE(eventtitle, " ", "-")) = ?', + [cleanedSlug, cleanedSlug] + ); + } + + if (rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Upcoming event not found' + }); + } + res.json({ + success: true, + data: rows[0] + }); + } catch (error) { + console.error('Error fetching upcoming event by slug:', error); + res.status(500).json({ + success: false, + message: 'Error fetching upcoming event', + error: error.message + }); + } +}); + +// POST create new upcoming event +app.post('/api/upcoming-events', async (req, res) => { + try { + const { title, eventtitle, slug, date, eventdate, time, location, venue, image, image_url, eventimageurl, link, btn_text, btnText, description, desc, eventdescription, admission } = req.body; + + const inputTitle = title || eventtitle; + if (!inputTitle || String(inputTitle).trim() === '') { + return res.status(400).json({ + success: false, + message: 'Event title is required' + }); + } + + const finalTitle = String(inputTitle).trim(); + const generatedSlug = slugify(finalTitle); + const finalSlug = (slug && String(slug).trim()) ? slugify(String(slug).trim()) : generatedSlug; + const finalLocation = location || venue || ''; + const finalImage = image || image_url || eventimageurl || ''; + const finalBtnText = btn_text || btnText || 'Details Coming Soon'; + const finalDesc = description || desc || eventdescription || ''; + const finalLink = (link && String(link).trim() && !String(link).startsWith('/upcoming-event/')) + ? String(link).trim() + : `/upcoming-event/${finalSlug}`; + const finalDate = date || eventdate || ''; + const finalTime = time || ''; + const finalAdmission = admission || null; + + const [result] = await pool.execute( + `INSERT INTO upcoming_events (title, eventtitle, slug, date, eventdate, time, location, image, eventimageurl, link, btn_text, btnText, description, eventdescription, admission) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [finalTitle, finalTitle, finalSlug, finalDate, finalDate, finalTime, finalLocation, finalImage, finalImage, finalLink, finalBtnText, finalBtnText, finalDesc, finalDesc, finalAdmission] + ); + + res.status(201).json({ + success: true, + message: 'Upcoming event created successfully', + data: { + id: result.insertId, + title: finalTitle, + slug: finalSlug, + link: finalLink + } + }); + } catch (error) { + console.error('Error creating upcoming event:', error); + res.status(500).json({ + success: false, + message: 'Error creating upcoming event', + error: error.message + }); + } +}); + +// PUT update upcoming event (supports full or partial updates) +app.put('/api/upcoming-events/:id', async (req, res) => { + try { + const eventId = req.params.id; + + // Fetch existing record + const [rows] = await pool.execute('SELECT * FROM upcoming_events WHERE id = ?', [eventId]); + if (rows.length === 0) { + return res.status(404).json({ + success: false, + message: 'Upcoming event not found' + }); + } + + const existing = rows[0]; + const { title, eventtitle, slug, date, eventdate, time, location, venue, image, image_url, eventimageurl, link, btn_text, btnText, description, desc, eventdescription, admission, sort_order } = req.body || {}; + + const inputTitle = title || eventtitle || existing.title || existing.eventtitle; + if (!inputTitle || String(inputTitle).trim() === '') { + return res.status(400).json({ + success: false, + message: 'Event title is required' + }); + } + + const finalTitle = String(inputTitle).trim(); + const generatedSlug = slugify(finalTitle); + + let finalSlug = generatedSlug; + if (slug && String(slug).trim()) { + const userSlug = slugify(String(slug).trim()); + const oldTitleSlug = slugify(existing.title || existing.eventtitle || ''); + if (userSlug !== oldTitleSlug) { + finalSlug = userSlug; + } + } + + const finalLocation = location !== undefined ? location : (venue !== undefined ? venue : (existing.location || '')); + const finalImage = image !== undefined ? image : (image_url !== undefined ? image_url : (eventimageurl !== undefined ? eventimageurl : (existing.image || existing.eventimageurl || ''))); + const finalBtnText = btn_text !== undefined ? btn_text : (btnText !== undefined ? btnText : (existing.btn_text || existing.btnText || 'Details Coming Soon')); + const finalDesc = description !== undefined ? description : (desc !== undefined ? desc : (eventdescription !== undefined ? eventdescription : (existing.description || existing.eventdescription || ''))); + const finalLink = (link && String(link).trim() && !String(link).startsWith('/upcoming-event/')) + ? String(link).trim() + : `/upcoming-event/${finalSlug}`; + const finalDate = date !== undefined ? date : (eventdate !== undefined ? eventdate : (existing.date || existing.eventdate || '')); + const finalTime = time !== undefined ? time : (existing.time || ''); + const finalAdmission = admission !== undefined ? admission : existing.admission; + const finalSortOrder = sort_order !== undefined ? Number(sort_order) : (existing.sort_order || 0); + + await pool.execute( + `UPDATE upcoming_events + SET title = ?, eventtitle = ?, slug = ?, date = ?, eventdate = ?, time = ?, location = ?, image = ?, eventimageurl = ?, link = ?, btn_text = ?, btnText = ?, description = ?, eventdescription = ?, admission = ?, sort_order = ? + WHERE id = ?`, + [finalTitle, finalTitle, finalSlug, finalDate, finalDate, finalTime, finalLocation, finalImage, finalImage, finalLink, finalBtnText, finalBtnText, finalDesc, finalDesc, finalAdmission, finalSortOrder, eventId] + ); + + res.json({ + success: true, + message: 'Upcoming event updated successfully' + }); + } catch (error) { + console.error('Error updating upcoming event:', error); + res.status(500).json({ + success: false, + message: 'Error updating upcoming event', + error: error.message + }); + } +}); + +// DELETE upcoming event +app.delete('/api/upcoming-events/:id', async (req, res) => { + try { + const [result] = await pool.execute('DELETE FROM upcoming_events WHERE id = ?', [req.params.id]); + + if (result.affectedRows === 0) { + return res.status(404).json({ + success: false, + message: 'Upcoming event not found' + }); + } + + res.json({ + success: true, + message: 'Upcoming event deleted successfully' + }); + } catch (error) { + console.error('Error deleting upcoming event:', error); + res.status(500).json({ + success: false, + message: 'Error deleting upcoming event', + error: error.message + }); + } +}); + // =============================== // EVENT_IMAGES CRUD ROUTES // =============================== @@ -579,9 +941,9 @@ app.get('/api/event-images', async (req, res) => { app.get('/api/event-images/event/:eventId', async (req, res) => { try { const [rows] = await pool.execute( - 'SELECT * FROM event_images WHERE eventid = ? ORDER BY sort_order ASC, id ASC', - [req.params.eventId] -); + 'SELECT * FROM event_images WHERE eventid = ? ORDER BY sort_order ASC, id ASC', + [req.params.eventId] + ); res.json({ success: true, data: rows @@ -664,7 +1026,7 @@ app.post('/api/event-images', async (req, res) => { // Bulk reorder images app.put('/api/event-images/reorder', async (req, res) => { try { - console.log("Request body:", req.body); + console.log("Request body:", req.body); const { images } = req.body; // Array of { id, sort_order } console.log(`Reorder request received for ${images?.length} images`); @@ -700,18 +1062,18 @@ app.put('/api/event-images/reorder', async (req, res) => { connection.release(); } } catch (error) { - console.error("===== REORDER ERROR ====="); - console.error(error); - console.error("Message:", error.message); - console.error("Code:", error.code); - console.error("========================="); + console.error("===== REORDER ERROR ====="); + console.error(error); + console.error("Message:", error.message); + console.error("Code:", error.code); + console.error("========================="); - res.status(500).json({ - success: false, - message: error.message, - code: error.code - }); -} + res.status(500).json({ + success: false, + message: error.message, + code: error.code + }); + } }); app.post('/api/event-images/bulk', async (req, res) => { diff --git a/setup-db.js b/setup-db.js index fd8bea6..b924d23 100644 --- a/setup-db.js +++ b/setup-db.js @@ -52,6 +52,26 @@ async function setup() { `); console.log('Verified event_images table exists.'); + // Create upcoming_events table + await pool.execute(` + CREATE TABLE IF NOT EXISTS upcoming_events ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + slug VARCHAR(255), + date VARCHAR(255), + time VARCHAR(255), + location VARCHAR(255), + image TEXT, + link TEXT, + btn_text VARCHAR(255) DEFAULT 'Details Coming Soon', + description TEXT, + admission VARCHAR(255), + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + console.log('Verified upcoming_events table exists.'); + // Check if admin user exists const [rows] = await pool.execute('SELECT * FROM admin_users WHERE email = ?', ['admin@example.com']);