diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js
index 276c270..d2f81cd 100644
--- a/backend/src/routes/admin.js
+++ b/backend/src/routes/admin.js
@@ -7,6 +7,7 @@ const archiveRoutes = require('./adminArchives');
const emailRoutes = require('./adminEmail');
const settingsRoutes = require('./adminSettings');
const eventsRoutes = require('./adminEvents');
+const photosRoutes = require('./adminPhotos');
// Mount sub-routers
router.use('/dashboard', dashboardRoutes);
@@ -14,5 +15,6 @@ router.use('/archives', archiveRoutes);
router.use('/email', emailRoutes);
router.use('/settings', settingsRoutes);
router.use('/events', eventsRoutes);
+router.use('/events', photosRoutes);
module.exports = router;
\ No newline at end of file
diff --git a/backend/src/routes/adminEmail.js b/backend/src/routes/adminEmail.js
index 111a8bc..3924c00 100644
--- a/backend/src/routes/adminEmail.js
+++ b/backend/src/routes/adminEmail.js
@@ -86,7 +86,7 @@ router.post('/config', [
await logActivity('email_config_updated',
{ smtp_host, from_email },
null,
- { type: 'admin', id: req.user.id, name: req.user.username }
+ { type: 'admin', id: req.admin.id, name: req.admin.username }
);
res.json({ message: 'Email configuration updated successfully' });
@@ -224,8 +224,8 @@ router.put('/templates/:key', [
await db('activity_logs').insert({
activity_type: 'email_template_updated',
actor_type: 'admin',
- actor_id: req.user.id,
- actor_name: req.user.username,
+ actor_id: req.admin.id,
+ actor_name: req.admin.username,
metadata: JSON.stringify({ template_key: req.params.key })
});
diff --git a/backend/src/routes/adminEvents.js b/backend/src/routes/adminEvents.js
index bf4580b..57f1bf7 100644
--- a/backend/src/routes/adminEvents.js
+++ b/backend/src/routes/adminEvents.js
@@ -284,7 +284,7 @@ router.put('/:id', adminAuth, [
await logActivity('event_updated',
{ changes: Object.keys(updates), eventName: event.event_name },
id,
- { type: 'admin', id: req.user.id, name: req.user.username }
+ { type: 'admin', id: req.admin.id, name: req.admin.username }
);
res.json({ message: 'Event updated successfully' });
@@ -315,7 +315,7 @@ router.delete('/:id', adminAuth, async (req, res) => {
await logActivity('event_deleted',
{ event_name: event.event_name },
null,
- { type: 'admin', id: req.user.id, name: req.user.username }
+ { type: 'admin', id: req.admin.id, name: req.admin.username }
);
res.json({ message: 'Event deleted successfully' });
@@ -347,7 +347,7 @@ router.post('/:id/toggle-status', adminAuth, async (req, res) => {
await logActivity(newStatus ? 'event_activated' : 'event_deactivated',
{ eventName: event.event_name },
id,
- { type: 'admin', id: req.user.id, name: req.user.username }
+ { type: 'admin', id: req.admin.id, name: req.admin.username }
);
res.json({
@@ -387,7 +387,7 @@ router.post('/:id/archive', adminAuth, async (req, res) => {
await logActivity('event_archived',
{ eventName: event.event_name },
id,
- { type: 'admin', id: req.user.id, name: req.user.username }
+ { type: 'admin', id: req.admin.id, name: req.admin.username }
);
res.json({ message: 'Event archived successfully' });
diff --git a/backend/src/routes/adminPhotos.js b/backend/src/routes/adminPhotos.js
new file mode 100644
index 0000000..880bccd
--- /dev/null
+++ b/backend/src/routes/adminPhotos.js
@@ -0,0 +1,217 @@
+const express = require('express');
+const multer = require('multer');
+const path = require('path');
+const fs = require('fs').promises;
+const { db, logActivity } = require('../database/db');
+const { adminAuth } = require('../middleware/auth');
+const { generateThumbnail } = require('../services/imageProcessor');
+const router = express.Router();
+
+// Get storage path from environment or default
+const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
+
+// Configure multer for file uploads
+const storage = multer.diskStorage({
+ destination: async (req, file, cb) => {
+ const { eventId } = req.params;
+ const { type = 'individual' } = req.body;
+
+ try {
+ // Get event details
+ const event = await db('events').where({ id: eventId }).first();
+ if (!event) {
+ return cb(new Error('Event not found'));
+ }
+
+ // Create destination path
+ const photoType = type === 'collage' ? 'collages' : 'individual';
+ const destPath = path.join(getStoragePath(), 'events/active', event.slug, photoType);
+
+ // Ensure directory exists
+ await fs.mkdir(destPath, { recursive: true });
+
+ cb(null, destPath);
+ } catch (error) {
+ cb(error);
+ }
+ },
+ filename: (req, file, cb) => {
+ // Generate unique filename
+ const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
+ const ext = path.extname(file.originalname);
+ const name = path.basename(file.originalname, ext);
+ cb(null, `${name}-${uniqueSuffix}${ext}`);
+ }
+});
+
+const upload = multer({
+ storage: storage,
+ limits: {
+ fileSize: 50 * 1024 * 1024, // 50MB limit
+ },
+ fileFilter: (req, file, cb) => {
+ // Accept images only
+ const allowedTypes = /jpeg|jpg|png|webp/;
+ const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
+ const mimetype = allowedTypes.test(file.mimetype);
+
+ if (mimetype && extname) {
+ return cb(null, true);
+ } else {
+ cb(new Error('Only JPEG, PNG and WebP images are allowed'));
+ }
+ }
+});
+
+// Upload photos for an event
+router.post('/:eventId/upload', adminAuth, upload.array('photos', 20), async (req, res) => {
+ try {
+ const { eventId } = req.params;
+ const { type = 'individual' } = req.body;
+
+ // Verify event exists and admin has access
+ const event = await db('events').where({ id: eventId }).first();
+ if (!event) {
+ return res.status(404).json({ error: 'Event not found' });
+ }
+
+ if (!req.files || req.files.length === 0) {
+ return res.status(400).json({ error: 'No files uploaded' });
+ }
+
+ const uploadedPhotos = [];
+
+ // Process each uploaded file
+ for (const file of req.files) {
+ try {
+ // Generate thumbnail
+ const thumbnailPath = await generateThumbnail(file.path);
+
+ // Calculate relative paths
+ const storagePath = getStoragePath();
+ const relativePath = path.relative(path.join(storagePath, 'events/active'), file.path);
+ const relativeThumbPath = thumbnailPath ? path.relative(path.join(storagePath, 'events/active'), thumbnailPath) : null;
+
+ // Add to database
+ const [photoId] = await db('photos').insert({
+ event_id: eventId,
+ filename: file.filename,
+ path: relativePath,
+ thumbnail_path: relativeThumbPath,
+ type: type === 'collage' ? 'collage' : 'individual',
+ size_bytes: file.size
+ });
+
+ uploadedPhotos.push({
+ id: photoId,
+ filename: file.filename,
+ size: file.size,
+ type
+ });
+ } catch (error) {
+ console.error(`Error processing file ${file.filename}:`, error);
+ // Continue with other files
+ }
+ }
+
+ // Log activity
+ await logActivity('photos_uploaded',
+ { count: uploadedPhotos.length, eventName: event.event_name },
+ eventId,
+ { type: 'admin', id: req.admin.id, name: req.admin.username }
+ );
+
+ res.json({
+ message: `Successfully uploaded ${uploadedPhotos.length} photos`,
+ photos: uploadedPhotos
+ });
+ } catch (error) {
+ console.error('Error uploading photos:', error);
+ res.status(500).json({ error: 'Failed to upload photos' });
+ }
+});
+
+// Delete a photo
+router.delete('/:eventId/photos/:photoId', adminAuth, async (req, res) => {
+ try {
+ const { eventId, photoId } = req.params;
+
+ // Get photo details
+ const photo = await db('photos')
+ .where({ id: photoId, event_id: eventId })
+ .first();
+
+ if (!photo) {
+ return res.status(404).json({ error: 'Photo not found' });
+ }
+
+ // Delete physical files
+ const storagePath = getStoragePath();
+ const photoPath = path.join(storagePath, 'events/active', photo.path);
+
+ try {
+ await fs.unlink(photoPath);
+ } catch (error) {
+ console.error('Error deleting photo file:', error);
+ }
+
+ // Delete thumbnail if exists
+ if (photo.thumbnail_path) {
+ const thumbPath = path.join(storagePath, 'events/active', photo.thumbnail_path);
+ try {
+ await fs.unlink(thumbPath);
+ } catch (error) {
+ console.error('Error deleting thumbnail:', error);
+ }
+ }
+
+ // Remove from database
+ await db('photos').where({ id: photoId }).delete();
+
+ // Log activity
+ const event = await db('events').where({ id: eventId }).first();
+ await logActivity('photo_deleted',
+ { filename: photo.filename, eventName: event.event_name },
+ eventId,
+ { type: 'admin', id: req.admin.id, name: req.admin.username }
+ );
+
+ res.json({ message: 'Photo deleted successfully' });
+ } catch (error) {
+ console.error('Error deleting photo:', error);
+ res.status(500).json({ error: 'Failed to delete photo' });
+ }
+});
+
+// Get all photos for an event
+router.get('/:eventId/photos', adminAuth, async (req, res) => {
+ try {
+ const { eventId } = req.params;
+ const { type } = req.query;
+
+ let query = db('photos').where({ event_id: eventId });
+
+ if (type) {
+ query = query.where({ type });
+ }
+
+ const photos = await query.orderBy('uploaded_at', 'desc');
+
+ res.json({
+ photos: photos.map(photo => ({
+ id: photo.id,
+ filename: photo.filename,
+ url: `/photos/${photo.path}`,
+ thumbnail_url: photo.thumbnail_path ? `/photos/${photo.thumbnail_path}` : null,
+ type: photo.type,
+ size: photo.size_bytes,
+ uploaded_at: photo.uploaded_at
+ }))
+ });
+ } catch (error) {
+ console.error('Error fetching photos:', error);
+ res.status(500).json({ error: 'Failed to fetch photos' });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/src/routes/adminSettings.js b/backend/src/routes/adminSettings.js
index 3c8e97b..d0b83b9 100644
--- a/backend/src/routes/adminSettings.js
+++ b/backend/src/routes/adminSettings.js
@@ -118,8 +118,8 @@ router.put('/branding', adminAuth, async (req, res) => {
await db('activity_logs').insert({
activity_type: 'branding_updated',
actor_type: 'admin',
- actor_id: req.user.id,
- actor_name: req.user.username,
+ actor_id: req.admin.id,
+ actor_name: req.admin.username,
metadata: JSON.stringify({ company_name })
});
@@ -215,8 +215,8 @@ router.put('/theme', adminAuth, async (req, res) => {
await db('activity_logs').insert({
activity_type: 'theme_updated',
actor_type: 'admin',
- actor_id: req.user.id,
- actor_name: req.user.username,
+ actor_id: req.admin.id,
+ actor_name: req.admin.username,
metadata: JSON.stringify({ theme_name: themeSettings.name || 'custom' })
});
@@ -252,8 +252,8 @@ router.put('/general', adminAuth, async (req, res) => {
await db('activity_logs').insert({
activity_type: 'general_settings_updated',
actor_type: 'admin',
- actor_id: req.user.id,
- actor_name: req.user.username,
+ actor_id: req.admin.id,
+ actor_name: req.admin.username,
metadata: JSON.stringify({ settings_count: Object.keys(settings).length })
});
@@ -289,8 +289,8 @@ router.put('/security', adminAuth, async (req, res) => {
await db('activity_logs').insert({
activity_type: 'security_settings_updated',
actor_type: 'admin',
- actor_id: req.user.id,
- actor_name: req.user.username,
+ actor_id: req.admin.id,
+ actor_name: req.admin.username,
metadata: JSON.stringify({ settings_count: Object.keys(settings).length })
});
diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js
index 210f337..5a7ef6b 100644
--- a/backend/src/routes/gallery.js
+++ b/backend/src/routes/gallery.js
@@ -30,23 +30,63 @@ async function verifyGalleryAccess(req, res, next) {
}
}
-// Get gallery info
-router.get('/:slug/info', async (req, res) => {
+// Verify share token
+router.get('/:slug/verify-token/:token', async (req, res) => {
try {
- const { slug } = req.params;
+ const { slug, token } = req.params;
const event = await db('events')
- .where({ slug })
- .select('event_name', 'event_type', 'event_date', 'expires_at', 'is_active')
+ .where({ slug, is_active: true })
+ .select('id', 'share_link')
.first();
if (!event) {
return res.status(404).json({ error: 'Gallery not found' });
}
+ // Extract token from share link and verify
+ const expectedToken = event.share_link.split('/').pop();
+ if (token !== expectedToken) {
+ return res.status(404).json({ error: 'Invalid gallery link' });
+ }
+
+ res.json({ valid: true });
+ } catch (error) {
+ res.status(500).json({ error: 'Failed to verify token' });
+ }
+});
+
+// Get gallery info (with optional token verification)
+router.get('/:slug/info', async (req, res) => {
+ try {
+ const { slug } = req.params;
+ const { token } = req.query;
+
+ const event = await db('events')
+ .where({ slug })
+ .select('event_name', 'event_type', 'event_date', 'expires_at', 'is_active', 'share_link')
+ .first();
+
+ if (!event) {
+ return res.status(404).json({ error: 'Gallery not found' });
+ }
+
+ // If token provided, verify it matches the share link
+ if (token) {
+ const expectedToken = event.share_link.split('/').pop();
+ if (token !== expectedToken) {
+ return res.status(404).json({ error: 'Invalid gallery link' });
+ }
+ }
+
res.json({
- ...event,
- is_expired: !event.is_active || new Date(event.expires_at) < new Date()
+ event_name: event.event_name,
+ event_type: event.event_type,
+ event_date: event.event_date,
+ expires_at: event.expires_at,
+ is_active: event.is_active,
+ is_expired: !event.is_active || new Date(event.expires_at) < new Date(),
+ requires_password: true
});
} catch (error) {
res.status(500).json({ error: 'Failed to fetch gallery info' });
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 1e740ac..86653f5 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -14,9 +14,9 @@
"date-fns": "^2.29.3",
"js-cookie": "^3.0.5",
"lucide-react": "^0.292.0",
- "react": "^19.1.0",
+ "react": "^18.3.1",
"react-countdown": "^2.3.5",
- "react-dom": "^19.1.0",
+ "react-dom": "^18.3.1",
"react-image-gallery": "^1.2.11",
"react-intersection-observer": "^9.4.3",
"react-router-dom": "^6.8.0",
@@ -26,8 +26,8 @@
"devDependencies": {
"@eslint/js": "^9.29.0",
"@types/js-cookie": "^3.0.6",
- "@types/react": "^19.1.8",
- "@types/react-dom": "^19.1.6",
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.5.2",
"autoprefixer": "^10.4.13",
"eslint": "^9.29.0",
@@ -1511,24 +1511,32 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/react": {
- "version": "19.1.8",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
- "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
+ "version": "18.3.23",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
+ "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
"dev": true,
"license": "MIT",
"dependencies": {
+ "@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
- "version": "19.1.6",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
- "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
- "@types/react": "^19.0.0"
+ "@types/react": "^18.0.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
@@ -2092,9 +2100,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001726",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz",
- "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==",
+ "version": "1.0.30001727",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
+ "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"dev": true,
"funding": [
{
@@ -2714,21 +2722,6 @@
"reusify": "^1.0.4"
}
},
- "node_modules/fdir": {
- "version": "6.4.6",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
- "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -3925,10 +3918,13 @@
"license": "MIT"
},
"node_modules/react": {
- "version": "19.1.0",
- "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
- "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
"engines": {
"node": ">=0.10.0"
}
@@ -3947,15 +3943,16 @@
}
},
"node_modules/react-dom": {
- "version": "19.1.0",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
- "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"dependencies": {
- "scheduler": "^0.26.0"
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
},
"peerDependencies": {
- "react": "^19.1.0"
+ "react": "^18.3.1"
}
},
"node_modules/react-image-gallery": {
@@ -4182,10 +4179,13 @@
}
},
"node_modules/scheduler": {
- "version": "0.26.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
- "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
- "license": "MIT"
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
},
"node_modules/semver": {
"version": "6.3.1",
@@ -4497,6 +4497,21 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
@@ -4716,6 +4731,21 @@
}
}
},
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 55a4a8b..c1bbd24 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -16,9 +16,9 @@
"date-fns": "^2.29.3",
"js-cookie": "^3.0.5",
"lucide-react": "^0.292.0",
- "react": "^19.1.0",
+ "react": "^18.3.1",
"react-countdown": "^2.3.5",
- "react-dom": "^19.1.0",
+ "react-dom": "^18.3.1",
"react-image-gallery": "^1.2.11",
"react-intersection-observer": "^9.4.3",
"react-router-dom": "^6.8.0",
@@ -28,8 +28,8 @@
"devDependencies": {
"@eslint/js": "^9.29.0",
"@types/js-cookie": "^3.0.6",
- "@types/react": "^19.1.8",
- "@types/react-dom": "^19.1.6",
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.5.2",
"autoprefixer": "^10.4.13",
"eslint": "^9.29.0",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 25cfa94..6cfd90c 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -5,7 +5,7 @@ import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { analyticsService } from './services/analytics.service';
-import { GalleryAuthProvider, AdminAuthProvider } from './contexts';
+import { GalleryAuthProvider } from './contexts';
import { ThemeProvider } from './contexts/ThemeContext';
import { GalleryPage } from './pages/GalleryPage';
import {
@@ -20,7 +20,7 @@ import {
BrandingPage,
SettingsPage
} from './pages/admin';
-import { AdminLayout } from './components/admin';
+import { AdminLayout, AdminAuthWrapper } from './components/admin';
import { PageErrorBoundary, OfflineIndicator, SkipLink } from './components/common';
// Create a client
@@ -57,32 +57,28 @@ function App() {
+ Click to upload or drag and drop
+
+ JPEG, PNG or WebP (max 50MB per file)
+
+ Selected files ({selectedFiles.length})
+
+ {file.name}
+
+ {formatFileSize(file.size)}
+
+ Storage Location: /storage/events/active/{event.slug}/ +
++ Photos can also be added by placing them in the 'individual' or 'collages' folders. +
+