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:
@@ -6,7 +6,7 @@ import DOMPurify from 'dompurify';
|
||||
import { Card } from './Card';
|
||||
import { Loading } from './Loading';
|
||||
import { cmsService } from '../../services/cms.service';
|
||||
import { api } from '../../config/api';
|
||||
import { usePublicSettings } from '../../hooks/usePublicSettings';
|
||||
import { buildResourceUrl } from '../../utils/url';
|
||||
import '../../styles/prose-overrides.css';
|
||||
|
||||
@@ -33,14 +33,7 @@ const ALLOWED_ATTR = ['href', 'target', 'rel', 'class', 'style', 'src', 'alt', '
|
||||
export const CMSContentBlock: React.FC<CMSContentBlockProps> = ({ slug, fallback }) => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ['public-settings'],
|
||||
queryFn: async () => {
|
||||
const response = await api.get('/public/settings');
|
||||
return response.data;
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
const { data: settings } = usePublicSettings();
|
||||
|
||||
const lang = settings?.default_language || i18n.language || 'en';
|
||||
|
||||
|
||||
@@ -1,25 +1,11 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getApiBaseUrl, buildResourceUrl } from '../../utils/url';
|
||||
import { usePublicSettings } from '../../hooks/usePublicSettings';
|
||||
import { buildResourceUrl } from '../../utils/url';
|
||||
|
||||
const DEFAULT_TITLE = 'PicPeak - Photo Sharing Platform';
|
||||
|
||||
export const DynamicFavicon: React.FC = () => {
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ['public-settings'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
const { data: settings } = usePublicSettings({ retry: false });
|
||||
|
||||
// Update favicon when branding settings change
|
||||
useEffect(() => {
|
||||
@@ -82,4 +68,4 @@ export const DynamicFavicon: React.FC = () => {
|
||||
}, [settings?.branding_company_name, settings?.branding_company_tagline]);
|
||||
|
||||
return null;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import ReCAPTCHA from 'react-google-recaptcha';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getApiBaseUrl } from '../../utils/url';
|
||||
import { usePublicSettings } from '../../hooks/usePublicSettings';
|
||||
|
||||
interface ReCaptchaProps {
|
||||
onChange: (token: string | null) => void;
|
||||
@@ -9,31 +8,16 @@ interface ReCaptchaProps {
|
||||
size?: 'normal' | 'compact';
|
||||
}
|
||||
|
||||
export const ReCaptcha: React.FC<ReCaptchaProps> = ({
|
||||
onChange,
|
||||
export const ReCaptcha: React.FC<ReCaptchaProps> = ({
|
||||
onChange,
|
||||
onExpired,
|
||||
size = 'normal'
|
||||
size = 'normal'
|
||||
}) => {
|
||||
const recaptchaRef = React.useRef<ReCAPTCHA>(null);
|
||||
const [siteKey, setSiteKey] = useState<string>('');
|
||||
const { data: settings } = usePublicSettings();
|
||||
|
||||
// Fetch public settings to get reCAPTCHA site key
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ['public-settings'],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||
return response.json();
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||
});
|
||||
const siteKey = settings?.recaptcha_site_key ?? '';
|
||||
|
||||
useEffect(() => {
|
||||
if (settings?.recaptcha_site_key) {
|
||||
setSiteKey(settings.recaptcha_site_key);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
// If reCAPTCHA is not enabled or site key is not available, return null
|
||||
if (!settings?.enable_recaptcha || !siteKey) {
|
||||
return null;
|
||||
}
|
||||
@@ -52,4 +36,4 @@ export const ReCaptcha: React.FC<ReCaptchaProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ReCaptcha;
|
||||
export default ReCaptcha;
|
||||
|
||||
@@ -1,23 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getApiBaseUrl } from '../../utils/url';
|
||||
import { usePublicSettings } from '../../hooks/usePublicSettings';
|
||||
|
||||
export const RobotsMetaTags: React.FC = () => {
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ['public-settings'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await fetch(`${getApiBaseUrl()}/public/settings`);
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
const { data: settings } = usePublicSettings({ retry: false });
|
||||
|
||||
useEffect(() => {
|
||||
// Remove any existing robots meta tags we previously injected
|
||||
|
||||
Reference in New Issue
Block a user