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 { feedbackService } from '../../services/feedback.service';
|
||||||
import { PhotoRating } from './PhotoRating';
|
import { PhotoRating } from './PhotoRating';
|
||||||
import { PhotoLikes } from './PhotoLikes';
|
import { PhotoLikes } from './PhotoLikes';
|
||||||
|
import { PhotoFavorites } from './PhotoFavorites';
|
||||||
import { PhotoComments } from './PhotoComments';
|
import { PhotoComments } from './PhotoComments';
|
||||||
import { Skeleton } from '../common';
|
import { Skeleton } from '../common';
|
||||||
|
|
||||||
@@ -42,13 +43,17 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
|||||||
const [currentRating, setCurrentRating] = useState(0);
|
const [currentRating, setCurrentRating] = useState(0);
|
||||||
const [isLiked, setIsLiked] = useState(false);
|
const [isLiked, setIsLiked] = useState(false);
|
||||||
const [likeCount, setLikeCount] = useState(0);
|
const [likeCount, setLikeCount] = useState(0);
|
||||||
|
const [isFavorited, setIsFavorited] = useState(false);
|
||||||
|
const [favoriteCount, setFavoriteCount] = useState(0);
|
||||||
|
|
||||||
// Update local state when data loads
|
// Update local state when data loads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (feedbackData) {
|
if (feedbackData) {
|
||||||
setCurrentRating(feedbackData.my_feedback.rating || 0);
|
setCurrentRating(feedbackData.my_feedback.rating || 0);
|
||||||
setIsLiked(feedbackData.my_feedback.liked);
|
setIsLiked(Boolean(feedbackData.my_feedback.liked));
|
||||||
setLikeCount(feedbackData.summary.like_count);
|
setLikeCount(Number(feedbackData.summary.like_count) || 0);
|
||||||
|
setIsFavorited(Boolean(feedbackData.my_feedback.favorited));
|
||||||
|
setFavoriteCount(Number(feedbackData.summary.favorite_count) || 0);
|
||||||
}
|
}
|
||||||
}, [feedbackData]);
|
}, [feedbackData]);
|
||||||
|
|
||||||
@@ -64,6 +69,12 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
|||||||
if (onFeedbackUpdate) onFeedbackUpdate();
|
if (onFeedbackUpdate) onFeedbackUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFavoriteChange = (favorited: boolean) => {
|
||||||
|
setIsFavorited(favorited);
|
||||||
|
setFavoriteCount(prev => favorited ? prev + 1 : Math.max(0, prev - 1));
|
||||||
|
if (onFeedbackUpdate) onFeedbackUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
if (settingsLoading) {
|
if (settingsLoading) {
|
||||||
return (
|
return (
|
||||||
<div className={`space-y-3 ${className}`}>
|
<div className={`space-y-3 ${className}`}>
|
||||||
@@ -78,7 +89,7 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasAnyFeedbackType = settings.allow_ratings || settings.allow_likes ||
|
const hasAnyFeedbackType = settings.allow_ratings || settings.allow_likes ||
|
||||||
settings.allow_comments;
|
settings.allow_comments || settings.allow_favorites;
|
||||||
|
|
||||||
if (!hasAnyFeedbackType) {
|
if (!hasAnyFeedbackType) {
|
||||||
return null;
|
return null;
|
||||||
@@ -101,8 +112,8 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
{settings.allow_likes && (
|
{(settings.allow_likes || settings.allow_favorites) && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{settings.allow_likes && (
|
{settings.allow_likes && (
|
||||||
<PhotoLikes
|
<PhotoLikes
|
||||||
photoId={photoId}
|
photoId={photoId}
|
||||||
@@ -114,6 +125,18 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
|||||||
onLikeChange={handleLikeChange}
|
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>
|
</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
|
// Maintenance mode callback
|
||||||
let maintenanceModeCallback: ((enabled: boolean) => void) | null = null;
|
let maintenanceModeCallback: ((enabled: boolean) => void) | null = null;
|
||||||
@@ -23,6 +29,60 @@ api.interceptors.request.use(
|
|||||||
delete config.headers?.['Content-Type'];
|
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;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import type { ReactNode } from 'react';
|
|||||||
import { api } from '../config/api';
|
import { api } from '../config/api';
|
||||||
import { authService, galleryService } from '../services';
|
import { authService, galleryService } from '../services';
|
||||||
import { cleanupOldGalleryAuth } from '../utils/cleanupGalleryAuth';
|
import { cleanupOldGalleryAuth } from '../utils/cleanupGalleryAuth';
|
||||||
|
import {
|
||||||
|
clearActiveGallerySlug,
|
||||||
|
clearGalleryToken,
|
||||||
|
setActiveGallerySlug,
|
||||||
|
storeGalleryToken,
|
||||||
|
} from '../utils/galleryAuthStorage';
|
||||||
|
|
||||||
interface GalleryEvent {
|
interface GalleryEvent {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -55,6 +61,13 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
cleanupOldGalleryAuth();
|
cleanupOldGalleryAuth();
|
||||||
|
|
||||||
|
const slugAtMount = getCurrentGallerySlug();
|
||||||
|
if (slugAtMount) {
|
||||||
|
setActiveGallerySlug(slugAtMount);
|
||||||
|
} else {
|
||||||
|
clearActiveGallerySlug();
|
||||||
|
}
|
||||||
|
|
||||||
const initialise = async () => {
|
const initialise = async () => {
|
||||||
const currentSlug = getCurrentGallerySlug();
|
const currentSlug = getCurrentGallerySlug();
|
||||||
|
|
||||||
@@ -63,6 +76,8 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActiveGallerySlug(currentSlug);
|
||||||
|
|
||||||
const storedEvent = sessionStorage.getItem(`gallery_event_${currentSlug}`);
|
const storedEvent = sessionStorage.getItem(`gallery_event_${currentSlug}`);
|
||||||
if (storedEvent) {
|
if (storedEvent) {
|
||||||
try {
|
try {
|
||||||
@@ -109,6 +124,10 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
|||||||
setEvent(response.event);
|
setEvent(response.event);
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
sessionStorage.setItem(`gallery_event_${currentSlug}`, JSON.stringify(response.event));
|
sessionStorage.setItem(`gallery_event_${currentSlug}`, JSON.stringify(response.event));
|
||||||
|
if (response.token) {
|
||||||
|
storeGalleryToken(currentSlug, response.token);
|
||||||
|
}
|
||||||
|
setActiveGallerySlug(currentSlug);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,16 +137,21 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
|||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||||
setEvent(null);
|
setEvent(null);
|
||||||
|
clearGalleryToken(currentSlug);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||||
setEvent(null);
|
setEvent(null);
|
||||||
|
clearGalleryToken(currentSlug);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
initialise();
|
initialise();
|
||||||
|
return () => {
|
||||||
|
clearActiveGallerySlug();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const login = async (slug: string, password: string, recaptchaToken?: string | null) => {
|
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);
|
const response = await authService.verifyGalleryPassword(slug, password, recaptchaToken);
|
||||||
setEvent(response.event);
|
setEvent(response.event);
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
|
if (response.token) {
|
||||||
|
storeGalleryToken(slug, response.token);
|
||||||
|
}
|
||||||
|
setActiveGallerySlug(slug);
|
||||||
|
|
||||||
// Store event data for quick reloads (non-sensitive)
|
// Store event data for quick reloads (non-sensitive)
|
||||||
sessionStorage.setItem(`gallery_event_${slug}`, JSON.stringify(response.event));
|
sessionStorage.setItem(`gallery_event_${slug}`, JSON.stringify(response.event));
|
||||||
@@ -152,12 +180,13 @@ export const GalleryAuthProvider: React.FC<GalleryAuthProviderProps> = ({ childr
|
|||||||
const currentSlug = getCurrentGallerySlug();
|
const currentSlug = getCurrentGallerySlug();
|
||||||
if (currentSlug) {
|
if (currentSlug) {
|
||||||
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
sessionStorage.removeItem(`gallery_event_${currentSlug}`);
|
||||||
|
clearGalleryToken(currentSlug);
|
||||||
}
|
}
|
||||||
authService.galleryLogout(currentSlug || undefined);
|
authService.galleryLogout(currentSlug || undefined);
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setEvent(null);
|
setEvent(null);
|
||||||
}
|
clearActiveGallerySlug();
|
||||||
;
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GalleryAuthContext.Provider
|
<GalleryAuthContext.Provider
|
||||||
|
|||||||
@@ -226,6 +226,8 @@ export const CreateEventPageEnhanced: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const feedbackSettings = formData.feedback_settings;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
event_type: formData.event_type,
|
event_type: formData.event_type,
|
||||||
event_name: formData.event_name,
|
event_name: formData.event_name,
|
||||||
@@ -239,7 +241,14 @@ export const CreateEventPageEnhanced: React.FC = () => {
|
|||||||
expiration_days: formData.expires_in_days,
|
expiration_days: formData.expires_in_days,
|
||||||
allow_user_uploads: formData.allow_user_uploads,
|
allow_user_uploads: formData.allow_user_uploads,
|
||||||
upload_category_id: formData.upload_category_id,
|
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);
|
createMutation.mutate(payload);
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ interface CreateEventData {
|
|||||||
expiration_days: number;
|
expiration_days: number;
|
||||||
allow_user_uploads?: boolean;
|
allow_user_uploads?: boolean;
|
||||||
upload_category_id?: number | null;
|
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 {
|
interface UpdateEventData {
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ export const cleanupOldGalleryAuth = () => {
|
|||||||
// Also clear session storage
|
// Also clear session storage
|
||||||
sessionStorage.removeItem('gallery_event');
|
sessionStorage.removeItem('gallery_event');
|
||||||
sessionStorage.removeItem('gallery_token');
|
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