Fix brand theme application and add comprehensive translations
- Fixed theme not being reflected on gallery and admin login pages - Created GlobalThemeProvider to apply themes globally - Updated gallery and admin login pages to use dynamic CSS variables - Added complete translations for all admin sections in English and German: - Notifications management - Event view and creation - Photo upload functionality - Category management - Archive page view - Analytics dashboard - Branding and theme settings - System settings - CMS page management - Email configuration - Fixed admin photo management display issues - Fixed photo upload category assignment - Added password reset functionality for galleries - Improved error handling and user feedback 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ const fs = require('fs').promises;
|
||||
const { db } = require('../database/db');
|
||||
const { adminAuth } = require('../middleware/auth');
|
||||
const archiver = require('archiver');
|
||||
const AdmZip = require('adm-zip');
|
||||
const router = express.Router();
|
||||
|
||||
// Get all archived events
|
||||
@@ -34,11 +35,13 @@ router.get('/', adminAuth, async (req, res) => {
|
||||
.offset(offset);
|
||||
|
||||
// Check if archive files exist and get their sizes
|
||||
const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||
const archivesWithFileInfo = await Promise.all(archives.map(async (archive) => {
|
||||
let archiveFileSize = 0;
|
||||
if (archive.archive_path) {
|
||||
try {
|
||||
const stats = await fs.stat(archive.archive_path);
|
||||
const fullArchivePath = path.join(storagePath, archive.archive_path);
|
||||
const stats = await fs.stat(fullArchivePath);
|
||||
archiveFileSize = stats.size;
|
||||
} catch (error) {
|
||||
console.error(`Archive file not found: ${archive.archive_path}`);
|
||||
@@ -52,8 +55,8 @@ router.get('/', adminAuth, async (req, res) => {
|
||||
eventDate: archive.event_date,
|
||||
eventType: archive.event_type,
|
||||
hostEmail: archive.host_email,
|
||||
archivedAt: archive.archived_at,
|
||||
expiresAt: archive.expires_at,
|
||||
archivedAt: archive.archived_at ? new Date(archive.archived_at).toISOString() : null,
|
||||
expiresAt: archive.expires_at ? new Date(archive.expires_at).toISOString() : null,
|
||||
photoCount: archive.photo_count || 0,
|
||||
originalSize: archive.total_size || 0,
|
||||
archiveSize: archiveFileSize,
|
||||
@@ -97,7 +100,9 @@ router.get('/:id', adminAuth, async (req, res) => {
|
||||
let archiveFileInfo = null;
|
||||
if (archive.archive_path) {
|
||||
try {
|
||||
const stats = await fs.stat(archive.archive_path);
|
||||
const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||
const fullArchivePath = path.join(storagePath, archive.archive_path);
|
||||
const stats = await fs.stat(fullArchivePath);
|
||||
archiveFileInfo = {
|
||||
size: stats.size,
|
||||
createdAt: stats.birthtime,
|
||||
@@ -142,12 +147,123 @@ router.post('/:id/restore', adminAuth, async (req, res) => {
|
||||
return res.status(404).json({ error: 'Archive not found' });
|
||||
}
|
||||
|
||||
// Check if archive directory exists
|
||||
const archiveDir = path.dirname(archive.archive_path);
|
||||
const extractedDir = archive.archive_path.replace('.zip', '');
|
||||
|
||||
// TODO: Implement actual extraction logic
|
||||
// For now, just update the database
|
||||
// Check if archive file exists
|
||||
if (!archive.archive_path) {
|
||||
return res.status(400).json({ error: 'No archive file found' });
|
||||
}
|
||||
|
||||
const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||
const fullArchivePath = path.join(storagePath, archive.archive_path);
|
||||
|
||||
try {
|
||||
await fs.access(fullArchivePath);
|
||||
} catch (error) {
|
||||
return res.status(404).json({ error: 'Archive file not found on disk' });
|
||||
}
|
||||
|
||||
// Extract the archive
|
||||
try {
|
||||
const zip = new AdmZip(fullArchivePath);
|
||||
const eventsDir = path.join(storagePath, 'events/active');
|
||||
const eventDir = path.join(eventsDir, archive.slug);
|
||||
|
||||
// Create event directory if it doesn't exist
|
||||
await fs.mkdir(eventDir, { recursive: true });
|
||||
|
||||
// Log ZIP contents for debugging
|
||||
console.log(`Extracting archive to: ${eventDir}`);
|
||||
const entries = zip.getEntries();
|
||||
console.log(`Archive contains ${entries.length} entries`);
|
||||
|
||||
// Extract files to the event directory
|
||||
zip.extractAllTo(eventDir, true);
|
||||
|
||||
// Get list of extracted files to update database
|
||||
const extractedPhotos = [];
|
||||
|
||||
// First, collect all category information from the ZIP structure
|
||||
const categoriesMap = new Map();
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory && entry.entryName.match(/\.(jpg|jpeg|png|gif|webp)$/i)) {
|
||||
const filename = path.basename(entry.entryName);
|
||||
const dirPath = path.dirname(entry.entryName);
|
||||
const actualFilePath = path.join(eventDir, entry.entryName);
|
||||
|
||||
try {
|
||||
// Check if file was extracted successfully
|
||||
const stats = await fs.stat(actualFilePath);
|
||||
|
||||
// Determine category from directory structure
|
||||
let categoryId = null;
|
||||
if (dirPath && dirPath !== '.') {
|
||||
// Get the first level directory as category
|
||||
const categoryName = dirPath.split(path.sep)[0];
|
||||
|
||||
if (!categoriesMap.has(categoryName)) {
|
||||
// Check if this category exists in the database
|
||||
const existingCategory = await db('photo_categories')
|
||||
.where('event_id', archive.id)
|
||||
.where('name', categoryName)
|
||||
.first();
|
||||
|
||||
if (existingCategory) {
|
||||
categoriesMap.set(categoryName, existingCategory.id);
|
||||
} else {
|
||||
// Create the category if it doesn't exist
|
||||
const [newCategoryId] = await db('photo_categories').insert({
|
||||
event_id: archive.id,
|
||||
name: categoryName,
|
||||
slug: categoryName.toLowerCase().replace(/[^a-z0-9]/g, '-'),
|
||||
created_at: new Date()
|
||||
});
|
||||
categoriesMap.set(categoryName, newCategoryId);
|
||||
}
|
||||
}
|
||||
|
||||
categoryId = categoriesMap.get(categoryName);
|
||||
}
|
||||
|
||||
// Check if photo already exists in database
|
||||
const existingPhoto = await db('photos')
|
||||
.where('event_id', archive.id)
|
||||
.where('filename', filename)
|
||||
.first();
|
||||
|
||||
if (!existingPhoto) {
|
||||
// Store relative path from storage root
|
||||
const relativePath = path.relative(storagePath, actualFilePath);
|
||||
extractedPhotos.push({
|
||||
event_id: archive.id,
|
||||
filename: filename,
|
||||
original_filename: filename,
|
||||
path: relativePath,
|
||||
thumbnail_path: null, // Will be regenerated by thumbnail service
|
||||
type: path.extname(filename).substring(1).toLowerCase(),
|
||||
size_bytes: stats.size,
|
||||
category_id: categoryId,
|
||||
uploaded_at: new Date()
|
||||
});
|
||||
}
|
||||
} catch (statError) {
|
||||
console.error(`Failed to stat file: ${actualFilePath}`);
|
||||
console.error(`Entry name was: ${entry.entryName}`);
|
||||
console.error(`Error:`, statError.message);
|
||||
// Skip this file if we can't stat it
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new photos if any
|
||||
if (extractedPhotos.length > 0) {
|
||||
await db('photos').insert(extractedPhotos);
|
||||
}
|
||||
|
||||
} catch (extractError) {
|
||||
console.error('Archive extraction error:', extractError);
|
||||
return res.status(500).json({ error: 'Failed to extract archive: ' + extractError.message });
|
||||
}
|
||||
|
||||
// Update event status
|
||||
await db('events')
|
||||
@@ -164,8 +280,8 @@ router.post('/:id/restore', adminAuth, async (req, res) => {
|
||||
await db('activity_logs').insert({
|
||||
activity_type: 'archive_restored',
|
||||
actor_type: 'admin',
|
||||
actor_id: req.user.id,
|
||||
actor_name: req.user.username,
|
||||
actor_id: req.admin.id,
|
||||
actor_name: req.admin.username,
|
||||
event_id: archive.id,
|
||||
metadata: JSON.stringify({ event_name: archive.event_name })
|
||||
});
|
||||
@@ -194,8 +310,11 @@ router.get('/:id/download', adminAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||
const fullArchivePath = path.join(storagePath, archive.archive_path);
|
||||
|
||||
try {
|
||||
await fs.access(archive.archive_path);
|
||||
await fs.access(fullArchivePath);
|
||||
} catch (error) {
|
||||
return res.status(404).json({ error: 'Archive file not found on disk' });
|
||||
}
|
||||
@@ -205,15 +324,15 @@ router.get('/:id/download', adminAuth, async (req, res) => {
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${archive.slug}.zip"`);
|
||||
|
||||
// Stream the file
|
||||
const fileStream = require('fs').createReadStream(archive.archive_path);
|
||||
const fileStream = require('fs').createReadStream(fullArchivePath);
|
||||
fileStream.pipe(res);
|
||||
|
||||
// Log download
|
||||
await db('activity_logs').insert({
|
||||
activity_type: 'archive_downloaded',
|
||||
actor_type: 'admin',
|
||||
actor_id: req.user.id,
|
||||
actor_name: req.user.username,
|
||||
actor_id: req.admin.id,
|
||||
actor_name: req.admin.username,
|
||||
event_id: archive.id,
|
||||
metadata: JSON.stringify({ event_name: archive.event_name })
|
||||
});
|
||||
@@ -251,8 +370,8 @@ router.delete('/:id', adminAuth, async (req, res) => {
|
||||
await db('activity_logs').insert({
|
||||
activity_type: 'archive_deleted',
|
||||
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({
|
||||
event_name: archive.event_name,
|
||||
archived_date: archive.archived_at
|
||||
|
||||
Reference in New Issue
Block a user