feat(frontend): dedupe /public/settings via shared usePublicSettings hook (#325)
Pre-dedup: 7 calls to /api/public/settings on a single /admin/login page load — 4 from raw-fetch consumers + 3 from React Query consumers using inconsistent queryKeys. Captured live in Chrome DevTools. Post-dedup: 1 call. Verified by tests/e2e/public-settings-dedup.spec.ts. Adds: - frontend/src/hooks/usePublicSettings.ts — single React Query hook, 60s staleTime, queryKey ['public-settings']. Vitest with mocked api proves multi-mount dedup. - Extended PublicSettings interface with seo_meta_* fields used by RobotsMetaTags and branding_logo_* fields used by GalleryView/AdminHeader. Migrates 19 call sites across 4 risk-ordered rounds: - Round A (high fan-in): GlobalThemeProvider, MaintenanceContext (with refetchInterval to preserve maintenance polling), MaintenanceWrapper (drops the now-redundant per-route ping; axios interceptor already handles 503), AdminHeader. - Round B (gallery/login): GalleryView, GalleryPage, ClientAccessPage, AdminLoginPage, MaintenanceMode. - Round C (decorative): RobotsMetaTags, DynamicFavicon, CMSContentBlock, ReCaptcha, useWatermarkSettings (rips out raw fetch + local state), LegalPage. - Round D (queryKey alignment): useLocalizedDate, UserPhotoUpload, CreateEventPage. EventDetailsPage Round D ships in the follow-up commit that adds presigned-download UI on the same page. App.tsx (AnalyticsBootstrap) and EventDetailsPage are deferred to later commits — both files mix #325 changes with backend feature work.
This commit is contained in:
@@ -2,12 +2,11 @@ import React, { useState } from 'react';
|
||||
import { useParams, useSearchParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { AlertCircle, Lock } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { Card, CardContent, Input, Button, Loading } from '../components/common';
|
||||
import { useGalleryAuth } from '../contexts';
|
||||
import { useGalleryInfo } from '../hooks/useGallery';
|
||||
import { api } from '../config/api';
|
||||
import { usePublicSettings } from '../hooks/usePublicSettings';
|
||||
import { buildResourceUrl } from '../utils/url';
|
||||
|
||||
export const ClientAccessPage: React.FC = () => {
|
||||
@@ -22,14 +21,7 @@ export const ClientAccessPage: React.FC = () => {
|
||||
|
||||
const { data: galleryInfo, isLoading: isLoadingInfo, error: infoError } = useGalleryInfo(slug);
|
||||
|
||||
const { data: settingsData } = useQuery({
|
||||
queryKey: ['gallery-settings'],
|
||||
queryFn: async () => {
|
||||
const response = await api.get('/public/settings');
|
||||
return response.data;
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
const { data: settingsData } = usePublicSettings();
|
||||
|
||||
// If already authenticated as client, redirect to gallery
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AlertCircle, Clock } from 'lucide-react';
|
||||
import { differenceInDays, parseISO } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocalizedDate } from '../hooks/useLocalizedDate';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { usePublicSettings } from '../hooks/usePublicSettings';
|
||||
|
||||
import { Card, CardContent, Input, Button, ReCaptcha, CMSContentBlock } from '../components/common';
|
||||
import { useGalleryAuth, useTheme } from '../contexts';
|
||||
@@ -13,7 +13,6 @@ import { GalleryView } from '../components/gallery';
|
||||
import { GallerySkeleton } from '../components/gallery/GallerySkeleton';
|
||||
import { analyticsService } from '../services/analytics.service';
|
||||
import { galleryService } from '../services';
|
||||
import { api } from '../config/api';
|
||||
import { GALLERY_THEME_PRESETS } from '../types/theme.types';
|
||||
import { buildResourceUrl } from '../utils/url';
|
||||
import { isGalleryPublic, normalizeRequirePassword } from '../utils/accessControl';
|
||||
@@ -106,15 +105,7 @@ export const GalleryPage: React.FC = () => {
|
||||
setAutoLoginAttempted(false);
|
||||
}, [resolvedSlug]);
|
||||
|
||||
// Fetch branding settings
|
||||
const { data: settingsData, isLoading: isLoadingSettings } = useQuery({
|
||||
queryKey: ['gallery-settings'],
|
||||
queryFn: async () => {
|
||||
const response = await api.get('/public/settings');
|
||||
return response.data;
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
});
|
||||
const { data: settingsData, isLoading: isLoadingSettings } = usePublicSettings();
|
||||
|
||||
// Set language from admin settings when on login page
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -2,12 +2,12 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Navigate, useSearchParams } from 'react-router-dom';
|
||||
import { Lock, Mail, Eye, EyeOff, AlertCircle } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button, Input, Card, ReCaptcha } from '../../components/common';
|
||||
import { useAdminAuth } from '../../contexts';
|
||||
import { authService } from '../../services/auth.service';
|
||||
import { usePublicSettings } from '../../hooks/usePublicSettings';
|
||||
import { api } from '../../config/api';
|
||||
|
||||
export const AdminLoginPage: React.FC = () => {
|
||||
@@ -25,15 +25,7 @@ export const AdminLoginPage: React.FC = () => {
|
||||
const [loginSuccess, setLoginSuccess] = useState(false);
|
||||
const [recaptchaToken, setRecaptchaToken] = useState<string | null>(null);
|
||||
|
||||
// Fetch branding settings (unauthenticated)
|
||||
const { data: settingsData } = useQuery({
|
||||
queryKey: ['admin-login-settings'],
|
||||
queryFn: async () => {
|
||||
const response = await api.get('/public/settings');
|
||||
return response.data;
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
});
|
||||
const { data: settingsData } = usePublicSettings();
|
||||
|
||||
const companyName = settingsData?.branding_company_name?.trim() || 'PicPeak';
|
||||
const logoUrl = settingsData?.branding_logo_url?.trim();
|
||||
|
||||
@@ -22,7 +22,7 @@ import { eventsService } from '../../services/events.service';
|
||||
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
|
||||
import { categoriesService } from '../../services/categories.service';
|
||||
import { settingsService } from '../../services/settings.service';
|
||||
import { publicSettingsService } from '../../services/publicSettings.service';
|
||||
import { usePublicSettings } from '../../hooks/usePublicSettings';
|
||||
import { cssTemplatesService } from '../../services/cssTemplates.service';
|
||||
import { eventTypesService } from '../../services/eventTypes.service';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -170,11 +170,7 @@ export const CreateEventPage: React.FC = () => {
|
||||
queryFn: () => settingsService.getAllSettings()
|
||||
});
|
||||
|
||||
// Fetch public settings for field requirements
|
||||
const { data: publicSettings } = useQuery({
|
||||
queryKey: ['public-settings'],
|
||||
queryFn: () => publicSettingsService.getPublicSettings()
|
||||
});
|
||||
const { data: publicSettings } = usePublicSettings();
|
||||
|
||||
// Get field requirements (default to true if not set)
|
||||
const requireCustomerName = publicSettings?.event_require_customer_name !== false;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ArrowLeft, Home } from 'lucide-react';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { Loading, Card } from '../../components/common';
|
||||
import { cmsService } from '../../services/cms.service';
|
||||
import { api } from '../../config/api';
|
||||
import { usePublicSettings } from '../../hooks/usePublicSettings';
|
||||
import '../../styles/prose-overrides.css';
|
||||
|
||||
export const LegalPage: React.FC = () => {
|
||||
@@ -18,15 +18,7 @@ export const LegalPage: React.FC = () => {
|
||||
const pathname = window.location.pathname;
|
||||
const pageSlug = slug || pathname.split('/').pop() || '';
|
||||
|
||||
// Fetch settings to get default language
|
||||
const { data: settingsData } = useQuery({
|
||||
queryKey: ['public-settings'],
|
||||
queryFn: async () => {
|
||||
const response = await api.get('/public/settings');
|
||||
return response.data;
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
});
|
||||
const { data: settingsData } = usePublicSettings();
|
||||
|
||||
// Use admin settings language
|
||||
const lang = settingsData?.default_language || 'en';
|
||||
|
||||
Reference in New Issue
Block a user