feat: Add CSS template system with custom gallery styling support
## Changes ### CSS Template System - Added CSS class hooks to gallery components for custom template targeting - Gallery sidebar, header, footer, and photo cards can now be styled via CSS templates - CSS variables on :root allow themes to override colors, effects, and spacing ### Gallery Component CSS Classes Added - `.gallery-page` - Main gallery container - `.gallery-header` - Top header bar - `.gallery-sidebar` - Filter/download sidebar - `.gallery-sidebar-header`, `.gallery-sidebar-title`, `.gallery-sidebar-close` - `.gallery-sidebar-content`, `.gallery-sidebar-section` - `.gallery-sidebar-search-input`, `.gallery-sidebar-search-icon` - `.gallery-sidebar-backdrop` - Mobile overlay - `.gallery-btn`, `.gallery-btn-download` - Sidebar buttons - `.gallery-footer` - Footer section - `.photo-card`, `.photo-grid` - Photo display elements ### CSS Templates (Database) - Elegant Dark (id=1): Dark navy theme with light text and red accents - Liquid Glass Light (id=2): iOS 26 frosted glass effect with gradient background ### Bug Fixes - Fixed CSS variables not inheriting (moved from .gallery-page to :root) - Fixed sidebar position breaking layout (removed position: relative override) - Fixed Elegant Dark sidebar text visibility (white on white issue) ### Other Changes - Settings page refactoring and cleanup - i18n locale updates for new gallery features - Vite proxy port configuration fix - Admin auth route improvements - CSS templates service updates
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Custom error classes for standardized error handling across the application.
|
||||
* These errors are caught by the global error handler and converted to appropriate HTTP responses.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base class for operational errors (expected errors that can occur during normal operation)
|
||||
*/
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.code = code;
|
||||
this.isOperational = true;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
error: this.message,
|
||||
code: this.code,
|
||||
...(process.env.NODE_ENV === 'development' && { stack: this.stack })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation error - for invalid input data (400 Bad Request)
|
||||
*/
|
||||
class ValidationError extends AppError {
|
||||
constructor(message = 'Validation failed', details = null) {
|
||||
super(message, 400, 'VALIDATION_ERROR');
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
...(this.details && { details: this.details })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not found error - for resources that don't exist (404 Not Found)
|
||||
*/
|
||||
class NotFoundError extends AppError {
|
||||
constructor(resource = 'Resource', identifier = null) {
|
||||
const message = identifier
|
||||
? `${resource} with identifier '${identifier}' not found`
|
||||
: `${resource} not found`;
|
||||
super(message, 404, 'NOT_FOUND');
|
||||
this.resource = resource;
|
||||
this.identifier = identifier;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unauthorized error - for missing or invalid authentication (401 Unauthorized)
|
||||
*/
|
||||
class UnauthorizedError extends AppError {
|
||||
constructor(message = 'Authentication required') {
|
||||
super(message, 401, 'UNAUTHORIZED');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forbidden error - for insufficient permissions (403 Forbidden)
|
||||
*/
|
||||
class ForbiddenError extends AppError {
|
||||
constructor(message = 'Access denied') {
|
||||
super(message, 403, 'FORBIDDEN');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Conflict error - for resource conflicts (409 Conflict)
|
||||
*/
|
||||
class ConflictError extends AppError {
|
||||
constructor(message = 'Resource conflict', field = null) {
|
||||
super(message, 409, 'CONFLICT');
|
||||
this.field = field;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
...super.toJSON(),
|
||||
...(this.field && { field: this.field })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limit error - for too many requests (429 Too Many Requests)
|
||||
*/
|
||||
class RateLimitError extends AppError {
|
||||
constructor(message = 'Too many requests', retryAfter = null) {
|
||||
super(message, 429, 'RATE_LIMIT_EXCEEDED');
|
||||
this.retryAfter = retryAfter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Service unavailable error - for maintenance mode or service issues (503 Service Unavailable)
|
||||
*/
|
||||
class ServiceUnavailableError extends AppError {
|
||||
constructor(message = 'Service temporarily unavailable') {
|
||||
super(message, 503, 'SERVICE_UNAVAILABLE');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
ValidationError,
|
||||
NotFoundError,
|
||||
UnauthorizedError,
|
||||
ForbiddenError,
|
||||
ConflictError,
|
||||
RateLimitError,
|
||||
ServiceUnavailableError
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Route helper utilities for standardized request handling.
|
||||
* Provides async error wrapping, validation, and response formatting.
|
||||
*/
|
||||
|
||||
const { validationResult } = require('express-validator');
|
||||
const { ValidationError } = require('./errors');
|
||||
|
||||
/**
|
||||
* Wraps an async route handler to catch errors and pass them to the error handler.
|
||||
* Eliminates the need for try/catch blocks in every route.
|
||||
*
|
||||
* @param {Function} fn - Async route handler function
|
||||
* @returns {Function} Express middleware function
|
||||
*
|
||||
* @example
|
||||
* router.get('/events', handleAsync(async (req, res) => {
|
||||
* const events = await eventService.getAll();
|
||||
* res.json(events);
|
||||
* }));
|
||||
*/
|
||||
const handleAsync = (fn) => {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the request using express-validator and throws ValidationError if invalid.
|
||||
* Should be called at the beginning of route handlers after validation middleware.
|
||||
*
|
||||
* @param {Request} req - Express request object
|
||||
* @throws {ValidationError} If validation fails
|
||||
*
|
||||
* @example
|
||||
* router.post('/events', [
|
||||
* body('name').notEmpty(),
|
||||
* body('date').isDate()
|
||||
* ], handleAsync(async (req, res) => {
|
||||
* validateRequest(req);
|
||||
* // ... rest of handler
|
||||
* }));
|
||||
*/
|
||||
const validateRequest = (req) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
const errorDetails = errors.array().map(err => ({
|
||||
field: err.path || err.param,
|
||||
message: err.msg
|
||||
}));
|
||||
throw new ValidationError('Validation failed', errorDetails);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a standardized success response.
|
||||
*
|
||||
* @param {Response} res - Express response object
|
||||
* @param {*} data - Data to send in the response
|
||||
* @param {number} [statusCode=200] - HTTP status code
|
||||
* @param {string} [message] - Optional success message
|
||||
*
|
||||
* @example
|
||||
* successResponse(res, { event }, 201, 'Event created successfully');
|
||||
*/
|
||||
const successResponse = (res, data, statusCode = 200, message = null) => {
|
||||
const response = message ? { message, ...data } : data;
|
||||
res.status(statusCode).json(response);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a standardized error response.
|
||||
* Note: Prefer throwing custom errors and letting the error handler format the response.
|
||||
*
|
||||
* @param {Response} res - Express response object
|
||||
* @param {string} message - Error message
|
||||
* @param {number} [statusCode=500] - HTTP status code
|
||||
* @param {string} [code] - Optional error code
|
||||
* @param {*} [details] - Optional additional error details
|
||||
*
|
||||
* @example
|
||||
* errorResponse(res, 'Invalid input', 400, 'VALIDATION_ERROR', { field: 'email' });
|
||||
*/
|
||||
const errorResponse = (res, message, statusCode = 500, code = null, details = null) => {
|
||||
const response = {
|
||||
error: message,
|
||||
...(code && { code }),
|
||||
...(details && { details })
|
||||
};
|
||||
res.status(statusCode).json(response);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a route handler with built-in validation.
|
||||
* Combines handleAsync and validateRequest for cleaner route definitions.
|
||||
*
|
||||
* @param {Function} fn - Async route handler function
|
||||
* @returns {Function} Express middleware function
|
||||
*
|
||||
* @example
|
||||
* router.post('/events', [
|
||||
* body('name').notEmpty()
|
||||
* ], withValidation(async (req, res) => {
|
||||
* const event = await eventService.create(req.body);
|
||||
* successResponse(res, { event }, 201);
|
||||
* }));
|
||||
*/
|
||||
const withValidation = (fn) => {
|
||||
return handleAsync(async (req, res, next) => {
|
||||
validateRequest(req);
|
||||
return fn(req, res, next);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts pagination parameters from query string with defaults.
|
||||
*
|
||||
* @param {Request} req - Express request object
|
||||
* @param {Object} [defaults] - Default values
|
||||
* @param {number} [defaults.page=1] - Default page number
|
||||
* @param {number} [defaults.limit=20] - Default items per page
|
||||
* @param {number} [defaults.maxLimit=100] - Maximum allowed limit
|
||||
* @returns {{ page: number, limit: number, offset: number }}
|
||||
*
|
||||
* @example
|
||||
* const { page, limit, offset } = getPagination(req);
|
||||
* const events = await db('events').limit(limit).offset(offset);
|
||||
*/
|
||||
const getPagination = (req, defaults = {}) => {
|
||||
const { page: defaultPage = 1, limit: defaultLimit = 20, maxLimit = 100 } = defaults;
|
||||
|
||||
let page = parseInt(req.query.page, 10) || defaultPage;
|
||||
let limit = parseInt(req.query.limit, 10) || defaultLimit;
|
||||
|
||||
// Ensure valid values
|
||||
page = Math.max(1, page);
|
||||
limit = Math.min(Math.max(1, limit), maxLimit);
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
return { page, limit, offset };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a paginated response with metadata.
|
||||
*
|
||||
* @param {*} data - Data array
|
||||
* @param {number} total - Total count of items
|
||||
* @param {number} page - Current page
|
||||
* @param {number} limit - Items per page
|
||||
* @returns {Object} Paginated response object
|
||||
*
|
||||
* @example
|
||||
* const events = await db('events').limit(limit).offset(offset);
|
||||
* const total = await db('events').count('* as count').first();
|
||||
* res.json(paginatedResponse(events, total.count, page, limit));
|
||||
*/
|
||||
const paginatedResponse = (data, total, page, limit) => {
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
return {
|
||||
data,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages,
|
||||
hasMore: page < totalPages
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
handleAsync,
|
||||
validateRequest,
|
||||
successResponse,
|
||||
errorResponse,
|
||||
withValidation,
|
||||
getPagination,
|
||||
paginatedResponse
|
||||
};
|
||||
Reference in New Issue
Block a user