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:
Claude
2025-11-25 19:31:07 +00:00
parent 0745b11745
commit d91ab436e8
13 changed files with 182 additions and 41 deletions
@@ -109,8 +109,11 @@ export const AdminPhotoViewer: React.FC<AdminPhotoViewerProps> = ({
await photosService.updatePhotoCategory(eventId, currentPhoto.id, categoryId);
toast.success('Category updated');
setShowCategoryMenu(false);
// Trigger refresh to update the photo data
onPhotoDeleted(); // This will refresh the photos list
// Invalidate photos query to refresh data
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) {
toast.error('Failed to update category');
}
@@ -29,6 +29,7 @@ interface GalleryLayoutProps {
logo_display_header?: boolean;
logo_display_hero?: boolean;
logo_display_mode?: 'logo_only' | 'text_only' | 'logo_and_text';
hide_powered_by?: boolean;
};
showLogout?: boolean;
onLogout?: () => void;
@@ -438,7 +439,10 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
</p>
)}
<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>
{brandingSettings?.company_name && brandingSettings?.company_tagline && (
<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.',
watermark_enabled: settingsData.branding_watermark_enabled || false,
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]);
@@ -231,7 +231,7 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
<Download className="w-5 h-5 text-neutral-800" />
</button>
)}
{showFeedbackActions && onQuickComment && (
{showFeedbackActions && feedbackOptions?.allowComments && onQuickComment && (
<button
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
onClick={(e) => {
@@ -117,7 +117,7 @@ const MasonryPhoto: React.FC<MasonryPhotoProps> = ({
<Download className="w-5 h-5 text-neutral-800" />
</button>
)}
{onQuickComment && (
{feedbackEnabled && feedbackOptions?.allowComments && onQuickComment && (
<button
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
onClick={(e) => { e.stopPropagation(); onQuickComment(); }}
@@ -127,7 +127,7 @@ const MasonryPhoto: React.FC<MasonryPhotoProps> = ({
<MessageSquare className="w-5 h-5 text-neutral-800" />
</button>
)}
{feedbackOptions?.allowLikes && (
{feedbackEnabled && feedbackOptions?.allowLikes && (
<button
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
onClick={async (e) => {
+29
View File
@@ -23,6 +23,14 @@ export const BrandingPage: React.FC = () => {
watermark_size: 15,
watermark_logo_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);
@@ -508,6 +516,27 @@ export const BrandingPage: React.FC = () => {
</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">
<label className="flex items-center gap-3 cursor-pointer">
<input
@@ -1122,8 +1122,9 @@ export const EventDetailsPage: React.FC = () => {
}
}
}}
isPreviewMode={false}
isPreviewMode={true}
showGalleryLayouts={true}
hideActions={true}
/>
</Card>
)}
+13 -4
View File
@@ -18,6 +18,7 @@ export interface BrandingSettings {
logo_display_header?: boolean;
logo_display_hero?: boolean;
logo_display_mode?: 'logo_only' | 'text_only' | 'logo_and_text';
hide_powered_by?: boolean;
}
export interface ThemeSettings {
@@ -256,6 +257,13 @@ export const settingsService = {
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
formatBrandingSettings(rawSettings: Record<string, any>): BrandingSettings {
return {
@@ -263,7 +271,7 @@ export const settingsService = {
company_tagline: rawSettings.branding_company_tagline || '',
support_email: rawSettings.branding_support_email || '',
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_opacity: rawSettings.branding_watermark_opacity || 50,
watermark_size: rawSettings.branding_watermark_size || 15,
@@ -273,9 +281,10 @@ export const settingsService = {
logo_size: rawSettings.branding_logo_size || 'medium',
logo_max_height: rawSettings.branding_logo_max_height || 48,
logo_position: rawSettings.branding_logo_position || 'left',
logo_display_header: rawSettings.branding_logo_display_header !== false,
logo_display_hero: rawSettings.branding_logo_display_hero !== false,
logo_display_mode: rawSettings.branding_logo_display_mode || 'logo_and_text'
logo_display_header: this._parseBoolean(rawSettings.branding_logo_display_header, true),
logo_display_hero: this._parseBoolean(rawSettings.branding_logo_display_hero, true),
logo_display_mode: rawSettings.branding_logo_display_mode || 'logo_and_text',
hide_powered_by: this._parseBoolean(rawSettings.branding_hide_powered_by, false)
};
},