Fix multiple bugs: thumbnail generation, branding settings, categories, theme, feedback icons, upload limit, email errors
Bug fixes included: #52 - Thumbnail Generation: Added proper parsing of settings values and validation of Sharp fit parameter to handle JSON-encoded strings correctly #61 - Branding Settings Not Persisting: Added _parseBoolean helper for reliable boolean parsing, added hide_powered_by option for white-label support #55 - Categories Not Applied: Fixed category update logic to properly handle numeric category IDs, added updated_at timestamp, improved cache invalidation #59/#56 - Gallery Layout & Apply Theme: Set isPreviewMode=true so theme changes immediately propagate to parent state, hidden redundant Apply button #58 - Feedback Icons Show When Disabled: Added feedbackEnabled check to comment and like buttons in MasonryGalleryLayout and GridGalleryLayout #57 - Upload Limit 100MB: Increased body parser limit from 100MB to 500MB to support larger batch uploads #54 - Wrong Error Message: Enhanced email error handling with specific error codes and translation keys for better user feedback
This commit is contained in:
+3
-3
@@ -324,9 +324,9 @@ async function initializeRateLimiters() {
|
|||||||
|
|
||||||
// Note: Rate limiters will be initialized after database connection
|
// Note: Rate limiters will be initialized after database connection
|
||||||
|
|
||||||
// Body parsing middleware with increased limits for large uploads
|
// Body parsing middleware with increased limits for large batch uploads
|
||||||
app.use(express.json({ limit: '100mb' }));
|
app.use(express.json({ limit: '500mb' }));
|
||||||
app.use(express.urlencoded({ extended: true, limit: '100mb' }));
|
app.use(express.urlencoded({ extended: true, limit: '500mb' }));
|
||||||
|
|
||||||
// Request logging for API routes (with timestamps)
|
// Request logging for API routes (with timestamps)
|
||||||
const apiRequestLogger = (req, res, next) => {
|
const apiRequestLogger = (req, res, next) => {
|
||||||
|
|||||||
@@ -173,26 +173,57 @@ router.post('/test', adminAuth, async (req, res) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Test email error:', error);
|
console.error('Test email error:', error);
|
||||||
console.error('Error stack:', error.stack);
|
console.error('Error stack:', error.stack);
|
||||||
|
|
||||||
// Provide more specific error messages
|
// Provide more specific error messages with translation keys
|
||||||
let errorMessage = 'Failed to send test email';
|
let errorMessage = 'Error sending email';
|
||||||
|
let errorKey = 'email.errors.sendFailed';
|
||||||
let details = error.message;
|
let details = error.message;
|
||||||
|
let detailsKey = 'email.errors.unknownError';
|
||||||
|
|
||||||
if (error.code === 'ECONNREFUSED') {
|
if (error.code === 'ECONNREFUSED') {
|
||||||
errorMessage = 'Failed to connect to SMTP server';
|
errorMessage = 'Failed to connect to SMTP server';
|
||||||
|
errorKey = 'email.errors.connectionRefused';
|
||||||
details = 'Please check your SMTP host and port settings';
|
details = 'Please check your SMTP host and port settings';
|
||||||
|
detailsKey = 'email.errors.checkHostPort';
|
||||||
} else if (error.code === 'EAUTH') {
|
} else if (error.code === 'EAUTH') {
|
||||||
errorMessage = 'SMTP authentication failed';
|
errorMessage = 'SMTP authentication failed';
|
||||||
|
errorKey = 'email.errors.authFailed';
|
||||||
details = 'Please check your SMTP username and password';
|
details = 'Please check your SMTP username and password';
|
||||||
|
detailsKey = 'email.errors.checkCredentials';
|
||||||
} else if (error.code === 'ESOCKET') {
|
} else if (error.code === 'ESOCKET') {
|
||||||
errorMessage = 'Network error';
|
errorMessage = 'Network error connecting to SMTP server';
|
||||||
|
errorKey = 'email.errors.networkError';
|
||||||
details = 'Could not establish connection to SMTP server';
|
details = 'Could not establish connection to SMTP server';
|
||||||
|
detailsKey = 'email.errors.connectionFailed';
|
||||||
|
} else if (error.code === 'ETIMEDOUT') {
|
||||||
|
errorMessage = 'Connection to SMTP server timed out';
|
||||||
|
errorKey = 'email.errors.timeout';
|
||||||
|
details = 'The server took too long to respond. Please check your network and SMTP settings.';
|
||||||
|
detailsKey = 'email.errors.timeoutDetails';
|
||||||
|
} else if (error.code === 'ENOTFOUND') {
|
||||||
|
errorMessage = 'SMTP server not found';
|
||||||
|
errorKey = 'email.errors.serverNotFound';
|
||||||
|
details = 'The SMTP host could not be resolved. Please verify the hostname.';
|
||||||
|
detailsKey = 'email.errors.checkHostname';
|
||||||
|
} else if (error.responseCode >= 500) {
|
||||||
|
errorMessage = 'SMTP server error';
|
||||||
|
errorKey = 'email.errors.serverError';
|
||||||
|
details = `Server returned error code ${error.responseCode}`;
|
||||||
|
detailsKey = 'email.errors.serverErrorDetails';
|
||||||
|
} else if (error.responseCode >= 400) {
|
||||||
|
errorMessage = 'Email rejected by server';
|
||||||
|
errorKey = 'email.errors.rejected';
|
||||||
|
details = error.response || 'The email was rejected. Check recipient address and settings.';
|
||||||
|
detailsKey = 'email.errors.rejectedDetails';
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
|
errorKey: errorKey,
|
||||||
details: details,
|
details: details,
|
||||||
code: error.code
|
detailsKey: detailsKey,
|
||||||
|
code: error.code,
|
||||||
|
responseCode: error.responseCode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -484,24 +484,42 @@ router.patch('/:eventId/photos/:photoId', adminAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prepare update data
|
// Prepare update data
|
||||||
const updateData = {};
|
const updateData = {
|
||||||
|
updated_at: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
// Handle type-based categories ('individual' or 'collage')
|
// Handle type-based categories ('individual' or 'collage')
|
||||||
// These are string values that map to the photo.type field
|
// These are string values that map to the photo.type field
|
||||||
if (category_id === 'individual' || category_id === 'collage') {
|
if (category_id === 'individual' || category_id === 'collage') {
|
||||||
updateData.type = category_id;
|
updateData.type = category_id;
|
||||||
updateData.category_id = null; // Clear legacy category_id
|
updateData.category_id = null; // Clear legacy category_id
|
||||||
|
} else if (category_id === null || category_id === undefined) {
|
||||||
|
// Explicitly clear category
|
||||||
|
updateData.category_id = null;
|
||||||
} else {
|
} else {
|
||||||
// Handle legacy numeric category IDs
|
// Handle numeric category IDs from photo_categories table
|
||||||
updateData.category_id = category_id || null;
|
const numericCategoryId = parseInt(category_id, 10);
|
||||||
|
if (!isNaN(numericCategoryId)) {
|
||||||
|
updateData.category_id = numericCategoryId;
|
||||||
|
} else {
|
||||||
|
updateData.category_id = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update photo
|
// Update photo
|
||||||
await db('photos')
|
await db('photos')
|
||||||
.where({ id: photoId })
|
.where({ id: photoId, event_id: eventId })
|
||||||
.update(updateData);
|
.update(updateData);
|
||||||
|
|
||||||
res.json({ message: 'Photo updated successfully' });
|
// Fetch and return updated photo for confirmation
|
||||||
|
const updatedPhoto = await db('photos')
|
||||||
|
.where({ id: photoId })
|
||||||
|
.first();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Photo updated successfully',
|
||||||
|
photo: updatedPhoto
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating photo:', error);
|
console.error('Error updating photo:', error);
|
||||||
res.status(500).json({ error: 'Failed to update photo' });
|
res.status(500).json({ error: 'Failed to update photo' });
|
||||||
@@ -581,33 +599,44 @@ router.post('/:eventId/photos/bulk-update', adminAuth, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { eventId } = req.params;
|
const { eventId } = req.params;
|
||||||
const { photoIds, updates } = req.body;
|
const { photoIds, updates } = req.body;
|
||||||
|
|
||||||
if (!Array.isArray(photoIds) || photoIds.length === 0) {
|
if (!Array.isArray(photoIds) || photoIds.length === 0) {
|
||||||
return res.status(400).json({ error: 'Invalid photo IDs' });
|
return res.status(400).json({ error: 'Invalid photo IDs' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify all photos belong to the event
|
// Verify all photos belong to the event
|
||||||
const photoCount = await db('photos')
|
const photoCount = await db('photos')
|
||||||
.whereIn('id', photoIds)
|
.whereIn('id', photoIds)
|
||||||
.where('event_id', eventId)
|
.where('event_id', eventId)
|
||||||
.count('id as count')
|
.count('id as count')
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (photoCount.count !== photoIds.length) {
|
if (parseInt(photoCount.count) !== photoIds.length) {
|
||||||
return res.status(400).json({ error: 'Some photos do not belong to this event' });
|
return res.status(400).json({ error: 'Some photos do not belong to this event' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare update data
|
// Prepare update data
|
||||||
const updateData = {};
|
const updateData = {
|
||||||
|
updated_at: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
if (updates.category_id !== undefined) {
|
if (updates.category_id !== undefined) {
|
||||||
// Handle type-based categories ('individual' or 'collage')
|
// Handle type-based categories ('individual' or 'collage')
|
||||||
// These are string values that map to the photo.type field
|
// These are string values that map to the photo.type field
|
||||||
if (updates.category_id === 'individual' || updates.category_id === 'collage') {
|
if (updates.category_id === 'individual' || updates.category_id === 'collage') {
|
||||||
updateData.type = updates.category_id;
|
updateData.type = updates.category_id;
|
||||||
updateData.category_id = null; // Clear legacy category_id
|
updateData.category_id = null; // Clear legacy category_id
|
||||||
|
} else if (updates.category_id === null) {
|
||||||
|
// Explicitly clear category
|
||||||
|
updateData.category_id = null;
|
||||||
} else {
|
} else {
|
||||||
// Handle legacy numeric category IDs
|
// Handle numeric category IDs from photo_categories table
|
||||||
updateData.category_id = updates.category_id || null;
|
const numericCategoryId = parseInt(updates.category_id, 10);
|
||||||
|
if (!isNaN(numericCategoryId)) {
|
||||||
|
updateData.category_id = numericCategoryId;
|
||||||
|
} else {
|
||||||
|
updateData.category_id = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,7 +191,8 @@ router.put('/branding', adminAuth, async (req, res) => {
|
|||||||
logo_position,
|
logo_position,
|
||||||
logo_display_header,
|
logo_display_header,
|
||||||
logo_display_hero,
|
logo_display_hero,
|
||||||
logo_display_mode
|
logo_display_mode,
|
||||||
|
hide_powered_by
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const brandingSettings = {
|
const brandingSettings = {
|
||||||
@@ -211,7 +212,8 @@ router.put('/branding', adminAuth, async (req, res) => {
|
|||||||
logo_position,
|
logo_position,
|
||||||
logo_display_header,
|
logo_display_header,
|
||||||
logo_display_hero,
|
logo_display_hero,
|
||||||
logo_display_mode
|
logo_display_mode,
|
||||||
|
hide_powered_by
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle favicon deletion if empty string or null is provided
|
// Handle favicon deletion if empty string or null is provided
|
||||||
|
|||||||
@@ -18,6 +18,29 @@ const DEFAULT_THUMBNAIL_FORMAT = 'jpeg';
|
|||||||
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||||
const getThumbnailPath = () => path.join(getStoragePath(), 'thumbnails');
|
const getThumbnailPath = () => path.join(getStoragePath(), 'thumbnails');
|
||||||
|
|
||||||
|
// Helper to parse setting value (handles both JSON-encoded and plain values)
|
||||||
|
function parseSettingValue(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Try to parse as JSON first (in case it's a JSON-encoded string like '"cover"')
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch (e) {
|
||||||
|
// If it's not valid JSON, return the raw value
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that fit value is valid for Sharp
|
||||||
|
function validateFitValue(fit) {
|
||||||
|
const validFitValues = ['cover', 'contain', 'fill', 'inside', 'outside'];
|
||||||
|
if (fit && validFitValues.includes(fit)) {
|
||||||
|
return fit;
|
||||||
|
}
|
||||||
|
return DEFAULT_THUMBNAIL_FIT;
|
||||||
|
}
|
||||||
|
|
||||||
// Get thumbnail settings from database
|
// Get thumbnail settings from database
|
||||||
async function getThumbnailSettings() {
|
async function getThumbnailSettings() {
|
||||||
try {
|
try {
|
||||||
@@ -30,16 +53,19 @@ async function getThumbnailSettings() {
|
|||||||
'thumbnail_format'
|
'thumbnail_format'
|
||||||
])
|
])
|
||||||
.select('setting_key', 'setting_value');
|
.select('setting_key', 'setting_value');
|
||||||
|
|
||||||
const settingsMap = {};
|
const settingsMap = {};
|
||||||
settings.forEach(s => {
|
settings.forEach(s => {
|
||||||
settingsMap[s.setting_key] = s.setting_value;
|
settingsMap[s.setting_key] = parseSettingValue(s.setting_value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Parse and validate fit value
|
||||||
|
const fitValue = validateFitValue(settingsMap.thumbnail_fit);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width: parseInt(settingsMap.thumbnail_width) || DEFAULT_THUMBNAIL_WIDTH,
|
width: parseInt(settingsMap.thumbnail_width) || DEFAULT_THUMBNAIL_WIDTH,
|
||||||
height: parseInt(settingsMap.thumbnail_height) || DEFAULT_THUMBNAIL_HEIGHT,
|
height: parseInt(settingsMap.thumbnail_height) || DEFAULT_THUMBNAIL_HEIGHT,
|
||||||
fit: settingsMap.thumbnail_fit || DEFAULT_THUMBNAIL_FIT,
|
fit: fitValue,
|
||||||
quality: parseInt(settingsMap.thumbnail_quality) || DEFAULT_THUMBNAIL_QUALITY,
|
quality: parseInt(settingsMap.thumbnail_quality) || DEFAULT_THUMBNAIL_QUALITY,
|
||||||
format: settingsMap.thumbnail_format || DEFAULT_THUMBNAIL_FORMAT
|
format: settingsMap.thumbnail_format || DEFAULT_THUMBNAIL_FORMAT
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -109,8 +109,11 @@ export const AdminPhotoViewer: React.FC<AdminPhotoViewerProps> = ({
|
|||||||
await photosService.updatePhotoCategory(eventId, currentPhoto.id, categoryId);
|
await photosService.updatePhotoCategory(eventId, currentPhoto.id, categoryId);
|
||||||
toast.success('Category updated');
|
toast.success('Category updated');
|
||||||
setShowCategoryMenu(false);
|
setShowCategoryMenu(false);
|
||||||
// Trigger refresh to update the photo data
|
// Invalidate photos query to refresh data
|
||||||
onPhotoDeleted(); // This will refresh the photos list
|
await queryClient.invalidateQueries({ queryKey: ['admin-event-photos', eventId.toString()] });
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['admin-event-photos', eventId] });
|
||||||
|
// Also trigger the parent's refresh callback
|
||||||
|
onPhotoDeleted();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('Failed to update category');
|
toast.error('Failed to update category');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ interface GalleryLayoutProps {
|
|||||||
logo_display_header?: boolean;
|
logo_display_header?: boolean;
|
||||||
logo_display_hero?: boolean;
|
logo_display_hero?: boolean;
|
||||||
logo_display_mode?: 'logo_only' | 'text_only' | 'logo_and_text';
|
logo_display_mode?: 'logo_only' | 'text_only' | 'logo_and_text';
|
||||||
|
hide_powered_by?: boolean;
|
||||||
};
|
};
|
||||||
showLogout?: boolean;
|
showLogout?: boolean;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
@@ -438,7 +439,10 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs sm:text-sm text-neutral-500">
|
<p className="text-xs sm:text-sm text-neutral-500">
|
||||||
{brandingSettings?.footer_text || '© 2024 Your Company. All rights reserved.'} | Powered by <span className="font-semibold">PicPeak</span>
|
{brandingSettings?.footer_text || '© 2024 Your Company. All rights reserved.'}
|
||||||
|
{!brandingSettings?.hide_powered_by && (
|
||||||
|
<> | Powered by <span className="font-semibold">PicPeak</span></>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
{brandingSettings?.company_name && brandingSettings?.company_tagline && (
|
{brandingSettings?.company_name && brandingSettings?.company_tagline && (
|
||||||
<p className="text-xs text-neutral-400 mt-2">
|
<p className="text-xs text-neutral-400 mt-2">
|
||||||
|
|||||||
@@ -165,6 +165,13 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
|||||||
footer_text: settingsData.branding_footer_text || '© 2024 Your Company. All rights reserved.',
|
footer_text: settingsData.branding_footer_text || '© 2024 Your Company. All rights reserved.',
|
||||||
watermark_enabled: settingsData.branding_watermark_enabled || false,
|
watermark_enabled: settingsData.branding_watermark_enabled || false,
|
||||||
logo_url: settingsData.branding_logo_url || null,
|
logo_url: settingsData.branding_logo_url || null,
|
||||||
|
logo_size: settingsData.branding_logo_size || 'medium',
|
||||||
|
logo_max_height: settingsData.branding_logo_max_height || 48,
|
||||||
|
logo_position: settingsData.branding_logo_position || 'left',
|
||||||
|
logo_display_header: settingsData.branding_logo_display_header !== false,
|
||||||
|
logo_display_hero: settingsData.branding_logo_display_hero !== false,
|
||||||
|
logo_display_mode: settingsData.branding_logo_display_mode || 'logo_and_text',
|
||||||
|
hide_powered_by: settingsData.branding_hide_powered_by === true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [settingsData]);
|
}, [settingsData]);
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
|
|||||||
<Download className="w-5 h-5 text-neutral-800" />
|
<Download className="w-5 h-5 text-neutral-800" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{showFeedbackActions && onQuickComment && (
|
{showFeedbackActions && feedbackOptions?.allowComments && onQuickComment && (
|
||||||
<button
|
<button
|
||||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ const MasonryPhoto: React.FC<MasonryPhotoProps> = ({
|
|||||||
<Download className="w-5 h-5 text-neutral-800" />
|
<Download className="w-5 h-5 text-neutral-800" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{onQuickComment && (
|
{feedbackEnabled && feedbackOptions?.allowComments && onQuickComment && (
|
||||||
<button
|
<button
|
||||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||||
onClick={(e) => { e.stopPropagation(); onQuickComment(); }}
|
onClick={(e) => { e.stopPropagation(); onQuickComment(); }}
|
||||||
@@ -127,7 +127,7 @@ const MasonryPhoto: React.FC<MasonryPhotoProps> = ({
|
|||||||
<MessageSquare className="w-5 h-5 text-neutral-800" />
|
<MessageSquare className="w-5 h-5 text-neutral-800" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{feedbackOptions?.allowLikes && (
|
{feedbackEnabled && feedbackOptions?.allowLikes && (
|
||||||
<button
|
<button
|
||||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ export const BrandingPage: React.FC = () => {
|
|||||||
watermark_size: 15,
|
watermark_size: 15,
|
||||||
watermark_logo_url: '',
|
watermark_logo_url: '',
|
||||||
favicon_url: '',
|
favicon_url: '',
|
||||||
|
logo_url: '',
|
||||||
|
logo_size: 'medium',
|
||||||
|
logo_max_height: 48,
|
||||||
|
logo_position: 'left',
|
||||||
|
logo_display_header: true,
|
||||||
|
logo_display_hero: true,
|
||||||
|
logo_display_mode: 'logo_and_text',
|
||||||
|
hide_powered_by: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [currentTheme, setCurrentTheme] = useState<ThemeConfig>(theme);
|
const [currentTheme, setCurrentTheme] = useState<ThemeConfig>(theme);
|
||||||
@@ -508,6 +516,27 @@ export const BrandingPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* White Label Settings */}
|
||||||
|
<div className="mt-6 pt-6 border-t border-neutral-200">
|
||||||
|
<h3 className="text-md font-semibold text-neutral-900 mb-4">{t('branding.whiteLabel', 'White Label')}</h3>
|
||||||
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={brandingSettings.hide_powered_by === true}
|
||||||
|
onChange={(e) => handleBrandingChange('hide_powered_by', e.target.checked)}
|
||||||
|
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-neutral-900">
|
||||||
|
{t('branding.hidePoweredBy', 'Hide "Powered by PicPeak" branding')}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-neutral-600">
|
||||||
|
{t('branding.hidePoweredByHelp', 'Remove the PicPeak attribution from gallery footers for a fully white-labeled experience')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 pt-6 border-t border-neutral-200">
|
<div className="mt-6 pt-6 border-t border-neutral-200">
|
||||||
<label className="flex items-center gap-3 cursor-pointer">
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -1122,8 +1122,9 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
isPreviewMode={false}
|
isPreviewMode={true}
|
||||||
showGalleryLayouts={true}
|
showGalleryLayouts={true}
|
||||||
|
hideActions={true}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface BrandingSettings {
|
|||||||
logo_display_header?: boolean;
|
logo_display_header?: boolean;
|
||||||
logo_display_hero?: boolean;
|
logo_display_hero?: boolean;
|
||||||
logo_display_mode?: 'logo_only' | 'text_only' | 'logo_and_text';
|
logo_display_mode?: 'logo_only' | 'text_only' | 'logo_and_text';
|
||||||
|
hide_powered_by?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ThemeSettings {
|
export interface ThemeSettings {
|
||||||
@@ -256,6 +257,13 @@ export const settingsService = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Helper to parse boolean values that might come as strings or actual booleans
|
||||||
|
_parseBoolean(value: any, defaultValue: boolean): boolean {
|
||||||
|
if (value === true || value === 'true') return true;
|
||||||
|
if (value === false || value === 'false') return false;
|
||||||
|
return defaultValue;
|
||||||
|
},
|
||||||
|
|
||||||
// Format branding settings from raw data
|
// Format branding settings from raw data
|
||||||
formatBrandingSettings(rawSettings: Record<string, any>): BrandingSettings {
|
formatBrandingSettings(rawSettings: Record<string, any>): BrandingSettings {
|
||||||
return {
|
return {
|
||||||
@@ -263,7 +271,7 @@ export const settingsService = {
|
|||||||
company_tagline: rawSettings.branding_company_tagline || '',
|
company_tagline: rawSettings.branding_company_tagline || '',
|
||||||
support_email: rawSettings.branding_support_email || '',
|
support_email: rawSettings.branding_support_email || '',
|
||||||
footer_text: rawSettings.branding_footer_text || '',
|
footer_text: rawSettings.branding_footer_text || '',
|
||||||
watermark_enabled: rawSettings.branding_watermark_enabled || false,
|
watermark_enabled: this._parseBoolean(rawSettings.branding_watermark_enabled, false),
|
||||||
watermark_position: rawSettings.branding_watermark_position || 'bottom-right',
|
watermark_position: rawSettings.branding_watermark_position || 'bottom-right',
|
||||||
watermark_opacity: rawSettings.branding_watermark_opacity || 50,
|
watermark_opacity: rawSettings.branding_watermark_opacity || 50,
|
||||||
watermark_size: rawSettings.branding_watermark_size || 15,
|
watermark_size: rawSettings.branding_watermark_size || 15,
|
||||||
@@ -273,9 +281,10 @@ export const settingsService = {
|
|||||||
logo_size: rawSettings.branding_logo_size || 'medium',
|
logo_size: rawSettings.branding_logo_size || 'medium',
|
||||||
logo_max_height: rawSettings.branding_logo_max_height || 48,
|
logo_max_height: rawSettings.branding_logo_max_height || 48,
|
||||||
logo_position: rawSettings.branding_logo_position || 'left',
|
logo_position: rawSettings.branding_logo_position || 'left',
|
||||||
logo_display_header: rawSettings.branding_logo_display_header !== false,
|
logo_display_header: this._parseBoolean(rawSettings.branding_logo_display_header, true),
|
||||||
logo_display_hero: rawSettings.branding_logo_display_hero !== false,
|
logo_display_hero: this._parseBoolean(rawSettings.branding_logo_display_hero, true),
|
||||||
logo_display_mode: rawSettings.branding_logo_display_mode || 'logo_and_text'
|
logo_display_mode: rawSettings.branding_logo_display_mode || 'logo_and_text',
|
||||||
|
hide_powered_by: this._parseBoolean(rawSettings.branding_hide_powered_by, false)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user