Skip to content

Commit b7ab40d

Browse files
committed
feat: Add advanced video gallery, update components, and enhance backend
- Add VideoGallery component with YouTube-style hover previews and playback continuity - Add VideoPlayer component with full controls and keyboard shortcuts - Add new components: Alert, Loader, FeaturedVideos, QuickActionBox, AboutEuphoric - Add video imports utility for centralized video management - Add Alert context and hooks for toast notifications - Add 404 page for better error handling - Enhance backend with Redis caching support (optional, graceful fallback) - Update README with comprehensive documentation of new features - Add multiple video assets for gallery - Improve accessibility with ARIA labels and focus management - Add mobile tap-to-play support for video gallery
1 parent 0794520 commit b7ab40d

47 files changed

Lines changed: 3865 additions & 462 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 453 additions & 59 deletions
Large diffs are not rendered by default.
-5.35 MB
Binary file not shown.

backend/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@
88
"build": "echo 'Backend build complete'"
99
},
1010
"dependencies": {
11-
"express": "^4.18.2",
12-
"mysql2": "^3.6.5",
1311
"cors": "^2.8.5",
14-
"helmet": "^7.1.0",
1512
"dotenv": "^16.3.1",
13+
"express": "^4.18.2",
1614
"express-rate-limit": "^7.1.5",
1715
"express-validator": "^7.0.1",
18-
"nodemailer": "^6.9.7"
16+
"helmet": "^7.1.0",
17+
"mysql2": "^3.6.5",
18+
"nodemailer": "^6.9.7",
19+
"redis": "^5.10.0"
1920
},
2021
"devDependencies": {
2122
"nodemon": "^3.0.2"
2223
}
2324
}
24-

backend/src/config/database.js

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,79 @@
1-
import mysql from 'mysql2/promise'
2-
import dotenv from 'dotenv'
1+
import mysql from "mysql2/promise";
2+
import dotenv from "dotenv";
33

4-
dotenv.config()
4+
dotenv.config();
55

6-
const pool = mysql.createPool({
7-
host: process.env.DB_HOST || 'localhost',
8-
user: process.env.DB_USER || 'root',
9-
password: process.env.DB_PASSWORD || '',
10-
database: process.env.DB_NAME || 'euphoric_db',
6+
// Database connection configuration
7+
const dbConfig = {
8+
host: process.env.DB_HOST || "localhost",
9+
user: process.env.DB_USER || "root",
10+
password: process.env.DB_PASSWORD || "",
11+
database: process.env.DB_NAME || "euphoric_db",
1112
waitForConnections: true,
1213
connectionLimit: 10,
1314
queueLimit: 0,
14-
})
15+
// Enable multiple statements if needed
16+
multipleStatements: false,
17+
// Connection timeout
18+
connectTimeout: 10000,
19+
// SSL configuration (set to false for local development)
20+
ssl: process.env.DB_SSL === "true" ? {} : false,
21+
};
1522

16-
// Test connection
23+
// Create connection pool
24+
const pool = mysql.createPool(dbConfig);
25+
26+
// Test connection with better error handling
1727
pool
1828
.getConnection()
1929
.then((connection) => {
20-
console.log('✅ Database connected successfully')
21-
connection.release()
30+
console.log("✅ Database connected successfully");
31+
connection.release();
2232
})
2333
.catch((error) => {
24-
console.error('❌ Database connection error:', error.message)
25-
})
34+
console.error("❌ Database connection error:", error.message);
2635

