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:
@@ -46,6 +46,13 @@ export const authService = {
|
||||
return normalizeGalleryResponse(response.data);
|
||||
},
|
||||
|
||||
async clientLogin(slug: string, password: string): Promise<GalleryAuthResponse> {
|
||||
const response = await api.post<GalleryAuthResponse>(`/auth/gallery/${slug}/client-login`, {
|
||||
password
|
||||
});
|
||||
return normalizeGalleryResponse(response.data);
|
||||
},
|
||||
|
||||
async shareLinkLogin(slug: string, token: string): Promise<GalleryAuthResponse> {
|
||||
const response = await api.post<GalleryAuthResponse>('/auth/gallery/share-login', {
|
||||
slug,
|
||||
|
||||
@@ -114,6 +114,16 @@ export const galleryService = {
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
|
||||
// Toggle photo visibility (client-only)
|
||||
async togglePhotoVisibility(slug: string, photoId: number, visibility: 'visible' | 'hidden'): Promise<void> {
|
||||
await api.patch(`/gallery/${slug}/photos/${photoId}/visibility`, { visibility });
|
||||
},
|
||||
|
||||
// Bulk toggle photo visibility (client-only)
|
||||
async bulkToggleVisibility(slug: string, photoIds: number[], visibility: 'visible' | 'hidden'): Promise<void> {
|
||||
await api.patch(`/gallery/${slug}/photos/visibility/bulk`, { photoIds, visibility });
|
||||
},
|
||||
|
||||
// Get gallery statistics
|
||||
async getGalleryStats(slug: string): Promise<GalleryStats> {
|
||||
const response = await api.get<GalleryStats>(`/gallery/${slug}/stats`);
|
||||
|
||||
@@ -73,12 +73,19 @@ class PhotosService {
|
||||
}
|
||||
|
||||
async updatePhotosCategory(eventId: number, photoIds: number[], categoryId: number | null): Promise<void> {
|
||||
await api.post(`/admin/events/${eventId}/photos/bulk-update`, {
|
||||
photoIds,
|
||||
await api.post(`/admin/events/${eventId}/photos/bulk-update`, {
|
||||
photoIds,
|
||||
updates: { category_id: categoryId }
|
||||
});
|
||||
}
|
||||
|
||||
async bulkUpdatePhotos(eventId: number, photoIds: number[], updates: Record<string, unknown>): Promise<void> {
|
||||
await api.post(`/admin/events/${eventId}/photos/bulk-update`, {
|
||||
photoIds,
|
||||
updates
|
||||
});
|
||||
}
|
||||
|
||||
async downloadPhoto(eventId: number, photoId: number, filename: string): Promise<void> {
|
||||
const response = await api.get(`/admin/events/${eventId}/photos/${photoId}/download`, {
|
||||
responseType: 'blob'
|
||||
|
||||
Reference in New Issue
Block a user