From a80f734ea0714d7d4033da5d13344ecd66ad8149 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Fri, 12 Dec 2025 17:53:46 +0200 Subject: [PATCH 01/96] WS-1838 adding tracking flag --- ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx b/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx index bab0d21abd1..f226e0f74cb 100644 --- a/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx +++ b/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx @@ -2,10 +2,18 @@ import { use } from 'react'; import Helmet from 'react-helmet'; import { ServiceContext } from '#contexts/ServiceContext'; import ErrorMain from '#app/legacy/components/ErrorMain'; +import useOfflinePageTracker from '#app/hooks/useOfflinePageTracker'; +import useConnectionBackOnlineTracker from '#app/hooks/useConnectionBackOnlineTracker'; const OfflinePage = () => { const { service, dir, script } = use(ServiceContext); + // Track offline page visit (sets flag in localStorage, PWA only) + useOfflinePageTracker(); + + // Track network back online (general) + useConnectionBackOnlineTracker(); + const title = 'You are offline'; const message = "It seems you don't have an internet connection at the moment. Please check your connection and reload the page."; From 8649365544a041b5d50345383bed3182abac9600 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Fri, 12 Dec 2025 17:53:51 +0200 Subject: [PATCH 02/96] WS-1838 adding tracking flag --- .../ATIAnalytics/canonical/index.tsx | 2 + src/app/hooks/useOfflinePageTracker/index.tsx | 27 +++++++ src/app/hooks/usePWAOfflineTracking/index.tsx | 75 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 src/app/hooks/useOfflinePageTracker/index.tsx create mode 100644 src/app/hooks/usePWAOfflineTracking/index.tsx diff --git a/src/app/components/ATIAnalytics/canonical/index.tsx b/src/app/components/ATIAnalytics/canonical/index.tsx index 7173be45855..0338eb0cc3d 100644 --- a/src/app/components/ATIAnalytics/canonical/index.tsx +++ b/src/app/components/ATIAnalytics/canonical/index.tsx @@ -14,6 +14,7 @@ import usePWAInstallTracker from '#app/hooks/usePWAInstallTracker'; import { reverbUrlHelper } from '@bbc/reverb-url-helper'; import useConnectionBackOnlineTracker from '#app/hooks/useConnectionBackOnlineTracker'; import useConnectionTypeTracker from '#app/hooks/useConnectionTypeTracker'; +import usePWAOfflineTracking from '#app/hooks/usePWAOfflineTracking'; import { ATIAnalyticsProps } from '../types'; import getNoScriptTrackingPixelUrl from './getNoScriptTrackingPixelUrl'; import sendPageViewBeaconOperaMini from './sendPageViewBeaconOperaMini'; @@ -55,6 +56,7 @@ const CanonicalATIAnalytics = ({ useConnectionTypeTracker(); useConnectionBackOnlineTracker(); + usePWAOfflineTracking(); const atiPageViewUrlString = getEnvConfig().SIMORGH_ATI_BASE_URL + pageviewParams; diff --git a/src/app/hooks/useOfflinePageTracker/index.tsx b/src/app/hooks/useOfflinePageTracker/index.tsx new file mode 100644 index 00000000000..97ea7915762 --- /dev/null +++ b/src/app/hooks/useOfflinePageTracker/index.tsx @@ -0,0 +1,27 @@ +import { useEffect } from 'react'; +import useIsPWA from '../useIsPWA'; +import useNetworkStatusTracker from '../useNetworkStatusTracker'; + +const OFFLINE_VISIT_FLAG = 'offline_page_visit'; + +/** + * Sets a flag in localStorage when user visits the offline page in PWA mode. + * This flag is checked by useConnectionBackOnlineTracker to send tracking when back online. + * Only tracks when app is running as PWA. + */ +const useOfflinePageTracker = () => { + const isPWA = useIsPWA(); + const { isOnline } = useNetworkStatusTracker(); + useEffect(() => { + if (typeof window === 'undefined' || !isPWA || !isOnline) return; + + try { + localStorage.setItem(OFFLINE_VISIT_FLAG, 'true'); + } catch (error) { + // Silently fail if localStorage is unavailable + } + }, [isOnline, isPWA]); +}; + +export default useOfflinePageTracker; +export { OFFLINE_VISIT_FLAG }; diff --git a/src/app/hooks/usePWAOfflineTracking/index.tsx b/src/app/hooks/usePWAOfflineTracking/index.tsx new file mode 100644 index 00000000000..9481565dddf --- /dev/null +++ b/src/app/hooks/usePWAOfflineTracking/index.tsx @@ -0,0 +1,75 @@ +import { useEffect, useRef } from 'react'; +import useIsPWA from '../useIsPWA'; +import useNetworkStatusTracker from '../useNetworkStatusTracker'; +import useCustomEventTracker from '../useCustomEventTracker'; +import { OFFLINE_VISIT_FLAG } from '../useOfflinePageTracker'; + +const OFFLINE_PAGE_VIEW_EVENT_NAME = 'offline-page-view'; +const DEBOUNCE_DELAY = 10000; + +/** + * Tracks when a user who visited the offline page comes back online (PWA only). + * Works in conjunction with useOfflinePageTracker which sets the flag. + */ +const usePWAOfflineTracking = () => { + const isPWA = useIsPWA(); + const { isOnline, networkType } = useNetworkStatusTracker(); + + const trackOfflinePageViewEvent = useCustomEventTracker({ + eventName: OFFLINE_PAGE_VIEW_EVENT_NAME, + }); + + const prevIsOnlineRef = useRef(true); + const lastEventTimeRef = useRef(0); + + // Notify service worker about PWA mode + useEffect(() => { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready.then(registration => { + if (registration.active) { + registration.active.postMessage({ + type: 'PWA_MODE', + isPWA, + }); + } + }); + } + }, [isPWA]); + + useEffect(() => { + if (!isPWA) return; + + const wasOnline = prevIsOnlineRef.current; + const now = Date.now(); + const timeSinceLastEvent = now - lastEventTimeRef.current; + + // Transitioned from offline to online + if (!wasOnline && isOnline && timeSinceLastEvent >= DEBOUNCE_DELAY) { + // Check if user visited offline page while offline + try { + const offlineVisitFlag = localStorage.getItem(OFFLINE_VISIT_FLAG); + if (offlineVisitFlag === 'true') { + // eslint-disable-next-line no-console + console.warn( + '🎯 PWA Offline Tra cking: User visited offline page, sending beacon...', + ); + lastEventTimeRef.current = now; + trackOfflinePageViewEvent(networkType); + localStorage.removeItem(OFFLINE_VISIT_FLAG); + // eslint-disable-next-line no-console + console.warn('✅ PWA Offline Tracking: Beacon sent, flag cleared'); + } + } catch (error) { + // eslint-disable-next-line no-console + console.warn( + 'PWA Offline Tracking: Error checking offline visit flag', + error, + ); + } + } + + prevIsOnlineRef.current = isOnline; + }, [isPWA, isOnline, networkType, trackOfflinePageViewEvent]); +}; + +export default usePWAOfflineTracking; From 02a40fcc614c91080bc512fc2ce4dd16275cf06d Mon Sep 17 00:00:00 2001 From: skumid01 Date: Tue, 16 Dec 2025 11:28:37 +0200 Subject: [PATCH 03/96] updating due to comments --- src/app/hooks/usePWAOfflineTracking/index.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/app/hooks/usePWAOfflineTracking/index.tsx b/src/app/hooks/usePWAOfflineTracking/index.tsx index 9481565dddf..940b55fc16e 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.tsx @@ -4,8 +4,7 @@ import useNetworkStatusTracker from '../useNetworkStatusTracker'; import useCustomEventTracker from '../useCustomEventTracker'; import { OFFLINE_VISIT_FLAG } from '../useOfflinePageTracker'; -const OFFLINE_PAGE_VIEW_EVENT_NAME = 'offline-page-view'; -const DEBOUNCE_DELAY = 10000; +const OFFLINE_PAGE_VIEW_EVENT_NAME = 'pwa-offline-page-view'; /** * Tracks when a user who visited the offline page comes back online (PWA only). @@ -41,23 +40,16 @@ const usePWAOfflineTracking = () => { const wasOnline = prevIsOnlineRef.current; const now = Date.now(); - const timeSinceLastEvent = now - lastEventTimeRef.current; // Transitioned from offline to online - if (!wasOnline && isOnline && timeSinceLastEvent >= DEBOUNCE_DELAY) { + if (!wasOnline && isOnline) { // Check if user visited offline page while offline try { const offlineVisitFlag = localStorage.getItem(OFFLINE_VISIT_FLAG); if (offlineVisitFlag === 'true') { - // eslint-disable-next-line no-console - console.warn( - '🎯 PWA Offline Tra cking: User visited offline page, sending beacon...', - ); lastEventTimeRef.current = now; trackOfflinePageViewEvent(networkType); localStorage.removeItem(OFFLINE_VISIT_FLAG); - // eslint-disable-next-line no-console - console.warn('✅ PWA Offline Tracking: Beacon sent, flag cleared'); } } catch (error) { // eslint-disable-next-line no-console From 56b06711c20ee1c065f336f872c7eec75bec8bf3 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Tue, 16 Dec 2025 13:37:10 +0200 Subject: [PATCH 04/96] small update for offline hooks --- .../index.tsx | 8 ++++---- src/app/hooks/usePWAOfflineTracking/index.tsx | 18 ++---------------- .../pages/[service]/offline/OfflinePage.tsx | 8 ++------ 3 files changed, 8 insertions(+), 26 deletions(-) rename src/app/hooks/{useOfflinePageTracker => useOfflinePageFlag}/index.tsx (72%) diff --git a/src/app/hooks/useOfflinePageTracker/index.tsx b/src/app/hooks/useOfflinePageFlag/index.tsx similarity index 72% rename from src/app/hooks/useOfflinePageTracker/index.tsx rename to src/app/hooks/useOfflinePageFlag/index.tsx index 97ea7915762..9edd1655fc5 100644 --- a/src/app/hooks/useOfflinePageTracker/index.tsx +++ b/src/app/hooks/useOfflinePageFlag/index.tsx @@ -6,14 +6,14 @@ const OFFLINE_VISIT_FLAG = 'offline_page_visit'; /** * Sets a flag in localStorage when user visits the offline page in PWA mode. - * This flag is checked by useConnectionBackOnlineTracker to send tracking when back online. + * This flag is checked by usePWAOfflineTracking to send tracking when back online. * Only tracks when app is running as PWA. */ -const useOfflinePageTracker = () => { +const useOfflinePageFlag = () => { const isPWA = useIsPWA(); const { isOnline } = useNetworkStatusTracker(); useEffect(() => { - if (typeof window === 'undefined' || !isPWA || !isOnline) return; + if (typeof window === 'undefined' || !isPWA || isOnline) return; try { localStorage.setItem(OFFLINE_VISIT_FLAG, 'true'); @@ -23,5 +23,5 @@ const useOfflinePageTracker = () => { }, [isOnline, isPWA]); }; -export default useOfflinePageTracker; +export default useOfflinePageFlag; export { OFFLINE_VISIT_FLAG }; diff --git a/src/app/hooks/usePWAOfflineTracking/index.tsx b/src/app/hooks/usePWAOfflineTracking/index.tsx index 940b55fc16e..02c1e46ed9c 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.tsx @@ -2,13 +2,13 @@ import { useEffect, useRef } from 'react'; import useIsPWA from '../useIsPWA'; import useNetworkStatusTracker from '../useNetworkStatusTracker'; import useCustomEventTracker from '../useCustomEventTracker'; -import { OFFLINE_VISIT_FLAG } from '../useOfflinePageTracker'; +import { OFFLINE_VISIT_FLAG } from '../useOfflinePageFlag'; const OFFLINE_PAGE_VIEW_EVENT_NAME = 'pwa-offline-page-view'; /** * Tracks when a user who visited the offline page comes back online (PWA only). - * Works in conjunction with useOfflinePageTracker which sets the flag. + * Works in conjunction with useOfflinePageFlag which sets the flag. */ const usePWAOfflineTracking = () => { const isPWA = useIsPWA(); @@ -21,20 +21,6 @@ const usePWAOfflineTracking = () => { const prevIsOnlineRef = useRef(true); const lastEventTimeRef = useRef(0); - // Notify service worker about PWA mode - useEffect(() => { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready.then(registration => { - if (registration.active) { - registration.active.postMessage({ - type: 'PWA_MODE', - isPWA, - }); - } - }); - } - }, [isPWA]); - useEffect(() => { if (!isPWA) return; diff --git a/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx b/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx index f226e0f74cb..70e74014020 100644 --- a/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx +++ b/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx @@ -2,17 +2,13 @@ import { use } from 'react'; import Helmet from 'react-helmet'; import { ServiceContext } from '#contexts/ServiceContext'; import ErrorMain from '#app/legacy/components/ErrorMain'; -import useOfflinePageTracker from '#app/hooks/useOfflinePageTracker'; -import useConnectionBackOnlineTracker from '#app/hooks/useConnectionBackOnlineTracker'; +import useOfflinePageFlag from '#app/hooks/useOfflinePageFlag'; const OfflinePage = () => { const { service, dir, script } = use(ServiceContext); // Track offline page visit (sets flag in localStorage, PWA only) - useOfflinePageTracker(); - - // Track network back online (general) - useConnectionBackOnlineTracker(); + useOfflinePageFlag(); const title = 'You are offline'; const message = From a73f35ffa55b9f9bb888c5f04f824ca7937361bd Mon Sep 17 00:00:00 2001 From: skumid01 Date: Tue, 16 Dec 2025 13:56:19 +0200 Subject: [PATCH 05/96] updating hooks offline tracking --- src/app/hooks/useOfflinePageFlag/index.tsx | 3 +- src/app/hooks/usePWAOfflineTracking/index.tsx | 41 +++++++------------ 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/src/app/hooks/useOfflinePageFlag/index.tsx b/src/app/hooks/useOfflinePageFlag/index.tsx index 9edd1655fc5..a338d77c235 100644 --- a/src/app/hooks/useOfflinePageFlag/index.tsx +++ b/src/app/hooks/useOfflinePageFlag/index.tsx @@ -18,7 +18,8 @@ const useOfflinePageFlag = () => { try { localStorage.setItem(OFFLINE_VISIT_FLAG, 'true'); } catch (error) { - // Silently fail if localStorage is unavailable + // eslint-disable-next-line no-console + console.warn('useOfflinePageFlag', error); } }, [isOnline, isPWA]); }; diff --git a/src/app/hooks/usePWAOfflineTracking/index.tsx b/src/app/hooks/usePWAOfflineTracking/index.tsx index 02c1e46ed9c..f471d283b1f 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import useIsPWA from '../useIsPWA'; import useNetworkStatusTracker from '../useNetworkStatusTracker'; import useCustomEventTracker from '../useCustomEventTracker'; @@ -18,35 +18,22 @@ const usePWAOfflineTracking = () => { eventName: OFFLINE_PAGE_VIEW_EVENT_NAME, }); - const prevIsOnlineRef = useRef(true); - const lastEventTimeRef = useRef(0); - useEffect(() => { - if (!isPWA) return; - - const wasOnline = prevIsOnlineRef.current; - const now = Date.now(); - - // Transitioned from offline to online - if (!wasOnline && isOnline) { - // Check if user visited offline page while offline - try { - const offlineVisitFlag = localStorage.getItem(OFFLINE_VISIT_FLAG); - if (offlineVisitFlag === 'true') { - lastEventTimeRef.current = now; - trackOfflinePageViewEvent(networkType); - localStorage.removeItem(OFFLINE_VISIT_FLAG); - } - } catch (error) { - // eslint-disable-next-line no-console - console.warn( - 'PWA Offline Tracking: Error checking offline visit flag', - error, - ); + if (typeof window === 'undefined' || !isPWA || !isOnline) return; + + try { + const offlineVisitFlag = localStorage.getItem(OFFLINE_VISIT_FLAG); + + if (offlineVisitFlag !== 'true') { + return; } - } - prevIsOnlineRef.current = isOnline; + trackOfflinePageViewEvent(networkType); + localStorage.removeItem(OFFLINE_VISIT_FLAG); + } catch (error) { + // eslint-disable-next-line no-console + console.error('usePWAOfflineTracking', error); + } }, [isPWA, isOnline, networkType, trackOfflinePageViewEvent]); }; From bc8c4c45ac8b3ef489a517d0e621fa69cf2c75bd Mon Sep 17 00:00:00 2001 From: skumid01 Date: Wed, 17 Dec 2025 14:21:43 +0200 Subject: [PATCH 06/96] update offline flag to work properly --- src/app/hooks/usePWAOfflineTracking/index.tsx | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/app/hooks/usePWAOfflineTracking/index.tsx b/src/app/hooks/usePWAOfflineTracking/index.tsx index f471d283b1f..c1f857d3240 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import useIsPWA from '../useIsPWA'; import useNetworkStatusTracker from '../useNetworkStatusTracker'; import useCustomEventTracker from '../useCustomEventTracker'; @@ -7,33 +7,61 @@ import { OFFLINE_VISIT_FLAG } from '../useOfflinePageFlag'; const OFFLINE_PAGE_VIEW_EVENT_NAME = 'pwa-offline-page-view'; /** - * Tracks when a user who visited the offline page comes back online (PWA only). - * Works in conjunction with useOfflinePageFlag which sets the flag. + * Tracks offline→online transitions in PWA mode after user has visited offline page. + * Fires when network comes back online while flag is set. + * Flag is set by useOfflinePageFlag when user visits offline page while offline. */ const usePWAOfflineTracking = () => { const isPWA = useIsPWA(); const { isOnline, networkType } = useNetworkStatusTracker(); + const prevIsOnlineRef = useRef(isOnline); + const hasFiredRef = useRef(false); const trackOfflinePageViewEvent = useCustomEventTracker({ eventName: OFFLINE_PAGE_VIEW_EVENT_NAME, }); useEffect(() => { - if (typeof window === 'undefined' || !isPWA || !isOnline) return; + if (typeof window === 'undefined' || !isPWA) { + prevIsOnlineRef.current = isOnline; + return; + } + + const wasOffline = prevIsOnlineRef.current === false; + const isNowOnline = isOnline === true; + const transitionedToOnline = wasOffline && isNowOnline; + + if (!isOnline) { + hasFiredRef.current = false; + prevIsOnlineRef.current = isOnline; + return; + } + + if (!transitionedToOnline) { + prevIsOnlineRef.current = isOnline; + } try { const offlineVisitFlag = localStorage.getItem(OFFLINE_VISIT_FLAG); if (offlineVisitFlag !== 'true') { + prevIsOnlineRef.current = isOnline; + return; + } + + if (hasFiredRef.current && !transitionedToOnline) { + prevIsOnlineRef.current = isOnline; return; } trackOfflinePageViewEvent(networkType); - localStorage.removeItem(OFFLINE_VISIT_FLAG); + hasFiredRef.current = true; } catch (error) { // eslint-disable-next-line no-console console.error('usePWAOfflineTracking', error); } + + prevIsOnlineRef.current = isOnline; }, [isPWA, isOnline, networkType, trackOfflinePageViewEvent]); }; From e58e68529ccfc14cc19daa6fb4ff3214055588c0 Mon Sep 17 00:00:00 2001 From: jinidev Date: Wed, 17 Dec 2025 15:45:25 +0200 Subject: [PATCH 07/96] removing nextjs static assets to avoid loop reloadand some other fixes --- public/sw.js | 307 ++++++++++++++++++++++++++----------------------- src/sw.test.js | 2 +- 2 files changed, 161 insertions(+), 148 deletions(-) diff --git a/public/sw.js b/public/sw.js index 5078288bf1c..4f26a6ecbf5 100644 --- a/public/sw.js +++ b/public/sw.js @@ -8,80 +8,73 @@ const version = 'v0.3.1'; const cacheName = 'simorghCache_v1'; -const getServiceFromUrl = url => new URL(url).pathname.split('/')[1]; -const getOfflinePagePath = serviceName => `/${serviceName}/offline`; -const getOfflinePageUrl = serviceName => - new URL(getOfflinePagePath(serviceName), self.location.origin).href; +// Track PWA clients +const pwaClients = new Map(); -const cacheOfflinePageAndResources = async serviceName => { - if (!serviceName) return; +console.log(`[SW v${version}] Service Worker loaded.`); - const cache = await caches.open(cacheName); - const offlinePageUrl = getOfflinePageUrl(serviceName); - - const response = await fetch(offlinePageUrl); - if (!response?.ok) return; - console.log(`[SW v${version}] Caching offline page for ${serviceName}`); - - await cache.put(offlinePageUrl, response.clone()); - - const html = await response.text(); - const scriptMatches = html.matchAll(/]+src=["']([^"']+)["']/gi); - const linkMatches = html.matchAll(/]+href=["']([^"']+)["'][^>]*>/gi); - - const scriptUrls = Array.from(scriptMatches) - .map(match => match[1]) - .filter(src => src && !src.startsWith('http')) - .map(src => new URL(src, self.location.origin).href); - - const linkUrls = Array.from(linkMatches) - .map(match => match[1]) - .filter( - href => - href && - !href.startsWith('http') && - (href.endsWith('.css') || href.includes('stylesheet')), - ) - .map(href => new URL(href, self.location.origin).href); - - const resourcesToCache = [...scriptUrls, ...linkUrls]; - - await Promise.all( - resourcesToCache.map(async url => { - try { - const res = await fetch(url); - if (res.ok) { - await cache.put(url, res); - } - } catch (error) { - // Ignore failed resource - } - }), - ); +// -------------------- +// Helper Functions +// -------------------- +const getServiceFromUrl = url => new URL(url).pathname.split('/')[1]; +const getOfflinePageUrl = service => `/${service}/offline`; + +const cacheResource = async (cache, url) => { + try { + const response = await fetch(url); + if (response.ok) await cache.put(url, response.clone()); + return response; + } catch (err) { + console.error(`[SW v${version}] Failed to cache ${url}:`, err); + return null; + } }; -// Track which clients are in PWA mode -const pwaClients = new Set(); - -// -------Message Event------------- -// Listen for messages from clients about their display mode -self.addEventListener('message', event => { - console.log(`[SW v${version}] Message received:`, event.data); +const cacheOfflinePageAndResources = async service => { + const cache = await caches.open(cacheName); + const offlinePageUrl = new URL( + getOfflinePageUrl(service), + self.location.origin, + ).href; + if (await cache.match(offlinePageUrl)) return; + + const resp = await cacheResource(cache, offlinePageUrl); + if (!resp || !resp.ok) return; + + console.log(`[SW v${version}] Cached offline page for ${service}`); + + const html = await resp.text(); + const scriptSrcs = [ + ...html.matchAll(/]+src=["']([^"']+)["']/g), + ].map(m => m[1]); + const linkHrefs = [...html.matchAll(/]+href=["']([^"']+)["']/g)].map( + m => m[1], + ); + const resources = [...scriptSrcs, ...linkHrefs] + .filter(url => url.startsWith('/') || url.startsWith(self.location.origin)) + .map(url => new URL(url, self.location.origin).href); - if (event.data && event.data.type === 'PWA_STATUS') { - const clientId = event.source?.id; - if (!clientId) return; + await Promise.allSettled(resources.map(url => cacheResource(cache, url))); +}; - if (event.data.isPWA) { - pwaClients.add(clientId); +// Cache patterns +const CACHEABLE_FILES = [ + // Reverb + /^https:\/\/static(?:\.test)?\.files\.bbci\.co\.uk\/ws\/(?:simorgh-assets|simorgh1-preview-assets|simorgh2-preview-assets)\/public\/static\/js\/reverb\/reverb-3.10.2.js$/, + // Smart Tag + 'https://mybbc-analytics.files.bbci.co.uk/reverb-client-js/smarttag-5.29.4.min.js', + // Fonts + /\.woff2$/, + // Frosted Promo (test and live environments only) + /^https:\/\/static(\.test)?\.files\.bbci\.co\.uk\/ws\/simorgh-assets\/public\/static\/js\/modern\.frosted_promo+.*?\.js$/, + // Moment + /\/moment-lib+.*?\.js$/, + // PWA Icons + /\/images\/icons\/icon-.*?\.png\??v?=?\d*$/, +]; - const serviceName = getServiceFromUrl(event.source?.url); - cacheOfflinePageAndResources(serviceName).catch(() => null); - } else { - pwaClients.delete(clientId); - } - } -}); +const WEBP_IMAGE = + /^https:\/\/ichef(\.test)?\.bbci\.co\.uk\/(news|images|ace\/(standard|ws))\/.+.webp$/; // -------------Install event ------- self.addEventListener('install', event => { @@ -89,6 +82,7 @@ self.addEventListener('install', event => { event.waitUntil( (async () => { + const cache = await caches.open(cacheName); const clients = await self.clients.matchAll({ type: 'window' }); // Get unique services from PWA clients only @@ -101,6 +95,13 @@ self.addEventListener('install', event => { ), ]; + if (pwaServices.length > 0) { + console.log( + `[SW v${version}] Caching offline pages for PWA:`, + pwaServices, + ); + } + // Cache offline pages for PWA services only await Promise.allSettled( pwaServices.map(async service => { @@ -117,49 +118,42 @@ self.addEventListener('activate', event => { console.log(`[SW v${version}] Activating...`); event.waitUntil( (async () => { - // Delete old caches - const cacheNames = await caches.keys(); + const keys = await caches.keys(); await Promise.all( - cacheNames - .filter(name => name !== cacheName) - .map(name => caches.delete(name)), + keys.map(key => key !== cacheName && caches.delete(key)), ); await self.clients.claim(); })(), ); }); -const CACHEABLE_FILES = [ - // Reverb - /^https:\/\/static(?:\.test)?\.files\.bbci\.co\.uk\/ws\/(?:simorgh-assets|simorgh1-preview-assets|simorgh2-preview-assets)\/public\/static\/js\/reverb\/reverb-3.10.2.js$/, - // Smart Tag - 'https://mybbc-analytics.files.bbci.co.uk/reverb-client-js/smarttag-5.29.4.min.js', - // Fonts - /\.woff2$/, - // Frosted Promo (test and live environments only) - /^https:\/\/static(\.test)?\.files\.bbci\.co\.uk\/ws\/simorgh-assets\/public\/static\/js\/modern\.frosted_promo+.*?\.js$/, - // Moment - /\/moment-lib+.*?\.js$/, - // PWA Icons - /\/images\/icons\/icon-.*?\.png\??v?=?\d*$/, - // Next.js static assets (JS chunks, CSS, fonts) - /\/_next\/static\/.+\.js$/, - /\/_next\/static\/.+\.css$/, -]; - -const WEBP_IMAGE = - /^https:\/\/ichef(\.test)?\.bbci\.co\.uk\/(news|images|ace\/(standard|ws))\/.+.webp$/; +// -------Message Event------------- +self.addEventListener('message', async event => { + console.log(`[SW v${version}] Message received:`, event.data); -// -------Fetch Handler------------- + if (event.data?.type === 'PWA_STATUS') { + const clientId = event.source.id; + const { isPWA } = event.data; + pwaClients.set(clientId, isPWA); -const fetchEventHandler = async event => { - const isRequestForCacheableFile = CACHEABLE_FILES.some(cacheableFile => { - if (cacheableFile instanceof RegExp) { - return cacheableFile.test(event.request.url); + if (isPWA) { + const cache = await caches.open(cacheName); + await cache.put('pwa_installed', new Response('true')); + const service = getServiceFromUrl(event.source.url); + await cacheOfflinePageAndResources(service); + } else { + const cache = await caches.open(cacheName); + await cache.delete('pwa_installed'); } + } +}); - return event.request.url === cacheableFile; - }); +// -------Fetch Handler------------- +const fetchEventHandler = async event => { + console.log(`[SW FETCH] Request: ${event.request.url}`); + const isRequestForCacheableFile = CACHEABLE_FILES.some(cacheableFile => + new RegExp(cacheableFile).test(event.request.url), + ); const isRequestForWebpImage = WEBP_IMAGE.test(event.request.url); @@ -188,61 +182,80 @@ const fetchEventHandler = async event => { const cache = await caches.open(cacheName); let response = await cache.match(event.request); if (!response) { - try { - response = await fetch(event.request.url); - cache.put(event.request, response.clone()); - } catch (error) { - // File not in cache and network unavailable - return new Response('', { - status: 408, - statusText: - 'You are offline . Please check your network and reload the page', - }); - } + response = await fetch(event.request.url); + cache.put(event.request, response.clone()); } return response; })(), ); } else if (event.request.mode === 'navigate') { - const clientId = event.clientId || event.resultingClientId; - const isInPWAMode = clientId && pwaClients.has(clientId); - console.log( - '[SW] Fetch event for navigation. isInPWAMode:', - isInPWAMode, - clientId, - ); - // Only intercept navigation for PWA clients to avoid loop in browser mode when offline - if (isInPWAMode) { - event.respondWith( - (async () => { - try { - const preloadResponse = await event.preloadResponse; - if (preloadResponse) return preloadResponse; - return await fetch(event.request); - } catch (error) { - console.log('[SW] Navigation failed:', event.request.url, error); - const cache = await caches.open(cacheName); - const serviceName = getServiceFromUrl(event.request.url); - const offlinePageUrl = getOfflinePageUrl(serviceName); - const cachedResponse = await cache.match(offlinePageUrl); - - return ( - cachedResponse || - new Response( - 'You are offline. Please check your network and reload the page', - { - status: 503, - headers: { 'Content-Type': 'text/plain' }, - }, - ) - ); - } - })(), - ); + const { url } = event.request; + const client = await self.clients.get(event.clientId); + const isPWA = client && pwaClients.get(client.id); + console.log(`[SW FETCH] Navigation: ${url} , isPWA: ${isPWA}`); + + if (!isPWA && cache.has('pwa_installed')) { + await cache.delete('pwa_installed'); } + + event.respondWith( + (async () => { + try { + // Use preload if available + const preloadResp = await event.preloadResponse; + if (preloadResp) return preloadResp; + + const networkResp = await fetch(event.request); + + // Cache offline page if in PWA mode + if (networkResp && networkResp.ok && event.clientId) { + console.log('[SW] Caching offline page if PWA if network is ok'); + // const client = await self.clients.get(event.clientId); + // const isPWA = client && pwaClients.get(client.id); + if (isPWA) { + const service = getServiceFromUrl(url); + cacheOfflinePageAndResources(service).catch(err => + console.error('[SW] Cache offline fail:', err), + ); + } + } + + return networkResp; + } catch (err) { + console.log('[SW] Navigation failed:', url, err); + + const cache = await caches.open(cacheName); + const pwaMarker = await cache.match('pwa_installed'); + console.log('[SW] PWA Marker:', pwaMarker); + + // Only show offline page for installed PWA + if (pwaMarker) { + const service = getServiceFromUrl(url); + const offlineUrl = new URL( + getOfflinePageUrl(service), + self.location.origin, + ).href; + + const cachedOffline = await cache.match(offlineUrl); + if (cachedOffline) { + return cachedOffline; + } + } + + // Canonical site offline fallback + return new Response( + 'You are offline. Please check your network and reload the page', + { + status: 503, + headers: { 'Content-Type': 'text/plain' }, + }, + ); + } + })(), + ); } - // For all other requests, let the browser handle it normally + return; }; -self.addEventListener('fetch', fetchEventHandler); +onfetch = fetchEventHandler; diff --git a/src/sw.test.js b/src/sw.test.js index e2ca293a2fb..5569ffb6c97 100644 --- a/src/sw.test.js +++ b/src/sw.test.js @@ -288,7 +288,7 @@ describe('Service Worker', () => { describe('version', () => { const CURRENT_VERSION = { number: 'v0.3.1', - fileContentHash: '4735c3bc2b27f6d1f364a0f775556e62', + fileContentHash: '281869e4927afed159d55ed3cbd4a7c6', }; it(`version number should be ${CURRENT_VERSION.number}`, async () => { From d5746f24806c600d41a4ae1cf3b227f718fb239e Mon Sep 17 00:00:00 2001 From: Santa Zena Date: Fri, 12 Dec 2025 13:51:43 +0200 Subject: [PATCH 08/96] WS-1826-collapsible-nav-for-seo --- src/app/components/CollapsibleNavigation/index.styles.ts | 3 +++ src/app/components/CollapsibleNavigation/index.tsx | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/app/components/CollapsibleNavigation/index.styles.ts b/src/app/components/CollapsibleNavigation/index.styles.ts index ade3828d305..f27ee78b729 100644 --- a/src/app/components/CollapsibleNavigation/index.styles.ts +++ b/src/app/components/CollapsibleNavigation/index.styles.ts @@ -222,6 +222,9 @@ const styles = { backgroundColor: palette.WHITE, }, }), + collapsed: css({ + display: 'none', + }), }; export default styles; diff --git a/src/app/components/CollapsibleNavigation/index.tsx b/src/app/components/CollapsibleNavigation/index.tsx index c201afe5f94..6a23ab140d0 100644 --- a/src/app/components/CollapsibleNavigation/index.tsx +++ b/src/app/components/CollapsibleNavigation/index.tsx @@ -86,7 +86,6 @@ const CollapsibleNavigation = ({
    {navigationSections.map(section => { const isActive = Boolean(openSection === section.id); - const shouldShowSubNav = isHydrated ? isActive : true; const isLink = section.href; const navigationLinkId = `nav-${section.id}`; @@ -113,10 +112,14 @@ const CollapsibleNavigation = ({ - {section.links && shouldShowSubNav && ( + {section.links && (
  • Date: Fri, 12 Dec 2025 15:15:56 +0200 Subject: [PATCH 09/96] Updating unit tests --- .../CollapsibleNavigation/index.test.tsx | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/app/components/CollapsibleNavigation/index.test.tsx b/src/app/components/CollapsibleNavigation/index.test.tsx index cd17d13238d..116be4f4780 100644 --- a/src/app/components/CollapsibleNavigation/index.test.tsx +++ b/src/app/components/CollapsibleNavigation/index.test.tsx @@ -32,14 +32,16 @@ describe('LanguageNavigation', () => { render(); sections.forEach(section => { - expect(screen.getByText(section.title)).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: section.title }), + ).toBeInTheDocument(); }); }); test('clicking a section toggles dropdown', () => { render(); - const sectionTitle = screen.getByText('Section 1'); + const sectionTitle = screen.getByRole('button', { name: 'Section 1' }); fireEvent.click(sectionTitle); @@ -51,23 +53,23 @@ describe('LanguageNavigation', () => { test('clicking the same section again closes the dropdown', () => { render(); - const sectionTitle = screen.getByText('Section 1'); + const sectionTitle = screen.getByRole('button', { name: 'Section 1' }); fireEvent.click(sectionTitle); - expect(screen.getByText('Link 1')).toBeInTheDocument(); - expect(screen.getByText('Link 2')).toBeInTheDocument(); + expect(screen.getByText('Link 1')).toBeVisible(); + expect(screen.getByText('Link 2')).toBeVisible(); fireEvent.click(sectionTitle); - expect(screen.queryByText('Link 1')).not.toBeInTheDocument(); - expect(screen.queryByText('Link 2')).not.toBeInTheDocument(); + expect(screen.getByText('Link 1')).not.toBeVisible(); + expect(screen.getByText('Link 2')).not.toBeVisible(); }); test('clicking close button closes the dropdown', () => { render(); - const sectionTitle = screen.getByText('Section 1'); + const sectionTitle = screen.getByRole('button', { name: 'Section 1' }); fireEvent.click(sectionTitle); @@ -79,15 +81,15 @@ describe('LanguageNavigation', () => { }); fireEvent.click(closeButton); - expect(closeButton).not.toBeInTheDocument(); - expect(screen.queryByText('Link 1')).not.toBeInTheDocument(); - expect(screen.queryByText('Link 2')).not.toBeInTheDocument(); + expect(closeButton).not.toBeVisible(); + expect(screen.queryByText('Link 1')).not.toBeVisible(); + expect(screen.queryByText('Link 2')).not.toBeVisible(); }); test('renders links correctly when section is active', () => { render(); - const sectionTitle = screen.getByText('Section 2'); + const sectionTitle = screen.getByRole('button', { name: 'Section 2' }); fireEvent.click(sectionTitle); @@ -105,13 +107,13 @@ describe('LanguageNavigation', () => { fireEvent.click(sectionLink); - expect(screen.queryByText('Link 1')).not.toBeInTheDocument(); + expect(screen.queryByText('Link 1')).not.toBeVisible(); }); test('applies lang attribute to links when provided', () => { render(); - const sectionTitle = screen.getByText('Section 1'); + const sectionTitle = screen.getByRole('button', { name: 'Section 1' }); fireEvent.click(sectionTitle); const link1 = screen.getByRole('link', { name: 'Link 1' }); @@ -151,7 +153,7 @@ describe('LanguageNavigation', () => { />, ); - const sectionTitle = screen.getByText('Section 1'); + const sectionTitle = screen.getByRole('button', { name: 'Section 1' }); fireEvent.click(sectionTitle); const link1 = screen.getByRole('link', { @@ -184,7 +186,7 @@ describe('LanguageNavigation', () => { , ); - const sectionTitle = screen.getByText('Section 1'); + const sectionTitle = screen.getByRole('button', { name: 'Section 1' }); fireEvent.click(sectionTitle); const link1 = screen.getByRole('link', { @@ -213,7 +215,7 @@ describe('LanguageNavigation', () => { render(); - const sectionTitle = screen.getByText('Section 1'); + const sectionTitle = screen.getByRole('button', { name: 'Section 1' }); fireEvent.click(sectionTitle); const link1 = screen.getByRole('link', { name: 'Link 1' }); @@ -244,7 +246,7 @@ describe('LanguageNavigation', () => { />, ); - const sectionTitle = screen.getByText('Section 1'); + const sectionTitle = screen.getByRole('button', { name: 'Section 1' }); fireEvent.click(sectionTitle); const link1 = screen.getByRole('link', { name: 'Link 1' }); @@ -272,7 +274,7 @@ describe('LanguageNavigation', () => { render(); - const sectionTitle = screen.getByText('Section 1'); + const sectionTitle = screen.getByRole('button', { name: 'Section 1' }); fireEvent.click(sectionTitle); const link1 = screen.getByRole('link', { name: 'Link 1' }); From 84d8a040662dc57144cc67c200d85e23cd524409 Mon Sep 17 00:00:00 2001 From: Santa Zena Date: Mon, 15 Dec 2025 11:15:34 +0200 Subject: [PATCH 10/96] Updating unit tests nr2 --- .../CollapsibleNavigation/index.test.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/components/CollapsibleNavigation/index.test.tsx b/src/app/components/CollapsibleNavigation/index.test.tsx index 116be4f4780..1696a040477 100644 --- a/src/app/components/CollapsibleNavigation/index.test.tsx +++ b/src/app/components/CollapsibleNavigation/index.test.tsx @@ -33,7 +33,7 @@ describe('LanguageNavigation', () => { sections.forEach(section => { expect( - screen.getByRole('button', { name: section.title }), + screen.getAllByRole('button', { name: section.title }[0]), ).toBeInTheDocument(); }); }); @@ -46,7 +46,7 @@ describe('LanguageNavigation', () => { fireEvent.click(sectionTitle); sections[0].links?.forEach(link => { - expect(screen.getByText(link.label)).toBeInTheDocument(); + expect(screen.getAllByText(link.label)[0]).toBeInTheDocument(); }); }); @@ -57,13 +57,13 @@ describe('LanguageNavigation', () => { fireEvent.click(sectionTitle); - expect(screen.getByText('Link 1')).toBeVisible(); - expect(screen.getByText('Link 2')).toBeVisible(); + expect(screen.getAllByText('Link 1')[0]).toBeVisible(); + expect(screen.getAllByText('Link 2')[0]).toBeVisible(); fireEvent.click(sectionTitle); - expect(screen.getByText('Link 1')).not.toBeVisible(); - expect(screen.getByText('Link 2')).not.toBeVisible(); + expect(screen.getAllByText('Link 1')[0]).not.toBeVisible(); + expect(screen.getAllByText('Link 2')[0]).not.toBeVisible(); }); test('clicking close button closes the dropdown', () => { @@ -73,8 +73,8 @@ describe('LanguageNavigation', () => { fireEvent.click(sectionTitle); - expect(screen.getByText('Link 1')).toBeInTheDocument(); - expect(screen.getByText('Link 2')).toBeInTheDocument(); + expect(screen.getAllByText('Link 1')[0]).toBeInTheDocument(); + expect(screen.getAllByText('Link 2')[0]).toBeInTheDocument(); const closeButton = screen.getByRole('button', { name: 'Close Section 1 submenu', @@ -82,8 +82,8 @@ describe('LanguageNavigation', () => { fireEvent.click(closeButton); expect(closeButton).not.toBeVisible(); - expect(screen.queryByText('Link 1')).not.toBeVisible(); - expect(screen.queryByText('Link 2')).not.toBeVisible(); + expect(screen.queryAllByText('Link 1')[0]).not.toBeVisible(); + expect(screen.queryAllByText('Link 2')[0]).not.toBeVisible(); }); test('renders links correctly when section is active', () => { @@ -94,7 +94,7 @@ describe('LanguageNavigation', () => { fireEvent.click(sectionTitle); sections[1].links?.forEach(link => { - expect(screen.getByText(link.label)).toBeInTheDocument(); + expect(screen.getAllByText(link.label)[0]).toBeInTheDocument(); }); }); @@ -107,7 +107,7 @@ describe('LanguageNavigation', () => { fireEvent.click(sectionLink); - expect(screen.queryByText('Link 1')).not.toBeVisible(); + expect(screen.queryAllByText('Link 1')[0]).not.toBeVisible(); }); test('applies lang attribute to links when provided', () => { From 05f499b9f78beda7647bde41e1637813d8f9e928 Mon Sep 17 00:00:00 2001 From: Santa Zena Date: Mon, 15 Dec 2025 12:26:31 +0200 Subject: [PATCH 11/96] Updating unit tests nr3 --- src/app/components/CollapsibleNavigation/index.test.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/components/CollapsibleNavigation/index.test.tsx b/src/app/components/CollapsibleNavigation/index.test.tsx index 1696a040477..68dd9145697 100644 --- a/src/app/components/CollapsibleNavigation/index.test.tsx +++ b/src/app/components/CollapsibleNavigation/index.test.tsx @@ -32,9 +32,10 @@ describe('LanguageNavigation', () => { render(); sections.forEach(section => { - expect( - screen.getAllByRole('button', { name: section.title }[0]), - ).toBeInTheDocument(); + const element = section.href + ? screen.getByRole('link', { name: section.title }) + : screen.getByRole('button', { name: section.title }); + expect(element).toBeInTheDocument(); }); }); From 0d2c3458b96fd3c65b97a3d0288a59509e23c003 Mon Sep 17 00:00:00 2001 From: Santa Zena Date: Wed, 17 Dec 2025 09:18:35 +0200 Subject: [PATCH 12/96] Removing right line in top nav --- src/app/components/CollapsibleNavigation/index.styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/CollapsibleNavigation/index.styles.ts b/src/app/components/CollapsibleNavigation/index.styles.ts index f27ee78b729..dd0e6f40ce1 100644 --- a/src/app/components/CollapsibleNavigation/index.styles.ts +++ b/src/app/components/CollapsibleNavigation/index.styles.ts @@ -47,7 +47,7 @@ const styles = { height: `${pixelsToRem(20)}rem`, backgroundColor: palette.GREY_10, }, - '&:last-of-type::after': { + '&:nth-last-child(2)::after': { background: 'none', }, }), From 574424b747d180c59de1627bf43352454c5eee0a Mon Sep 17 00:00:00 2001 From: skumid01 Date: Thu, 18 Dec 2025 16:24:44 +0200 Subject: [PATCH 13/96] Fix onload condition for promo images & add blurred bg --- data/mundo/topics/c7zp57yyz25t.json | 70 ++++ data/persian/topics/c6z7mnr559gt.json | 315 ++++++++++++++++++ data/ws/homePage/index.json | 15 + .../Curation/CurationPromo/index.tsx | 2 + src/app/components/Image/index.tsx | 16 +- src/app/legacy/components/Promo/image.jsx | 23 ++ src/app/models/types/curationData.ts | 1 + src/app/pages/TopicPage/index.styles.jsx | 6 + 8 files changed, 446 insertions(+), 2 deletions(-) create mode 100644 data/persian/topics/c6z7mnr559gt.json diff --git a/data/mundo/topics/c7zp57yyz25t.json b/data/mundo/topics/c7zp57yyz25t.json index 93e8408bbc5..b71b3889484 100644 --- a/data/mundo/topics/c7zp57yyz25t.json +++ b/data/mundo/topics/c7zp57yyz25t.json @@ -6,6 +6,76 @@ "curations": [ { "summaries": [ + { + "type": "video", + "duration": "PT1M56S", + "isLive": false, + "title": "«واکنش به ربات‌نماهای کیش؛ از «آبروریزی جهانی» تا «بچه رباط کریم", + "firstPublished": "2025-11-16T20:29:46.464Z", + "lastPublished": "2025-11-16T20:29:46.464Z", + "link": "https://www.bbc.com/persian/articles/cn7e3651ydxo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dffd/live/936d9340-c32a-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "- «من یک داده هستم» ویدیوهای زن و مرد ربات‌نما در نمایشگاه فناوری کیش اینوکس در شبکه‌های اجتماعی ایران ترند شده است؛ تصاویری که ابتدا با تیترهای غلط‌انداز منتشر شد و در دو سه روز گذشته واکنش‌های بسیاری برانگیخته است که بیشتر آنها انتقادی یا طنزآلود است.", + "imageAlt": "زن و مرد با گریم ربات در نمایشگاه فناوری کیش", + "isPortraitImage": true, + "id": "cn7e3651ydxo" + }, + { + "type": "video", + "duration": "PT1M24S", + "isLive": false, + "title": "نمایشگاه «هفته دیزاین» در دانشگاه تهران با اعتراض بسیج دانشجویی لغو شد", + "firstPublished": "2025-11-16T15:29:22.961Z", + "lastPublished": "2025-11-16T15:29:22.961Z", + "link": "https://www.bbc.com/persian/articles/c4gk12nnye0o", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/a02c/live/7e65c320-c300-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "روابط‌عمومی دانشگاه تهران از لغو دو روز باقیمانده از نمایشگاه رویداد «هفته طراحی تهران» در دانشکده هنرهای زیبای این دانشگاه خبر داد. در پی پربازدید شدن ویدیوهایی از این رویداد و حضور بازدیدکنندگان مشتاق، بسیج دانشجویی این دانشگاه بیانیه‌ای اعتراضی صادر کرد. کمیته برگزاری این رویداد در اطلاعیه‌ای با تائید لغو نمایشگاه، به «نگرانی مسئولان دانشگاه از انضباط برگزاری و امنیت مهمانان» اشاره کرده است. در اطلاعیه دانشگاه تهران به «استقبال فراتر از تصور» و نگرانی از آسیب به بازدیدکنندگان بر اثر تراکم جمعیت اشاره شده است. رویداد «هفته دیزاین تهران» از ۲۰ تا ۲۶ آبان در نقاط مختلفی از پایتخت ایران از جمله در نمایشگاه بین‌المللی تهران، چندین گالری و موسسه هنری و فضاهای شهری در حال برگزاری است. دانشگاه تهران در بیانیه خود به ضرورت «نمایش فضای عادی زندگی اجتماعی پس از جنگ تحمیلی ۱۲ روزه» تاکید شده ولی آمده است که تراکم جمعیت بازدیدکنندگان، ممکن است آسیب‌های ایمنی مانند برق‌گرفتگی ایجاد کند. در این بیانیه همچنین به ورود افرادی غیر دانشجو و «عدم رعایت شئون دانشگاه و جامعه» اشاره شده است. بسیج دانشجویی پردیس هنرهای زیبا با انتشار بیانیه‌ای اعتراضی که خبرگزاری‌های منتقد دولت همچون فارس آن را پوشش دادند، این رویداد را «نماد افت استانداردهای علمی و انضباطی» در دانشگاه دانست. چندی پیش هم پربازدید شدن ویدیوهای حضور علاقمندان به اجرای خیابانی یک گروه راک در مرکز تهران، منجر به اعمال محدودیت بر اعضا و بسته شدن اینستاگرام آنها شد.", + "imageAlt": "پوستر هفته دیزاین تهران", + "isPortraitImage": true, + "id": "c4gk12nnye0o" + }, + { + "type": "video", + "duration": "PT20S", + "isLive": false, + "title": "ترکش‌های بوسه بدون رضایت؛ «عموی تنی» روبیالس به او تخم مرغ پرتاب کرد", + "firstPublished": "2025-11-14T19:42:34.380Z", + "lastPublished": "2025-11-14T19:42:34.380Z", + "link": "https://www.bbc.com/persian/articles/c9v19erz9rdo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/42f8/live/74262140-c191-11f0-8669-5560f5c90fbe.jpg.webp", + "description": "لوئیس روبیالس، رئیس پیشین فدراسیون فوتبال اسپانیا، ۱۳ نوامبر، ۲۲ آبان در مراسم رونمایی از کتاب جدیدش با عنوان «کشتن روبیالس» در مادرید، هدف پرتاب تخم‌مرغ قرار گرفت. او گفت این حمله به‌دست عمویش انجام شده است. روبیالس توضیح داد که ابتدا تصور کرده عمویش اسلحه دارد.", + "imageAlt": "لوئیس روبیالس", + "isPortraitImage": true, + "id": "c9v19erz9rdo" + }, + { + "type": "video", + "duration": "PT28S", + "isLive": false, + "title": "آشوب در مراسم اکران؛ همبازی آریانا گرانده از دست مرد مزاحم نجاتش داد", + "firstPublished": "2025-11-14T13:48:30.871Z", + "lastPublished": "2025-11-14T13:48:30.871Z", + "link": "https://www.bbc.com/persian/articles/c14pd0njkrlo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/cc12/live/4ae2bcc0-c160-11f0-8456-eff94716b162.jpg.webp", + "description": "در اکران افتتاحیه قسمت دوم فیلم ویکد که دیروز ۱۳ نوامبر، ۲۲ آبان در سنگاپور برگزار شد، مردی با عبور از حفاظ امنیتی بر فرش زرد مراسم پرید و تلاش کرد آریانا گرانده را بگیرد. بسیاری از هواداران و ناظران از سرعت عمل سینتیا اریوو در حائل شدن میان مرد مهاجم و همبازی‌ خود تقدیر کرده‌اند.", + "imageAlt": "آریانا گرانده و سینتیا اریوو", + "isPortraitImage": true, + "id": "c14pd0njkrlo" + }, + { + "type": "video", + "duration": "PT59S", + "isLive": false, + "title": "بازداشت دو مرد با لباس نظامی و پرچم شیر و خورشید در متروی تهران", + "firstPublished": "2025-11-13T18:21:35.585Z", + "lastPublished": "2025-11-13T18:21:35.585Z", + "link": "https://www.bbc.com/persian/articles/c4gw348pl30o", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/b1f2/live/cc166610-c0bc-11f0-8456-eff94716b162.jpg.webp", + "description": "به گزارش رسانه‌های ایران دو مرد که با لباس ارتشی و بلندگویی در دست، پرچم شیروخورشید نشان را در متروی تهران بلند کرده بودند، دستگیر شدند.", + "imageAlt": "دو مرد با لباس نظامی و پرچم شیر و خورشید در متروی تهران", + "isPortraitImage": true, + "id": "c4gw348pl30o" + }, { "type": "article", "title": "Las intrigantes caras talladas en rocas que quedaron expuestas por la grave sequía en el Amazonas", diff --git a/data/persian/topics/c6z7mnr559gt.json b/data/persian/topics/c6z7mnr559gt.json new file mode 100644 index 00000000000..ab5a7d2cf0c --- /dev/null +++ b/data/persian/topics/c6z7mnr559gt.json @@ -0,0 +1,315 @@ +{ + "data": { + "title": "ویدیو", + "description": "", + "imageData": null, + "curations": [ + { + "summaries": [ + { + "type": "video", + "duration": "PT1M56S", + "isLive": false, + "title": "«واکنش به ربات‌نماهای کیش؛ از «آبروریزی جهانی» تا «بچه رباط کریم", + "firstPublished": "2025-11-16T20:29:46.464Z", + "lastPublished": "2025-11-16T20:29:46.464Z", + "link": "https://www.bbc.com/persian/articles/cn7e3651ydxo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dffd/live/936d9340-c32a-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "- «من یک داده هستم» ویدیوهای زن و مرد ربات‌نما در نمایشگاه فناوری کیش اینوکس در شبکه‌های اجتماعی ایران ترند شده است؛ تصاویری که ابتدا با تیترهای غلط‌انداز منتشر شد و در دو سه روز گذشته واکنش‌های بسیاری برانگیخته است که بیشتر آنها انتقادی یا طنزآلود است.", + "imageAlt": "زن و مرد با گریم ربات در نمایشگاه فناوری کیش", + "isPortraitImage": true, + "id": "cn7e3651ydxo" + }, + { + "type": "video", + "duration": "PT1M24S", + "isLive": false, + "title": "نمایشگاه «هفته دیزاین» در دانشگاه تهران با اعتراض بسیج دانشجویی لغو شد", + "firstPublished": "2025-11-16T15:29:22.961Z", + "lastPublished": "2025-11-16T15:29:22.961Z", + "link": "https://www.bbc.com/persian/articles/c4gk12nnye0o", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/a02c/live/7e65c320-c300-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "روابط‌عمومی دانشگاه تهران از لغو دو روز باقیمانده از نمایشگاه رویداد «هفته طراحی تهران» در دانشکده هنرهای زیبای این دانشگاه خبر داد. در پی پربازدید شدن ویدیوهایی از این رویداد و حضور بازدیدکنندگان مشتاق، بسیج دانشجویی این دانشگاه بیانیه‌ای اعتراضی صادر کرد. کمیته برگزاری این رویداد در اطلاعیه‌ای با تائید لغو نمایشگاه، به «نگرانی مسئولان دانشگاه از انضباط برگزاری و امنیت مهمانان» اشاره کرده است. در اطلاعیه دانشگاه تهران به «استقبال فراتر از تصور» و نگرانی از آسیب به بازدیدکنندگان بر اثر تراکم جمعیت اشاره شده است. رویداد «هفته دیزاین تهران» از ۲۰ تا ۲۶ آبان در نقاط مختلفی از پایتخت ایران از جمله در نمایشگاه بین‌المللی تهران، چندین گالری و موسسه هنری و فضاهای شهری در حال برگزاری است. دانشگاه تهران در بیانیه خود به ضرورت «نمایش فضای عادی زندگی اجتماعی پس از جنگ تحمیلی ۱۲ روزه» تاکید شده ولی آمده است که تراکم جمعیت بازدیدکنندگان، ممکن است آسیب‌های ایمنی مانند برق‌گرفتگی ایجاد کند. در این بیانیه همچنین به ورود افرادی غیر دانشجو و «عدم رعایت شئون دانشگاه و جامعه» اشاره شده است. بسیج دانشجویی پردیس هنرهای زیبا با انتشار بیانیه‌ای اعتراضی که خبرگزاری‌های منتقد دولت همچون فارس آن را پوشش دادند، این رویداد را «نماد افت استانداردهای علمی و انضباطی» در دانشگاه دانست. چندی پیش هم پربازدید شدن ویدیوهای حضور علاقمندان به اجرای خیابانی یک گروه راک در مرکز تهران، منجر به اعمال محدودیت بر اعضا و بسته شدن اینستاگرام آنها شد.", + "imageAlt": "پوستر هفته دیزاین تهران", + "isPortraitImage": true, + "id": "c4gk12nnye0o" + }, + { + "type": "video", + "duration": "PT20S", + "isLive": false, + "title": "ترکش‌های بوسه بدون رضایت؛ «عموی تنی» روبیالس به او تخم مرغ پرتاب کرد", + "firstPublished": "2025-11-14T19:42:34.380Z", + "lastPublished": "2025-11-14T19:42:34.380Z", + "link": "https://www.bbc.com/persian/articles/c9v19erz9rdo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/42f8/live/74262140-c191-11f0-8669-5560f5c90fbe.jpg.webp", + "description": "لوئیس روبیالس، رئیس پیشین فدراسیون فوتبال اسپانیا، ۱۳ نوامبر، ۲۲ آبان در مراسم رونمایی از کتاب جدیدش با عنوان «کشتن روبیالس» در مادرید، هدف پرتاب تخم‌مرغ قرار گرفت. او گفت این حمله به‌دست عمویش انجام شده است. روبیالس توضیح داد که ابتدا تصور کرده عمویش اسلحه دارد.", + "imageAlt": "لوئیس روبیالس", + "isPortraitImage": true, + "id": "c9v19erz9rdo" + }, + { + "type": "video", + "duration": "PT28S", + "isLive": false, + "title": "آشوب در مراسم اکران؛ همبازی آریانا گرانده از دست مرد مزاحم نجاتش داد", + "firstPublished": "2025-11-14T13:48:30.871Z", + "lastPublished": "2025-11-14T13:48:30.871Z", + "link": "https://www.bbc.com/persian/articles/c14pd0njkrlo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/cc12/live/4ae2bcc0-c160-11f0-8456-eff94716b162.jpg.webp", + "description": "در اکران افتتاحیه قسمت دوم فیلم ویکد که دیروز ۱۳ نوامبر، ۲۲ آبان در سنگاپور برگزار شد، مردی با عبور از حفاظ امنیتی بر فرش زرد مراسم پرید و تلاش کرد آریانا گرانده را بگیرد. بسیاری از هواداران و ناظران از سرعت عمل سینتیا اریوو در حائل شدن میان مرد مهاجم و همبازی‌ خود تقدیر کرده‌اند.", + "imageAlt": "آریانا گرانده و سینتیا اریوو", + "isPortraitImage": true, + "id": "c14pd0njkrlo" + }, + { + "type": "video", + "duration": "PT59S", + "isLive": false, + "title": "بازداشت دو مرد با لباس نظامی و پرچم شیر و خورشید در متروی تهران", + "firstPublished": "2025-11-13T18:21:35.585Z", + "lastPublished": "2025-11-13T18:21:35.585Z", + "link": "https://www.bbc.com/persian/articles/c4gw348pl30o", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/b1f2/live/cc166610-c0bc-11f0-8456-eff94716b162.jpg.webp", + "description": "به گزارش رسانه‌های ایران دو مرد که با لباس ارتشی و بلندگویی در دست، پرچم شیروخورشید نشان را در متروی تهران بلند کرده بودند، دستگیر شدند.", + "imageAlt": "دو مرد با لباس نظامی و پرچم شیر و خورشید در متروی تهران", + "isPortraitImage": true, + "id": "c4gw348pl30o" + }, + { + "type": "video", + "duration": "PT37S", + "isLive": false, + "title": "انسان‌های ربات‌نما در نمایشگاه فناوری کیش", + "firstPublished": "2025-11-13T17:49:51.352Z", + "lastPublished": "2025-11-13T17:49:51.352Z", + "link": "https://www.bbc.com/persian/articles/c9d68nd391eo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/7f99/live/b6e5e300-c0b8-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "تصاویری از زن و مردی که در یکی از غرفه‌های نمایشگاه‌ امسال کیش «اینوکس» در نقش ربات انسان‌نما ظاهر شده‌اند، واکنش‌های بسیاری در شبکه‌های اجتماعی برانگیخته است. زن در نقش ربات خود را «میس دیتا» معرفی می‌کند و می‌گوید یک «داده» است که در فضای «بلاک‌چین» زندگی می‌کند. مرد ربات‌نما هم می‌گوید او یک هوش مصنوعی است.", + "imageAlt": "زن با گریم ربات در نمایشگاه فناوری کیش", + "isPortraitImage": true, + "id": "c9d68nd391eo" + }, + { + "type": "video", + "duration": "PT2M39S", + "isLive": false, + "title": "بحث زنوزی و فردوسی‌پور بر سر «تجزیه‌طلبی و تفرقه‌افکنی»", + "firstPublished": "2025-11-13T16:32:13.086Z", + "lastPublished": "2025-11-13T16:32:13.086Z", + "link": "https://www.bbc.com/persian/articles/cr4395nlkpvo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/ea0d/live/f5f86fa0-c0ad-11f0-8669-5560f5c90fbe.jpg.webp", + "description": "بحث عادل فردوسی‌پور، مجری و تهیه‌کننده با محمدرضا زنوزی، مالک باشگاه تراکتور بر سر «تجزیه‌طلبی و تفرقه‌افکنی» واکنش‌برانگیز شده است.", + "imageAlt": "محمدرضا زنوزی", + "isPortraitImage": true, + "id": "cr4395nlkpvo" + }, + { + "type": "video", + "duration": "PT26S", + "isLive": false, + "title": "ترامپ به احمد شرع: این عطر هم هدیه همسرت. راستی چند تا زن داری؟", + "firstPublished": "2025-11-13T15:32:27.229Z", + "lastPublished": "2025-11-13T15:32:27.229Z", + "link": "https://www.bbc.com/persian/articles/cy0yj5307nwo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/1c9f/live/aae31130-c0a5-11f0-8669-5560f5c90fbe.jpg.webp", + "description": "‌ویدیویی از حاشیه دیدار احمد شرع، رئیس‌‌جمهور موقت سوریه با دونالد ترامپ، رئیس‌جمهور آمریکا در کاخ سفید پربازدید شده است که نشان می‌دهد آقای ترامپ در دفتر کارش دو شیشه عطر مردانه و زنانه از برند خودش را به آقای شرع هدیه می‌دهد و از او می‌پرسد: «راستی چند تا زن داری؟ یکی؟»", + "imageAlt": "احمد شرع و دونالد ترامپ", + "isPortraitImage": true, + "id": "cy0yj5307nwo" + }, + { + "type": "video", + "duration": "PT1M17S", + "isLive": false, + "title": "کارکنان بهزیستی شهرهای مختلف ایران «خسته از تبعیض و بی‌عدالتی» تحصن کردند", + "firstPublished": "2025-11-11T17:33:29.800Z", + "lastPublished": "2025-11-11T17:33:29.800Z", + "link": "https://www.bbc.com/persian/articles/cn0g7r0en3ko", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/5f43/live/e69f1030-bf23-11f0-8456-eff94716b162.jpg.webp", + "description": "تعدادی از کارکنان ادارات بهزیستی در شهرهای ایران مختلف ایران در اعتراض به شرایط کاری و آنچه «تبعیض و بی‌عدالتی» دانستند، امروز ۲۰ آبان ۱۴۰۴ اعتصاب کردند.", + "imageAlt": "اعتراض کارکنان بهزیستی شهرهای مختلف ایران نسبت به وضعیت معیشتی، بی عدالتی و شرایط کاری", + "isPortraitImage": true, + "id": "cn0g7r0en3ko" + }, + { + "type": "video", + "duration": "PT31S", + "isLive": false, + "title": "ببینید؛ آتشفشان هاوایی که گدازه‌ها را تا ارتفاع ۳۰۰ متری پرتاب می‌کند", + "firstPublished": "2025-11-10T22:58:11.485Z", + "lastPublished": "2025-11-10T22:58:11.485Z", + "link": "https://www.bbc.com/persian/articles/cj6ng2w3y96o", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/e316/live/6bbe3870-be68-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "آتش‌فشان کيلاوئا در جزایر هاوايی آمریکا، يکی از فعال‌ترين آتش‌فشان‌های جهان، از روز گذشته - ۹ نوامبر/۱۸آبان - بار ديگر فوران کرد و فواره‌ای عظيم از گدازه را به آسمان پرتاب می‌کند. ", + "imageAlt": "Red lava and smoke shoots from black mountains into the air", + "isPortraitImage": true, + "id": "cj6ng2w3y96o" + }, + { + "type": "video", + "duration": "PT42S", + "isLive": false, + "title": "محققان می‌گویند احتمالا بزرگترین تارعنکبوت جهان را کشف کرده‌اند", + "firstPublished": "2025-11-10T17:17:12.993Z", + "lastPublished": "2025-11-10T17:17:12.993Z", + "link": "https://www.bbc.com/persian/articles/c5y4enp9kk5o", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/65c2/live/d6e22630-be58-11f0-8669-5560f5c90fbe.jpg.webp", + "description": "بر اساس یک مقاله تحقیقاتی که در یک مجله بین‌المللی زیست‌شناسی منتشر شده است، محققان در سال ۲۰۲۲ یک «ابرشهر» عظیم از عنکبوت‌ها را در غار سولفور (گوگرد)، واقع در مرز بین آلبانی و یونان، کشف کردند. پس از سال‌ها مطالعه، محققان تخمین می‌زنند که این تار عنکبوت که بیش از ۱۰۰ متر مربع وسعت دارد، محل زندگی حدود ۱۱۰ هزار عنکبوت است. ", + "imageAlt": "تارعنکبوت با وسعت ۱۰۰ متر مربع در مرز آلبانی و یونان ", + "isPortraitImage": true, + "id": "c5y4enp9kk5o" + }, + { + "type": "video", + "duration": "PT34S", + "isLive": false, + "title": "گردباد برزیل شش کشته به ‌جا‌گذاشت", + "firstPublished": "2025-11-10T16:08:58.217Z", + "lastPublished": "2025-11-10T16:08:58.217Z", + "link": "https://www.bbc.com/persian/articles/c4gk47dkrr7o", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/4873/live/54912ea0-be4f-11f0-8669-5560f5c90fbe.jpg.webp", + "description": "تصاویری هوایی منتشر شده نشان می‌دهد بر اثر گردباد شدیدی که روز جمعه، شهر ریو بونیتو دو ایگواسو در جنوب برزیل را درنوردیده، خسارات سنگینی به خانه‌ها و خودروها وارد شده است. به گفته مقام‌های رسمی، دست‌کم شش نفر جان باختند و دست‌کم ۷۵۰ نفر در این گردباد زخمی شدند. ", + "imageAlt": "خرابی‌های ناشی از گردباد در بزریل", + "isPortraitImage": true, + "id": "c4gk47dkrr7o" + }, + { + "type": "video", + "duration": "PT1M8S", + "isLive": false, + "title": "نیروی هوافضای سپاه: تصاویر منتشر شده ارتباطی با تونل‌های موشکی و پهپادی سپاه ندارد", + "firstPublished": "2025-11-10T11:42:51.707Z", + "lastPublished": "2025-11-10T11:42:51.707Z", + "link": "https://www.bbc.com/persian/articles/c625e1x476wo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/18d5/live/376d76f0-be2a-11f0-8456-eff94716b162.png.webp", + "description": "پس از آن که انتشار تصاویری از یک تونل زیرزمینی با «درهای ضدانفجار» در صفحات مجازی یک کسب و کار فروش گاوصندوق در ایران واکنش‌برانگیز شد، روابط عمومی نیروی هوافضای سپاه اعلام کرده این تصاویر «هیچ ارتباطی با تونل‌های موشکی و پهپادی» این نیرو ندارد.", + "imageAlt": "تصاویر تونل‌های زیر کوه که سپاه می‌گوید ارتباطی با آن ندارد", + "isPortraitImage": true, + "id": "c625e1x476wo" + }, + { + "type": "video", + "duration": "PT20S", + "isLive": false, + "title": "«دیپلماسی ورزشی» واکنش‌برانگیز؛ بسکتبال احمد شرع با ژنرال‌های آمریکایی", + "firstPublished": "2025-11-09T19:49:53.732Z", + "lastPublished": "2025-11-09T19:49:53.732Z", + "link": "https://www.bbc.com/persian/articles/ckgkp34mm32o", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/980a/live/ba7bb100-bda4-11f0-8456-eff94716b162.png.webp", + "description": "تصاویری که اسعد حسن الشیبانی، وزیر خارجه سوریه، در اینستاگرامش از سفر احمد شرع، رئیس‌جمهور این کشور به آمریکا منتشر کرده است، این دو در حال بازی بسکتبال با کوین لمبرت، فرمانده نیروهای ائتلاف در عراق و برد کوپر، فرمانده ستاد مرکزی ارتش آمریکا (سنتکام) نشان می‌دهد که بسیار پربازدید شده است.", + "imageAlt": "احمد شرع", + "isPortraitImage": true, + "id": "ckgkp34mm32o" + }, + { + "type": "video", + "duration": "PT1M39S", + "isLive": false, + "title": "دبیر شورای عالی انقلاب فرهنگی: استاندار یا امام جمعه نمی‌تواند جلوی کنسرتی را بگیرد", + "firstPublished": "2025-11-09T19:07:40.615Z", + "lastPublished": "2025-11-09T19:07:40.615Z", + "link": "https://www.bbc.com/persian/articles/cpd2p1lz3wgo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/0a26/live/aed17390-bd9e-11f0-8456-eff94716b162.jpg.webp", + "description": "عبدالحسین خسروپناه، دبیر شورای عالی انقلاب فرهنگی درباره اعمال نفوذ و «قانون‌ستیزی» مقامات در جلوگیری از برگزاری کنسرت،‌ بدون اشاره به مشهد گفت: «هیچ‌کس فراتر از قانون نیست. کسی نمی‌تواند بگوید من استاندار، فرماندار، امام‌جمعه، روحانی یا بزرگ محل هستم و اجازه اجرای یک برنامه را به دلیل اینکه خلاف شرع می‌دانم یا نمی‌پسندم نمی‌دهم.»", + "imageAlt": "عبدالحسین خسروپناه، دبیر شورای عالی انقلاب فرهنگی", + "isPortraitImage": true, + "id": "cpd2p1lz3wgo" + }, + { + "type": "video", + "duration": "PT1M39S", + "isLive": false, + "title": "سفیر پیشین ایران در پکن: چین نمی‌تواند با ایران که رئیس‌جمهورش در لحظه چیزی می‌گوید، کار کند", + "firstPublished": "2025-11-09T17:39:09.663Z", + "lastPublished": "2025-11-09T17:39:09.663Z", + "link": "https://www.bbc.com/persian/articles/cwy72g5ykgqo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/8296/live/6e8760e0-bd73-11f0-8456-eff94716b162.jpg.webp", + "description": "محمدحسین ملائک، سفیر پیشین ایران در چین، در مورد نگاه‌ها در این کشور نسبت به ایران گفت: «از نظر چینی‌ها، نظام سیاسی ایران یکی از بی‌برنامه‌ترین نظام‌های منطقه و خاورمیانه است.» او افزود که روابط ایران و چین در بسیاری از موارد بر پایه «دیپلماسی اضطراری» شکل گرفته است، به این معنا که ایران معمولا زمانی به چین رجوع کرده که با بن‌بست‌های سیاسی و اقتصادی مواجه شده است.", + "imageAlt": "سفیر پیشین ایران در پکن درمورد نظر چینی‌ها نسبت به ایران می‌گوید", + "isPortraitImage": true, + "id": "cwy72g5ykgqo" + }, + { + "type": "video", + "duration": "PT58S", + "isLive": false, + "title": "پنج کشته در سقوط هلی‌کوپتر روسیه در ساحل خزر", + "firstPublished": "2025-11-09T17:30:25.163Z", + "lastPublished": "2025-11-09T17:30:25.163Z", + "link": "https://www.bbc.com/persian/articles/c0l72np133xo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/485a/live/15dabe10-bd91-11f0-8456-eff94716b162.png.webp", + "description": "ویدیویی از لحظات منجر به سقوط یک هلی‌کوپتر خصوصی در جمهوری داغستان منتشر شده است که به‌نظر می‌رسد در هنگام فرود دچار سانحه می‌شود و در پی شکستن دم، به‌رغم تلاش خلبان برای کنترل، در نهایت به زمین برخورد می‌‌کند.", + "imageAlt": "سقوط هلی‌کوپتر روسیه در ساحل خزر", + "isPortraitImage": true, + "id": "c0l72np133xo" + }, + { + "type": "video", + "duration": "PT59S", + "isLive": false, + "title": "بازداشت چهار نفر پس از آتش‌افروزی در کنسرت ارکستر اسرائیل در پاریس", + "firstPublished": "2025-11-08T17:32:06.556Z", + "lastPublished": "2025-11-08T17:32:06.556Z", + "link": "https://www.bbc.com/persian/articles/cpv1xpx948jo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/f029/live/798cbbc0-bcc9-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "بازداشت چهار نفر پس از آتش‌افروزی در کنسرت ارکستر اسرائیل در پاریس", + "imageAlt": "بازداشت چهار نفر پس آتش‌افروزی در کنسرت ارکستر اسرائیل در پاریس", + "isPortraitImage": true, + "id": "cpv1xpx948jo" + }, + { + "type": "video", + "duration": "PT44S", + "isLive": false, + "title": "خودسوزی جوان اهوازی؛ دادستانی خوزستان: اجازه بهره‌برداری از احساسات قومیتی را نمی‌دهیم", + "firstPublished": "2025-11-07T21:51:55.052Z", + "lastPublished": "2025-11-07T21:51:55.052Z", + "link": "https://www.bbc.com/persian/articles/ckgkwnw9097o", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dcad/live/69066770-bc23-11f0-8456-eff94716b162.png.webp", + "description": "در پی اقدام به خودسوزی جوانی به نام احمد بالدی در اعتراض به تخریب دکه اغذیه‌فروشی پدرش در پارک زیتون اهواز به دست ماموران شهرداری، دادسرای عمومی و انقلاب خوزستان در بیانیه با تاکید بر «رسیدگی قضایی دقیق و همه‌جانبه»، تهدید کرد که «با جدیت با کسانی که از این حادثه، برای ایجاد تفرقه و تحریک احساسات قومیتی بهره‌برداری کنند، با جدیت برخورد می‌کند.»", + "imageAlt": "احمد بالدی", + "isPortraitImage": true, + "id": "ckgkwnw9097o" + }, + { + "type": "video", + "duration": "PT56S", + "isLive": false, + "title": "تیدا، سگ ستاره سینما و شبکه‌های اجتماعی ایران مرد", + "firstPublished": "2025-11-07T19:28:20.482Z", + "lastPublished": "2025-11-07T19:28:20.482Z", + "link": "https://www.bbc.com/persian/articles/c4g3vz9gx4ro", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/fab5/live/a8253f30-bc0f-11f0-8456-eff94716b162.png.webp", + "description": "تیدا، سگ محبوب در شبکه‌های اجتماعی ایران که در چند فیلم و سریال و صحنه فجایعی مانند ریزش ساختمان پلاسکو در تهران و متروپل آبادان و زلزله کرمانشاه حضور داشت، در ۱۶ سالگی مرد.", + "imageAlt": "تیدا، سگ ستاره شبکه‌های اجتماعی ایران", + "isPortraitImage": true, + "id": "c4g3vz9gx4ro" + } + ], + "activePage": 1, + "pageCount": 40, + "curationId": "urn:bbc:vivo:curation:6978ef91-d1fd-4d17-bdba-baa8aaaee606", + "curationType": "vivo-stream", + "position": 0, + "visualProminence": "NORMAL", + "visualStyle": "NONE" + } + ], + "activePage": 1, + "pageCount": 40, + "metadata": { + "analytics": { + "name": "persian.topics.c6z7mnr559gt.page", + "producer": "PERSIAN" + }, + "atiAnalytics": { + "contentId": "urn:bbc:tipo:topic:c6z7mnr559gt", + "contentType": "index-category", + "pageIdentifier": "persian.topics.c6z7mnr559gt.page", + "pageTitle": "ویدیو" + } + } + }, + "contentType": "application/json; charset=utf-8" +} diff --git a/data/ws/homePage/index.json b/data/ws/homePage/index.json index 73e47be3dde..bbeabd736b3 100644 --- a/data/ws/homePage/index.json +++ b/data/ws/homePage/index.json @@ -5,6 +5,21 @@ "curations": [ { "summaries": [ + { + "type": "topic", + "duration": "PT1M56S", + "isLive": false, + "title": "«واکنش به ربات‌نماهای کیش؛ از «آبروریزی جهانی» تا «بچه رباط کریم", + "firstPublished": "2025-11-16T20:29:46.464Z", + "lastPublished": "2025-11-16T20:29:46.464Z", + "link": "https://www.bbc.com/persian/articles/cn7e3651ydxo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dffd/live/936d9340-c32a-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "- «من یک داده هستم» ویدیوهای زن و مرد ربات‌نما در نمایشگاه فناوری کیش اینوکس در شبکه‌های اجتماعی ایران ترند شده است؛ تصاویری که ابتدا با تیترهای غلط‌انداز منتشر شد و در دو سه روز گذشته واکنش‌های بسیاری برانگیخته است که بیشتر آنها انتقادی یا طنزآلود است.", + "isPortraitImage": true, + "imageAlt": "زن و مرد با گریم ربات در نمایشگاه فناوری کیش", + "id": "cn7e3651ydxo", + "visualProminence": "MAXIMUM" + }, { "type": "topic", "isLive": false, diff --git a/src/app/components/Curation/CurationPromo/index.tsx b/src/app/components/Curation/CurationPromo/index.tsx index c1e75e063ad..5ab8c5025f7 100644 --- a/src/app/components/Curation/CurationPromo/index.tsx +++ b/src/app/components/Curation/CurationPromo/index.tsx @@ -30,6 +30,7 @@ const CurationPromo = ({ eventTrackingData, timeOfDayExperimentName, timeOfDayVariant, + isPortraitImage, }: Summary) => { const { isAmp, isLite } = use(RequestContext); const { translations } = use(ServiceContext); @@ -69,6 +70,7 @@ const CurationPromo = ({ alt={imageAlt} lazyLoad={lazy} isAmp={isAmp} + isPortraitImage={isPortraitImage} {...(isLite && { css: styles.image })} > {isMedia && ( diff --git a/src/app/components/Image/index.tsx b/src/app/components/Image/index.tsx index b0c995747c5..d65f7b7a6a5 100644 --- a/src/app/components/Image/index.tsx +++ b/src/app/components/Image/index.tsx @@ -1,4 +1,6 @@ -import { Fragment, PropsWithChildren, useState, use } from 'react'; +/** @jsx jsx */ +/* @jsxFrag React.Fragment */ +import { Fragment, PropsWithChildren, useState, use, useCallback } from 'react'; import { Global } from '@emotion/react'; import { Helmet } from 'react-helmet'; import styles from './index.styles'; @@ -57,6 +59,12 @@ const Image = ({ }: PropsWithChildren) => { const { pageType, isLite, isAmp } = use(RequestContext); const [isLoaded, setIsLoaded] = useState(false); + const handleImgRef = useCallback((img: HTMLImageElement | null) => { + if (!img) return; + if (img.complete) { + setIsLoaded(true); + } + }, []); if (isLite) return null; const showPlaceholder = placeholder && !isLoaded; @@ -88,6 +96,7 @@ const Image = ({ }; const imgSrcSet = getImgSrcSet(); const imgSizes = getImgSizes(); + return ( <> {preload && ( @@ -163,7 +172,10 @@ const Image = ({ )} setIsLoaded(true)} + onLoad={() => { + setIsLoaded(true); + }} + ref={handleImgRef} src={src} {...(srcSet && { srcSet: imgSrcSet })} {...(imgSizes && { sizes: imgSizes })} diff --git a/src/app/legacy/components/Promo/image.jsx b/src/app/legacy/components/Promo/image.jsx index 8599c31d5ad..2c0a62514d7 100644 --- a/src/app/legacy/components/Promo/image.jsx +++ b/src/app/legacy/components/Promo/image.jsx @@ -9,6 +9,26 @@ import IMAGE from '../../../components/Image'; const Wrapper = styled.div` margin-bottom: ${GEL_SPACING}; position: relative; + > div > img { + object-fit: contain; + } + overflow: hidden; +`; + +const BlurredBackgrounnd = styled.span` + display: block; + position: absolute; + /* When the image is blurred by the filter, it leaves a transparent gradient + around the edge that's double the length of the blur. We are hiding the + edge using positioning to compensate. */ + top: -30px; + right: -30px; + bottom: -30px; + left: -30px; + background-position: center; + background-size: cover; + background-repeat: no-repeat; + filter: blur(15px); `; const ChildWrapper = styled.div` @@ -78,6 +98,9 @@ const Image = props => { const sizes = createSizes(useLargeImages, isProgrammeImage); return ( + Date: Fri, 5 Dec 2025 15:44:20 +0000 Subject: [PATCH 14/96] Add isPortraitImage condition back --- data/ws/homePage/index.json | 15 --------------- src/app/legacy/components/Promo/image.jsx | 5 +++-- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/data/ws/homePage/index.json b/data/ws/homePage/index.json index bbeabd736b3..73e47be3dde 100644 --- a/data/ws/homePage/index.json +++ b/data/ws/homePage/index.json @@ -5,21 +5,6 @@ "curations": [ { "summaries": [ - { - "type": "topic", - "duration": "PT1M56S", - "isLive": false, - "title": "«واکنش به ربات‌نماهای کیش؛ از «آبروریزی جهانی» تا «بچه رباط کریم", - "firstPublished": "2025-11-16T20:29:46.464Z", - "lastPublished": "2025-11-16T20:29:46.464Z", - "link": "https://www.bbc.com/persian/articles/cn7e3651ydxo", - "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dffd/live/936d9340-c32a-11f0-ae46-bd64331f0fd4.jpg.webp", - "description": "- «من یک داده هستم» ویدیوهای زن و مرد ربات‌نما در نمایشگاه فناوری کیش اینوکس در شبکه‌های اجتماعی ایران ترند شده است؛ تصاویری که ابتدا با تیترهای غلط‌انداز منتشر شد و در دو سه روز گذشته واکنش‌های بسیاری برانگیخته است که بیشتر آنها انتقادی یا طنزآلود است.", - "isPortraitImage": true, - "imageAlt": "زن و مرد با گریم ربات در نمایشگاه فناوری کیش", - "id": "cn7e3651ydxo", - "visualProminence": "MAXIMUM" - }, { "type": "topic", "isLive": false, diff --git a/src/app/legacy/components/Promo/image.jsx b/src/app/legacy/components/Promo/image.jsx index 2c0a62514d7..e3c26d7c5c9 100644 --- a/src/app/legacy/components/Promo/image.jsx +++ b/src/app/legacy/components/Promo/image.jsx @@ -10,7 +10,7 @@ const Wrapper = styled.div` margin-bottom: ${GEL_SPACING}; position: relative; > div > img { - object-fit: contain; + object-fit: ${props => (props.isPortraitImage ? 'contain' : 'cover')}; } overflow: hidden; `; @@ -82,6 +82,7 @@ const Image = props => { src, useLargeImages = false, className, + isPortraitImage, ...rest } = props; const isProgrammeImage = src.startsWith( @@ -97,7 +98,7 @@ const Image = props => { const sizes = createSizes(useLargeImages, isProgrammeImage); return ( - + From beb1dcb68a4fb6e2e2420b2fd4ffab3589bf6656 Mon Sep 17 00:00:00 2001 From: hotinglok Date: Tue, 9 Dec 2025 11:20:47 +0000 Subject: [PATCH 15/96] Move css into separate component, apply changes to MAPs --- .../Curation/CurationGrid/index.styles.ts | 4 +- .../components/Image/BlurredBackground.tsx | 20 ++++++++++ src/app/components/Image/index.styles.tsx | 15 ++++++++ .../components/OptimoPromos/Image/index.jsx | 2 + src/app/legacy/components/Promo/image.jsx | 38 +++++++------------ .../LatestMediaIndicator/index.styles.ts | 11 +++++- .../LatestMediaItem/index.styles.ts | 5 +++ .../LatestMediaItem/index.tsx | 3 ++ .../LatestMediaSection/types.ts | 1 + src/app/pages/TopicPage/index.styles.jsx | 6 --- 10 files changed, 72 insertions(+), 33 deletions(-) create mode 100644 src/app/components/Image/BlurredBackground.tsx diff --git a/src/app/components/Curation/CurationGrid/index.styles.ts b/src/app/components/Curation/CurationGrid/index.styles.ts index d19a3d432ab..6f2fa48e482 100644 --- a/src/app/components/Curation/CurationGrid/index.styles.ts +++ b/src/app/components/Curation/CurationGrid/index.styles.ts @@ -34,7 +34,7 @@ const styles = { display: 'inline-block', verticalAlign: 'top', }, - 'div div:nth-child(2)': { + 'div div:last-child': { [mq.GROUP_1_MAX_WIDTH]: { position: 'relative', }, @@ -51,7 +51,7 @@ const styles = { }, }, time: { - marginLeft: `${spacings.FULL}rem`, + marginInlineStart: `${spacings.FULL}rem`, padding: '0', }, }, diff --git a/src/app/components/Image/BlurredBackground.tsx b/src/app/components/Image/BlurredBackground.tsx new file mode 100644 index 00000000000..f2116e923fe --- /dev/null +++ b/src/app/components/Image/BlurredBackground.tsx @@ -0,0 +1,20 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/react'; +import styles from './index.styles'; + +type BlurredBackgroundProps = { + src?: string; +}; + +const BlurredBackground = ({ src }: BlurredBackgroundProps) => { + return ( + + ); +}; + +export default BlurredBackground; diff --git a/src/app/components/Image/index.styles.tsx b/src/app/components/Image/index.styles.tsx index 5b9b63bcc34..59d6c2d81d0 100644 --- a/src/app/components/Image/index.styles.tsx +++ b/src/app/components/Image/index.styles.tsx @@ -38,6 +38,21 @@ const styles = { portraitOrientation: css({ position: 'absolute', }), + blurredBackground: css({ + display: 'block', + position: 'absolute', + /* When the image is blurred by the filter, it leaves a transparent gradient + around the edge that's double the length of the blur. We are hiding the + edge using positioning to compensate. */ + top: '-30px', + right: '-30px', + bottom: '-30px', + left: '-30px', + backgroundPosition: 'center', + backgroundSize: 'cover', + backgroundRepeat: 'no-repeat', + filter: 'blur(15px)', + }), }; export default styles; diff --git a/src/app/legacy/components/OptimoPromos/Image/index.jsx b/src/app/legacy/components/OptimoPromos/Image/index.jsx index b499fd4d0e0..1ecc6bdd652 100644 --- a/src/app/legacy/components/OptimoPromos/Image/index.jsx +++ b/src/app/legacy/components/OptimoPromos/Image/index.jsx @@ -7,6 +7,7 @@ const Image = ({ fallbackSrcset = '', width, height, + isPortraitImage = false, }) => { const ASPECT_RATIO = [16, 9]; @@ -20,6 +21,7 @@ const Image = ({ width={width} height={height} lazyLoad + {...(isPortraitImage && { placeholder: false })} /> ); }; diff --git a/src/app/legacy/components/Promo/image.jsx b/src/app/legacy/components/Promo/image.jsx index e3c26d7c5c9..d98a8fdd505 100644 --- a/src/app/legacy/components/Promo/image.jsx +++ b/src/app/legacy/components/Promo/image.jsx @@ -4,31 +4,20 @@ import { GEL_GROUP_3_SCREEN_WIDTH_MIN, GEL_GROUP_4_SCREEN_WIDTH_MIN, } from '#psammead/gel-foundations/src/breakpoints'; -import IMAGE from '../../../components/Image'; +import IMAGE from '#app/components/Image'; +import BlurredBackground from '#app/components/Image/BlurredBackground'; const Wrapper = styled.div` margin-bottom: ${GEL_SPACING}; position: relative; - > div > img { - object-fit: ${props => (props.isPortraitImage ? 'contain' : 'cover')}; - } overflow: hidden; -`; - -const BlurredBackgrounnd = styled.span` - display: block; - position: absolute; - /* When the image is blurred by the filter, it leaves a transparent gradient - around the edge that's double the length of the blur. We are hiding the - edge using positioning to compensate. */ - top: -30px; - right: -30px; - bottom: -30px; - left: -30px; - background-position: center; - background-size: cover; - background-repeat: no-repeat; - filter: blur(15px); + ${({ isPortraitImage }) => + isPortraitImage && + ` + > div > img { + object-fit: contain; + } + `} `; const ChildWrapper = styled.div` @@ -97,20 +86,21 @@ const Image = props => { ); const sizes = createSizes(useLargeImages, isProgrammeImage); + + const srcWith240w = src.replace('{width}', 240); return ( - + {isPortraitImage && } {children && ( {children} diff --git a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaIndicator/index.styles.ts b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaIndicator/index.styles.ts index 0f808e4281b..37c581f20bc 100644 --- a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaIndicator/index.styles.ts +++ b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaIndicator/index.styles.ts @@ -1,14 +1,23 @@ import { css, Theme } from '@emotion/react'; const styles = { - placeholderInfo: ({ mq, fontSizes, fontVariants, palette, isLite }: Theme) => + placeholderInfo: ({ + mq, + fontSizes, + fontVariants, + palette, + isLite, + isDarkUi, + }: Theme) => css({ + position: 'relative', width: '100%', ...fontSizes.minion, ...fontVariants.sansRegular, display: 'flex', minWidth: '5rem', padding: '0.5rem 0.125rem', + backgroundColor: isDarkUi ? palette.GREY_10 : palette.GREY_2, ...(!isLite && { [mq.GROUP_3_ONLY]: { diff --git a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts index 4ec7433a795..19e47544160 100644 --- a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts +++ b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts @@ -32,12 +32,17 @@ const styles = { }), imageWrapper: ({ mq }: Theme) => css({ + position: 'relative', width: '33%', display: 'inline-block', verticalAlign: 'top', [mq.GROUP_3_ONLY]: { width: '100%', }, + overflow: 'hidden', + ' > div > img': { + objectFit: 'contain', + }, }), promoLink: ({ palette }: Theme) => css({ diff --git a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.tsx b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.tsx index b998c378990..f07ff97a97f 100644 --- a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.tsx +++ b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.tsx @@ -1,5 +1,6 @@ import { forwardRef } from 'react'; +import BlurredBackground from '#app/components/Image/BlurredBackground'; import Promo from '../../../../../legacy/components/OptimoPromos'; import { LatestMediaItemProp } from '../types'; import LatestMediaIndicator from '../LatestMediaIndicator'; @@ -24,11 +25,13 @@ const LatestMediaItem = forwardRef( css={styles.promoStyle} >
    + {item.isPortraitImage && }
    diff --git a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/types.ts b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/types.ts index 442b3fc67a6..d552166abd2 100644 --- a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/types.ts +++ b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/types.ts @@ -12,6 +12,7 @@ export type LatestMedia = { title: string; type: Media; imageAlt?: string; + isPortraitImage?: boolean; }; export type LatestMediaItemProp = { diff --git a/src/app/pages/TopicPage/index.styles.jsx b/src/app/pages/TopicPage/index.styles.jsx index f214a707d5f..1ed70975e14 100644 --- a/src/app/pages/TopicPage/index.styles.jsx +++ b/src/app/pages/TopicPage/index.styles.jsx @@ -7,12 +7,6 @@ const styles = { [mq.GROUP_2_MIN_WIDTH]: { margin: `0 ${spacings.DOUBLE}rem`, }, - // '.promo-image': { - // img: { - // background: 'black', - // objectFit: 'contain', - // }, - // }, }), inner: css({ maxWidth: '63rem', From e5a6c30d7d53052de72899560ca6cf48ad98e710 Mon Sep 17 00:00:00 2001 From: hotinglok Date: Tue, 9 Dec 2025 13:21:10 +0000 Subject: [PATCH 16/96] Add changes to hierarchical grid & billboard, update selectors --- data/mundo/homePage/index.json | 14 ++++++++++++++ .../BillboardCurationGrid/index.styles.ts | 2 +- .../components/Curation/HierarchicalGrid/index.tsx | 1 + src/app/components/Image/BlurredBackground.tsx | 2 -- src/app/legacy/components/Promo/image.jsx | 2 +- .../LatestMediaItem/index.styles.ts | 2 +- 6 files changed, 18 insertions(+), 5 deletions(-) diff --git a/data/mundo/homePage/index.json b/data/mundo/homePage/index.json index 2f2eb2f647f..f67a69ff2d1 100644 --- a/data/mundo/homePage/index.json +++ b/data/mundo/homePage/index.json @@ -37,6 +37,20 @@ "description": "La superficie del mar alcanzó un nuevo récord de temperatura debido a un aumento que nunca se había producido de manera tan brusca.", "imageAlt": "Sol sobre el océano", "id": "631efe72-a2bc-4a61-98f5-97ca8d85ba89" + }, + { + "type": "video", + "duration": "PT1M56S", + "isLive": false, + "title": "«واکنش به ربات‌نماهای کیش؛ از «آبروریزی جهانی» تا «بچه رباط کریم", + "firstPublished": "2025-11-16T20:29:46.464Z", + "lastPublished": "2025-11-16T20:29:46.464Z", + "link": "https://www.bbc.com/persian/articles/cn7e3651ydxo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dffd/live/936d9340-c32a-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "- «من یک داده هستم» ویدیوهای زن و مرد ربات‌نما در نمایشگاه فناوری کیش اینوکس در شبکه‌های اجتماعی ایران ترند شده است؛ تصاویری که ابتدا با تیترهای غلط‌انداز منتشر شد و در دو سه روز گذشته واکنش‌های بسیاری برانگیخته است که بیشتر آنها انتقادی یا طنزآلود است.", + "imageAlt": "زن و مرد با گریم ربات در نمایشگاه فناوری کیش", + "isPortraitImage": true, + "id": "cn7e3651ydxo" } ], "activePage": 1, diff --git a/src/app/components/Billboard/BillboardCurationGrid/index.styles.ts b/src/app/components/Billboard/BillboardCurationGrid/index.styles.ts index b37bc7aea19..51049ae8b02 100644 --- a/src/app/components/Billboard/BillboardCurationGrid/index.styles.ts +++ b/src/app/components/Billboard/BillboardCurationGrid/index.styles.ts @@ -46,7 +46,7 @@ const styles = { display: 'inline-block', verticalAlign: 'top', }, - 'div div:nth-child(2)': { + 'div div:last-child': { [mq.GROUP_1_MAX_WIDTH]: { position: 'relative', }, diff --git a/src/app/components/Curation/HierarchicalGrid/index.tsx b/src/app/components/Curation/HierarchicalGrid/index.tsx index b271ccde91c..8736e66aafb 100644 --- a/src/app/components/Curation/HierarchicalGrid/index.tsx +++ b/src/app/components/Curation/HierarchicalGrid/index.tsx @@ -114,6 +114,7 @@ const HiearchicalGrid = ({ lazyLoad={lazyLoadImages} fetchPriority={fetchpriority} isAmp={isAmp} + isPortraitImage={promo.isPortraitImage} > {isMedia && ( diff --git a/src/app/components/Image/BlurredBackground.tsx b/src/app/components/Image/BlurredBackground.tsx index f2116e923fe..d4d462b83a7 100644 --- a/src/app/components/Image/BlurredBackground.tsx +++ b/src/app/components/Image/BlurredBackground.tsx @@ -1,5 +1,3 @@ -/** @jsx jsx */ -import { jsx } from '@emotion/react'; import styles from './index.styles'; type BlurredBackgroundProps = { diff --git a/src/app/legacy/components/Promo/image.jsx b/src/app/legacy/components/Promo/image.jsx index d98a8fdd505..288edb4f009 100644 --- a/src/app/legacy/components/Promo/image.jsx +++ b/src/app/legacy/components/Promo/image.jsx @@ -14,7 +14,7 @@ const Wrapper = styled.div` ${({ isPortraitImage }) => isPortraitImage && ` - > div > img { + > * img { object-fit: contain; } `} diff --git a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts index 19e47544160..83f44d82454 100644 --- a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts +++ b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts @@ -40,7 +40,7 @@ const styles = { width: '100%', }, overflow: 'hidden', - ' > div > img': { + '> * img': { objectFit: 'contain', }, }), From d67f6ebb146811645e040e429a4520447480bf07 Mon Sep 17 00:00:00 2001 From: hotinglok Date: Tue, 9 Dec 2025 16:12:53 +0000 Subject: [PATCH 17/96] Make blurred background image src smaller --- src/app/components/Image/BlurredBackground.tsx | 3 ++- src/app/legacy/components/Promo/image.jsx | 5 ++--- .../LatestMediaSection/LatestMediaItem/index.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/components/Image/BlurredBackground.tsx b/src/app/components/Image/BlurredBackground.tsx index d4d462b83a7..f0493bde20b 100644 --- a/src/app/components/Image/BlurredBackground.tsx +++ b/src/app/components/Image/BlurredBackground.tsx @@ -5,11 +5,12 @@ type BlurredBackgroundProps = { }; const BlurredBackground = ({ src }: BlurredBackgroundProps) => { + const lowResImageSrc = src?.replace('{width}', '1'); return ( ); diff --git a/src/app/legacy/components/Promo/image.jsx b/src/app/legacy/components/Promo/image.jsx index 288edb4f009..ef49ba28269 100644 --- a/src/app/legacy/components/Promo/image.jsx +++ b/src/app/legacy/components/Promo/image.jsx @@ -87,13 +87,12 @@ const Image = props => { const sizes = createSizes(useLargeImages, isProgrammeImage); - const srcWith240w = src.replace('{width}', 240); return ( - {isPortraitImage && } + {isPortraitImage && } ( css={styles.promoStyle} >
    - {item.isPortraitImage && } + {item.isPortraitImage && } Date: Tue, 9 Dec 2025 16:38:50 +0000 Subject: [PATCH 18/96] Fix invalid ichef image size --- src/app/components/Image/BlurredBackground.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/Image/BlurredBackground.tsx b/src/app/components/Image/BlurredBackground.tsx index f0493bde20b..3422afa2f58 100644 --- a/src/app/components/Image/BlurredBackground.tsx +++ b/src/app/components/Image/BlurredBackground.tsx @@ -5,7 +5,7 @@ type BlurredBackgroundProps = { }; const BlurredBackground = ({ src }: BlurredBackgroundProps) => { - const lowResImageSrc = src?.replace('{width}', '1'); + const lowResImageSrc = src?.replace('{width}', '10'); return ( Date: Wed, 10 Dec 2025 11:43:58 +0000 Subject: [PATCH 19/96] Prevent any fetching on lite site pages --- src/app/components/Curation/CurationPromo/index.tsx | 2 +- src/app/components/Curation/HierarchicalGrid/index.tsx | 3 ++- src/app/legacy/components/Promo/image.jsx | 5 +++-- .../LatestMediaSection/LatestMediaItem/index.tsx | 6 ++++-- .../PagePromoSections/LatestMediaSection/index.tsx | 9 ++++++++- .../PagePromoSections/LatestMediaSection/types.ts | 1 + src/app/pages/MediaArticlePage/SecondaryColumn.tsx | 5 ++++- 7 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/app/components/Curation/CurationPromo/index.tsx b/src/app/components/Curation/CurationPromo/index.tsx index 5ab8c5025f7..1dfadca92e6 100644 --- a/src/app/components/Curation/CurationPromo/index.tsx +++ b/src/app/components/Curation/CurationPromo/index.tsx @@ -71,7 +71,7 @@ const CurationPromo = ({ lazyLoad={lazy} isAmp={isAmp} isPortraitImage={isPortraitImage} - {...(isLite && { css: styles.image })} + {...(isLite && { css: styles.image, isLite: true })} > {isMedia && ( diff --git a/src/app/components/Curation/HierarchicalGrid/index.tsx b/src/app/components/Curation/HierarchicalGrid/index.tsx index 8736e66aafb..fbc4e7ba6c0 100644 --- a/src/app/components/Curation/HierarchicalGrid/index.tsx +++ b/src/app/components/Curation/HierarchicalGrid/index.tsx @@ -39,7 +39,7 @@ const HiearchicalGrid = ({ eventTrackingData, timeOfDayVariant, }: CurationGridProps) => { - const { isAmp } = use(RequestContext); + const { isAmp, isLite } = use(RequestContext); const { translations } = use(ServiceContext); const audioTranslation = path(['media', 'audio'], translations); const videoTranslation = path(['media', 'video'], translations); @@ -114,6 +114,7 @@ const HiearchicalGrid = ({ lazyLoad={lazyLoadImages} fetchPriority={fetchpriority} isAmp={isAmp} + isLite={isLite} isPortraitImage={promo.isPortraitImage} > {isMedia && ( diff --git a/src/app/legacy/components/Promo/image.jsx b/src/app/legacy/components/Promo/image.jsx index ef49ba28269..11cca7fff32 100644 --- a/src/app/legacy/components/Promo/image.jsx +++ b/src/app/legacy/components/Promo/image.jsx @@ -72,6 +72,7 @@ const Image = props => { useLargeImages = false, className, isPortraitImage, + isLite, ...rest } = props; const isProgrammeImage = src.startsWith( @@ -88,8 +89,8 @@ const Image = props => { const sizes = createSizes(useLargeImages, isProgrammeImage); return ( - - {isPortraitImage && } + + {isPortraitImage && !isLite && } ( - ({ item, ariaLabelledBy, eventTrackingData }, viewTracker) => { + ({ item, ariaLabelledBy, eventTrackingData, isLite }, viewTracker) => { if (!item || Object.keys(item).length === 0) return null; const timestamp = item.firstPublished; @@ -25,7 +25,9 @@ const LatestMediaItem = forwardRef( css={styles.promoStyle} >
    - {item.isPortraitImage && } + {item.isPortraitImage && !isLite && ( + + )} { +const LatestMediaSection = ({ + content, + isLite, +}: { + content: LatestMedia[] | null; + isLite: boolean; +}) => { const { service, dir, translations, script } = use(ServiceContext); const eventTrackingData = { @@ -89,6 +95,7 @@ const LatestMediaSection = ({ content }: { content: LatestMedia[] | null }) => { ariaLabelledBy={ariaLabelledBy} ref={viewTracker} eventTrackingData={eventTrackingData} + isLite={isLite} /> ); diff --git a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/types.ts b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/types.ts index d552166abd2..71985a21ca4 100644 --- a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/types.ts +++ b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/types.ts @@ -20,6 +20,7 @@ export type LatestMediaItemProp = { ariaLabelledBy: string; ref: () => Promise; eventTrackingData: EventTrackingBlock; + isLite: boolean; }; export type ImageProp = { diff --git a/src/app/pages/MediaArticlePage/SecondaryColumn.tsx b/src/app/pages/MediaArticlePage/SecondaryColumn.tsx index b90ace764e1..69e3c2a0f57 100644 --- a/src/app/pages/MediaArticlePage/SecondaryColumn.tsx +++ b/src/app/pages/MediaArticlePage/SecondaryColumn.tsx @@ -1,16 +1,19 @@ import { Article } from '#app/models/types/optimo'; +import { RequestContext } from '#app/contexts/RequestContext'; +import { use } from 'react'; import LatestMediaSection from './PagePromoSections/LatestMediaSection'; import styles from './MediaArticlePage.styles'; const SecondaryColumn = ({ pageData }: { pageData: Article }) => { const latestMediaContent = pageData?.secondaryColumn?.latestMedia; if (!latestMediaContent) return null; + const { isLite } = use(RequestContext); return (
    {latestMediaContent && (
    - +
    )}
    From 1425a861cac3772eefff126fa9f86b530a197f8b Mon Sep 17 00:00:00 2001 From: hotinglok Date: Thu, 11 Dec 2025 10:03:06 +0000 Subject: [PATCH 20/96] Add stories, add isLite condition to LatestMediaSection --- data/mundo/homePage/index.json | 14 ----- data/persian/homePage/index.json | 54 +++++++++++++++++++ data/pidgin/articles/cw0x29n2pvqo.json | 14 +++++ data/pidgin/topics/c95y35941vrt.json | 16 ++++++ .../components/Billboard/index.stories.tsx | 21 ++++++++ .../Curation/CurationPromo/index.stories.tsx | 5 ++ .../Curation/HierarchicalGrid/fixtures.js | 16 ++++++ .../LatestMediaSection/index.stories.tsx | 2 +- .../LatestMediaSection/index.tsx | 4 +- .../LatestMediaSection/types.ts | 3 +- 10 files changed, 132 insertions(+), 17 deletions(-) diff --git a/data/mundo/homePage/index.json b/data/mundo/homePage/index.json index f67a69ff2d1..2f2eb2f647f 100644 --- a/data/mundo/homePage/index.json +++ b/data/mundo/homePage/index.json @@ -37,20 +37,6 @@ "description": "La superficie del mar alcanzó un nuevo récord de temperatura debido a un aumento que nunca se había producido de manera tan brusca.", "imageAlt": "Sol sobre el océano", "id": "631efe72-a2bc-4a61-98f5-97ca8d85ba89" - }, - { - "type": "video", - "duration": "PT1M56S", - "isLive": false, - "title": "«واکنش به ربات‌نماهای کیش؛ از «آبروریزی جهانی» تا «بچه رباط کریم", - "firstPublished": "2025-11-16T20:29:46.464Z", - "lastPublished": "2025-11-16T20:29:46.464Z", - "link": "https://www.bbc.com/persian/articles/cn7e3651ydxo", - "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dffd/live/936d9340-c32a-11f0-ae46-bd64331f0fd4.jpg.webp", - "description": "- «من یک داده هستم» ویدیوهای زن و مرد ربات‌نما در نمایشگاه فناوری کیش اینوکس در شبکه‌های اجتماعی ایران ترند شده است؛ تصاویری که ابتدا با تیترهای غلط‌انداز منتشر شد و در دو سه روز گذشته واکنش‌های بسیاری برانگیخته است که بیشتر آنها انتقادی یا طنزآلود است.", - "imageAlt": "زن و مرد با گریم ربات در نمایشگاه فناوری کیش", - "isPortraitImage": true, - "id": "cn7e3651ydxo" } ], "activePage": 1, diff --git a/data/persian/homePage/index.json b/data/persian/homePage/index.json index 6250b6370a3..e0205a914f6 100644 --- a/data/persian/homePage/index.json +++ b/data/persian/homePage/index.json @@ -148,6 +148,60 @@ "title": "پوشش ویژه", "visualProminence": "MAXIMUM", "visualStyle": "BANNER" + }, + { + "summaries": [ + { + "type": "video", + "duration": "PT1M56S", + "isLive": false, + "title": "«واکنش به ربات‌نماهای کیش؛ از «آبروریزی جهانی» تا «بچه رباط کریم", + "firstPublished": "2025-11-16T20:29:46.464Z", + "lastPublished": "2025-11-16T20:29:46.464Z", + "link": "https://www.bbc.com/persian/articles/cn7e3651ydxo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dffd/live/936d9340-c32a-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "- «من یک داده هستم» ویدیوهای زن و مرد ربات‌نما در نمایشگاه فناوری کیش اینوکس در شبکه‌های اجتماعی ایران ترند شده است؛ تصاویری که ابتدا با تیترهای غلط‌انداز منتشر شد و در دو سه روز گذشته واکنش‌های بسیاری برانگیخته است که بیشتر آنها انتقادی یا طنزآلود است.", + "imageAlt": "زن و مرد با گریم ربات در نمایشگاه فناوری کیش", + "isPortraitImage": true, + "id": "cn7e3651ydxo" + }, + { + "type": "video", + "duration": "PT1M24S", + "isLive": false, + "title": "نمایشگاه «هفته دیزاین» در دانشگاه تهران با اعتراض بسیج دانشجویی لغو شد", + "firstPublished": "2025-11-16T15:29:22.961Z", + "lastPublished": "2025-11-16T15:29:22.961Z", + "link": "https://www.bbc.com/persian/articles/c4gk12nnye0o", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/a02c/live/7e65c320-c300-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "روابط‌عمومی دانشگاه تهران از لغو دو روز باقیمانده از نمایشگاه رویداد «هفته طراحی تهران» در دانشکده هنرهای زیبای این دانشگاه خبر داد. در پی پربازدید شدن ویدیوهایی از این رویداد و حضور بازدیدکنندگان مشتاق، بسیج دانشجویی این دانشگاه بیانیه‌ای اعتراضی صادر کرد. کمیته برگزاری این رویداد در اطلاعیه‌ای با تائید لغو نمایشگاه، به «نگرانی مسئولان دانشگاه از انضباط برگزاری و امنیت مهمانان» اشاره کرده است. در اطلاعیه دانشگاه تهران به «استقبال فراتر از تصور» و نگرانی از آسیب به بازدیدکنندگان بر اثر تراکم جمعیت اشاره شده است. رویداد «هفته دیزاین تهران» از ۲۰ تا ۲۶ آبان در نقاط مختلفی از پایتخت ایران از جمله در نمایشگاه بین‌المللی تهران، چندین گالری و موسسه هنری و فضاهای شهری در حال برگزاری است. دانشگاه تهران در بیانیه خود به ضرورت «نمایش فضای عادی زندگی اجتماعی پس از جنگ تحمیلی ۱۲ روزه» تاکید شده ولی آمده است که تراکم جمعیت بازدیدکنندگان، ممکن است آسیب‌های ایمنی مانند برق‌گرفتگی ایجاد کند. در این بیانیه همچنین به ورود افرادی غیر دانشجو و «عدم رعایت شئون دانشگاه و جامعه» اشاره شده است. بسیج دانشجویی پردیس هنرهای زیبا با انتشار بیانیه‌ای اعتراضی که خبرگزاری‌های منتقد دولت همچون فارس آن را پوشش دادند، این رویداد را «نماد افت استانداردهای علمی و انضباطی» در دانشگاه دانست. چندی پیش هم پربازدید شدن ویدیوهای حضور علاقمندان به اجرای خیابانی یک گروه راک در مرکز تهران، منجر به اعمال محدودیت بر اعضا و بسته شدن اینستاگرام آنها شد.", + "imageAlt": "پوستر هفته دیزاین تهران", + "isPortraitImage": true, + "id": "c4gk12nnye0o" + }, + { + "type": "video", + "duration": "PT20S", + "isLive": false, + "title": "ترکش‌های بوسه بدون رضایت؛ «عموی تنی» روبیالس به او تخم مرغ پرتاب کرد", + "firstPublished": "2025-11-14T19:42:34.380Z", + "lastPublished": "2025-11-14T19:42:34.380Z", + "link": "https://www.bbc.com/persian/articles/c9v19erz9rdo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/42f8/live/74262140-c191-11f0-8669-5560f5c90fbe.jpg.webp", + "description": "لوئیس روبیالس، رئیس پیشین فدراسیون فوتبال اسپانیا، ۱۳ نوامبر، ۲۲ آبان در مراسم رونمایی از کتاب جدیدش با عنوان «کشتن روبیالس» در مادرید، هدف پرتاب تخم‌مرغ قرار گرفت. او گفت این حمله به‌دست عمویش انجام شده است. روبیالس توضیح داد که ابتدا تصور کرده عمویش اسلحه دارد.", + "imageAlt": "لوئیس روبیالس", + "isPortraitImage": true, + "id": "c9v19erz9rdo" + } + ], + "activePage": 1, + "pageCount": 1, + "curationId": "urn:bbc:tipo:list:c9b24e64-2271-4ec0-a985-71f85efa2677", + "curationType": "tipo-curation", + "position": 3, + "title": "پوشش ویژه", + "visualProminence": "MAXIMUM", + "visualStyle": "BANNER" } ], "metadata": { diff --git a/data/pidgin/articles/cw0x29n2pvqo.json b/data/pidgin/articles/cw0x29n2pvqo.json index a10f5ec48c4..466327a87b8 100644 --- a/data/pidgin/articles/cw0x29n2pvqo.json +++ b/data/pidgin/articles/cw0x29n2pvqo.json @@ -7096,6 +7096,20 @@ "imageUrl": "https://ichef.bbci.co.uk/ace/standard/{width}/cpsprodpb/1a8b/live/c4ce3eb0-98a5-11ed-86bf-4b2f5da2cf01.jpg", "description": "Professor Mahmood Yakubu speak wit BBC on Inec plans to conduct free and fair elections.", "imageAlt": "Inec boss Prof. Mahmood Yakubu" + }, + { + "id": "ce3zyp0z93do", + "type": "video", + "duration": "PT5M21S", + "isLive": false, + "title": "'Evribodi dey negotiate wit outlaws, including America' - Sheikh Gumi", + "firstPublished": "2025-12-09T13:55:32.118Z", + "lastPublished": "2025-12-09T13:55:32.118Z", + "link": "https://www.bbc.com/pidgin/articles/ce3zyp0z93do", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/a5fb/live/a7edb960-d506-11f0-8c06-f5d460985095.jpg.webp", + "imageAlt": "Sheik Ahmad Gumi", + "isPortraitImage": true, + "readTime": 1 } ] } diff --git a/data/pidgin/topics/c95y35941vrt.json b/data/pidgin/topics/c95y35941vrt.json index 38af8998ad2..27739874d3e 100644 --- a/data/pidgin/topics/c95y35941vrt.json +++ b/data/pidgin/topics/c95y35941vrt.json @@ -245,6 +245,22 @@ "description": "Users of Donald Trump new website go dey able to like posts - and also share them on Twitter and Facebook accounts.", "imageAlt": "Trump", "id": "682b5bcd-0dfd-4e9f-b1a0-14dfe206df83" + }, + { + "type": "video", + "duration": "PT5M21S", + "isLive": false, + "title": + "'Evribodi dey negotiate wit outlaws, including America' - Sheikh Gumi", + "firstPublished": "2025-12-09T13:55:32.118Z", + "lastPublished": "2025-12-09T13:55:32.118Z", + "link": "https://www.bbc.com/pidgin/articles/ce3zyp0z93do", + "imageUrl": + "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/a5fb/live/a7edb960-d506-11f0-8c06-f5d460985095.jpg.webp", + "imageAlt": "Sheik Ahmad Gumi", + "isPortraitImage": true, + "id": "ce3zyp0z93do", + "readTime": 1 } ], "activePage": 1, diff --git a/src/app/components/Billboard/index.stories.tsx b/src/app/components/Billboard/index.stories.tsx index 99e78002c90..950e5a7bcdf 100644 --- a/src/app/components/Billboard/index.stories.tsx +++ b/src/app/components/Billboard/index.stories.tsx @@ -155,3 +155,24 @@ export const PersianBillboard = () => {
    ); }; + +export const PersianBillboardWithPVPromos = () => { + const summary = persianData.data.curations[2].summaries[0]; + return ( +
    + + + + + +
    + ); +}; + diff --git a/src/app/components/Curation/CurationPromo/index.stories.tsx b/src/app/components/Curation/CurationPromo/index.stories.tsx index 6530ef2bf0e..aadc03cdad4 100644 --- a/src/app/components/Curation/CurationPromo/index.stories.tsx +++ b/src/app/components/Curation/CurationPromo/index.stories.tsx @@ -28,6 +28,11 @@ const WithMediaIndicator = () => { type={MEDIA_TYPES.PHOTO_GALLERY} duration={123} /> +
    ); }; diff --git a/src/app/components/Curation/HierarchicalGrid/fixtures.js b/src/app/components/Curation/HierarchicalGrid/fixtures.js index 8639e4ffd8f..a8ff4ffe6c8 100644 --- a/src/app/components/Curation/HierarchicalGrid/fixtures.js +++ b/src/app/components/Curation/HierarchicalGrid/fixtures.js @@ -283,4 +283,20 @@ export const pidginPromosWithMedia = [ imageAlt: 'NYSC corps members', id: 'cgm1ekn44p1o', }, + { + type: 'video', + duration: 'PT5M21S', + isLive: false, + title: + "'Evribodi dey negotiate wit outlaws, including America' - Sheikh Gumi", + firstPublished: '2025-12-09T13:55:32.118Z', + lastPublished: '2025-12-09T13:55:32.118Z', + link: 'https://www.bbc.com/pidgin/articles/ce3zyp0z93do', + imageUrl: + 'https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/a5fb/live/a7edb960-d506-11f0-8c06-f5d460985095.jpg.webp', + imageAlt: 'Sheik Ahmad Gumi', + isPortraitImage: true, + id: 'ce3zyp0z93do', + readTime: 1, + }, ]; diff --git a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/index.stories.tsx b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/index.stories.tsx index 05c03833b3c..38ec9bca463 100644 --- a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/index.stories.tsx +++ b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/index.stories.tsx @@ -38,7 +38,7 @@ const Component = ({ > - + diff --git a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/index.tsx b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/index.tsx index 637f23fd621..fdad71240e4 100644 --- a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/index.tsx +++ b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/index.tsx @@ -14,7 +14,7 @@ const LatestMediaSection = ({ isLite, }: { content: LatestMedia[] | null; - isLite: boolean; + isLite?: boolean; }) => { const { service, dir, translations, script } = use(ServiceContext); @@ -71,6 +71,8 @@ const LatestMediaSection = ({ ariaLabelledBy={ariaLabelledBy} ref={viewTracker} eventTrackingData={eventTrackingData} + isLite={isLite} + isPortraitImage={singleItem.isPortraitImage} />
    ) : ( diff --git a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/types.ts b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/types.ts index 71985a21ca4..9c39ab366b4 100644 --- a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/types.ts +++ b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/types.ts @@ -20,7 +20,8 @@ export type LatestMediaItemProp = { ariaLabelledBy: string; ref: () => Promise; eventTrackingData: EventTrackingBlock; - isLite: boolean; + isPortraitImage?: boolean; + isLite?: boolean; }; export type ImageProp = { From 12cb6ba5fb2f837b73a9aafed66e5c245f221922 Mon Sep 17 00:00:00 2001 From: hotinglok Date: Thu, 11 Dec 2025 10:11:25 +0000 Subject: [PATCH 21/96] Add aria-hidden to BlurredBackground --- src/app/components/Image/BlurredBackground.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/components/Image/BlurredBackground.tsx b/src/app/components/Image/BlurredBackground.tsx index 3422afa2f58..3e750b66c52 100644 --- a/src/app/components/Image/BlurredBackground.tsx +++ b/src/app/components/Image/BlurredBackground.tsx @@ -8,6 +8,7 @@ const BlurredBackground = ({ src }: BlurredBackgroundProps) => { const lowResImageSrc = src?.replace('{width}', '10'); return (
  • {jobRole ? (
  • diff --git a/src/app/pages/ArticlePage/fixtureData.ts b/src/app/pages/ArticlePage/fixtureData.ts index b68f5ae6bf5..e8907b92beb 100644 --- a/src/app/pages/ArticlePage/fixtureData.ts +++ b/src/app/pages/ArticlePage/fixtureData.ts @@ -3415,6 +3415,181 @@ export const bylineWithMultipleContributors = [ }, ] as OptimoBylineContributorBlock[]; +export const bylineWithMultipleContributorsNoRole = [ + { + type: 'contributor', + model: { + topicId: '', + topicUrl: '/news/topics/c8qx38nq177t', + blocks: [ + { + type: 'name', + model: { + blocks: [ + { + type: 'text', + model: { + blocks: [ + { + type: 'paragraph', + model: { + text: 'Mayeni Jones', + blocks: [ + { + type: 'fragment', + model: { + text: 'Mayeni Jones', + attributes: [], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + { + type: 'location', + model: { + blocks: [ + { + type: 'text', + model: { + blocks: [ + { + type: 'paragraph', + model: { + text: 'Lagos, Nigeria', + blocks: [ + { + type: 'fragment', + model: { + text: 'Lagos, Nigeria', + attributes: [], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + { + type: 'contributor', + model: { + topicId: '', + topicUrl: '/news/topics/c8qx38nq177t', + blocks: [ + { + type: 'name', + model: { + blocks: [ + { + type: 'text', + model: { + blocks: [ + { + type: 'paragraph', + model: { + text: 'Mayeni Jones', + blocks: [ + { + type: 'fragment', + model: { + text: 'Mayeni Jones', + attributes: [], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + { + type: 'role', + model: { + blocks: [ + { + type: 'text', + model: { + blocks: [ + { + type: 'paragraph', + model: { + text: 'Journalist', + blocks: [ + { + type: 'fragment', + model: { + text: 'Journalist', + attributes: [], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + { + type: 'contributor', + model: { + topicId: '', + topicUrl: '/news/topics/c8qx38nq177t', + blocks: [ + { + type: 'name', + model: { + blocks: [ + { + type: 'text', + model: { + blocks: [ + { + type: 'paragraph', + model: { + text: 'Mayeni Jones', + blocks: [ + { + type: 'fragment', + model: { + text: 'Mayeni Jones', + attributes: [], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, +] as OptimoBylineContributorBlock[]; + export const sampleRecommendations = [ { locators: { From 30f934f36fa1e163ce7ae3b6bf5b4b44e870077d Mon Sep 17 00:00:00 2001 From: skumid01 Date: Thu, 18 Dec 2025 16:26:00 +0200 Subject: [PATCH 36/96] added route for homepages as well as condition for page type header --- .../pages/[service]/[[...]].page.tsx | 2 + .../homepages/handleHomepageRoute.ts | 112 ++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts diff --git a/ws-nextjs-app/pages/[service]/[[...]].page.tsx b/ws-nextjs-app/pages/[service]/[[...]].page.tsx index 89232ed9889..ead10437203 100644 --- a/ws-nextjs-app/pages/[service]/[[...]].page.tsx +++ b/ws-nextjs-app/pages/[service]/[[...]].page.tsx @@ -10,6 +10,7 @@ import { CORRESPONDENT_STORY_PAGE, MEDIA_ASSET_PAGE, PHOTO_GALLERY_PAGE, + HOME_PAGE, } from '#app/routes/utils/pageTypes'; import { PageTypes } from '#app/models/types/global'; import PageDataParams from '#app/models/types/pageDataParams'; @@ -24,6 +25,7 @@ import { AvEmbedsPageProps } from './av-embeds/types'; // Articles (Optimo + CPS) import handleArticleRoute from './articles/handleArticleRoute'; import { ArticlePageProps } from './articles/types'; +import handleHomepageRoute from './homepages/handleHomepageRoute'; // Dynamic imports of page layouts const AvEmbedsPageLayout = dynamic( diff --git a/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts new file mode 100644 index 00000000000..ec18aa4ca49 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts @@ -0,0 +1,112 @@ +import { GetServerSidePropsContext } from 'next'; +import extractHeaders from '#server/utilities/extractHeaders'; +import { HOME_PAGE } from '#app/routes/utils/pageTypes'; +import parseRoute from '#app/routes/utils/parseRoute'; +import nodeLogger from '#lib/logger.node'; +import { OK } from '#app/lib/statusCodes.const'; +import { ROUTING_INFORMATION } from '#app/lib/logger.const'; +import getPathExtension from '#app/utilities/getPathExtension'; +import PageDataParams from '#app/models/types/pageDataParams'; +import handleError from '#app/routes/utils/handleError'; +import { getServerExperiments } from '#server/utilities/experimentHeader'; +import shouldRender from '../articles/shouldRender'; +import getPageData from '../../../utilities/pageRequests/getPageData'; + +const logger = nodeLogger(__filename); + +export default async (context: GetServerSidePropsContext) => { + const { + resolvedUrl, + req: { headers: reqHeaders }, + } = context; + + const { service, renderer_env: rendererEnv } = + context.query as PageDataParams; + + const resolvedUrlWithoutQuery = resolvedUrl.split('?')?.[0]; + + const { isAmp, isApp, isLite } = getPathExtension(resolvedUrlWithoutQuery); + const { variant } = parseRoute(resolvedUrl); + + const { data, toggles } = await getPageData({ + id: resolvedUrlWithoutQuery, + service, + variant: variant || undefined, + rendererEnv, + resolvedUrl: resolvedUrlWithoutQuery, + pageType: HOME_PAGE, + isAmp, + }); + + const { pageData, status } = data; + + context.res.statusCode = status; + + let routingInfoLogger = logger.debug; + + const { hasRequestSucceeded, status: renderStatus } = shouldRender( + { pageData, status }, + service, + ); + + if (!hasRequestSucceeded && renderStatus !== OK) { + routingInfoLogger = logger.error; + + return { + props: { + isApp, + isAmp, + isLite, + isNextJs: true, + service, + status: renderStatus, + timeOnServer: Date.now(), + variant: variant || null, + pageType: HOME_PAGE, + pathname: resolvedUrlWithoutQuery, + toggles, + ...extractHeaders(reqHeaders), + }, + }; + } + + if (!pageData) { + throw handleError('HomePage data is malformed', 500); + } + + context.res.setHeader( + 'Cache-Control', + 'public, stale-if-error=90, stale-while-revalidate=30, max-age=60', + ); + + routingInfoLogger(ROUTING_INFORMATION, { + url: resolvedUrlWithoutQuery, + status, + pageType: HOME_PAGE, + }); + + const serverSideExperiments = getServerExperiments({ + headers: reqHeaders, + service, + pageType: HOME_PAGE, + }); + + return { + props: { + id: resolvedUrlWithoutQuery, + isAmp, + isApp, + isLite, + isNextJs: true, + pageData, + pageType: HOME_PAGE, + pathname: resolvedUrlWithoutQuery, + serverSideExperiments, + service, + status, + toggles, + variant: variant || null, + ...extractHeaders(reqHeaders), + }, + }; +}; From c10fac29b84be07e73113203026222942683e44e Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Thu, 11 Dec 2025 11:41:25 +0000 Subject: [PATCH 37/96] added unit tests --- .../homepages/handleHomepageRoute.test.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.test.ts diff --git a/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.test.ts b/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.test.ts new file mode 100644 index 00000000000..8fdaf6b6de6 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.test.ts @@ -0,0 +1,111 @@ +import { GetServerSidePropsContext } from 'next'; +import defaultToggles from '#app/lib/config/toggles'; +import pidginHomepageFixtureData from '#data/pidgin/homePage/index.json'; +import * as shouldRender from '../articles/shouldRender'; +import * as getPageDataModule from '../../../utilities/pageRequests/getPageData'; +import handleHomepageRoute from './handleHomepageRoute'; + +jest.mock('../../../utilities/pageRequests/getPageData'); +jest.mock('../articles/shouldRender', () => { + const originalModule = jest.requireActual('../articles/shouldRender'); + return { + __esModule: true, + ...originalModule, + }; +}); + +describe('handleHomepageRoute', () => { + const mockSetHeader = jest.fn(); + const toggles = defaultToggles.local; + + const mockGetServerSidePropsContext = { + req: { + headers: {}, + } as unknown as GetServerSidePropsContext['req'], + res: { + setHeader: mockSetHeader, + removeHeader: jest.fn(), + } as unknown as GetServerSidePropsContext['res'], + resolvedUrl: '/pidgin', + query: { service: 'pidgin' }, + } satisfies GetServerSidePropsContext; + + beforeEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + jest.spyOn(getPageDataModule, 'default').mockResolvedValue({ + data: { + pageData: pidginHomepageFixtureData.data, + status: 200, + }, + toggles, + }); + }); + + it('returns expected props if shouldRender succeeds', async () => { + jest.spyOn(Date, 'now').mockImplementation(() => 1234567890000); + + const result = await handleHomepageRoute(mockGetServerSidePropsContext); + + expect(result.props.status).toEqual(200); + expect(result.props.pageType).toEqual('home'); + }); + + it('returns error props if shouldRender fails - 500', async () => { + jest.spyOn(shouldRender, 'default').mockReturnValue({ + hasRequestSucceeded: false, + status: 500, + }); + + jest.spyOn(Date, 'now').mockImplementation(() => 1234567890000); + + const result = await handleHomepageRoute(mockGetServerSidePropsContext); + + expect(result).toEqual({ + props: expect.objectContaining({ + status: 500, + pageType: 'home', + pathname: '/pidgin', + }), + }); + }); + + it('returns error props if shouldRender fails - 404', async () => { + jest.spyOn(shouldRender, 'default').mockReturnValue({ + hasRequestSucceeded: false, + status: 404, + }); + + jest.spyOn(Date, 'now').mockImplementation(() => 1234567890000); + + const result = await handleHomepageRoute(mockGetServerSidePropsContext); + + expect(result).toEqual({ + props: expect.objectContaining({ + status: 404, + pageType: 'home', + pathname: '/pidgin', + }), + }); + }); + + it('throws if pageData is missing', async () => { + jest.spyOn(getPageDataModule, 'default').mockResolvedValue({ + data: { pageData: null, status: 200 }, + toggles, + }); + + await expect( + handleHomepageRoute(mockGetServerSidePropsContext), + ).rejects.toThrow('HomePage data is malformed'); + }); + + it('sets correct cache-control header', async () => { + await handleHomepageRoute(mockGetServerSidePropsContext); + + expect(mockSetHeader).toHaveBeenCalledWith( + 'Cache-Control', + expect.stringContaining('max-age=60'), + ); + }); +}); From fcec076f8da81fea69c18f41e901974528ec7bb4 Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Fri, 12 Dec 2025 12:20:21 +0000 Subject: [PATCH 38/96] updated structure of data fetch --- .../pages/[service]/homepages/handleHomepageRoute.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts index ec18aa4ca49..69a7db99666 100644 --- a/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts +++ b/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts @@ -98,7 +98,14 @@ export default async (context: GetServerSidePropsContext) => { isApp, isLite, isNextJs: true, - pageData, + pageData: { + title: pageData.title, + seoTitle: pageData.seoTitle, + metadata: { ...pageData.metadata, type: HOME_PAGE }, + curations: pageData.curations, + description: pageData.description, + seoDescription: pageData.seoDescription, + }, pageType: HOME_PAGE, pathname: resolvedUrlWithoutQuery, serverSideExperiments, From 3e72ac6844f68707e950fd600812ce6e830c31bf Mon Sep 17 00:00:00 2001 From: skumid01 Date: Thu, 18 Dec 2025 16:27:13 +0200 Subject: [PATCH 39/96] called Homepage component to render it --- ws-nextjs-app/pages/[service]/[[...]].page.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/[[...]].page.tsx b/ws-nextjs-app/pages/[service]/[[...]].page.tsx index ead10437203..fea845f77fe 100644 --- a/ws-nextjs-app/pages/[service]/[[...]].page.tsx +++ b/ws-nextjs-app/pages/[service]/[[...]].page.tsx @@ -16,6 +16,7 @@ import { PageTypes } from '#app/models/types/global'; import PageDataParams from '#app/models/types/pageDataParams'; import deriveVariant from '#nextjs/utilities/deriveVariant'; import withOptimizelyProvider from '#app/legacy/containers/PageHandlers/withOptimizelyProvider'; +import { HomePageProps } from '#app/pages/HomePage/HomePage'; import { getEnvConfig } from '#app/lib/utilities/getEnvConfig'; import derivePageType from '#nextjs/utilities/derivePageType'; @@ -25,7 +26,7 @@ import { AvEmbedsPageProps } from './av-embeds/types'; // Articles (Optimo + CPS) import handleArticleRoute from './articles/handleArticleRoute'; import { ArticlePageProps } from './articles/types'; -import handleHomepageRoute from './homepages/handleHomepageRoute'; +import handleHomepageRoute from './homepage/handleHomepageRoute'; // Dynamic imports of page layouts const AvEmbedsPageLayout = dynamic( @@ -35,6 +36,7 @@ const ArticlePage = dynamic(() => import('#app/pages/ArticlePage/ArticlePage')); const MediaArticlePage = dynamic( () => import('#app/pages/MediaArticlePage/MediaArticlePage'), ); +const HomePage = dynamic(() => import('#app/pages/HomePage/HomePage')); const getPageType = ({ resolvedUrl, @@ -67,6 +69,7 @@ const getPageType = ({ const ROUTE_HANDLERS = { [AV_EMBEDS]: handleAvRoute, [ARTICLE_PAGE]: handleArticleRoute, + [HOME_PAGE]: handleHomepageRoute, }; export const getServerSideProps: GetServerSideProps = async context => { @@ -104,7 +107,8 @@ export const getServerSideProps: GetServerSideProps = async context => { type PageProps = { pageType?: PageTypes; } & AvEmbedsPageProps & - ArticlePageProps; + ArticlePageProps & + HomePageProps; export default function PageTypeToRender({ pageType, ...props }: PageProps) { switch (pageType) { @@ -120,8 +124,10 @@ export default function PageTypeToRender({ pageType, ...props }: PageProps) { // Media Article Pages (CPS + Legacy TC2 assets) case MEDIA_ASSET_PAGE: return ; + case HOME_PAGE: + return ; default: // Return nothing, 404 is handled in _app.tsx return null; } -} +} \ No newline at end of file From e1be75983d0592d6d74249c049c2928538ed4205 Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Mon, 15 Dec 2025 10:59:04 +0000 Subject: [PATCH 40/96] renamed folder to homepage and homepage return in derived page type --- .../{homepages => homepage}/handleHomepageRoute.test.ts | 0 .../{homepages => homepage}/handleHomepageRoute.ts | 0 ws-nextjs-app/utilities/derivePageType/index.ts | 8 ++++++++ 3 files changed, 8 insertions(+) rename ws-nextjs-app/pages/[service]/{homepages => homepage}/handleHomepageRoute.test.ts (100%) rename ws-nextjs-app/pages/[service]/{homepages => homepage}/handleHomepageRoute.ts (100%) diff --git a/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.test.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts similarity index 100% rename from ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.test.ts rename to ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts diff --git a/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts similarity index 100% rename from ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts rename to ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts diff --git a/ws-nextjs-app/utilities/derivePageType/index.ts b/ws-nextjs-app/utilities/derivePageType/index.ts index d0441759b37..0e02b213170 100644 --- a/ws-nextjs-app/utilities/derivePageType/index.ts +++ b/ws-nextjs-app/utilities/derivePageType/index.ts @@ -5,12 +5,19 @@ import { DOWNLOADS_PAGE, LIVE_PAGE, UGC_PAGE, + HOME_PAGE, } from '#app/routes/utils/pageTypes'; import { isOptimoIdCheck, isCpsIdCheck, removeRendererExtension, } from '#app/routes/utils/constructPageFetchUrl'; +import SERVICES from '#app/lib/config/services'; + +const isHomePagePath = (pathname: string) => + SERVICES.some( + service => pathname === `/${service}` || pathname === `/${service}/`, + ); export default function derivePageType( pathname: string, @@ -20,6 +27,7 @@ export default function derivePageType( 'http://bbc.com', ).pathname; + if (isHomePagePath(sanitisedPathname)) return HOME_PAGE; if (sanitisedPathname.includes('live')) return LIVE_PAGE; if (sanitisedPathname.includes('send')) return UGC_PAGE; if (sanitisedPathname.includes('av-embeds')) return AV_EMBEDS; From e1a633b8ca93e3f18f1b99785cabd7d8a96f8dce Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Mon, 15 Dec 2025 11:20:03 +0000 Subject: [PATCH 41/96] removed toggles --- .../pages/[service]/homepage/handleHomepageRoute.test.ts | 5 ----- .../pages/[service]/homepage/handleHomepageRoute.ts | 4 +--- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts index 8fdaf6b6de6..57d30c187db 100644 --- a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts +++ b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts @@ -1,5 +1,4 @@ import { GetServerSidePropsContext } from 'next'; -import defaultToggles from '#app/lib/config/toggles'; import pidginHomepageFixtureData from '#data/pidgin/homePage/index.json'; import * as shouldRender from '../articles/shouldRender'; import * as getPageDataModule from '../../../utilities/pageRequests/getPageData'; @@ -16,8 +15,6 @@ jest.mock('../articles/shouldRender', () => { describe('handleHomepageRoute', () => { const mockSetHeader = jest.fn(); - const toggles = defaultToggles.local; - const mockGetServerSidePropsContext = { req: { headers: {}, @@ -38,7 +35,6 @@ describe('handleHomepageRoute', () => { pageData: pidginHomepageFixtureData.data, status: 200, }, - toggles, }); }); @@ -92,7 +88,6 @@ describe('handleHomepageRoute', () => { it('throws if pageData is missing', async () => { jest.spyOn(getPageDataModule, 'default').mockResolvedValue({ data: { pageData: null, status: 200 }, - toggles, }); await expect( diff --git a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts index 69a7db99666..4f9ffa671a4 100644 --- a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts +++ b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts @@ -28,7 +28,7 @@ export default async (context: GetServerSidePropsContext) => { const { isAmp, isApp, isLite } = getPathExtension(resolvedUrlWithoutQuery); const { variant } = parseRoute(resolvedUrl); - const { data, toggles } = await getPageData({ + const { data } = await getPageData({ id: resolvedUrlWithoutQuery, service, variant: variant || undefined, @@ -64,7 +64,6 @@ export default async (context: GetServerSidePropsContext) => { variant: variant || null, pageType: HOME_PAGE, pathname: resolvedUrlWithoutQuery, - toggles, ...extractHeaders(reqHeaders), }, }; @@ -111,7 +110,6 @@ export default async (context: GetServerSidePropsContext) => { serverSideExperiments, service, status, - toggles, variant: variant || null, ...extractHeaders(reqHeaders), }, From d19802998569dc7cf60ec00e4569b1f5d41332d0 Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Mon, 15 Dec 2025 12:16:58 +0000 Subject: [PATCH 42/96] added variant support to derivePageType utility [copilot] --- .../[service]/homepage/handleHomepageRoute.ts | 10 ++++---- .../utilities/derivePageType/index.ts | 25 ++++++++++++++++--- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts index 4f9ffa671a4..17047be8066 100644 --- a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts +++ b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts @@ -98,12 +98,12 @@ export default async (context: GetServerSidePropsContext) => { isLite, isNextJs: true, pageData: { - title: pageData.title, - seoTitle: pageData.seoTitle, + title: pageData.title ?? null, + seoTitle: pageData.seoTitle ?? null, metadata: { ...pageData.metadata, type: HOME_PAGE }, - curations: pageData.curations, - description: pageData.description, - seoDescription: pageData.seoDescription, + curations: pageData.curations ?? null, + description: pageData.description ?? null, + seoDescription: pageData.seoDescription ?? null, }, pageType: HOME_PAGE, pathname: resolvedUrlWithoutQuery, diff --git a/ws-nextjs-app/utilities/derivePageType/index.ts b/ws-nextjs-app/utilities/derivePageType/index.ts index 0e02b213170..936eaecf4ca 100644 --- a/ws-nextjs-app/utilities/derivePageType/index.ts +++ b/ws-nextjs-app/utilities/derivePageType/index.ts @@ -14,10 +14,29 @@ import { } from '#app/routes/utils/constructPageFetchUrl'; import SERVICES from '#app/lib/config/services'; +const SERVICES_WITH_VARIANTS = { + serbian: ['lat', 'cyr'], + ukchina: ['simp', 'trad'], + uzbek: ['lat', 'cyr'], + zhongwen: ['simp', 'trad'], + ukrainian: ['lat', 'cyr'], +}; + const isHomePagePath = (pathname: string) => - SERVICES.some( - service => pathname === `/${service}` || pathname === `/${service}/`, - ); + SERVICES.some(service => { + if (pathname === `/${service}` || pathname === `/${service}/`) { + return true; + } + const variants = SERVICES_WITH_VARIANTS[service]; + if (variants) { + return variants.some( + variant => + pathname === `/${service}/${variant}` || + pathname === `/${service}/${variant}/`, + ); + } + return false; + }); export default function derivePageType( pathname: string, From 2cf4cdf553c7ac5e70521335d38a3eaf7cffe7d5 Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Mon, 15 Dec 2025 12:25:04 +0000 Subject: [PATCH 43/96] added unit tests for homepage derived type --- .../utilities/derivePageType/index.test.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ws-nextjs-app/utilities/derivePageType/index.test.ts b/ws-nextjs-app/utilities/derivePageType/index.test.ts index 6259b26a06b..6ebcb6ed3ef 100644 --- a/ws-nextjs-app/utilities/derivePageType/index.test.ts +++ b/ws-nextjs-app/utilities/derivePageType/index.test.ts @@ -1,4 +1,4 @@ -import { LIVE_PAGE, UGC_PAGE } from '#app/routes/utils/pageTypes'; +import { LIVE_PAGE, UGC_PAGE, HOME_PAGE } from '#app/routes/utils/pageTypes'; import derivePageType from '.'; describe('derivePageType', () => { @@ -25,4 +25,15 @@ describe('derivePageType', () => { const result = derivePageType(pathname); expect(result).toEqual(LIVE_PAGE); }); + it('should return HOME_PAGE for a base service homepage', () => { + const pathname = '/pidgin'; + const result = derivePageType(pathname); + expect(result).toEqual(HOME_PAGE); + }); + + it('should return HOME_PAGE for a service variant homepage', () => { + const pathname = '/serbian/lat'; + const result = derivePageType(pathname); + expect(result).toEqual(HOME_PAGE); + }); }); From dc4d691beaac360de7e32352b25222235e8ad396 Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Mon, 15 Dec 2025 12:31:09 +0000 Subject: [PATCH 44/96] removed unneccessary keys --- .../pages/[service]/homepage/handleHomepageRoute.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts index 17047be8066..8289a3dbc46 100644 --- a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts +++ b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts @@ -93,10 +93,6 @@ export default async (context: GetServerSidePropsContext) => { return { props: { id: resolvedUrlWithoutQuery, - isAmp, - isApp, - isLite, - isNextJs: true, pageData: { title: pageData.title ?? null, seoTitle: pageData.seoTitle ?? null, @@ -111,7 +107,6 @@ export default async (context: GetServerSidePropsContext) => { service, status, variant: variant || null, - ...extractHeaders(reqHeaders), }, }; }; From 48e0e4cdc1c689fc165b918e9abb90fec912fb27 Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Mon, 15 Dec 2025 12:59:06 +0000 Subject: [PATCH 45/96] moved shouldRender function into utilites folder and updated imports --- .../pages/[service]/articles/handleArticleRoute.test.ts | 2 +- ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts | 2 +- .../pages/[service]/homepage/handleHomepageRoute.test.ts | 2 +- ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts | 2 +- .../[service]/articles => utilities}/shouldRender/index.test.ts | 0 .../[service]/articles => utilities}/shouldRender/index.ts | 0 6 files changed, 4 insertions(+), 4 deletions(-) rename ws-nextjs-app/{pages/[service]/articles => utilities}/shouldRender/index.test.ts (100%) rename ws-nextjs-app/{pages/[service]/articles => utilities}/shouldRender/index.ts (100%) diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts index 3e3688b6f2e..777b9c9f13a 100644 --- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts +++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts @@ -1,6 +1,6 @@ import pidginMediaArticleFixtureData from '#data/pidgin/articles/cvpde7nqj92o.json'; import { GetServerSidePropsContext } from 'next'; -import * as shouldRender from './shouldRender'; +import * as shouldRender from '../../../utilities/shouldRender'; import * as getPageDataModule from '../../../utilities/pageRequests/getPageData'; import handleArticleRoute from './handleArticleRoute'; diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts index aebcdbff7de..24b5ee71380 100644 --- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts +++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts @@ -12,7 +12,7 @@ import { PageTypes } from '#app/models/types/global'; import { ArticleMetadata } from '#app/models/types/optimo'; import { getServerExperiments } from '#server/utilities/experimentHeader'; import augmentWithDisclaimer from './augmentWithDisclaimer'; -import shouldRender from './shouldRender'; +import shouldRender from '../../../utilities/shouldRender'; import getPageData from '../../../utilities/pageRequests/getPageData'; // EXPERIMENT: Location based Topics Experiment diff --git a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts index 57d30c187db..846c7a5ad81 100644 --- a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts +++ b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts @@ -1,6 +1,6 @@ import { GetServerSidePropsContext } from 'next'; import pidginHomepageFixtureData from '#data/pidgin/homePage/index.json'; -import * as shouldRender from '../articles/shouldRender'; +import * as shouldRender from '../../../utilities/shouldRender'; import * as getPageDataModule from '../../../utilities/pageRequests/getPageData'; import handleHomepageRoute from './handleHomepageRoute'; diff --git a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts index 8289a3dbc46..dfdb2f04659 100644 --- a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts +++ b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts @@ -9,7 +9,7 @@ import getPathExtension from '#app/utilities/getPathExtension'; import PageDataParams from '#app/models/types/pageDataParams'; import handleError from '#app/routes/utils/handleError'; import { getServerExperiments } from '#server/utilities/experimentHeader'; -import shouldRender from '../articles/shouldRender'; +import shouldRender from '../../../utilities/shouldRender'; import getPageData from '../../../utilities/pageRequests/getPageData'; const logger = nodeLogger(__filename); diff --git a/ws-nextjs-app/pages/[service]/articles/shouldRender/index.test.ts b/ws-nextjs-app/utilities/shouldRender/index.test.ts similarity index 100% rename from ws-nextjs-app/pages/[service]/articles/shouldRender/index.test.ts rename to ws-nextjs-app/utilities/shouldRender/index.test.ts diff --git a/ws-nextjs-app/pages/[service]/articles/shouldRender/index.ts b/ws-nextjs-app/utilities/shouldRender/index.ts similarity index 100% rename from ws-nextjs-app/pages/[service]/articles/shouldRender/index.ts rename to ws-nextjs-app/utilities/shouldRender/index.ts From 5dc4dec447e325c47643e7eb7db1be76c1c6a83b Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Mon, 15 Dec 2025 13:00:26 +0000 Subject: [PATCH 46/96] removed redundant props --- .../pages/[service]/homepage/handleHomepageRoute.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts index dfdb2f04659..3e3087457f6 100644 --- a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts +++ b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts @@ -25,7 +25,7 @@ export default async (context: GetServerSidePropsContext) => { const resolvedUrlWithoutQuery = resolvedUrl.split('?')?.[0]; - const { isAmp, isApp, isLite } = getPathExtension(resolvedUrlWithoutQuery); + const { isAmp } = getPathExtension(resolvedUrlWithoutQuery); const { variant } = parseRoute(resolvedUrl); const { data } = await getPageData({ @@ -54,10 +54,6 @@ export default async (context: GetServerSidePropsContext) => { return { props: { - isApp, - isAmp, - isLite, - isNextJs: true, service, status: renderStatus, timeOnServer: Date.now(), From 432c37f8a64b8d6481f47dbaa9720f68d6779451 Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Mon, 15 Dec 2025 14:16:58 +0000 Subject: [PATCH 47/96] updated shouldRender imports --- src/app/legacy/containers/PageHandlers/withData/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/legacy/containers/PageHandlers/withData/index.jsx b/src/app/legacy/containers/PageHandlers/withData/index.jsx index 60f12742c57..3640bc2eadc 100644 --- a/src/app/legacy/containers/PageHandlers/withData/index.jsx +++ b/src/app/legacy/containers/PageHandlers/withData/index.jsx @@ -1,6 +1,6 @@ import { use } from 'react'; import ErrorPage from '#pages/ErrorPage/ErrorPage'; -import shouldRender from '#nextjs/pages/[service]/articles/shouldRender'; +import shouldRender from '#nextjs/utilities/shouldRender'; import { ServiceContext } from '../../../../contexts/ServiceContext'; const WithData = Component => { From d7e3b4b5548e2ac76716113d2f96c8a0c29c2dcb Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Mon, 15 Dec 2025 14:31:45 +0000 Subject: [PATCH 48/96] fixed imports again --- .../pages/[service]/articles/handleArticleRoute.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts index 777b9c9f13a..55aa0206b0e 100644 --- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts +++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts @@ -5,8 +5,8 @@ import * as getPageDataModule from '../../../utilities/pageRequests/getPageData' import handleArticleRoute from './handleArticleRoute'; jest.mock('../../../utilities/pageRequests/getPageData'); -jest.mock('./shouldRender', () => { - const originalModule = jest.requireActual('./shouldRender'); +jest.mock('../../../utilities/shouldRender', () => { + const originalModule = jest.requireActual('../../../utilities/shouldRender'); return { __esModule: true, ...originalModule, From 2dd08511fabaf2800b97c5eb5f2db42b3b172631 Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Mon, 15 Dec 2025 14:56:13 +0000 Subject: [PATCH 49/96] updated imports again --- .../pages/[service]/homepage/handleHomepageRoute.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts index 846c7a5ad81..821e2981f60 100644 --- a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts +++ b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts @@ -5,8 +5,8 @@ import * as getPageDataModule from '../../../utilities/pageRequests/getPageData' import handleHomepageRoute from './handleHomepageRoute'; jest.mock('../../../utilities/pageRequests/getPageData'); -jest.mock('../articles/shouldRender', () => { - const originalModule = jest.requireActual('../articles/shouldRender'); +jest.mock('../../../utilities/shouldRender', () => { + const originalModule = jest.requireActual('../../../utilities/shouldRender'); return { __esModule: true, ...originalModule, From 235a544371fd146a86e975a7fe3924b318eeac8b Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Mon, 15 Dec 2025 15:46:22 +0000 Subject: [PATCH 50/96] removed unused props --- .../pages/[service]/homepage/handleHomepageRoute.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts index 3e3087457f6..4ab6d02faee 100644 --- a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts +++ b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts @@ -1,5 +1,4 @@ import { GetServerSidePropsContext } from 'next'; -import extractHeaders from '#server/utilities/extractHeaders'; import { HOME_PAGE } from '#app/routes/utils/pageTypes'; import parseRoute from '#app/routes/utils/parseRoute'; import nodeLogger from '#lib/logger.node'; @@ -57,10 +56,6 @@ export default async (context: GetServerSidePropsContext) => { service, status: renderStatus, timeOnServer: Date.now(), - variant: variant || null, - pageType: HOME_PAGE, - pathname: resolvedUrlWithoutQuery, - ...extractHeaders(reqHeaders), }, }; } From 7743d123accc1cb8e04620b9060cb41aeca791d4 Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Mon, 15 Dec 2025 15:49:56 +0000 Subject: [PATCH 51/96] reinstated required props --- ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts index 4ab6d02faee..2ec0cb5d032 100644 --- a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts +++ b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts @@ -56,6 +56,9 @@ export default async (context: GetServerSidePropsContext) => { service, status: renderStatus, timeOnServer: Date.now(), + variant: variant || null, + pageType: HOME_PAGE, + pathname: resolvedUrlWithoutQuery, }, }; } From 500f45dd3def0a00d69a6b1ed6e95335ad86fe6e Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Tue, 16 Dec 2025 09:34:14 +0000 Subject: [PATCH 52/96] updated cache control --- ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts index 2ec0cb5d032..80d7b72abeb 100644 --- a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts +++ b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts @@ -69,7 +69,7 @@ export default async (context: GetServerSidePropsContext) => { context.res.setHeader( 'Cache-Control', - 'public, stale-if-error=90, stale-while-revalidate=30, max-age=60', + 'public, stale-if-error=300, stale-while-revalidate=120, max-age=30', ); routingInfoLogger(ROUTING_INFORMATION, { From d4070e79cc41c7336498cde04110e3b03ae85d7a Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Tue, 16 Dec 2025 09:47:37 +0000 Subject: [PATCH 53/96] updated unit tests --- .../pages/[service]/homepage/handleHomepageRoute.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts index 821e2981f60..dc4a5b653eb 100644 --- a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts +++ b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.test.ts @@ -100,7 +100,7 @@ describe('handleHomepageRoute', () => { expect(mockSetHeader).toHaveBeenCalledWith( 'Cache-Control', - expect.stringContaining('max-age=60'), + expect.stringContaining('max-age=30'), ); }); }); From e074bf927a465e5526bb842cfbac2ecb8e3a6f93 Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Tue, 16 Dec 2025 10:38:14 +0000 Subject: [PATCH 54/96] removed amp as it is not supported in home pages --- ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts index 80d7b72abeb..88d8d67713e 100644 --- a/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts +++ b/ws-nextjs-app/pages/[service]/homepage/handleHomepageRoute.ts @@ -4,7 +4,6 @@ import parseRoute from '#app/routes/utils/parseRoute'; import nodeLogger from '#lib/logger.node'; import { OK } from '#app/lib/statusCodes.const'; import { ROUTING_INFORMATION } from '#app/lib/logger.const'; -import getPathExtension from '#app/utilities/getPathExtension'; import PageDataParams from '#app/models/types/pageDataParams'; import handleError from '#app/routes/utils/handleError'; import { getServerExperiments } from '#server/utilities/experimentHeader'; @@ -24,7 +23,6 @@ export default async (context: GetServerSidePropsContext) => { const resolvedUrlWithoutQuery = resolvedUrl.split('?')?.[0]; - const { isAmp } = getPathExtension(resolvedUrlWithoutQuery); const { variant } = parseRoute(resolvedUrl); const { data } = await getPageData({ @@ -34,7 +32,6 @@ export default async (context: GetServerSidePropsContext) => { rendererEnv, resolvedUrl: resolvedUrlWithoutQuery, pageType: HOME_PAGE, - isAmp, }); const { pageData, status } = data; From 7f66610580d87fcc486fd5c36f4a675ea41211e1 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 16 Dec 2025 12:55:58 +0000 Subject: [PATCH 55/96] Add test --- .../pages/[service]/[[...].page.test.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/ws-nextjs-app/pages/[service]/[[...].page.test.tsx b/ws-nextjs-app/pages/[service]/[[...].page.test.tsx index d1dffff24dd..d89b9c72a69 100644 --- a/ws-nextjs-app/pages/[service]/[[...].page.test.tsx +++ b/ws-nextjs-app/pages/[service]/[[...].page.test.tsx @@ -3,6 +3,7 @@ import { GetServerSidePropsContext } from 'next/types'; import { getServerSideProps } from './[[...]].page'; import handleAvRoute from './av-embeds/handleAvRoute'; import handleArticleRoute from './articles/handleArticleRoute'; +import handleHomepageRoute from './homepage/handleHomepageRoute'; jest.mock('#server/utilities/logResponseTime', () => ({ __esModule: true, @@ -19,6 +20,11 @@ jest.mock('./articles/handleArticleRoute', () => ({ default: jest.fn().mockResolvedValue({}), })); +jest.mock('./homepage/handleHomepageRoute', () => ({ + __esModule: true, + default: jest.fn().mockResolvedValue({}), +})); + const commonContext = { req: { headers: {} }, query: { service: 'pidgin' }, @@ -86,6 +92,32 @@ describe('catch-all route', () => { }); }); + describe('Homepage page type', () => { + it('should call the Homepage route handler if homepage is requested using URL', async () => { + const context = { + ...commonContext, + resolvedUrl: '/pidgin', + }; + + await getServerSideProps(context); + + expect(handleHomepageRoute).toHaveBeenCalled(); + }); + + it('should call the Homepage route handler if homepage is requested using page-type header', async () => { + const context = { + ...commonContext, + req: { + headers: { 'page-type': 'home' }, + } as unknown as GetServerSidePropsContext['req'], + }; + + await getServerSideProps(context); + + expect(handleHomepageRoute).toHaveBeenCalled(); + }); + }); + it('should return 404 for unsupported page types', async () => { const context = { ...commonContext, From b8e933dbfda90929719f38d3a5d33ee1c47d9367 Mon Sep 17 00:00:00 2001 From: Nabeel Khan Date: Tue, 16 Dec 2025 14:27:21 +0000 Subject: [PATCH 56/96] updated readme --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 086e91257f2..8212c568aa3 100644 --- a/README.md +++ b/README.md @@ -207,23 +207,21 @@ Services with variants can't be accessed using the format above, instead the var - [http://localhost:7081/zhongwen/articles/c3xd4x9prgyo/simp](http://localhost:7081/zhongwen/articles/c3xd4x9prgyo/simp) - [http://localhost:7081/zhongwen/articles/c3xd4x9prgyo/simp.amp](http://localhost:7081/zhongwen/articles/c3xd4x9prgyo/simp.amp). -### Front pages +### Home pages -World Service front pages are served in the format `/:service` where `service` represents a World Service site: +World Service home pages are served in the format `/:service` where `service` represents a World Service site: - [http://localhost:7080/igbo](http://localhost:7080/igbo) - [http://localhost:7080/pidgin](http://localhost:7080/pidgin) -The World Service front pages follow the article format for AMP too, being available at `/:service.amp`: - -- [http://localhost:7080/igbo.amp](http://localhost:7080/igbo.amp) -- [http://localhost:7080/pidgin.amp](http://localhost:7080/pidgin.amp) - Services with variants can't be accessed using the format above, instead the variant must be provided in the URL. - [http://localhost:7080/zhongwen/simp](http://localhost:7080/zhongwen/simp) - [http://localhost:7080/zhongwen/simp.amp](http://localhost:7080/zhongwen/simp.amp). +World Service home pages do not support AMP. + + ### Topic Pages Topic pages use internal BBC APIs that are not publicly accessible. This can cause the following warnings to appear when developing locally: From 62cb94d926dc571d58cd1c60da4721f317671d32 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 12 Dec 2025 09:12:13 +0000 Subject: [PATCH 57/96] WS-1175: Removes optimizely dependency from article readtime --- src/app/components/ReadTime/index.tsx | 137 +++--------------- .../containers/ArticleTimestamp/index.jsx | 10 +- src/app/pages/ArticlePage/ArticlePage.tsx | 75 ++-------- 3 files changed, 37 insertions(+), 185 deletions(-) diff --git a/src/app/components/ReadTime/index.tsx b/src/app/components/ReadTime/index.tsx index d382b079b69..005acf1706c 100644 --- a/src/app/components/ReadTime/index.tsx +++ b/src/app/components/ReadTime/index.tsx @@ -1,4 +1,3 @@ -import type { PropsWithChildren } from 'react'; import { use } from 'react'; import { ServiceContext } from '#app/contexts/ServiceContext'; import { EventTrackingData } from '#app/lib/analyticsUtils/types'; @@ -7,7 +6,7 @@ import Text from '#app/components/Text'; import styles from './index.styles'; type ReadTimeProps = { - readTimeValue?: number; + readTimeValue: number; className?: string; readTimeVariant?: string | null; promoId?: string; @@ -15,25 +14,14 @@ type ReadTimeProps = { promoPosition?: number; }; -const DEFAULT_TRANSLATIONS = { - long: 'Long read', - minute: 'min', - read: 'Read time', -}; - -const ProcessReadTime = ({ - readTimeValue, - readTimeVariant, -}: { - readTimeValue: number; - readTimeVariant: string; -}) => { +const ProcessReadTime = ({ readTimeValue }: { readTimeValue: number }) => { const { translations, service } = use(ServiceContext); - const singleMinuteSuffix = - translations.readTime?.minute ?? DEFAULT_TRANSLATIONS.minute; - const readCopy = - translations.readTime?.readTimePrefix ?? DEFAULT_TRANSLATIONS.read; + const singleMinuteSuffix = translations.readTime?.minute; + const readCopy = translations.readTime?.readTimePrefix; + const longReadCopy = translations.readTime?.long; + + if (!singleMinuteSuffix || !readCopy || !longReadCopy) return null; const servicesWithMinutesBeforeNumber = [ 'hausa', @@ -50,50 +38,35 @@ const ProcessReadTime = ({ : `${readCopy}${separator}${readTimeValue} ${singleMinuteSuffix}`; const isLongRead = readTimeValue >= 6; - if (readTimeVariant === 'long_read_written' && isLongRead) { - copy = translations.readTime?.long ?? DEFAULT_TRANSLATIONS.long; + if (isLongRead) { + copy = longReadCopy; } - const readTimeInMilliseconds = readTimeValue * 60000; - return { - readTimeInMilliseconds, - minutesLabel: DEFAULT_TRANSLATIONS.minute, copy, }; }; -// EXPERIMENT: Article Read Time 2 -export const ReadTimeArticleExperiment = ({ - readTimeValue, - readTimeVariant, - className, -}: ReadTimeProps) => { - if (!readTimeValue) return null; - const showReadTime = readTimeVariant && readTimeVariant !== 'off'; - if (!showReadTime) return null; - - const { readTimeInMilliseconds, minutesLabel, copy } = ProcessReadTime({ - readTimeValue, - readTimeVariant, - }); +const ReadTimeArticle = ({ readTimeValue, className }: ReadTimeProps) => { + const readTimeInMilliseconds = readTimeValue * 60000; const eventTrackingData: EventTrackingData = { componentName: 'read-time-on-article', - sendOptimizelyEvents: true, - experimentName: 'newswb_ws_article_read_time_2', - experimentVariant: readTimeVariant, itemTracker: { - label: `Read time: ${readTimeValue} ${minutesLabel}`, + label: `Read time: ${readTimeValue} min`, duration: readTimeInMilliseconds, type: `read-time`, }, }; - // eslint-disable-next-line react-hooks/rules-of-hooks const viewRef = useViewTracker(eventTrackingData); - if (readTimeVariant === 'control') - return
    ; + + const { copy } = + ProcessReadTime({ + readTimeValue, + }) || {}; + + if (!readTimeInMilliseconds || !copy) return null; return (
    ( -
    -); - -export const ReadTime = ({ - readTimeValue, - readTimeVariant, - promoId, - promoType, - promoPosition, - className, -}: ReadTimeProps) => { - const { service } = use(ServiceContext); - - const validRender = [ - readTimeValue, - readTimeVariant, - readTimeVariant !== 'off', - ].every(Boolean); - - // EXPERIMENT: Homepage Read Time - const experimentEnabledServices = ['turkce', 'mundo']; - - if (readTimeVariant === null && experimentEnabledServices.includes(service)) - return ; - - if (!validRender) return null; - - const { readTimeInMilliseconds, copy } = ProcessReadTime({ - readTimeValue: readTimeValue as number, - readTimeVariant: readTimeVariant as string, - }); - - const optimizelyTrackingData: EventTrackingData = { - componentName: 'read-time', - sendOptimizelyEvents: true, - experimentName: 'newswb_ws_homepage_read_time', - experimentVariant: readTimeVariant, - }; - - const eventTrackingData: EventTrackingData = { - ...optimizelyTrackingData, - itemTracker: { - type: promoType, - position: promoPosition, - label: `Read time: ${readTimeValue} ${readTimeValue === 1 ? 'minute' : 'minutes'}`, - duration: readTimeInMilliseconds, - resourceId: promoId, - }, - }; - - const isControlVariant = readTimeVariant === 'control'; - - // eslint-disable-next-line react-hooks/rules-of-hooks - const viewRef = useViewTracker(eventTrackingData); - - if (isControlVariant) return ; - - return ( -
    - - {copy} - -
    - ); -}; +export default ReadTimeArticle; diff --git a/src/app/legacy/containers/ArticleTimestamp/index.jsx b/src/app/legacy/containers/ArticleTimestamp/index.jsx index b70f727b27a..a65922679fb 100644 --- a/src/app/legacy/containers/ArticleTimestamp/index.jsx +++ b/src/app/legacy/containers/ArticleTimestamp/index.jsx @@ -17,7 +17,7 @@ const ArticleTimestamp = ({ popOut = true, minutesTolerance = 0, className = '', - showReadTimeBelowTimestamp = false, + hasReadTime = false, }) => { const { articleTimestampPrefix, @@ -72,8 +72,8 @@ const ArticleTimestamp = ({ {displayLastUpdatedTimestamp && ( // Div has been used for No CSS formatting see #5554 @@ -81,8 +81,8 @@ const ArticleTimestamp = ({
    )} diff --git a/src/app/pages/ArticlePage/ArticlePage.tsx b/src/app/pages/ArticlePage/ArticlePage.tsx index c588bdce5a7..95efa2b9c52 100644 --- a/src/app/pages/ArticlePage/ArticlePage.tsx +++ b/src/app/pages/ArticlePage/ArticlePage.tsx @@ -46,7 +46,7 @@ import { Recommendation } from '#app/models/types/onwardJourney'; import ScrollablePromo from '#components/ScrollablePromo'; import Recommendations from '#app/components/Recommendations'; -import { ReadTimeArticleExperiment as ReadTime } from '#app/components/ReadTime'; +import ReadTimeArticle from '#app/components/ReadTime'; import PWAPromotionalBanner from '#app/components/PWAPromotionalBanner'; import PersonalisedContent from '../../components/PersonalisedContent'; import ElectionBanner from './ElectionBanner'; @@ -85,7 +85,6 @@ import { // EXPERIMENT: Article Read Time 2 interface ReadTimeData { readTimeValue: number | undefined; - readTimeVariant: string; } const getImageComponent = @@ -97,16 +96,6 @@ const getImageComponent = /> ); -// EXPERIMENT: Article Read Time 2 -const Placeholder = ({ className }: { className?: string }) => { - const { service } = use(ServiceContext); - const servicesInExperiment = ['']; // adding services will show placeholder regardless of whether experiment is running - return servicesInExperiment.includes(service) ? ( -
    - ) : null; -}; - -// EXPERIMENT: Article Read Time 2 const getTimestampComponent = ( hasByline: boolean, @@ -116,48 +105,22 @@ const getTimestampComponent = readTimeData: ReadTimeData, ) => (props: ComponentToRenderProps & TimeStampProps) => { - // EXPERIMENT: Article Read Time 2 - const { readTimeValue, readTimeVariant } = readTimeData; - const isReadTimeVariantValid = readTimeVariant !== 'off' && readTimeVariant; - const showReadTimeBelowTimestamp = - !!readTimeValue && readTimeValue !== 0 && !!isReadTimeVariantValid; + const { readTimeValue } = readTimeData; return hasByline ? ( - <> - - - {showReadTimeBelowTimestamp && ( - - )} - - {!showReadTimeBelowTimestamp && ( - - )} - - ) : ( - <> + - {/* EXPERIMENT: Article Read Time 2 */} - {showReadTimeBelowTimestamp ? ( - - ) : ( - - )} + {readTimeValue && } + + ) : ( + <> + + {readTimeValue && } ); }; @@ -232,13 +195,6 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { palette: { GREY_2 }, } = useTheme(); - // EXPERIMENT: Article Read Time 2 - const readTimeExperimentName = 'newswb_ws_article_read_time_2'; - const readTimeExperimentVariant = useOptimizelyVariation({ - experimentName: readTimeExperimentName, - experimentType: ExperimentType.CLIENT_SIDE, - }); - // EXPERIMENT: Time of Day Experiment const timeOfDayExperimentName = 'newswb_ws_tod_article'; const timeOfDayExperimentVariant = useOptimizelyVariation({ @@ -303,13 +259,7 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { const atiData = { ...atiAnalytics, ...(isCPS && { pageTitle: `${atiAnalytics.pageTitle} - ${brandName}` }), - // EXPERIMENT: Article Read Time 2 // Better way to handle this? - ...(readTimeExperimentVariant && - readTimeExperimentVariant !== 'off' && { - experimentName: readTimeExperimentName, - experimentVariant: readTimeExperimentVariant, - }), ...(timeOfDayExperimentVariant && timeOfDayExperimentVariant !== 'off' && { experimentName: timeOfDayExperimentName, @@ -320,7 +270,6 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { // EXPERIMENT: Article Read Time 2 const readTimeData = { readTimeValue, - readTimeVariant: readTimeExperimentVariant || 'off', }; const hasContinueReadingBlock = blocks.some( From ff0e20474ba01a9d201d3cada128dc48a5fac2d4 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 12 Dec 2025 09:31:00 +0000 Subject: [PATCH 58/96] WS-1175: Refactor ReadTime component --- src/app/components/ReadTime/index.tsx | 58 ++++++++++++++------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/app/components/ReadTime/index.tsx b/src/app/components/ReadTime/index.tsx index 005acf1706c..2d42c0c52f5 100644 --- a/src/app/components/ReadTime/index.tsx +++ b/src/app/components/ReadTime/index.tsx @@ -1,5 +1,6 @@ import { use } from 'react'; import { ServiceContext } from '#app/contexts/ServiceContext'; +import { Services } from '#app/models/types/global'; import { EventTrackingData } from '#app/lib/analyticsUtils/types'; import useViewTracker from '#app/hooks/useViewTracker'; import Text from '#app/components/Text'; @@ -14,40 +15,40 @@ type ReadTimeProps = { promoPosition?: number; }; -const ProcessReadTime = ({ readTimeValue }: { readTimeValue: number }) => { - const { translations, service } = use(ServiceContext); - - const singleMinuteSuffix = translations.readTime?.minute; - const readCopy = translations.readTime?.readTimePrefix; - const longReadCopy = translations.readTime?.long; - - if (!singleMinuteSuffix || !readCopy || !longReadCopy) return null; - - const servicesWithMinutesBeforeNumber = [ +const formatReadTime = ({ + readTimeValue, + singleMinuteSuffix, + readTimePrefix, + service, +}: { + readTimeValue: number; + singleMinuteSuffix?: string; + readTimePrefix?: string; + service: Services; +}) => { + if (!singleMinuteSuffix || !readTimePrefix) return null; + const servicesWithMinutesBeforeNumber: Services[] = [ 'hausa', 'igbo', 'yoruba', 'swahili', ]; - const servicesWithoutColon = ['igbo', 'pidgin']; + const servicesWithoutColon: Services[] = ['igbo', 'pidgin']; const separator = servicesWithoutColon.includes(service) ? ' ' : ': '; - let copy = servicesWithMinutesBeforeNumber.includes(service) - ? `${readCopy}${separator}${singleMinuteSuffix} ${readTimeValue}` - : `${readCopy}${separator}${readTimeValue} ${singleMinuteSuffix}`; - - const isLongRead = readTimeValue >= 6; - if (isLongRead) { - copy = longReadCopy; - } - - return { - copy, - }; + return servicesWithMinutesBeforeNumber.includes(service) + ? `${readTimePrefix}${separator}${singleMinuteSuffix} ${readTimeValue}` + : `${readTimePrefix}${separator}${readTimeValue} ${singleMinuteSuffix}`; }; const ReadTimeArticle = ({ readTimeValue, className }: ReadTimeProps) => { + const { translations, service } = use(ServiceContext); + const { readTime } = translations; + + const singleMinuteSuffix = readTime?.minute; + const readTimePrefix = readTime?.readTimePrefix; + const readTimeInMilliseconds = readTimeValue * 60000; const eventTrackingData: EventTrackingData = { @@ -61,12 +62,15 @@ const ReadTimeArticle = ({ readTimeValue, className }: ReadTimeProps) => { const viewRef = useViewTracker(eventTrackingData); - const { copy } = - ProcessReadTime({ + const readTimeText = + formatReadTime({ readTimeValue, + singleMinuteSuffix, + readTimePrefix, + service, }) || {}; - if (!readTimeInMilliseconds || !copy) return null; + if (!readTimeInMilliseconds || !readTimeText) return null; return (
    { data-testid="read-time" > - {copy} + {readTimeText}
    ); From d33ca2605c1fa6ceb1db876f9bb4d9bc7de6dca2 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 12 Dec 2025 11:25:28 +0000 Subject: [PATCH 59/96] WS-1175: Fix react issue --- src/app/components/ReadTime/index.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/components/ReadTime/index.tsx b/src/app/components/ReadTime/index.tsx index 2d42c0c52f5..8ea1db66b19 100644 --- a/src/app/components/ReadTime/index.tsx +++ b/src/app/components/ReadTime/index.tsx @@ -27,6 +27,7 @@ const formatReadTime = ({ service: Services; }) => { if (!singleMinuteSuffix || !readTimePrefix) return null; + const servicesWithMinutesBeforeNumber: Services[] = [ 'hausa', 'igbo', @@ -62,13 +63,12 @@ const ReadTimeArticle = ({ readTimeValue, className }: ReadTimeProps) => { const viewRef = useViewTracker(eventTrackingData); - const readTimeText = - formatReadTime({ - readTimeValue, - singleMinuteSuffix, - readTimePrefix, - service, - }) || {}; + const readTimeText = formatReadTime({ + readTimeValue, + singleMinuteSuffix, + readTimePrefix, + service, + }); if (!readTimeInMilliseconds || !readTimeText) return null; From b4cc30a821b2e17d8f6c74424b8847fa03db3cba Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 12 Dec 2025 11:43:31 +0000 Subject: [PATCH 60/96] WS-1175: Move translations check higher up the tree --- src/app/components/ReadTime/index.tsx | 15 ++++++--- src/app/pages/ArticlePage/ArticlePage.tsx | 40 +++++++++++++---------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/app/components/ReadTime/index.tsx b/src/app/components/ReadTime/index.tsx index 8ea1db66b19..d0267042579 100644 --- a/src/app/components/ReadTime/index.tsx +++ b/src/app/components/ReadTime/index.tsx @@ -1,6 +1,7 @@ import { use } from 'react'; import { ServiceContext } from '#app/contexts/ServiceContext'; import { Services } from '#app/models/types/global'; +import { Translations } from '#app/models/types/translations'; import { EventTrackingData } from '#app/lib/analyticsUtils/types'; import useViewTracker from '#app/hooks/useViewTracker'; import Text from '#app/components/Text'; @@ -8,6 +9,7 @@ import styles from './index.styles'; type ReadTimeProps = { readTimeValue: number; + readTimeTranslations: Translations['readTime']; className?: string; readTimeVariant?: string | null; promoId?: string; @@ -43,12 +45,15 @@ const formatReadTime = ({ : `${readTimePrefix}${separator}${readTimeValue} ${singleMinuteSuffix}`; }; -const ReadTimeArticle = ({ readTimeValue, className }: ReadTimeProps) => { - const { translations, service } = use(ServiceContext); - const { readTime } = translations; +const ReadTimeArticle = ({ + readTimeValue, + readTimeTranslations, + className, +}: ReadTimeProps) => { + const { service } = use(ServiceContext); - const singleMinuteSuffix = readTime?.minute; - const readTimePrefix = readTime?.readTimePrefix; + const singleMinuteSuffix = readTimeTranslations?.minute; + const readTimePrefix = readTimeTranslations?.readTimePrefix; const readTimeInMilliseconds = readTimeValue * 60000; diff --git a/src/app/pages/ArticlePage/ArticlePage.tsx b/src/app/pages/ArticlePage/ArticlePage.tsx index 95efa2b9c52..3bfa92a8ff2 100644 --- a/src/app/pages/ArticlePage/ArticlePage.tsx +++ b/src/app/pages/ArticlePage/ArticlePage.tsx @@ -82,11 +82,6 @@ import { isPortraitVideoUnderHeadline, } from '../utils/portraitVideo'; -// EXPERIMENT: Article Read Time 2 -interface ReadTimeData { - readTimeValue: number | undefined; -} - const getImageComponent = (preloadLeadImageToggle: boolean) => (props: ComponentToRenderProps) => ( (props: ComponentToRenderProps & TimeStampProps) => { - const { readTimeValue } = readTimeData; + const shouldDisplayReadTime = !!(readTimeTranslations && readTimeValue); return hasByline ? ( @@ -113,14 +109,28 @@ const getTimestampComponent = firstPublished={new Date(firstPublished).getTime()} lastPublished={new Date(lastPublished).getTime()} popOut={false} - hasReadTime // update this + hasReadTime={shouldDisplayReadTime} /> - {readTimeValue && } + {shouldDisplayReadTime && ( + + )} ) : ( <> - - {readTimeValue && } + + {shouldDisplayReadTime && ( + + )} ); }; @@ -267,11 +277,6 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { }), }; - // EXPERIMENT: Article Read Time 2 - const readTimeData = { - readTimeValue, - }; - const hasContinueReadingBlock = blocks.some( block => block.type === 'continueReading', ); @@ -298,7 +303,8 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { bylineContribBlocks, firstPublished, lastPublished, - readTimeData, + readTimeValue, + translations.readTime, ), social: SocialEmbedContainer, embed: UnsupportedEmbed, From a82cac6242ec562f5358f40f4738c6af4baa6140 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 12 Dec 2025 11:45:17 +0000 Subject: [PATCH 61/96] WS-1175: Reverts passing translations as prop --- src/app/components/ReadTime/index.tsx | 19 +++++-------------- src/app/pages/ArticlePage/ArticlePage.tsx | 10 ++-------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/app/components/ReadTime/index.tsx b/src/app/components/ReadTime/index.tsx index d0267042579..d3d50a63405 100644 --- a/src/app/components/ReadTime/index.tsx +++ b/src/app/components/ReadTime/index.tsx @@ -1,7 +1,6 @@ import { use } from 'react'; import { ServiceContext } from '#app/contexts/ServiceContext'; import { Services } from '#app/models/types/global'; -import { Translations } from '#app/models/types/translations'; import { EventTrackingData } from '#app/lib/analyticsUtils/types'; import useViewTracker from '#app/hooks/useViewTracker'; import Text from '#app/components/Text'; @@ -9,12 +8,7 @@ import styles from './index.styles'; type ReadTimeProps = { readTimeValue: number; - readTimeTranslations: Translations['readTime']; className?: string; - readTimeVariant?: string | null; - promoId?: string; - promoType?: string; - promoPosition?: number; }; const formatReadTime = ({ @@ -45,15 +39,12 @@ const formatReadTime = ({ : `${readTimePrefix}${separator}${readTimeValue} ${singleMinuteSuffix}`; }; -const ReadTimeArticle = ({ - readTimeValue, - readTimeTranslations, - className, -}: ReadTimeProps) => { - const { service } = use(ServiceContext); +const ReadTimeArticle = ({ readTimeValue, className }: ReadTimeProps) => { + const { translations, service } = use(ServiceContext); - const singleMinuteSuffix = readTimeTranslations?.minute; - const readTimePrefix = readTimeTranslations?.readTimePrefix; + const { readTime } = translations; + const singleMinuteSuffix = readTime?.minute; + const readTimePrefix = readTime?.readTimePrefix; const readTimeInMilliseconds = readTimeValue * 60000; diff --git a/src/app/pages/ArticlePage/ArticlePage.tsx b/src/app/pages/ArticlePage/ArticlePage.tsx index 3bfa92a8ff2..bc643ef609f 100644 --- a/src/app/pages/ArticlePage/ArticlePage.tsx +++ b/src/app/pages/ArticlePage/ArticlePage.tsx @@ -112,10 +112,7 @@ const getTimestampComponent = hasReadTime={shouldDisplayReadTime} /> {shouldDisplayReadTime && ( - + )} ) : ( @@ -126,10 +123,7 @@ const getTimestampComponent = hasReadTime={shouldDisplayReadTime} /> {shouldDisplayReadTime && ( - + )} ); From d7d1e30753446da7e016e054d13db30c6f3f1e58 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 12 Dec 2025 12:08:44 +0000 Subject: [PATCH 62/96] WS-1175: Tidy up code labelled with experiment comments --- src/app/components/Byline/index.tsx | 1 - .../experimentsForPageMetrics.ts | 3 +- src/app/components/ReadTime/README.md | 6 +- src/app/components/ReadTime/index.test.tsx | 93 ++----------------- .../pages/ArticlePage/ArticlePage.styles.ts | 5 - src/app/pages/ArticlePage/ArticlePage.tsx | 6 +- src/app/pages/ArticlePage/index.test.tsx | 14 ++- 7 files changed, 23 insertions(+), 105 deletions(-) diff --git a/src/app/components/Byline/index.tsx b/src/app/components/Byline/index.tsx index 045d5944b7c..733d6cb61f0 100644 --- a/src/app/components/Byline/index.tsx +++ b/src/app/components/Byline/index.tsx @@ -210,7 +210,6 @@ const Byline = ({ isSingleContributor={isSingleContributor} />
  • - {/* EXPERIMENT: Article Read Time */} {children && Children.map(children, (child, index) => ( // eslint-disable-next-line react/no-array-index-key diff --git a/src/app/components/OptimizelyPageMetrics/experimentsForPageMetrics.ts b/src/app/components/OptimizelyPageMetrics/experimentsForPageMetrics.ts index 7941a85fe42..fd39e92960c 100644 --- a/src/app/components/OptimizelyPageMetrics/experimentsForPageMetrics.ts +++ b/src/app/components/OptimizelyPageMetrics/experimentsForPageMetrics.ts @@ -11,11 +11,10 @@ type ExperimentsForPageTypeMetrics = { const experimentsForPageMetrics: ExperimentsForPageTypeMetrics = [ { - // EXPERIMENT: Continue Reading button for articles & EXPERIMENT: Article Read Time 2 + // EXPERIMENT: Continue Reading button for articles pageType: ARTICLE_PAGE, activeExperiments: [ 'newswb_ws_read_more_b', - 'newswb_ws_article_read_time_2', 'newswb_ws_tod_article', 'newswb_ws_pwa_promo_prompt', ], diff --git a/src/app/components/ReadTime/README.md b/src/app/components/ReadTime/README.md index 5242ed633d4..ba9a53fdb58 100644 --- a/src/app/components/ReadTime/README.md +++ b/src/app/components/ReadTime/README.md @@ -1,5 +1,7 @@ ## Description -This component renders the estimated read time for an article, by using the `readTime` parameter passed into it. +This component renders the estimated read time for an article, by using the `readTime` parameter passed into it. -If no `readTime` is supplied nothing is rendered. \ No newline at end of file +If no `readTime` is supplied nothing is rendered. + +If no translations exist for a service, then the Read Time is not rendered. This is intentional because we have only collected translations for services which we believe the default calculation done by Ares is suitable for. diff --git a/src/app/components/ReadTime/index.test.tsx b/src/app/components/ReadTime/index.test.tsx index 30695b14b46..5b84c4c23f2 100644 --- a/src/app/components/ReadTime/index.test.tsx +++ b/src/app/components/ReadTime/index.test.tsx @@ -1,107 +1,34 @@ import { render } from '#app/components/react-testing-library-with-providers'; import * as viewTracking from '../../hooks/useViewTracker'; -import { ReadTimeArticleExperiment, ReadTime } from '.'; +import ReadTimeArticle from '.'; describe('ReadTime', () => { beforeEach(() => { jest.clearAllMocks(); }); - it.each([ - { - variant: 'Long Read Numerical', - variantKey: 'long_read_numerical', - expectedCopy: 'Read time: 4 min', - readTimeValue: 4, - }, - { - variant: 'Long Read Numerical', - variantKey: 'long_read_numerical', - expectedCopy: 'Read time: 6 min', - readTimeValue: 6, - }, - { - variant: 'Long Read Written', - variantKey: 'long_read_written', - expectedCopy: 'Long read', - readTimeValue: 7, - }, - ])( - 'should render $expectedCopy when readTime is supplied with a $variant variant', - ({ variantKey, expectedCopy, readTimeValue }) => { - const { getByText } = render( - , - ); - expect(getByText(expectedCopy)).toBeInTheDocument(); - }, - ); - it('Optimizely - Should render a blank div for a control variant', () => { - const container = render( - , - ); - expect(container.queryByTestId('read-time')).not.toBeInTheDocument(); - }); - describe('view tracking', () => { - const viewTrackerSpy = jest.spyOn(viewTracking, 'default'); - - it('should register view tracker', () => { - render( - , - ); - - const expected = { - componentName: 'read-time', - experimentName: 'newswb_ws_homepage_read_time', - experimentVariant: 'long_read_numerical', - sendOptimizelyEvents: true, - itemTracker: { - duration: 240000, - label: 'Read time: 4 minutes', - resourceId: '12345', - }, - }; - - expect(viewTrackerSpy).toHaveBeenCalledWith(expected); - }); - }); - describe('On Article Page Experiment', () => { + describe('On Article Page', () => { it('should render when readTime is supplied', () => { - const { getByText } = render( - , - ); - expect(getByText('Read time: 4 min')).toBeInTheDocument(); + const { getByTestId } = render(, { + service: 'pidgin', + }); + const readTime = getByTestId('read-time'); + expect(readTime).toBeInTheDocument(); }); describe('view tracking', () => { const viewTrackerSpy = jest.spyOn(viewTracking, 'default'); it('should register view tracker', () => { - render( - , - ); + render(, { + service: 'pidgin', + }); const expected = { componentName: 'read-time-on-article', - experimentName: 'newswb_ws_article_read_time_2', - experimentVariant: 'long_read_numerical', itemTracker: { duration: 240000, label: 'Read time: 4 min', type: 'read-time', }, - sendOptimizelyEvents: true, }; expect(viewTrackerSpy).toHaveBeenCalledWith(expected); diff --git a/src/app/pages/ArticlePage/ArticlePage.styles.ts b/src/app/pages/ArticlePage/ArticlePage.styles.ts index 8712d4f94ac..5ab3500ffe1 100644 --- a/src/app/pages/ArticlePage/ArticlePage.styles.ts +++ b/src/app/pages/ArticlePage/ArticlePage.styles.ts @@ -176,9 +176,4 @@ export default { }), commonMarginSpacing, ], - // EXPERIMENT: Article Read Time - readTimePlaceholderBelowTimestamp: () => - css({ - marginBottom: `${pixelsToRem(18.5)}rem`, - }), }; diff --git a/src/app/pages/ArticlePage/ArticlePage.tsx b/src/app/pages/ArticlePage/ArticlePage.tsx index bc643ef609f..1fff9caeb9a 100644 --- a/src/app/pages/ArticlePage/ArticlePage.tsx +++ b/src/app/pages/ArticlePage/ArticlePage.tsx @@ -223,9 +223,6 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { const { enabled: podcastPromoEnabled } = useToggle('podcastPromo'); - // EXPERIMENT: Article Read Time 2 - const readTimeValue = pageData?.metadata?.stats?.readTime; - const headline = getHeadline(pageData) ?? ''; const description = getSummary(pageData) || getHeadline(pageData); const firstPublished = getFirstPublished(pageData); @@ -250,6 +247,8 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { ? getAuthorTwitterHandle(blocks) : null; + const readTimeValue = pageData?.metadata?.stats?.readTime; + const taggings = pageData?.metadata?.passport?.taggings ?? []; const formats = pageData?.metadata?.passport?.predicates?.formats ?? []; @@ -291,7 +290,6 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { video: getVideoComponent(translations, blocks), text, image: getImageComponent(preloadLeadImageToggle), - // EXPERIMENT: Article Read Time 2 timestamp: getTimestampComponent( hasByline, bylineContribBlocks, diff --git a/src/app/pages/ArticlePage/index.test.tsx b/src/app/pages/ArticlePage/index.test.tsx index ba34dd046de..7d52d0b77f7 100644 --- a/src/app/pages/ArticlePage/index.test.tsx +++ b/src/app/pages/ArticlePage/index.test.tsx @@ -982,12 +982,11 @@ describe('Article Page', () => { expect(title).not.toBeInTheDocument(); }); - // EXPERIMENT: Article Read Time - it.skip('should render read time component when readTime is supplied in metadata', () => { + it('should render read time component when readTime is supplied in metadata', () => { const dataWithReadTime = { - ...articleDataPidgin, + ...articleDataPidginWithByline, metadata: { - ...articleDataPidgin.metadata, + ...articleDataPidginWithByline.metadata, stats: { readTime: 5, wordCount: 500, @@ -1003,12 +1002,11 @@ describe('Article Page', () => { expect(queryByTestId('read-time')).toBeInTheDocument(); }); - // EXPERIMENT: Article Read Time - it.skip('should not render read time component when readTime is not supplied in metadata', () => { + it('should not render read time component when readTime is not supplied in metadata', () => { const dataMissingReadTime = { - ...articleDataPidgin, + ...articleDataPidginWithByline, metadata: { - ...articleDataPidgin.metadata, + ...articleDataPidginWithByline.metadata, stats: {}, }, }; From 5f3bbb21b80b6bb9f38f662b5fadfb58630a3984 Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 12 Dec 2025 12:26:35 +0000 Subject: [PATCH 63/96] WS-1175: Fix stories and styles --- src/app/components/ReadTime/index.stories.tsx | 19 +++++++++++-------- src/app/components/ReadTime/index.styles.ts | 12 ------------ 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/app/components/ReadTime/index.stories.tsx b/src/app/components/ReadTime/index.stories.tsx index 3e0da31c972..8ccf0195fa3 100644 --- a/src/app/components/ReadTime/index.stories.tsx +++ b/src/app/components/ReadTime/index.stories.tsx @@ -1,9 +1,16 @@ -import { ReadTimeArticleExperiment as ReadTime } from '.'; +import ReadTimeArticle from '.'; +import { ServiceContextProvider } from '#app/contexts/ServiceContext'; import readme from './README.md'; import metadata from './metadata.json'; +// Example for pidgin service and default variant +const service = 'pidgin'; +const variant = 'default'; + const Component = ({ readTime }: { readTime: number }) => ( - + + + ); export default { @@ -15,9 +22,5 @@ export default { }, }; -export const Example = () => ( - -); -export const OneMinuteReadTime = () => ( - -); +export const SevenMinuteReadTime = () => ; +export const OneMinuteReadTime = () => ; diff --git a/src/app/components/ReadTime/index.styles.ts b/src/app/components/ReadTime/index.styles.ts index 72d102e672f..d234a0ac95f 100644 --- a/src/app/components/ReadTime/index.styles.ts +++ b/src/app/components/ReadTime/index.styles.ts @@ -1,5 +1,4 @@ import { css, Theme } from '@emotion/react'; -import pixelsToRem from '../../utilities/pixelsToRem'; export default { readTimeText: ({ palette }: Theme) => @@ -16,15 +15,4 @@ export default { margin: `${spacings.FULL}rem 0 ${spacings.DOUBLE}rem`, }, }), - readTimePlaceholderControl: ({ spacings }: Theme) => - css({ - margin: `0 0 ${spacings.DOUBLE}rem`, - }), - readTimeHomepagePlaceholderControl: ({ mq }: Theme) => - css({ - height: `${pixelsToRem(18.4)}rem`, - [mq.GROUP_2_MAX_WIDTH]: { - height: `${pixelsToRem(19.1)}rem`, - }, - }), }; From 93ac0fed60af8850ff150e544bb3dfa594a69b3f Mon Sep 17 00:00:00 2001 From: Isabella-Mitchell Date: Fri, 12 Dec 2025 14:01:11 +0000 Subject: [PATCH 64/96] WS-1175: Tidies --- src/app/legacy/containers/ArticleTimestamp/index.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/legacy/containers/ArticleTimestamp/index.jsx b/src/app/legacy/containers/ArticleTimestamp/index.jsx index a65922679fb..c395ceb979f 100644 --- a/src/app/legacy/containers/ArticleTimestamp/index.jsx +++ b/src/app/legacy/containers/ArticleTimestamp/index.jsx @@ -72,7 +72,6 @@ const ArticleTimestamp = ({ {displayLastUpdatedTimestamp && ( @@ -81,7 +80,6 @@ const ArticleTimestamp = ({ From 48b8017ef1c553c4a478edbdb50a3a140c98d737 Mon Sep 17 00:00:00 2001 From: jinidev Date: Wed, 17 Dec 2025 16:19:18 +0200 Subject: [PATCH 65/96] cache was missing --- public/sw.js | 2 +- src/sw.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/sw.js b/public/sw.js index 4f26a6ecbf5..59f809e265f 100644 --- a/public/sw.js +++ b/public/sw.js @@ -192,6 +192,7 @@ const fetchEventHandler = async event => { const { url } = event.request; const client = await self.clients.get(event.clientId); const isPWA = client && pwaClients.get(client.id); + const cache = await caches.open(cacheName); console.log(`[SW FETCH] Navigation: ${url} , isPWA: ${isPWA}`); if (!isPWA && cache.has('pwa_installed')) { @@ -224,7 +225,6 @@ const fetchEventHandler = async event => { } catch (err) { console.log('[SW] Navigation failed:', url, err); - const cache = await caches.open(cacheName); const pwaMarker = await cache.match('pwa_installed'); console.log('[SW] PWA Marker:', pwaMarker); diff --git a/src/sw.test.js b/src/sw.test.js index 5569ffb6c97..380f9bc783a 100644 --- a/src/sw.test.js +++ b/src/sw.test.js @@ -288,7 +288,7 @@ describe('Service Worker', () => { describe('version', () => { const CURRENT_VERSION = { number: 'v0.3.1', - fileContentHash: '281869e4927afed159d55ed3cbd4a7c6', + fileContentHash: 'b5f154bf74c8a459ecc6a9ef67ac8c93', }; it(`version number should be ${CURRENT_VERSION.number}`, async () => { From 0f8261f0e37ee174955dd68cca2423112643c2c1 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Thu, 18 Dec 2025 16:13:07 +0200 Subject: [PATCH 66/96] adding tests --- .../hooks/useOfflinePageFlag/index.test.tsx | 115 +++++++ .../usePWAOfflineTracking/index.test.tsx | 282 ++++++++++++++++++ src/app/hooks/usePWAOfflineTracking/index.tsx | 1 + src/app/hooks/useSendPWAStatus/index.test.tsx | 184 ++++++++++++ .../index.test.tsx | 189 ++++++++++++ 5 files changed, 771 insertions(+) create mode 100644 src/app/hooks/useOfflinePageFlag/index.test.tsx create mode 100644 src/app/hooks/usePWAOfflineTracking/index.test.tsx create mode 100644 src/app/hooks/useSendPWAStatus/index.test.tsx create mode 100644 src/app/hooks/useServiceWorkerRegistration/index.test.tsx diff --git a/src/app/hooks/useOfflinePageFlag/index.test.tsx b/src/app/hooks/useOfflinePageFlag/index.test.tsx new file mode 100644 index 00000000000..25a79aa8873 --- /dev/null +++ b/src/app/hooks/useOfflinePageFlag/index.test.tsx @@ -0,0 +1,115 @@ +import { renderHook } from '@testing-library/react'; +import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server'; +import useOfflinePageFlag, { OFFLINE_VISIT_FLAG } from './index'; + +describe('useOfflinePageFlag', () => { + const originalMatchMedia = window.matchMedia; + const originalNavigator = window.navigator; + const originalLocalStorage = window.localStorage; + + beforeEach(() => { + Storage.prototype.setItem = jest.fn(); + Storage.prototype.getItem = jest.fn(); + Storage.prototype.removeItem = jest.fn(); + }); + + afterEach(() => { + window.matchMedia = originalMatchMedia; + window.navigator = originalNavigator; + window.localStorage = originalLocalStorage; + jest.restoreAllMocks(); + }); + + const mockMatchMedia = (queries: Record) => { + window.matchMedia = jest.fn().mockImplementation((query: string) => ({ + matches: !!queries[query], + })); + }; + + const mockNavigator = (onLine: boolean) => { + jest.spyOn(window, 'navigator', 'get').mockImplementation( + () => + ({ + onLine, + }) as unknown as Navigator, + ); + }; + + it('should set offline flag when offline in PWA mode', () => { + mockMatchMedia({ '(display-mode: standalone)': true }); + mockNavigator(false); + + renderHook(() => useOfflinePageFlag()); + + expect(localStorage.setItem).toHaveBeenCalledWith( + OFFLINE_VISIT_FLAG, + 'true', + ); + }); + + it('should not set flag when online in PWA mode', () => { + mockMatchMedia({ '(display-mode: standalone)': true }); + mockNavigator(true); + + renderHook(() => useOfflinePageFlag()); + + expect(localStorage.setItem).not.toHaveBeenCalled(); + }); + + it('should not set flag when offline in browser mode', () => { + mockMatchMedia({ '(display-mode: browser)': true }); + mockNavigator(false); + + renderHook(() => useOfflinePageFlag()); + + expect(localStorage.setItem).not.toHaveBeenCalled(); + }); + + it('should not set flag when online in browser mode', () => { + mockMatchMedia({ '(display-mode: browser)': true }); + mockNavigator(true); + + renderHook(() => useOfflinePageFlag()); + + expect(localStorage.setItem).not.toHaveBeenCalled(); + }); + + it('should handle localStorage errors gracefully', () => { + mockMatchMedia({ '(display-mode: standalone)': true }); + mockNavigator(false); + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + + Storage.prototype.setItem = jest.fn().mockImplementation(() => { + throw new Error('localStorage is full'); + }); + + expect(() => renderHook(() => useOfflinePageFlag())).not.toThrow(); + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'useOfflinePageFlag', + expect.any(Error), + ); + }); + + it('should not set flag on server side', () => { + renderSSRHook(() => useOfflinePageFlag()); + + expect(localStorage.setItem).not.toHaveBeenCalled(); + }); + + it('should work with iOS standalone mode', () => { + mockMatchMedia({}); + mockNavigator(false); + jest + .spyOn(window, 'navigator', 'get') + .mockImplementation( + () => ({ standalone: true, onLine: false }) as unknown as Navigator, + ); + + renderHook(() => useOfflinePageFlag()); + + expect(localStorage.setItem).toHaveBeenCalledWith( + OFFLINE_VISIT_FLAG, + 'true', + ); + }); +}); diff --git a/src/app/hooks/usePWAOfflineTracking/index.test.tsx b/src/app/hooks/usePWAOfflineTracking/index.test.tsx new file mode 100644 index 00000000000..bf505f32725 --- /dev/null +++ b/src/app/hooks/usePWAOfflineTracking/index.test.tsx @@ -0,0 +1,282 @@ +import { renderHook } from '@testing-library/react'; +import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server'; +import usePWAOfflineTracking from './index'; +import useIsPWA from '../useIsPWA'; +import useNetworkStatusTracker from '../useNetworkStatusTracker'; +import useCustomEventTracker from '../useCustomEventTracker'; + +jest.mock('../useIsPWA'); +jest.mock('../useNetworkStatusTracker'); +jest.mock('../useCustomEventTracker'); + +describe('usePWAOfflineTracking', () => { + const mockTrackOfflinePageViewEvent = jest.fn(); + const mockUseIsPWA = useIsPWA as jest.MockedFunction; + const mockUseNetworkStatusTracker = + useNetworkStatusTracker as jest.MockedFunction< + typeof useNetworkStatusTracker + >; + const mockUseCustomEventTracker = + useCustomEventTracker as jest.MockedFunction; + + beforeEach(() => { + jest.clearAllMocks(); + Storage.prototype.getItem = jest.fn(); + Storage.prototype.setItem = jest.fn(); + Storage.prototype.removeItem = jest.fn(); + + mockUseCustomEventTracker.mockReturnValue(mockTrackOfflinePageViewEvent); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should not fire event when not in PWA mode', () => { + mockUseIsPWA.mockReturnValue(false); + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType: '4g', + }); + + renderHook(() => usePWAOfflineTracking()); + + expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled(); + }); + + it('should not fire event when offline flag is not set', () => { + mockUseIsPWA.mockReturnValue(true); + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType: '4g', + }); + Storage.prototype.getItem = jest.fn().mockReturnValue(null); + + renderHook(() => usePWAOfflineTracking()); + + expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled(); + }); + + it('should fire event when in PWA mode, online, and flag is set', () => { + mockUseIsPWA.mockReturnValue(true); + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType: '4g', + }); + Storage.prototype.getItem = jest.fn().mockReturnValue('true'); + + renderHook(() => usePWAOfflineTracking()); + + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1); + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledWith('4g'); + expect(localStorage.removeItem).toHaveBeenCalledWith('offline_page_visit'); + }); + + it('should fire event on offline→online transition', () => { + mockUseIsPWA.mockReturnValue(true); + Storage.prototype.getItem = jest.fn().mockReturnValue('true'); + + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: false, + networkType: 'unknown', + }); + + const { rerender } = renderHook(() => usePWAOfflineTracking()); + + expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled(); + + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType: '4g', + }); + + rerender(); + + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1); + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledWith('4g'); + expect(localStorage.removeItem).toHaveBeenCalledWith('offline_page_visit'); + }); + + it('should not fire event again without flag being set', () => { + mockUseIsPWA.mockReturnValue(true); + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType: '4g', + }); + const mockGetItem = jest + .fn() + .mockReturnValueOnce('true') + .mockReturnValue(null); + Storage.prototype.getItem = mockGetItem; + + const { rerender } = renderHook(() => usePWAOfflineTracking()); + + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1); + + rerender(); + + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1); + }); + + it('should fire event again after flag is set again on next offline visit', () => { + mockUseIsPWA.mockReturnValue(true); + const mockGetItem = jest + .fn() + .mockReturnValueOnce('true') + .mockReturnValueOnce('true'); + Storage.prototype.getItem = mockGetItem; + + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: false, + networkType: 'unknown', + }); + + const { rerender } = renderHook(() => usePWAOfflineTracking()); + + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType: '4g', + }); + + rerender(); + + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1); + expect(localStorage.removeItem).toHaveBeenCalledWith('offline_page_visit'); + + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: false, + networkType: 'unknown', + }); + + rerender(); + + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType: '5g', + }); + + rerender(); + + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(2); + expect(mockTrackOfflinePageViewEvent).toHaveBeenLastCalledWith('5g'); + expect(localStorage.removeItem).toHaveBeenCalledTimes(2); + }); + + it('should remove flag after firing event', () => { + mockUseIsPWA.mockReturnValue(true); + Storage.prototype.getItem = jest.fn().mockReturnValue('true'); + + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: false, + networkType: 'unknown', + }); + + const { rerender } = renderHook(() => usePWAOfflineTracking()); + + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType: '4g', + }); + + rerender(); + + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1); + expect(localStorage.removeItem).toHaveBeenCalledWith('offline_page_visit'); + }); + + it('should not fire when offline even if flag is set', () => { + mockUseIsPWA.mockReturnValue(true); + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: false, + networkType: 'unknown', + }); + Storage.prototype.getItem = jest.fn().mockReturnValue('true'); + + renderHook(() => usePWAOfflineTracking()); + + expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled(); + }); + + it('should handle localStorage errors gracefully', () => { + mockUseIsPWA.mockReturnValue(true); + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType: '4g', + }); + + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + Storage.prototype.getItem = jest.fn().mockImplementation(() => { + throw new Error('localStorage access denied'); + }); + + expect(() => renderHook(() => usePWAOfflineTracking())).not.toThrow(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'usePWAOfflineTracking', + expect.any(Error), + ); + expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled(); + }); + + it('should not track on server side', () => { + mockUseIsPWA.mockReturnValue(true); + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType: '4g', + }); + Storage.prototype.getItem = jest.fn().mockReturnValue('true'); + + renderSSRHook(() => usePWAOfflineTracking()); + + expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled(); + }); + + it('should pass correct network type to tracking function', () => { + mockUseIsPWA.mockReturnValue(true); + Storage.prototype.getItem = jest.fn().mockReturnValue('true'); + + const networkTypes = [ + 'slow-2g', + '2g', + '3g', + '4g', + '5g', + 'unknown', + ] as const; + + networkTypes.forEach(networkType => { + jest.clearAllMocks(); + + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType, + }); + + renderHook(() => usePWAOfflineTracking()); + + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledWith(networkType); + }); + }); + + it('should only fire on actual offline→online transition, not online→offline', () => { + mockUseIsPWA.mockReturnValue(true); + Storage.prototype.getItem = jest.fn().mockReturnValue('true'); + + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType: '4g', + }); + + const { rerender } = renderHook(() => usePWAOfflineTracking()); + + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1); + + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: false, + networkType: 'unknown', + }); + + rerender(); + + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/app/hooks/usePWAOfflineTracking/index.tsx b/src/app/hooks/usePWAOfflineTracking/index.tsx index c1f857d3240..df28fddc773 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.tsx @@ -56,6 +56,7 @@ const usePWAOfflineTracking = () => { trackOfflinePageViewEvent(networkType); hasFiredRef.current = true; + localStorage.removeItem(OFFLINE_VISIT_FLAG); } catch (error) { // eslint-disable-next-line no-console console.error('usePWAOfflineTracking', error); diff --git a/src/app/hooks/useSendPWAStatus/index.test.tsx b/src/app/hooks/useSendPWAStatus/index.test.tsx new file mode 100644 index 00000000000..e88c893eab6 --- /dev/null +++ b/src/app/hooks/useSendPWAStatus/index.test.tsx @@ -0,0 +1,184 @@ +import { renderHook } from '@testing-library/react'; +import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server'; +import useSendPWAStatus from './index'; + +describe('useSendPWAStatus', () => { + const mockPostMessage = jest.fn(); + const mockAddEventListener = jest.fn(); + const mockRemoveEventListener = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + Object.defineProperty(global, 'navigator', { + value: { + serviceWorker: { + controller: { + postMessage: mockPostMessage, + state: 'activated', + }, + ready: Promise.resolve(), + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, + }, + }, + writable: true, + configurable: true, + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should send PWA status message when SW is ready and isPWA is true', async () => { + renderHook(() => useSendPWAStatus(true)); + + await Promise.resolve(); + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: 'PWA_STATUS', + isPWA: true, + }); + }); + + it('should send PWA status message when SW is ready and isPWA is false', async () => { + renderHook(() => useSendPWAStatus(false)); + + await Promise.resolve(); + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: 'PWA_STATUS', + isPWA: false, + }); + }); + + it('should add controllerchange event listener', () => { + renderHook(() => useSendPWAStatus(true)); + + expect(mockAddEventListener).toHaveBeenCalledWith( + 'controllerchange', + expect.any(Function), + ); + }); + + it('should remove controllerchange event listener on unmount', () => { + const { unmount } = renderHook(() => useSendPWAStatus(true)); + + unmount(); + + expect(mockRemoveEventListener).toHaveBeenCalledWith( + 'controllerchange', + expect.any(Function), + ); + }); + + it('should not send message when serviceWorker is not available', () => { + Object.defineProperty(global, 'navigator', { + value: {}, + writable: true, + configurable: true, + }); + + renderHook(() => useSendPWAStatus(true)); + + expect(mockPostMessage).not.toHaveBeenCalled(); + }); + + it('should not send message when controller is not activated', async () => { + Object.defineProperty(global, 'navigator', { + value: { + serviceWorker: { + controller: { + postMessage: mockPostMessage, + state: 'installing', + }, + ready: Promise.resolve(), + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, + }, + }, + writable: true, + configurable: true, + }); + + renderHook(() => useSendPWAStatus(true)); + + await Promise.resolve(); + + expect(mockPostMessage).not.toHaveBeenCalled(); + }); + + it('should not send message when controller is null', async () => { + Object.defineProperty(global, 'navigator', { + value: { + serviceWorker: { + controller: null, + ready: Promise.resolve(), + addEventListener: mockAddEventListener, + removeEventListener: mockRemoveEventListener, + }, + }, + writable: true, + configurable: true, + }); + + renderHook(() => useSendPWAStatus(true)); + + await Promise.resolve(); + + expect(mockPostMessage).not.toHaveBeenCalled(); + }); + + it('should send message on controllerchange event', async () => { + let controllerChangeHandler: (() => void) | undefined; + + mockAddEventListener.mockImplementation( + (event: string, handler: () => void) => { + if (event === 'controllerchange') { + controllerChangeHandler = handler; + } + }, + ); + + renderHook(() => useSendPWAStatus(true)); + + await Promise.resolve(); + mockPostMessage.mockClear(); + + expect(controllerChangeHandler).toBeDefined(); + (controllerChangeHandler as () => void)(); + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: 'PWA_STATUS', + isPWA: true, + }); + }); + + it('should update message when isPWA prop changes', async () => { + const { rerender } = renderHook(({ isPWA }) => useSendPWAStatus(isPWA), { + initialProps: { isPWA: false }, + }); + + await Promise.resolve(); + expect(mockPostMessage).toHaveBeenCalledWith({ + type: 'PWA_STATUS', + isPWA: false, + }); + + mockPostMessage.mockClear(); + rerender({ isPWA: true }); + + await Promise.resolve(); + expect(mockPostMessage).toHaveBeenCalledWith({ + type: 'PWA_STATUS', + isPWA: true, + }); + }); + + it('should handle server-side rendering gracefully', () => { + renderSSRHook(() => useSendPWAStatus(true)); + + expect(mockPostMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/hooks/useServiceWorkerRegistration/index.test.tsx b/src/app/hooks/useServiceWorkerRegistration/index.test.tsx new file mode 100644 index 00000000000..54a15f8f80c --- /dev/null +++ b/src/app/hooks/useServiceWorkerRegistration/index.test.tsx @@ -0,0 +1,189 @@ +import { renderHook } from '@testing-library/react'; +import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server'; +import useServiceWorkerRegistration from './index'; + +describe('useServiceWorkerRegistration', () => { + const mockRegister = jest.fn(); + let consoleWarnSpy: jest.SpyInstance; + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + + Object.defineProperty(global, 'navigator', { + value: { + serviceWorker: { + register: mockRegister, + }, + }, + writable: true, + configurable: true, + }); + + mockRegister.mockResolvedValue({}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should register service worker with correct path when service is provided', () => { + renderHook(() => useServiceWorkerRegistration('mundo')); + + expect(mockRegister).toHaveBeenCalledWith('/mundo/sw.js'); + }); + + it('should register service worker for different services', () => { + const { rerender } = renderHook( + ({ service }) => useServiceWorkerRegistration(service), + { + initialProps: { service: 'news' }, + }, + ); + + expect(mockRegister).toHaveBeenCalledWith('/news/sw.js'); + + mockRegister.mockClear(); + rerender({ service: 'sport' }); + + expect(mockRegister).toHaveBeenCalledWith('/sport/sw.js'); + }); + + it('should not register service worker when service is undefined', () => { + renderHook(() => useServiceWorkerRegistration(undefined)); + + expect(mockRegister).not.toHaveBeenCalled(); + }); + + it('should not register service worker when service is empty string', () => { + renderHook(() => useServiceWorkerRegistration('')); + + expect(mockRegister).not.toHaveBeenCalled(); + }); + + it('should not register when serviceWorker is not supported', () => { + Object.defineProperty(global, 'navigator', { + value: {}, + writable: true, + configurable: true, + }); + + renderHook(() => useServiceWorkerRegistration('mundo')); + + expect(mockRegister).not.toHaveBeenCalled(); + }); + + it('should warn and not register when register is not a function', () => { + Object.defineProperty(global, 'navigator', { + value: { + serviceWorker: { + register: null, + }, + }, + writable: true, + configurable: true, + }); + + renderHook(() => useServiceWorkerRegistration('mundo')); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'ServiceWorker API exists but register() is not available.', + ); + expect(mockRegister).not.toHaveBeenCalled(); + }); + + it('should handle registration errors gracefully', async () => { + const error = new Error('Registration failed'); + mockRegister.mockRejectedValue(error); + + renderHook(() => useServiceWorkerRegistration('mundo')); + + await Promise.resolve(); + await Promise.resolve(); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Service worker registration failed:', + error, + ); + }); + + it('should handle server-side rendering gracefully', () => { + renderSSRHook(() => useServiceWorkerRegistration('mundo')); + + expect(mockRegister).not.toHaveBeenCalled(); + }); + + it('should re-register when service prop changes', () => { + const { rerender } = renderHook( + ({ service }) => useServiceWorkerRegistration(service), + { + initialProps: { service: 'mundo' }, + }, + ); + + expect(mockRegister).toHaveBeenCalledTimes(1); + expect(mockRegister).toHaveBeenCalledWith('/mundo/sw.js'); + + mockRegister.mockClear(); + rerender({ service: 'news' }); + + expect(mockRegister).toHaveBeenCalledTimes(1); + expect(mockRegister).toHaveBeenCalledWith('/news/sw.js'); + }); + + it('should not re-register when service prop stays the same', () => { + const { rerender } = renderHook( + ({ service }) => useServiceWorkerRegistration(service), + { + initialProps: { service: 'mundo' }, + }, + ); + + expect(mockRegister).toHaveBeenCalledTimes(1); + + mockRegister.mockClear(); + rerender({ service: 'mundo' }); + + expect(mockRegister).not.toHaveBeenCalled(); + }); + + it('should handle registration promise that resolves with registration object', async () => { + const mockRegistration = { + installing: null, + waiting: null, + active: { state: 'activated' }, + }; + mockRegister.mockResolvedValue(mockRegistration); + + renderHook(() => useServiceWorkerRegistration('mundo')); + + expect(mockRegister).toHaveBeenCalledWith('/mundo/sw.js'); + + await Promise.resolve(); + + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); + + it('should handle synchronous registration error', async () => { + const error = new Error('Sync registration failed'); + mockRegister.mockImplementation(() => { + const rejection = Promise.reject(error); + rejection.catch(() => { + // Suppress unhandled rejection + }); + return rejection; + }); + + renderHook(() => useServiceWorkerRegistration('mundo')); + + await Promise.resolve(); + await Promise.resolve(); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Service worker registration failed:', + error, + ); + }); +}); From 1b42ee95f34546bc34b334980a15bf8053e66cc6 Mon Sep 17 00:00:00 2001 From: Dmytro Skumin Date: Thu, 18 Dec 2025 16:46:08 +0200 Subject: [PATCH 67/96] Update index.tsx --- src/app/components/Image/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/components/Image/index.tsx b/src/app/components/Image/index.tsx index 7d290cf405e..b0c995747c5 100644 --- a/src/app/components/Image/index.tsx +++ b/src/app/components/Image/index.tsx @@ -88,7 +88,6 @@ const Image = ({ }; const imgSrcSet = getImgSrcSet(); const imgSizes = getImgSizes(); - return ( <> {preload && ( From e0b3d5f624af1334f7f034ee2f6b057da2ca2b32 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Thu, 18 Dec 2025 16:35:45 +0200 Subject: [PATCH 68/96] Fix onload condition for promo images & add blurred bg --- data/mundo/topics/c7zp57yyz25t.json | 70 +++++++++++++++++++++++ data/ws/homePage/index.json | 15 +++++ src/app/components/Image/index.tsx | 15 ++++- src/app/legacy/components/Promo/image.jsx | 2 +- src/app/pages/TopicPage/index.styles.jsx | 6 ++ 5 files changed, 105 insertions(+), 3 deletions(-) diff --git a/data/mundo/topics/c7zp57yyz25t.json b/data/mundo/topics/c7zp57yyz25t.json index 93e8408bbc5..b71b3889484 100644 --- a/data/mundo/topics/c7zp57yyz25t.json +++ b/data/mundo/topics/c7zp57yyz25t.json @@ -6,6 +6,76 @@ "curations": [ { "summaries": [ + { + "type": "video", + "duration": "PT1M56S", + "isLive": false, + "title": "«واکنش به ربات‌نماهای کیش؛ از «آبروریزی جهانی» تا «بچه رباط کریم", + "firstPublished": "2025-11-16T20:29:46.464Z", + "lastPublished": "2025-11-16T20:29:46.464Z", + "link": "https://www.bbc.com/persian/articles/cn7e3651ydxo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dffd/live/936d9340-c32a-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "- «من یک داده هستم» ویدیوهای زن و مرد ربات‌نما در نمایشگاه فناوری کیش اینوکس در شبکه‌های اجتماعی ایران ترند شده است؛ تصاویری که ابتدا با تیترهای غلط‌انداز منتشر شد و در دو سه روز گذشته واکنش‌های بسیاری برانگیخته است که بیشتر آنها انتقادی یا طنزآلود است.", + "imageAlt": "زن و مرد با گریم ربات در نمایشگاه فناوری کیش", + "isPortraitImage": true, + "id": "cn7e3651ydxo" + }, + { + "type": "video", + "duration": "PT1M24S", + "isLive": false, + "title": "نمایشگاه «هفته دیزاین» در دانشگاه تهران با اعتراض بسیج دانشجویی لغو شد", + "firstPublished": "2025-11-16T15:29:22.961Z", + "lastPublished": "2025-11-16T15:29:22.961Z", + "link": "https://www.bbc.com/persian/articles/c4gk12nnye0o", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/a02c/live/7e65c320-c300-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "روابط‌عمومی دانشگاه تهران از لغو دو روز باقیمانده از نمایشگاه رویداد «هفته طراحی تهران» در دانشکده هنرهای زیبای این دانشگاه خبر داد. در پی پربازدید شدن ویدیوهایی از این رویداد و حضور بازدیدکنندگان مشتاق، بسیج دانشجویی این دانشگاه بیانیه‌ای اعتراضی صادر کرد. کمیته برگزاری این رویداد در اطلاعیه‌ای با تائید لغو نمایشگاه، به «نگرانی مسئولان دانشگاه از انضباط برگزاری و امنیت مهمانان» اشاره کرده است. در اطلاعیه دانشگاه تهران به «استقبال فراتر از تصور» و نگرانی از آسیب به بازدیدکنندگان بر اثر تراکم جمعیت اشاره شده است. رویداد «هفته دیزاین تهران» از ۲۰ تا ۲۶ آبان در نقاط مختلفی از پایتخت ایران از جمله در نمایشگاه بین‌المللی تهران، چندین گالری و موسسه هنری و فضاهای شهری در حال برگزاری است. دانشگاه تهران در بیانیه خود به ضرورت «نمایش فضای عادی زندگی اجتماعی پس از جنگ تحمیلی ۱۲ روزه» تاکید شده ولی آمده است که تراکم جمعیت بازدیدکنندگان، ممکن است آسیب‌های ایمنی مانند برق‌گرفتگی ایجاد کند. در این بیانیه همچنین به ورود افرادی غیر دانشجو و «عدم رعایت شئون دانشگاه و جامعه» اشاره شده است. بسیج دانشجویی پردیس هنرهای زیبا با انتشار بیانیه‌ای اعتراضی که خبرگزاری‌های منتقد دولت همچون فارس آن را پوشش دادند، این رویداد را «نماد افت استانداردهای علمی و انضباطی» در دانشگاه دانست. چندی پیش هم پربازدید شدن ویدیوهای حضور علاقمندان به اجرای خیابانی یک گروه راک در مرکز تهران، منجر به اعمال محدودیت بر اعضا و بسته شدن اینستاگرام آنها شد.", + "imageAlt": "پوستر هفته دیزاین تهران", + "isPortraitImage": true, + "id": "c4gk12nnye0o" + }, + { + "type": "video", + "duration": "PT20S", + "isLive": false, + "title": "ترکش‌های بوسه بدون رضایت؛ «عموی تنی» روبیالس به او تخم مرغ پرتاب کرد", + "firstPublished": "2025-11-14T19:42:34.380Z", + "lastPublished": "2025-11-14T19:42:34.380Z", + "link": "https://www.bbc.com/persian/articles/c9v19erz9rdo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/42f8/live/74262140-c191-11f0-8669-5560f5c90fbe.jpg.webp", + "description": "لوئیس روبیالس، رئیس پیشین فدراسیون فوتبال اسپانیا، ۱۳ نوامبر، ۲۲ آبان در مراسم رونمایی از کتاب جدیدش با عنوان «کشتن روبیالس» در مادرید، هدف پرتاب تخم‌مرغ قرار گرفت. او گفت این حمله به‌دست عمویش انجام شده است. روبیالس توضیح داد که ابتدا تصور کرده عمویش اسلحه دارد.", + "imageAlt": "لوئیس روبیالس", + "isPortraitImage": true, + "id": "c9v19erz9rdo" + }, + { + "type": "video", + "duration": "PT28S", + "isLive": false, + "title": "آشوب در مراسم اکران؛ همبازی آریانا گرانده از دست مرد مزاحم نجاتش داد", + "firstPublished": "2025-11-14T13:48:30.871Z", + "lastPublished": "2025-11-14T13:48:30.871Z", + "link": "https://www.bbc.com/persian/articles/c14pd0njkrlo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/cc12/live/4ae2bcc0-c160-11f0-8456-eff94716b162.jpg.webp", + "description": "در اکران افتتاحیه قسمت دوم فیلم ویکد که دیروز ۱۳ نوامبر، ۲۲ آبان در سنگاپور برگزار شد، مردی با عبور از حفاظ امنیتی بر فرش زرد مراسم پرید و تلاش کرد آریانا گرانده را بگیرد. بسیاری از هواداران و ناظران از سرعت عمل سینتیا اریوو در حائل شدن میان مرد مهاجم و همبازی‌ خود تقدیر کرده‌اند.", + "imageAlt": "آریانا گرانده و سینتیا اریوو", + "isPortraitImage": true, + "id": "c14pd0njkrlo" + }, + { + "type": "video", + "duration": "PT59S", + "isLive": false, + "title": "بازداشت دو مرد با لباس نظامی و پرچم شیر و خورشید در متروی تهران", + "firstPublished": "2025-11-13T18:21:35.585Z", + "lastPublished": "2025-11-13T18:21:35.585Z", + "link": "https://www.bbc.com/persian/articles/c4gw348pl30o", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/b1f2/live/cc166610-c0bc-11f0-8456-eff94716b162.jpg.webp", + "description": "به گزارش رسانه‌های ایران دو مرد که با لباس ارتشی و بلندگویی در دست، پرچم شیروخورشید نشان را در متروی تهران بلند کرده بودند، دستگیر شدند.", + "imageAlt": "دو مرد با لباس نظامی و پرچم شیر و خورشید در متروی تهران", + "isPortraitImage": true, + "id": "c4gw348pl30o" + }, { "type": "article", "title": "Las intrigantes caras talladas en rocas que quedaron expuestas por la grave sequía en el Amazonas", diff --git a/data/ws/homePage/index.json b/data/ws/homePage/index.json index 73e47be3dde..bbeabd736b3 100644 --- a/data/ws/homePage/index.json +++ b/data/ws/homePage/index.json @@ -5,6 +5,21 @@ "curations": [ { "summaries": [ + { + "type": "topic", + "duration": "PT1M56S", + "isLive": false, + "title": "«واکنش به ربات‌نماهای کیش؛ از «آبروریزی جهانی» تا «بچه رباط کریم", + "firstPublished": "2025-11-16T20:29:46.464Z", + "lastPublished": "2025-11-16T20:29:46.464Z", + "link": "https://www.bbc.com/persian/articles/cn7e3651ydxo", + "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dffd/live/936d9340-c32a-11f0-ae46-bd64331f0fd4.jpg.webp", + "description": "- «من یک داده هستم» ویدیوهای زن و مرد ربات‌نما در نمایشگاه فناوری کیش اینوکس در شبکه‌های اجتماعی ایران ترند شده است؛ تصاویری که ابتدا با تیترهای غلط‌انداز منتشر شد و در دو سه روز گذشته واکنش‌های بسیاری برانگیخته است که بیشتر آنها انتقادی یا طنزآلود است.", + "isPortraitImage": true, + "imageAlt": "زن و مرد با گریم ربات در نمایشگاه فناوری کیش", + "id": "cn7e3651ydxo", + "visualProminence": "MAXIMUM" + }, { "type": "topic", "isLive": false, diff --git a/src/app/components/Image/index.tsx b/src/app/components/Image/index.tsx index b0c995747c5..158a14fa148 100644 --- a/src/app/components/Image/index.tsx +++ b/src/app/components/Image/index.tsx @@ -1,4 +1,6 @@ -import { Fragment, PropsWithChildren, useState, use } from 'react'; +/** @jsx jsx */ +/* @jsxFrag React.Fragment */ +import { Fragment, PropsWithChildren, useState, use, useCallback } from 'react'; import { Global } from '@emotion/react'; import { Helmet } from 'react-helmet'; import styles from './index.styles'; @@ -57,6 +59,12 @@ const Image = ({ }: PropsWithChildren) => { const { pageType, isLite, isAmp } = use(RequestContext); const [isLoaded, setIsLoaded] = useState(false); + const handleImgRef = useCallback((img: HTMLImageElement | null) => { + if (!img) return; + if (img.complete) { + setIsLoaded(true); + } + }, []); if (isLite) return null; const showPlaceholder = placeholder && !isLoaded; @@ -163,7 +171,10 @@ const Image = ({ )} setIsLoaded(true)} + onLoad={() => { + setIsLoaded(true); + }} + ref={handleImgRef} src={src} {...(srcSet && { srcSet: imgSrcSet })} {...(imgSizes && { sizes: imgSizes })} diff --git a/src/app/legacy/components/Promo/image.jsx b/src/app/legacy/components/Promo/image.jsx index d177d385f4d..86ea21a7c98 100644 --- a/src/app/legacy/components/Promo/image.jsx +++ b/src/app/legacy/components/Promo/image.jsx @@ -108,4 +108,4 @@ const Image = props => { ); }; -export default Image; +export default Image; \ No newline at end of file diff --git a/src/app/pages/TopicPage/index.styles.jsx b/src/app/pages/TopicPage/index.styles.jsx index 1ed70975e14..f214a707d5f 100644 --- a/src/app/pages/TopicPage/index.styles.jsx +++ b/src/app/pages/TopicPage/index.styles.jsx @@ -7,6 +7,12 @@ const styles = { [mq.GROUP_2_MIN_WIDTH]: { margin: `0 ${spacings.DOUBLE}rem`, }, + // '.promo-image': { + // img: { + // background: 'black', + // objectFit: 'contain', + // }, + // }, }), inner: css({ maxWidth: '63rem', From 35c170aada82d8f4cbbb3e800a0035320c7e3325 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Thu, 18 Dec 2025 16:37:01 +0200 Subject: [PATCH 69/96] Move css into separate component, apply changes to MAPs --- src/app/legacy/components/Promo/image.jsx | 2 +- .../LatestMediaSection/LatestMediaItem/index.styles.ts | 4 ++++ src/app/pages/TopicPage/index.styles.jsx | 6 ------ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/legacy/components/Promo/image.jsx b/src/app/legacy/components/Promo/image.jsx index 86ea21a7c98..d177d385f4d 100644 --- a/src/app/legacy/components/Promo/image.jsx +++ b/src/app/legacy/components/Promo/image.jsx @@ -108,4 +108,4 @@ const Image = props => { ); }; -export default Image; \ No newline at end of file +export default Image; diff --git a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts index 51824e1e14d..12ec12e1d13 100644 --- a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts +++ b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts @@ -39,6 +39,10 @@ const styles = { [mq.GROUP_3_ONLY]: { width: '100%', }, + overflow: 'hidden', + ' > div > img': { + objectFit: 'contain', + }, }), portraitImage: () => css({ diff --git a/src/app/pages/TopicPage/index.styles.jsx b/src/app/pages/TopicPage/index.styles.jsx index f214a707d5f..1ed70975e14 100644 --- a/src/app/pages/TopicPage/index.styles.jsx +++ b/src/app/pages/TopicPage/index.styles.jsx @@ -7,12 +7,6 @@ const styles = { [mq.GROUP_2_MIN_WIDTH]: { margin: `0 ${spacings.DOUBLE}rem`, }, - // '.promo-image': { - // img: { - // background: 'black', - // objectFit: 'contain', - // }, - // }, }), inner: css({ maxWidth: '63rem', From 622a6218bfa1c6ec067b933f3e1fdb900e83e796 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Thu, 18 Dec 2025 16:37:16 +0200 Subject: [PATCH 70/96] added route for homepages as well as condition for page type header --- .../homepages/handleHomepageRoute.ts | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts diff --git a/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts new file mode 100644 index 00000000000..ec18aa4ca49 --- /dev/null +++ b/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts @@ -0,0 +1,112 @@ +import { GetServerSidePropsContext } from 'next'; +import extractHeaders from '#server/utilities/extractHeaders'; +import { HOME_PAGE } from '#app/routes/utils/pageTypes'; +import parseRoute from '#app/routes/utils/parseRoute'; +import nodeLogger from '#lib/logger.node'; +import { OK } from '#app/lib/statusCodes.const'; +import { ROUTING_INFORMATION } from '#app/lib/logger.const'; +import getPathExtension from '#app/utilities/getPathExtension'; +import PageDataParams from '#app/models/types/pageDataParams'; +import handleError from '#app/routes/utils/handleError'; +import { getServerExperiments } from '#server/utilities/experimentHeader'; +import shouldRender from '../articles/shouldRender'; +import getPageData from '../../../utilities/pageRequests/getPageData'; + +const logger = nodeLogger(__filename); + +export default async (context: GetServerSidePropsContext) => { + const { + resolvedUrl, + req: { headers: reqHeaders }, + } = context; + + const { service, renderer_env: rendererEnv } = + context.query as PageDataParams; + + const resolvedUrlWithoutQuery = resolvedUrl.split('?')?.[0]; + + const { isAmp, isApp, isLite } = getPathExtension(resolvedUrlWithoutQuery); + const { variant } = parseRoute(resolvedUrl); + + const { data, toggles } = await getPageData({ + id: resolvedUrlWithoutQuery, + service, + variant: variant || undefined, + rendererEnv, + resolvedUrl: resolvedUrlWithoutQuery, + pageType: HOME_PAGE, + isAmp, + }); + + const { pageData, status } = data; + + context.res.statusCode = status; + + let routingInfoLogger = logger.debug; + + const { hasRequestSucceeded, status: renderStatus } = shouldRender( + { pageData, status }, + service, + ); + + if (!hasRequestSucceeded && renderStatus !== OK) { + routingInfoLogger = logger.error; + + return { + props: { + isApp, + isAmp, + isLite, + isNextJs: true, + service, + status: renderStatus, + timeOnServer: Date.now(), + variant: variant || null, + pageType: HOME_PAGE, + pathname: resolvedUrlWithoutQuery, + toggles, + ...extractHeaders(reqHeaders), + }, + }; + } + + if (!pageData) { + throw handleError('HomePage data is malformed', 500); + } + + context.res.setHeader( + 'Cache-Control', + 'public, stale-if-error=90, stale-while-revalidate=30, max-age=60', + ); + + routingInfoLogger(ROUTING_INFORMATION, { + url: resolvedUrlWithoutQuery, + status, + pageType: HOME_PAGE, + }); + + const serverSideExperiments = getServerExperiments({ + headers: reqHeaders, + service, + pageType: HOME_PAGE, + }); + + return { + props: { + id: resolvedUrlWithoutQuery, + isAmp, + isApp, + isLite, + isNextJs: true, + pageData, + pageType: HOME_PAGE, + pathname: resolvedUrlWithoutQuery, + serverSideExperiments, + service, + status, + toggles, + variant: variant || null, + ...extractHeaders(reqHeaders), + }, + }; +}; From 8fbd336d7a359ecb0a5c5415d3a8aed2b716b2f1 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Thu, 18 Dec 2025 16:37:46 +0200 Subject: [PATCH 71/96] renamed folder to homepage and homepage return in derived page type --- .../pages/[service]/[[...]].page.tsx | 2 +- .../homepages/handleHomepageRoute.ts | 112 ------------------ 2 files changed, 1 insertion(+), 113 deletions(-) delete mode 100644 ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts diff --git a/ws-nextjs-app/pages/[service]/[[...]].page.tsx b/ws-nextjs-app/pages/[service]/[[...]].page.tsx index fea845f77fe..bd6d832ae25 100644 --- a/ws-nextjs-app/pages/[service]/[[...]].page.tsx +++ b/ws-nextjs-app/pages/[service]/[[...]].page.tsx @@ -130,4 +130,4 @@ export default function PageTypeToRender({ pageType, ...props }: PageProps) { // Return nothing, 404 is handled in _app.tsx return null; } -} \ No newline at end of file +} diff --git a/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts b/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts deleted file mode 100644 index ec18aa4ca49..00000000000 --- a/ws-nextjs-app/pages/[service]/homepages/handleHomepageRoute.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { GetServerSidePropsContext } from 'next'; -import extractHeaders from '#server/utilities/extractHeaders'; -import { HOME_PAGE } from '#app/routes/utils/pageTypes'; -import parseRoute from '#app/routes/utils/parseRoute'; -import nodeLogger from '#lib/logger.node'; -import { OK } from '#app/lib/statusCodes.const'; -import { ROUTING_INFORMATION } from '#app/lib/logger.const'; -import getPathExtension from '#app/utilities/getPathExtension'; -import PageDataParams from '#app/models/types/pageDataParams'; -import handleError from '#app/routes/utils/handleError'; -import { getServerExperiments } from '#server/utilities/experimentHeader'; -import shouldRender from '../articles/shouldRender'; -import getPageData from '../../../utilities/pageRequests/getPageData'; - -const logger = nodeLogger(__filename); - -export default async (context: GetServerSidePropsContext) => { - const { - resolvedUrl, - req: { headers: reqHeaders }, - } = context; - - const { service, renderer_env: rendererEnv } = - context.query as PageDataParams; - - const resolvedUrlWithoutQuery = resolvedUrl.split('?')?.[0]; - - const { isAmp, isApp, isLite } = getPathExtension(resolvedUrlWithoutQuery); - const { variant } = parseRoute(resolvedUrl); - - const { data, toggles } = await getPageData({ - id: resolvedUrlWithoutQuery, - service, - variant: variant || undefined, - rendererEnv, - resolvedUrl: resolvedUrlWithoutQuery, - pageType: HOME_PAGE, - isAmp, - }); - - const { pageData, status } = data; - - context.res.statusCode = status; - - let routingInfoLogger = logger.debug; - - const { hasRequestSucceeded, status: renderStatus } = shouldRender( - { pageData, status }, - service, - ); - - if (!hasRequestSucceeded && renderStatus !== OK) { - routingInfoLogger = logger.error; - - return { - props: { - isApp, - isAmp, - isLite, - isNextJs: true, - service, - status: renderStatus, - timeOnServer: Date.now(), - variant: variant || null, - pageType: HOME_PAGE, - pathname: resolvedUrlWithoutQuery, - toggles, - ...extractHeaders(reqHeaders), - }, - }; - } - - if (!pageData) { - throw handleError('HomePage data is malformed', 500); - } - - context.res.setHeader( - 'Cache-Control', - 'public, stale-if-error=90, stale-while-revalidate=30, max-age=60', - ); - - routingInfoLogger(ROUTING_INFORMATION, { - url: resolvedUrlWithoutQuery, - status, - pageType: HOME_PAGE, - }); - - const serverSideExperiments = getServerExperiments({ - headers: reqHeaders, - service, - pageType: HOME_PAGE, - }); - - return { - props: { - id: resolvedUrlWithoutQuery, - isAmp, - isApp, - isLite, - isNextJs: true, - pageData, - pageType: HOME_PAGE, - pathname: resolvedUrlWithoutQuery, - serverSideExperiments, - service, - status, - toggles, - variant: variant || null, - ...extractHeaders(reqHeaders), - }, - }; -}; From 9c3783a3cf97ea1a52e4c030701fd609bf097e69 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Fri, 19 Dec 2025 13:59:38 +0200 Subject: [PATCH 72/96] removing unrelated --- public/sw.js | 94 ++------- src/app/hooks/useSendPWAStatus/index.test.tsx | 184 ----------------- .../index.test.tsx | 189 ------------------ 3 files changed, 17 insertions(+), 450 deletions(-) delete mode 100644 src/app/hooks/useSendPWAStatus/index.test.tsx delete mode 100644 src/app/hooks/useServiceWorkerRegistration/index.test.tsx diff --git a/public/sw.js b/public/sw.js index 5d8b07943f0..e263bd121f7 100644 --- a/public/sw.js +++ b/public/sw.js @@ -44,13 +44,16 @@ const cacheOfflinePageAndResources = async service => { console.log(`[SW v${version}] Cached offline page for ${service}`); const html = await resp.text(); - const scriptSrcs = [ - ...html.matchAll(/]+src=["']([^"']+)["']/g), - ].map(m => m[1]); - const linkHrefs = [...html.matchAll(/]+href=["']([^"']+)["']/g)].map( - m => m[1], + const doc = new DOMParser().parseFromString(html, 'text/html'); + const scriptSrcs = Array.from(doc.querySelectorAll('script[src]')).map(el => + el.getAttribute('src'), ); + const linkHrefs = Array.from(doc.querySelectorAll('link[href]')).map(el => + el.getAttribute('href'), + ); + const resources = [...scriptSrcs, ...linkHrefs] + .filter(Boolean) .filter(url => url.startsWith('/') || url.startsWith(self.location.origin)) .map(url => new URL(url, self.location.origin).href); @@ -77,40 +80,9 @@ const WEBP_IMAGE = /^https:\/\/ichef(\.test)?\.bbci\.co\.uk\/(news|images|ace\/(standard|ws))\/.+.webp$/; // -------------Install event ------- -self.addEventListener('install', event => { +self.addEventListener('install', () => { console.log(`[SW v${version}] Installing...`); - - event.waitUntil( - (async () => { - const cache = await caches.open(cacheName); - const clients = await self.clients.matchAll({ type: 'window' }); - - // Get unique services from PWA clients only - const pwaServices = [ - ...new Set( - clients - .filter(client => pwaClients.get(client.id)) - .map(client => getServiceFromUrl(client.url)) - .filter(Boolean), - ), - ]; - - if (pwaServices.length > 0) { - console.log( - `[SW v${version}] Caching offline pages for PWA:`, - pwaServices, - ); - } - - // Cache offline pages for PWA services only - await Promise.allSettled( - pwaServices.map(async service => { - return cacheOfflinePageAndResources(service); - }), - ); - self.skipWaiting(); - })(), - ); + self.skipWaiting(); }); // -------Activate Handler------------- @@ -188,38 +160,12 @@ const fetchEventHandler = async event => { return response; })(), ); - } else if (event.request.url.includes('/_next/static/')) { - // Network-first for Next.js chunks (dev mode compatibility) - event.respondWith( - (async () => { - try { - const networkResp = await fetch(event.request); - const cache = await caches.open(cacheName); - cache.put(event.request, networkResp.clone()); - return networkResp; - } catch (err) { - const cache = await caches.open(cacheName); - const cachedResp = await cache.match(event.request); - if (cachedResp) return cachedResp; - throw err; - } - })(), - ); } else if (event.request.mode === 'navigate') { const { url } = event.request; + console.log(`[SW FETCH] Navigation: ${url}`); event.respondWith( (async () => { - const client = await self.clients.get(event.clientId); - const isPWA = client && pwaClients.get(client.id); - const cache = await caches.open(cacheName); - console.log(`[SW FETCH] Navigation: ${url} , isPWA: ${isPWA}`); - - const pwaMarkerExists = await cache.match('pwa_installed'); - if (!isPWA && pwaMarkerExists) { - await cache.delete('pwa_installed'); - } - try { // Use preload if available const preloadResp = await event.preloadResponse; @@ -230,8 +176,8 @@ const fetchEventHandler = async event => { // Cache offline page if in PWA mode if (networkResp && networkResp.ok && event.clientId) { console.log('[SW] Caching offline page if PWA if network is ok'); - // const client = await self.clients.get(event.clientId); - // const isPWA = client && pwaClients.get(client.id); + const client = await self.clients.get(event.clientId); + const isPWA = client && pwaClients.get(client.id); if (isPWA) { const service = getServiceFromUrl(url); cacheOfflinePageAndResources(service).catch(err => @@ -244,11 +190,12 @@ const fetchEventHandler = async event => { } catch (err) { console.log('[SW] Navigation failed:', url, err); + const cache = await caches.open(cacheName); const pwaMarker = await cache.match('pwa_installed'); console.log('[SW] PWA Marker:', pwaMarker); // Only show offline page for installed PWA - if (pwaMarker || isPWA) { + if (pwaMarker) { const service = getServiceFromUrl(url); const offlineUrl = new URL( getOfflinePageUrl(service), @@ -257,19 +204,12 @@ const fetchEventHandler = async event => { const cachedOffline = await cache.match(offlineUrl); if (cachedOffline) { - console.log('[SW] Serving cached offline page'); return cachedOffline; } } - // Canonical site offline fallback - return new Response( - 'You are offline. Please check your network and reload the page', - { - status: 503, - headers: { 'Content-Type': 'text/plain' }, - }, - ); + // Fallback to default browser behavior + throw err; } })(), ); diff --git a/src/app/hooks/useSendPWAStatus/index.test.tsx b/src/app/hooks/useSendPWAStatus/index.test.tsx deleted file mode 100644 index e88c893eab6..00000000000 --- a/src/app/hooks/useSendPWAStatus/index.test.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server'; -import useSendPWAStatus from './index'; - -describe('useSendPWAStatus', () => { - const mockPostMessage = jest.fn(); - const mockAddEventListener = jest.fn(); - const mockRemoveEventListener = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - - Object.defineProperty(global, 'navigator', { - value: { - serviceWorker: { - controller: { - postMessage: mockPostMessage, - state: 'activated', - }, - ready: Promise.resolve(), - addEventListener: mockAddEventListener, - removeEventListener: mockRemoveEventListener, - }, - }, - writable: true, - configurable: true, - }); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('should send PWA status message when SW is ready and isPWA is true', async () => { - renderHook(() => useSendPWAStatus(true)); - - await Promise.resolve(); - - expect(mockPostMessage).toHaveBeenCalledWith({ - type: 'PWA_STATUS', - isPWA: true, - }); - }); - - it('should send PWA status message when SW is ready and isPWA is false', async () => { - renderHook(() => useSendPWAStatus(false)); - - await Promise.resolve(); - - expect(mockPostMessage).toHaveBeenCalledWith({ - type: 'PWA_STATUS', - isPWA: false, - }); - }); - - it('should add controllerchange event listener', () => { - renderHook(() => useSendPWAStatus(true)); - - expect(mockAddEventListener).toHaveBeenCalledWith( - 'controllerchange', - expect.any(Function), - ); - }); - - it('should remove controllerchange event listener on unmount', () => { - const { unmount } = renderHook(() => useSendPWAStatus(true)); - - unmount(); - - expect(mockRemoveEventListener).toHaveBeenCalledWith( - 'controllerchange', - expect.any(Function), - ); - }); - - it('should not send message when serviceWorker is not available', () => { - Object.defineProperty(global, 'navigator', { - value: {}, - writable: true, - configurable: true, - }); - - renderHook(() => useSendPWAStatus(true)); - - expect(mockPostMessage).not.toHaveBeenCalled(); - }); - - it('should not send message when controller is not activated', async () => { - Object.defineProperty(global, 'navigator', { - value: { - serviceWorker: { - controller: { - postMessage: mockPostMessage, - state: 'installing', - }, - ready: Promise.resolve(), - addEventListener: mockAddEventListener, - removeEventListener: mockRemoveEventListener, - }, - }, - writable: true, - configurable: true, - }); - - renderHook(() => useSendPWAStatus(true)); - - await Promise.resolve(); - - expect(mockPostMessage).not.toHaveBeenCalled(); - }); - - it('should not send message when controller is null', async () => { - Object.defineProperty(global, 'navigator', { - value: { - serviceWorker: { - controller: null, - ready: Promise.resolve(), - addEventListener: mockAddEventListener, - removeEventListener: mockRemoveEventListener, - }, - }, - writable: true, - configurable: true, - }); - - renderHook(() => useSendPWAStatus(true)); - - await Promise.resolve(); - - expect(mockPostMessage).not.toHaveBeenCalled(); - }); - - it('should send message on controllerchange event', async () => { - let controllerChangeHandler: (() => void) | undefined; - - mockAddEventListener.mockImplementation( - (event: string, handler: () => void) => { - if (event === 'controllerchange') { - controllerChangeHandler = handler; - } - }, - ); - - renderHook(() => useSendPWAStatus(true)); - - await Promise.resolve(); - mockPostMessage.mockClear(); - - expect(controllerChangeHandler).toBeDefined(); - (controllerChangeHandler as () => void)(); - - expect(mockPostMessage).toHaveBeenCalledWith({ - type: 'PWA_STATUS', - isPWA: true, - }); - }); - - it('should update message when isPWA prop changes', async () => { - const { rerender } = renderHook(({ isPWA }) => useSendPWAStatus(isPWA), { - initialProps: { isPWA: false }, - }); - - await Promise.resolve(); - expect(mockPostMessage).toHaveBeenCalledWith({ - type: 'PWA_STATUS', - isPWA: false, - }); - - mockPostMessage.mockClear(); - rerender({ isPWA: true }); - - await Promise.resolve(); - expect(mockPostMessage).toHaveBeenCalledWith({ - type: 'PWA_STATUS', - isPWA: true, - }); - }); - - it('should handle server-side rendering gracefully', () => { - renderSSRHook(() => useSendPWAStatus(true)); - - expect(mockPostMessage).not.toHaveBeenCalled(); - }); -}); diff --git a/src/app/hooks/useServiceWorkerRegistration/index.test.tsx b/src/app/hooks/useServiceWorkerRegistration/index.test.tsx deleted file mode 100644 index 54a15f8f80c..00000000000 --- a/src/app/hooks/useServiceWorkerRegistration/index.test.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server'; -import useServiceWorkerRegistration from './index'; - -describe('useServiceWorkerRegistration', () => { - const mockRegister = jest.fn(); - let consoleWarnSpy: jest.SpyInstance; - let consoleErrorSpy: jest.SpyInstance; - - beforeEach(() => { - jest.clearAllMocks(); - consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); - - Object.defineProperty(global, 'navigator', { - value: { - serviceWorker: { - register: mockRegister, - }, - }, - writable: true, - configurable: true, - }); - - mockRegister.mockResolvedValue({}); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('should register service worker with correct path when service is provided', () => { - renderHook(() => useServiceWorkerRegistration('mundo')); - - expect(mockRegister).toHaveBeenCalledWith('/mundo/sw.js'); - }); - - it('should register service worker for different services', () => { - const { rerender } = renderHook( - ({ service }) => useServiceWorkerRegistration(service), - { - initialProps: { service: 'news' }, - }, - ); - - expect(mockRegister).toHaveBeenCalledWith('/news/sw.js'); - - mockRegister.mockClear(); - rerender({ service: 'sport' }); - - expect(mockRegister).toHaveBeenCalledWith('/sport/sw.js'); - }); - - it('should not register service worker when service is undefined', () => { - renderHook(() => useServiceWorkerRegistration(undefined)); - - expect(mockRegister).not.toHaveBeenCalled(); - }); - - it('should not register service worker when service is empty string', () => { - renderHook(() => useServiceWorkerRegistration('')); - - expect(mockRegister).not.toHaveBeenCalled(); - }); - - it('should not register when serviceWorker is not supported', () => { - Object.defineProperty(global, 'navigator', { - value: {}, - writable: true, - configurable: true, - }); - - renderHook(() => useServiceWorkerRegistration('mundo')); - - expect(mockRegister).not.toHaveBeenCalled(); - }); - - it('should warn and not register when register is not a function', () => { - Object.defineProperty(global, 'navigator', { - value: { - serviceWorker: { - register: null, - }, - }, - writable: true, - configurable: true, - }); - - renderHook(() => useServiceWorkerRegistration('mundo')); - - expect(consoleWarnSpy).toHaveBeenCalledWith( - 'ServiceWorker API exists but register() is not available.', - ); - expect(mockRegister).not.toHaveBeenCalled(); - }); - - it('should handle registration errors gracefully', async () => { - const error = new Error('Registration failed'); - mockRegister.mockRejectedValue(error); - - renderHook(() => useServiceWorkerRegistration('mundo')); - - await Promise.resolve(); - await Promise.resolve(); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Service worker registration failed:', - error, - ); - }); - - it('should handle server-side rendering gracefully', () => { - renderSSRHook(() => useServiceWorkerRegistration('mundo')); - - expect(mockRegister).not.toHaveBeenCalled(); - }); - - it('should re-register when service prop changes', () => { - const { rerender } = renderHook( - ({ service }) => useServiceWorkerRegistration(service), - { - initialProps: { service: 'mundo' }, - }, - ); - - expect(mockRegister).toHaveBeenCalledTimes(1); - expect(mockRegister).toHaveBeenCalledWith('/mundo/sw.js'); - - mockRegister.mockClear(); - rerender({ service: 'news' }); - - expect(mockRegister).toHaveBeenCalledTimes(1); - expect(mockRegister).toHaveBeenCalledWith('/news/sw.js'); - }); - - it('should not re-register when service prop stays the same', () => { - const { rerender } = renderHook( - ({ service }) => useServiceWorkerRegistration(service), - { - initialProps: { service: 'mundo' }, - }, - ); - - expect(mockRegister).toHaveBeenCalledTimes(1); - - mockRegister.mockClear(); - rerender({ service: 'mundo' }); - - expect(mockRegister).not.toHaveBeenCalled(); - }); - - it('should handle registration promise that resolves with registration object', async () => { - const mockRegistration = { - installing: null, - waiting: null, - active: { state: 'activated' }, - }; - mockRegister.mockResolvedValue(mockRegistration); - - renderHook(() => useServiceWorkerRegistration('mundo')); - - expect(mockRegister).toHaveBeenCalledWith('/mundo/sw.js'); - - await Promise.resolve(); - - expect(consoleErrorSpy).not.toHaveBeenCalled(); - }); - - it('should handle synchronous registration error', async () => { - const error = new Error('Sync registration failed'); - mockRegister.mockImplementation(() => { - const rejection = Promise.reject(error); - rejection.catch(() => { - // Suppress unhandled rejection - }); - return rejection; - }); - - renderHook(() => useServiceWorkerRegistration('mundo')); - - await Promise.resolve(); - await Promise.resolve(); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - 'Service worker registration failed:', - error, - ); - }); -}); From a6ddf13c28f20f4a04c2fe7779f2a28de8db14fd Mon Sep 17 00:00:00 2001 From: skumid01 Date: Fri, 19 Dec 2025 14:01:57 +0200 Subject: [PATCH 73/96] removing unrelated --- data/mundo/topics/c7zp57yyz25t.json | 70 ----------------------------- data/ws/homePage/index.json | 15 ------- 2 files changed, 85 deletions(-) diff --git a/data/mundo/topics/c7zp57yyz25t.json b/data/mundo/topics/c7zp57yyz25t.json index b71b3889484..93e8408bbc5 100644 --- a/data/mundo/topics/c7zp57yyz25t.json +++ b/data/mundo/topics/c7zp57yyz25t.json @@ -6,76 +6,6 @@ "curations": [ { "summaries": [ - { - "type": "video", - "duration": "PT1M56S", - "isLive": false, - "title": "«واکنش به ربات‌نماهای کیش؛ از «آبروریزی جهانی» تا «بچه رباط کریم", - "firstPublished": "2025-11-16T20:29:46.464Z", - "lastPublished": "2025-11-16T20:29:46.464Z", - "link": "https://www.bbc.com/persian/articles/cn7e3651ydxo", - "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dffd/live/936d9340-c32a-11f0-ae46-bd64331f0fd4.jpg.webp", - "description": "- «من یک داده هستم» ویدیوهای زن و مرد ربات‌نما در نمایشگاه فناوری کیش اینوکس در شبکه‌های اجتماعی ایران ترند شده است؛ تصاویری که ابتدا با تیترهای غلط‌انداز منتشر شد و در دو سه روز گذشته واکنش‌های بسیاری برانگیخته است که بیشتر آنها انتقادی یا طنزآلود است.", - "imageAlt": "زن و مرد با گریم ربات در نمایشگاه فناوری کیش", - "isPortraitImage": true, - "id": "cn7e3651ydxo" - }, - { - "type": "video", - "duration": "PT1M24S", - "isLive": false, - "title": "نمایشگاه «هفته دیزاین» در دانشگاه تهران با اعتراض بسیج دانشجویی لغو شد", - "firstPublished": "2025-11-16T15:29:22.961Z", - "lastPublished": "2025-11-16T15:29:22.961Z", - "link": "https://www.bbc.com/persian/articles/c4gk12nnye0o", - "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/a02c/live/7e65c320-c300-11f0-ae46-bd64331f0fd4.jpg.webp", - "description": "روابط‌عمومی دانشگاه تهران از لغو دو روز باقیمانده از نمایشگاه رویداد «هفته طراحی تهران» در دانشکده هنرهای زیبای این دانشگاه خبر داد. در پی پربازدید شدن ویدیوهایی از این رویداد و حضور بازدیدکنندگان مشتاق، بسیج دانشجویی این دانشگاه بیانیه‌ای اعتراضی صادر کرد. کمیته برگزاری این رویداد در اطلاعیه‌ای با تائید لغو نمایشگاه، به «نگرانی مسئولان دانشگاه از انضباط برگزاری و امنیت مهمانان» اشاره کرده است. در اطلاعیه دانشگاه تهران به «استقبال فراتر از تصور» و نگرانی از آسیب به بازدیدکنندگان بر اثر تراکم جمعیت اشاره شده است. رویداد «هفته دیزاین تهران» از ۲۰ تا ۲۶ آبان در نقاط مختلفی از پایتخت ایران از جمله در نمایشگاه بین‌المللی تهران، چندین گالری و موسسه هنری و فضاهای شهری در حال برگزاری است. دانشگاه تهران در بیانیه خود به ضرورت «نمایش فضای عادی زندگی اجتماعی پس از جنگ تحمیلی ۱۲ روزه» تاکید شده ولی آمده است که تراکم جمعیت بازدیدکنندگان، ممکن است آسیب‌های ایمنی مانند برق‌گرفتگی ایجاد کند. در این بیانیه همچنین به ورود افرادی غیر دانشجو و «عدم رعایت شئون دانشگاه و جامعه» اشاره شده است. بسیج دانشجویی پردیس هنرهای زیبا با انتشار بیانیه‌ای اعتراضی که خبرگزاری‌های منتقد دولت همچون فارس آن را پوشش دادند، این رویداد را «نماد افت استانداردهای علمی و انضباطی» در دانشگاه دانست. چندی پیش هم پربازدید شدن ویدیوهای حضور علاقمندان به اجرای خیابانی یک گروه راک در مرکز تهران، منجر به اعمال محدودیت بر اعضا و بسته شدن اینستاگرام آنها شد.", - "imageAlt": "پوستر هفته دیزاین تهران", - "isPortraitImage": true, - "id": "c4gk12nnye0o" - }, - { - "type": "video", - "duration": "PT20S", - "isLive": false, - "title": "ترکش‌های بوسه بدون رضایت؛ «عموی تنی» روبیالس به او تخم مرغ پرتاب کرد", - "firstPublished": "2025-11-14T19:42:34.380Z", - "lastPublished": "2025-11-14T19:42:34.380Z", - "link": "https://www.bbc.com/persian/articles/c9v19erz9rdo", - "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/42f8/live/74262140-c191-11f0-8669-5560f5c90fbe.jpg.webp", - "description": "لوئیس روبیالس، رئیس پیشین فدراسیون فوتبال اسپانیا، ۱۳ نوامبر، ۲۲ آبان در مراسم رونمایی از کتاب جدیدش با عنوان «کشتن روبیالس» در مادرید، هدف پرتاب تخم‌مرغ قرار گرفت. او گفت این حمله به‌دست عمویش انجام شده است. روبیالس توضیح داد که ابتدا تصور کرده عمویش اسلحه دارد.", - "imageAlt": "لوئیس روبیالس", - "isPortraitImage": true, - "id": "c9v19erz9rdo" - }, - { - "type": "video", - "duration": "PT28S", - "isLive": false, - "title": "آشوب در مراسم اکران؛ همبازی آریانا گرانده از دست مرد مزاحم نجاتش داد", - "firstPublished": "2025-11-14T13:48:30.871Z", - "lastPublished": "2025-11-14T13:48:30.871Z", - "link": "https://www.bbc.com/persian/articles/c14pd0njkrlo", - "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/cc12/live/4ae2bcc0-c160-11f0-8456-eff94716b162.jpg.webp", - "description": "در اکران افتتاحیه قسمت دوم فیلم ویکد که دیروز ۱۳ نوامبر، ۲۲ آبان در سنگاپور برگزار شد، مردی با عبور از حفاظ امنیتی بر فرش زرد مراسم پرید و تلاش کرد آریانا گرانده را بگیرد. بسیاری از هواداران و ناظران از سرعت عمل سینتیا اریوو در حائل شدن میان مرد مهاجم و همبازی‌ خود تقدیر کرده‌اند.", - "imageAlt": "آریانا گرانده و سینتیا اریوو", - "isPortraitImage": true, - "id": "c14pd0njkrlo" - }, - { - "type": "video", - "duration": "PT59S", - "isLive": false, - "title": "بازداشت دو مرد با لباس نظامی و پرچم شیر و خورشید در متروی تهران", - "firstPublished": "2025-11-13T18:21:35.585Z", - "lastPublished": "2025-11-13T18:21:35.585Z", - "link": "https://www.bbc.com/persian/articles/c4gw348pl30o", - "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/b1f2/live/cc166610-c0bc-11f0-8456-eff94716b162.jpg.webp", - "description": "به گزارش رسانه‌های ایران دو مرد که با لباس ارتشی و بلندگویی در دست، پرچم شیروخورشید نشان را در متروی تهران بلند کرده بودند، دستگیر شدند.", - "imageAlt": "دو مرد با لباس نظامی و پرچم شیر و خورشید در متروی تهران", - "isPortraitImage": true, - "id": "c4gw348pl30o" - }, { "type": "article", "title": "Las intrigantes caras talladas en rocas que quedaron expuestas por la grave sequía en el Amazonas", diff --git a/data/ws/homePage/index.json b/data/ws/homePage/index.json index bbeabd736b3..73e47be3dde 100644 --- a/data/ws/homePage/index.json +++ b/data/ws/homePage/index.json @@ -5,21 +5,6 @@ "curations": [ { "summaries": [ - { - "type": "topic", - "duration": "PT1M56S", - "isLive": false, - "title": "«واکنش به ربات‌نماهای کیش؛ از «آبروریزی جهانی» تا «بچه رباط کریم", - "firstPublished": "2025-11-16T20:29:46.464Z", - "lastPublished": "2025-11-16T20:29:46.464Z", - "link": "https://www.bbc.com/persian/articles/cn7e3651ydxo", - "imageUrl": "https://ichef.bbci.co.uk/ace/ws/{width}/cpsprodpb/dffd/live/936d9340-c32a-11f0-ae46-bd64331f0fd4.jpg.webp", - "description": "- «من یک داده هستم» ویدیوهای زن و مرد ربات‌نما در نمایشگاه فناوری کیش اینوکس در شبکه‌های اجتماعی ایران ترند شده است؛ تصاویری که ابتدا با تیترهای غلط‌انداز منتشر شد و در دو سه روز گذشته واکنش‌های بسیاری برانگیخته است که بیشتر آنها انتقادی یا طنزآلود است.", - "isPortraitImage": true, - "imageAlt": "زن و مرد با گریم ربات در نمایشگاه فناوری کیش", - "id": "cn7e3651ydxo", - "visualProminence": "MAXIMUM" - }, { "type": "topic", "isLive": false, From 81614e5437750d28b1c20f7496b4418d444eaeaf Mon Sep 17 00:00:00 2001 From: skumid01 Date: Fri, 19 Dec 2025 14:02:38 +0200 Subject: [PATCH 74/96] removing unrelated --- public/sw.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sw.js b/public/sw.js index e263bd121f7..6b06642eaaa 100644 --- a/public/sw.js +++ b/public/sw.js @@ -218,4 +218,4 @@ const fetchEventHandler = async event => { return; }; -onfetch = fetchEventHandler; \ No newline at end of file +onfetch = fetchEventHandler; From 0db80e1c8e8a7dd6b65d2028f2d5ea7efbac83eb Mon Sep 17 00:00:00 2001 From: skumid01 Date: Fri, 19 Dec 2025 14:04:37 +0200 Subject: [PATCH 75/96] removing unrelated --- src/app/components/Image/index.tsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/app/components/Image/index.tsx b/src/app/components/Image/index.tsx index 158a14fa148..b0c995747c5 100644 --- a/src/app/components/Image/index.tsx +++ b/src/app/components/Image/index.tsx @@ -1,6 +1,4 @@ -/** @jsx jsx */ -/* @jsxFrag React.Fragment */ -import { Fragment, PropsWithChildren, useState, use, useCallback } from 'react'; +import { Fragment, PropsWithChildren, useState, use } from 'react'; import { Global } from '@emotion/react'; import { Helmet } from 'react-helmet'; import styles from './index.styles'; @@ -59,12 +57,6 @@ const Image = ({ }: PropsWithChildren) => { const { pageType, isLite, isAmp } = use(RequestContext); const [isLoaded, setIsLoaded] = useState(false); - const handleImgRef = useCallback((img: HTMLImageElement | null) => { - if (!img) return; - if (img.complete) { - setIsLoaded(true); - } - }, []); if (isLite) return null; const showPlaceholder = placeholder && !isLoaded; @@ -171,10 +163,7 @@ const Image = ({ )} { - setIsLoaded(true); - }} - ref={handleImgRef} + onLoad={() => setIsLoaded(true)} src={src} {...(srcSet && { srcSet: imgSrcSet })} {...(imgSizes && { sizes: imgSizes })} From 02d61047545d3636ca3dcdc0dfc9c0d6cf6b2382 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Fri, 19 Dec 2025 14:05:28 +0200 Subject: [PATCH 76/96] removing unrelated --- .../LatestMediaSection/LatestMediaItem/index.styles.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts index 12ec12e1d13..51824e1e14d 100644 --- a/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts +++ b/src/app/pages/MediaArticlePage/PagePromoSections/LatestMediaSection/LatestMediaItem/index.styles.ts @@ -39,10 +39,6 @@ const styles = { [mq.GROUP_3_ONLY]: { width: '100%', }, - overflow: 'hidden', - ' > div > img': { - objectFit: 'contain', - }, }), portraitImage: () => css({ From edab507e7592615527a434e3570928ee01a32811 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Fri, 19 Dec 2025 15:48:25 +0200 Subject: [PATCH 77/96] updating audit --- package.json | 12 +- yarn.lock | 597 ++++++++++++++++++++++++--------------------------- 2 files changed, 284 insertions(+), 325 deletions(-) diff --git a/package.json b/package.json index d341c4ab7f1..61c1058b1d3 100644 --- a/package.json +++ b/package.json @@ -164,11 +164,11 @@ "@jest/globals": "30.2.0", "@loadable/babel-plugin": "5.16.1", "@loadable/webpack-plugin": "5.15.2", - "@storybook/addon-a11y": "10.0.8", - "@storybook/addon-docs": "10.0.8", - "@storybook/builder-webpack5": "10.0.8", - "@storybook/cli": "10.0.8", - "@storybook/react-webpack5": "10.0.8", + "@storybook/addon-a11y": "10.1.10", + "@storybook/addon-docs": "10.1.10", + "@storybook/builder-webpack5": "10.1.10", + "@storybook/cli": "10.1.10", + "@storybook/react-webpack5": "10.1.10", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.0", @@ -237,7 +237,7 @@ "puppeteer": "24.22.3", "retry": "0.13.1", "start-server-nestjs-webpack-plugin": "2.2.5", - "storybook": "10.0.8", + "storybook": "10.1.10", "stream-browserify": "3.0.0", "strip-ansi": "7.1.2", "supertest": "7.1.4", diff --git a/yarn.lock b/yarn.lock index 76486e9a7c2..c79563eaa31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -127,7 +127,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:7.28.5": +"@babel/core@npm:7.28.5, @babel/core@npm:^7.28.0": version: 7.28.5 resolution: "@babel/core@npm:7.28.5" dependencies: @@ -2709,41 +2709,34 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/aix-ppc64@npm:0.25.6" +"@esbuild/aix-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/aix-ppc64@npm:0.27.2" conditions: os=aix & cpu=ppc64 languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/android-arm64@npm:0.25.6" +"@esbuild/android-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm64@npm:0.27.2" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@esbuild/android-arm@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/android-arm@npm:0.25.6" +"@esbuild/android-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm@npm:0.27.2" conditions: os=android & cpu=arm languageName: node linkType: hard -"@esbuild/android-x64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/android-x64@npm:0.25.6" +"@esbuild/android-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-x64@npm:0.27.2" conditions: os=android & cpu=x64 languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/darwin-arm64@npm:0.25.6" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.27.0": version: 0.27.0 resolution: "@esbuild/darwin-arm64@npm:0.27.0" @@ -2752,10 +2745,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/darwin-x64@npm:0.25.6" - conditions: os=darwin & cpu=x64 +"@esbuild/darwin-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-arm64@npm:0.27.2" + conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -2767,24 +2760,24 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/freebsd-arm64@npm:0.25.6" - conditions: os=freebsd & cpu=arm64 +"@esbuild/darwin-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-x64@npm:0.27.2" + conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/freebsd-x64@npm:0.25.6" - conditions: os=freebsd & cpu=x64 +"@esbuild/freebsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-arm64@npm:0.27.2" + conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/linux-arm64@npm:0.25.6" - conditions: os=linux & cpu=arm64 +"@esbuild/freebsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-x64@npm:0.27.2" + conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -2796,62 +2789,62 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/linux-arm@npm:0.25.6" +"@esbuild/linux-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm64@npm:0.27.2" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm@npm:0.27.2" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/linux-ia32@npm:0.25.6" +"@esbuild/linux-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ia32@npm:0.27.2" conditions: os=linux & cpu=ia32 languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/linux-loong64@npm:0.25.6" +"@esbuild/linux-loong64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-loong64@npm:0.27.2" conditions: os=linux & cpu=loong64 languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/linux-mips64el@npm:0.25.6" +"@esbuild/linux-mips64el@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-mips64el@npm:0.27.2" conditions: os=linux & cpu=mips64el languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/linux-ppc64@npm:0.25.6" +"@esbuild/linux-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ppc64@npm:0.27.2" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/linux-riscv64@npm:0.25.6" +"@esbuild/linux-riscv64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-riscv64@npm:0.27.2" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/linux-s390x@npm:0.25.6" +"@esbuild/linux-s390x@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-s390x@npm:0.27.2" conditions: os=linux & cpu=s390x languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/linux-x64@npm:0.25.6" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.27.0": version: 0.27.0 resolution: "@esbuild/linux-x64@npm:0.27.0" @@ -2860,55 +2853,55 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/netbsd-arm64@npm:0.25.6" +"@esbuild/linux-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-x64@npm:0.27.2" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-arm64@npm:0.27.2" conditions: os=netbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/netbsd-x64@npm:0.25.6" +"@esbuild/netbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-x64@npm:0.27.2" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/openbsd-arm64@npm:0.25.6" +"@esbuild/openbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-arm64@npm:0.27.2" conditions: os=openbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/openbsd-x64@npm:0.25.6" +"@esbuild/openbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-x64@npm:0.27.2" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openharmony-arm64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/openharmony-arm64@npm:0.25.6" +"@esbuild/openharmony-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openharmony-arm64@npm:0.27.2" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/sunos-x64@npm:0.25.6" +"@esbuild/sunos-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/sunos-x64@npm:0.27.2" conditions: os=sunos & cpu=x64 languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/win32-arm64@npm:0.25.6" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.27.0": version: 0.27.0 resolution: "@esbuild/win32-arm64@npm:0.27.0" @@ -2917,17 +2910,17 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/win32-ia32@npm:0.25.6" - conditions: os=win32 & cpu=ia32 +"@esbuild/win32-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-arm64@npm:0.27.2" + conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.25.6": - version: 0.25.6 - resolution: "@esbuild/win32-x64@npm:0.25.6" - conditions: os=win32 & cpu=x64 +"@esbuild/win32-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-ia32@npm:0.27.2" + conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -2939,6 +2932,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-x64@npm:0.27.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0": version: 4.7.0 resolution: "@eslint-community/eslint-utils@npm:4.7.0" @@ -4664,40 +4664,41 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-a11y@npm:10.0.8": - version: 10.0.8 - resolution: "@storybook/addon-a11y@npm:10.0.8" +"@storybook/addon-a11y@npm:10.1.10": + version: 10.1.10 + resolution: "@storybook/addon-a11y@npm:10.1.10" dependencies: "@storybook/global": "npm:^5.0.0" axe-core: "npm:^4.2.0" peerDependencies: - storybook: ^10.0.8 - checksum: 10/5d0c7647c0ce96fef51cd06ef7100bb9d9d823c2e3e8a6273fb4484bb3e88e5c1391c2fd83983a4c0c179472ea963a5492711561bf2feaca9ad909a4d6418768 + storybook: ^10.1.10 + checksum: 10/793368ed69ee445719fd43583d7277d63628ae72c7325bc03ba6d3d11733689dabe2b4a428cb8b9b7123783fe56118283b706ff502f432723948aa1f98cac1ee languageName: node linkType: hard -"@storybook/addon-docs@npm:10.0.8": - version: 10.0.8 - resolution: "@storybook/addon-docs@npm:10.0.8" +"@storybook/addon-docs@npm:10.1.10": + version: 10.1.10 + resolution: "@storybook/addon-docs@npm:10.1.10" dependencies: "@mdx-js/react": "npm:^3.0.0" - "@storybook/csf-plugin": "npm:10.0.8" - "@storybook/icons": "npm:^1.6.0" - "@storybook/react-dom-shim": "npm:10.0.8" + "@storybook/csf-plugin": "npm:10.1.10" + "@storybook/icons": "npm:^2.0.0" + "@storybook/react-dom-shim": "npm:10.1.10" react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" react-dom: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^10.0.8 - checksum: 10/18494916a3da93674487a09aa0ae2fb813b2842de046a4f8d896e6e844095d0e02fb875ea14076cdcde26a59ee1c7c939c7255c447c3367f202fe816b8306e8a + storybook: ^10.1.10 + checksum: 10/e6dd2e6f2c1795bf83a639f6e7a22128b647e4a251fd03eba239733d0421e69b74ab672484dd313ef1cb616f95db36340e6b1a6d5f74c113970396ba2879fd72 languageName: node linkType: hard -"@storybook/builder-webpack5@npm:10.0.8": - version: 10.0.8 - resolution: "@storybook/builder-webpack5@npm:10.0.8" +"@storybook/builder-webpack5@npm:10.1.10": + version: 10.1.10 + resolution: "@storybook/builder-webpack5@npm:10.1.10" dependencies: - "@storybook/core-webpack": "npm:10.0.8" + "@storybook/core-webpack": "npm:10.1.10" + "@vitest/mocker": "npm:3.2.4" case-sensitive-paths-webpack-plugin: "npm:^2.4.0" cjs-module-lexer: "npm:^1.2.3" css-loader: "npm:^7.1.2" @@ -4713,68 +4714,67 @@ __metadata: webpack-hot-middleware: "npm:^2.25.1" webpack-virtual-modules: "npm:^0.6.0" peerDependencies: - storybook: ^10.0.8 + storybook: ^10.1.10 peerDependenciesMeta: typescript: optional: true - checksum: 10/136ad58f58a3595d443fa4bd3a5dd9cca30a52a59c3a34e82d1b32b1be82e292b02d3c4b74204b1e5d92c3bab3de86a164ae69f19671a52408da5cc14f550720 + checksum: 10/e8f19e4955c6e313578b0669f7f771cfff33d8f3cb353cb5a6d730dad94e3b05432f03410dc0fd2b11afc78f81b1ef7bdf903a0f8f8b3d00b5a632e743a54405 languageName: node linkType: hard -"@storybook/cli@npm:10.0.8": - version: 10.0.8 - resolution: "@storybook/cli@npm:10.0.8" +"@storybook/cli@npm:10.1.10": + version: 10.1.10 + resolution: "@storybook/cli@npm:10.1.10" dependencies: - "@storybook/codemod": "npm:10.0.8" + "@storybook/codemod": "npm:10.1.10" "@types/semver": "npm:^7.3.4" commander: "npm:^14.0.1" - create-storybook: "npm:10.0.8" - giget: "npm:^2.0.0" + create-storybook: "npm:10.1.10" jscodeshift: "npm:^0.15.1" - storybook: "npm:10.0.8" + storybook: "npm:10.1.10" ts-dedent: "npm:^2.0.0" bin: cli: ./dist/bin/index.js - checksum: 10/500a381ee7b2e4aa0c8285850079572c4757f662581442816df32d7e9cf891caa8ee168028095d9769bb2066776eea5a37ad776ac92576ab61a32e61cc342a8f + checksum: 10/a019ce2cbbfbeea26e162549aed2ad4a869d8e7a69ff4da736574580da3ef3f0a4595e30ef6d7aec830b71886c49bd6eb5a889a81c1897f55c04bd72e1b5aca3 languageName: node linkType: hard -"@storybook/codemod@npm:10.0.8": - version: 10.0.8 - resolution: "@storybook/codemod@npm:10.0.8" +"@storybook/codemod@npm:10.1.10": + version: 10.1.10 + resolution: "@storybook/codemod@npm:10.1.10" dependencies: "@types/cross-spawn": "npm:^6.0.6" cross-spawn: "npm:^7.0.6" es-toolkit: "npm:^1.36.0" jscodeshift: "npm:^0.15.1" prettier: "npm:^3.5.3" - storybook: "npm:10.0.8" + storybook: "npm:10.1.10" tiny-invariant: "npm:^1.3.1" tinyglobby: "npm:^0.2.13" - checksum: 10/5526252ce068be9693b5c6ea51d28a7727d1bffb61746d8421c2351ef41da9c816948f3a0d4946770479b56479913bc20f4baac15e2983dca32e8a89a0bfea80 + checksum: 10/29650a158530af8756aff4b898811750833be2535a863b23e8d4d3606e0e18587c4f687e4546dafbe1df5f46316da59d22786313dbbdd932af5477da421b80af languageName: node linkType: hard -"@storybook/core-webpack@npm:10.0.8": - version: 10.0.8 - resolution: "@storybook/core-webpack@npm:10.0.8" +"@storybook/core-webpack@npm:10.1.10": + version: 10.1.10 + resolution: "@storybook/core-webpack@npm:10.1.10" dependencies: ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^10.0.8 - checksum: 10/b71baea4b0c929d1173c39bf287be94fd2862c91cf0945bed62331a4ed2b6f96dc9273b1d8b229e885cfd260d0cac7d7a8a86582a20d95c5bf27669be198752e + storybook: ^10.1.10 + checksum: 10/42444753269754c411ee7ba70f75b69715e0b00f6326b1168a956dbc216088eaa9efac3377ed57b22e03eeac14729c8614a9ef25e6ade4f45da45a2ffab94240 languageName: node linkType: hard -"@storybook/csf-plugin@npm:10.0.8": - version: 10.0.8 - resolution: "@storybook/csf-plugin@npm:10.0.8" +"@storybook/csf-plugin@npm:10.1.10": + version: 10.1.10 + resolution: "@storybook/csf-plugin@npm:10.1.10" dependencies: unplugin: "npm:^2.3.5" peerDependencies: esbuild: "*" rollup: "*" - storybook: ^10.0.8 + storybook: ^10.1.10 vite: "*" webpack: "*" peerDependenciesMeta: @@ -4786,7 +4786,7 @@ __metadata: optional: true webpack: optional: true - checksum: 10/bd6bf26b4201be3db8d3ac96740c861cd91140eaf4d8cba463c9ad691a60dafbff820663c648f7ce0082310fccf45133555ed944dff7e1fb7b2db4d21f2a78b2 + checksum: 10/66a7dce037818d63f0fc73be7134125374076f53add953e211b1c2c290ee27d222c0c2dba5122f6557e0eefaae058b0609ad51ed680100fa004e482394f8c159 languageName: node linkType: hard @@ -4797,21 +4797,21 @@ __metadata: languageName: node linkType: hard -"@storybook/icons@npm:^1.6.0": - version: 1.6.0 - resolution: "@storybook/icons@npm:1.6.0" +"@storybook/icons@npm:^2.0.0": + version: 2.0.1 + resolution: "@storybook/icons@npm:2.0.1" peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - checksum: 10/f9036ca3b0d2904778eb4e202305f2780b549434380f9760f0bc704fe3ee19d7332f9560a66435ebb2156346cee9a863e40fa5e4b27790bf993b0c1180a3146d + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/04ffa285f4defc611def51f82492688bc49f6f4e8ce4e7ba5c99a1c1410b7e8820b5da65c33610a497df2409de7b48fae399052c5cacab6a4a4a9b48a36ebfd5 languageName: node linkType: hard -"@storybook/preset-react-webpack@npm:10.0.8": - version: 10.0.8 - resolution: "@storybook/preset-react-webpack@npm:10.0.8" +"@storybook/preset-react-webpack@npm:10.1.10": + version: 10.1.10 + resolution: "@storybook/preset-react-webpack@npm:10.1.10" dependencies: - "@storybook/core-webpack": "npm:10.0.8" + "@storybook/core-webpack": "npm:10.1.10" "@storybook/react-docgen-typescript-plugin": "npm:1.0.6--canary.9.0c3f3b7.0" "@types/semver": "npm:^7.3.4" magic-string: "npm:^0.30.5" @@ -4823,11 +4823,11 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.0.8 + storybook: ^10.1.10 peerDependenciesMeta: typescript: optional: true - checksum: 10/b361b27b1550db1f03888fb06596cee35e1b6724aa41eafc23e47b28623d64c5ec1d33d0120f0280e49064a573144ccf8de114014f1d2f23730045be0ac86773 + checksum: 10/096891a86660d18bcb92a054212aaa6977a6cab656607bbfd1a10e748828692a6a4e0ee6cb58aa94862aa8a269ef413a00f985c3ce8ded5a48566c65b1da6bbb languageName: node linkType: hard @@ -4849,51 +4849,52 @@ __metadata: languageName: node linkType: hard -"@storybook/react-dom-shim@npm:10.0.8": - version: 10.0.8 - resolution: "@storybook/react-dom-shim@npm:10.0.8" +"@storybook/react-dom-shim@npm:10.1.10": + version: 10.1.10 + resolution: "@storybook/react-dom-shim@npm:10.1.10" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.0.8 - checksum: 10/99106afb92644ce801c0c24923e40d78d7ed935b9f8f10ceb1a1a827d777d294c37ecbdf793148747dcb4e735a34d876c691b6bac82c7058deef9ad451147dd1 + storybook: ^10.1.10 + checksum: 10/381c5140c54478733d8507a00e83aef51e728f7c0808534b161f9304623c8ab400d1d0ee3397d108498d7a02fc697ca5b9496e85d8c037a1ac589be97e2f467f languageName: node linkType: hard -"@storybook/react-webpack5@npm:10.0.8": - version: 10.0.8 - resolution: "@storybook/react-webpack5@npm:10.0.8" +"@storybook/react-webpack5@npm:10.1.10": + version: 10.1.10 + resolution: "@storybook/react-webpack5@npm:10.1.10" dependencies: - "@storybook/builder-webpack5": "npm:10.0.8" - "@storybook/preset-react-webpack": "npm:10.0.8" - "@storybook/react": "npm:10.0.8" + "@storybook/builder-webpack5": "npm:10.1.10" + "@storybook/preset-react-webpack": "npm:10.1.10" + "@storybook/react": "npm:10.1.10" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.0.8 + storybook: ^10.1.10 typescript: ">= 4.9.x" peerDependenciesMeta: typescript: optional: true - checksum: 10/384b1261a9a94c4399109da0121c0070b8138a1a1ce7d1a39bab43fe5c4898144ffacb22308d6d7101677f5c4b70ffe0001b41054cc675ffe8d0fa7c84e9ed24 + checksum: 10/b9fac7aa38de27d9f1b6654cef0caf64a0e67f41031dcc35be4c48c630b80e8087a24c74b36918f617ec39a432269188650a5b8d4bd2f124c75fabaebfba1d42 languageName: node linkType: hard -"@storybook/react@npm:10.0.8": - version: 10.0.8 - resolution: "@storybook/react@npm:10.0.8" +"@storybook/react@npm:10.1.10": + version: 10.1.10 + resolution: "@storybook/react@npm:10.1.10" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/react-dom-shim": "npm:10.0.8" + "@storybook/react-dom-shim": "npm:10.1.10" + react-docgen: "npm:^8.0.2" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^10.0.8 + storybook: ^10.1.10 typescript: ">= 4.9.x" peerDependenciesMeta: typescript: optional: true - checksum: 10/cbb46246958a0d2db0cc641661a14e5ae20b08ee4d2626e6d39ae4332053b3c6d84bdb77e36bc839ae7dd770504049f187466383ee06aac33bc2ef5a1c69b78f + checksum: 10/0b542c09657daf5081600e2b8c39fc411aa9307af88bcba8945f28b03f898f372c0c0ab3c3a6b1d687eb62415cc4f27003bb4767755f0d9d5799a0bee82b6c17 languageName: node linkType: hard @@ -5226,6 +5227,15 @@ __metadata: languageName: node linkType: hard +"@types/babel__traverse@npm:^7.20.7": + version: 7.28.0 + resolution: "@types/babel__traverse@npm:7.28.0" + dependencies: + "@babel/types": "npm:^7.28.2" + checksum: 10/371c5e1b40399ef17570e630b2943617b84fafde2860a56f0ebc113d8edb1d0534ade0175af89eda1ae35160903c33057ed42457e165d4aa287fedab2c82abcf + languageName: node + linkType: hard + "@types/body-parser@npm:*": version: 1.19.6 resolution: "@types/body-parser@npm:1.19.6" @@ -7934,15 +7944,6 @@ __metadata: languageName: node linkType: hard -"citty@npm:^0.1.6": - version: 0.1.6 - resolution: "citty@npm:0.1.6" - dependencies: - consola: "npm:^3.2.3" - checksum: 10/3208947e73abb699a12578ee2bfee254bf8dd1ce0d5698e8a298411cabf16bd3620d63433aef5bd88cdb2b9da71aef18adefa3b4ffd18273bb62dd1d28c344f5 - languageName: node - linkType: hard - "cjs-module-lexer@npm:^1.2.2, cjs-module-lexer@npm:^1.2.3": version: 1.4.3 resolution: "cjs-module-lexer@npm:1.4.3" @@ -8295,13 +8296,6 @@ __metadata: languageName: node linkType: hard -"confbox@npm:^0.2.2": - version: 0.2.2 - resolution: "confbox@npm:0.2.2" - checksum: 10/988c7216f9b5aee5d8a8f32153a9164e1b58d92d8335c5daa323fd3fdee91f742ffc25f6c28b059474b6319204085eca985ab14c5a246988dc7ef1fe29414108 - languageName: node - linkType: hard - "configstore@npm:^7.0.0": version: 7.1.0 resolution: "configstore@npm:7.1.0" @@ -8328,13 +8322,6 @@ __metadata: languageName: node linkType: hard -"consola@npm:^3.2.3, consola@npm:^3.4.0, consola@npm:^3.4.2": - version: 3.4.2 - resolution: "consola@npm:3.4.2" - checksum: 10/32192c9f50d7cac27c5d7c4ecd3ff3679aea863e6bf5bd6a9cc2b05d1cd78addf5dae71df08c54330c142be8e7fbd46f051030129b57c6aacdd771efe409c4b2 - languageName: node - linkType: hard - "content-disposition@npm:0.5.4": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" @@ -8506,15 +8493,15 @@ __metadata: languageName: node linkType: hard -"create-storybook@npm:10.0.8": - version: 10.0.8 - resolution: "create-storybook@npm:10.0.8" +"create-storybook@npm:10.1.10": + version: 10.1.10 + resolution: "create-storybook@npm:10.1.10" dependencies: semver: "npm:^7.6.2" - storybook: "npm:10.0.8" + storybook: "npm:10.1.10" bin: create-storybook: ./dist/bin/index.js - checksum: 10/c4a512049c0d1c8e61f05899614848874c796d2f08ff1950f0e3f5c77f9549c4d5911c2513ca50d0c1b4ff27e36547dfc8896391de25d2331a81a1adeda0c3c9 + checksum: 10/58c0288c11841f5fc9b8773cc867641c9d2b1e06f05abc752bf58597e229b55dd5f6435fbc9d0d2beae34b211b95d2e917e4bb8887c66c4d6bcd14cdde7e55c2 languageName: node linkType: hard @@ -9001,13 +8988,6 @@ __metadata: languageName: node linkType: hard -"defu@npm:^6.1.4": - version: 6.1.4 - resolution: "defu@npm:6.1.4" - checksum: 10/aeffdb47300f45b4fdef1c5bd3880ac18ea7a1fd5b8a8faf8df29350ff03bf16dd34f9800205cab513d476e4c0a3783aa0cff0a433aff0ac84a67ddc4c8a2d64 - languageName: node - linkType: hard - "degenerator@npm:^5.0.0": version: 5.0.1 resolution: "degenerator@npm:5.0.1" @@ -9719,36 +9699,36 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0": - version: 0.25.6 - resolution: "esbuild@npm:0.25.6" - dependencies: - "@esbuild/aix-ppc64": "npm:0.25.6" - "@esbuild/android-arm": "npm:0.25.6" - "@esbuild/android-arm64": "npm:0.25.6" - "@esbuild/android-x64": "npm:0.25.6" - "@esbuild/darwin-arm64": "npm:0.25.6" - "@esbuild/darwin-x64": "npm:0.25.6" - "@esbuild/freebsd-arm64": "npm:0.25.6" - "@esbuild/freebsd-x64": "npm:0.25.6" - "@esbuild/linux-arm": "npm:0.25.6" - "@esbuild/linux-arm64": "npm:0.25.6" - "@esbuild/linux-ia32": "npm:0.25.6" - "@esbuild/linux-loong64": "npm:0.25.6" - "@esbuild/linux-mips64el": "npm:0.25.6" - "@esbuild/linux-ppc64": "npm:0.25.6" - "@esbuild/linux-riscv64": "npm:0.25.6" - "@esbuild/linux-s390x": "npm:0.25.6" - "@esbuild/linux-x64": "npm:0.25.6" - "@esbuild/netbsd-arm64": "npm:0.25.6" - "@esbuild/netbsd-x64": "npm:0.25.6" - "@esbuild/openbsd-arm64": "npm:0.25.6" - "@esbuild/openbsd-x64": "npm:0.25.6" - "@esbuild/openharmony-arm64": "npm:0.25.6" - "@esbuild/sunos-x64": "npm:0.25.6" - "@esbuild/win32-arm64": "npm:0.25.6" - "@esbuild/win32-ia32": "npm:0.25.6" - "@esbuild/win32-x64": "npm:0.25.6" +"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0": + version: 0.27.2 + resolution: "esbuild@npm:0.27.2" + dependencies: + "@esbuild/aix-ppc64": "npm:0.27.2" + "@esbuild/android-arm": "npm:0.27.2" + "@esbuild/android-arm64": "npm:0.27.2" + "@esbuild/android-x64": "npm:0.27.2" + "@esbuild/darwin-arm64": "npm:0.27.2" + "@esbuild/darwin-x64": "npm:0.27.2" + "@esbuild/freebsd-arm64": "npm:0.27.2" + "@esbuild/freebsd-x64": "npm:0.27.2" + "@esbuild/linux-arm": "npm:0.27.2" + "@esbuild/linux-arm64": "npm:0.27.2" + "@esbuild/linux-ia32": "npm:0.27.2" + "@esbuild/linux-loong64": "npm:0.27.2" + "@esbuild/linux-mips64el": "npm:0.27.2" + "@esbuild/linux-ppc64": "npm:0.27.2" + "@esbuild/linux-riscv64": "npm:0.27.2" + "@esbuild/linux-s390x": "npm:0.27.2" + "@esbuild/linux-x64": "npm:0.27.2" + "@esbuild/netbsd-arm64": "npm:0.27.2" + "@esbuild/netbsd-x64": "npm:0.27.2" + "@esbuild/openbsd-arm64": "npm:0.27.2" + "@esbuild/openbsd-x64": "npm:0.27.2" + "@esbuild/openharmony-arm64": "npm:0.27.2" + "@esbuild/sunos-x64": "npm:0.27.2" + "@esbuild/win32-arm64": "npm:0.27.2" + "@esbuild/win32-ia32": "npm:0.27.2" + "@esbuild/win32-x64": "npm:0.27.2" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -9804,7 +9784,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10/b1c94893d53e39f3be02493c3a082bc9d090ca1702bf6017967ace1391d31e596da4dfd87e02efef3b7b4f0426918dbc5aa6909420a6ba42d51a872266ab6f2e + checksum: 10/7f1229328b0efc63c4184a61a7eb303df1e99818cc1d9e309fb92600703008e69821e8e984e9e9f54a627da14e0960d561db3a93029482ef96dc82dd267a60c2 languageName: node linkType: hard @@ -10420,13 +10400,6 @@ __metadata: languageName: node linkType: hard -"exsolve@npm:^1.0.7": - version: 1.0.8 - resolution: "exsolve@npm:1.0.8" - checksum: 10/e7e8eac048af9f6856628a46df15529ab37428bdb5f7bc5b7824614383223de1aff60ebe85f44d9c8d4ee218d98c71df1a3e2d336f7d022a4dccd97a0651ec5b - languageName: node - linkType: hard - "extend@npm:~3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -11061,22 +11034,6 @@ __metadata: languageName: node linkType: hard -"giget@npm:^2.0.0": - version: 2.0.0 - resolution: "giget@npm:2.0.0" - dependencies: - citty: "npm:^0.1.6" - consola: "npm:^3.4.0" - defu: "npm:^6.1.4" - node-fetch-native: "npm:^1.6.6" - nypm: "npm:^0.6.0" - pathe: "npm:^2.0.3" - bin: - giget: dist/cli.mjs - checksum: 10/3ee0f4aa06bdaeda9d4d31791d6a1e4349f15e20ff1dbe60535c709d3acc03f29f36a648cd047851a332fc1a0e9997ab6c5036410cc1629c09ad45ee155ee6dd - languageName: node - linkType: hard - "glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -14327,13 +14284,6 @@ __metadata: languageName: node linkType: hard -"node-fetch-native@npm:^1.6.6": - version: 1.6.6 - resolution: "node-fetch-native@npm:1.6.6" - checksum: 10/e90d5287fdfb10b9b13276158c9c0ff4318eef222562cf4a504e71665236dea9dda10ae77eb9f0f89cb7677a32ccf2326b9b54f7090121c2af2ac617c1256f8f - languageName: node - linkType: hard - "node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -14467,21 +14417,6 @@ __metadata: languageName: node linkType: hard -"nypm@npm:^0.6.0": - version: 0.6.2 - resolution: "nypm@npm:0.6.2" - dependencies: - citty: "npm:^0.1.6" - consola: "npm:^3.4.2" - pathe: "npm:^2.0.3" - pkg-types: "npm:^2.3.0" - tinyexec: "npm:^1.0.1" - bin: - nypm: dist/cli.mjs - checksum: 10/3bbf25b02b9eab5565a9a11c1f0946d0065cc6a9028e8f438ebf5256f3139cfac0763a3852984a7ae92c761ab1c2ce881272f9b1a863107e195e7f7cae05b598 - languageName: node - linkType: hard - "object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -14649,6 +14584,18 @@ __metadata: languageName: node linkType: hard +"open@npm:^10.2.0": + version: 10.2.0 + resolution: "open@npm:10.2.0" + dependencies: + default-browser: "npm:^5.2.1" + define-lazy-prop: "npm:^3.0.0" + is-inside-container: "npm:^1.0.0" + wsl-utils: "npm:^0.1.0" + checksum: 10/e6ad9474734eac3549dcc7d85e952394856ccaee48107c453bd6a725b82e3b8ed5f427658935df27efa76b411aeef62888edea8a9e347e8e7c82632ec966b30e + languageName: node + linkType: hard + "open@npm:^8.4.0": version: 8.4.2 resolution: "open@npm:8.4.2" @@ -14938,13 +14885,6 @@ __metadata: languageName: node linkType: hard -"pathe@npm:^2.0.3": - version: 2.0.3 - resolution: "pathe@npm:2.0.3" - checksum: 10/01e9a69928f39087d96e1751ce7d6d50da8c39abf9a12e0ac2389c42c83bc76f78c45a475bd9026a02e6a6f79be63acc75667df855862fe567d99a00a540d23d - languageName: node - linkType: hard - "pathval@npm:^2.0.0": version: 2.0.1 resolution: "pathval@npm:2.0.1" @@ -15069,17 +15009,6 @@ __metadata: languageName: node linkType: hard -"pkg-types@npm:^2.3.0": - version: 2.3.0 - resolution: "pkg-types@npm:2.3.0" - dependencies: - confbox: "npm:^0.2.2" - exsolve: "npm:^1.0.7" - pathe: "npm:^2.0.3" - checksum: 10/4b36e4eb12693a1beb145573c564ec6fb74b1008d3b457eaa1f0072331edf05cb7c479c47fe0c4bfdec76c2caff5b68215ff270e5fe49634c07984a7a0197118 - languageName: node - linkType: hard - "please-upgrade-node@npm:^3.2.0": version: 3.2.0 resolution: "please-upgrade-node@npm:3.2.0" @@ -15562,6 +15491,24 @@ __metadata: languageName: node linkType: hard +"react-docgen@npm:^8.0.2": + version: 8.0.2 + resolution: "react-docgen@npm:8.0.2" + dependencies: + "@babel/core": "npm:^7.28.0" + "@babel/traverse": "npm:^7.28.0" + "@babel/types": "npm:^7.28.2" + "@types/babel__core": "npm:^7.20.5" + "@types/babel__traverse": "npm:^7.20.7" + "@types/doctrine": "npm:^0.0.9" + "@types/resolve": "npm:^1.20.2" + doctrine: "npm:^3.0.0" + resolve: "npm:^1.22.1" + strip-indent: "npm:^4.0.0" + checksum: 10/b56f594237a0bdf1356dee6a416ab6f2f38f60a7330cbdaf1da93d366f29f6bfbbfedecd51d47f6ba2a898985c3205c369e7f6cad528478560d9363717243ff8 + languageName: node + linkType: hard + "react-dom@npm:19.2.0": version: 19.2.0 resolution: "react-dom@npm:19.2.0" @@ -16721,11 +16668,11 @@ __metadata: "@loadable/server": "npm:5.16.7" "@loadable/webpack-plugin": "npm:5.15.2" "@optimizely/react-sdk": "npm:3.2.4" - "@storybook/addon-a11y": "npm:10.0.8" - "@storybook/addon-docs": "npm:10.0.8" - "@storybook/builder-webpack5": "npm:10.0.8" - "@storybook/cli": "npm:10.0.8" - "@storybook/react-webpack5": "npm:10.0.8" + "@storybook/addon-a11y": "npm:10.1.10" + "@storybook/addon-docs": "npm:10.1.10" + "@storybook/builder-webpack5": "npm:10.1.10" + "@storybook/cli": "npm:10.1.10" + "@storybook/react-webpack5": "npm:10.1.10" "@testing-library/dom": "npm:10.4.1" "@testing-library/jest-dom": "npm:6.9.1" "@testing-library/react": "npm:16.3.0" @@ -16816,7 +16763,7 @@ __metadata: react-router-dom: "npm:5.3.4" retry: "npm:0.13.1" start-server-nestjs-webpack-plugin: "npm:2.2.5" - storybook: "npm:10.0.8" + storybook: "npm:10.1.10" stream-browserify: "npm:3.0.0" strip-ansi: "npm:7.1.2" supertest: "npm:7.1.4" @@ -17120,20 +17067,21 @@ __metadata: languageName: node linkType: hard -"storybook@npm:10.0.8": - version: 10.0.8 - resolution: "storybook@npm:10.0.8" +"storybook@npm:10.1.10": + version: 10.1.10 + resolution: "storybook@npm:10.1.10" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.6.0" + "@storybook/icons": "npm:^2.0.0" "@testing-library/jest-dom": "npm:^6.6.3" "@testing-library/user-event": "npm:^14.6.1" "@vitest/expect": "npm:3.2.4" - "@vitest/mocker": "npm:3.2.4" "@vitest/spy": "npm:3.2.4" - esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0" + esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0" + open: "npm:^10.2.0" recast: "npm:^0.23.5" semver: "npm:^7.6.2" + use-sync-external-store: "npm:^1.5.0" ws: "npm:^8.18.0" peerDependencies: prettier: ^2 || ^3 @@ -17142,7 +17090,7 @@ __metadata: optional: true bin: storybook: ./dist/bin/dispatcher.js - checksum: 10/a30b54248d69d7406f78e1871e075092ed9ae366d1c7cec56dc10b31b72a5196ef9e751453c9a7546797228c4fe0f01893c6fa541d24cf51037124d4f0e1566e + checksum: 10/c1f01c7ab57e80d2f2ef3a5c49baad5904e77c8e079199ad134e98a7ae455d52422390cd704e64142b36668874c055670dbffe0e334e1f4d541ebd4384052dd7 languageName: node linkType: hard @@ -17741,13 +17689,6 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^1.0.1": - version: 1.0.2 - resolution: "tinyexec@npm:1.0.2" - checksum: 10/cb709ed4240e873d3816e67f851d445f5676e0ae3a52931a60ff571d93d388da09108c8057b62351766133ee05ff3159dd56c3a0fbd39a5933c6639ce8771405 - languageName: node - linkType: hard - "tinyglobby@npm:^0.2.12": version: 0.2.14 resolution: "tinyglobby@npm:0.2.14" @@ -18496,6 +18437,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.5.0": + version: 1.6.0 + resolution: "use-sync-external-store@npm:1.6.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/b40ad2847ba220695bff2d4ba4f4d60391c0fb4fb012faa7a4c18eb38b69181936f5edc55a522c4d20a788d1a879b73c3810952c9d0fd128d01cb3f22042c09e + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -19304,6 +19254,15 @@ __metadata: languageName: node linkType: hard +"wsl-utils@npm:^0.1.0": + version: 0.1.0 + resolution: "wsl-utils@npm:0.1.0" + dependencies: + is-wsl: "npm:^3.1.0" + checksum: 10/de4c92187e04c3c27b4478f410a02e81c351dc85efa3447bf1666f34fc80baacd890a6698ec91995631714086992036013286aea3d77e6974020d40a08e00aec + languageName: node + linkType: hard + "xdg-basedir@npm:^5.1.0": version: 5.1.0 resolution: "xdg-basedir@npm:5.1.0" From 78d336dea16b8f3093138c9b6a4e5d5922495495 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Fri, 19 Dec 2025 16:15:53 +0200 Subject: [PATCH 78/96] removing ref --- src/app/hooks/usePWAOfflineTracking/index.tsx | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/app/hooks/usePWAOfflineTracking/index.tsx b/src/app/hooks/usePWAOfflineTracking/index.tsx index df28fddc773..2b9f46ad6d6 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.tsx @@ -15,7 +15,6 @@ const usePWAOfflineTracking = () => { const isPWA = useIsPWA(); const { isOnline, networkType } = useNetworkStatusTracker(); const prevIsOnlineRef = useRef(isOnline); - const hasFiredRef = useRef(false); const trackOfflinePageViewEvent = useCustomEventTracker({ eventName: OFFLINE_PAGE_VIEW_EVENT_NAME, @@ -27,35 +26,19 @@ const usePWAOfflineTracking = () => { return; } - const wasOffline = prevIsOnlineRef.current === false; - const isNowOnline = isOnline === true; - const transitionedToOnline = wasOffline && isNowOnline; - if (!isOnline) { - hasFiredRef.current = false; prevIsOnlineRef.current = isOnline; return; } - if (!transitionedToOnline) { - prevIsOnlineRef.current = isOnline; - } - try { const offlineVisitFlag = localStorage.getItem(OFFLINE_VISIT_FLAG); if (offlineVisitFlag !== 'true') { - prevIsOnlineRef.current = isOnline; - return; - } - - if (hasFiredRef.current && !transitionedToOnline) { - prevIsOnlineRef.current = isOnline; return; } trackOfflinePageViewEvent(networkType); - hasFiredRef.current = true; localStorage.removeItem(OFFLINE_VISIT_FLAG); } catch (error) { // eslint-disable-next-line no-console From 8f5bd54fb0da31002fa7a3fe693f0bf7adeb6ca3 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Mon, 22 Dec 2025 12:32:08 +0200 Subject: [PATCH 79/96] simplefying offline tracking logic --- src/app/hooks/usePWAOfflineTracking/index.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/app/hooks/usePWAOfflineTracking/index.tsx b/src/app/hooks/usePWAOfflineTracking/index.tsx index 2b9f46ad6d6..5564a1b5638 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import useIsPWA from '../useIsPWA'; import useNetworkStatusTracker from '../useNetworkStatusTracker'; import useCustomEventTracker from '../useCustomEventTracker'; @@ -14,20 +14,13 @@ const OFFLINE_PAGE_VIEW_EVENT_NAME = 'pwa-offline-page-view'; const usePWAOfflineTracking = () => { const isPWA = useIsPWA(); const { isOnline, networkType } = useNetworkStatusTracker(); - const prevIsOnlineRef = useRef(isOnline); const trackOfflinePageViewEvent = useCustomEventTracker({ eventName: OFFLINE_PAGE_VIEW_EVENT_NAME, }); useEffect(() => { - if (typeof window === 'undefined' || !isPWA) { - prevIsOnlineRef.current = isOnline; - return; - } - - if (!isOnline) { - prevIsOnlineRef.current = isOnline; + if (typeof window === 'undefined' || !isPWA || !isOnline) { return; } @@ -44,8 +37,6 @@ const usePWAOfflineTracking = () => { // eslint-disable-next-line no-console console.error('usePWAOfflineTracking', error); } - - prevIsOnlineRef.current = isOnline; }, [isPWA, isOnline, networkType, trackOfflinePageViewEvent]); }; From 6ec847a4da40c624bfea412ee0bf40505ccbe19b Mon Sep 17 00:00:00 2001 From: jinidev Date: Mon, 22 Dec 2025 14:17:06 +0200 Subject: [PATCH 80/96] upgraded the cachename in sw to see updated offlinepage --- public/sw.js | 2 +- src/sw.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/sw.js b/public/sw.js index 52f4c994a49..8b886684215 100644 --- a/public/sw.js +++ b/public/sw.js @@ -6,7 +6,7 @@ /* eslint-disable no-console */ const version = 'v0.3.1'; -const cacheName = 'simorghCache_v1'; +const cacheName = 'simorghCache_v2'; // Track PWA clients const pwaClients = new Map(); diff --git a/src/sw.test.js b/src/sw.test.js index c8fdb880a6a..ac92b2bc3ca 100644 --- a/src/sw.test.js +++ b/src/sw.test.js @@ -438,7 +438,7 @@ describe('Service Worker', () => { describe('version', () => { const CURRENT_VERSION = { number: 'v0.3.1', - fileContentHash: '53ed26916a6ca43ba3d1ff7ac4ab0115', + fileContentHash: '45eca6cd67d53c369b20013fb7c7210d', }; it(`version number should be ${CURRENT_VERSION.number}`, async () => { From b03ca3f00235f50301e6618793d9eb2f66df1ec4 Mon Sep 17 00:00:00 2001 From: jinidev Date: Mon, 22 Dec 2025 16:30:51 +0200 Subject: [PATCH 81/96] putting logs and removed isONline check from useOfflinePageFlag hook --- src/app/hooks/useCustomEventTracker/index.tsx | 12 ++++++++++++ src/app/hooks/useOfflinePageFlag/index.tsx | 6 ++++-- src/app/hooks/usePWAOfflineTracking/index.tsx | 6 ++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/app/hooks/useCustomEventTracker/index.tsx b/src/app/hooks/useCustomEventTracker/index.tsx index aef267599ad..13b450d0846 100644 --- a/src/app/hooks/useCustomEventTracker/index.tsx +++ b/src/app/hooks/useCustomEventTracker/index.tsx @@ -56,6 +56,18 @@ const useCustomEventTracker = ({ statsDestination, ].every(Boolean); + console.log('Custom Event Tracker', { + shouldSendEvent, + campaignID, + eventName, + pageIdentifier, + platform, + producerId, + producerName, + service, + statsDestination, + }); + if (shouldSendEvent) { try { await sendEventBeacon({ diff --git a/src/app/hooks/useOfflinePageFlag/index.tsx b/src/app/hooks/useOfflinePageFlag/index.tsx index a338d77c235..cff9f1bfc1a 100644 --- a/src/app/hooks/useOfflinePageFlag/index.tsx +++ b/src/app/hooks/useOfflinePageFlag/index.tsx @@ -12,8 +12,10 @@ const OFFLINE_VISIT_FLAG = 'offline_page_visit'; const useOfflinePageFlag = () => { const isPWA = useIsPWA(); const { isOnline } = useNetworkStatusTracker(); + console.log('useOfflinePageFlag invoked', { isPWA, isOnline }); + useEffect(() => { - if (typeof window === 'undefined' || !isPWA || isOnline) return; + if (typeof window === 'undefined' || !isPWA) return; try { localStorage.setItem(OFFLINE_VISIT_FLAG, 'true'); @@ -21,7 +23,7 @@ const useOfflinePageFlag = () => { // eslint-disable-next-line no-console console.warn('useOfflinePageFlag', error); } - }, [isOnline, isPWA]); + }, [isPWA]); }; export default useOfflinePageFlag; diff --git a/src/app/hooks/usePWAOfflineTracking/index.tsx b/src/app/hooks/usePWAOfflineTracking/index.tsx index 5564a1b5638..84be6970ed5 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.tsx @@ -19,6 +19,12 @@ const usePWAOfflineTracking = () => { eventName: OFFLINE_PAGE_VIEW_EVENT_NAME, }); + console.log('usePWAOfflineTracking invoked', { + isPWA, + isOnline, + networkType, + }); + useEffect(() => { if (typeof window === 'undefined' || !isPWA || !isOnline) { return; From 9b1cde512b4f6d847ebd75cb1247efa6c3f13dc6 Mon Sep 17 00:00:00 2001 From: jinidev Date: Mon, 22 Dec 2025 17:47:37 +0200 Subject: [PATCH 82/96] sw changes for nextjs bundling/cache resorucing --- public/sw.js | 26 +++++++++---------- src/sw.test.js | 2 +- .../pages/[service]/offline/OfflinePage.tsx | 2 ++ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/public/sw.js b/public/sw.js index 8b886684215..56d4c790b0e 100644 --- a/public/sw.js +++ b/public/sw.js @@ -6,7 +6,7 @@ /* eslint-disable no-console */ const version = 'v0.3.1'; -const cacheName = 'simorghCache_v2'; +const cacheName = 'simorghCache_v3'; // Track PWA clients const pwaClients = new Map(); @@ -36,7 +36,9 @@ const cacheOfflinePageAndResources = async service => { getOfflinePageUrl(service), self.location.origin, ).href; - if (await cache.match(offlinePageUrl)) return; + + // Commenting out to force re-cache during testing + // if (await cache.match(offlinePageUrl)) return; const resp = await cacheResource(cache, offlinePageUrl); if (!resp || !resp.ok) return; @@ -155,20 +157,16 @@ const fetchEventHandler = async event => { })(), ); } else if (event.request.url.includes('/_next/static/')) { - // Network-first for Next.js chunks (dev mode compatibility) + // Cache Next.js static files - cache-first strategy event.respondWith( (async () => { - try { - const networkResp = await fetch(event.request); - const cache = await caches.open(cacheName); - cache.put(event.request, networkResp.clone()); - return networkResp; - } catch (err) { - const cache = await caches.open(cacheName); - const cachedResp = await cache.match(event.request); - if (cachedResp) return cachedResp; - throw err; - } + const cache = await caches.open(cacheName); + const cached = await cache.match(event.request); + if (cached) return cached; + + const networkResp = await fetch(event.request); + cache.put(event.request, networkResp.clone()); + return networkResp; })(), ); } else if (event.request.mode === 'navigate') { diff --git a/src/sw.test.js b/src/sw.test.js index ac92b2bc3ca..ceb1b1afcae 100644 --- a/src/sw.test.js +++ b/src/sw.test.js @@ -438,7 +438,7 @@ describe('Service Worker', () => { describe('version', () => { const CURRENT_VERSION = { number: 'v0.3.1', - fileContentHash: '45eca6cd67d53c369b20013fb7c7210d', + fileContentHash: 'ad7f08d290f094551fb25d32b18623c3', }; it(`version number should be ${CURRENT_VERSION.number}`, async () => { diff --git a/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx b/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx index 70e74014020..a303ba88abd 100644 --- a/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx +++ b/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx @@ -18,6 +18,8 @@ const OfflinePage = () => { 'Refresh the page when your connection is restored', ]; + console.log('OfflinePage rendered'); + return ( <> From d81aba233ebcf53ab40524484d0861c000844265 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Mon, 22 Dec 2025 18:29:26 +0200 Subject: [PATCH 83/96] updating test --- .../hooks/useOfflinePageFlag/index.test.tsx | 82 +++++++++---------- 1 file changed, 39 insertions(+), 43 deletions(-) diff --git a/src/app/hooks/useOfflinePageFlag/index.test.tsx b/src/app/hooks/useOfflinePageFlag/index.test.tsx index 25a79aa8873..d03f8543fa5 100644 --- a/src/app/hooks/useOfflinePageFlag/index.test.tsx +++ b/src/app/hooks/useOfflinePageFlag/index.test.tsx @@ -2,42 +2,28 @@ import { renderHook } from '@testing-library/react'; import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server'; import useOfflinePageFlag, { OFFLINE_VISIT_FLAG } from './index'; -describe('useOfflinePageFlag', () => { - const originalMatchMedia = window.matchMedia; - const originalNavigator = window.navigator; - const originalLocalStorage = window.localStorage; +jest.mock('../useIsPWA'); +jest.mock('../useNetworkStatusTracker'); + +const mockUseIsPWA = jest.requireMock('../useIsPWA').default as jest.Mock; +const mockUseNetworkStatusTracker = jest.requireMock( + '../useNetworkStatusTracker', +).default as jest.Mock; +describe('useOfflinePageFlag', () => { beforeEach(() => { Storage.prototype.setItem = jest.fn(); Storage.prototype.getItem = jest.fn(); Storage.prototype.removeItem = jest.fn(); + jest.clearAllMocks(); }); - afterEach(() => { - window.matchMedia = originalMatchMedia; - window.navigator = originalNavigator; - window.localStorage = originalLocalStorage; - jest.restoreAllMocks(); - }); - - const mockMatchMedia = (queries: Record) => { - window.matchMedia = jest.fn().mockImplementation((query: string) => ({ - matches: !!queries[query], - })); - }; - - const mockNavigator = (onLine: boolean) => { - jest.spyOn(window, 'navigator', 'get').mockImplementation( - () => - ({ - onLine, - }) as unknown as Navigator, - ); - }; - it('should set offline flag when offline in PWA mode', () => { - mockMatchMedia({ '(display-mode: standalone)': true }); - mockNavigator(false); + mockUseIsPWA.mockReturnValue(true); + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: false, + networkType: '4g', + }); renderHook(() => useOfflinePageFlag()); @@ -48,8 +34,11 @@ describe('useOfflinePageFlag', () => { }); it('should not set flag when online in PWA mode', () => { - mockMatchMedia({ '(display-mode: standalone)': true }); - mockNavigator(true); + mockUseIsPWA.mockReturnValue(true); + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType: '4g', + }); renderHook(() => useOfflinePageFlag()); @@ -57,8 +46,11 @@ describe('useOfflinePageFlag', () => { }); it('should not set flag when offline in browser mode', () => { - mockMatchMedia({ '(display-mode: browser)': true }); - mockNavigator(false); + mockUseIsPWA.mockReturnValue(false); + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: false, + networkType: '4g', + }); renderHook(() => useOfflinePageFlag()); @@ -66,8 +58,11 @@ describe('useOfflinePageFlag', () => { }); it('should not set flag when online in browser mode', () => { - mockMatchMedia({ '(display-mode: browser)': true }); - mockNavigator(true); + mockUseIsPWA.mockReturnValue(false); + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: true, + networkType: '4g', + }); renderHook(() => useOfflinePageFlag()); @@ -75,8 +70,11 @@ describe('useOfflinePageFlag', () => { }); it('should handle localStorage errors gracefully', () => { - mockMatchMedia({ '(display-mode: standalone)': true }); - mockNavigator(false); + mockUseIsPWA.mockReturnValue(true); + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: false, + networkType: '4g', + }); const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); Storage.prototype.setItem = jest.fn().mockImplementation(() => { @@ -97,13 +95,11 @@ describe('useOfflinePageFlag', () => { }); it('should work with iOS standalone mode', () => { - mockMatchMedia({}); - mockNavigator(false); - jest - .spyOn(window, 'navigator', 'get') - .mockImplementation( - () => ({ standalone: true, onLine: false }) as unknown as Navigator, - ); + mockUseIsPWA.mockReturnValue(true); + mockUseNetworkStatusTracker.mockReturnValue({ + isOnline: false, + networkType: '4g', + }); renderHook(() => useOfflinePageFlag()); From 0a72b7d6891de300cdbf23944caa0db19900e807 Mon Sep 17 00:00:00 2001 From: jinidev Date: Mon, 22 Dec 2025 19:11:57 +0200 Subject: [PATCH 84/96] revert sw.js to original --- public/sw.js | 26 ++++++++++++++------------ src/sw.test.js | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/public/sw.js b/public/sw.js index 56d4c790b0e..52f4c994a49 100644 --- a/public/sw.js +++ b/public/sw.js @@ -6,7 +6,7 @@ /* eslint-disable no-console */ const version = 'v0.3.1'; -const cacheName = 'simorghCache_v3'; +const cacheName = 'simorghCache_v1'; // Track PWA clients const pwaClients = new Map(); @@ -36,9 +36,7 @@ const cacheOfflinePageAndResources = async service => { getOfflinePageUrl(service), self.location.origin, ).href; - - // Commenting out to force re-cache during testing - // if (await cache.match(offlinePageUrl)) return; + if (await cache.match(offlinePageUrl)) return; const resp = await cacheResource(cache, offlinePageUrl); if (!resp || !resp.ok) return; @@ -157,16 +155,20 @@ const fetchEventHandler = async event => { })(), ); } else if (event.request.url.includes('/_next/static/')) { - // Cache Next.js static files - cache-first strategy + // Network-first for Next.js chunks (dev mode compatibility) event.respondWith( (async () => { - const cache = await caches.open(cacheName); - const cached = await cache.match(event.request); - if (cached) return cached; - - const networkResp = await fetch(event.request); - cache.put(event.request, networkResp.clone()); - return networkResp; + try { + const networkResp = await fetch(event.request); + const cache = await caches.open(cacheName); + cache.put(event.request, networkResp.clone()); + return networkResp; + } catch (err) { + const cache = await caches.open(cacheName); + const cachedResp = await cache.match(event.request); + if (cachedResp) return cachedResp; + throw err; + } })(), ); } else if (event.request.mode === 'navigate') { diff --git a/src/sw.test.js b/src/sw.test.js index ceb1b1afcae..c8fdb880a6a 100644 --- a/src/sw.test.js +++ b/src/sw.test.js @@ -438,7 +438,7 @@ describe('Service Worker', () => { describe('version', () => { const CURRENT_VERSION = { number: 'v0.3.1', - fileContentHash: 'ad7f08d290f094551fb25d32b18623c3', + fileContentHash: '53ed26916a6ca43ba3d1ff7ac4ab0115', }; it(`version number should be ${CURRENT_VERSION.number}`, async () => { From 2aa1f463ce800c508200a4f2f78450f5e71222e4 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Mon, 22 Dec 2025 18:43:04 +0200 Subject: [PATCH 85/96] update testds --- .../hooks/useOfflinePageFlag/index.test.tsx | 50 ++----------------- 1 file changed, 3 insertions(+), 47 deletions(-) diff --git a/src/app/hooks/useOfflinePageFlag/index.test.tsx b/src/app/hooks/useOfflinePageFlag/index.test.tsx index d03f8543fa5..7c86326fd4b 100644 --- a/src/app/hooks/useOfflinePageFlag/index.test.tsx +++ b/src/app/hooks/useOfflinePageFlag/index.test.tsx @@ -3,12 +3,8 @@ import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server import useOfflinePageFlag, { OFFLINE_VISIT_FLAG } from './index'; jest.mock('../useIsPWA'); -jest.mock('../useNetworkStatusTracker'); const mockUseIsPWA = jest.requireMock('../useIsPWA').default as jest.Mock; -const mockUseNetworkStatusTracker = jest.requireMock( - '../useNetworkStatusTracker', -).default as jest.Mock; describe('useOfflinePageFlag', () => { beforeEach(() => { @@ -18,12 +14,8 @@ describe('useOfflinePageFlag', () => { jest.clearAllMocks(); }); - it('should set offline flag when offline in PWA mode', () => { + it('should set offline flag when in PWA mode', () => { mockUseIsPWA.mockReturnValue(true); - mockUseNetworkStatusTracker.mockReturnValue({ - isOnline: false, - networkType: '4g', - }); renderHook(() => useOfflinePageFlag()); @@ -33,36 +25,8 @@ describe('useOfflinePageFlag', () => { ); }); - it('should not set flag when online in PWA mode', () => { - mockUseIsPWA.mockReturnValue(true); - mockUseNetworkStatusTracker.mockReturnValue({ - isOnline: true, - networkType: '4g', - }); - - renderHook(() => useOfflinePageFlag()); - - expect(localStorage.setItem).not.toHaveBeenCalled(); - }); - - it('should not set flag when offline in browser mode', () => { - mockUseIsPWA.mockReturnValue(false); - mockUseNetworkStatusTracker.mockReturnValue({ - isOnline: false, - networkType: '4g', - }); - - renderHook(() => useOfflinePageFlag()); - - expect(localStorage.setItem).not.toHaveBeenCalled(); - }); - - it('should not set flag when online in browser mode', () => { + it('should not set flag when in browser mode', () => { mockUseIsPWA.mockReturnValue(false); - mockUseNetworkStatusTracker.mockReturnValue({ - isOnline: true, - networkType: '4g', - }); renderHook(() => useOfflinePageFlag()); @@ -71,10 +35,6 @@ describe('useOfflinePageFlag', () => { it('should handle localStorage errors gracefully', () => { mockUseIsPWA.mockReturnValue(true); - mockUseNetworkStatusTracker.mockReturnValue({ - isOnline: false, - networkType: '4g', - }); const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); Storage.prototype.setItem = jest.fn().mockImplementation(() => { @@ -94,12 +54,8 @@ describe('useOfflinePageFlag', () => { expect(localStorage.setItem).not.toHaveBeenCalled(); }); - it('should work with iOS standalone mode', () => { + it('should set flag with iOS standalone mode', () => { mockUseIsPWA.mockReturnValue(true); - mockUseNetworkStatusTracker.mockReturnValue({ - isOnline: false, - networkType: '4g', - }); renderHook(() => useOfflinePageFlag()); From 64fd026efc75ce34b247ba0c634d9bb2a1cd323b Mon Sep 17 00:00:00 2001 From: Dmytro Skumin Date: Fri, 2 Jan 2026 15:45:43 +0200 Subject: [PATCH 86/96] Update src/app/hooks/usePWAOfflineTracking/index.test.tsx Co-authored-by: Andrew Bennett --- src/app/hooks/usePWAOfflineTracking/index.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/hooks/usePWAOfflineTracking/index.test.tsx b/src/app/hooks/usePWAOfflineTracking/index.test.tsx index bf505f32725..0fad5fc60d2 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.test.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.test.tsx @@ -115,7 +115,7 @@ describe('usePWAOfflineTracking', () => { rerender(); - expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1); + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(0); }); it('should fire event again after flag is set again on next offline visit', () => { From d840930fa7049284bfc8453159c9694af9d0b12a Mon Sep 17 00:00:00 2001 From: skumid01 Date: Fri, 2 Jan 2026 16:31:04 +0200 Subject: [PATCH 87/96] updates due to comments --- src/app/hooks/useCustomEventTracker/index.tsx | 12 ----- .../hooks/useOfflinePageFlag/index.test.tsx | 30 +---------- src/app/hooks/useOfflinePageFlag/index.tsx | 19 +++---- .../usePWAOfflineTracking/index.test.tsx | 53 ++++--------------- src/app/hooks/usePWAOfflineTracking/index.tsx | 20 +++---- .../[service]/offline/OfflinePage.test.tsx | 4 +- .../pages/[service]/offline/OfflinePage.tsx | 6 +-- 7 files changed, 29 insertions(+), 115 deletions(-) diff --git a/src/app/hooks/useCustomEventTracker/index.tsx b/src/app/hooks/useCustomEventTracker/index.tsx index 13b450d0846..aef267599ad 100644 --- a/src/app/hooks/useCustomEventTracker/index.tsx +++ b/src/app/hooks/useCustomEventTracker/index.tsx @@ -56,18 +56,6 @@ const useCustomEventTracker = ({ statsDestination, ].every(Boolean); - console.log('Custom Event Tracker', { - shouldSendEvent, - campaignID, - eventName, - pageIdentifier, - platform, - producerId, - producerName, - service, - statsDestination, - }); - if (shouldSendEvent) { try { await sendEventBeacon({ diff --git a/src/app/hooks/useOfflinePageFlag/index.test.tsx b/src/app/hooks/useOfflinePageFlag/index.test.tsx index 7c86326fd4b..5160c61df49 100644 --- a/src/app/hooks/useOfflinePageFlag/index.test.tsx +++ b/src/app/hooks/useOfflinePageFlag/index.test.tsx @@ -1,10 +1,6 @@ import { renderHook } from '@testing-library/react'; import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server'; -import useOfflinePageFlag, { OFFLINE_VISIT_FLAG } from './index'; - -jest.mock('../useIsPWA'); - -const mockUseIsPWA = jest.requireMock('../useIsPWA').default as jest.Mock; +import { useOfflinePageFlag, OFFLINE_VISIT_FLAG } from './index'; describe('useOfflinePageFlag', () => { beforeEach(() => { @@ -14,9 +10,7 @@ describe('useOfflinePageFlag', () => { jest.clearAllMocks(); }); - it('should set offline flag when in PWA mode', () => { - mockUseIsPWA.mockReturnValue(true); - + it('should set offline flag when rendered', () => { renderHook(() => useOfflinePageFlag()); expect(localStorage.setItem).toHaveBeenCalledWith( @@ -25,16 +19,7 @@ describe('useOfflinePageFlag', () => { ); }); - it('should not set flag when in browser mode', () => { - mockUseIsPWA.mockReturnValue(false); - - renderHook(() => useOfflinePageFlag()); - - expect(localStorage.setItem).not.toHaveBeenCalled(); - }); - it('should handle localStorage errors gracefully', () => { - mockUseIsPWA.mockReturnValue(true); const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); Storage.prototype.setItem = jest.fn().mockImplementation(() => { @@ -53,15 +38,4 @@ describe('useOfflinePageFlag', () => { expect(localStorage.setItem).not.toHaveBeenCalled(); }); - - it('should set flag with iOS standalone mode', () => { - mockUseIsPWA.mockReturnValue(true); - - renderHook(() => useOfflinePageFlag()); - - expect(localStorage.setItem).toHaveBeenCalledWith( - OFFLINE_VISIT_FLAG, - 'true', - ); - }); }); diff --git a/src/app/hooks/useOfflinePageFlag/index.tsx b/src/app/hooks/useOfflinePageFlag/index.tsx index cff9f1bfc1a..2282b7d2ce1 100644 --- a/src/app/hooks/useOfflinePageFlag/index.tsx +++ b/src/app/hooks/useOfflinePageFlag/index.tsx @@ -1,21 +1,15 @@ import { useEffect } from 'react'; -import useIsPWA from '../useIsPWA'; -import useNetworkStatusTracker from '../useNetworkStatusTracker'; const OFFLINE_VISIT_FLAG = 'offline_page_visit'; /** - * Sets a flag in localStorage when user visits the offline page in PWA mode. - * This flag is checked by usePWAOfflineTracking to send tracking when back online. - * Only tracks when app is running as PWA. + * Sets a flag in localStorage when user visits the offline page. + * Note: Offline page is only accessible in PWA mode (via service worker), + * so no need to check isPWA - if this hook runs, we're already in PWA. */ const useOfflinePageFlag = () => { - const isPWA = useIsPWA(); - const { isOnline } = useNetworkStatusTracker(); - console.log('useOfflinePageFlag invoked', { isPWA, isOnline }); - useEffect(() => { - if (typeof window === 'undefined' || !isPWA) return; + if (typeof window === 'undefined') return; try { localStorage.setItem(OFFLINE_VISIT_FLAG, 'true'); @@ -23,8 +17,7 @@ const useOfflinePageFlag = () => { // eslint-disable-next-line no-console console.warn('useOfflinePageFlag', error); } - }, [isPWA]); + }, []); }; -export default useOfflinePageFlag; -export { OFFLINE_VISIT_FLAG }; +export { useOfflinePageFlag, OFFLINE_VISIT_FLAG }; diff --git a/src/app/hooks/usePWAOfflineTracking/index.test.tsx b/src/app/hooks/usePWAOfflineTracking/index.test.tsx index 0fad5fc60d2..69de0442728 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.test.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.test.tsx @@ -1,17 +1,15 @@ import { renderHook } from '@testing-library/react'; import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server'; import usePWAOfflineTracking from './index'; -import useIsPWA from '../useIsPWA'; import useNetworkStatusTracker from '../useNetworkStatusTracker'; import useCustomEventTracker from '../useCustomEventTracker'; +import { EffectiveNetworkType } from '../useNetworkStatusTracker/type'; -jest.mock('../useIsPWA'); jest.mock('../useNetworkStatusTracker'); jest.mock('../useCustomEventTracker'); describe('usePWAOfflineTracking', () => { const mockTrackOfflinePageViewEvent = jest.fn(); - const mockUseIsPWA = useIsPWA as jest.MockedFunction; const mockUseNetworkStatusTracker = useNetworkStatusTracker as jest.MockedFunction< typeof useNetworkStatusTracker @@ -32,20 +30,7 @@ describe('usePWAOfflineTracking', () => { jest.restoreAllMocks(); }); - it('should not fire event when not in PWA mode', () => { - mockUseIsPWA.mockReturnValue(false); - mockUseNetworkStatusTracker.mockReturnValue({ - isOnline: true, - networkType: '4g', - }); - - renderHook(() => usePWAOfflineTracking()); - - expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled(); - }); - it('should not fire event when offline flag is not set', () => { - mockUseIsPWA.mockReturnValue(true); mockUseNetworkStatusTracker.mockReturnValue({ isOnline: true, networkType: '4g', @@ -57,8 +42,7 @@ describe('usePWAOfflineTracking', () => { expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled(); }); - it('should fire event when in PWA mode, online, and flag is set', () => { - mockUseIsPWA.mockReturnValue(true); + it('should fire event when online and flag is set', () => { mockUseNetworkStatusTracker.mockReturnValue({ isOnline: true, networkType: '4g', @@ -73,7 +57,6 @@ describe('usePWAOfflineTracking', () => { }); it('should fire event on offline→online transition', () => { - mockUseIsPWA.mockReturnValue(true); Storage.prototype.getItem = jest.fn().mockReturnValue('true'); mockUseNetworkStatusTracker.mockReturnValue({ @@ -98,7 +81,6 @@ describe('usePWAOfflineTracking', () => { }); it('should not fire event again without flag being set', () => { - mockUseIsPWA.mockReturnValue(true); mockUseNetworkStatusTracker.mockReturnValue({ isOnline: true, networkType: '4g', @@ -119,7 +101,6 @@ describe('usePWAOfflineTracking', () => { }); it('should fire event again after flag is set again on next offline visit', () => { - mockUseIsPWA.mockReturnValue(true); const mockGetItem = jest .fn() .mockReturnValueOnce('true') @@ -163,7 +144,6 @@ describe('usePWAOfflineTracking', () => { }); it('should remove flag after firing event', () => { - mockUseIsPWA.mockReturnValue(true); Storage.prototype.getItem = jest.fn().mockReturnValue('true'); mockUseNetworkStatusTracker.mockReturnValue({ @@ -185,7 +165,6 @@ describe('usePWAOfflineTracking', () => { }); it('should not fire when offline even if flag is set', () => { - mockUseIsPWA.mockReturnValue(true); mockUseNetworkStatusTracker.mockReturnValue({ isOnline: false, networkType: 'unknown', @@ -198,7 +177,6 @@ describe('usePWAOfflineTracking', () => { }); it('should handle localStorage errors gracefully', () => { - mockUseIsPWA.mockReturnValue(true); mockUseNetworkStatusTracker.mockReturnValue({ isOnline: true, networkType: '4g', @@ -218,7 +196,6 @@ describe('usePWAOfflineTracking', () => { }); it('should not track on server side', () => { - mockUseIsPWA.mockReturnValue(true); mockUseNetworkStatusTracker.mockReturnValue({ isOnline: true, networkType: '4g', @@ -230,35 +207,23 @@ describe('usePWAOfflineTracking', () => { expect(mockTrackOfflinePageViewEvent).not.toHaveBeenCalled(); }); - it('should pass correct network type to tracking function', () => { - mockUseIsPWA.mockReturnValue(true); - Storage.prototype.getItem = jest.fn().mockReturnValue('true'); - - const networkTypes = [ - 'slow-2g', - '2g', - '3g', - '4g', - '5g', - 'unknown', - ] as const; - - networkTypes.forEach(networkType => { - jest.clearAllMocks(); + it.each(['slow-2g', '2g', '3g', '4g', '5g', 'unknown'])( + 'should pass correct network type to tracking function: %s', + networkType => { + Storage.prototype.getItem = jest.fn().mockReturnValue('true'); mockUseNetworkStatusTracker.mockReturnValue({ isOnline: true, - networkType, + networkType: networkType as EffectiveNetworkType, }); renderHook(() => usePWAOfflineTracking()); expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledWith(networkType); - }); - }); + }, + ); it('should only fire on actual offline→online transition, not online→offline', () => { - mockUseIsPWA.mockReturnValue(true); Storage.prototype.getItem = jest.fn().mockReturnValue('true'); mockUseNetworkStatusTracker.mockReturnValue({ diff --git a/src/app/hooks/usePWAOfflineTracking/index.tsx b/src/app/hooks/usePWAOfflineTracking/index.tsx index 84be6970ed5..a05677779c7 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.tsx @@ -1,5 +1,4 @@ import { useEffect } from 'react'; -import useIsPWA from '../useIsPWA'; import useNetworkStatusTracker from '../useNetworkStatusTracker'; import useCustomEventTracker from '../useCustomEventTracker'; import { OFFLINE_VISIT_FLAG } from '../useOfflinePageFlag'; @@ -7,26 +6,23 @@ import { OFFLINE_VISIT_FLAG } from '../useOfflinePageFlag'; const OFFLINE_PAGE_VIEW_EVENT_NAME = 'pwa-offline-page-view'; /** - * Tracks offline→online transitions in PWA mode after user has visited offline page. + * Tracks offline→online transitions after user has visited offline page. * Fires when network comes back online while flag is set. - * Flag is set by useOfflinePageFlag when user visits offline page while offline. + * + * Flag can only be set in PWA mode (offline page requires service worker). + * By not checking isPWA at dispatch time, we track each offline session separately + * even if user switches between PWA/browser modes during reconnection. + * This prevents data loss and ensures accurate analytics. */ const usePWAOfflineTracking = () => { - const isPWA = useIsPWA(); const { isOnline, networkType } = useNetworkStatusTracker(); const trackOfflinePageViewEvent = useCustomEventTracker({ eventName: OFFLINE_PAGE_VIEW_EVENT_NAME, }); - console.log('usePWAOfflineTracking invoked', { - isPWA, - isOnline, - networkType, - }); - useEffect(() => { - if (typeof window === 'undefined' || !isPWA || !isOnline) { + if (typeof window === 'undefined' || !isOnline) { return; } @@ -43,7 +39,7 @@ const usePWAOfflineTracking = () => { // eslint-disable-next-line no-console console.error('usePWAOfflineTracking', error); } - }, [isPWA, isOnline, networkType, trackOfflinePageViewEvent]); + }, [isOnline, networkType, trackOfflinePageViewEvent]); }; export default usePWAOfflineTracking; diff --git a/ws-nextjs-app/pages/[service]/offline/OfflinePage.test.tsx b/ws-nextjs-app/pages/[service]/offline/OfflinePage.test.tsx index e24431f1d65..c2a55189f8b 100644 --- a/ws-nextjs-app/pages/[service]/offline/OfflinePage.test.tsx +++ b/ws-nextjs-app/pages/[service]/offline/OfflinePage.test.tsx @@ -22,7 +22,7 @@ describe('OfflinePage', () => { expect( screen.getByText( - "It seems you don't have an internet connection at the moment. Please check your connection and reload the page.", + 'Looks like you’re not online right now. Please check your network and reconnect. Once you’re back, just refresh the page to continue.', ), ).toBeInTheDocument(); }); @@ -48,7 +48,7 @@ describe('OfflinePage', () => { expect(screen.getByText('You are offline')).toBeInTheDocument(); expect( screen.getByText( - "It seems you don't have an internet connection at the moment. Please check your connection and reload the page.", + 'Looks like you’re not online right now. Please check your network and reconnect. Once you’re back, just refresh the page to continue.', ), ).toBeInTheDocument(); }); diff --git a/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx b/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx index a303ba88abd..e4cf34ddb0f 100644 --- a/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx +++ b/ws-nextjs-app/pages/[service]/offline/OfflinePage.tsx @@ -2,7 +2,7 @@ import { use } from 'react'; import Helmet from 'react-helmet'; import { ServiceContext } from '#contexts/ServiceContext'; import ErrorMain from '#app/legacy/components/ErrorMain'; -import useOfflinePageFlag from '#app/hooks/useOfflinePageFlag'; +import { useOfflinePageFlag } from '#app/hooks/useOfflinePageFlag'; const OfflinePage = () => { const { service, dir, script } = use(ServiceContext); @@ -12,14 +12,12 @@ const OfflinePage = () => { const title = 'You are offline'; const message = - "It seems you don't have an internet connection at the moment. Please check your connection and reload the page."; + 'Looks like you’re not online right now. Please check your network and reconnect. Once you’re back, just refresh the page to continue.'; const solutions = [ 'Check your internet connection', 'Refresh the page when your connection is restored', ]; - console.log('OfflinePage rendered'); - return ( <> From 9ee5583cf1cdd27c50d0a23996b71aed6a64061c Mon Sep 17 00:00:00 2001 From: skumid01 Date: Fri, 2 Jan 2026 16:42:10 +0200 Subject: [PATCH 88/96] updates due to comments --- .../PageLayoutWrapper/__snapshots__/index.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/PageLayoutWrapper/__snapshots__/index.test.tsx.snap b/src/app/components/PageLayoutWrapper/__snapshots__/index.test.tsx.snap index 17c29daf1ab..ba46852a119 100644 --- a/src/app/components/PageLayoutWrapper/__snapshots__/index.test.tsx.snap +++ b/src/app/components/PageLayoutWrapper/__snapshots__/index.test.tsx.snap @@ -1735,7 +1735,7 @@ exports[`PageLayoutWrapper should render default page wrapper with children 1`] © - 2025 BBC. The BBC is not responsible for the content of external sites. + 2026 BBC. The BBC is not responsible for the content of external sites. Date: Fri, 2 Jan 2026 16:54:30 +0200 Subject: [PATCH 89/96] updates due to comments --- src/app/hooks/usePWAOfflineTracking/index.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/hooks/usePWAOfflineTracking/index.test.tsx b/src/app/hooks/usePWAOfflineTracking/index.test.tsx index 69de0442728..14fb05f7da9 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.test.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.test.tsx @@ -97,7 +97,7 @@ describe('usePWAOfflineTracking', () => { rerender(); - expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(0); + expect(mockTrackOfflinePageViewEvent).toHaveBeenCalledTimes(1); }); it('should fire event again after flag is set again on next offline visit', () => { From af8a64d0fa1b9d6dceb2fe361e9fc1541fb750a9 Mon Sep 17 00:00:00 2001 From: skumid01 Date: Fri, 2 Jan 2026 17:02:23 +0200 Subject: [PATCH 90/96] updates due to comments --- .../PageLayoutWrapper/__snapshots__/index.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/PageLayoutWrapper/__snapshots__/index.test.tsx.snap b/src/app/components/PageLayoutWrapper/__snapshots__/index.test.tsx.snap index ba46852a119..17c29daf1ab 100644 --- a/src/app/components/PageLayoutWrapper/__snapshots__/index.test.tsx.snap +++ b/src/app/components/PageLayoutWrapper/__snapshots__/index.test.tsx.snap @@ -1735,7 +1735,7 @@ exports[`PageLayoutWrapper should render default page wrapper with children 1`] © - 2026 BBC. The BBC is not responsible for the content of external sites. + 2025 BBC. The BBC is not responsible for the content of external sites. Date: Mon, 5 Jan 2026 18:00:55 +0200 Subject: [PATCH 91/96] Adding console logs to help debug event tracking issues --- public/sw.js | 6 +++++- src/app/hooks/useCustomEventTracker/index.tsx | 17 +++++++++++++++++ src/app/hooks/useOfflinePageFlag/index.tsx | 4 +++- src/app/hooks/usePWAOfflineTracking/index.tsx | 9 +++++++++ src/app/lib/analyticsUtils/sendBeacon/index.ts | 6 ++++++ 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/public/sw.js b/public/sw.js index b5a0dcf8b3a..a48cb1509d7 100644 --- a/public/sw.js +++ b/public/sw.js @@ -58,7 +58,11 @@ const cacheOfflinePageAndResources = async service => { .filter(Boolean) .filter(url => url.startsWith('/') || url.startsWith(self.location.origin)) .map(url => new URL(url, self.location.origin).href); - + // Adding console logs to help debug event tracking issues - will remove later + console.log( + `[SW v${version}] Caching offline resources for ${service}:`, + resources, + ); await Promise.allSettled(resources.map(url => cacheResource(cache, url))); }; diff --git a/src/app/hooks/useCustomEventTracker/index.tsx b/src/app/hooks/useCustomEventTracker/index.tsx index aef267599ad..98467a333af 100644 --- a/src/app/hooks/useCustomEventTracker/index.tsx +++ b/src/app/hooks/useCustomEventTracker/index.tsx @@ -57,6 +57,23 @@ const useCustomEventTracker = ({ ].every(Boolean); if (shouldSendEvent) { + // Adding console logs to help debug event tracking issues - will remove later + // eslint-disable-next-line no-console + console.log('Tracking custom event:', { + eventName, + stringifiedData, + campaignID, + pageIdentifier, + platform, + producerId, + producerName, + service, + statsDestination, + useReverb, + experimentName, + experimentVariant, + }); + try { await sendEventBeacon({ type: VIEW_EVENT, diff --git a/src/app/hooks/useOfflinePageFlag/index.tsx b/src/app/hooks/useOfflinePageFlag/index.tsx index 2282b7d2ce1..8e912485486 100644 --- a/src/app/hooks/useOfflinePageFlag/index.tsx +++ b/src/app/hooks/useOfflinePageFlag/index.tsx @@ -10,7 +10,9 @@ const OFFLINE_VISIT_FLAG = 'offline_page_visit'; const useOfflinePageFlag = () => { useEffect(() => { if (typeof window === 'undefined') return; - + // Adding console logs to help debug event tracking issues - will remove later + // eslint-disable-next-line no-console + console.log('useOfflinePageFlag: Setting offline page visit flag.'); try { localStorage.setItem(OFFLINE_VISIT_FLAG, 'true'); } catch (error) { diff --git a/src/app/hooks/usePWAOfflineTracking/index.tsx b/src/app/hooks/usePWAOfflineTracking/index.tsx index a05677779c7..8481b08723b 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.tsx @@ -25,6 +25,9 @@ const usePWAOfflineTracking = () => { if (typeof window === 'undefined' || !isOnline) { return; } + // Adding console logs to help debug event tracking issues - will remove later + // eslint-disable-next-line no-console + console.log('usePWAOfflineTracking: Online detected, checking flag.'); try { const offlineVisitFlag = localStorage.getItem(OFFLINE_VISIT_FLAG); @@ -32,6 +35,12 @@ const usePWAOfflineTracking = () => { if (offlineVisitFlag !== 'true') { return; } + // Adding console logs to help debug event tracking issues - will remove later + // eslint-disable-next-line no-console + console.log( + 'usePWAOfflineTracking: Offline visit flag found, tracking event.', + { networkType, isOnline }, + ); trackOfflinePageViewEvent(networkType); localStorage.removeItem(OFFLINE_VISIT_FLAG); diff --git a/src/app/lib/analyticsUtils/sendBeacon/index.ts b/src/app/lib/analyticsUtils/sendBeacon/index.ts index 40b792339f0..b065d34dbd6 100644 --- a/src/app/lib/analyticsUtils/sendBeacon/index.ts +++ b/src/app/lib/analyticsUtils/sendBeacon/index.ts @@ -138,6 +138,12 @@ const sendBeacon = async ( ) => { if (onClient()) { try { + // Adding console logs to help debug event tracking issues - will remove later + // eslint-disable-next-line no-console + console.log('sendBeacon: Sending beacon to URL:', { + url, + reverbBeaconConfig, + }); if (reverbBeaconConfig) { const { params: { page, user }, From ca20f883ea81171df969f17eea77cc2124fdc741 Mon Sep 17 00:00:00 2001 From: jinidev Date: Mon, 5 Jan 2026 20:39:50 +0200 Subject: [PATCH 92/96] prefetching offline page in client side for js chunks pre-caching --- src/app/hooks/useSendPWAStatus/index.tsx | 24 ++++++++++++++++++++++-- ws-nextjs-app/pages/_app.page.tsx | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/app/hooks/useSendPWAStatus/index.tsx b/src/app/hooks/useSendPWAStatus/index.tsx index 540d889ed25..4e8802257cc 100644 --- a/src/app/hooks/useSendPWAStatus/index.tsx +++ b/src/app/hooks/useSendPWAStatus/index.tsx @@ -1,6 +1,13 @@ import { useEffect } from 'react'; +import { useRouter } from 'next/router'; + +const useSendPWAStatus = ( + isPWA: boolean, + service?: string, + swVersion?: string, +) => { + const router = useRouter(); -const useSendPWAStatus = (isPWA: boolean) => { useEffect(() => { // Service workers not available - exit . if (typeof window === 'undefined' || !navigator.serviceWorker) { @@ -15,6 +22,19 @@ const useSendPWAStatus = (isPWA: boolean) => { type: 'PWA_STATUS', isPWA, }); + + // Prefetch offline route ONCE + Object.keys(localStorage) + .filter(k => k.startsWith(`offline-prefetched-`)) + .forEach(k => localStorage.removeItem(k)); + + const key = `offline-prefetched-${service}-${swVersion}`; + if (localStorage.getItem(key)) return; + + router.prefetch(`/${service}/offline`); + localStorage.setItem(key, 'true'); + + console.log('[PWA] Offline route prefetched'); } }; @@ -32,7 +52,7 @@ const useSendPWAStatus = (isPWA: boolean) => { return () => { sw.removeEventListener('controllerchange', sendPWAStatus); }; - }, [isPWA]); + }, [isPWA, service, router, swVersion]); }; export default useSendPWAStatus; diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 42163c4eb1f..9c63477e080 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -91,7 +91,7 @@ export default function App({ Component, pageProps }: Props) { useServiceWorkerRegistration(service); // Send PWA status to service worker - useSendPWAStatus(isPWA); + useSendPWAStatus(isPWA, service, 'v1'); const RenderChildrenOrError = status === 200 ? ( From 52e6aa0c0427b382533d1b38ab2c9eb7fca65371 Mon Sep 17 00:00:00 2001 From: jinidev Date: Tue, 6 Jan 2026 13:51:32 +0200 Subject: [PATCH 93/96] revert prefetch , remove filtering in cache of resources --- public/sw.js | 20 ++++++++++++-------- src/app/hooks/useSendPWAStatus/index.tsx | 24 ++---------------------- ws-nextjs-app/pages/_app.page.tsx | 2 +- 3 files changed, 15 insertions(+), 31 deletions(-) diff --git a/public/sw.js b/public/sw.js index a48cb1509d7..17b55985daf 100644 --- a/public/sw.js +++ b/public/sw.js @@ -5,9 +5,9 @@ /* eslint-disable no-restricted-globals */ /* eslint-disable no-console */ -const version = 'v0.3.1'; +const version = 'v0.3.2'; // Update cache name when changing caching logic / changes in offlinepage.tsx -const cacheName = 'simorghCache_v3'; +const cacheName = 'simorghCache_v2'; // Track PWA clients const pwaClients = new Map(); @@ -53,14 +53,18 @@ const cacheOfflinePageAndResources = async service => { const linkHrefs = [...html.matchAll(/]+href=["']([^"']+)["']/g)].map( m => m[1], ); - - const resources = [...scriptSrcs, ...linkHrefs] - .filter(Boolean) - .filter(url => url.startsWith('/') || url.startsWith(self.location.origin)) - .map(url => new URL(url, self.location.origin).href); // Adding console logs to help debug event tracking issues - will remove later console.log( - `[SW v${version}] Caching offline resources for ${service}:`, + `[SW v${version}] Caching scriptSrcs ,linkHrefs for ${service}:`, + scriptSrcs, + linkHrefs, + ); + const resources = [...scriptSrcs, ...linkHrefs].filter(Boolean); + // .filter(url => url.startsWith('/') || url.startsWith(self.location.origin)) + // .map(url => new URL(url, self.location.origin).href); + + console.log( + `[SW v${version}] Caching final offline resources for ${service}:`, resources, ); await Promise.allSettled(resources.map(url => cacheResource(cache, url))); diff --git a/src/app/hooks/useSendPWAStatus/index.tsx b/src/app/hooks/useSendPWAStatus/index.tsx index 4e8802257cc..540d889ed25 100644 --- a/src/app/hooks/useSendPWAStatus/index.tsx +++ b/src/app/hooks/useSendPWAStatus/index.tsx @@ -1,13 +1,6 @@ import { useEffect } from 'react'; -import { useRouter } from 'next/router'; - -const useSendPWAStatus = ( - isPWA: boolean, - service?: string, - swVersion?: string, -) => { - const router = useRouter(); +const useSendPWAStatus = (isPWA: boolean) => { useEffect(() => { // Service workers not available - exit . if (typeof window === 'undefined' || !navigator.serviceWorker) { @@ -22,19 +15,6 @@ const useSendPWAStatus = ( type: 'PWA_STATUS', isPWA, }); - - // Prefetch offline route ONCE - Object.keys(localStorage) - .filter(k => k.startsWith(`offline-prefetched-`)) - .forEach(k => localStorage.removeItem(k)); - - const key = `offline-prefetched-${service}-${swVersion}`; - if (localStorage.getItem(key)) return; - - router.prefetch(`/${service}/offline`); - localStorage.setItem(key, 'true'); - - console.log('[PWA] Offline route prefetched'); } }; @@ -52,7 +32,7 @@ const useSendPWAStatus = ( return () => { sw.removeEventListener('controllerchange', sendPWAStatus); }; - }, [isPWA, service, router, swVersion]); + }, [isPWA]); }; export default useSendPWAStatus; diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 9c63477e080..42163c4eb1f 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -91,7 +91,7 @@ export default function App({ Component, pageProps }: Props) { useServiceWorkerRegistration(service); // Send PWA status to service worker - useSendPWAStatus(isPWA, service, 'v1'); + useSendPWAStatus(isPWA); const RenderChildrenOrError = status === 200 ? ( From 8c7d64cef4278835b1a4fc14c8c536ea596b137f Mon Sep 17 00:00:00 2001 From: jinidev Date: Wed, 7 Jan 2026 13:32:54 +0200 Subject: [PATCH 94/96] revert sw changes, removed logs --- public/sw.js | 22 ++++++------------- src/app/hooks/useOfflinePageFlag/index.tsx | 3 --- src/app/hooks/usePWAOfflineTracking/index.tsx | 11 ---------- .../lib/analyticsUtils/sendBeacon/index.ts | 6 ----- 4 files changed, 7 insertions(+), 35 deletions(-) diff --git a/public/sw.js b/public/sw.js index 17b55985daf..b5a0dcf8b3a 100644 --- a/public/sw.js +++ b/public/sw.js @@ -5,9 +5,9 @@ /* eslint-disable no-restricted-globals */ /* eslint-disable no-console */ -const version = 'v0.3.2'; +const version = 'v0.3.1'; // Update cache name when changing caching logic / changes in offlinepage.tsx -const cacheName = 'simorghCache_v2'; +const cacheName = 'simorghCache_v3'; // Track PWA clients const pwaClients = new Map(); @@ -53,20 +53,12 @@ const cacheOfflinePageAndResources = async service => { const linkHrefs = [...html.matchAll(/]+href=["']([^"']+)["']/g)].map( m => m[1], ); - // Adding console logs to help debug event tracking issues - will remove later - console.log( - `[SW v${version}] Caching scriptSrcs ,linkHrefs for ${service}:`, - scriptSrcs, - linkHrefs, - ); - const resources = [...scriptSrcs, ...linkHrefs].filter(Boolean); - // .filter(url => url.startsWith('/') || url.startsWith(self.location.origin)) - // .map(url => new URL(url, self.location.origin).href); - console.log( - `[SW v${version}] Caching final offline resources for ${service}:`, - resources, - ); + const resources = [...scriptSrcs, ...linkHrefs] + .filter(Boolean) + .filter(url => url.startsWith('/') || url.startsWith(self.location.origin)) + .map(url => new URL(url, self.location.origin).href); + await Promise.allSettled(resources.map(url => cacheResource(cache, url))); }; diff --git a/src/app/hooks/useOfflinePageFlag/index.tsx b/src/app/hooks/useOfflinePageFlag/index.tsx index 8e912485486..0a79b5dca6d 100644 --- a/src/app/hooks/useOfflinePageFlag/index.tsx +++ b/src/app/hooks/useOfflinePageFlag/index.tsx @@ -10,9 +10,6 @@ const OFFLINE_VISIT_FLAG = 'offline_page_visit'; const useOfflinePageFlag = () => { useEffect(() => { if (typeof window === 'undefined') return; - // Adding console logs to help debug event tracking issues - will remove later - // eslint-disable-next-line no-console - console.log('useOfflinePageFlag: Setting offline page visit flag.'); try { localStorage.setItem(OFFLINE_VISIT_FLAG, 'true'); } catch (error) { diff --git a/src/app/hooks/usePWAOfflineTracking/index.tsx b/src/app/hooks/usePWAOfflineTracking/index.tsx index 8481b08723b..e0bbcee52ae 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.tsx @@ -25,23 +25,12 @@ const usePWAOfflineTracking = () => { if (typeof window === 'undefined' || !isOnline) { return; } - // Adding console logs to help debug event tracking issues - will remove later - // eslint-disable-next-line no-console - console.log('usePWAOfflineTracking: Online detected, checking flag.'); - try { const offlineVisitFlag = localStorage.getItem(OFFLINE_VISIT_FLAG); if (offlineVisitFlag !== 'true') { return; } - // Adding console logs to help debug event tracking issues - will remove later - // eslint-disable-next-line no-console - console.log( - 'usePWAOfflineTracking: Offline visit flag found, tracking event.', - { networkType, isOnline }, - ); - trackOfflinePageViewEvent(networkType); localStorage.removeItem(OFFLINE_VISIT_FLAG); } catch (error) { diff --git a/src/app/lib/analyticsUtils/sendBeacon/index.ts b/src/app/lib/analyticsUtils/sendBeacon/index.ts index b065d34dbd6..40b792339f0 100644 --- a/src/app/lib/analyticsUtils/sendBeacon/index.ts +++ b/src/app/lib/analyticsUtils/sendBeacon/index.ts @@ -138,12 +138,6 @@ const sendBeacon = async ( ) => { if (onClient()) { try { - // Adding console logs to help debug event tracking issues - will remove later - // eslint-disable-next-line no-console - console.log('sendBeacon: Sending beacon to URL:', { - url, - reverbBeaconConfig, - }); if (reverbBeaconConfig) { const { params: { page, user }, From eef7277ef06c4de0d3055c8d22ed639e8def439d Mon Sep 17 00:00:00 2001 From: jinidev Date: Thu, 8 Jan 2026 17:58:45 +0200 Subject: [PATCH 95/96] EffectiveNetworkType import issue fix --- src/app/hooks/usePWAOfflineTracking/index.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/hooks/usePWAOfflineTracking/index.test.tsx b/src/app/hooks/usePWAOfflineTracking/index.test.tsx index 14fb05f7da9..6bffb85cc94 100644 --- a/src/app/hooks/usePWAOfflineTracking/index.test.tsx +++ b/src/app/hooks/usePWAOfflineTracking/index.test.tsx @@ -1,9 +1,9 @@ import { renderHook } from '@testing-library/react'; import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server'; +import { EffectiveNetworkType } from '#app/models/types/global'; import usePWAOfflineTracking from './index'; import useNetworkStatusTracker from '../useNetworkStatusTracker'; import useCustomEventTracker from '../useCustomEventTracker'; -import { EffectiveNetworkType } from '../useNetworkStatusTracker/type'; jest.mock('../useNetworkStatusTracker'); jest.mock('../useCustomEventTracker'); From 2af8adab1f4a2015776e42e23e21926db6097a0d Mon Sep 17 00:00:00 2001 From: Elvinas Valenas Date: Fri, 9 Jan 2026 10:28:00 +0200 Subject: [PATCH 96/96] fix: undefined variable --- src/app/hooks/useCustomEventTracker/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/hooks/useCustomEventTracker/index.tsx b/src/app/hooks/useCustomEventTracker/index.tsx index dc039227ae3..eaf6d7e4369 100644 --- a/src/app/hooks/useCustomEventTracker/index.tsx +++ b/src/app/hooks/useCustomEventTracker/index.tsx @@ -57,7 +57,7 @@ const useCustomEventTracker = ({ ].every(Boolean); if (shouldSendEvent) { - // Adding console logs to help debug event tracking issues - will remove later + // TEMP: Adding console logs to help debug event tracking issues - will remove later // eslint-disable-next-line no-console console.log('Tracking custom event:', { eventName, @@ -69,7 +69,6 @@ const useCustomEventTracker = ({ producerName, service, statsDestination, - useReverb, experimentName, experimentVariant, });