27-
export default pool
36+
// Provide helpful error messages
37+
if (
38+
error.code === "ER_NOT_SUPPORTED_AUTH_MODE" ||
39+
error.code === "AUTH_SWITCH_PLUGIN_ERROR" ||
40+
error.message.includes("auth_gssapi_client") ||
41+
error.message.includes("unknown plugin")
42+
) {
43+
console.error("\n💡 Authentication Plugin Error Detected");
44+
console.error(
45+
" This usually happens with MySQL 8.0+ default authentication."
46+
);
47+
console.error(
48+
" Your MySQL user is using an unsupported authentication method."
49+
);
50+
console.error("\n To fix this, run these SQL commands in MySQL:");
51+
console.error(
52+
` ALTER USER '${dbConfig.user}'@'${dbConfig.host}' IDENTIFIED WITH mysql_native_password BY '${dbConfig.password}';`
53+
);
54+
console.error(" FLUSH PRIVILEGES;");
55+
console.error("\n Or if using a different host:");
56+
console.error(
57+
` ALTER USER '${dbConfig.user}'@'%' IDENTIFIED WITH mysql_native_password BY '${dbConfig.password}';`
58+
);
59+
console.error(" FLUSH PRIVILEGES;");
60+
console.error("\n After running these commands, restart the backend server.");
61+
} else if (error.code === "ECONNREFUSED") {
62+
console.error("\n💡 Connection Refused");
63+
console.error(" Make sure MySQL server is running and accessible.");
64+
console.error(` Check host: ${dbConfig.host}, port: 3306`);
65+
} else if (error.code === "ER_ACCESS_DENIED_ERROR") {
66+
console.error("\n💡 Access Denied");
67+
console.error(" Check your database credentials in .env file:");
68+
console.error(` DB_USER: ${dbConfig.user}`);
69+
console.error(` DB_PASSWORD: ${dbConfig.password ? "***" : "(empty)"}`);
70+
} else if (error.code === "ER_BAD_DB_ERROR") {
71+
console.error("\n💡 Database Not Found");
72+
console.error(` Database '${dbConfig.database}' does not exist.`);
73+
console.error(
74+
" Create it with: CREATE DATABASE " + dbConfig.database + ";"
75+
);
76+
}
77+
});
2878

79+
export default pool;

