Fix gallery login persistence and favorites (#29)
This commit is contained in:
@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { feedbackService } from '../../services/feedback.service';
|
||||
import { PhotoRating } from './PhotoRating';
|
||||
import { PhotoLikes } from './PhotoLikes';
|
||||
import { PhotoFavorites } from './PhotoFavorites';
|
||||
import { PhotoComments } from './PhotoComments';
|
||||
import { Skeleton } from '../common';
|
||||
|
||||
@@ -42,13 +43,17 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
||||
const [currentRating, setCurrentRating] = useState(0);
|
||||
const [isLiked, setIsLiked] = useState(false);
|
||||
const [likeCount, setLikeCount] = useState(0);
|
||||
const [isFavorited, setIsFavorited] = useState(false);
|
||||
const [favoriteCount, setFavoriteCount] = useState(0);
|
||||
|
||||
// Update local state when data loads
|
||||
useEffect(() => {
|
||||
if (feedbackData) {
|
||||
setCurrentRating(feedbackData.my_feedback.rating || 0);
|
||||
setIsLiked(feedbackData.my_feedback.liked);
|
||||
setLikeCount(feedbackData.summary.like_count);
|
||||
setIsLiked(Boolean(feedbackData.my_feedback.liked));
|
||||
setLikeCount(Number(feedbackData.summary.like_count) || 0);
|
||||
setIsFavorited(Boolean(feedbackData.my_feedback.favorited));
|
||||
setFavoriteCount(Number(feedbackData.summary.favorite_count) || 0);
|
||||
}
|
||||
}, [feedbackData]);
|
||||
|
||||
@@ -64,6 +69,12 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
||||
if (onFeedbackUpdate) onFeedbackUpdate();
|
||||
};
|
||||
|
||||
const handleFavoriteChange = (favorited: boolean) => {
|
||||
setIsFavorited(favorited);
|
||||
setFavoriteCount(prev => favorited ? prev + 1 : Math.max(0, prev - 1));
|
||||
if (onFeedbackUpdate) onFeedbackUpdate();
|
||||
};
|
||||
|
||||
if (settingsLoading) {
|
||||
return (
|
||||
<div className={`space-y-3 ${className}`}>
|
||||
@@ -78,7 +89,7 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
||||
}
|
||||
|
||||
const hasAnyFeedbackType = settings.allow_ratings || settings.allow_likes ||
|
||||
settings.allow_comments;
|
||||
settings.allow_comments || settings.allow_favorites;
|
||||
|
||||
if (!hasAnyFeedbackType) {
|
||||
return null;
|
||||
@@ -101,8 +112,8 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
{settings.allow_likes && (
|
||||
<div className="flex items-center gap-2">
|
||||
{(settings.allow_likes || settings.allow_favorites) && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{settings.allow_likes && (
|
||||
<PhotoLikes
|
||||
photoId={photoId}
|
||||
@@ -114,6 +125,18 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
||||
onLikeChange={handleLikeChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{settings.allow_favorites && (
|
||||
<PhotoFavorites
|
||||
photoId={photoId}
|
||||
gallerySlug={gallerySlug}
|
||||
isFavorited={isFavorited}
|
||||
favoriteCount={favoriteCount}
|
||||
isEnabled={true}
|
||||
requireNameEmail={settings.require_name_email || false}
|
||||
onFavoriteChange={handleFavoriteChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import axios from 'axios';
|
||||
import axios, { AxiosHeaders } from 'axios';
|
||||
import {
|
||||
getActiveGallerySlug,
|
||||
getGalleryToken,
|
||||
inferGallerySlugFromLocation,
|
||||
resolveSlugFromRequestUrl,
|
||||
} from '../utils/galleryAuthStorage';
|
||||
|
||||
// Maintenance mode callback
|
||||
let maintenanceModeCallback: ((enabled: boolean) => void) | null = null;
|
||||
@@ -23,6 +29,60 @@ api.interceptors.request.use(
|
||||
delete config.headers?.['Content-Type'];
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const pathSlug = resolveSlugFromRequestUrl(config.url || '');
|
||||
const params = config.params as Record<string, unknown> | undefined;
|
||||
const paramSlug = typeof params?.slug === 'string' ? (params.slug as string) : null;
|
||||
|
||||
const rawPath = (() => {
|
||||
if (!config.url) return '';
|
||||
try {
|
||||
if (config.url.startsWith('http://') || config.url.startsWith('https://')) {
|
||||
return new URL(config.url).pathname;
|
||||
}
|
||||
} catch (error) {
|
||||
return config.url;
|
||||
}
|
||||
return config.url;
|
||||
})();
|
||||
|
||||
const pathname = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
|
||||
|
||||
const isGalleryEndpoint = /^\/gallery\//.test(pathname)
|
||||
|| /^\/secure-images\//.test(pathname)
|
||||
|| /^\/auth\/gallery\//.test(pathname);
|
||||
|
||||
const isGallerySessionCheck = pathname === '/auth/session'
|
||||
&& (!!paramSlug || window.location.pathname.startsWith('/gallery/'));
|
||||
|
||||
if (isGalleryEndpoint || isGallerySessionCheck) {
|
||||
const fallbackSlug = getActiveGallerySlug()
|
||||
|| inferGallerySlugFromLocation();
|
||||
const slug = pathSlug || paramSlug || fallbackSlug;
|
||||
|
||||
if (slug) {
|
||||
const token = getGalleryToken(slug);
|
||||
if (token) {
|
||||
if (!config.headers) {
|
||||
config.headers = new AxiosHeaders();
|
||||
}
|
||||
|
||||
if (config.headers instanceof AxiosHeaders) {
|
||||
const existing = config.headers.get('Authorization');
|
||||
if (!existing) {
|
||||
config.headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
} else {
|
||||
const headersRecord = config.headers as Record<string, string | undefined>;
|
||||
if (!headersRecord.Authorization) {
|
||||
headersRecord.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
|
||||
@@ -3,6 +3,12 @@ import type { ReactNode } from 'react';
|
||||
import { api } from '../config/api';
|
||||
import { authService, galleryService } from '../services';
|
||||
import { cleanupOldGalleryAuth } from '../utils/cleanupGalleryAuth';
|
||||
import {
|
||||
clearActiveGallerySlug,
|
||||
clearGalleryToken,
|
||||
setActiveGallerySlug,
|
||||
storeGalleryToken,
|
||||
} from '../utils/galleryAuthStorage';
|
||||
|
||||
interface GalleryEvent {
|
||||
id: number;
|
||||
@@ -55,6 +61,13 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
||||
useEffect(() => {
|
||||
cleanupOldGalleryAuth();
|
||||
|
||||
const slugAtMount = getCurrentGallerySlug();
|
||||
if (slugAtMount) {
|
||||
setActiveGallerySlug(slugAtMount);
|
||||
} else {
|
||||
clearActiveGallerySlug();
|
||||
}
|
||||
|
||||
const initialise = async () => {
|
||||
const currentSlug = getCurrentGallerySlug();
|
||||
|
||||
@@ -63,6 +76,8 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveGallerySlug(currentSlug);
|
||||
|
||||
const storedEvent = sessionStorage.getItem(`gallery_event_${currentSlug}`);
|
||||
if (storedEvent) {
|
||||
try {
|
||||
@@ -109,6 +124,10 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
||||
setEvent(response.event);
|
||||
setIsAuthenticated(true);
|
||||
sessionStorage.setItem(`gallery_event_${currentSlug}`, JSON.stringify(response.event));
|
||||
if (response.token) {
|
||||
storeGalleryToken(currentSlug, response.token);
|
||||
}
|
||||
setActiveGallerySlug(currentSlug);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -118,16 +137,21 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
||||
setIsAuthenticated(false);
|
||||
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||
setEvent(null);
|
||||
clearGalleryToken(currentSlug);
|
||||
} catch (error) {
|
||||
setIsAuthenticated(false);
|
||||
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||
setEvent(null);
|
||||
clearGalleryToken(currentSlug);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initialise();
|
||||
return () => {
|
||||
clearActiveGallerySlug();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const login = async (slug: string, password: string, recaptchaToken?: string | null) => {
|
||||
@@ -137,6 +161,10 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
||||
const response = await authService.verifyGalleryPassword(slug, password, recaptchaToken);
|
||||
setEvent(response.event);
|
||||
setIsAuthenticated(true);
|
||||
if (response.token) {
|
||||
storeGalleryToken(slug, response.token);
|
||||
}
|
||||
setActiveGallerySlug(slug);
|
||||
|
||||
// Store event data for quick reloads (non-sensitive)
|
||||
sessionStorage.setItem(`gallery_event_${slug}`, JSON.stringify(response.event));
|
||||
@@ -152,12 +180,13 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
||||
const currentSlug = getCurrentGallerySlug();
|
||||
if (currentSlug) {
|
||||
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||
clearGalleryToken(currentSlug);
|
||||
}
|
||||
authService.galleryLogout(currentSlug || undefined);
|
||||
setIsAuthenticated(false);
|
||||
setEvent(null);
|
||||
}
|
||||
;
|
||||
clearActiveGallerySlug();
|
||||
};
|
||||
|
||||
return (
|
||||
<GalleryAuthContext.Provider
|
||||
|
||||
@@ -226,6 +226,8 @@ export const CreateEventPageEnhanced: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const feedbackSettings = formData.feedback_settings;
|
||||
|
||||
const payload = {
|
||||
event_type: formData.event_type,
|
||||
event_name: formData.event_name,
|
||||
@@ -239,7 +241,14 @@ export const CreateEventPageEnhanced: React.FC = () => {
|
||||
expiration_days: formData.expires_in_days,
|
||||
allow_user_uploads: formData.allow_user_uploads,
|
||||
upload_category_id: formData.upload_category_id,
|
||||
feedback_settings: formData.feedback_settings,
|
||||
feedback_enabled: feedbackSettings.feedback_enabled,
|
||||
allow_ratings: feedbackSettings.allow_ratings,
|
||||
allow_likes: feedbackSettings.allow_likes,
|
||||
allow_comments: feedbackSettings.allow_comments,
|
||||
allow_favorites: feedbackSettings.allow_favorites,
|
||||
require_name_email: feedbackSettings.require_name_email,
|
||||
moderate_comments: feedbackSettings.moderate_comments,
|
||||
show_feedback_to_guests: feedbackSettings.show_feedback_to_guests,
|
||||
};
|
||||
|
||||
createMutation.mutate(payload);
|
||||
|
||||
@@ -13,6 +13,14 @@ interface CreateEventData {
|
||||
expiration_days: number;
|
||||
allow_user_uploads?: boolean;
|
||||
upload_category_id?: number | null;
|
||||
feedback_enabled?: boolean;
|
||||
allow_ratings?: boolean;
|
||||
allow_likes?: boolean;
|
||||
allow_comments?: boolean;
|
||||
allow_favorites?: boolean;
|
||||
require_name_email?: boolean;
|
||||
moderate_comments?: boolean;
|
||||
show_feedback_to_guests?: boolean;
|
||||
}
|
||||
|
||||
interface UpdateEventData {
|
||||
|
||||
@@ -23,4 +23,5 @@ export const cleanupOldGalleryAuth = () => {
|
||||
// Also clear session storage
|
||||
sessionStorage.removeItem('gallery_event');
|
||||
sessionStorage.removeItem('gallery_token');
|
||||
sessionStorage.removeItem('gallery_active_slug');
|
||||
};
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
const TOKEN_STORAGE_PREFIX = 'gallery_token_';
|
||||
const ACTIVE_SLUG_KEY = 'gallery_active_slug';
|
||||
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
const getSessionStorage = (): Storage | null => {
|
||||
if (!isBrowser) return null;
|
||||
try {
|
||||
return window.sessionStorage;
|
||||
} catch (error) {
|
||||
console.warn('Session storage unavailable', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const extractSlugFromPath = (path: string): string | null => {
|
||||
if (!path) return null;
|
||||
const match = path.match(/\/gallery\/([^\/?#]+)/);
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
};
|
||||
|
||||
export const inferGallerySlugFromLocation = (): string | null => {
|
||||
if (!isBrowser) return null;
|
||||
return extractSlugFromPath(window.location.pathname);
|
||||
};
|
||||
|
||||
export const setActiveGallerySlug = (slug: string | null) => {
|
||||
const storage = getSessionStorage();
|
||||
if (!storage) return;
|
||||
if (slug) {
|
||||
storage.setItem(ACTIVE_SLUG_KEY, slug);
|
||||
} else {
|
||||
storage.removeItem(ACTIVE_SLUG_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
export const getActiveGallerySlug = (): string | null => {
|
||||
const storage = getSessionStorage();
|
||||
if (!storage) return null;
|
||||
return storage.getItem(ACTIVE_SLUG_KEY);
|
||||
};
|
||||
|
||||
export const clearActiveGallerySlug = () => {
|
||||
const storage = getSessionStorage();
|
||||
if (!storage) return;
|
||||
storage.removeItem(ACTIVE_SLUG_KEY);
|
||||
};
|
||||
|
||||
export const storeGalleryToken = (slug: string, token: string) => {
|
||||
const storage = getSessionStorage();
|
||||
if (!storage || !slug) return;
|
||||
storage.setItem(`${TOKEN_STORAGE_PREFIX}${slug}`, token);
|
||||
};
|
||||
|
||||
export const getGalleryToken = (slug?: string | null): string | null => {
|
||||
const storage = getSessionStorage();
|
||||
if (!storage) return null;
|
||||
const resolvedSlug = slug || getActiveGallerySlug() || inferGallerySlugFromLocation();
|
||||
if (!resolvedSlug) return null;
|
||||
return storage.getItem(`${TOKEN_STORAGE_PREFIX}${resolvedSlug}`);
|
||||
};
|
||||
|
||||
export const clearGalleryToken = (slug?: string | null) => {
|
||||
const storage = getSessionStorage();
|
||||
if (!storage) return;
|
||||
|
||||
if (slug) {
|
||||
storage.removeItem(`${TOKEN_STORAGE_PREFIX}${slug}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const active = storage.getItem(ACTIVE_SLUG_KEY);
|
||||
if (active) {
|
||||
storage.removeItem(`${TOKEN_STORAGE_PREFIX}${active}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const clearAllGalleryTokens = () => {
|
||||
const storage = getSessionStorage();
|
||||
if (!storage) return;
|
||||
|
||||
const keysToRemove: string[] = [];
|
||||
for (let i = 0; i < storage.length; i += 1) {
|
||||
const key = storage.key(i);
|
||||
if (key && key.startsWith(TOKEN_STORAGE_PREFIX)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach((key) => storage.removeItem(key));
|
||||
};
|
||||
|
||||
export const resolveSlugFromRequestUrl = (url?: string | null): string | null => {
|
||||
if (!url) return null;
|
||||
let pathname = url;
|
||||
|
||||
try {
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
pathname = new URL(url).pathname;
|
||||
}
|
||||
} catch (error) {
|
||||
// Leave pathname as provided if URL parsing fails
|
||||
}
|
||||
|
||||
if (!pathname.startsWith('/')) {
|
||||
pathname = `/${pathname}`;
|
||||
}
|
||||
|
||||
return extractSlugFromPath(pathname);
|
||||
};
|
||||
Reference in New Issue
Block a user