feat: add hero image focal point picker with anchor positioning (#162)

Add interactive focal point picker for hero images, allowing precise
crop positioning via click or preset buttons (top/center/bottom).
Includes backend validation, migrations, and gallery rendering support.
This commit is contained in:
Paul Nothaft
2026-02-03 10:08:58 +01:00
parent f554f463b3
commit 734868abc2
11 changed files with 273 additions and 9 deletions
+32 -1
View File
@@ -52,7 +52,7 @@ import { toast } from 'react-toastify';
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
import { Button, Input, Card, Loading } from '../../components/common';
import { EventCategoryManager, AdminPhotoGrid, AdminPhotoViewer, PhotoFilters, PasswordResetModal, ThemeCustomizerEnhanced, ThemeDisplay, HeroPhotoSelector, PhotoUploadModal, FeedbackSettings, FeedbackModerationPanel, EventRenameDialog, PhotoFilterPanel, PhotoExportMenu } from '../../components/admin';
import { EventCategoryManager, AdminPhotoGrid, AdminPhotoViewer, PhotoFilters, PasswordResetModal, ThemeCustomizerEnhanced, ThemeDisplay, HeroPhotoSelector, FocalPointPicker, PhotoUploadModal, FeedbackSettings, FeedbackModerationPanel, EventRenameDialog, PhotoFilterPanel, PhotoExportMenu } from '../../components/admin';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { eventsService } from '../../services/events.service';
import { api } from '../../config/api';
@@ -168,6 +168,8 @@ export const EventDetailsPage: React.FC = () => {
hero_logo_visible: boolean;
hero_logo_size: 'small' | 'medium' | 'large' | 'xlarge';
hero_logo_position: 'top' | 'center' | 'bottom';
// Hero image anchor position (#162) keyword or "X% Y%" focal point
hero_image_anchor: string;
};
const [isEditing, setIsEditing] = useState(false);
@@ -196,6 +198,8 @@ export const EventDetailsPage: React.FC = () => {
hero_logo_visible: true,
hero_logo_size: 'medium',
hero_logo_position: 'top',
// Hero image anchor position (#162)
hero_image_anchor: 'center',
});
const [feedbackSettings, setFeedbackSettings] = useState<FeedbackSettingsType>({
feedback_enabled: false,
@@ -402,6 +406,8 @@ export const EventDetailsPage: React.FC = () => {
hero_logo_visible: event.hero_logo_visible ?? true,
hero_logo_size: event.hero_logo_size || 'medium',
hero_logo_position: event.hero_logo_position || 'top',
// Hero image anchor position (#162)
hero_image_anchor: event.hero_image_anchor || 'center',
});
setShowNewPassword(false);
@@ -528,6 +534,8 @@ export const EventDetailsPage: React.FC = () => {
hero_logo_visible: editForm.hero_logo_visible,
hero_logo_size: editForm.hero_logo_size,
hero_logo_position: editForm.hero_logo_position,
// Hero image anchor position (#162)
hero_image_anchor: editForm.hero_image_anchor,
};
// Only include fields that have defined values
@@ -859,6 +867,29 @@ export const EventDetailsPage: React.FC = () => {
isEditing={isEditing}
/>
{/* Hero Image Focal Point Picker (#162) */}
{editForm.hero_photo_id && (() => {
const heroPhoto = (photos || []).find((p: any) => p.id === editForm.hero_photo_id);
const heroImageUrl = heroPhoto?.thumbnail_url || heroPhoto?.url;
if (!heroImageUrl) return null;
return (
<div className="ml-6 mt-2">
<label className="block text-sm font-medium text-neutral-700 mb-1">
{t('events.heroImageAnchor', 'Hero Image Crop Position')}
</label>
<p className="text-xs text-neutral-500 mb-2">
{t('events.heroImageAnchorDescription', 'Click on the image to set the focal point for cropping.')}
</p>
<FocalPointPicker
imageUrl={heroImageUrl}
currentValue={editForm.hero_image_anchor}
onChange={(value) => setEditForm(prev => ({ ...prev, hero_image_anchor: value }))}
slug={event.slug}
/>
</div>
);
})()}
<div>
<label className="flex items-start gap-2">
<input