From cb3ab34d0facd434312717dad4ca6e58ce9acdc2 Mon Sep 17 00:00:00 2001 From: Gabriel Horacio Cutrini Date: Wed, 27 May 2026 15:37:19 -0300 Subject: [PATCH 1/8] perf(clock): migrate summit_phase consumers to ClockProvider hook Wraps the app root in uicore's ClockProvider and adds useSummitPhase, a selector hook that returns the current summit phase computed from the shared clock context. All eight Redux-bound consumers of clockState.summit_phase now call the hook directly, dropping the prop and the related mapStateToProps entry. Receiver components (templates, routes, pages) that were just relaying the value via prop drilling are now self-sufficient. --- gatsby-browser.js | 8 +++++- gatsby-ssr.js | 7 ++++- package.json | 2 +- .../AttendeeToAttendeeWidgetComponent.js | 7 +++-- src/components/AuthComponent.js | 6 ++-- src/components/ClockProvider.js | 17 +++++++++++ src/components/Navbar/index.js | 5 ++-- src/pages/a/[...].js | 28 +++++++++---------- src/pages/a/index.js | 9 +++--- src/pages/a/sponsors.js | 10 +++---- src/pages/index.js | 4 --- src/routes/ShowOpenRoute.js | 4 +-- .../marketing-page-template/MainColumn.js | 5 ++-- .../marketing-page-template/index.js | 4 +-- src/templates/schedule-page.js | 6 ++-- src/utils/hooks/useSummitPhase.js | 8 ++++++ src/utils/withScheduleData.js | 2 -- yarn.lock | 15 +++++++--- 18 files changed, 89 insertions(+), 58 deletions(-) create mode 100644 src/components/ClockProvider.js create mode 100644 src/utils/hooks/useSummitPhase.js diff --git a/gatsby-browser.js b/gatsby-browser.js index 1b325bb8..2958d4d1 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -1,7 +1,9 @@ +import React from "react"; import * as Sentry from "@sentry/gatsby"; import { RewriteFrames as RewriteFramesIntegration } from "@sentry/integrations"; import ReduxWrapper from "./src/state/ReduxWrapper"; import wrapThemeProvider from "./src/utils/wrapThemeProvider"; +import ClockProvider from "./src/components/ClockProvider"; import CookieManager from "./src/utils/cookies/CookieManager"; import KlaroProvider from "./src/utils/cookies/providers/KlaroProvider"; import cookieServices from "./src/utils/cookies/services"; @@ -25,7 +27,11 @@ import colors from "data/colors.json"; import marketingSettings from "data/marketing-settings.json"; export const wrapRootElement = ({ element }) => { - return wrapThemeProvider({ element: ReduxWrapper({ element }) }); + return wrapThemeProvider({ + element: ReduxWrapper({ + element: {element}, + }), + }); }; export const onClientEntry = () => { diff --git a/gatsby-ssr.js b/gatsby-ssr.js index 51c063b5..a36b61f0 100644 --- a/gatsby-ssr.js +++ b/gatsby-ssr.js @@ -9,6 +9,7 @@ import { } from "./src/components/HeadComponents"; import ReduxWrapper from "./src/state/ReduxWrapper"; import wrapThemeProvider from "./src/utils/wrapThemeProvider"; +import ClockProvider from "./src/components/ClockProvider"; // Polyfills for build environment import "./src/utils/buildPolyfills"; @@ -17,7 +18,11 @@ const renderToStringWithEmotion = (bodyComponent) => { const cache = createEmotionCache(); const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache); - const wrappedComponent = wrapThemeProvider({ element: ReduxWrapper({ element: bodyComponent }) }); + const wrappedComponent = wrapThemeProvider({ + element: ReduxWrapper({ + element: {bodyComponent}, + }), + }); const html = ReactDOMServer.renderToString(wrappedComponent); const emotionChunks = extractCriticalToChunks(html); diff --git a/package.json b/package.json index 4d1c2b5b..228ea11c 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "moment-timezone": "^0.5.31", "my-orders-tickets-widget": "1.0.10", "object.assign": "^4.1.5", - "openstack-uicore-foundation": "4.2.30", + "openstack-uicore-foundation": "4.2.31", "path-browserify": "^1.0.1", "prop-types": "^15.6.0", "react": "^18.2.0", diff --git a/src/components/AttendeeToAttendeeWidgetComponent.js b/src/components/AttendeeToAttendeeWidgetComponent.js index da4eb7f1..82661ade 100644 --- a/src/components/AttendeeToAttendeeWidgetComponent.js +++ b/src/components/AttendeeToAttendeeWidgetComponent.js @@ -19,6 +19,7 @@ import { SUPABASE_KEY, } from "@utils/envVariables"; import {PHASES} from "@utils/phasesUtils"; +import {useSummitPhase} from "@utils/hooks/useSummitPhase"; import "attendee-to-attendee-widget/dist/index.css"; @@ -203,7 +204,8 @@ const mapState = ({settingState}) => ({ export const AttendeesWidget = connect(mapState)(AttendeesWidgetComponent); -const AccessTracker = ({user, isLoggedUser, summitPhase, chatSettings, updateChatProfileEnabled = false}) => { +const AccessTracker = ({user, isLoggedUser, chatSettings, updateChatProfileEnabled = false}) => { + const summitPhase = useSummitPhase(); const chatProps = { streamApiKey: getEnvVariable(STREAM_IO_API_KEY), apiBaseUrl: getEnvVariable(IDP_BASE_URL), @@ -317,10 +319,9 @@ const AccessTracker = ({user, isLoggedUser, summitPhase, chatSettings, updateCha return ; }; -const mapStateToProps = ({loggedUserState, userState, clockState, settingState}) => ({ +const mapStateToProps = ({loggedUserState, userState, settingState}) => ({ isLoggedUser: loggedUserState.isLoggedUser, user: userState, - summitPhase: clockState.summit_phase, chatSettings: settingState.widgets.chat }); diff --git a/src/components/AuthComponent.js b/src/components/AuthComponent.js index b3733e60..3adc4b90 100644 --- a/src/components/AuthComponent.js +++ b/src/components/AuthComponent.js @@ -21,6 +21,7 @@ import { getDefaultLocation, validateIdentityProviderButtons } from "@utils/logi import { userHasAccessLevel, VIRTUAL_ACCESS_LEVEL } from "@utils/authorizedGroups"; import useSiteSettings from "@utils/useSiteSettings"; import { PHASES } from "@utils/phasesUtils"; +import { useSummitPhase } from "@utils/hooks/useSummitPhase"; import { getEnvVariable, TENANT_ID } from "@utils/envVariables"; import styles from "../styles/auth-component.module.scss"; @@ -34,7 +35,6 @@ const AuthComponent = ({ allowsNativeAuth, allowsOtpAuth, isLoggedUser, - summitPhase, userProfile, eventRedirect, location, @@ -43,6 +43,7 @@ const AuthComponent = ({ renderLoginButton = null, renderEnterButton = null }) => { + const summitPhase = useSummitPhase(); const [isActive, setIsActive] = useState(false); const [initialEmailValue, setInitialEmailValue] = useState(''); const [otpLogin, setOtpLogin] = useState(false); @@ -235,7 +236,7 @@ const AuthComponent = ({ ) }; -const mapStateToProps = ({ userState, summitState, settingState, clockState, loggedUserState }) => { +const mapStateToProps = ({ userState, summitState, settingState, loggedUserState }) => { return ({ loadingProfile: userState.loading, loadingIDP: userState.loadingIDP, @@ -246,7 +247,6 @@ const mapStateToProps = ({ userState, summitState, settingState, clockState, log colorSettings: settingState.colorSettings, userProfile: userState.userProfile, marketingPageSettings: settingState.marketingPageSettings, - summitPhase: clockState.summit_phase, isLoggedUser: loggedUserState.isLoggedUser, // TODO: move to site settings i/o marketing page settings eventRedirect: settingState.marketingPageSettings.eventRedirect diff --git a/src/components/ClockProvider.js b/src/components/ClockProvider.js new file mode 100644 index 00000000..735db202 --- /dev/null +++ b/src/components/ClockProvider.js @@ -0,0 +1,17 @@ +import React from "react"; +import { useSelector } from "react-redux"; +import { ClockProvider as UICoreClockProvider } from "openstack-uicore-foundation/lib/components/clock-context"; + +const ClockProvider = ({ children }) => { + const summit = useSelector((state) => state.summitState.summit); + return ( + + {children} + + ); +}; + +export default ClockProvider; diff --git a/src/components/Navbar/index.js b/src/components/Navbar/index.js index e40c15b4..5a72977c 100644 --- a/src/components/Navbar/index.js +++ b/src/components/Navbar/index.js @@ -8,13 +8,13 @@ import { userHasAccessLevel, VIRTUAL_ACCESS_LEVEL } from "@utils/authorizedGroup import { getDefaultLocation } from "@utils/loginUtils"; import { PHASES } from "@utils/phasesUtils"; +import { useSummitPhase } from "@utils/hooks/useSummitPhase"; import { USER_REQUIREMENTS, PAGE_RESTRICTIONS } from "@utils/pageAccessConstants"; import navbarContent from "content/navbar/index.json"; const Navbar = ({ location, - summitPhase, summit, isLoggedUser, isAuthorized, @@ -23,6 +23,7 @@ const Navbar = ({ userProfile, eventRedirect }) => { + const summitPhase = useSummitPhase(); // we store this calculation to use it later const hasVirtualBadge = useMemo(() => @@ -110,14 +111,12 @@ const Navbar = ({ }; const mapStateToProps = ({ - clockState, settingState, summitState, loggedUserState, userState }) => ({ summit: summitState.summit, - summitPhase: clockState.summit_phase, isLoggedUser: loggedUserState.isLoggedUser, isAuthorized: userState.isAuthorized, hasTicket: userState.hasTicket, diff --git a/src/pages/a/[...].js b/src/pages/a/[...].js index 361689e5..57ac8759 100644 --- a/src/pages/a/[...].js +++ b/src/pages/a/[...].js @@ -23,17 +23,16 @@ import Link from "../../components/Link"; import { titleFromPathname } from "../../utils/urlFormating"; import {graphql} from "gatsby"; -const mySchedulePage = ({ location, summitPhase,isLoggedUser, user, allowClick, title, key }) => { +const mySchedulePage = ({ location, isLoggedUser, user, allowClick, title, key }) => { return Show Schedule }} schedKey={key} @@ -61,7 +60,7 @@ export const appQuery = graphql` `; -const App = ({ isLoggedUser, user, summitPhase, allowClick = true, data }) => { +const App = ({ isLoggedUser, user, allowClick = true, data }) => { const { mySchedulePageJson } = data; @@ -82,21 +81,21 @@ const App = ({ isLoggedUser, user, summitPhase, allowClick = true, data }) => { - + - { mySchedulePageJson && !mySchedulePageJson.needsTicketAuthz && mySchedulePage({location, summitPhase,isLoggedUser, user, allowClick, title:mySchedulePageJson.title, key: mySchedulePageJson.key }) } - + { mySchedulePageJson && !mySchedulePageJson.needsTicketAuthz && mySchedulePage({location, isLoggedUser, user, allowClick, title:mySchedulePageJson.title, key: mySchedulePageJson.key }) } + - { mySchedulePageJson && mySchedulePageJson.needsTicketAuthz && mySchedulePage({location, summitPhase,isLoggedUser, user, allowClick, title: mySchedulePageJson.title, key: mySchedulePageJson.key }) } - - - + { mySchedulePageJson && mySchedulePageJson.needsTicketAuthz && mySchedulePage({location, isLoggedUser, user, allowClick, title: mySchedulePageJson.title, key: mySchedulePageJson.key }) } + + + - - + + @@ -106,9 +105,8 @@ const App = ({ isLoggedUser, user, summitPhase, allowClick = true, data }) => { ); }; -const mapStateToProps = ({ loggedUserState, userState, clockState, settingState, summitState }) => ({ +const mapStateToProps = ({ loggedUserState, userState, settingState, summitState }) => ({ isLoggedUser: loggedUserState.isLoggedUser, - summitPhase: clockState.summit_phase, user: userState, summitId: summitState?.summit?.id, lastBuild: settingState.lastBuild, diff --git a/src/pages/a/index.js b/src/pages/a/index.js index 588929e6..fb14e3f2 100644 --- a/src/pages/a/index.js +++ b/src/pages/a/index.js @@ -43,14 +43,14 @@ export const lobbyPageQuery = graphql` } `; -const App = ({ data, isLoggedUser, user, summitPhase }) => { +const App = ({ data, isLoggedUser, user }) => { return ( {({ location }) => ( - - + + @@ -61,9 +61,8 @@ const App = ({ data, isLoggedUser, user, summitPhase }) => { ); }; -const mapStateToProps = ({ loggedUserState, userState, clockState }) => ({ +const mapStateToProps = ({ loggedUserState, userState }) => ({ isLoggedUser: loggedUserState.isLoggedUser, - summitPhase: clockState.summit_phase, user: userState }); diff --git a/src/pages/a/sponsors.js b/src/pages/a/sponsors.js index 1631eced..e3c8c666 100644 --- a/src/pages/a/sponsors.js +++ b/src/pages/a/sponsors.js @@ -35,16 +35,15 @@ export const expoHallPageQuery = graphql` const App = ({ data, isLoggedUser, - user, - summitPhase + user }) => { return ( {({ location }) => ( - - + + @@ -57,10 +56,9 @@ const App = ({ const mapStateToProps = ({ loggedUserState, - userState, clockState + userState }) => ({ isLoggedUser: loggedUserState.isLoggedUser, - summitPhase: clockState.summit_phase, user: userState }); diff --git a/src/pages/index.js b/src/pages/index.js index 39e06de0..07e7233c 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -113,7 +113,6 @@ const MarketingPage = ({ data, lastDataSync, summit, - summitPhase, isLoggedUser, }) => ( ); @@ -129,12 +127,10 @@ const MarketingPage = ({ const mapStateToProps = ({ settingState, summitState, - clockState, loggedUserState }) => ({ lastDataSync: settingState.lastDataSync, summit: summitState.summit, - summitPhase: clockState.summit_phase, isLoggedUser: loggedUserState.isLoggedUser, }); diff --git a/src/routes/ShowOpenRoute.js b/src/routes/ShowOpenRoute.js index 00bb7e9e..40ff8297 100644 --- a/src/routes/ShowOpenRoute.js +++ b/src/routes/ShowOpenRoute.js @@ -2,6 +2,7 @@ import React, {useEffect} from "react"; import { connect } from "react-redux"; import { PHASES } from "../utils/phasesUtils"; +import { useSummitPhase } from "../utils/hooks/useSummitPhase"; import { requireExtraQuestions, doVirtualCheckIn } from "../actions/user-actions"; import Interstitial from "../components/Interstitial"; import FragmentParser from "openstack-uicore-foundation/lib/utils/fragment-parser"; @@ -11,7 +12,6 @@ import moment from "moment-timezone"; * * @param children * @param isAuthorized - * @param summitPhase * @param requireExtraQuestions * @param hasTicket * @param userProfile @@ -22,12 +22,12 @@ import moment from "moment-timezone"; const ShowOpenRoute = ({ children, isAuthorized, - summitPhase, requireExtraQuestions, hasTicket, userProfile, doVirtualCheckIn }) => { + const summitPhase = useSummitPhase(); // if we are at show time, and we have an attendee, perform virtual check-in useEffect(() => { diff --git a/src/templates/marketing-page-template/MainColumn.js b/src/templates/marketing-page-template/MainColumn.js index a3616331..1d8ca1d6 100644 --- a/src/templates/marketing-page-template/MainColumn.js +++ b/src/templates/marketing-page-template/MainColumn.js @@ -10,6 +10,7 @@ import ResponsiveImage from "../../components/ResponsiveImage"; import Link from "../../components/Link"; import { PHASES } from "@utils/phasesUtils"; +import { useSummitPhase } from "@utils/hooks/useSummitPhase"; import styles from "./styles.module.scss"; @@ -20,8 +21,9 @@ const shortcodes = { const onEventClick = (ev) => navigate(`/a/event/${ev.id}`); -const MainColumn = ({ widgets, summitPhase, isLoggedUser, onEventClick, lastDataSync, fullWidth, maxHeight }) => { +const MainColumn = ({ widgets, isLoggedUser, onEventClick, lastDataSync, fullWidth, maxHeight }) => { const { content, schedule, disqus, image } = widgets || {}; + const summitPhase = useSummitPhase(); const scheduleProps = schedule && isLoggedUser && summitPhase !== PHASES.BEFORE ? { onEventClick } : {}; @@ -68,7 +70,6 @@ const MainColumn = ({ widgets, summitPhase, isLoggedUser, onEventClick, lastData MainColumn.propTypes = { widgets: PropTypes.object, - summitPhase: PropTypes.number, isLoggedUser: PropTypes.bool, lastDataSync: PropTypes.number, fullWidth: PropTypes.bool, diff --git a/src/templates/marketing-page-template/index.js b/src/templates/marketing-page-template/index.js index 9ccdfc96..070aa1c5 100644 --- a/src/templates/marketing-page-template/index.js +++ b/src/templates/marketing-page-template/index.js @@ -11,7 +11,7 @@ import { useResize } from "@utils/hooks"; import styles from "./styles.module.scss"; -const MarketingPageTemplate = ({ data, location, summit, summitPhase, isLoggedUser, lastDataSync }) => { +const MarketingPageTemplate = ({ data, location, summit, isLoggedUser, lastDataSync }) => { const masonryRef = useRef(); const [rightColumnHeight, setRightColumnHeight] = useState(); @@ -46,7 +46,6 @@ const MarketingPageTemplate = ({ data, location, summit, summitPhase, isLoggedUs
{ - +const SchedulePage = ({ summit, scheduleState, isLoggedUser, location, colorSettings, updateFilter, scheduleProps, schedKey, allowClick, lastDataSync, clearFilters, callAction }) => { + const summitPhase = useSummitPhase(); const [showFilters, setShowfilters] = useState(false); const filtersWrapperRef = useRef(null); const { key, events, allEvents, filters, view, timezone, timeFormat, colorSource } = scheduleState || {}; @@ -109,7 +110,6 @@ const SchedulePage = ({ summit, scheduleState, summitPhase, isLoggedUser, locati SchedulePage.propTypes = { schedKey: PropTypes.string.isRequired, - summitPhase: PropTypes.number, isLoggedUser: PropTypes.bool, }; diff --git a/src/utils/hooks/useSummitPhase.js b/src/utils/hooks/useSummitPhase.js new file mode 100644 index 00000000..3938877b --- /dev/null +++ b/src/utils/hooks/useSummitPhase.js @@ -0,0 +1,8 @@ +import { useSelector } from "react-redux"; +import { useClockSelector } from "openstack-uicore-foundation/lib/components/clock-context"; +import { getSummitPhase } from "../phasesUtils"; + +export const useSummitPhase = () => { + const summit = useSelector((state) => state.summitState.summit); + return useClockSelector((nowUtc) => getSummitPhase(summit, nowUtc)); +}; diff --git a/src/utils/withScheduleData.js b/src/utils/withScheduleData.js index 59e9d9ab..792f620c 100644 --- a/src/utils/withScheduleData.js +++ b/src/utils/withScheduleData.js @@ -30,13 +30,11 @@ const componentWrapper = (WrappedComponent) => ({schedules, ...props}) => { const mapStateToProps = ({ summitState, - clockState, loggedUserState, allSchedulesState, settingState, }) => ({ summit: summitState.summit, - summitPhase: clockState.summit_phase, isLoggedUser: loggedUserState.isLoggedUser, schedules: allSchedulesState.schedules, colorSettings: settingState.colorSettings, diff --git a/yarn.lock b/yarn.lock index 8a729a7e..745d11fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15369,10 +15369,12 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" -openstack-uicore-foundation@4.2.30: - version "4.2.30" - resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-4.2.30.tgz#a7671ae66bcb465d4be7cf763dd8321218725ae3" - integrity sha512-GSH67s/XqviESUY/gSfQ0eRIDYSoy6MX5UYE0zhCFkg7tEFVBSBbuQwEGy2/Cida/uhcxhj58Txl9FStiWsK4Q== +openstack-uicore-foundation@4.2.31: + version "4.2.31" + resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-4.2.31.tgz#593b12ee1cd80cfa299f813b2470dcbde46cc580" + integrity sha512-DE44A0hr5mM9OCak8h4uEdNSjqBzeI+9yPf1/J+TgfzJPtZNrkW2+y43Z8jIHoByI6lBOCug31FuAo6ysDyaiw== + dependencies: + use-sync-external-store "^1.6.0" opentracing@^0.14.7: version "0.14.7" @@ -20885,6 +20887,11 @@ use-ssr@^1.0.25: resolved "https://registry.yarnpkg.com/use-ssr/-/use-ssr-1.0.25.tgz#c7f54b59d6e52db26749b1d4115a650101a190bd" integrity sha512-VYF8kJKI+X7+U4XgGoUER2BUl0vIr+8OhlIhyldgSGE0KHMoDRXPvWeHUUeUktq7ACEOVLzXGq1+QRxcvtwvyQ== +use-sync-external-store@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + utf8-byte-length@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz#f9f63910d15536ee2b2d5dd4665389715eac5c1e" From b8a5f83ba45fb7dfc526fb9db75458ab44102b0a Mon Sep 17 00:00:00 2001 From: Gabriel Horacio Cutrini Date: Wed, 27 May 2026 16:58:54 -0300 Subject: [PATCH 2/8] perf(clock): migrate nowUtc consumers to useClock Countdown converts from class to function. SpeakersWidget drops the clockState entry from mapStateToProps and reads nowUtc from the hook inside the component. --- src/components/Countdown.js | 78 ++++++++++------------- src/components/SpeakersWidgetComponent.js | 9 +-- 2 files changed, 40 insertions(+), 47 deletions(-) diff --git a/src/components/Countdown.js b/src/components/Countdown.js index 5c91feb0..9f99fb33 100644 --- a/src/components/Countdown.js +++ b/src/components/Countdown.js @@ -12,59 +12,51 @@ **/ import React from 'react'; -import { connect } from "react-redux"; import moment from "moment-timezone"; import { epochToMomentTimeZone } from "openstack-uicore-foundation/lib/utils/methods"; +import { useClock } from "openstack-uicore-foundation/lib/components/clock-context"; import styles from '../styles/countdown.module.scss' -class Countdown extends React.Component { +const Countdown = ({ summit, text }) => { + const now = useClock(); - render() { - const { summit, now, text } = this.props; + if (!now || !summit.start_date || !summit.time_zone_id) return null; - if (!now || !summit.start_date || !summit.time_zone_id) return null; + let summitDate = epochToMomentTimeZone(summit.start_date, summit.time_zone_id) + let nowFormatted = epochToMomentTimeZone(now, summit.time_zone_id) - let summitDate = epochToMomentTimeZone(summit.start_date, summit.time_zone_id) - let nowFormatted = epochToMomentTimeZone(now, summit.time_zone_id) + let diff = moment.duration(summitDate.diff(nowFormatted)); + let days = parseInt(diff.asDays()); + let hours = parseInt(diff.asHours()); //2039 hours, but it gives total hours in given miliseconds which is not expacted. + hours = hours - days * 24; + let minutes = parseInt(diff.asMinutes()); //122360 minutes,but it gives total minutes in given miliseconds which is not expacted. + minutes = minutes - (days * 24 * 60 + hours * 60); - let diff = moment.duration(summitDate.diff(nowFormatted)); - let days = parseInt(diff.asDays()); - let hours = parseInt(diff.asHours()); //2039 hours, but it gives total hours in given miliseconds which is not expacted. - hours = hours - days * 24; - let minutes = parseInt(diff.asMinutes()); //122360 minutes,but it gives total minutes in given miliseconds which is not expacted. - minutes = minutes - (days * 24 * 60 + hours * 60); - - if (diff.asMilliseconds() > 0) { - return ( -
-
-
-
{text}
-
-
-
- {days} Days -
-
- {hours} Hours -
-
- {minutes} Minutes -
-
+ if (diff.asMilliseconds() > 0) { + return ( +
+
+
+
{text}
+
+
+
+ {days} Days +
+
+ {hours} Hours +
+
+ {minutes} Minutes +
- ) - } else { - return null - } +
+ ) + } else { + return null } +}; -} - -const mapStateToProps = ({ clockState }) => ({ - now: clockState.nowUtc, -}) - -export default connect(mapStateToProps, null)(Countdown); +export default Countdown; diff --git a/src/components/SpeakersWidgetComponent.js b/src/components/SpeakersWidgetComponent.js index 8977e07c..9b69cc11 100644 --- a/src/components/SpeakersWidgetComponent.js +++ b/src/components/SpeakersWidgetComponent.js @@ -3,13 +3,15 @@ import * as Sentry from "@sentry/react"; import { connect } from "react-redux"; import SpeakersWidget from 'speakers-widget/dist'; import 'speakers-widget/dist/index.css'; -// awesome-bootstrap-checkbox css dependency +// awesome-bootstrap-checkbox css dependency // https://cdnjs.cloudflare.com/ajax/libs/awesome-bootstrap-checkbox/1.0.2/awesome-bootstrap-checkbox.min.css // injected through HeadComponents +import { useClock } from "openstack-uicore-foundation/lib/components/clock-context"; import { SentryFallbackFunction } from "./SentryErrorComponent"; -const SpeakersWidgetComponent = ({now, colorSettings, allEvents, speakers, schedules, ...props}) => { +const SpeakersWidgetComponent = ({colorSettings, allEvents, speakers, schedules, ...props}) => { + const now = useClock(); const scheduleState = schedules?.find( s => s.key === 'schedule-main'); const widgetProps = { @@ -32,8 +34,7 @@ const SpeakersWidgetComponent = ({now, colorSettings, allEvents, speakers, sched ) } -const mapStateToProps = ({ clockState, allSchedulesState, speakerState, settingState }) => ({ - now: clockState.nowUtc, +const mapStateToProps = ({ allSchedulesState, speakerState, settingState }) => ({ colorSettings: settingState.colorSettings, schedules: allSchedulesState.schedules, speakers: speakerState.speakers From b6cc85dc0930377cce56c990fd936bf5975dad91 Mon Sep 17 00:00:00 2001 From: Gabriel Horacio Cutrini Date: Wed, 27 May 2026 17:00:46 -0300 Subject: [PATCH 3/8] perf(clock): migrate events_phases and event-page to clock hooks Adds useEventPhase(event) selector hook backed by useClockSelector. The EventPage container reads nowUtc and eventPhase from hooks and passes them to the EventPageTemplate class, which now compares eventPhase directly instead of looking up entries in clockState.events_phases. --- src/templates/event-page.js | 33 +++++++++++++------------------- src/utils/hooks/useEventPhase.js | 5 +++++ 2 files changed, 18 insertions(+), 20 deletions(-) create mode 100644 src/utils/hooks/useEventPhase.js diff --git a/src/templates/event-page.js b/src/templates/event-page.js index 244207fa..05eb26a9 100644 --- a/src/templates/event-page.js +++ b/src/templates/event-page.js @@ -22,6 +22,8 @@ import { PHASES } from "../utils/phasesUtils"; import { getEventById, getEventStreamingInfoById } from "../actions/event-actions"; import URI from "urijs"; import useMarketingSettings, { MARKETING_SETTINGS_KEYS } from "@utils/useMarketingSettings"; +import { useEventPhase } from "@utils/hooks/useEventPhase"; +import { useClock } from "openstack-uicore-foundation/lib/components/clock-context"; import { checkMuxTokens, isMuxVideo } from "../utils/videoUtils"; /** @@ -36,19 +38,15 @@ export const EventPageTemplate = class extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - const {eventId, event, eventTokens, eventsPhases, lastDataSync} = this.props; + const {eventId, event, eventTokens, eventPhase, lastDataSync} = this.props; if (eventId !== nextProps.eventId) return true; if (!isEqual(event, nextProps.event)) return true; if (!isEqual(eventTokens, nextProps.eventTokens)) return true; // a synch did happened! if (lastDataSync !== nextProps.lastDataSync) return true; // compare current event phase with next one - const currentPhase = eventsPhases.find((e) => parseInt(e.id) === parseInt(eventId))?.phase; - const nextCurrentPhase = nextProps.eventsPhases.find( - (e) => parseInt(e.id) === parseInt(eventId) - )?.phase; - const finishing = (currentPhase === PHASES.DURING && nextCurrentPhase === PHASES.AFTER); - return (currentPhase !== nextCurrentPhase && !finishing ); + const finishing = (eventPhase === PHASES.DURING && nextProps.eventPhase === PHASES.AFTER); + return (eventPhase !== nextProps.eventPhase && !finishing ); } canRenderVideo = (currentPhase) => { @@ -77,11 +75,9 @@ export const EventPageTemplate = class extends React.Component { render() { - const {event, eventTokens, user, loading, nowUtc, summit, eventsPhases, eventId, lastDataSync, activityCtaText} = this.props; - // get current event phase - const currentPhaseInfo = eventsPhases.find((e) => parseInt(e.id) === parseInt(eventId)); - const currentPhase = currentPhaseInfo?.phase; - console.log(`EventPageTemplate::render lastDataSync ${lastDataSync} currentPhase ${currentPhase}`, currentPhaseInfo); + const {event, eventTokens, user, loading, nowUtc, summit, eventPhase, eventId, lastDataSync, activityCtaText} = this.props; + const currentPhase = eventPhase; + console.log(`EventPageTemplate::render lastDataSync ${lastDataSync} currentPhase ${currentPhase}`); const firstHalf = currentPhase === PHASES.DURING ? nowUtc < ((event?.start_date + event?.end_date) / 2) : false; const eventQuery = event.streaming_url ? URI(event.streaming_url).search(true) : null; const autoPlay = eventQuery?.autoplay !== '0'; @@ -216,8 +212,6 @@ const EventPage = ({ eventTokens, eventId, user, - eventsPhases, - nowUtc, getEventById, getEventStreamingInfoById, lastUpdate, @@ -226,6 +220,8 @@ const EventPage = ({ const { getSettingByKey } = useMarketingSettings(); const activityCtaText = getSettingByKey(MARKETING_SETTINGS_KEYS.activityCtaText); + const nowUtc = useClock(); + const eventPhase = useEventPhase(event); return ( @@ -243,7 +239,7 @@ const EventPage = ({ eventId={eventId} loading={loading} user={user} - eventsPhases={eventsPhases} + eventPhase={eventPhase} nowUtc={nowUtc} location={location} getEventById={getEventById} @@ -263,7 +259,6 @@ EventPage.propTypes = { lastUpdate: PropTypes.object, eventId: PropTypes.string, user: PropTypes.object, - eventsPhases: PropTypes.array, getEventById: PropTypes.func, getEventStreamingInfoById: PropTypes.func, }; @@ -275,7 +270,8 @@ EventPageTemplate.propTypes = { loading: PropTypes.bool, eventId: PropTypes.string, user: PropTypes.object, - eventsPhases: PropTypes.array, + eventPhase: PropTypes.number, + nowUtc: PropTypes.number, getEventById: PropTypes.func, getEventStreamingInfoById: PropTypes.func, activityCtaText: PropTypes.string, @@ -285,7 +281,6 @@ const mapStateToProps = ({ eventState, summitState, userState, - clockState, settingState }) => ({ loading: eventState.loading, @@ -293,8 +288,6 @@ const mapStateToProps = ({ eventTokens: eventState.tokens, user: userState, summit: summitState.summit, - eventsPhases: clockState.events_phases, - nowUtc: clockState.nowUtc, lastUpdate: eventState.lastUpdate, lastDataSync: settingState.lastDataSync, }); diff --git a/src/utils/hooks/useEventPhase.js b/src/utils/hooks/useEventPhase.js new file mode 100644 index 00000000..a06be553 --- /dev/null +++ b/src/utils/hooks/useEventPhase.js @@ -0,0 +1,5 @@ +import { useClockSelector } from "openstack-uicore-foundation/lib/components/clock-context"; +import { getEventPhase } from "../phasesUtils"; + +export const useEventPhase = (event) => + useClockSelector((nowUtc) => (event ? getEventPhase(event, nowUtc) : null)); From faa2117d4cdf2ff98696b3723a4b180a011fe158 Mon Sep 17 00:00:00 2001 From: Gabriel Horacio Cutrini Date: Wed, 27 May 2026 19:01:29 -0300 Subject: [PATCH 4/8] perf(clock): migrate voting periods to hook and remove Redux clock Adds useVotingPeriodPhase / useVotingPeriodsPhasesMap, migrates poster grid and detail pages off the Redux-mirrored voting-period phase, and deletes the entire Redux clock plumbing (clock-actions, clock-reducer, ClockComponent). Phase transition notifications now compare against a useRef snapshot of the previous phases map. --- src/actions/clock-actions.js | 94 ---------------------- src/actions/presentation-actions.js | 23 +----- src/components/ClockComponent.js | 17 ---- src/components/Layout.js | 23 +----- src/components/PosterDescription.js | 6 +- src/components/poster-grid/index.jsx | 11 +-- src/content/site-settings/index.json | 54 +------------ src/content/sponsors.json | 2 +- src/model/VotingPeriod.js | 11 +-- src/reducers/clock-reducer.js | 101 ------------------------ src/reducers/index.js | 2 - src/reducers/presentations-reducer.js | 14 ---- src/state/store.js | 1 - src/styles/colors.scss | 14 ++-- src/templates/poster-detail-page.js | 26 +++--- src/templates/posters-page.js | 25 +++--- src/utils/hooks/useVotingPeriodPhase.js | 25 ++++++ 17 files changed, 81 insertions(+), 368 deletions(-) delete mode 100644 src/actions/clock-actions.js delete mode 100644 src/components/ClockComponent.js delete mode 100644 src/reducers/clock-reducer.js create mode 100644 src/utils/hooks/useVotingPeriodPhase.js diff --git a/src/actions/clock-actions.js b/src/actions/clock-actions.js deleted file mode 100644 index a8247918..00000000 --- a/src/actions/clock-actions.js +++ /dev/null @@ -1,94 +0,0 @@ -import { createAction } from "openstack-uicore-foundation/lib/utils/actions"; -import { PHASES, getSummitPhase, getEventPhase } from "@utils/phasesUtils"; -import { updateVotingPeriodsPhase } from "../actions/presentation-actions"; -import { sanitizeHash } from "../actions/security-actions"; - -export const SUMMIT_PHASE_AFTER = "SUMMIT_PHASE_AFTER"; -export const SUMMIT_PHASE_DURING = "SUMMIT_PHASE_DURING"; -export const SUMMIT_PHASE_BEFORE = "SUMMIT_PHASE_BEFORE"; -export const EVENT_PHASE_BEFORE = "EVENT_PHASE_BEFORE"; -export const EVENT_PHASE_DURING = "EVENT_PHASE_DURING"; -export const EVENT_PHASE_AFTER = "EVENT_PHASE_AFTER"; -export const EVENT_PHASE_ADD = "EVENT_PHASE_ADD"; -export const UPDATE_CLOCK = "UPDATE_CLOCK"; - -export const updateClock = (timestamp) => (dispatch) => { - dispatch(createAction(UPDATE_CLOCK)({ timestamp })); - - dispatch(updateSummitPhase()); - dispatch(updateEventsPhase()); - dispatch(updateVotingPeriodsPhase()); - - dispatch(sanitizeHash()); -}; - -export const updateSummitPhase = () => (dispatch, getState) => { - - const { clockState: { nowUtc, summit_phase }, summitState: {summit} } = getState(); - - if (nowUtc) { - const summitPhase = getSummitPhase(summit, nowUtc, summit_phase); - if (summit_phase !== summitPhase) { - switch (summitPhase) { - case PHASES.BEFORE: - dispatch(createAction(SUMMIT_PHASE_BEFORE)(PHASES.BEFORE)); - break; - case PHASES.DURING: - dispatch(createAction(SUMMIT_PHASE_DURING)(PHASES.DURING)); - break; - case PHASES.AFTER: - dispatch(createAction(SUMMIT_PHASE_AFTER)(PHASES.AFTER)); - break; - default: - break; - } - } - } -}; - -export const updateEventsPhase = () => (dispatch, getState) => { - - // get current activity and check phase - const { eventState: { event }, clockState: { nowUtc, events_phases } } = getState(); - - if (event?.id) { - const newEvent = { - id: event.id, - start_date: event.start_date, - end_date: event.end_date, - phase: null - }; - - // if phase for the event is not calculated then create a new empty - if (!events_phases.some(event => event.id === newEvent.id)) { - dispatch(createAction(EVENT_PHASE_ADD)(newEvent)); - } - } - - // on the previous calculated ones , recalculate the advance - events_phases.forEach(event => { - const newPhase = getEventPhase(event, nowUtc); - // if has change the phase - if (event.phase !== newPhase) { - switch (newPhase) { - case PHASES.BEFORE: { - const updatedEvent = { ...event, phase: PHASES.BEFORE }; - dispatch(createAction(EVENT_PHASE_BEFORE)(updatedEvent)); - break; - } - case PHASES.DURING: { - const updatedEvent = { ...event, phase: PHASES.DURING }; - dispatch(createAction(EVENT_PHASE_DURING)(updatedEvent)); - break; - } - case PHASES.AFTER: { - const updatedEvent = { ...event, phase: PHASES.AFTER }; - dispatch(createAction(EVENT_PHASE_AFTER)(updatedEvent)); - break; - } - default: - break; - } - } - }); -}; diff --git a/src/actions/presentation-actions.js b/src/actions/presentation-actions.js index 42f0cd3e..623fbaae 100644 --- a/src/actions/presentation-actions.js +++ b/src/actions/presentation-actions.js @@ -13,7 +13,6 @@ import { customErrorHandler } from '../utils/customErrorHandler'; import { VotingPeriod } from '../model/VotingPeriod'; -import { getVotingPeriodPhase } from '../utils/phasesUtils'; import { mapVotesPerTrackGroup } from '../utils/voting-utils'; import { getEnvVariable, SUMMIT_API_BASE_URL, SUMMIT_ID } from '../utils/envVariables'; @@ -28,7 +27,6 @@ export const GET_PRESENTATION_DETAILS = 'GET_PRESENTATION_DETAILS'; export const GET_PRESENTATION_DETAILS_ERROR = 'GET_PRESENTATION_DETAILS_ERROR'; export const GET_RECOMMENDED_PRESENTATIONS = 'GET_RECOMMENDED_PRESENTATIONS'; export const VOTING_PERIODS_CREATE = 'VOTING_PERIODS_CREATE'; -export const VOTING_PERIODS_PHASE_CHANGE = 'VOTING_PERIODS_PHASE_CHANGE'; const PresentationsDefaultPageSize = 30; export const setInitialDataset = () => (dispatch) => Promise.resolve().then(() => { @@ -174,25 +172,8 @@ export const getRecommendedPresentations = (trackGroups) => async (dispatch) => }); }; -export const updateVotingPeriodsPhase = () => (dispatch, getState) => { - const { clockState: { nowUtc }, presentationsState: { votingPeriods } } = getState(); - if (Object.keys(votingPeriods).length) { - const phaseChanges = []; - Object.entries(votingPeriods).forEach(entry => { - const [trackGroupId, votingPeriod] = entry; - const newPhase = getVotingPeriodPhase(votingPeriod, nowUtc); - if (newPhase !== votingPeriod.phase) { - phaseChanges.push({ trackGroupId, phase: newPhase }); - } - }); - if (phaseChanges.length) - dispatch(createAction(VOTING_PERIODS_PHASE_CHANGE)(phaseChanges)); - } -}; - export const createVotingPeriods = () => (dispatch, getState) => { - const { clockState: { nowUtc }, - userState: { attendee }, + const { userState: { attendee }, summitState: { summit: { track_groups: trackGroups } }, presentationsState: { voteablePresentations: { ssrPresentations: allBuildTimePresentations } } } = getState(); @@ -203,7 +184,7 @@ export const createVotingPeriods = () => (dispatch, getState) => { const { name, begin_attendee_voting_period_date: startDate, end_attendee_voting_period_date: endDate, max_attendee_votes: maxAttendeeVotes } = trackGroup; - const votingPeriod = VotingPeriod({ name, startDate, endDate, maxAttendeeVotes }, nowUtc); + const votingPeriod = VotingPeriod({ name, startDate, endDate, maxAttendeeVotes }); if (votesPerTrackGroup[trackGroup.id]) votingPeriod.addVotes = votesPerTrackGroup[trackGroup.id]; votingPeriods.push({ trackGroupId: trackGroup.id, votingPeriod }); }); diff --git a/src/components/ClockComponent.js b/src/components/ClockComponent.js deleted file mode 100644 index 421f6ae8..00000000 --- a/src/components/ClockComponent.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; -import { connect } from "react-redux"; -import Clock from "openstack-uicore-foundation/lib/components/clock"; -import { updateClock } from "../actions/clock-actions"; - -const ClockComponent = ({ - active, - summit, - updateClock -}) => { - if (!active || !summit) return null; - return ( - updateClock(timestamp)} timezone={summit.time_zone_id} /> - ); -} - -export default connect(null, { updateClock })(ClockComponent); diff --git a/src/components/Layout.js b/src/components/Layout.js index d9d21b31..6a0b5bfa 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -1,39 +1,20 @@ -import React, { useEffect, useState } from "react"; -import { connect } from "react-redux"; +import React from "react"; import Navbar from "../components/Navbar"; -import ClockComponent from "../components/ClockComponent"; import Footer from "../components/Footer"; const TemplateWrapper = ({ children, location, - summit, marketing }) => { - const [isFocus, setIsFocus] = useState(true); - const onFocus = () => setIsFocus(true); - const onBlur = () => setIsFocus(false); - useEffect(() => { - window.addEventListener("focus", onFocus); - window.addEventListener("blur", onBlur); - return () => { - window.removeEventListener("focus", onFocus); - window.removeEventListener("blur", onBlur); - }; - }); return (
Skip to content -
{children}
) }; -const mapStateToProps = ({ summitState }) => ({ - summit: summitState.summit -}); - -export default connect(mapStateToProps, {})(TemplateWrapper); +export default TemplateWrapper; diff --git a/src/components/PosterDescription.js b/src/components/PosterDescription.js index 94f86827..df256394 100644 --- a/src/components/PosterDescription.js +++ b/src/components/PosterDescription.js @@ -5,14 +5,14 @@ import { PHASES } from '../utils/phasesUtils'; import styles from '../styles/poster-components.module.scss'; -const PosterDescription = ({ poster: { speakers, title, description, custom_order, track }, poster, votingPeriods, votes, isVoted, toggleVote, votingAllowed }) => { +const PosterDescription = ({ poster: { speakers, title, description, custom_order, track }, poster, votingPeriods, votingPeriodsPhases, votes, isVoted, toggleVote, votingAllowed }) => { const isDuringVotingPhase = useCallback((poster) => { const results = poster.track?.track_groups?.map(trackGroupId => - votingPeriods[trackGroupId]?.phase === PHASES.DURING + votingPeriodsPhases[trackGroupId] === PHASES.DURING ); return results && results.length ? results.every(r => !!r) : false; - }, [votingPeriods]); + }, [votingPeriodsPhases]); const canVote = useCallback((poster) => { const results = poster.track?.track_groups?.map(trackGroupId => diff --git a/src/components/poster-grid/index.jsx b/src/components/poster-grid/index.jsx index 287f613f..223c87f6 100644 --- a/src/components/poster-grid/index.jsx +++ b/src/components/poster-grid/index.jsx @@ -7,14 +7,14 @@ import { PHASES } from '../../utils/phasesUtils'; import styles from './index.module.scss'; -const PosterGrid = ({ posters, showDetailPage = null, votingAllowed, votingPeriods, votes, toggleVote }) => { +const PosterGrid = ({ posters, showDetailPage = null, votingAllowed, votingPeriods, votingPeriodsPhases, votes, toggleVote }) => { const isDuringVotingPhase = useCallback((poster) => { const results = poster.track?.track_groups?.map(trackGroupId => - votingPeriods[trackGroupId]?.phase === PHASES.DURING + votingPeriodsPhases[trackGroupId] === PHASES.DURING ); return results && results.length ? results.every(r => !!r) : false; - }, [votingPeriods]); + }, [votingPeriodsPhases]); const canVote = useCallback((poster) => { const results = poster.track?.track_groups?.map(trackGroupId => @@ -25,7 +25,7 @@ const PosterGrid = ({ posters, showDetailPage = null, votingAllowed, votingPerio if (!posters) return null; - const cards = posters.map(poster => + const cards = posters.map(poster => { +export const VotingPeriod = (params) => { const isValidStartDate = isValidUTC(params.startDate); const isValidEndDate = isValidUTC(params.endDate); return { @@ -10,10 +9,6 @@ export const VotingPeriod = (params, now) => { endDate: isValidEndDate ? params.endDate : null, attendeeVotes: (Number.isInteger(params.attendeeVotes) || params.attendeeVotes === Infinity) && params.attendeeVotes >= 0 ? params.attendeeVotes : null, maxAttendeeVotes: (Number.isInteger(params.maxAttendeeVotes) || params.maxAttendeeVotes === Infinity) && params.maxAttendeeVotes >= 0 ? params.maxAttendeeVotes === 0 ? Infinity : params.maxAttendeeVotes : null, - phase: // if no valid start date or end date, seems you can still vote in api - isValidUTC(now) ? - getVotingPeriodPhase({ startDate: params.startDate, endDate: params.endDate }, now) : - Number.isInteger(params.phase) && (params.phase === PHASES.BEFORE || params.phase === PHASES.DURING || params.phase === PHASES.AFTER) ? params.phase : null, get remainingVotes() { if (this.attendeeVotes === this.maxAttendeeVotes === null) return null; if (this.attendeeVotes === null && this.maxAttendeeVotes !== null) return this.maxAttendeeVotes; @@ -28,4 +23,4 @@ export const VotingPeriod = (params, now) => { this.attendeeVotes = this.attendeeVotes - value; } } -}; \ No newline at end of file +}; diff --git a/src/reducers/clock-reducer.js b/src/reducers/clock-reducer.js deleted file mode 100644 index 61061f14..00000000 --- a/src/reducers/clock-reducer.js +++ /dev/null @@ -1,101 +0,0 @@ -import { START_LOADING, STOP_LOADING } from "openstack-uicore-foundation/lib/utils/actions"; -import { LOGOUT_USER } from "openstack-uicore-foundation/lib/security/actions"; - -import { - UPDATE_CLOCK, - SUMMIT_PHASE_AFTER, - SUMMIT_PHASE_DURING, - SUMMIT_PHASE_BEFORE, - EVENT_PHASE_AFTER, - EVENT_PHASE_DURING, - EVENT_PHASE_BEFORE, - EVENT_PHASE_ADD -} from "../actions/clock-actions"; - -import {RESET_STATE, SYNC_DATA} from "../actions/base-actions-definitions"; - -import {getEventPhase, getSummitPhase} from "../utils/phasesUtils"; - -import summitData from "data/summit.json"; - -const localNowUtc = Math.round(+new Date() / 1000); - -// calculate on initial state the nowUtc ( local ) and the summit phase using the json data -const DEFAULT_STATE = { - loading: false, - nowUtc: localNowUtc, - summit_phase: getSummitPhase(summitData, localNowUtc), - events_phases: [], -}; - -const clockReducer = (state = DEFAULT_STATE, action) => { - const { type, payload } = action; - switch (type) { - case RESET_STATE: - case LOGOUT_USER: - return DEFAULT_STATE; - case SYNC_DATA: { - const {eventsData, summitData, eventsIDXData } = payload; - // recalculate existent event phases - let oldEventPhases = state.events_phases; - let newEventPhases = oldEventPhases.filter((oldEvent) => { - return eventsIDXData.hasOwnProperty(oldEvent.id) && - (eventsData.length - 1) >= eventsIDXData[oldEvent.id] && - eventsData[eventsIDXData[oldEvent.id]].id == oldEvent.id; - }).map(oldEvent => { - - let idx = eventsIDXData[oldEvent.id]; - let e = eventsData[idx]; - - let newEvent = { - id: e.id, - start_date: e.start_date, - end_date: e.end_date, - phase: null - }; - - const newPhase = getEventPhase(newEvent, state.nowUtc); - - return {...newEvent, phase: newPhase} - }); - - return {...state, summit_phase: getSummitPhase(summitData, state.nowUtc), events_phases:newEventPhases }; - } - case START_LOADING: - return { ...state, loading: true }; - case STOP_LOADING: - return { ...state, loading: false }; - case UPDATE_CLOCK: { - const { timestamp } = payload; - return { ...state, nowUtc: timestamp }; - } - case SUMMIT_PHASE_AFTER: { - return { ...state, summit_phase: payload }; - } - case SUMMIT_PHASE_DURING: { - return { ...state, summit_phase: payload }; - } - case SUMMIT_PHASE_BEFORE: { - return { ...state, summit_phase: payload }; - } - case EVENT_PHASE_ADD: { - return { ...state, events_phases: [...state.events_phases, payload] }; - } - case EVENT_PHASE_AFTER: { - let eventsPhases = [...new Set(state.events_phases.filter(s => s.id !== payload.id))]; - return { ...state, events_phases: [...eventsPhases, payload] }; - } - case EVENT_PHASE_DURING: { - let eventsPhases = [...new Set(state.events_phases.filter(s => s.id !== payload.id))]; - return { ...state, events_phases: [...eventsPhases, payload] }; - } - case EVENT_PHASE_BEFORE: { - let eventsPhases = [...new Set(state.events_phases.filter(s => s.id !== payload.id))]; - return { ...state, events_phases: [...eventsPhases, payload] }; - } - default: - return state; - } -}; - -export default clockReducer; diff --git a/src/reducers/index.js b/src/reducers/index.js index 09326b4f..4ac6ecd3 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,7 +1,6 @@ import { loggedUserReducer } from "openstack-uicore-foundation/lib/security/reducers"; import settingReducer from "./setting-reducer"; import userReducer from "./user-reducer"; -import clockReducer from "./clock-reducer"; import summitReducer from "./summit-reducer"; import allSchedulesReducer from "./all-schedules-reducer"; import presentationsReducer from "./presentations-reducer"; @@ -14,7 +13,6 @@ export { loggedUserReducer, settingReducer, userReducer, - clockReducer, summitReducer, allSchedulesReducer, presentationsReducer, diff --git a/src/reducers/presentations-reducer.js b/src/reducers/presentations-reducer.js index 404164d3..7cfb080c 100644 --- a/src/reducers/presentations-reducer.js +++ b/src/reducers/presentations-reducer.js @@ -21,7 +21,6 @@ import { GET_PRESENTATION_DETAILS, GET_RECOMMENDED_PRESENTATIONS, VOTING_PERIODS_CREATE, - VOTING_PERIODS_PHASE_CHANGE, } from "../actions/presentation-actions"; import { filterEventsByAccessLevels } from "../utils/authorizedGroups"; @@ -137,19 +136,6 @@ const votingPeriods = (state = {}, action) => { } return newState; } - case VOTING_PERIODS_PHASE_CHANGE: { - var newState = { ...state }; - for (const { trackGroupId, phase } of payload) { - newState = { - ...newState, - [trackGroupId]: { - ...newState[trackGroupId], - phase - } - }; - } - return newState; - } case CAST_PRESENTATION_VOTE_REQUEST: case UNCAST_PRESENTATION_VOTE_REQUEST: case TOGGLE_PRESENTATION_VOTE: { diff --git a/src/state/store.js b/src/state/store.js index 4de4bfb6..dc725d85 100644 --- a/src/state/store.js +++ b/src/state/store.js @@ -28,7 +28,6 @@ const states = { loggedUserState: reducers.loggedUserReducer, settingState: reducers.settingReducer, userState: reducers.userReducer, - clockState: reducers.clockReducer, summitState: reducers.summitReducer, allSchedulesState: reducers.allSchedulesReducer, presentationsState: reducers.presentationsReducer, diff --git a/src/styles/colors.scss b/src/styles/colors.scss index aa61303b..49cd68fe 100644 --- a/src/styles/colors.scss +++ b/src/styles/colors.scss @@ -2,12 +2,12 @@ $color_accent: #8ac82d; $color_alerts: #ff0000; $color_background_light: #ffffff; $color_background_dark: #000000; -$color_button_background_color: #ffffff; -$color_button_color: #000000; -$color_gray_lighter: #f2f2f2; -$color_gray_light: #dfdfdf; +$color_button_background_color: #FC5200; +$color_button_color: #FFFFFF; +$color_gray_lighter: #F2F2F2; +$color_gray_light: #DFDFDF; $color_gray_dark: #999999; -$color_gray_darker: #4a4a4a; +$color_gray_darker: #4A4A4A; $color_horizontal_rule_light: #e5e5e5; $color_horizontal_rule_dark: #7b7b7b; $color_icon_light: #ffffff; @@ -19,8 +19,8 @@ $color_input_text_color_light: #363636; $color_input_text_color_dark: #ffffff; $color_input_text_color_disabled_light: #ffffff; $color_input_text_color_disabled_dark: #ffffff; -$color_primary: #8DC63F; -$color_primary_contrast: #FFFFFF; +$color_primary: #8dc63f; +$color_primary_contrast: #ffffff; $color_secondary: #26387f; $color_secondary_contrast: #005870; $color_text_light: #ffffff; diff --git a/src/templates/poster-detail-page.js b/src/templates/poster-detail-page.js index 7ff3a36d..1f9a9333 100644 --- a/src/templates/poster-detail-page.js +++ b/src/templates/poster-detail-page.js @@ -26,6 +26,7 @@ import { castPresentationVote, uncastPresentationVote } from '../actions/user-ac import { PHASES } from '../utils/phasesUtils'; import { isAuthorizedBadge } from '../utils/authorizedGroups'; +import { useVotingPeriodsPhasesMap } from '@utils/hooks/useVotingPeriodPhase'; import useMarketingSettings, { MARKETING_SETTINGS_KEYS } from "@utils/useMarketingSettings"; @@ -56,9 +57,11 @@ export const PosterDetailPage = ({ const [notifiedVotingPeriodsOnLoad, setNotifiedVotingPeriodsOnLoad] = useState(false); const [notifiedMaximunAllowedVotesOnLoad, setNotifiedMaximunAllowedVotesOnLoad] = useState(false); - const [previousVotingPeriods, setPreviousVotingPeriods] = useState(votingPeriods); const [votedPosterTrackGroups, setVotedPosterTrackGroups] = useState([]); + const votingPeriodsPhases = useVotingPeriodsPhasesMap(votingPeriods); + const previousPhasesRef = useRef(votingPeriodsPhases); + const notificationRef = useRef(null); const pushNotification = useCallback((notification) => { @@ -85,16 +88,17 @@ export const PosterDetailPage = ({ }, [presentationId]); useEffect(() => { + const previousPhases = previousPhasesRef.current; if (!notifiedVotingPeriodsOnLoad && posterTrackGroups.length && posterTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { posterTrackGroups.forEach(tg => { - if (votingPeriods[tg].phase === PHASES.BEFORE) { + if (votingPeriodsPhases[tg] === PHASES.BEFORE) { const startDate = new Date(votingPeriods[tg].startDate * 1000).toLocaleDateString('en-US'); const startTime = new Date(votingPeriods[tg].startDate * 1000).toLocaleTimeString('en-US'); pushNotification(`Voting has not begun. ${votingPeriods[tg].name} will allow for votes starting on ${startDate} ${startTime}`); setNotifiedVotingPeriodsOnLoad(true); - } else if (votingPeriods[tg].phase === PHASES.AFTER) { + } else if (votingPeriodsPhases[tg] === PHASES.AFTER) { const endDate = new Date(votingPeriods[tg].endDate * 1000).toLocaleDateString('en-US'); const endTime = new Date(votingPeriods[tg].endDate * 1000).toLocaleTimeString('en-US'); pushNotification(`Voting has ended. ${votingPeriods[tg].name} does not allow for votes after ${endDate} ${endTime}`); @@ -104,11 +108,11 @@ export const PosterDetailPage = ({ } if (posterTrackGroups.length && posterTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined) && - posterTrackGroups.map(tg => previousVotingPeriods[tg]).every(vp => vp !== undefined)) { + posterTrackGroups.map(tg => previousPhases[tg]).every(p => p !== undefined)) { posterTrackGroups.forEach(tg => { - if (previousVotingPeriods[tg].phase === PHASES.BEFORE && votingPeriods[tg].phase === PHASES.DURING) { + if (previousPhases[tg] === PHASES.BEFORE && votingPeriodsPhases[tg] === PHASES.DURING) { pushNotification(`Voting has now begun! You are allowed ${votingPeriods[tg].maxAttendeeVotes} votes in ${votingPeriods[tg].name}`); - } else if (previousVotingPeriods[tg].phase === PHASES.DURING && votingPeriods[tg].phase === PHASES.AFTER) { + } else if (previousPhases[tg] === PHASES.DURING && votingPeriodsPhases[tg] === PHASES.AFTER) { const endDate = new Date(votingPeriods[tg].endDate * 1000).toLocaleDateString('en-US'); const endTime = new Date(votingPeriods[tg].endDate * 1000).toLocaleTimeString('en-US'); pushNotification(`Voting has ended. ${votingPeriods[tg].name} does not allow for votes after ${endDate} ${endTime}`); @@ -119,7 +123,7 @@ export const PosterDetailPage = ({ posterTrackGroups.length && posterTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { posterTrackGroups.forEach(tg => { - if (votingPeriods[tg].phase === PHASES.DURING && votingPeriods[tg].remainingVotes === 0) { + if (votingPeriodsPhases[tg] === PHASES.DURING && votingPeriods[tg].remainingVotes === 0) { pushNotification(`You've reached your maximum votes. ${votingPeriods[tg].name} only allows for ${votingPeriods[tg].maxAttendeeVotes} votes per attendee`); setNotifiedMaximunAllowedVotesOnLoad(true); } @@ -129,14 +133,14 @@ export const PosterDetailPage = ({ posterTrackGroups.length && posterTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { votedPosterTrackGroups.forEach(tg => { - if (votingPeriods[tg].phase === PHASES.DURING && votingPeriods[tg].remainingVotes === 0) { + if (votingPeriodsPhases[tg] === PHASES.DURING && votingPeriods[tg].remainingVotes === 0) { pushNotification(`You've reached your maximum votes. ${votingPeriods[tg].name} only allows for ${votingPeriods[tg].maxAttendeeVotes} votes per attendee`); setVotedPosterTrackGroups([]); } }); } - setPreviousVotingPeriods(votingPeriods); - }, [posterTrackGroups, votingPeriods]); + previousPhasesRef.current = votingPeriodsPhases; + }, [posterTrackGroups, votingPeriods, votingPeriodsPhases]); const { getSettingByKey } = useMarketingSettings(); @@ -206,6 +210,7 @@ export const PosterDetailPage = ({ poster={poster} votingAllowed={!!attendee} votingPeriods={votingPeriods} + votingPeriodsPhases={votingPeriodsPhases} votes={votes} isVoted={!!votes.find(v => v.presentation_id === poster.id)} toggleVote={toggleVote} @@ -218,6 +223,7 @@ export const PosterDetailPage = ({ posters={recommendedPosters} votingAllowed={!!attendee} votingPeriods={votingPeriods} + votingPeriodsPhases={votingPeriodsPhases} votes={votes} toggleVote={toggleVote} showDetailPage={(posterId) => navigate(`/a/poster/${posterId}`)} diff --git a/src/templates/posters-page.js b/src/templates/posters-page.js index 3e133a06..e7fbe174 100644 --- a/src/templates/posters-page.js +++ b/src/templates/posters-page.js @@ -24,6 +24,7 @@ import { import { filterByTrackGroup, randomSort } from '../utils/filterUtils'; import { PHASES } from '../utils/phasesUtils'; +import { useVotingPeriodsPhasesMap } from '@utils/hooks/useVotingPeriodPhase'; import styles from '../styles/posters-page.module.scss'; @@ -55,9 +56,11 @@ const PostersPage = ({ const [pageTrackGroups, setPageTrackGroups] = useState([]); const [notifiedMaximunAllowedVotesOnLoad, setNotifiedMaximunAllowedVotesOnLoad] = useState(false); const [notifiedVotingPeriodsOnLoad, setNotifiedVotingPeriodsOnLoad] = useState(false); - const [previousVotingPeriods, setPreviousVotingPeriods] = useState(votingPeriods); const [votedPosterTrackGroups, setVotedPosterTrackGroups] = useState([]); + const votingPeriodsPhases = useVotingPeriodsPhasesMap(votingPeriods); + const previousPhasesRef = useRef(votingPeriodsPhases); + const notificationRef = useRef(null); const filtersWrapperRef = useRef(null); @@ -124,16 +127,17 @@ const PostersPage = ({ }, [filteredPosters]); useEffect(() => { + const previousPhases = previousPhasesRef.current; if (!notifiedVotingPeriodsOnLoad && pageTrackGroups.length && pageTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { pageTrackGroups.forEach(tg => { - if (votingPeriods[tg].phase === PHASES.BEFORE) { + if (votingPeriodsPhases[tg] === PHASES.BEFORE) { const startDate = new Date(votingPeriods[tg].startDate * 1000).toLocaleDateString('en-US'); const startTime = new Date(votingPeriods[tg].startDate * 1000).toLocaleTimeString('en-US'); pushNotification(`Voting has not begun. ${votingPeriods[tg].name} will allow for votes starting on ${startDate} ${startTime}`); setNotifiedVotingPeriodsOnLoad(true); - } else if (votingPeriods[tg].phase === PHASES.AFTER) { + } else if (votingPeriodsPhases[tg] === PHASES.AFTER) { const endDate = new Date(votingPeriods[tg].endDate * 1000).toLocaleDateString('en-US'); const endTime = new Date(votingPeriods[tg].endDate * 1000).toLocaleTimeString('en-US'); pushNotification(`Voting has ended. ${votingPeriods[tg].name} does not allow for votes after ${endDate} ${endTime}`); @@ -143,11 +147,11 @@ const PostersPage = ({ } if (pageTrackGroups.length && pageTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined) && - pageTrackGroups.map(tg => previousVotingPeriods[tg]).every(vp => vp !== undefined)) { + pageTrackGroups.map(tg => previousPhases[tg]).every(p => p !== undefined)) { pageTrackGroups.forEach(tg => { - if (previousVotingPeriods[tg].phase === PHASES.BEFORE && votingPeriods[tg].phase === PHASES.DURING) { + if (previousPhases[tg] === PHASES.BEFORE && votingPeriodsPhases[tg] === PHASES.DURING) { pushNotification(`Voting has now begun! You are allowed ${votingPeriods[tg].maxAttendeeVotes} votes in ${votingPeriods[tg].name}`); - } else if (previousVotingPeriods[tg].phase === PHASES.DURING && votingPeriods[tg].phase === PHASES.AFTER) { + } else if (previousPhases[tg] === PHASES.DURING && votingPeriodsPhases[tg] === PHASES.AFTER) { const endDate = new Date(votingPeriods[tg].endDate * 1000).toLocaleDateString('en-US'); const endTime = new Date(votingPeriods[tg].endDate * 1000).toLocaleTimeString('en-US'); pushNotification(`Voting has ended. ${votingPeriods[tg].name} does not allow for votes after ${endDate} ${endTime}`); @@ -158,7 +162,7 @@ const PostersPage = ({ pageTrackGroups.length && pageTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { pageTrackGroups.forEach(tg => { - if (votingPeriods[tg].phase === PHASES.DURING && votingPeriods[tg].remainingVotes === 0) { + if (votingPeriodsPhases[tg] === PHASES.DURING && votingPeriods[tg].remainingVotes === 0) { pushNotification(`You've reached your maximum votes. ${votingPeriods[tg].name} only allows for ${votingPeriods[tg].maxAttendeeVotes} votes per attendee`); setNotifiedMaximunAllowedVotesOnLoad(true); } @@ -168,14 +172,14 @@ const PostersPage = ({ pageTrackGroups.length && pageTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { votedPosterTrackGroups.forEach(tg => { - if (votingPeriods[tg].phase === PHASES.DURING && votingPeriods[tg].remainingVotes === 0) { + if (votingPeriodsPhases[tg] === PHASES.DURING && votingPeriods[tg].remainingVotes === 0) { pushNotification(`You've reached your maximum votes. ${votingPeriods[tg].name} only allows for ${votingPeriods[tg].maxAttendeeVotes} votes per attendee`); setVotedPosterTrackGroups([]); } }); } - setPreviousVotingPeriods(votingPeriods); - }, [pageTrackGroups, votingPeriods]); + previousPhasesRef.current = votingPeriodsPhases; + }, [pageTrackGroups, votingPeriods, votingPeriodsPhases]); const filterProps = { summit, @@ -207,6 +211,7 @@ const PostersPage = ({ posters={filteredPosters} showDetailPage={(posterId) => navigate(`/a/poster/${posterId}`)} votingPeriods={votingPeriods} + votingPeriodsPhases={votingPeriodsPhases} votingAllowed={!!attendee} votes={votes} toggleVote={toggleVote} diff --git a/src/utils/hooks/useVotingPeriodPhase.js b/src/utils/hooks/useVotingPeriodPhase.js new file mode 100644 index 00000000..c7e2c404 --- /dev/null +++ b/src/utils/hooks/useVotingPeriodPhase.js @@ -0,0 +1,25 @@ +import { useClockSelector } from "openstack-uicore-foundation/lib/components/clock-context"; +import { getVotingPeriodPhase } from "../phasesUtils"; + +export const useVotingPeriodPhase = (votingPeriod) => + useClockSelector((nowUtc) => (votingPeriod ? getVotingPeriodPhase(votingPeriod, nowUtc) : null)); + +const shallowEqualPhasesMap = (a, b) => { + const keysA = Object.keys(a); + if (keysA.length !== Object.keys(b).length) return false; + return keysA.every((k) => a[k] === b[k]); +}; + +export const useVotingPeriodsPhasesMap = (votingPeriods) => + useClockSelector( + (nowUtc) => { + const result = {}; + if (votingPeriods && nowUtc) { + Object.entries(votingPeriods).forEach(([id, vp]) => { + if (vp) result[id] = getVotingPeriodPhase(vp, nowUtc); + }); + } + return result; + }, + shallowEqualPhasesMap + ); From 9a7add238fab59257b0d285da6681076a1f0414d Mon Sep 17 00:00:00 2001 From: Gabriel Horacio Cutrini Date: Thu, 28 May 2026 07:49:38 -0300 Subject: [PATCH 5/8] refactor(clock): throttle Countdown and SpeakersWidget to minute granularity Adds useClockMinute, a clock selector that snaps nowUtc down to the nearest minute. Countdown and SpeakersWidget consume it instead of useClock, so they re-render at minute boundaries rather than every tick. Both display minute-or-coarser precision, so behaviour is preserved. --- src/components/Countdown.js | 4 ++-- src/components/SpeakersWidgetComponent.js | 4 ++-- src/utils/hooks/useClockMinute.js | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 src/utils/hooks/useClockMinute.js diff --git a/src/components/Countdown.js b/src/components/Countdown.js index 9f99fb33..3714a0fb 100644 --- a/src/components/Countdown.js +++ b/src/components/Countdown.js @@ -14,12 +14,12 @@ import React from 'react'; import moment from "moment-timezone"; import { epochToMomentTimeZone } from "openstack-uicore-foundation/lib/utils/methods"; -import { useClock } from "openstack-uicore-foundation/lib/components/clock-context"; +import { useClockMinute } from "@utils/hooks/useClockMinute"; import styles from '../styles/countdown.module.scss' const Countdown = ({ summit, text }) => { - const now = useClock(); + const now = useClockMinute(); if (!now || !summit.start_date || !summit.time_zone_id) return null; diff --git a/src/components/SpeakersWidgetComponent.js b/src/components/SpeakersWidgetComponent.js index 9b69cc11..2f5aec53 100644 --- a/src/components/SpeakersWidgetComponent.js +++ b/src/components/SpeakersWidgetComponent.js @@ -6,12 +6,12 @@ import 'speakers-widget/dist/index.css'; // awesome-bootstrap-checkbox css dependency // https://cdnjs.cloudflare.com/ajax/libs/awesome-bootstrap-checkbox/1.0.2/awesome-bootstrap-checkbox.min.css // injected through HeadComponents -import { useClock } from "openstack-uicore-foundation/lib/components/clock-context"; +import { useClockMinute } from "@utils/hooks/useClockMinute"; import { SentryFallbackFunction } from "./SentryErrorComponent"; const SpeakersWidgetComponent = ({colorSettings, allEvents, speakers, schedules, ...props}) => { - const now = useClock(); + const now = useClockMinute(); const scheduleState = schedules?.find( s => s.key === 'schedule-main'); const widgetProps = { diff --git a/src/utils/hooks/useClockMinute.js b/src/utils/hooks/useClockMinute.js new file mode 100644 index 00000000..b2114a5a --- /dev/null +++ b/src/utils/hooks/useClockMinute.js @@ -0,0 +1,4 @@ +import { useClockSelector } from "openstack-uicore-foundation/lib/components/clock-context"; + +export const useClockMinute = () => + useClockSelector((nowUtc) => Math.floor((nowUtc ?? 0) / 60) * 60); From 8c1bf9dbebd4e5559b646f828ef25b16502dc8a9 Mon Sep 17 00:00:00 2001 From: Gabriel Horacio Cutrini Date: Thu, 28 May 2026 08:01:27 -0300 Subject: [PATCH 6/8] refactor(event-page): inline eventPhase usage and drop debug log Removes the const currentPhase = eventPhase alias that was being kept for compatibility with the pre-hook internal variable name, replacing the internal currentPhase usages with the eventPhase prop directly. Also removes the EventPageTemplate::render console.log left over from the pre-hook era. --- src/templates/event-page.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/templates/event-page.js b/src/templates/event-page.js index 05eb26a9..a8a52517 100644 --- a/src/templates/event-page.js +++ b/src/templates/event-page.js @@ -49,9 +49,9 @@ export const EventPageTemplate = class extends React.Component { return (eventPhase !== nextProps.eventPhase && !finishing ); } - canRenderVideo = (currentPhase) => { + canRenderVideo = (eventPhase) => { const {event} = this.props; - return (currentPhase >= PHASES.DURING || event.streaming_type === 'VOD') && event.streaming_url; + return (eventPhase >= PHASES.DURING || event.streaming_type === 'VOD') && event.streaming_url; }; componentDidUpdate(prevProps, prevState, snapshot) { @@ -76,16 +76,14 @@ export const EventPageTemplate = class extends React.Component { render() { const {event, eventTokens, user, loading, nowUtc, summit, eventPhase, eventId, lastDataSync, activityCtaText} = this.props; - const currentPhase = eventPhase; - console.log(`EventPageTemplate::render lastDataSync ${lastDataSync} currentPhase ${currentPhase}`); - const firstHalf = currentPhase === PHASES.DURING ? nowUtc < ((event?.start_date + event?.end_date) / 2) : false; + const firstHalf = eventPhase === PHASES.DURING ? nowUtc < ((event?.start_date + event?.end_date) / 2) : false; const eventQuery = event.streaming_url ? URI(event.streaming_url).search(true) : null; const autoPlay = eventQuery?.autoplay !== '0'; // Start time set into seconds, first number is minutes so it multiply per 60 const startTime = eventQuery?.start?.split(',').reduce((a, b, index) => (index === 0 ? parseInt(b) * 60 : parseInt(b)) + a, 0); // if event is loading or we are still calculating the current phase ... - if (loading || currentPhase === undefined || currentPhase === null) { + if (loading || eventPhase === undefined || eventPhase === null) { return ; } @@ -101,7 +99,7 @@ export const EventPageTemplate = class extends React.Component {
- {this.canRenderVideo(currentPhase) ? ( + {this.canRenderVideo(eventPhase) ? (
)} From a37943618007a81d99838c0aa261f78ebe944eaa Mon Sep 17 00:00:00 2001 From: Gabriel Horacio Cutrini Date: Thu, 28 May 2026 08:01:42 -0300 Subject: [PATCH 7/8] refactor(clock): extract useVotingPeriodNotifications hook Posters page and poster detail page had nearly identical effects for voting period notifications (initial load, BEFORE->DURING / DURING->AFTER transitions, max-votes reached). Moves the logic into a shared hook and hides the notifiedOnLoad and previous-phases-ref state inside it. --- src/templates/poster-detail-page.js | 67 +++------------ src/templates/posters-page.js | 67 +++------------ .../hooks/useVotingPeriodNotifications.js | 81 +++++++++++++++++++ 3 files changed, 99 insertions(+), 116 deletions(-) create mode 100644 src/utils/hooks/useVotingPeriodNotifications.js diff --git a/src/templates/poster-detail-page.js b/src/templates/poster-detail-page.js index 1f9a9333..f33bb959 100644 --- a/src/templates/poster-detail-page.js +++ b/src/templates/poster-detail-page.js @@ -24,9 +24,9 @@ import AccessTracker, { AttendeesWidget } from '../components/AttendeeToAttendee import { getAllVoteablePresentations, getPresentationById, setInitialDataset } from '../actions/presentation-actions'; import { castPresentationVote, uncastPresentationVote } from '../actions/user-actions'; -import { PHASES } from '../utils/phasesUtils'; import { isAuthorizedBadge } from '../utils/authorizedGroups'; import { useVotingPeriodsPhasesMap } from '@utils/hooks/useVotingPeriodPhase'; +import { useVotingPeriodNotifications } from '@utils/hooks/useVotingPeriodNotifications'; import useMarketingSettings, { MARKETING_SETTINGS_KEYS } from "@utils/useMarketingSettings"; @@ -55,12 +55,9 @@ export const PosterDetailPage = ({ posterViewable: false }); - const [notifiedVotingPeriodsOnLoad, setNotifiedVotingPeriodsOnLoad] = useState(false); - const [notifiedMaximunAllowedVotesOnLoad, setNotifiedMaximunAllowedVotesOnLoad] = useState(false); const [votedPosterTrackGroups, setVotedPosterTrackGroups] = useState([]); const votingPeriodsPhases = useVotingPeriodsPhasesMap(votingPeriods); - const previousPhasesRef = useRef(votingPeriodsPhases); const notificationRef = useRef(null); @@ -87,60 +84,14 @@ export const PosterDetailPage = ({ }).catch(e => console.log(e)); }, [presentationId]); - useEffect(() => { - const previousPhases = previousPhasesRef.current; - if (!notifiedVotingPeriodsOnLoad && - posterTrackGroups.length && - posterTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { - posterTrackGroups.forEach(tg => { - if (votingPeriodsPhases[tg] === PHASES.BEFORE) { - const startDate = new Date(votingPeriods[tg].startDate * 1000).toLocaleDateString('en-US'); - const startTime = new Date(votingPeriods[tg].startDate * 1000).toLocaleTimeString('en-US'); - pushNotification(`Voting has not begun. ${votingPeriods[tg].name} will allow for votes starting on ${startDate} ${startTime}`); - setNotifiedVotingPeriodsOnLoad(true); - } else if (votingPeriodsPhases[tg] === PHASES.AFTER) { - const endDate = new Date(votingPeriods[tg].endDate * 1000).toLocaleDateString('en-US'); - const endTime = new Date(votingPeriods[tg].endDate * 1000).toLocaleTimeString('en-US'); - pushNotification(`Voting has ended. ${votingPeriods[tg].name} does not allow for votes after ${endDate} ${endTime}`); - setNotifiedVotingPeriodsOnLoad(true); - } - }); - } - if (posterTrackGroups.length && - posterTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined) && - posterTrackGroups.map(tg => previousPhases[tg]).every(p => p !== undefined)) { - posterTrackGroups.forEach(tg => { - if (previousPhases[tg] === PHASES.BEFORE && votingPeriodsPhases[tg] === PHASES.DURING) { - pushNotification(`Voting has now begun! You are allowed ${votingPeriods[tg].maxAttendeeVotes} votes in ${votingPeriods[tg].name}`); - } else if (previousPhases[tg] === PHASES.DURING && votingPeriodsPhases[tg] === PHASES.AFTER) { - const endDate = new Date(votingPeriods[tg].endDate * 1000).toLocaleDateString('en-US'); - const endTime = new Date(votingPeriods[tg].endDate * 1000).toLocaleTimeString('en-US'); - pushNotification(`Voting has ended. ${votingPeriods[tg].name} does not allow for votes after ${endDate} ${endTime}`); - } - }); - } - if (!notifiedMaximunAllowedVotesOnLoad && - posterTrackGroups.length && - posterTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { - posterTrackGroups.forEach(tg => { - if (votingPeriodsPhases[tg] === PHASES.DURING && votingPeriods[tg].remainingVotes === 0) { - pushNotification(`You've reached your maximum votes. ${votingPeriods[tg].name} only allows for ${votingPeriods[tg].maxAttendeeVotes} votes per attendee`); - setNotifiedMaximunAllowedVotesOnLoad(true); - } - }); - } else if (votedPosterTrackGroups && - votedPosterTrackGroups.length && - posterTrackGroups.length && - posterTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { - votedPosterTrackGroups.forEach(tg => { - if (votingPeriodsPhases[tg] === PHASES.DURING && votingPeriods[tg].remainingVotes === 0) { - pushNotification(`You've reached your maximum votes. ${votingPeriods[tg].name} only allows for ${votingPeriods[tg].maxAttendeeVotes} votes per attendee`); - setVotedPosterTrackGroups([]); - } - }); - } - previousPhasesRef.current = votingPeriodsPhases; - }, [posterTrackGroups, votingPeriods, votingPeriodsPhases]); + useVotingPeriodNotifications({ + trackGroups: posterTrackGroups, + votingPeriods, + votingPeriodsPhases, + votedTrackGroups: votedPosterTrackGroups, + onVotedTrackGroupsHandled: () => setVotedPosterTrackGroups([]), + pushNotification, + }); const { getSettingByKey } = useMarketingSettings(); diff --git a/src/templates/posters-page.js b/src/templates/posters-page.js index e7fbe174..e1312636 100644 --- a/src/templates/posters-page.js +++ b/src/templates/posters-page.js @@ -23,8 +23,8 @@ import { } from '../actions/user-actions'; import { filterByTrackGroup, randomSort } from '../utils/filterUtils'; -import { PHASES } from '../utils/phasesUtils'; import { useVotingPeriodsPhasesMap } from '@utils/hooks/useVotingPeriodPhase'; +import { useVotingPeriodNotifications } from '@utils/hooks/useVotingPeriodNotifications'; import styles from '../styles/posters-page.module.scss'; @@ -54,12 +54,9 @@ const PostersPage = ({ const [appliedPageFilter, setAppliedPageFilter] = useState(null); const [filteredPosters, setFilteredPosters] = useState(posters); const [pageTrackGroups, setPageTrackGroups] = useState([]); - const [notifiedMaximunAllowedVotesOnLoad, setNotifiedMaximunAllowedVotesOnLoad] = useState(false); - const [notifiedVotingPeriodsOnLoad, setNotifiedVotingPeriodsOnLoad] = useState(false); const [votedPosterTrackGroups, setVotedPosterTrackGroups] = useState([]); const votingPeriodsPhases = useVotingPeriodsPhasesMap(votingPeriods); - const previousPhasesRef = useRef(votingPeriodsPhases); const notificationRef = useRef(null); const filtersWrapperRef = useRef(null); @@ -126,60 +123,14 @@ const PostersPage = ({ setPageTrackGroups(pageTrackGroups); }, [filteredPosters]); - useEffect(() => { - const previousPhases = previousPhasesRef.current; - if (!notifiedVotingPeriodsOnLoad && - pageTrackGroups.length && - pageTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { - pageTrackGroups.forEach(tg => { - if (votingPeriodsPhases[tg] === PHASES.BEFORE) { - const startDate = new Date(votingPeriods[tg].startDate * 1000).toLocaleDateString('en-US'); - const startTime = new Date(votingPeriods[tg].startDate * 1000).toLocaleTimeString('en-US'); - pushNotification(`Voting has not begun. ${votingPeriods[tg].name} will allow for votes starting on ${startDate} ${startTime}`); - setNotifiedVotingPeriodsOnLoad(true); - } else if (votingPeriodsPhases[tg] === PHASES.AFTER) { - const endDate = new Date(votingPeriods[tg].endDate * 1000).toLocaleDateString('en-US'); - const endTime = new Date(votingPeriods[tg].endDate * 1000).toLocaleTimeString('en-US'); - pushNotification(`Voting has ended. ${votingPeriods[tg].name} does not allow for votes after ${endDate} ${endTime}`); - setNotifiedVotingPeriodsOnLoad(true); - } - }); - } - if (pageTrackGroups.length && - pageTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined) && - pageTrackGroups.map(tg => previousPhases[tg]).every(p => p !== undefined)) { - pageTrackGroups.forEach(tg => { - if (previousPhases[tg] === PHASES.BEFORE && votingPeriodsPhases[tg] === PHASES.DURING) { - pushNotification(`Voting has now begun! You are allowed ${votingPeriods[tg].maxAttendeeVotes} votes in ${votingPeriods[tg].name}`); - } else if (previousPhases[tg] === PHASES.DURING && votingPeriodsPhases[tg] === PHASES.AFTER) { - const endDate = new Date(votingPeriods[tg].endDate * 1000).toLocaleDateString('en-US'); - const endTime = new Date(votingPeriods[tg].endDate * 1000).toLocaleTimeString('en-US'); - pushNotification(`Voting has ended. ${votingPeriods[tg].name} does not allow for votes after ${endDate} ${endTime}`); - } - }); - } - if (!notifiedMaximunAllowedVotesOnLoad && - pageTrackGroups.length && - pageTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { - pageTrackGroups.forEach(tg => { - if (votingPeriodsPhases[tg] === PHASES.DURING && votingPeriods[tg].remainingVotes === 0) { - pushNotification(`You've reached your maximum votes. ${votingPeriods[tg].name} only allows for ${votingPeriods[tg].maxAttendeeVotes} votes per attendee`); - setNotifiedMaximunAllowedVotesOnLoad(true); - } - }); - } else if (votedPosterTrackGroups && - votedPosterTrackGroups.length && - pageTrackGroups.length && - pageTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { - votedPosterTrackGroups.forEach(tg => { - if (votingPeriodsPhases[tg] === PHASES.DURING && votingPeriods[tg].remainingVotes === 0) { - pushNotification(`You've reached your maximum votes. ${votingPeriods[tg].name} only allows for ${votingPeriods[tg].maxAttendeeVotes} votes per attendee`); - setVotedPosterTrackGroups([]); - } - }); - } - previousPhasesRef.current = votingPeriodsPhases; - }, [pageTrackGroups, votingPeriods, votingPeriodsPhases]); + useVotingPeriodNotifications({ + trackGroups: pageTrackGroups, + votingPeriods, + votingPeriodsPhases, + votedTrackGroups: votedPosterTrackGroups, + onVotedTrackGroupsHandled: () => setVotedPosterTrackGroups([]), + pushNotification, + }); const filterProps = { summit, diff --git a/src/utils/hooks/useVotingPeriodNotifications.js b/src/utils/hooks/useVotingPeriodNotifications.js new file mode 100644 index 00000000..26ae9683 --- /dev/null +++ b/src/utils/hooks/useVotingPeriodNotifications.js @@ -0,0 +1,81 @@ +import { useEffect, useRef, useState } from "react"; +import { PHASES } from "../phasesUtils"; + +const formatDate = (epochSeconds) => new Date(epochSeconds * 1000).toLocaleDateString("en-US"); +const formatTime = (epochSeconds) => new Date(epochSeconds * 1000).toLocaleTimeString("en-US"); + +const votingEndedMessage = (vp) => + `Voting has ended. ${vp.name} does not allow for votes after ${formatDate(vp.endDate)} ${formatTime(vp.endDate)}`; + +const maxVotesMessage = (vp) => + `You've reached your maximum votes. ${vp.name} only allows for ${vp.maxAttendeeVotes} votes per attendee`; + +export const useVotingPeriodNotifications = ({ + trackGroups, + votingPeriods, + votingPeriodsPhases, + votedTrackGroups, + onVotedTrackGroupsHandled, + pushNotification, +}) => { + const [notifiedOnLoad, setNotifiedOnLoad] = useState(false); + const [notifiedMaxVotesOnLoad, setNotifiedMaxVotesOnLoad] = useState(false); + const previousPhasesRef = useRef(votingPeriodsPhases); + + useEffect(() => { + const previousPhases = previousPhasesRef.current; + const allLoaded = trackGroups.length && trackGroups.every((tg) => votingPeriods[tg] !== undefined); + if (!allLoaded) return; + + // Initial-load phase notification (BEFORE or AFTER on first observable load). + if (!notifiedOnLoad) { + trackGroups.forEach((tg) => { + const vp = votingPeriods[tg]; + const phase = votingPeriodsPhases[tg]; + if (phase === PHASES.BEFORE) { + pushNotification(`Voting has not begun. ${vp.name} will allow for votes starting on ${formatDate(vp.startDate)} ${formatTime(vp.startDate)}`); + setNotifiedOnLoad(true); + } else if (phase === PHASES.AFTER) { + pushNotification(votingEndedMessage(vp)); + setNotifiedOnLoad(true); + } + }); + } + + // Phase transitions (BEFORE->DURING, DURING->AFTER). + const previousAllLoaded = trackGroups.every((tg) => previousPhases[tg] !== undefined); + if (previousAllLoaded) { + trackGroups.forEach((tg) => { + const vp = votingPeriods[tg]; + const prev = previousPhases[tg]; + const next = votingPeriodsPhases[tg]; + if (prev === PHASES.BEFORE && next === PHASES.DURING) { + pushNotification(`Voting has now begun! You are allowed ${vp.maxAttendeeVotes} votes in ${vp.name}`); + } else if (prev === PHASES.DURING && next === PHASES.AFTER) { + pushNotification(votingEndedMessage(vp)); + } + }); + } + + // Max-votes notification. + if (!notifiedMaxVotesOnLoad) { + trackGroups.forEach((tg) => { + const vp = votingPeriods[tg]; + if (votingPeriodsPhases[tg] === PHASES.DURING && vp.remainingVotes === 0) { + pushNotification(maxVotesMessage(vp)); + setNotifiedMaxVotesOnLoad(true); + } + }); + } else if (votedTrackGroups?.length && votedTrackGroups.every((tg) => votingPeriods[tg] !== undefined)) { + votedTrackGroups.forEach((tg) => { + const vp = votingPeriods[tg]; + if (votingPeriodsPhases[tg] === PHASES.DURING && vp.remainingVotes === 0) { + pushNotification(maxVotesMessage(vp)); + onVotedTrackGroupsHandled?.(); + } + }); + } + + previousPhasesRef.current = votingPeriodsPhases; + }, [trackGroups, votingPeriods, votingPeriodsPhases, votedTrackGroups]); +}; From 67d65ef523515e534127684c22710c1157685381 Mon Sep 17 00:00:00 2001 From: Gabriel Horacio Cutrini Date: Thu, 28 May 2026 08:10:21 -0300 Subject: [PATCH 8/8] fix(event-page): make firstHalf flip at the event midpoint shouldComponentUpdate only allowed re-renders on phase change, so the inline firstHalf calculation inside render was stuck on its initial DURING value and never flipped at the midpoint. Computes firstHalf via useClockSelector in the outer functional EventPage, passes it as a prop, and adds it to the class sCU check so the midpoint transition triggers a re-render. Drops the now-unused nowUtc prop from EventPageTemplate. --- src/templates/event-page.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/templates/event-page.js b/src/templates/event-page.js index a8a52517..6e49afb9 100644 --- a/src/templates/event-page.js +++ b/src/templates/event-page.js @@ -23,7 +23,7 @@ import { getEventById, getEventStreamingInfoById } from "../actions/event-action import URI from "urijs"; import useMarketingSettings, { MARKETING_SETTINGS_KEYS } from "@utils/useMarketingSettings"; import { useEventPhase } from "@utils/hooks/useEventPhase"; -import { useClock } from "openstack-uicore-foundation/lib/components/clock-context"; +import { useClockSelector } from "openstack-uicore-foundation/lib/components/clock-context"; import { checkMuxTokens, isMuxVideo } from "../utils/videoUtils"; /** @@ -38,12 +38,13 @@ export const EventPageTemplate = class extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - const {eventId, event, eventTokens, eventPhase, lastDataSync} = this.props; + const {eventId, event, eventTokens, eventPhase, firstHalf, lastDataSync} = this.props; if (eventId !== nextProps.eventId) return true; if (!isEqual(event, nextProps.event)) return true; if (!isEqual(eventTokens, nextProps.eventTokens)) return true; // a synch did happened! if (lastDataSync !== nextProps.lastDataSync) return true; + if (firstHalf !== nextProps.firstHalf) return true; // compare current event phase with next one const finishing = (eventPhase === PHASES.DURING && nextProps.eventPhase === PHASES.AFTER); return (eventPhase !== nextProps.eventPhase && !finishing ); @@ -75,8 +76,7 @@ export const EventPageTemplate = class extends React.Component { render() { - const {event, eventTokens, user, loading, nowUtc, summit, eventPhase, eventId, lastDataSync, activityCtaText} = this.props; - const firstHalf = eventPhase === PHASES.DURING ? nowUtc < ((event?.start_date + event?.end_date) / 2) : false; + const {event, eventTokens, user, loading, summit, eventPhase, firstHalf, eventId, lastDataSync, activityCtaText} = this.props; const eventQuery = event.streaming_url ? URI(event.streaming_url).search(true) : null; const autoPlay = eventQuery?.autoplay !== '0'; // Start time set into seconds, first number is minutes so it multiply per 60 @@ -218,8 +218,12 @@ const EventPage = ({ const { getSettingByKey } = useMarketingSettings(); const activityCtaText = getSettingByKey(MARKETING_SETTINGS_KEYS.activityCtaText); - const nowUtc = useClock(); const eventPhase = useEventPhase(event); + const firstHalf = useClockSelector((nowUtc) => + eventPhase === PHASES.DURING && event + ? nowUtc < ((event.start_date + event.end_date) / 2) + : false + ); return ( @@ -238,7 +242,7 @@ const EventPage = ({ loading={loading} user={user} eventPhase={eventPhase} - nowUtc={nowUtc} + firstHalf={firstHalf} location={location} getEventById={getEventById} getEventStreamingInfoById={getEventStreamingInfoById} @@ -269,7 +273,7 @@ EventPageTemplate.propTypes = { eventId: PropTypes.string, user: PropTypes.object, eventPhase: PropTypes.number, - nowUtc: PropTypes.number, + firstHalf: PropTypes.bool, getEventById: PropTypes.func, getEventStreamingInfoById: PropTypes.func, activityCtaText: PropTypes.string,