feat: implement 4 new features with bug fixes and refactoring plan
## 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
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* XMP Sidecar File Generator
|
||||
* Generates Adobe XMP metadata files for photos with guest feedback
|
||||
*/
|
||||
|
||||
class XmpGenerator {
|
||||
/**
|
||||
* Generate XMP sidecar content for a photo
|
||||
* @param {Object} photo - Photo object with feedback data
|
||||
* @param {Object} options - Generation options
|
||||
* @returns {string} XMP file content
|
||||
*/
|
||||
generateXmp(photo, options = {}) {
|
||||
const {
|
||||
include_rating = true,
|
||||
include_label = true,
|
||||
include_description = true,
|
||||
include_keywords = true
|
||||
} = options;
|
||||
|
||||
const rating = include_rating ? this.mapRating(photo.average_rating) : 0;
|
||||
const label = include_label ? this.mapLabel(photo.average_rating) : null;
|
||||
|
||||
const descriptionXml = include_description ? this.generateDescription(photo) : '';
|
||||
const keywordsXml = include_keywords ? this.generateKeywords(photo) : '';
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="PicPeak Export">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
|
||||
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"
|
||||
xmlns:Iptc4xmpCore="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"
|
||||
xmp:Rating="${rating}"${label ? `
|
||||
xmp:Label="${label}"` : ''}>
|
||||
${descriptionXml}
|
||||
${keywordsXml}
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map PicPeak average rating to XMP 1-5 rating
|
||||
* @param {number} avgRating - Average rating (0-5, decimal)
|
||||
* @returns {number} XMP rating (0-5, integer)
|
||||
*/
|
||||
mapRating(avgRating) {
|
||||
if (!avgRating || avgRating === 0) return 0;
|
||||
if (avgRating >= 4.5) return 5;
|
||||
if (avgRating >= 3.5) return 4;
|
||||
if (avgRating >= 2.5) return 3;
|
||||
if (avgRating >= 1.5) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map PicPeak rating to XMP color label
|
||||
* @param {number} avgRating - Average rating
|
||||
* @returns {string|null} XMP label color
|
||||
*/
|
||||
mapLabel(avgRating) {
|
||||
if (!avgRating || avgRating === 0) return null;
|
||||
if (avgRating >= 4.5) return 'Red'; // Top picks
|
||||
if (avgRating >= 3.5) return 'Yellow'; // Good
|
||||
if (avgRating >= 2.5) return 'Green'; // Average
|
||||
if (avgRating >= 1.5) return 'Blue'; // Below average
|
||||
return 'Purple'; // Low
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate XMP description element
|
||||
* @param {Object} photo - Photo object
|
||||
* @returns {string} Description XML
|
||||
*/
|
||||
generateDescription(photo) {
|
||||
const rating = photo.average_rating ? photo.average_rating.toFixed(1) : '0';
|
||||
const likes = photo.like_count || 0;
|
||||
const favorites = photo.favorite_count || 0;
|
||||
|
||||
const desc = `PicPeak Guest Feedback: ${rating} stars, ${likes} likes, ${favorites} favorites`;
|
||||
|
||||
return `<dc:description>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang="x-default">${this.escapeXml(desc)}</rdf:li>
|
||||
</rdf:Alt>
|
||||
</dc:description>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate XMP keywords element
|
||||
* @param {Object} photo - Photo object
|
||||
* @returns {string} Keywords XML
|
||||
*/
|
||||
generateKeywords(photo) {
|
||||
const keywords = ['picpeak-export'];
|
||||
|
||||
if (photo.average_rating >= 4) {
|
||||
keywords.push('guest-pick');
|
||||
}
|
||||
|
||||
if (photo.average_rating >= 4.5) {
|
||||
keywords.push('top-rated');
|
||||
}
|
||||
|
||||
if (photo.like_count >= 5) {
|
||||
keywords.push('popular');
|
||||
}
|
||||
|
||||
if (photo.favorite_count > 0) {
|
||||
keywords.push('favorited');
|
||||
}
|
||||
|
||||
if (photo.category_name) {
|
||||
keywords.push(this.sanitizeKeyword(photo.category_name));
|
||||
}
|
||||
|
||||
return `<dc:subject>
|
||||
<rdf:Bag>
|
||||
${keywords.map(k => `<rdf:li>${this.escapeXml(k)}</rdf:li>`).join('\n ')}
|
||||
</rdf:Bag>
|
||||
</dc:subject>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape special XML characters
|
||||
* @param {string} str - Input string
|
||||
* @returns {string} Escaped string
|
||||
*/
|
||||
escapeXml(str) {
|
||||
if (!str) return '';
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize keyword for XMP
|
||||
* @param {string} keyword - Raw keyword
|
||||
* @returns {string} Sanitized keyword
|
||||
*/
|
||||
sanitizeKeyword(keyword) {
|
||||
return keyword
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get XMP filename from photo filename
|
||||
* @param {string} photoFilename - Photo filename
|
||||
* @returns {string} XMP filename
|
||||
*/
|
||||
getXmpFilename(photoFilename) {
|
||||
return photoFilename.replace(/\.[^.]+$/, '.xmp');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { XmpGenerator };
|
||||
Reference in New Issue
Block a user