feat: photo visibility control with client access (#172)
Add two-tier gallery access system allowing clients (e.g., wedding couples) to review and hide photos before the gallery is shared with guests. Backend: - Migration 074: add visibility column to photos, client_access_enabled/ client_password_hash/client_share_token to events - Client login endpoint (POST /auth/gallery/:slug/client-login) with bcrypt PIN - Gallery photo list filters hidden photos for guests, shows all for clients - Visibility toggle endpoints (single + bulk) for client access level - Admin event CRUD supports client access fields - Email template includes client access link + PIN (EN/DE/RU/PT) Frontend: - ClientAccessPage: PIN entry form at /gallery/:slug/client-access - GalleryView: client mode banner, visibility counter, toggle controls - GridGalleryLayout: eye/eye-off overlay per photo for clients - AdminPhotoGrid: visibility badge, bulk Hide/Show buttons - EventDetailsPage: Client Access settings section (toggle, PIN, link) - CreateEventPage: client access toggle + PIN in event creation form - GalleryAuthContext: accessLevel/isClient/clientLogin support - New complete pt-BR locale (pt.json) with all translations - Client access i18n keys for EN, DE, RU, PT
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Check, Download, Trash2, Eye, Package, MessageSquare, Star, Video, FolderOpen } from 'lucide-react';
|
||||
import { Check, Download, Trash2, Eye, EyeOff, Package, MessageSquare, Star, Video, FolderOpen } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -201,6 +201,34 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
>
|
||||
{t('photos.moveToCategory', 'Move to Category')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await photosService.bulkUpdatePhotos(eventId, Array.from(selectedPhotos), { visibility: 'hidden' });
|
||||
toast.success(t('admin.photos.hiddenSuccess', 'Photos hidden'));
|
||||
onPhotosDeleted();
|
||||
} catch { toast.error(t('common.error')); }
|
||||
}}
|
||||
leftIcon={<EyeOff className="w-4 h-4" />}
|
||||
>
|
||||
{t('admin.photos.hideSelected', 'Hide')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await photosService.bulkUpdatePhotos(eventId, Array.from(selectedPhotos), { visibility: 'visible' });
|
||||
toast.success(t('admin.photos.visibleSuccess', 'Photos visible'));
|
||||
onPhotosDeleted();
|
||||
} catch { toast.error(t('common.error')); }
|
||||
}}
|
||||
leftIcon={<Eye className="w-4 h-4" />}
|
||||
>
|
||||
{t('admin.photos.showSelected', 'Show')}
|
||||
</Button>
|
||||
<button
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={isDeleting}
|
||||
@@ -260,6 +288,16 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Visibility badge (#172) */}
|
||||
{(photo as any).visibility === 'hidden' && (
|
||||
<div className="absolute top-2 left-2 z-20">
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-red-500/90 text-white text-[10px] font-medium">
|
||||
<EyeOff className="w-3 h-3" />
|
||||
{t('admin.photos.hidden', 'Hidden')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Thumbnail */}
|
||||
<div className="aspect-square">
|
||||
{photo.thumbnail_url ? (
|
||||
|
||||
Reference in New Issue
Block a user