## Features Implemented ### 1. Event Rename Functionality - Add EventRenameDialog component with live slug preview - Create eventRenameService for safe event renaming - Add slug_redirects table for old URL redirects - Support optional email notification on rename - Fix date formatting in slug (YYYY-MM-DD format) ### 2. Optional Event Contact Fields - Add settings to make customer name/email/admin email optional - Create migration for field requirement settings - Update CreateEventPage forms to show "(optional)" labels - Fix boolean parsing in publicSettings.js ### 3. Photo Filtering & Export - Add PhotoFilterPanel with rating/likes/favorites/comments filters - Create PhotoExportMenu with ZIP/metadata/XMP export options - Add photoExportService with Lightroom XMP sidecar generation - Create photoFilterBuilder utility for query construction - Wire up photo selection to export button via onSelectionChange ### 4. Custom CSS Gallery Templates - Add CssTemplateEditor component with 3 template slots - Create cssSanitizer utility blocking XSS vectors - Add gallery CSS endpoint for template delivery - Integrate Custom CSS tab into Settings page - Include default "Elegant Dark" template ## Bug Fixes - Fix event rename date formatting (was showing full Date string) - Fix common.optional translation key missing in locales - Fix photo export button staying disabled when photos selected - Fix authService import missing in SettingsPage ## Documentation - Add comprehensive REFACTORING_PLAN.md for codebase improvement - Add test specification documents for all features - Add feature documentation for CSS templates ## Database Migrations - 049_add_slug_redirects.js - 050_add_optional_event_fields_settings.js - 051_add_photo_filter_indexes.js - 052_add_css_templates.js
60 KiB
PicPeak Comprehensive Refactoring Plan
Version: 1.0 Created: January 2026 Status: Approved for Implementation Architecture Goal: Clean Architecture with Domain-Driven Design principles
Table of Contents
- Executive Summary
- Current Architecture Analysis
- Target Architecture Vision
- Phase 1: Code Duplication Elimination
- Phase 2: Backend Route & Service Refactoring
- Phase 3: Frontend Page Decomposition
- Phase 4: Cross-Cutting Concerns
- Testing Strategy
- Migration & Rollback Plan
- Implementation Checklist
Executive Summary
Scope
This refactoring addresses 20+ files totaling over 15,000 lines of code that have grown beyond maintainable sizes or contain duplicated logic. The goal is to achieve world-class architecture following Clean Architecture and SOLID principles.
Key Outcomes
- 50%+ reduction in average file size for targeted files
- Zero code duplication for utility functions
- Clear separation of concerns between routes, services, and domain logic
- Improved testability with dependency injection and smaller units
- Better developer experience with intuitive file organization
Risk Mitigation
- All changes are backwards-compatible
- Each phase can be deployed independently
- Comprehensive test coverage required before each merge
- Feature flags for gradual rollout where applicable
Current Architecture Analysis
Backend Issues
┌─────────────────────────────────────────────────────────────┐
│ CURRENT STATE │
├─────────────────────────────────────────────────────────────┤
│ Routes (1000+ lines each) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ adminEvents.js │ │ adminSettings.js│ │
│ │ 1,088 lines │ │ 1,056 lines │ │
│ │ - HTTP handlers │ │ - HTTP handlers │ │
│ │ - Business logic│ │ - Business logic│ │
│ │ - DB queries │ │ - File uploads │ │
│ │ - Validation │ │ - Validation │ │
│ │ - Helpers │ │ - Multer config │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ Services (1000+ lines each) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ backupService │ │ restoreService │ │
│ │ 1,120 lines │ │ 1,220 lines │ │
│ │ - Scheduling │ │ - Validation │ │
│ │ - Execution │ │ - Execution │ │
│ │ - S3 upload │ │ - Rollback │ │
│ │ - Notifications │ │ - Logging │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ PROBLEMS: │
│ ✗ Business logic in route handlers │
│ ✗ No service layer abstraction for events/photos │
│ ✗ Duplicate utility functions across files │
│ ✗ God objects with too many responsibilities │
└─────────────────────────────────────────────────────────────┘
Frontend Issues
┌─────────────────────────────────────────────────────────────┐
│ CURRENT STATE │
├─────────────────────────────────────────────────────────────┤
│ Pages (1000+ lines each) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ SettingsPage │ │EventDetailsPage │ │
│ │ 1,839 lines │ │ 1,479 lines │ │
│ │ - 27 hooks │ │ - 33 hooks │ │
│ │ - 8 tab sections│ │ - Photo grid │ │
│ │ - All mutations │ │ - Event editing │ │
│ │ - All queries │ │ - Theme config │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ Duplicate Files │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │CreateEventPage │ │ CMSPage.tsx │ │
│ │CreateEventPage │ │ CMSPageEnhanced │ │
│ │ Enhanced │ │ │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ PROBLEMS: │
│ ✗ Mega-components with 20+ hooks │
│ ✗ Multiple duplicate page implementations │
│ ✗ No custom hooks for shared logic │
│ ✗ Mixed concerns in single components │
└─────────────────────────────────────────────────────────────┘
Code Duplication Map
| Function/Pattern | Locations | Lines Duplicated |
|---|---|---|
parseBooleanInput |
3 files | ~60 lines |
| Auth routes | 3 versions | ~900 lines |
| CreateEvent pages | 2 versions | ~1,400 lines |
| CMS pages | 2 versions | ~1,100 lines |
| Multer config | 4+ files | ~200 lines |
Target Architecture Vision
Backend Target Architecture
┌─────────────────────────────────────────────────────────────┐
│ TARGET STATE │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ROUTES LAYER │ │
│ │ (Thin controllers - HTTP handling only) │ │
│ │ ~100-200 lines per file │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SERVICES LAYER │ │
│ │ (Business logic orchestration) │ │
│ │ ~200-400 lines per file │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │EventService │ │PhotoService │ │SettingsServ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ DOMAIN LAYER │ │
│ │ (Pure business logic, no I/O) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Validators │ │ Transformers│ │ Factories │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ INFRASTRUCTURE LAYER │ │
│ │ (Database, File System, External APIs) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │Repositories │ │ FileStorage │ │ S3Adapter │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SHARED UTILITIES │ │
│ │ (Pure functions, zero side effects) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ parsers.js │ │validators.js│ │formatters.js│ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Frontend Target Architecture
┌─────────────────────────────────────────────────────────────┐
│ TARGET STATE │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PAGES │ │
│ │ (Route components - composition only) │ │
│ │ ~100-200 lines per file │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ SettingsPage.tsx │ │ │
│ │ │ <SettingsLayout> │ │ │
│ │ │ <GeneralSettingsTab /> │ │ │
│ │ │ <EventSettingsTab /> │ │ │
│ │ │ <SecuritySettingsTab /> │ │ │
│ │ │ ... │ │ │
│ │ │ </SettingsLayout> │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ FEATURES │ │
│ │ (Feature-specific components with hooks) │ │
│ │ ~200-400 lines per file │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ settings/ │ │ events/ │ │ photos/ │ │ │
│ │ │ ├─ tabs/ │ │ ├─ editor/ │ │ ├─ grid/ │ │ │
│ │ │ ├─ hooks/ │ │ ├─ viewer/ │ │ ├─ viewer/ │ │ │
│ │ │ └─ index.ts │ │ └─ hooks/ │ │ └─ export/ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SHARED COMPONENTS │ │
│ │ (Reusable UI primitives) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ common/ │ │ forms/ │ │ layout/ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SHARED HOOKS │ │
│ │ (Reusable stateful logic) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │useSettings │ │ usePhotos │ │ useEvents │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Phase 1: Code Duplication Elimination
Priority: CRITICAL Estimated Effort: 2-3 days Risk: Low Dependencies: None
1.1 Extract Shared Parsers Utility
Problem: parseBooleanInput function duplicated in 3 files
Files Affected:
backend/src/routes/adminEvents.js(lines 61-81)backend/src/routes/events.js(lines 14-34)frontend/src/pages/admin/SettingsPage.tsx(lines 33-52, namedtoBoolean)
Solution: Create unified parser utilities
Backend Implementation
Create: backend/src/utils/parsers.js
/**
* Shared Parser Utilities
* Pure functions for parsing and transforming input values
*/
/**
* Parse any input value to boolean with configurable default
* @param {*} value - Input value to parse
* @param {boolean} defaultValue - Default if value is undefined/null
* @returns {boolean}
*/
const parseBooleanInput = (value, defaultValue = true) => {
if (value === undefined || value === null) {
return defaultValue;
}
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
if (Number.isNaN(value)) return defaultValue;
return value !== 0;
}
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
if (['false', '0', 'no', 'off', ''].includes(normalized)) {
return false;
}
if (['true', '1', 'yes', 'on'].includes(normalized)) {
return true;
}
}
return defaultValue;
};
/**
* Parse numeric input with validation
* @param {*} value - Input value to parse
* @param {number} defaultValue - Default if invalid
* @param {Object} options - Min/max bounds
* @returns {number}
*/
const parseNumberInput = (value, defaultValue, { min, max } = {}) => {
if (value === undefined || value === null || value === '') {
return defaultValue;
}
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return defaultValue;
}
if (min !== undefined && parsed < min) return min;
if (max !== undefined && parsed > max) return max;
return parsed;
};
/**
* Parse string input with trimming and null handling
* @param {*} value - Input value
* @param {string|null} defaultValue - Default if empty
* @returns {string|null}
*/
const parseStringInput = (value, defaultValue = null) => {
if (value === undefined || value === null) {
return defaultValue;
}
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed || defaultValue;
}
return String(value);
};
/**
* Parse JSON string safely
* @param {*} value - JSON string or already parsed value
* @param {*} defaultValue - Default if parsing fails
* @returns {*}
*/
const parseJsonInput = (value, defaultValue = null) => {
if (value === undefined || value === null) {
return defaultValue;
}
if (typeof value !== 'string') {
return value; // Already parsed
}
try {
return JSON.parse(value);
} catch {
return defaultValue;
}
};
module.exports = {
parseBooleanInput,
parseNumberInput,
parseStringInput,
parseJsonInput
};
Frontend Implementation
Create: frontend/src/utils/parsers.ts
/**
* Shared Parser Utilities for Frontend
*/
/**
* Parse any input value to boolean with configurable default
*/
export const toBoolean = (value: unknown, defaultValue = false): boolean => {
if (value === undefined || value === null) {
return defaultValue;
}
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
if (Number.isNaN(value)) return defaultValue;
return value !== 0;
}
if (typeof value === 'string') {
const normalized = value.toLowerCase().trim();
if (['true', '1', 'yes', 'on'].includes(normalized)) return true;
if (['false', '0', 'no', 'off', ''].includes(normalized)) return false;
}
return defaultValue;
};
/**
* Parse numeric input with validation
*/
export const toNumber = (
value: unknown,
defaultValue: number,
options?: { min?: number; max?: number }
): number => {
if (value === undefined || value === null || value === '') {
return defaultValue;
}
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return defaultValue;
}
if (options?.min !== undefined && parsed < options.min) return options.min;
if (options?.max !== undefined && parsed > options.max) return options.max;
return parsed;
};
Migration Steps
- Create new utility files
- Add comprehensive unit tests for all parser functions
- Update imports in affected files one at a time:
adminEvents.js: Replace inline function with importevents.js: Replace inline function with importSettingsPage.tsx: Replace inline function with import
- Remove duplicate function definitions
- Run full test suite
- Deploy
1.2 Consolidate Auth Routes
Problem: Three versions of auth routes exist
Files:
backend/src/routes/auth.js(136 lines) - Basic versionbackend/src/routes/auth-enhanced.js(395 lines) - Enhanced v1backend/src/routes/auth-enhanced-v2.js(393 lines) - Enhanced v2
Analysis Required:
# Determine which version is actively used
grep -r "auth-enhanced" backend/src/ --include="*.js"
grep -r "auth.js" backend/src/ --include="*.js"
Solution: Consolidate into single auth.js
Implementation Plan
-
Audit Current Usage
- Identify which auth file is mounted in
server.js - Document all endpoints from each version
- Create feature comparison matrix
- Identify which auth file is mounted in
-
Merge Strategy
auth.js (NEW - Consolidated) ├── All endpoints from auth-enhanced-v2.js (active version) ├── Any unique endpoints from other versions └── Deprecated endpoints marked for removal -
Create Feature Flags (if needed)
// config/features.js module.exports = { AUTH_USE_ENHANCED_TOKENS: true, AUTH_REQUIRE_EMAIL_VERIFICATION: false, }; -
Migration Steps
- Create new consolidated
auth.js - Update
server.jsto use new auth routes - Keep old files temporarily with deprecation notices
- Monitor for errors in production
- Remove deprecated files after 2 weeks
- Create new consolidated
1.3 Consolidate CreateEvent Pages
Problem: Two nearly identical event creation pages
Files:
frontend/src/pages/admin/CreateEventPage.tsx(709 lines)frontend/src/pages/admin/CreateEventPageEnhanced.tsx(709 lines)
Solution: Single CreateEventPage.tsx with all features
Difference Analysis
| Feature | Standard | Enhanced |
|---|---|---|
| Basic form fields | ✓ | ✓ |
| Theme customizer | ✓ | ✓ |
| Password generator | ✓ | ✓ |
| Advanced validation | ? | ✓ |
| Auto-save draft | ? | ✓ |
Implementation Plan
-
Diff the files to identify exact differences:
diff -u CreateEventPage.tsx CreateEventPageEnhanced.tsx > event_pages_diff.txt -
Merge enhanced features into standard page
-
Extract shared logic into custom hooks:
// hooks/useEventForm.ts export function useEventForm(initialData?: Partial<Event>) { // Form state management // Validation logic // Submit handlers } // hooks/useEventDraft.ts export function useEventDraft(eventData: Partial<Event>) { // Auto-save logic // Draft recovery } -
Update router to use single page
-
Delete duplicate file
1.4 Consolidate CMS Pages
Problem: Two CMS page implementations
Files:
frontend/src/pages/admin/CMSPage.tsx(538 lines)frontend/src/pages/admin/CMSPageEnhanced.tsx(618 lines)
Enhanced Features:
- Auto-save functionality
- Unsaved changes warning
- Enhanced editor toolbar
Solution: Single CMSPage.tsx with optional features
Implementation Plan
-
Create feature-complete CMSPage.tsx:
interface CMSPageProps { enableAutoSave?: boolean; enableUnsavedWarning?: boolean; } export const CMSPage: React.FC<CMSPageProps> = ({ enableAutoSave = true, enableUnsavedWarning = true }) => { // Merged implementation }; -
Extract reusable hooks:
// hooks/useAutoSave.ts export function useAutoSave<T>( data: T, saveFn: (data: T) => Promise<void>, debounceMs = 2000 ) { ... } // hooks/useUnsavedChanges.ts export function useUnsavedChanges(hasChanges: boolean) { ... } -
Delete duplicate file
1.5 Extract Multer Configuration Factory
Problem: Multer storage configuration duplicated across multiple route files
Files with Multer configs:
adminPhotos.jsadminSettings.jsadminEvents.jsprotectedImages.js
Solution: Centralized multer configuration factory
Implementation
Create: backend/src/config/multerConfig.js
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');
const fs = require('fs').promises;
/**
* Create disk storage configuration
* @param {Object} options
* @param {string} options.destination - Upload directory path
* @param {Function} options.filenameGenerator - Custom filename generator
*/
const createDiskStorage = ({ destination, filenameGenerator }) => {
return multer.diskStorage({
destination: async (req, file, cb) => {
try {
await fs.mkdir(destination, { recursive: true });
cb(null, destination);
} catch (error) {
cb(error);
}
},
filename: filenameGenerator || ((req, file, cb) => {
const uniqueSuffix = `${Date.now()}-${crypto.randomBytes(6).toString('hex')}`;
const ext = path.extname(file.originalname).toLowerCase();
cb(null, `${uniqueSuffix}${ext}`);
})
});
};
/**
* Create multer upload instance with standard limits
* @param {Object} options
* @param {multer.StorageEngine} options.storage - Multer storage engine
* @param {number} options.maxFileSize - Max file size in bytes (default 50MB)
* @param {string[]} options.allowedMimeTypes - Allowed MIME types
* @param {number} options.maxFiles - Max files per request (default 100)
*/
const createUpload = ({
storage,
maxFileSize = 50 * 1024 * 1024,
allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
maxFiles = 100
}) => {
return multer({
storage,
limits: {
fileSize: maxFileSize,
files: maxFiles
},
fileFilter: (req, file, cb) => {
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error(`File type ${file.mimetype} not allowed`));
}
}
});
};
/**
* Pre-configured uploads for common use cases
*/
const uploads = {
photos: (storagePath) => createUpload({
storage: createDiskStorage({ destination: storagePath }),
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'video/mp4', 'video/webm', 'video/quicktime'],
maxFiles: 500
}),
logos: (storagePath) => createUpload({
storage: createDiskStorage({ destination: storagePath }),
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/svg+xml', 'image/webp'],
maxFileSize: 5 * 1024 * 1024,
maxFiles: 1
}),
general: (storagePath) => createUpload({
storage: createDiskStorage({ destination: storagePath })
})
};
module.exports = {
createDiskStorage,
createUpload,
uploads
};
Phase 2: Backend Route & Service Refactoring
Priority: HIGH Estimated Effort: 5-7 days Risk: Medium Dependencies: Phase 1 complete
2.1 Create Event Service Layer
Current: adminEvents.js (1,088 lines) with business logic in route handlers
Target: Thin routes + dedicated EventService
New File Structure
backend/src/
├── routes/
│ └── adminEvents.js # ~200 lines (HTTP only)
├── services/
│ └── events/
│ ├── index.js # Public exports
│ ├── eventService.js # Main service (~300 lines)
│ ├── eventValidator.js # Validation logic (~100 lines)
│ ├── eventMapper.js # Data transformation (~80 lines)
│ └── eventRepository.js # Database queries (~150 lines)
└── utils/
└── parsers.js # Shared parsers
EventService Implementation
Create: backend/src/services/events/eventService.js
const { db, logActivity } = require('../../database/db');
const eventRepository = require('./eventRepository');
const eventValidator = require('./eventValidator');
const eventMapper = require('./eventMapper');
const { queueEmail } = require('../emailProcessor');
const { buildShareLinkVariants } = require('../shareLinkService');
const logger = require('../../utils/logger');
class EventService {
/**
* Create a new event
* @param {Object} eventData - Event creation data
* @param {Object} admin - Admin user creating the event
* @returns {Promise<Object>} Created event
*/
async createEvent(eventData, admin) {
// Validate input
const validation = await eventValidator.validateCreate(eventData);
if (!validation.valid) {
throw new ValidationError(validation.errors);
}
// Transform data
const eventRecord = eventMapper.toDatabase(eventData);
// Generate slug and share link
const slug = this.generateSlug(eventData);
const shareToken = crypto.randomBytes(16).toString('hex');
const { shareLinkToStore } = await buildShareLinkVariants({ slug, shareToken });
// Create in database
const [event] = await eventRepository.create({
...eventRecord,
slug,
share_token: shareToken,
share_link: shareLinkToStore,
created_by: admin.id
});
// Create storage folder
await this.createEventFolder(slug);
// Queue welcome email
if (eventData.customer_email) {
await queueEmail(event.id, eventData.customer_email, 'event_created', {
event_name: event.event_name,
gallery_link: shareLinkToStore
});
}
// Log activity
await logActivity('event_created', 'admin', admin.id, admin.username, {
event_id: event.id,
event_name: event.event_name
});
return eventMapper.toApi(event);
}
/**
* Update an existing event
*/
async updateEvent(eventId, updateData, admin) {
const event = await eventRepository.findById(eventId);
if (!event) {
throw new NotFoundError('Event not found');
}
const validation = await eventValidator.validateUpdate(updateData, event);
if (!validation.valid) {
throw new ValidationError(validation.errors);
}
const updatedEvent = await eventRepository.update(eventId,
eventMapper.toDatabase(updateData)
);
await logActivity('event_updated', 'admin', admin.id, admin.username, {
event_id: eventId,
changes: updateData
});
return eventMapper.toApi(updatedEvent);
}
/**
* Archive an event
*/
async archiveEvent(eventId, admin) {
// Implementation
}
/**
* Delete an event
*/
async deleteEvent(eventId, admin) {
// Implementation
}
/**
* List events with filtering
*/
async listEvents(filters, pagination) {
const events = await eventRepository.findMany(filters, pagination);
return events.map(eventMapper.toApi);
}
}
module.exports = new EventService();
Refactored Route Handler
Update: backend/src/routes/adminEvents.js
const express = require('express');
const { body, query, validationResult } = require('express-validator');
const { adminAuth } = require('../middleware/auth-enhanced-v2');
const eventService = require('../services/events');
const { handleAsync } = require('../utils/routeHelpers');
const router = express.Router();
// Apply admin auth to all routes
router.use(adminAuth);
/**
* GET /api/admin/events
* List all events with optional filtering
*/
router.get('/', [
query('status').optional().isIn(['active', 'archived', 'expiring']),
query('search').optional().isString(),
query('page').optional().isInt({ min: 1 }),
query('limit').optional().isInt({ min: 1, max: 100 })
], handleAsync(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const events = await eventService.listEvents(req.query, {
page: req.query.page || 1,
limit: req.query.limit || 20
});
res.json(events);
}));
/**
* POST /api/admin/events
* Create a new event
*/
router.post('/', [
body('event_name').isString().trim().isLength({ min: 3, max: 200 }),
body('event_type').isIn(['wedding', 'birthday', 'corporate', 'other']),
body('event_date').isISO8601(),
body('customer_email').optional().isEmail(),
body('password').optional().isLength({ min: 6 })
], handleAsync(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const event = await eventService.createEvent(req.body, req.admin);
res.status(201).json(event);
}));
/**
* PUT /api/admin/events/:id
* Update an event
*/
router.put('/:id', handleAsync(async (req, res) => {
const event = await eventService.updateEvent(
parseInt(req.params.id),
req.body,
req.admin
);
res.json(event);
}));
/**
* DELETE /api/admin/events/:id
* Delete an event
*/
router.delete('/:id', handleAsync(async (req, res) => {
await eventService.deleteEvent(parseInt(req.params.id), req.admin);
res.status(204).send();
}));
/**
* POST /api/admin/events/:id/archive
* Archive an event
*/
router.post('/:id/archive', handleAsync(async (req, res) => {
const result = await eventService.archiveEvent(
parseInt(req.params.id),
req.admin
);
res.json(result);
}));
module.exports = router;
2.2 Create Photo Service Layer
Current: adminPhotos.js (1,005 lines)
Target: Similar structure to EventService
New File Structure
backend/src/services/photos/
├── index.js
├── photoService.js # Main service
├── photoProcessor.js # Image/video processing
├── photoRepository.js # Database queries
├── photoStorage.js # File system operations
└── photoValidator.js # Validation
2.3 Create Settings Service Layer
Current: adminSettings.js (1,056 lines)
Target: Domain-specific services
New File Structure
backend/src/services/settings/
├── index.js
├── settingsService.js # Main orchestrator
├── brandingService.js # Logo, favicon, company info
├── themeService.js # Theme configuration
├── securityService.js # Security settings
└── settingsRepository.js # Database queries
2.4 Decompose Backup/Restore Services
Current:
backupService.js(1,120 lines)restoreService.js(1,220 lines)
Target: Smaller, focused classes
New File Structure
backend/src/services/backup/
├── index.js
├── BackupOrchestrator.js # Main coordinator (~200 lines)
├── BackupScheduler.js # Cron job management (~150 lines)
├── BackupExecutor.js # Backup creation (~300 lines)
├── BackupUploader.js # S3/remote upload (~200 lines)
├── BackupValidator.js # Validation logic (~100 lines)
└── BackupNotifier.js # Email notifications (~100 lines)
backend/src/services/restore/
├── index.js
├── RestoreOrchestrator.js # Main coordinator (~200 lines)
├── RestoreValidator.js # Pre-restore validation (~200 lines)
├── RestoreExecutor.js # Actual restore logic (~300 lines)
├── RestoreRollback.js # Rollback on failure (~150 lines)
└── RestoreLogger.js # Detailed logging (~100 lines)
Phase 3: Frontend Page Decomposition
Priority: HIGH Estimated Effort: 5-7 days Risk: Medium Dependencies: Phase 1 complete
3.1 Decompose SettingsPage
Current: SettingsPage.tsx (1,839 lines) with 8 tabs and 27 hooks
Target: Composition-based page with tab components
New File Structure
frontend/src/features/settings/
├── index.ts # Public exports
├── SettingsPage.tsx # Main page (~100 lines)
├── SettingsLayout.tsx # Tab layout component
├── tabs/
│ ├── index.ts
│ ├── GeneralSettingsTab.tsx # ~200 lines
│ ├── EventSettingsTab.tsx # ~150 lines
│ ├── StatusTab.tsx # ~200 lines
│ ├── SecuritySettingsTab.tsx # ~200 lines
│ ├── CategoriesTab.tsx # ~100 lines
│ ├── AnalyticsSettingsTab.tsx # ~150 lines
│ ├── ModerationTab.tsx # ~100 lines
│ └── StylingTab.tsx # ~100 lines
├── hooks/
│ ├── index.ts
│ ├── useGeneralSettings.ts # Settings query/mutation
│ ├── useSecuritySettings.ts
│ ├── useAnalyticsSettings.ts
│ ├── useSystemStatus.ts
│ └── useAdminProfile.ts
└── components/
├── AccountForm.tsx
├── FeatureToggles.tsx
├── StorageIndicator.tsx
└── SystemStatusCard.tsx
Refactored SettingsPage
// features/settings/SettingsPage.tsx
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SettingsLayout } from './SettingsLayout';
import {
GeneralSettingsTab,
EventSettingsTab,
StatusTab,
SecuritySettingsTab,
CategoriesTab,
AnalyticsSettingsTab,
ModerationTab,
StylingTab
} from './tabs';
type SettingsTab =
| 'general'
| 'events'
| 'status'
| 'security'
| 'categories'
| 'analytics'
| 'moderation'
| 'styling';
export const SettingsPage: React.FC = () => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<SettingsTab>('general');
const tabs = [
{ id: 'general', label: t('settings.general.title') },
{ id: 'events', label: t('settings.events.title') },
{ id: 'status', label: t('settings.status.title') },
{ id: 'security', label: t('settings.security.title') },
{ id: 'categories', label: t('settings.categories.title') },
{ id: 'analytics', label: t('settings.analytics.title') },
{ id: 'moderation', label: t('settings.moderation.title') },
{ id: 'styling', label: t('settings.styling.title') },
] as const;
const renderTabContent = () => {
switch (activeTab) {
case 'general':
return <GeneralSettingsTab />;
case 'events':
return <EventSettingsTab />;
case 'status':
return <StatusTab />;
case 'security':
return <SecuritySettingsTab />;
case 'categories':
return <CategoriesTab />;
case 'analytics':
return <AnalyticsSettingsTab />;
case 'moderation':
return <ModerationTab />;
case 'styling':
return <StylingTab />;
}
};
return (
<SettingsLayout
title={t('settings.title')}
description={t('settings.description')}
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
>
{renderTabContent()}
</SettingsLayout>
);
};
Example Tab Component
// features/settings/tabs/GeneralSettingsTab.tsx
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Card } from '../../../components/common';
import {
useGeneralSettings,
useAdminProfile,
useSaveGeneralSettings
} from '../hooks';
import { AccountForm } from '../components/AccountForm';
import { FeatureToggles } from '../components/FeatureToggles';
import { LanguageSelector } from '../components/LanguageSelector';
import { DateFormatSelector } from '../components/DateFormatSelector';
export const GeneralSettingsTab: React.FC = () => {
const { t } = useTranslation();
const { data: settings, isLoading } = useGeneralSettings();
const { data: profile } = useAdminProfile();
const saveMutation = useSaveGeneralSettings();
if (isLoading) {
return <Loading />;
}
return (
<div className="space-y-6">
<Card padding="md">
<h2 className="text-lg font-semibold mb-4">
{t('settings.general.accountSection')}
</h2>
<AccountForm
initialData={profile}
onSave={(data) => saveMutation.mutate(data)}
isSaving={saveMutation.isPending}
/>
</Card>
<Card padding="md">
<h2 className="text-lg font-semibold mb-4">
{t('settings.general.features')}
</h2>
<FeatureToggles settings={settings} />
</Card>
<Card padding="md">
<h2 className="text-lg font-semibold mb-4">
{t('settings.general.localization')}
</h2>
<LanguageSelector />
<DateFormatSelector />
</Card>
</div>
);
};
3.2 Decompose EventDetailsPage
Current: EventDetailsPage.tsx (1,479 lines) with 33 hooks
Target: Section-based composition
New File Structure
frontend/src/features/events/
├── index.ts
├── EventDetailsPage.tsx # Main page (~150 lines)
├── sections/
│ ├── index.ts
│ ├── EventInfoSection.tsx # ~200 lines
│ ├── PhotoManagementSection.tsx # ~250 lines
│ ├── ThemeSection.tsx # ~150 lines
│ ├── FeedbackSection.tsx # ~150 lines
│ ├── ShareLinkSection.tsx # ~150 lines
│ └── ActionsSection.tsx # ~100 lines
├── hooks/
│ ├── index.ts
│ ├── useEventDetails.ts
│ ├── useEventPhotos.ts
│ ├── useEventTheme.ts
│ ├── useEventFeedback.ts
│ └── useEventActions.ts
└── components/
├── EventHeader.tsx
├── EventStats.tsx
├── PhotoGrid.tsx
└── PhotoViewer.tsx
3.3 Decompose Large Components
ThemeCustomizerEnhanced (607 lines)
Split into:
frontend/src/components/admin/theme/
├── index.ts
├── ThemeCustomizer.tsx # Main component (~150 lines)
├── ColorEditor.tsx # Color pickers (~150 lines)
├── TypographyEditor.tsx # Font selection (~100 lines)
├── LayoutSelector.tsx # Gallery layout (~100 lines)
├── PresetSelector.tsx # Theme presets (~80 lines)
└── ThemePreview.tsx # Live preview (~100 lines)
CMSEditor (572 lines)
Split into:
frontend/src/components/admin/cms/
├── index.ts
├── CMSEditor.tsx # Main editor (~150 lines)
├── EditorToolbar.tsx # Formatting toolbar (~150 lines)
├── EditorPreview.tsx # Preview pane (~80 lines)
├── ViewModeSelector.tsx # Edit/Preview/Split (~60 lines)
├── EditorStats.tsx # Word/char count (~50 lines)
└── EditorHelp.tsx # Help modal (~80 lines)
AdminPhotoViewer (515 lines)
Split into:
frontend/src/components/admin/photos/
├── index.ts
├── PhotoViewer.tsx # Main viewer (~150 lines)
├── PhotoNavigation.tsx # Prev/next controls (~80 lines)
├── PhotoMetadata.tsx # File info display (~100 lines)
├── PhotoFeedback.tsx # Ratings/comments (~100 lines)
├── PhotoActions.tsx # Download/delete (~80 lines)
└── hooks/
├── usePhotoNavigation.ts
└── usePhotoFeedback.ts
Phase 4: Cross-Cutting Concerns
Priority: MEDIUM Estimated Effort: 2-3 days Dependencies: Phases 1-3 complete
4.1 Create Route Helpers Utility
Create: backend/src/utils/routeHelpers.js
/**
* Async route handler wrapper
* Catches errors and passes to error middleware
*/
const handleAsync = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
/**
* Validate request and return early if invalid
*/
const validateRequest = (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(400).json({ errors: errors.array() });
return false;
}
return true;
};
/**
* Standard success response
*/
const successResponse = (res, data, statusCode = 200) => {
res.status(statusCode).json({
success: true,
data
});
};
/**
* Standard error response
*/
const errorResponse = (res, message, statusCode = 500, details = null) => {
res.status(statusCode).json({
success: false,
error: message,
details
});
};
module.exports = {
handleAsync,
validateRequest,
successResponse,
errorResponse
};
4.2 Create Custom Error Classes
Create: backend/src/utils/errors.js
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);
}
}
class ValidationError extends AppError {
constructor(errors) {
super('Validation failed', 400, 'VALIDATION_ERROR');
this.errors = errors;
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404, 'NOT_FOUND');
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401, 'UNAUTHORIZED');
}
}
class ForbiddenError extends AppError {
constructor(message = 'Forbidden') {
super(message, 403, 'FORBIDDEN');
}
}
class ConflictError extends AppError {
constructor(message = 'Resource conflict') {
super(message, 409, 'CONFLICT');
}
}
module.exports = {
AppError,
ValidationError,
NotFoundError,
UnauthorizedError,
ForbiddenError,
ConflictError
};
4.3 Create Global Error Handler
Update: backend/src/middleware/errorHandler.js
const logger = require('../utils/logger');
const { AppError } = require('../utils/errors');
const errorHandler = (err, req, res, next) => {
// Log error
logger.error('Request error', {
error: err.message,
stack: err.stack,
path: req.path,
method: req.method,
body: req.body,
user: req.admin?.id
});
// Operational errors (expected)
if (err instanceof AppError && err.isOperational) {
return res.status(err.statusCode).json({
success: false,
error: err.message,
code: err.code,
...(err.errors && { errors: err.errors })
});
}
// Multer errors
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
success: false,
error: 'File too large',
code: 'FILE_TOO_LARGE'
});
}
// Database errors
if (err.code === 'SQLITE_CONSTRAINT' || err.code === '23505') {
return res.status(409).json({
success: false,
error: 'Resource already exists',
code: 'DUPLICATE_ENTRY'
});
}
// Unknown errors - don't leak details
res.status(500).json({
success: false,
error: 'An unexpected error occurred',
code: 'INTERNAL_ERROR'
});
};
module.exports = errorHandler;
Testing Strategy
Unit Tests Required
Backend
| File | Test File | Coverage Target |
|---|---|---|
utils/parsers.js |
parsers.test.js |
100% |
services/events/eventService.js |
eventService.test.js |
90% |
services/events/eventValidator.js |
eventValidator.test.js |
100% |
services/photos/photoService.js |
photoService.test.js |
90% |
services/settings/settingsService.js |
settingsService.test.js |
90% |
services/backup/BackupOrchestrator.js |
BackupOrchestrator.test.js |
85% |
services/restore/RestoreOrchestrator.js |
RestoreOrchestrator.test.js |
85% |
Frontend
| Component | Test File | Coverage Target |
|---|---|---|
utils/parsers.ts |
parsers.test.ts |
100% |
features/settings/hooks/* |
*.test.ts |
90% |
features/settings/tabs/* |
*.test.tsx |
80% |
features/events/hooks/* |
*.test.ts |
90% |
Integration Tests Required
| Scenario | Test File |
|---|---|
| Event CRUD operations | events.integration.test.js |
| Photo upload flow | photos.integration.test.js |
| Settings management | settings.integration.test.js |
| Backup/Restore cycle | backup.integration.test.js |
E2E Tests Required
| Flow | Test File |
|---|---|
| Create event end-to-end | create-event.e2e.ts |
| Settings page navigation | settings.e2e.ts |
| Photo management | photos.e2e.ts |
Migration & Rollback Plan
Pre-Migration Checklist
- All existing tests pass
- Database backup created
- Feature flags configured
- Rollback scripts prepared
- Monitoring alerts configured
Phase Rollout Strategy
┌─────────────────────────────────────────────────────────────┐
│ ROLLOUT TIMELINE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Week 1: Phase 1 (Code Duplication) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Day 1-2: Create shared utilities │ │
│ │ Day 3: Update imports, run tests │ │
│ │ Day 4: Deploy to staging │ │
│ │ Day 5: Monitor, fix issues, deploy to production │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Week 2-3: Phase 2 (Backend Services) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Day 1-3: EventService extraction │ │
│ │ Day 4-5: PhotoService extraction │ │
│ │ Day 6-7: SettingsService extraction │ │
│ │ Day 8-10: Backup/Restore decomposition │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Week 4-5: Phase 3 (Frontend Decomposition) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Day 1-3: SettingsPage decomposition │ │
│ │ Day 4-5: EventDetailsPage decomposition │ │
│ │ Day 6-7: Component decomposition │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Week 6: Phase 4 (Cross-Cutting) + Cleanup │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Day 1-2: Error handling, route helpers │ │
│ │ Day 3-4: Delete deprecated files │ │
│ │ Day 5: Final testing, documentation │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Rollback Procedures
Phase 1 Rollback
# Revert parser utility changes
git revert <commit-hash>
# No database changes required
Phase 2 Rollback
# Service layer changes are additive
# Routes can be reverted without data loss
git revert <commit-hash>
# Restart backend services
docker-compose restart backend
Phase 3 Rollback
# Frontend changes are purely presentational
git revert <commit-hash>
# Rebuild frontend
docker-compose build frontend
docker-compose restart frontend
Implementation Checklist
Phase 1: Code Duplication (Days 1-3)
-
1.1 Shared Parsers
- Create
backend/src/utils/parsers.js - Create
frontend/src/utils/parsers.ts - Write unit tests (100% coverage)
- Update
adminEvents.jsimports - Update
events.jsimports - Update
SettingsPage.tsximports - Delete inline function definitions
- Run full test suite
- Create
-
1.2 Auth Routes Consolidation
- Audit current auth route usage
- Create consolidated
auth.js - Update
server.jsmount point - Add deprecation notices to old files
- Test auth flows
- Schedule old file deletion
-
1.3 CreateEvent Page Consolidation
- Diff files to identify differences
- Create
hooks/useEventForm.ts - Merge enhanced features into standard
- Update router
- Delete duplicate file
-
1.4 CMS Page Consolidation
- Diff files to identify differences
- Create
hooks/useAutoSave.ts - Create
hooks/useUnsavedChanges.ts - Merge into single CMSPage
- Delete duplicate file
-
1.5 Multer Config Factory
- Create
config/multerConfig.js - Update
adminPhotos.js - Update
adminSettings.js - Update other files using multer
- Test all upload flows
- Create
Phase 2: Backend Services (Days 4-10)
-
2.1 Event Service
- Create
services/events/directory structure - Implement
eventService.js - Implement
eventRepository.js - Implement
eventValidator.js - Implement
eventMapper.js - Refactor
adminEvents.jsto use service - Write unit tests (90% coverage)
- Write integration tests
- Create
-
2.2 Photo Service
- Create
services/photos/directory structure - Implement service layer
- Refactor
adminPhotos.js - Write tests
- Create
-
2.3 Settings Service
- Create
services/settings/directory structure - Implement service layer
- Refactor
adminSettings.js - Write tests
- Create
-
2.4 Backup/Restore Decomposition
- Create
services/backup/directory structure - Split
backupService.jsinto components - Create
services/restore/directory structure - Split
restoreService.jsinto components - Write tests
- Create
Phase 3: Frontend Decomposition (Days 11-17)
-
3.1 Settings Page
- Create
features/settings/directory structure - Extract tab components
- Create custom hooks
- Refactor main page
- Write tests
- Create
-
3.2 Event Details Page
- Create
features/events/directory structure - Extract section components
- Create custom hooks
- Refactor main page
- Write tests
- Create
-
3.3 Component Decomposition
- Split
ThemeCustomizerEnhanced - Split
CMSEditor - Split
AdminPhotoViewer - Write tests
- Split
Phase 4: Cross-Cutting (Days 18-20)
-
4.1 Route Helpers
- Create
utils/routeHelpers.js - Update routes to use helpers
- Write tests
- Create
-
4.2 Error Classes
- Create
utils/errors.js - Create global error handler
- Update services to throw typed errors
- Write tests
- Create
-
4.3 Cleanup
- Delete deprecated auth route files
- Delete duplicate page files
- Update documentation
- Final review
Post-Implementation
- Update README with new architecture
- Create architecture diagram
- Conduct code review
- Performance benchmarking
- Monitor error rates for 1 week
Success Metrics
| Metric | Current | Target |
|---|---|---|
| Largest file (lines) | 1,839 | < 400 |
| Average file size | ~500 | < 200 |
| Code duplication | ~3,500 lines | 0 |
| Test coverage | ~60% | > 85% |
| Build time | - | No increase |
| Bundle size | - | No increase |
Appendix: File Impact Summary
Files to Create
| Path | Purpose | Est. Lines |
|---|---|---|
backend/src/utils/parsers.js |
Shared parsers | 80 |
backend/src/utils/routeHelpers.js |
Route utilities | 50 |
backend/src/utils/errors.js |
Error classes | 60 |
backend/src/config/multerConfig.js |
Upload config | 100 |
backend/src/services/events/* |
Event service layer | 600 |
backend/src/services/photos/* |
Photo service layer | 500 |
backend/src/services/settings/* |
Settings service layer | 400 |
backend/src/services/backup/* |
Backup components | 800 |
backend/src/services/restore/* |
Restore components | 700 |
frontend/src/utils/parsers.ts |
Shared parsers | 50 |
frontend/src/features/settings/* |
Settings feature | 1,200 |
frontend/src/features/events/* |
Events feature | 1,000 |
Files to Modify
| Path | Change Type |
|---|---|
backend/src/routes/adminEvents.js |
Reduce to ~200 lines |
backend/src/routes/adminPhotos.js |
Reduce to ~200 lines |
backend/src/routes/adminSettings.js |
Reduce to ~200 lines |
backend/server.js |
Update route imports |
frontend/src/pages/admin/SettingsPage.tsx |
Reduce to ~100 lines |
frontend/src/pages/admin/EventDetailsPage.tsx |
Reduce to ~150 lines |
Files to Delete
| Path | Reason |
|---|---|
backend/src/routes/auth-enhanced.js |
Consolidated into auth.js |
backend/src/routes/auth-enhanced-v2.js |
Consolidated into auth.js |
frontend/src/pages/admin/CreateEventPageEnhanced.tsx |
Merged into CreateEventPage |
frontend/src/pages/admin/CMSPageEnhanced.tsx |
Merged into CMSPage |
Document maintained by: Development Team Last updated: January 2026