backend/src/controllers/enquiryController.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@ const createTransporter = () => {
1717
// Create enquiry
1818
export const createEnquiry = async (req, res) => {
1919
try {
20+
// Validation handled by middleware
2021
const { name, phone, email, message } = req.body
2122

22-
// Validation
23-
if (!name || !email || !message) {
24-
return res.status(400).json({ error: 'Name, email, and message are required' })
25-
}
23+
// Additional sanitization
24+
const sanitizedName = name.trim().substring(0, 100)
25+
const sanitizedEmail = email.trim().toLowerCase().substring(0, 255)
26+
const sanitizedPhone = phone ? phone.trim().replace(/[^0-9+\-() ]/g, '').substring(0, 20) : null
27+
const sanitizedMessage = message.trim().substring(0, 2000)
2628

27-
// Save to database
29+
// Save to database using prepared statements
2830
const [result] = await pool.execute(
2931
'INSERT INTO enquiries (name, phone, email, message) VALUES (?, ?, ?, ?)',
30-
[name, phone || null, email, message]
32+
[sanitizedName, sanitizedPhone, sanitizedEmail, sanitizedMessage]
3133
)
3234

3335
// Send email if configured
@@ -40,11 +42,11 @@ export const createEnquiry = async (req, res) => {
4042
subject: `New Enquiry from ${name}`,
4143
html: `
4244
<h2>New Enquiry Received</h2>
43-
<p><strong>Name:</strong> ${name}</p>
44-
<p><strong>Phone:</strong> ${phone || 'Not provided'}</p>
45-
<p><strong>Email:</strong> ${email}</p>
45+
<p><strong>Name:</strong> ${sanitizedName}</p>
46+
<p><strong>Phone:</strong> ${sanitizedPhone || 'Not provided'}</p>
47+
<p><strong>Email:</strong> ${sanitizedEmail}</p>
4648
<p><strong>Message:</strong></p>
47-
<p>${message}</p>
49+
<p>${sanitizedMessage.replace(/\n/g, '<br>')}</p>
4850
`,
4951
})
5052
} catch (emailError) {

backend/src/controllers/eventsController.js

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
import pool from '../config/database.js'
2+
import { getCache, setCache, deleteCache, invalidateCachePattern } from '../utils/cache.js'
23

34
// Get all events
45
export const getEvents = async (req, res) => {
56
try {
7+
// Try to get from cache
8+
const cacheKey = 'events:upcoming'
9+
const cached = await getCache(cacheKey)
10+
if (cached) {
11+
return res.json(cached)
12+
}
13+
14+
// Fetch from database using prepared statement
615
const [events] = await pool.execute(
716
'SELECT * FROM events WHERE date >= CURDATE() ORDER BY date ASC'
817
)
18+
19+
// Cache for 30 minutes
20+
await setCache(cacheKey, events, 1800)
21+
922
res.json(events)
1023
} catch (error) {
1124
console.error('Error fetching events:', error)
@@ -33,22 +46,27 @@ export const getEvent = async (req, res) => {
3346
// Create a new event
3447
export const createEvent = async (req, res) => {
3548
try {
49+
// Validation handled by middleware
3650
const { title, date, location, image, description } = req.body
3751

38-
// Validation
39-
if (!title || !date || !location) {
40-
return res.status(400).json({ error: 'Title, date, and location are required' })
41-
}
52+
// Sanitize inputs
53+
const sanitizedTitle = title.trim().substring(0, 200)
54+
const sanitizedLocation = location.trim().substring(0, 200)
55+
const sanitizedDescription = description ? description.trim().substring(0, 2000) : null
4256

57+
// Use prepared statements
4358
const [result] = await pool.execute(
4459
'INSERT INTO events (title, date, location, image, description) VALUES (?, ?, ?, ?, ?)',
45-
[title, date, location, image || null, description || null]
60+
[sanitizedTitle, date, sanitizedLocation, image || null, sanitizedDescription]
4661
)
4762

4863
const [newEvent] = await pool.execute('SELECT * FROM events WHERE id = ?', [
4964
result.insertId,
5065
])
5166

67+
// Invalidate cache
68+
await invalidateCachePattern('events:*')
69+
5270
res.status(201).json(newEvent[0])
5371
} catch (error) {
5472
console.error('Error creating event:', error)
@@ -62,16 +80,26 @@ export const updateEvent = async (req, res) => {
6280
const { id } = req.params
6381
const { title, date, location, image, description } = req.body
6482

83+
// Sanitize inputs
84+
const sanitizedTitle = title ? title.trim().substring(0, 200) : null
85+
const sanitizedLocation = location ? location.trim().substring(0, 200) : null
86+
const sanitizedDescription = description ? description.trim().substring(0, 2000) : null
87+
88+
// Use prepared statements with parameterized query
6589
const [result] = await pool.execute(
6690
'UPDATE events SET title = ?, date = ?, location = ?, image = ?, description = ? WHERE id = ?',
67-
[title, date, location, image || null, description || null, id]
91+
[sanitizedTitle, date, sanitizedLocation, image || null, sanitizedDescription, parseInt(id, 10)]
6892
)
6993

7094
if (result.affectedRows === 0) {
7195
return res.status(404).json({ error: 'Event not found' })
7296
}
7397

74-
const [updatedEvent] = await pool.execute('SELECT * FROM events WHERE id = ?', [id])
98+
const [updatedEvent] = await pool.execute('SELECT * FROM events WHERE id = ?', [parseInt(id, 10)])
99+
100+
// Invalidate cache
101+
await invalidateCachePattern('events:*')
102+
75103
res.json(updatedEvent[0])
76104
} catch (error) {
77105
console.error('Error updating event:', error)
@@ -83,12 +111,17 @@ export const updateEvent = async (req, res) => {
83111
export const deleteEvent = async (req, res) => {
84112
try {
85113
const { id } = req.params
86-
const [result] = await pool.execute('DELETE FROM events WHERE id = ?', [id])
114+
115+
// Use prepared statement with parameterized query
116+
const [result] = await pool.execute('DELETE FROM events WHERE id = ?', [parseInt(id, 10)])
87117

88118
if (result.affectedRows === 0) {
89119
return res.status(404).json({ error: 'Event not found' })
90120
}
91121

122+
// Invalidate cache
123+
await invalidateCachePattern('events:*')
124+
92125
res.json({ message: 'Event deleted successfully' })
93126
} catch (error) {
94127
console.error('Error deleting event:', error)

backend/src/controllers/reviewsController.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
import pool from '../config/database.js'
2+
import { getCache, setCache, deleteCache } from '../utils/cache.js'
23

34
// Get all reviews
45
export const getReviews = async (req, res) => {
56
try {
7+
// Try to get from cache
8+
const cacheKey = 'reviews:all'
9+
const cached = await getCache(cacheKey)
10+
if (cached) {
11+
return res.json(cached)
12+
}
13+
14+
// Fetch from database
615
const [reviews] = await pool.execute(
716
'SELECT * FROM reviews ORDER BY created_at DESC'
817
)
18+
19+
// Cache for 1 hour
20+
await setCache(cacheKey, reviews, 3600)
21+
922
res.json(reviews)
1023
} catch (error) {
1124
console.error('Error fetching reviews:', error)
@@ -16,26 +29,26 @@ export const getReviews = async (req, res) => {
1629
// Create a new review
1730
export const createReview = async (req, res) => {
1831
try {
32+
// Validation is handled by middleware, but we sanitize here too
1933
const { name, message, rating } = req.body
2034

21-
// Validation
22-
if (!name || !message || !rating) {
23-
return res.status(400).json({ error: 'Name, message, and rating are required' })
24-
}
25-
26-
if (rating < 1 || rating > 5) {
27-
return res.status(400).json({ error: 'Rating must be between 1 and 5' })
28-
}
35+
// Additional sanitization (validation middleware already handled basic validation)
36+
const sanitizedName = name.trim().substring(0, 100)
37+
const sanitizedMessage = message.trim().substring(0, 1000)
2938

39+
// Use prepared statements (already done, but ensuring)
3040
const [result] = await pool.execute(
3141
'INSERT INTO reviews (name, message, rating) VALUES (?, ?, ?)',
32-
[name, message, rating]
42+
[sanitizedName, sanitizedMessage, parseInt(rating, 10)]
3343
)
3444

3545
const [newReview] = await pool.execute('SELECT * FROM reviews WHERE id = ?', [
3646
result.insertId,
3747
])
3848

49+
// Invalidate cache
50+
await deleteCache('reviews:all')
51+
3952
res.status(201).json(newReview[0])
4053
} catch (error) {
4154
console.error('Error creating review:', error)

backend/src/models/initDatabase.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import pool from '../config/database.js'
22

33
const initDatabase = async () => {
44
try {
5+
// Test connection first
6+
const connection = await pool.getConnection()
7+
await connection.ping()
8+
connection.release()
9+
510
// Create reviews table
611
await pool.execute(`
712
CREATE TABLE IF NOT EXISTS reviews (
@@ -42,6 +47,26 @@ const initDatabase = async () => {
4247
console.log('✅ Database tables initialized successfully')
4348
} catch (error) {
4449
console.error('❌ Error initializing database:', error.message)
50+
51+
// Provide specific help for authentication errors
52+
if (error.message.includes('auth_gssapi_client') || error.message.includes('authentication')) {
53+
console.error('\n🔧 FIX REQUIRED: MySQL Authentication Plugin Issue')
54+
console.error(' Your MySQL user is using an unsupported authentication method.')
55+
console.error('\n Run these commands in MySQL to fix:')
56+
console.error(' ----------------------------------------')
57+
const user = process.env.DB_USER || 'root'
58+
const host = process.env.DB_HOST || 'localhost'
59+
console.error(` ALTER USER '${user}'@'${host}' IDENTIFIED WITH mysql_native_password BY '${process.env.DB_PASSWORD || ''}';`)
60+
console.error(' FLUSH PRIVILEGES;')
61+
console.error(' ----------------------------------------')
62+
console.error('\n Or if connecting from any host:')
63+
console.error(` ALTER USER '${user}'@'%' IDENTIFIED WITH mysql_native_password BY '${process.env.DB_PASSWORD || ''}';`)
64+
console.error(' FLUSH PRIVILEGES;')
65+
console.error('\n After running these commands, restart the backend server.')
66+
}
67+
68+
// Re-throw to prevent server from starting without database
69+
throw error
4570
}
4671
}
4772

backend/src/routes/enquiry.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import express from 'express'
22
import { createEnquiry } from '../controllers/enquiryController.js'
33
import rateLimit from 'express-rate-limit'
4+
import { enquiryValidation, handleValidationErrors } from '../utils/validation.js'
45

56
const router = express.Router()
67

@@ -9,9 +10,11 @@ const createEnquiryLimiter = rateLimit({
910
windowMs: 15 * 60 * 1000, // 15 minutes
1011
max: 10, // Limit each IP to 10 requests per windowMs
1112
message: 'Too many enquiry submissions, please try again later.',
13+
standardHeaders: true,
14+
legacyHeaders: false,
1215
})
1316

14-
router.post('/', createEnquiryLimiter, createEnquiry)
17+
router.post('/', createEnquiryLimiter, enquiryValidation, handleValidationErrors, createEnquiry)
1518

1619
export default router
1720

0 commit comments

Comments
 (0)