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/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/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/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/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/Countdown.js b/src/components/Countdown.js index 5c91feb0..3714a0fb 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 { useClockMinute } from "@utils/hooks/useClockMinute"; import styles from '../styles/countdown.module.scss' -class Countdown extends React.Component { +const Countdown = ({ summit, text }) => { + const now = useClockMinute(); - 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/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/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/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/SpeakersWidgetComponent.js b/src/components/SpeakersWidgetComponent.js index 8977e07c..2f5aec53 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 { useClockMinute } from "@utils/hooks/useClockMinute"; import { SentryFallbackFunction } from "./SentryErrorComponent"; -const SpeakersWidgetComponent = ({now, colorSettings, allEvents, speakers, schedules, ...props}) => { +const SpeakersWidgetComponent = ({colorSettings, allEvents, speakers, schedules, ...props}) => { + const now = useClockMinute(); 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 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/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/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/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/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/event-page.js b/src/templates/event-page.js index 244207fa..6e49afb9 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 { useClockSelector } from "openstack-uicore-foundation/lib/components/clock-context"; import { checkMuxTokens, isMuxVideo } from "../utils/videoUtils"; /** @@ -36,24 +38,21 @@ export const EventPageTemplate = class extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - const {eventId, event, eventTokens, eventsPhases, 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 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) => { + 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) { @@ -77,19 +76,14 @@ 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 firstHalf = currentPhase === 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 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 ; } @@ -105,7 +99,7 @@ export const EventPageTemplate = class extends React.Component {
- {this.canRenderVideo(currentPhase) ? ( + {this.canRenderVideo(eventPhase) ? (
)} @@ -216,8 +210,6 @@ const EventPage = ({ eventTokens, eventId, user, - eventsPhases, - nowUtc, getEventById, getEventStreamingInfoById, lastUpdate, @@ -226,6 +218,12 @@ const EventPage = ({ const { getSettingByKey } = useMarketingSettings(); const activityCtaText = getSettingByKey(MARKETING_SETTINGS_KEYS.activityCtaText); + const eventPhase = useEventPhase(event); + const firstHalf = useClockSelector((nowUtc) => + eventPhase === PHASES.DURING && event + ? nowUtc < ((event.start_date + event.end_date) / 2) + : false + ); return ( @@ -243,8 +241,8 @@ const EventPage = ({ eventId={eventId} loading={loading} user={user} - eventsPhases={eventsPhases} - nowUtc={nowUtc} + eventPhase={eventPhase} + firstHalf={firstHalf} location={location} getEventById={getEventById} getEventStreamingInfoById={getEventStreamingInfoById} @@ -263,7 +261,6 @@ EventPage.propTypes = { lastUpdate: PropTypes.object, eventId: PropTypes.string, user: PropTypes.object, - eventsPhases: PropTypes.array, getEventById: PropTypes.func, getEventStreamingInfoById: PropTypes.func, }; @@ -275,7 +272,8 @@ EventPageTemplate.propTypes = { loading: PropTypes.bool, eventId: PropTypes.string, user: PropTypes.object, - eventsPhases: PropTypes.array, + eventPhase: PropTypes.number, + firstHalf: PropTypes.bool, getEventById: PropTypes.func, getEventStreamingInfoById: PropTypes.func, activityCtaText: PropTypes.string, @@ -285,7 +283,6 @@ const mapStateToProps = ({ eventState, summitState, userState, - clockState, settingState }) => ({ loading: eventState.loading, @@ -293,8 +290,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/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
{ @@ -84,59 +84,14 @@ export const PosterDetailPage = ({ }).catch(e => console.log(e)); }, [presentationId]); - useEffect(() => { - if (!notifiedVotingPeriodsOnLoad && - posterTrackGroups.length && - posterTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { - posterTrackGroups.forEach(tg => { - if (votingPeriods[tg].phase === 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) { - 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 => previousVotingPeriods[tg]).every(vp => vp !== undefined)) { - posterTrackGroups.forEach(tg => { - if (previousVotingPeriods[tg].phase === PHASES.BEFORE && votingPeriods[tg].phase === 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) { - 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 (votingPeriods[tg].phase === 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 (votingPeriods[tg].phase === 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]); + useVotingPeriodNotifications({ + trackGroups: posterTrackGroups, + votingPeriods, + votingPeriodsPhases, + votedTrackGroups: votedPosterTrackGroups, + onVotedTrackGroupsHandled: () => setVotedPosterTrackGroups([]), + pushNotification, + }); const { getSettingByKey } = useMarketingSettings(); @@ -206,6 +161,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 +174,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..e1312636 100644 --- a/src/templates/posters-page.js +++ b/src/templates/posters-page.js @@ -23,7 +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'; @@ -53,11 +54,10 @@ 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 [previousVotingPeriods, setPreviousVotingPeriods] = useState(votingPeriods); const [votedPosterTrackGroups, setVotedPosterTrackGroups] = useState([]); + const votingPeriodsPhases = useVotingPeriodsPhasesMap(votingPeriods); + const notificationRef = useRef(null); const filtersWrapperRef = useRef(null); @@ -123,59 +123,14 @@ const PostersPage = ({ setPageTrackGroups(pageTrackGroups); }, [filteredPosters]); - useEffect(() => { - if (!notifiedVotingPeriodsOnLoad && - pageTrackGroups.length && - pageTrackGroups.map(tg => votingPeriods[tg]).every(vp => vp !== undefined)) { - pageTrackGroups.forEach(tg => { - if (votingPeriods[tg].phase === 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) { - 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 => previousVotingPeriods[tg]).every(vp => vp !== undefined)) { - pageTrackGroups.forEach(tg => { - if (previousVotingPeriods[tg].phase === PHASES.BEFORE && votingPeriods[tg].phase === 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) { - 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 (votingPeriods[tg].phase === 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 (votingPeriods[tg].phase === 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]); + useVotingPeriodNotifications({ + trackGroups: pageTrackGroups, + votingPeriods, + votingPeriodsPhases, + votedTrackGroups: votedPosterTrackGroups, + onVotedTrackGroupsHandled: () => setVotedPosterTrackGroups([]), + pushNotification, + }); const filterProps = { summit, @@ -207,6 +162,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/templates/schedule-page.js b/src/templates/schedule-page.js index 46f5b582..7189dc06 100644 --- a/src/templates/schedule-page.js +++ b/src/templates/schedule-page.js @@ -10,13 +10,14 @@ import AttendanceTrackerComponent from "../components/AttendanceTrackerComponent import AccessTracker from "../components/AttendeeToAttendeeWidgetComponent"; import { PageScrollInspector, SCROLL_DIRECTION } from '../components/PageScrollInspector'; import { PHASES } from "../utils/phasesUtils"; +import { useSummitPhase } from "../utils/hooks/useSummitPhase"; import FilterButton from "../components/FilterButton"; import NotFoundPage from "../pages/404"; import withScheduleData from '../utils/withScheduleData' import styles from "../styles/full-schedule.module.scss"; -const SchedulePage = ({ summit, scheduleState, summitPhase, isLoggedUser, location, colorSettings, updateFilter, scheduleProps, schedKey, allowClick, lastDataSync, clearFilters, callAction }) => { - +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/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); 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)); 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/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]); +}; 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 + ); 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"