feat: add photo cap per event and Portuguese (pt-BR) locale
- Add photo_cap column to events table (migration 074) to limit photos per event - Enforce photo cap in upload route, returning 400 when limit exceeded - Pass photo_cap through all event CRUD routes and frontend forms - Add complete Portuguese (pt-BR) translation (2300+ strings) - Register pt locale in i18n config, language selector, date formatting - Add photoCap/photoCapHelp translation keys to all locale files (en, de, ru, pt)
This commit is contained in:
@@ -8,7 +8,8 @@ import {
|
||||
ArrowLeft,
|
||||
Palette,
|
||||
Eye,
|
||||
EyeOff
|
||||
EyeOff,
|
||||
Image
|
||||
} from 'lucide-react';
|
||||
import { addDays } from 'date-fns';
|
||||
import { toast } from 'react-toastify';
|
||||
@@ -44,6 +45,7 @@ interface FormData {
|
||||
allow_user_uploads: boolean;
|
||||
upload_category_id: number | null;
|
||||
css_template_id: number | null;
|
||||
photo_cap: number;
|
||||
feedback_settings: {
|
||||
feedback_enabled: boolean;
|
||||
allow_ratings: boolean;
|
||||
@@ -98,6 +100,7 @@ export const CreateEventPage: React.FC = () => {
|
||||
allow_user_uploads: false,
|
||||
upload_category_id: null,
|
||||
css_template_id: null,
|
||||
photo_cap: 0,
|
||||
feedback_settings: {
|
||||
feedback_enabled: false,
|
||||
allow_ratings: true,
|
||||
@@ -298,6 +301,7 @@ export const CreateEventPage: React.FC = () => {
|
||||
allow_user_uploads: formData.allow_user_uploads,
|
||||
upload_category_id: formData.upload_category_id,
|
||||
css_template_id: formData.css_template_id,
|
||||
photo_cap: formData.photo_cap > 0 ? formData.photo_cap : null,
|
||||
feedback_enabled: feedbackSettings.feedback_enabled,
|
||||
allow_ratings: feedbackSettings.allow_ratings,
|
||||
allow_likes: feedbackSettings.allow_likes,
|
||||
@@ -717,6 +721,27 @@ export const CreateEventPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Photo Cap */}
|
||||
<div className="pt-4 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
|
||||
{t('events.photoCap', 'Photo Limit')}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-32">
|
||||
<Input
|
||||
type="number"
|
||||
value={formData.photo_cap}
|
||||
onChange={(e) => setFormData({ ...formData, photo_cap: parseInt(e.target.value) || 0 })}
|
||||
min={0}
|
||||
leftIcon={<Image className="w-5 h-5" />}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{t('events.photoCapHelp', 'Maximum number of photos allowed. 0 = unlimited')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Upload Settings */}
|
||||
<div className="pt-4 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<label className="flex items-center gap-3">
|
||||
|
||||
@@ -172,6 +172,8 @@ export const EventDetailsPage: React.FC = () => {
|
||||
hero_logo_position: 'top' | 'center' | 'bottom';
|
||||
// Hero image anchor position (#162) – keyword or "X% Y%" focal point
|
||||
hero_image_anchor: string;
|
||||
// Photo cap
|
||||
photo_cap: number;
|
||||
};
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
@@ -202,6 +204,8 @@ export const EventDetailsPage: React.FC = () => {
|
||||
hero_logo_position: 'top',
|
||||
// Hero image anchor position (#162)
|
||||
hero_image_anchor: 'center',
|
||||
// Photo cap
|
||||
photo_cap: 0,
|
||||
});
|
||||
const [feedbackSettings, setFeedbackSettings] = useState<FeedbackSettingsType>({
|
||||
feedback_enabled: false,
|
||||
@@ -417,6 +421,8 @@ export const EventDetailsPage: React.FC = () => {
|
||||
hero_logo_position: event.hero_logo_position || 'top',
|
||||
// Hero image anchor position (#162)
|
||||
hero_image_anchor: event.hero_image_anchor || 'center',
|
||||
// Photo cap
|
||||
photo_cap: event.photo_cap || 0,
|
||||
});
|
||||
|
||||
setShowNewPassword(false);
|
||||
@@ -550,6 +556,8 @@ export const EventDetailsPage: React.FC = () => {
|
||||
hero_logo_position: editForm.hero_logo_position,
|
||||
// Hero image anchor position (#162)
|
||||
hero_image_anchor: editForm.hero_image_anchor,
|
||||
// Photo cap
|
||||
photo_cap: editForm.photo_cap > 0 ? editForm.photo_cap : null,
|
||||
// Header style settings (decoupled from layout, #158)
|
||||
header_style: currentTheme?.headerStyle || 'standard',
|
||||
hero_divider_style: currentTheme?.heroDividerStyle || 'wave',
|
||||
@@ -1026,6 +1034,25 @@ export const EventDetailsPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Photo Cap */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||
{t('events.photoCap', 'Photo Limit')}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.photo_cap}
|
||||
onChange={(e) => setEditForm(prev => ({ ...prev, photo_cap: parseInt(e.target.value) || 0 }))}
|
||||
min={0}
|
||||
className="w-24 px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
/>
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{t('events.photoCapHelp', 'Maximum number of photos allowed. 0 = unlimited')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user