diff --git a/src/api/profile.ts b/src/api/profile.ts index e7be0ec375..f44271135e 100644 --- a/src/api/profile.ts +++ b/src/api/profile.ts @@ -1,6 +1,10 @@ import Bugsnag from "@bugsnag/js"; import firebase from "firebase/app"; +import { VenueEvent } from "types/venues"; + +import { WithVenueId } from "utils/id"; + export interface MakeUpdateUserGridLocationProps { venueId: string; userUid: string; @@ -48,3 +52,50 @@ export const makeUpdateUserGridLocation = ({ firestore.doc(doc).set(newData); }); }; + +export interface UpdatePersonalizedScheduleProps { + event: WithVenueId; + userId: string; + removeMode?: boolean; +} + +export const addEventToPersonalizedSchedule = ({ + event, + userId, +}: Omit): Promise => + updatePersonalizedSchedule({ event, userId }); + +export const removeEventFromPersonalizedSchedule = ({ + event, + userId, +}: Omit): Promise => + updatePersonalizedSchedule({ event, userId, removeMode: true }); + +export const updatePersonalizedSchedule = async ({ + event, + userId, + removeMode = false, +}: UpdatePersonalizedScheduleProps): Promise => { + const userProfileRef = firebase.firestore().collection("users").doc(userId); + + const modify = removeMode + ? firebase.firestore.FieldValue.arrayRemove + : firebase.firestore.FieldValue.arrayUnion; + + const newSavedEvents = { + [`myPersonalizedSchedule.${event.venueId}`]: modify(event.id), + }; + + return userProfileRef.update(newSavedEvents).catch((err) => { + Bugsnag.notify(err, (event) => { + event.addMetadata("context", { + location: "api/profile::saveEventToProfile", + userId, + event, + removeMode, + }); + + throw err; + }); + }); +}; diff --git a/src/assets/icons/icon-loading.svg b/src/assets/icons/icon-loading.svg new file mode 100644 index 0000000000..48d84d3657 --- /dev/null +++ b/src/assets/icons/icon-loading.svg @@ -0,0 +1,14 @@ + + + icon-loading + + + + + + + + + + + diff --git a/src/components/molecules/EventDisplay/EventDisplay.tsx b/src/components/molecules/EventDisplay/EventDisplay.tsx index f75a535ef7..865dfcc11d 100644 --- a/src/components/molecules/EventDisplay/EventDisplay.tsx +++ b/src/components/molecules/EventDisplay/EventDisplay.tsx @@ -16,6 +16,13 @@ export interface EventDisplayProps { venue?: WithId; } +/** + * @dept the componnet is used in the OnlineStats and VenuePreview (Playa) which are to be removed as part of the Playa cleanup work. + * + * @see https://github.com/sparkletown/sparkle/pull/833 + * + * @deprecated since https://github.com/sparkletown/sparkle/pull/1302 is merged; the component is replaced by ScheduleEvent + */ export const EventDisplay: React.FC = ({ event, venue }) => { const eventRoomTitle = event.room; diff --git a/src/components/molecules/EventPaymentButton/EventPaymentButton.tsx b/src/components/molecules/EventPaymentButton/EventPaymentButton.tsx index e6cfc498a5..a99394d3fb 100644 --- a/src/components/molecules/EventPaymentButton/EventPaymentButton.tsx +++ b/src/components/molecules/EventPaymentButton/EventPaymentButton.tsx @@ -1,16 +1,12 @@ import React from "react"; import "firebase/functions"; +import { Link } from "react-router-dom"; import { VenueEvent } from "types/venues"; -import "./EventPaymentButton.scss"; -import useConnectUserPurchaseHistory from "hooks/useConnectUserPurchaseHistory"; -import { Link } from "react-router-dom"; import { hasUserBoughtTicketForEvent } from "utils/hasUserBoughtTicket"; import { isUserAMember } from "utils/isUserAMember"; -import { canUserJoinTheEvent } from "utils/time"; -import { useUser } from "hooks/useUser"; -import { useSelector } from "hooks/useSelector"; +import { isEventStartingSoon } from "utils/event"; import { WithId } from "utils/id"; import { venueEntranceUrl } from "utils/url"; import { @@ -18,6 +14,12 @@ import { userPurchaseHistorySelector, } from "utils/selectors"; +import { useUser } from "hooks/useUser"; +import { useSelector } from "hooks/useSelector"; +import useConnectUserPurchaseHistory from "hooks/useConnectUserPurchaseHistory"; + +import "./EventPaymentButton.scss"; + interface PropsType { event: WithId; venueId: string; @@ -57,7 +59,7 @@ const EventPaymentButton: React.FunctionComponent = ({ diff --git a/src/components/molecules/LiveSchedule/LiveSchedule.scss b/src/components/molecules/LiveSchedule/LiveSchedule.scss deleted file mode 100644 index 4dab6aaaa2..0000000000 --- a/src/components/molecules/LiveSchedule/LiveSchedule.scss +++ /dev/null @@ -1,51 +0,0 @@ -@import "scss/constants.scss"; - -.sidebar-container .schedule-container { - @include scrollbar; - height: calc(100% - #{$chat-input-height + $footer-height}); - flex-direction: column; - display: none; - overflow-y: scroll; - overflow-x: hidden; - - &.show { - display: flex; - } - - .schedule-tabs { - height: auto; - padding: 20px 12px; - li { - font-size: 0.9rem; - padding: 10px 6px; - } - } - - .schedule-event-container { - display: flex; - align-items: flex-start; - padding: 12px; - margin-bottom: 4px; - - &:nth-child(odd) { - background-color: rgba($black, 0.3); - } - &.schedule-event-container--live { - background-color: rgba($primary, 0.2); - } - - .schedule-event-time { - width: 88px; - flex-shrink: 0; - } - } -} - -.schedule-event-empty { - width: 100%; - height: 100%; - flex-grow: 1; - display: flex; - align-items: center; - justify-content: center; -} diff --git a/src/components/molecules/LiveSchedule/LiveSchedule.tsx b/src/components/molecules/LiveSchedule/LiveSchedule.tsx deleted file mode 100644 index 0482d30089..0000000000 --- a/src/components/molecules/LiveSchedule/LiveSchedule.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { FC, useCallback, useMemo } from "react"; - -import { AnyVenue, VenueEvent } from "types/venues"; - -import { isEventLive } from "utils/event"; -import { venueSelector } from "utils/selectors"; -import { WithId, WithVenueId } from "utils/id"; - -// import { useLegacyConnectRelatedVenues } from "hooks/useRelatedVenues"; -import { useSelector } from "hooks/useSelector"; -// import { useVenueId } from "hooks/useVenueId"; - -import { EventDisplay } from "../EventDisplay"; - -import "./LiveSchedule.scss"; -import { hasElements } from "utils/types"; - -const emptyArray: never[] = []; - -const LiveSchedule: FC = () => { - // const venueId = useVenueId(); - const currentVenue = useSelector(venueSelector); - - // @debt Stubbing out legacy code as this component isn't used anymore and is getting deleted in a different PR. - // useLegacyConnectRelatedVenues({ venueId }); - // - // const { relatedVenueEvents, relatedVenues } = useLegacyConnectRelatedVenues({ - // venueId, - // withEvents: true, - // }); - const relatedVenues: WithId[] = emptyArray; - const relatedVenueEvents: WithVenueId[] = emptyArray; - - const relatedVenueFor = useCallback( - (event: WithVenueId) => - relatedVenues.find((venue) => venue.id === event.venueId) ?? currentVenue, - [currentVenue, relatedVenues] - ); - - const events = useMemo(() => { - return relatedVenueEvents && relatedVenueEvents.length - ? relatedVenueEvents.filter((event) => isEventLive(event)) - : []; - }, [relatedVenueEvents]); - - const hasEvents = hasElements(events); - - const renderedEvents = useMemo(() => { - if (!hasEvents) return null; - - return events.map((event, index) => ( - - )); - }, [events, hasEvents, relatedVenueFor]); - - if (!hasEvents) { - return
No live events for now
; - } - - return ( -
-
{renderedEvents}
-
- ); -}; - -export default LiveSchedule; diff --git a/src/components/molecules/LiveSchedule/index.ts b/src/components/molecules/LiveSchedule/index.ts deleted file mode 100644 index 9c9ba4f49d..0000000000 --- a/src/components/molecules/LiveSchedule/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./LiveSchedule"; diff --git a/src/components/molecules/Loading/Loading.scss b/src/components/molecules/Loading/Loading.scss new file mode 100644 index 0000000000..9d5c8234e8 --- /dev/null +++ b/src/components/molecules/Loading/Loading.scss @@ -0,0 +1,33 @@ +@keyframes loadingspin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +$Loading__icon--size: 40px; +$Loading__icon--margin-bottom: 1rem; +$Loading__message--font-size: 0.9rem; +$Loading__message--opacity: 0.6; + +.Loading { + display: flex; + flex-direction: column; + width: 100%; + text-align: center; + + &__icon { + margin: 0 auto; + width: $Loading__icon--size; + height: $Loading__icon--size; + margin-bottom: $Loading__icon--margin-bottom; + animation: loadingspin 0.6s infinite linear; + } + + &__message { + font-size: $Loading__message--font-size; + opacity: $Loading__message--opacity; + } +} diff --git a/src/components/molecules/Loading/Loading.tsx b/src/components/molecules/Loading/Loading.tsx new file mode 100644 index 0000000000..f2d3f4b038 --- /dev/null +++ b/src/components/molecules/Loading/Loading.tsx @@ -0,0 +1,18 @@ +import React, { FC } from "react"; + +import LoadingIcon from "assets/icons/icon-loading.svg"; + +import "./Loading.scss"; + +export interface LoadingProps { + message?: string; +} + +export const Loading: FC = ({ message }) => { + return ( +
+ loading + {message} +
+ ); +}; diff --git a/src/components/molecules/Loading/index.ts b/src/components/molecules/Loading/index.ts new file mode 100644 index 0000000000..0de49db4c0 --- /dev/null +++ b/src/components/molecules/Loading/index.ts @@ -0,0 +1 @@ +export { Loading } from "./Loading"; diff --git a/src/components/molecules/NavBar/NavBar.scss b/src/components/molecules/NavBar/NavBar.scss index c15158f9d7..fdbcca6e04 100644 --- a/src/components/molecules/NavBar/NavBar.scss +++ b/src/components/molecules/NavBar/NavBar.scss @@ -205,97 +205,6 @@ $border-radius: 28px; text-shadow: 0 2px 3px rgb(0 0 0 / 35%); } -.schedule-dropdown-body { - position: fixed; - z-index: z(navbar-schedule); - top: 0; - left: 0; - width: 100%; - height: 620px; - display: flex; - flex-wrap: wrap; - padding: 60px 40px 0; - background-color: rgba($black, 0.9); - overflow: none; - - pointer-events: none; - opacity: 0; - transform: translateY(-800px); - backdrop-filter: blur(5px); - box-shadow: 0 20px 50px 0 rgba(0, 0, 0, 0.33); - transition: all 400ms $transition-function; - - &.show { - pointer-events: all; - opacity: 1; - transform: translateY(0px); - } - - .partyinfo-container { - flex: 1; - margin-right: 60px; - .partyinfo-main { - display: flex; - align-items: center; - margin-top: 2rem; - margin-bottom: 1rem; - .partyinfo-pic { - flex-shrink: 0; - width: 92px; - height: 92px; - background-size: cover; - border-radius: 46px; - margin-right: 20px; - cursor: pointer; - transition: all 400ms $transition-function; - box-shadow: 0 8px 16px rgba($black, 0.3); - &:hover { - transform: scale(1.1); - } - &:active { - transform: scale(0.9); - } - } - .partyinfo-title { - h2 { - font-style: italic; - margin-bottom: 5px; - } - h3 { - font-weight: 400; - } - } - } - .partyinfo-desc { - font-size: 0.8rem; - opacity: 0.8; - } - } - - .schedule-container { - position: relative; - flex: 2; - .schedule-tabs { - height: 50px; - margin-top: 30px; - position: fixed; - } - .schedule-day-container { - @include scrollbar; - position: absolute; - width: 100%; - top: 70px; - height: calc(100% - 70px); - overflow-y: scroll; - padding-bottom: 20px; - - .schedule-event-container { - border-radius: $border-radius/2; - } - } - } -} - .schedule-dropdown-backdrop { position: fixed; left: 0; @@ -350,128 +259,6 @@ $border-radius: 28px; } } -.schedule-container { - @include scrollbar; - - .schedule-tabs { - li { - display: inline; - cursor: pointer; - padding: 10px 18px; - margin-right: 10px; - border-radius: $border-radius; - &:hover { - background-color: rgba($black, 0.3); - } - &.active { - background-color: $primary; - text-shadow: 0 2px 4px rgba($black, 0.3); - box-shadow: 0 3px 6px rgba($black, 0.2); - font-weight: 500; - } - } - } - - .schedule-event-container { - display: flex; - align-items: flex-start; - padding: 12px; - margin-bottom: 10px; - - &:nth-child(odd) { - background-color: rgba($black, 0.3); - } - &.schedule-event-container--live { - background-color: rgba($primary, 0.2); - } - - .schedule-event-time { - width: 100px; - flex-shrink: 0; - .schedule-event-time-start { - font-size: 0.9rem; - font-weight: 500; - } - .schedule-event-time-end { - font-size: 0.9rem; - opacity: 0.8; - } - .schedule-event-time-live, - .schedule-event-time-soon { - display: inline-block; - font-size: 0.8rem; - font-weight: 500; - margin-top: 10px; - background-color: $primary; - box-shadow: 0 0 20px rgba($white, 0.2); - padding: 4px 10px; - border-radius: $border-radius/2; - text-align: center; - } - .schedule-event-time-soon { - box-shadow: none; - - background-color: rgba($primary, 0.5); - } - } - .schedule-event-info { - .schedule-event-info-title { - font-weight: 700; - } - .schedule-event-info-description { - font-size: 0.9rem; - opacity: 0.8; - } - .schedule-event-info-room { - margin-top: 10px; - font-size: 0.9rem; - span.schedule-event-info-room-icon { - display: inline-block; - height: 15px; - width: 15px; - vertical-align: middle; - margin-left: 4px; - margin-right: 4px; - background-size: 90px 15px; - &.schedule-event-info-room-icon_conversation { - background-position: 0 0; - } - &.schedule-event-info-room-icon_auditorium { - background-position: -15px 0; - } - &.schedule-event-info-room-icon_art { - background-position: -30px 0; - } - &.schedule-event-info-room-icon_musicbar { - background-position: -45px 0; - } - &.schedule-event-info-room-icon_reception { - background-position: -60px 0; - } - &.schedule-event-info-room-icon_concert { - background-position: -75px 0; - } - } - a { - opacity: 0.8; - text-decoration: underline; - &:hover { - opacity: 1; - } - } - div { - opacity: 0.8; - text-decoration: underline; - cursor: pointer; - &:hover { - opacity: 1; - } - } - } - } - } -} - @media (max-width: 600px) { .schedule-text { display: none; diff --git a/src/components/molecules/NavBar/NavBar.tsx b/src/components/molecules/NavBar/NavBar.tsx index eeabdcee6c..ad929173a7 100644 --- a/src/components/molecules/NavBar/NavBar.tsx +++ b/src/components/molecules/NavBar/NavBar.tsx @@ -29,7 +29,7 @@ import { useFirestoreConnect } from "hooks/useFirestoreConnect"; import { GiftTicketModal } from "components/organisms/GiftTicketModal/GiftTicketModal"; import { ProfilePopoverContent } from "components/organisms/ProfileModal"; import { RadioModal } from "components/organisms/RadioModal/RadioModal"; -import { SchedulePageModal } from "components/organisms/SchedulePageModal/SchedulePageModal"; +import { NavBarSchedule } from "components/organisms/NavBarSchedule/NavBarSchedule"; import NavSearchBar from "components/molecules/NavSearchBar"; import UpcomingTickets from "components/molecules/UpcomingTickets"; @@ -68,6 +68,8 @@ const GiftPopover = ( ); +const navBarScheduleClassName = "NavBar__schedule-dropdown"; + interface NavBarPropsType { redirectionUrl?: string; hasBackButton?: boolean; @@ -141,7 +143,9 @@ const NavBar: React.FC = ({ const toggleEventSchedule = useCallback(() => { setEventScheduleVisible(!isEventScheduleVisible); }, [isEventScheduleVisible]); - const hideEventSchedule = useCallback(() => { + const hideEventSchedule = useCallback((e) => { + if (e.target.closest(`.${navBarScheduleClassName}`)) return; + setEventScheduleVisible(false); }, []); @@ -315,10 +319,9 @@ const NavBar: React.FC = ({ }`} onClick={hideEventSchedule} > - +
+ +
{/* @debt Remove back button from Navbar */} diff --git a/src/components/molecules/NavSearchBar/NavSearchBar.tsx b/src/components/molecules/NavSearchBar/NavSearchBar.tsx index 427e06ed65..6e050d9052 100644 --- a/src/components/molecules/NavSearchBar/NavSearchBar.tsx +++ b/src/components/molecules/NavSearchBar/NavSearchBar.tsx @@ -3,7 +3,7 @@ import classNames from "classnames"; import { faSearch } from "@fortawesome/free-solid-svg-icons"; -import { DEFAULT_PARTY_NAME } from "settings"; +import { DEFAULT_PARTY_NAME, DEFAULT_VENUE_LOGO } from "settings"; import { VenueEvent } from "types/venues"; import { Room, RoomTypes } from "types/rooms"; @@ -115,7 +115,7 @@ const NavSearchBar = () => { description={`Event - ${uppercaseFirstChar( formatUtcSecondsRelativeToNow(event.start_utc_seconds) )}`} - image={imageUrl} + image={imageUrl ?? DEFAULT_VENUE_LOGO} /> ); }); diff --git a/src/components/molecules/OnlineStats/OnlineStats.scss b/src/components/molecules/OnlineStats/OnlineStats.scss index 8ca0915597..74ed9e375b 100644 --- a/src/components/molecules/OnlineStats/OnlineStats.scss +++ b/src/components/molecules/OnlineStats/OnlineStats.scss @@ -99,14 +99,10 @@ .venue-address { display: block; + @include line-clamp-with-overflow(3); font-size: 0.8rem; opacity: 0.8; margin-bottom: 4px; - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; } .whatson-container { @@ -125,14 +121,10 @@ font-weight: 500; } .whatson-description-container-description { + @include line-clamp-with-overflow(2); font-size: 0.8rem; opacity: 0.8; margin-bottom: 4px; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; } .centered-flex { .btn { diff --git a/src/components/molecules/OnlineStats/OnlineStats.tsx b/src/components/molecules/OnlineStats/OnlineStats.tsx index 10ac9062c4..d0aad8f5b9 100644 --- a/src/components/molecules/OnlineStats/OnlineStats.tsx +++ b/src/components/molecules/OnlineStats/OnlineStats.tsx @@ -78,6 +78,7 @@ const OnlineStats: React.FC = () => { const venueName = venue?.name; const { openUserProfileModal } = useProfileModalControls(); + // @debt FIVE_MINUTES_MS is deprecated; create needed constant in settings useInterval(() => { firebase .functions() diff --git a/src/components/molecules/Schedule/Schedule.constants.scss b/src/components/molecules/Schedule/Schedule.constants.scss new file mode 100644 index 0000000000..6dce9c84a8 --- /dev/null +++ b/src/components/molecules/Schedule/Schedule.constants.scss @@ -0,0 +1,4 @@ +$Schedule--padding: 20px; + +$room--margin-bottom: 1rem; +$room--height: 70px; diff --git a/src/components/molecules/Schedule/Schedule.scss b/src/components/molecules/Schedule/Schedule.scss new file mode 100644 index 0000000000..315bc0e641 --- /dev/null +++ b/src/components/molecules/Schedule/Schedule.scss @@ -0,0 +1,98 @@ +@import "scss/constants.scss"; +@import "./Schedule.constants.scss"; + +$room-count: var(--room-count, 1); +$current-time--position: var(--current-time--position, -2); +$hours-count: var(--hours-count, 8); + +$hour-width: var(--hour-width, 200px); +$half-hour-width: calc(#{$hour-width} / 2); + +$rooms-column--width: 215px; +$timeline--height: 36px; + +$time-line--margin-top: 18px; +$time-line--width: 1px; +$current-time-line--width: 2px; + +$time-line--height: calc( + (#{$room--height} + #{$room--margin-bottom}) * #{$room-count} +); + +$schedule--width: calc(#{$hours-count} * #{$hour-width} + #{$half-hour-width}); + +.Schedule { + padding-top: $Schedule--padding; + display: flex; + position: relative; + + &__rooms { + flex-basis: $rooms-column--width; + flex-shrink: 0; + margin-top: $timeline--height; + padding-right: $Schedule--padding; + box-shadow: box-shadow--large(); + font-size: 0.8rem; + } + + &__room { + margin-bottom: $room--margin-bottom; + height: $room--height; + display: flex; + justify-content: center; + flex-flow: column; + } + + &__schedule { + @include scrollbar; + flex-grow: 1; + overflow-x: auto; + position: relative; + } + + &__room-title { + margin-bottom: 0; + font-weight: 600; + } + + &__timeline { + height: $timeline--height; + font-size: $font-size--md; + opacity: 0.6; + white-space: nowrap; + } + + &__hour { + width: $hour-width; + text-align: center; + display: inline-block; + + &::after { + content: ""; + display: block; + height: $time-line--height; + border-left: #{$time-line--width} solid white; + margin-left: $half-hour-width; + margin-top: $time-line--margin-top; + } + } + + &__user-schedule { + height: $room--height; + margin-bottom: $room--margin-bottom; + background-color: rgba(255, 255, 255, 0.2); + width: $schedule--width; + } + + &__current-time-line { + position: absolute; + width: $current-time-line--width; + height: $time-line--height; + left: calc(#{$current-time--position} * 1px); + background-color: $primary; + } + + &__no-events { + padding: $Schedule--padding; + } +} diff --git a/src/components/molecules/Schedule/Schedule.tsx b/src/components/molecules/Schedule/Schedule.tsx new file mode 100644 index 0000000000..47e999dcaf --- /dev/null +++ b/src/components/molecules/Schedule/Schedule.tsx @@ -0,0 +1,183 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import classNames from "classnames"; +import { + eachHourOfInterval, + endOfDay, + format, + getHours, + getUnixTime, + setHours, +} from "date-fns"; +import { useCss } from "react-use"; + +import { + SCHEDULE_CURRENT_TIMELINE_MS, + SCHEDULE_HOUR_COLUMN_WIDTH_PX, + SCHEDULE_MAX_START_HOUR, +} from "settings"; + +import { PersonalizedVenueEvent, LocatedEvents } from "types/venues"; + +import { eventStartTime } from "utils/event"; +import { formatMeasurement } from "utils/formatMeasurement"; + +import { useInterval } from "hooks/useInterval"; + +import { Loading } from "components/molecules/Loading"; +import { ScheduleRoomEvents } from "components/molecules/ScheduleRoomEvents"; + +import { calcStartPosition } from "./Schedule.utils"; + +import "./Schedule.scss"; + +export interface ScheduleProps { + locatedEvents: LocatedEvents[]; + personalEvents: PersonalizedVenueEvent[]; + scheduleDate: Date; + isToday: boolean; + isLoading: boolean; +} + +export const Schedule: React.FC = ({ + locatedEvents, + personalEvents, + scheduleDate, + isToday, + isLoading, +}) => { + const hasEvents = locatedEvents.length > 0; + + const scheduleStartHour = useMemo( + () => + Math.min( + ...locatedEvents.map(({ events }) => + events.reduce( + (acc, event) => Math.min(acc, getHours(eventStartTime(event))), + SCHEDULE_MAX_START_HOUR + ) + ), + SCHEDULE_MAX_START_HOUR + ), + [locatedEvents] + ); + + const scheduleStartDateTime = useMemo( + () => setHours(scheduleDate, scheduleStartHour), + [scheduleStartHour, scheduleDate] + ); + + // pairs (venueId, roomTitle) are unique because they are grouped earlier (see NavBarSchedule#schedule) + const roomCells = useMemo( + () => + locatedEvents?.map(({ location, events }) => ( +
+

+ {location.roomTitle || location.venueTitle || location.venueId} +

+ + {formatMeasurement(events.length, "event")} + +
+ )), + [locatedEvents] + ); + + const hoursRow = useMemo( + () => + eachHourOfInterval({ + start: scheduleStartDateTime, + end: endOfDay(scheduleStartDateTime), + }).map((scheduleHour) => ( + + {format(scheduleHour, "h a")} + + )), + [scheduleStartDateTime] + ); + + const [currentTimePosition, setCurrentTimePosition] = useState(0); + + const calcCurrentTimePosition = useCallback( + () => + setCurrentTimePosition( + calcStartPosition(getUnixTime(Date.now()), scheduleStartHour) + ), + [scheduleStartHour] + ); + + useEffect(() => calcCurrentTimePosition(), [calcCurrentTimePosition]); + + useInterval(() => { + calcCurrentTimePosition(); + }, SCHEDULE_CURRENT_TIMELINE_MS); + + const containerVars = useCss({ + "--room-count": locatedEvents.length + 1, // +1 is needed for the 1st personalized line of the schedule + "--current-time--position": currentTimePosition, + "--hours-count": hoursRow.length, + "--hour-width": `${SCHEDULE_HOUR_COLUMN_WIDTH_PX}px`, + }); + + const containerClasses = classNames("Schedule", containerVars); + + // pairs (venueId, roomTitle) are unique because they are grouped earlier (see NavBarSchedule#schedule) + const rowsWithTheEvents = useMemo( + () => + locatedEvents.map(({ location, events }) => ( + + )), + [locatedEvents, scheduleStartHour] + ); + + if (isLoading) + return ( +
+ +
+ ); + + if (!hasEvents) + return ( +
+
No events scheduled
+
+ ); + + return ( +
+
+
+

My Daily Schedule

+ + {personalEvents.length} events + +
+ + {roomCells} +
+ +
+
{hoursRow}
+ + {isToday &&
} + +
+ +
+ + {rowsWithTheEvents} +
+
+ ); +}; diff --git a/src/components/molecules/Schedule/Schedule.utils.ts b/src/components/molecules/Schedule/Schedule.utils.ts new file mode 100644 index 0000000000..5c945b8b21 --- /dev/null +++ b/src/components/molecules/Schedule/Schedule.utils.ts @@ -0,0 +1,18 @@ +import { SCHEDULE_HOUR_COLUMN_WIDTH_PX } from "settings"; + +import { getSecondsFromStartOfDay, ONE_HOUR_IN_SECONDS } from "utils/time"; + +export const calcStartPosition = ( + startTimeUtcSeconds: number, + scheduleStartHour: number +) => { + const startTimeSeconds = getSecondsFromStartOfDay(startTimeUtcSeconds); + // @debt ONE_HOUR_IN_SECONDS is deprecated; use utils/time or date-fns function instead + const hoursToSkip = + startTimeSeconds / ONE_HOUR_IN_SECONDS - scheduleStartHour; + const halfHourWidth = SCHEDULE_HOUR_COLUMN_WIDTH_PX / 2; + + return Math.floor( + halfHourWidth + hoursToSkip * SCHEDULE_HOUR_COLUMN_WIDTH_PX + ); +}; diff --git a/src/components/molecules/Schedule/index.ts b/src/components/molecules/Schedule/index.ts new file mode 100644 index 0000000000..100acaf802 --- /dev/null +++ b/src/components/molecules/Schedule/index.ts @@ -0,0 +1 @@ +export { Schedule } from "./Schedule"; diff --git a/src/components/molecules/ScheduleEvent/ScheduleEvent.scss b/src/components/molecules/ScheduleEvent/ScheduleEvent.scss new file mode 100644 index 0000000000..4fd3500196 --- /dev/null +++ b/src/components/molecules/ScheduleEvent/ScheduleEvent.scss @@ -0,0 +1,99 @@ +@import "scss/constants.scss"; + +$grey--regular: #3f3d42; +$grey--hover: #4c494f; +$grey--users: #4c494f; +$grey--light: #ebebeb; + +$purple--regular: #7c46fb; +$purple--hover: #7c46fb; + +$ScheduleEvent--margin-top: 0.25rem; +$ScheduleEvent--height: 3.75rem; +$ScheduleEvent--padding: 0 0.25rem 0 0.75rem; +$ScheduleEvent--border-radius: 18px; +$ScheduleEvent--box-shadow: 0 5px 10px rgba(0, 0, 0, 0.65); + +$bookmark--padding: 0.5rem; +$bookmark-hover--padding-top: 0.375rem; + +$description--margin-top: 2px; + +.ScheduleEvent { + display: flex; + position: absolute; + border-radius: $ScheduleEvent--border-radius; + padding: $ScheduleEvent--padding; + cursor: pointer; + height: $ScheduleEvent--height; + justify-content: space-between; + align-items: center; + background-color: $grey--regular; + box-shadow: $ScheduleEvent--box-shadow; + margin-top: $ScheduleEvent--margin-top; + margin-left: var(--event--margin-left); + width: var(--event--width); + + &:hover { + background-color: $grey--hover; + z-index: z(navbar__schedule-event--hover); + } + + &--users { + color: $grey--hover; + background-color: $grey--light; + + &:hover { + background-color: $white; + } + + .ScheduleEvent__bookmark { + color: $black; + } + } + + &--live { + background-color: $purple--regular; + color: $white; + + &:hover { + background-color: $purple--hover; + } + + .ScheduleEvent__bookmark { + color: $white; + } + } + + &__info { + font-size: 0.8rem; + overflow: hidden; + } + + &__title { + @include line-clamp-with-overflow(2); + white-space: pre-line; + font-weight: bold; + } + + &__host { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; + margin-top: $description--margin-top; + font-size: 0.7rem; + } + + &__bookmark { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + padding: $bookmark--padding; + + &:hover { + padding-top: $bookmark-hover--padding-top; + } + } +} diff --git a/src/components/molecules/ScheduleEvent/ScheduleEvent.tsx b/src/components/molecules/ScheduleEvent/ScheduleEvent.tsx new file mode 100644 index 0000000000..fbfb230851 --- /dev/null +++ b/src/components/molecules/ScheduleEvent/ScheduleEvent.tsx @@ -0,0 +1,84 @@ +import React, { MouseEventHandler, useCallback } from "react"; +import classNames from "classnames"; +import { useCss } from "react-use"; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faBookmark as solidBookmark } from "@fortawesome/free-solid-svg-icons"; +import { faBookmark as regularBookmark } from "@fortawesome/free-regular-svg-icons"; + +import { SCHEDULE_HOUR_COLUMN_WIDTH_PX } from "settings"; + +import { PersonalizedVenueEvent } from "types/venues"; + +import { isEventLive } from "utils/event"; + +import { ONE_HOUR_IN_MINUTES } from "utils/time"; + +import { + addEventToPersonalizedSchedule, + removeEventFromPersonalizedSchedule, +} from "api/profile"; +import { useUser } from "hooks/useUser"; + +import { calcStartPosition } from "components/molecules/Schedule/Schedule.utils"; + +import "./ScheduleEvent.scss"; + +export interface ScheduleEventProps { + event: PersonalizedVenueEvent; + scheduleStartHour: number; + personalizedEvent?: boolean; +} + +export const ScheduleEvent: React.FC = ({ + event, + scheduleStartHour, + personalizedEvent: isPersonalizedEvent = false, +}) => { + const { userId } = useUser(); + + // @debt ONE_HOUR_IN_MINUTES is deprectated; refactor to use utils/time or date-fns functions + const eventWidth = + (event.duration_minutes * SCHEDULE_HOUR_COLUMN_WIDTH_PX) / + ONE_HOUR_IN_MINUTES; + + const containerCssVars = useCss({ + "--event--margin-left": `${calcStartPosition( + event.start_utc_seconds, + scheduleStartHour + )}px`, + "--event--width": `${eventWidth}px`, + }); + + const containerClasses = classNames( + "ScheduleEvent", + { + "ScheduleEvent--live": isEventLive(event), + "ScheduleEvent--users": isPersonalizedEvent, + }, + containerCssVars + ); + + const bookmarkEvent: MouseEventHandler = useCallback(() => { + if (!userId || !event.id) return; + + event.isSaved + ? removeEventFromPersonalizedSchedule({ event, userId }) + : addEventToPersonalizedSchedule({ event, userId }); + }, [userId, event]); + + return ( +
+
+
{event.name}
+
by {event.host}
+
+ +
+ +
+
+ ); +}; diff --git a/src/components/molecules/ScheduleEvent/index.ts b/src/components/molecules/ScheduleEvent/index.ts new file mode 100644 index 0000000000..4b20b69aaa --- /dev/null +++ b/src/components/molecules/ScheduleEvent/index.ts @@ -0,0 +1 @@ +export { ScheduleEvent } from "./ScheduleEvent"; diff --git a/src/components/molecules/ScheduleRoomEvents/ScheduleRoomEvents.scss b/src/components/molecules/ScheduleRoomEvents/ScheduleRoomEvents.scss new file mode 100644 index 0000000000..6be7bfd3d4 --- /dev/null +++ b/src/components/molecules/ScheduleRoomEvents/ScheduleRoomEvents.scss @@ -0,0 +1,6 @@ +@import "../Schedule/Schedule.constants.scss"; + +.ScheduleRoomEvents { + height: $room--height; + margin-bottom: $room--margin-bottom; +} diff --git a/src/components/molecules/ScheduleRoomEvents/ScheduleRoomEvents.tsx b/src/components/molecules/ScheduleRoomEvents/ScheduleRoomEvents.tsx new file mode 100644 index 0000000000..48bc57258b --- /dev/null +++ b/src/components/molecules/ScheduleRoomEvents/ScheduleRoomEvents.tsx @@ -0,0 +1,34 @@ +import React, { useMemo } from "react"; + +import { PersonalizedVenueEvent } from "types/venues"; + +import { ScheduleEvent } from "components/molecules/ScheduleEvent"; + +import "./ScheduleRoomEvents.scss"; + +export interface ScheduleRoomEventsProps { + events: PersonalizedVenueEvent[]; + scheduleStartHour: number; + personalizedRoom?: boolean; +} + +export const ScheduleRoomEvents: React.FC = ({ + events, + scheduleStartHour, + personalizedRoom, +}) => { + const eventBlocks = useMemo( + () => + events.map((event) => ( + + )), + [events, personalizedRoom, scheduleStartHour] + ); + + return
{eventBlocks}
; +}; diff --git a/src/components/molecules/ScheduleRoomEvents/index.ts b/src/components/molecules/ScheduleRoomEvents/index.ts new file mode 100644 index 0000000000..d899c995e0 --- /dev/null +++ b/src/components/molecules/ScheduleRoomEvents/index.ts @@ -0,0 +1 @@ +export { ScheduleRoomEvents } from "./ScheduleRoomEvents"; diff --git a/src/components/molecules/ScheduleVenueDescription/ScheduleVenueDescription.scss b/src/components/molecules/ScheduleVenueDescription/ScheduleVenueDescription.scss new file mode 100644 index 0000000000..c0ed410df0 --- /dev/null +++ b/src/components/molecules/ScheduleVenueDescription/ScheduleVenueDescription.scss @@ -0,0 +1,52 @@ +@import "scss/constants.scss"; +@import "../Schedule/Schedule.constants.scss"; + +$image-size: 92px; + +$main--margin-top: 2rem; +$main--margin-bottom: 1rem; + +$name--margin-bottom: 5px; + +.ScheduleVenueDescription { + flex: 1; + padding-left: $Schedule--padding; + + &__main { + display: flex; + align-items: center; + margin-top: $main--margin-top; + margin-bottom: $main--margin-bottom; + } + + &__pic { + flex-shrink: 0; + width: $image-size; + height: $image-size; + background-size: cover; + border-radius: 50%; + margin-right: $Schedule--padding; + cursor: pointer; + transition: all 400ms $transition-function; + box-shadow: 0 8px 16px rgba($black, 0.3); + background-image: var(--venue-picture--background-image); + + &:hover { + transform: scale(1.1); + } + } + + &__desc { + font-size: 0.8rem; + opacity: 0.8; + } + + &__name { + font-style: italic; + margin-bottom: $name--margin-bottom; + } + + &__subtitle { + font-weight: 400; + } +} diff --git a/src/components/molecules/ScheduleVenueDescription/ScheduleVenueDescription.tsx b/src/components/molecules/ScheduleVenueDescription/ScheduleVenueDescription.tsx new file mode 100644 index 0000000000..77848d5d66 --- /dev/null +++ b/src/components/molecules/ScheduleVenueDescription/ScheduleVenueDescription.tsx @@ -0,0 +1,51 @@ +import React, { FC } from "react"; +import { useCss } from "react-use"; +import classNames from "classnames"; + +import { DEFAULT_VENUE_LOGO } from "settings"; + +import { useRelatedVenues } from "hooks/useRelatedVenues"; + +import "./ScheduleVenueDescription.scss"; + +export interface ScheduleVenueDescriptionProps { + venueId: string; +} + +export const ScheduleVenueDescription: FC = ({ + venueId, +}) => { + const { sovereignVenue } = useRelatedVenues({ + currentVenueId: venueId, + }); + + const venuePictureCssVars = useCss({ + "--venue-picture--background-image": `url(${ + sovereignVenue?.host?.icon ?? DEFAULT_VENUE_LOGO + })`, + }); + + const venuePictureClasses = classNames( + "ScheduleVenueDescription__pic", + venuePictureCssVars + ); + + return ( +
+
+
+
+

+ {sovereignVenue?.name ?? "Schedule"} +

+

+ {sovereignVenue?.config?.landingPageConfig?.subtitle} +

+
+
+
+

{sovereignVenue?.config?.landingPageConfig?.description}

+
+
+ ); +}; diff --git a/src/components/molecules/ScheduleVenueDescription/index.ts b/src/components/molecules/ScheduleVenueDescription/index.ts new file mode 100644 index 0000000000..8250ae4631 --- /dev/null +++ b/src/components/molecules/ScheduleVenueDescription/index.ts @@ -0,0 +1 @@ +export { ScheduleVenueDescription } from "./ScheduleVenueDescription"; diff --git a/src/components/molecules/UserReactions/UserReactions.tsx b/src/components/molecules/UserReactions/UserReactions.tsx index 4a832c2eda..fd89ca5c39 100644 --- a/src/components/molecules/UserReactions/UserReactions.tsx +++ b/src/components/molecules/UserReactions/UserReactions.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from "react"; import { useCss } from "react-use"; import classNames from "classnames"; +import { getSeconds } from "date-fns"; import { REACTION_TIMEOUT } from "settings"; @@ -11,16 +12,13 @@ import { } from "types/reactions"; import { uniqueEmojiReactionsDataMapReducer } from "utils/reactions"; -import { ONE_SECOND_IN_MILLISECONDS } from "utils/time"; import { useReactions } from "hooks/reactions"; import { useSelector } from "hooks/useSelector"; import "./UserReactions.scss"; -const REACTION_TIMEOUT_CSS = `${ - REACTION_TIMEOUT / ONE_SECOND_IN_MILLISECONDS -}s`; +const REACTION_TIMEOUT_CSS = `${getSeconds(REACTION_TIMEOUT)}s`; export interface UserReactionsProps { userId: string; diff --git a/src/components/molecules/VenueInfoEvents/VenueInfoEvents.scss b/src/components/molecules/VenueInfoEvents/VenueInfoEvents.scss index 7cbd4bad9d..4de88543fb 100644 --- a/src/components/molecules/VenueInfoEvents/VenueInfoEvents.scss +++ b/src/components/molecules/VenueInfoEvents/VenueInfoEvents.scss @@ -59,12 +59,8 @@ font-weight: 500; } .whatson-description-container-description { + @include line-clamp-with-overflow(2); font-size: 0.8rem; opacity: 0.8; margin-bottom: 4px; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; } diff --git a/src/components/organisms/AppRouter/AppRouter.jsx b/src/components/organisms/AppRouter/AppRouter.jsx index e39f95b6a9..89d7af575a 100644 --- a/src/components/organisms/AppRouter/AppRouter.jsx +++ b/src/components/organisms/AppRouter/AppRouter.jsx @@ -6,7 +6,7 @@ import { Redirect, } from "react-router-dom"; -// import SplashPage from "pages/Account/SplashPage"; +// import { SplashPage } from "pages/Account/SplashPage"; import Step1 from "pages/Account/Step1"; import Step2 from "pages/Account/Step2"; import Step3 from "pages/Account/Step3"; @@ -32,7 +32,6 @@ import { DEFAULT_REDIRECT_URL, SPARKLEVERSE_HOMEPAGE_URL } from "settings"; import VenuePage from "pages/VenuePage"; import { venueLandingUrl } from "utils/url"; import { RoomsForm } from "pages/Admin/Venue/Rooms/RoomsForm"; -import { SchedulePage } from "pages/Schedule/SchedulePage"; import { VenueAdminPage } from "pages/Admin/Venue/VenueAdminPage"; const AppRouter = () => { @@ -75,7 +74,6 @@ const AppRouter = () => { - ( diff --git a/src/components/organisms/DustStorm/DustStorm.tsx b/src/components/organisms/DustStorm/DustStorm.tsx index 9ee4f86cf6..f22fe8f550 100644 --- a/src/components/organisms/DustStorm/DustStorm.tsx +++ b/src/components/organisms/DustStorm/DustStorm.tsx @@ -58,6 +58,7 @@ export const DustStorm = () => { [] ); + // @debt FIVE_MINUTES_MS is deprecated; create needed constant in settings useInterval(() => { firebase .functions() diff --git a/src/components/organisms/InformationLeftColumn/InformationLeftColumn.tsx b/src/components/organisms/InformationLeftColumn/InformationLeftColumn.tsx index 2cf340fae3..3a4063d0d5 100644 --- a/src/components/organisms/InformationLeftColumn/InformationLeftColumn.tsx +++ b/src/components/organisms/InformationLeftColumn/InformationLeftColumn.tsx @@ -45,6 +45,7 @@ export const InformationLeftColumn = forwardRef< const [isExpanded, setExpanded] = useState(false); const toggleExpanded = useCallback((e?: React.MouseEvent) => { + // @debt we should try to avoid using event.stopPropagation() e && e.stopPropagation(); setExpanded((prev) => !prev); diff --git a/src/components/organisms/NavBarSchedule/NavBarSchedule.scss b/src/components/organisms/NavBarSchedule/NavBarSchedule.scss new file mode 100644 index 0000000000..faf4818d4f --- /dev/null +++ b/src/components/organisms/NavBarSchedule/NavBarSchedule.scss @@ -0,0 +1,61 @@ +@import "scss/constants.scss"; + +$NavBarSchedule--height: 620px; +$NavBarSchedule--padding: 20px; + +$weekday--padding: 8px 10px; +$weekday--margin-right: 10px; + +.NavBarSchedule { + @include scrollbar; + position: fixed; + z-index: z(navbar-schedule); + top: 0; + left: 0; + width: 100%; + height: $NavBarSchedule--height; + + padding-top: $navbar-height; + padding-left: $NavBarSchedule--padding; + + background-color: rgba($black, 0.9); + overflow: auto; + + pointer-events: none; + opacity: 0; + transform: translateY(-#{$NavBarSchedule--height}); + backdrop-filter: blur(5px); + box-shadow: 0 20px 50px 0 rgba(0, 0, 0, 0.33); + transition: all 400ms $transition-function; + + &--show { + pointer-events: all; + opacity: 1; + transform: translateY(0px); + } + + &__weekdays { + display: flex; + padding-left: $NavBarSchedule--padding; + } + + &__weekday { + display: inline; + cursor: pointer; + padding: $weekday--padding; + margin-right: $weekday--margin-right; + border-radius: $border-radius--xl; + opacity: 0.8; + + &--active { + opacity: 1; + background-color: $primary; + text-shadow: 0 2px 4px rgba($black, 0.3); + box-shadow: 0 3px 6px rgba($black, 0.2); + } + + &:hover { + opacity: 1; + } + } +} diff --git a/src/components/organisms/NavBarSchedule/NavBarSchedule.tsx b/src/components/organisms/NavBarSchedule/NavBarSchedule.tsx new file mode 100644 index 0000000000..a5467b7556 --- /dev/null +++ b/src/components/organisms/NavBarSchedule/NavBarSchedule.tsx @@ -0,0 +1,155 @@ +import React, { + useState, + useMemo, + FC, + MouseEventHandler, + useCallback, +} from "react"; +import { + addDays, + startOfToday, + format, + getUnixTime, + fromUnixTime, +} from "date-fns"; +import { groupBy, range } from "lodash"; +import classNames from "classnames"; + +import { SCHEDULE_SHOW_DAYS_AHEAD } from "settings"; + +import { useRelatedVenues } from "hooks/useRelatedVenues"; +import { useVenueId } from "hooks/useVenueId"; +import { useUser } from "hooks/useUser"; +import { useVenueEvents } from "hooks/events"; + +import { + PersonalizedVenueEvent, + VenueLocation, + LocatedEvents, + VenueEvent, +} from "types/venues"; + +import { Schedule } from "components/molecules/Schedule"; +import { ScheduleVenueDescription } from "components/molecules/ScheduleVenueDescription"; + +import { + buildLocationString, + extractLocation, + prepareForSchedule, +} from "./utils"; +import { isEventWithinDate } from "utils/event"; +import { WithVenueId } from "utils/id"; + +import "./NavBarSchedule.scss"; + +interface NavBarScheduleProps { + isVisible?: boolean; +} + +const emptyRelatedEvents: WithVenueId[] = []; + +export interface ScheduleDay { + isToday: boolean; + dayStartUtcSeconds: number; + locatedEvents: LocatedEvents[]; + personalEvents: PersonalizedVenueEvent[]; +} + +export const emptyPersonalizedSchedule = {}; + +export const NavBarSchedule: FC = ({ isVisible }) => { + const venueId = useVenueId(); + const { userWithId } = useUser(); + const userEventIds = + userWithId?.myPersonalizedSchedule ?? emptyPersonalizedSchedule; + + const { isLoading, relatedVenues, relatedVenueIds } = useRelatedVenues({ + currentVenueId: venueId, + }); + + const { + isEventsLoading, + events: relatedVenueEvents = emptyRelatedEvents, + } = useVenueEvents({ + venueIds: relatedVenueIds, + }); + + const isLoadingSchedule = isLoading || isEventsLoading; + + const [selectedDayIndex, setSelectedDayIndex] = useState(0); + + const weekdays = useMemo(() => { + const today = startOfToday(); + + return range(0, SCHEDULE_SHOW_DAYS_AHEAD).map((dayIndex) => { + const day = addDays(today, dayIndex); + const classes = classNames("NavBarSchedule__weekday", { + "NavBarSchedule__weekday--active": dayIndex === selectedDayIndex, + }); + + const onWeekdayClick: MouseEventHandler = () => { + setSelectedDayIndex(dayIndex); + }; + + return ( +
  • + {dayIndex === 0 ? "Today" : format(day, "E")} +
  • + ); + }); + }, [selectedDayIndex]); + + const getEventLocation = useCallback( + (locString: string): VenueLocation => { + const [venueId, roomTitle = ""] = extractLocation(locString); + const venueTitle = relatedVenues.find((venue) => venue.id === venueId) + ?.name; + return { venueId, roomTitle, venueTitle }; + }, + [relatedVenues] + ); + + const schedule: ScheduleDay = useMemo(() => { + const dayStart = addDays(startOfToday(), selectedDayIndex); + const daysEvents = relatedVenueEvents + .filter(isEventWithinDate(selectedDayIndex === 0 ? Date.now() : dayStart)) + .map(prepareForSchedule(dayStart, userEventIds)); + + const locatedEvents: LocatedEvents[] = Object.entries( + groupBy(daysEvents, buildLocationString) + ).map(([group, events]) => ({ + events, + location: getEventLocation(group), + })); + + return { + locatedEvents, + isToday: selectedDayIndex === 0, + dayStartUtcSeconds: getUnixTime(dayStart), + personalEvents: daysEvents.filter((event) => event.isSaved), + }; + }, [relatedVenueEvents, userEventIds, selectedDayIndex, getEventLocation]); + + const containerClasses = classNames("NavBarSchedule", { + "NavBarSchedule--show": isVisible, + }); + + return ( +
    + {venueId && } +
      {weekdays}
    + + +
    + ); +}; diff --git a/src/components/organisms/NavBarSchedule/utils.ts b/src/components/organisms/NavBarSchedule/utils.ts new file mode 100644 index 0000000000..0de6b9c7f6 --- /dev/null +++ b/src/components/organisms/NavBarSchedule/utils.ts @@ -0,0 +1,38 @@ +import { + differenceInMinutes, + endOfDay, + getUnixTime, + max, + min, + startOfDay, +} from "date-fns"; + +import { PersonalizedVenueEvent, VenueEvent } from "types/venues"; +import { MyPersonalizedSchedule } from "types/User"; + +import { WithVenueId } from "utils/id"; +import { eventEndTime, eventStartTime } from "utils/event"; +import { isTruthy } from "utils/types"; + +export const prepareForSchedule = ( + day: Date, + usersEvents: MyPersonalizedSchedule +) => (event: WithVenueId): PersonalizedVenueEvent => { + const startOfEventToShow = max([eventStartTime(event), startOfDay(day)]); + const endOfEventToShow = min([eventEndTime(event), endOfDay(day)]); + + return { + ...event, + start_utc_seconds: getUnixTime(startOfEventToShow), + duration_minutes: differenceInMinutes(endOfEventToShow, startOfEventToShow), + isSaved: isTruthy( + event.id && usersEvents[event.venueId]?.includes(event.id) + ), + }; +}; + +export const buildLocationString = (event: WithVenueId) => + `${event.venueId}#${event.room ?? ""}`; + +export const extractLocation = (locationStr: string) => + locationStr.split("#", 2); diff --git a/src/components/organisms/SchedulePageModal/SchedulePageModal.scss b/src/components/organisms/SchedulePageModal/SchedulePageModal.scss deleted file mode 100644 index b056cd6dd4..0000000000 --- a/src/components/organisms/SchedulePageModal/SchedulePageModal.scss +++ /dev/null @@ -1,72 +0,0 @@ -@import "scss/constants.scss"; - -$side-padding: 30px; - -$gradient: linear-gradient(-124deg, #e15ada 0%, #6f43ff 50%, #00f6d5 100%); - -$dark: #1a1d24; -$border-radius: 28px; -$large-shadow: box-shadow--large(0.34); -$login-max-width: 540px; -$modal-max-width: 540px; -$page-max-width: 1240px; - -$sand: #937c63; - -.modal-content { - width: 100%; - max-width: $modal-max-width; - margin: 0 auto; - padding: 20px 0 0 0; - background-color: $black; - box-shadow: $large-shadow; - text-align: left; - border-radius: $border-radius; - overflow: hidden; - - h3 { - margin-bottom: 1rem; - padding: 0 $side-padding; - } - .modal-tabs { - display: flex; - flex-wrap: nowrap; - width: 100%; - align-items: center; /* Align Items Vertically */ - justify-content: space-around; - margin-bottom: 2rem; - border-bottom: 1px solid rgba($white, 0.2); - overflow-x: auto; - overflow-y: hidden; - button { - background-color: transparent; - border: none; - display: block; - position: relative; - color: white; - text-align: center; - padding: 4px 10px; - opacity: 0.7; - font-weight: 400; - &:hover { - opacity: 1; - } - &.selected { - opacity: 1; - font-weight: bold; - color: yellow; - outline: none; - &:after { - content: ""; - position: absolute; - height: 2px; - border-radius: 4px; - width: 100%; - background-color: white; - left: 0; - bottom: -1px; - } - } - } - } -} diff --git a/src/components/organisms/SchedulePageModal/SchedulePageModal.tsx b/src/components/organisms/SchedulePageModal/SchedulePageModal.tsx deleted file mode 100644 index 3e0bd2dc34..0000000000 --- a/src/components/organisms/SchedulePageModal/SchedulePageModal.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useState, useMemo } from "react"; -import { startOfDay, addDays, isWithinInterval, endOfDay } from "date-fns"; -import { range } from "lodash"; - -import { AnyVenue, VenueEvent } from "types/venues"; - -import { formatDate, formatDateToWeekday } from "utils/time"; -import { WithId, WithVenueId } from "utils/id"; -import { itemsToObjectByIdReducer } from "utils/reducers"; -import { isEventLiveOrFuture } from "utils/event"; - -import { useVenueEvents } from "hooks/events"; -import { useRelatedVenues } from "hooks/useRelatedVenues"; - -import { EventDisplay } from "components/molecules/EventDisplay/EventDisplay"; - -type DatedEvents = Array<{ - dateDay: Date; - events: Array>; -}>; - -const DAYS_AHEAD = 7; - -interface SchedulePageModalProps { - venueId?: string; - isVisible?: boolean; -} - -export const SchedulePageModal: React.FC = ({ - venueId, - isVisible, -}) => { - const { - parentVenue, - currentVenue, - relatedVenues, - relatedVenueIds, - } = useRelatedVenues({ - currentVenueId: venueId, - }); - - const { events: relatedVenueEvents } = useVenueEvents({ - venueIds: relatedVenueIds, - }); - - const relatedVenuesById: Partial< - Record> - > = relatedVenues.reduce(itemsToObjectByIdReducer, {}); - - const orderedEvents: DatedEvents = useMemo(() => { - const liveAndFutureEvents = relatedVenueEvents.filter(isEventLiveOrFuture); - const hasEvents = liveAndFutureEvents.length > 0; - - const nowDay = startOfDay(new Date()); - - const dates: DatedEvents = range(0, DAYS_AHEAD).map((idx) => { - const day = addDays(nowDay, idx); - - const todaysEvents = liveAndFutureEvents - .filter((event) => { - return isWithinInterval(day, { - start: startOfDay(new Date(event.start_utc_seconds * 1000)), - end: endOfDay( - new Date( - (event.start_utc_seconds + event.duration_minutes * 60) * 1000 - ) - ), - }); - }) - .sort((a, b) => a.start_utc_seconds - b.start_utc_seconds); - - return { - dateDay: day, - events: hasEvents ? todaysEvents : [], - }; - }); - - return dates; - }, [relatedVenueEvents]); - - const [date, setDate] = useState(0); - - const scheduleTabs = useMemo( - () => - orderedEvents.map((day, idx) => ( -
  • { - e.stopPropagation(); - setDate(idx); - }} - > - {formatDateToWeekday(day.dateDay.getTime() / 1000)} -
  • - )), - [date, orderedEvents] - ); - - const events = useMemo( - () => - orderedEvents[date]?.events.map((event, index) => ( - - )), - [currentVenue, date, orderedEvents, relatedVenuesById] - ); - - const hasEvents = !!orderedEvents?.[date]?.events.length; - - // TODO: this was essentially used in the old logic, but the styles look - // as though they will hide it anyway, so I think it's better without this? - // if (!isVisible) return
    ; - - // TODO: ideally this would find the top most parent of parents and use those details - const hasParentVenue = !!parentVenue; - - const partyinfoImage = hasParentVenue - ? parentVenue?.host?.icon - : currentVenue?.host?.icon; - - const titleText = hasParentVenue ? parentVenue?.name : currentVenue?.name; - - const subtitleText = hasParentVenue - ? parentVenue?.config?.landingPageConfig.subtitle - : currentVenue?.config?.landingPageConfig.subtitle; - - const descriptionText = hasParentVenue - ? parentVenue?.config?.landingPageConfig.description - : currentVenue?.config?.landingPageConfig.description; - - return ( -
    -
    -
    -
    -
    -
    -

    {titleText}

    -

    {subtitleText}

    -
    -
    -
    -

    {descriptionText}

    -
    -
    - -
    -
      {scheduleTabs}
    -
    - {events} - {!hasEvents && ( -
    There are no events scheduled for this day.
    - )} -
    -
    -
    -
    - ); -}; diff --git a/src/components/templates/ArtPiece/ArtPiece.tsx b/src/components/templates/ArtPiece/ArtPiece.tsx index 42266992fb..c73fbcd1ae 100644 --- a/src/components/templates/ArtPiece/ArtPiece.tsx +++ b/src/components/templates/ArtPiece/ArtPiece.tsx @@ -1,5 +1,4 @@ -import React, { useState } from "react"; -import { Modal } from "react-bootstrap"; +import React from "react"; import { IFRAME_ALLOW } from "settings"; @@ -9,11 +8,9 @@ import { GenericVenue } from "types/venues"; import { ConvertToEmbeddableUrl } from "utils/ConvertToEmbeddableUrl"; import { WithId } from "utils/id"; -import { InformationLeftColumn } from "components/organisms/InformationLeftColumn"; import Room from "components/organisms/Room"; -import { SchedulePageModal } from "components/organisms/SchedulePageModal/SchedulePageModal"; import WithNavigationBar from "components/organisms/WithNavigationBar"; - +import { InformationLeftColumn } from "components/organisms/InformationLeftColumn"; import InformationCard from "components/molecules/InformationCard"; import SparkleFairiesPopUp from "components/molecules/SparkleFairiesPopUp/SparkleFairiesPopUp"; @@ -24,7 +21,7 @@ export interface ArtPieceProps { } export const ArtPiece: React.FC = ({ venue }) => { - const [showEventSchedule, setShowEventSchedule] = useState(false); + if (!venue) return <>Loading...; const iframeUrl = ConvertToEmbeddableUrl(venue.iframeUrl); @@ -73,15 +70,6 @@ export const ArtPiece: React.FC = ({ venue }) => {
    )} - setShowEventSchedule(false)} - dialogClassName="custom-dialog" - > - - - - ); }; diff --git a/src/components/templates/PartyMap/components/RoomModal/RoomModal.tsx b/src/components/templates/PartyMap/components/RoomModal/RoomModal.tsx index 123725f4c6..156ee335b0 100644 --- a/src/components/templates/PartyMap/components/RoomModal/RoomModal.tsx +++ b/src/components/templates/PartyMap/components/RoomModal/RoomModal.tsx @@ -1,12 +1,12 @@ import React, { useMemo } from "react"; import { Modal } from "react-bootstrap"; +import { isBefore } from "date-fns"; import { Room } from "types/rooms"; import { AnyVenue } from "types/venues"; -import { getCurrentEvent } from "utils/event"; +import { eventEndTime, getCurrentEvent } from "utils/event"; import { venueEventsSelector } from "utils/selectors"; -import { getCurrentTimeInUTCSeconds, ONE_MINUTE_IN_SECONDS } from "utils/time"; import { useCustomSound } from "hooks/sounds"; import { useSelector } from "hooks/useSelector"; @@ -61,10 +61,7 @@ export const RoomModalContent: React.FC = ({ return venueEvents.filter( (event) => - event.room === room.title && - event.start_utc_seconds + - event.duration_minutes * ONE_MINUTE_IN_SECONDS > - getCurrentTimeInUTCSeconds() + event.room === room.title && isBefore(Date.now(), eventEndTime(event)) ); }, [room, venueEvents]); diff --git a/src/components/templates/Playa/Playa.tsx b/src/components/templates/Playa/Playa.tsx index 8fbfd01dd8..789c73e271 100644 --- a/src/components/templates/Playa/Playa.tsx +++ b/src/components/templates/Playa/Playa.tsx @@ -134,7 +134,6 @@ const Playa = () => { const venue = useSelector(currentVenueSelectorData); const [showModal, setShowModal] = useState(false); - const [showEventSchedule, setShowEventSchedule] = useState(false); const [selectedVenue, setSelectedVenue] = useState>(); const [zoom, setZoom] = useState(minZoom()); const [centerX, setCenterX] = useState(GATE_X); @@ -311,6 +310,7 @@ const Playa = () => { ); }, 1); + // @debt we should try to avoid using event.stopPropagation() const zoomListener = (event: WheelEvent) => { event.preventDefault(); event.stopPropagation(); @@ -1051,17 +1051,6 @@ const Playa = () => { /> )} - setShowEventSchedule(false)} - dialogClassName="custom-dialog" - > - - {/* @debt Stubbing out legacy code as this component isn't used anymore and is getting deleted in a different PR. */} - Schedule Disabled - {/**/} - - ); }, [ @@ -1082,7 +1071,6 @@ const Playa = () => { isUserVenueOwner, dustStorm, changeDustStorm, - showEventSchedule, inVideoChat, videoChatHeight, mapContainer, diff --git a/src/pages/Account/SplashPage.tsx b/src/pages/Account/SplashPage.tsx index 86a2757f2a..ec3c7a3baa 100644 --- a/src/pages/Account/SplashPage.tsx +++ b/src/pages/Account/SplashPage.tsx @@ -1,37 +1,28 @@ -import React, { useCallback } from "react"; +import React from "react"; import { useHistory } from "react-router-dom"; -import "firebase/storage"; -import "./Account.scss"; + import { PLAYA_IMAGE, PLAYA_VENUE_NAME, PLAYA_VENUE_ID } from "settings"; + import { venueInsideUrl } from "utils/url"; + import { useUser } from "hooks/useUser"; -import { useQuery } from "hooks/useQuery"; -import { Modal } from "react-bootstrap"; -import { SchedulePageModal } from "components/organisms/SchedulePageModal/SchedulePageModal"; + +import "firebase/storage"; +import "./Account.scss"; export interface ProfileFormData { partyName: string; pictureUrl: string; } -const SplashPage = () => { +export const SplashPage = () => { const history = useHistory(); const { user } = useUser(); - const queryParams = useQuery(); - const showSchedule = !!queryParams.get("schedule"); const onSubmit = () => { history.push(user ? venueInsideUrl(PLAYA_VENUE_ID) : "/enter/step1"); }; - const onHideSchedule = useCallback(() => { - history.replace(history.location.pathname); - }, [history]); - - const onShowSchedulePress = useCallback(() => { - history.replace({ search: "schedule=true" }); - }, [history]); - return ( <>
    @@ -52,25 +43,8 @@ const SplashPage = () => { > Enter the burn -
    - - - - - ); }; - -export default SplashPage; diff --git a/src/pages/Admin/VenueEventDetails.tsx b/src/pages/Admin/VenueEventDetails.tsx index 3444846efd..336ef3010f 100644 --- a/src/pages/Admin/VenueEventDetails.tsx +++ b/src/pages/Admin/VenueEventDetails.tsx @@ -1,16 +1,13 @@ import React from "react"; -import { format } from "date-fns"; +import { format, getUnixTime } from "date-fns"; import { VenueEvent } from "types/venues"; import { WithId } from "utils/id"; -import { - formatHourAndMinute, - ONE_MINUTE_IN_SECONDS, - ONE_SECOND_IN_MILLISECONDS, -} from "utils/time"; +import { formatHourAndMinute } from "utils/time"; +import { eventEndTime, eventStartTime } from "utils/event"; -interface Props { +export interface VenueEventDetailsProps { venueEvent: WithId; setEditedEvent: Function | undefined; setShowCreateEventModal: Function; @@ -24,16 +21,10 @@ const VenueEventDetails = ({ setShowCreateEventModal, setShowDeleteEventModal, className, -}: Props) => { +}: VenueEventDetailsProps) => { const startTime = formatHourAndMinute(venueEvent.start_utc_seconds); - const endTime = formatHourAndMinute( - venueEvent.start_utc_seconds + - ONE_MINUTE_IN_SECONDS * venueEvent.duration_minutes - ); - const startDay = format( - venueEvent.start_utc_seconds * ONE_SECOND_IN_MILLISECONDS, - "EEEE LLLL do" - ); + const endTime = formatHourAndMinute(getUnixTime(eventEndTime(venueEvent))); + const startDay = format(eventStartTime(venueEvent), "EEEE LLLL do"); return (
    @@ -51,6 +42,7 @@ const VenueEventDetails = ({
    {venueEvent.description} + {venueEvent.descriptions?.map((description, index) => (

    {description}

    ))} diff --git a/src/pages/Schedule/SchedulePage.scss b/src/pages/Schedule/SchedulePage.scss deleted file mode 100644 index f7c2a78999..0000000000 --- a/src/pages/Schedule/SchedulePage.scss +++ /dev/null @@ -1,28 +0,0 @@ -.schedule-page { - padding: 20px; - overflow: hidden; - height: 100%; - max-height: 100%; -} - -.schedule-container { - height: 100%; - max-height: 100%; -} - -.scheduling-table { - &table { - border-collapse: collapse; - } - - &table, - th, - td { - border: 1px solid blue; - } - - th, - td { - padding: 5px; - } -} diff --git a/src/pages/Schedule/SchedulePage.tsx b/src/pages/Schedule/SchedulePage.tsx deleted file mode 100644 index cf22de3bbf..0000000000 --- a/src/pages/Schedule/SchedulePage.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import Bugsnag from "@bugsnag/js"; -import React, { useState, useMemo } from "react"; -import WithNavigationBar from "components/organisms/WithNavigationBar"; -import firebase from "firebase/app"; -import { OnlineStatsData } from "types/OnlineStatsData"; -import { range } from "lodash"; -import { startOfDay, addDays, isWithinInterval, endOfDay } from "date-fns"; -import "./SchedulePage.scss"; -import { Link } from "react-router-dom"; -import { DEFAULT_VENUE } from "settings"; -import { venueInsideUrl } from "utils/url"; -import { useInterval } from "hooks/useInterval"; -import { FIVE_MINUTES_MS } from "utils/time"; - -type OpenVenues = OnlineStatsData["openVenues"]; -type OpenVenue = OpenVenues[number]; - -type VenueEvent = { - venue: OpenVenue["venue"]; - event: OpenVenue["currentEvents"][number]; -}; - -type DatedEvents = Array<{ - dateDay: Date; - events: Array; -}>; - -const DAYS_AHEAD = 100; - -export const SchedulePage = () => { - const [openVenues, setOpenVenues] = useState(); - - useInterval(() => { - firebase - .functions() - .httpsCallable("stats-getOnlineStats")() - .then((result) => { - const { openVenues } = result.data as OnlineStatsData; - - setOpenVenues(openVenues); - }) - .catch(Bugsnag.notify); - }, FIVE_MINUTES_MS); - - const orderedEvents: DatedEvents = useMemo(() => { - if (!openVenues) return []; - - const nowDay = startOfDay(new Date()); - - const allEvents = openVenues.reduce>( - (acc, ov) => [ - ...acc, - ...ov.currentEvents.map((event) => ({ venue: ov.venue, event })), - ], - [] - ); - - const dates: DatedEvents = range(0, DAYS_AHEAD).map((idx) => { - const day = addDays(nowDay, idx); - - return { - dateDay: day, - events: allEvents - .filter((ve) => - // some events will span multiple days. Pick events for which `day` is between the event start and end - isWithinInterval(day, { - start: startOfDay(new Date(ve.event.start_utc_seconds * 1000)), - end: endOfDay( - new Date( - (ve.event.start_utc_seconds + - ve.event.duration_minutes * 60) * - 1000 - ) - ), - }) - ) - .sort( - (a, b) => a.event.start_utc_seconds - b.event.start_utc_seconds - ), - }; - }); - - return dates; - }, [openVenues]); - - return ( - -
    -

    Sparkleverse Schedule

    -
    - {orderedEvents.map((event) => ( -
    -

    {event.dateDay.toDateString()}

    - - - - - - - - - {event.events.map((dayEvent) => ( - - - - - - - - ))} -
    StartsEndsTitleDescriptionLocation
    - {new Date( - dayEvent.event.start_utc_seconds * 1000 - ).toString()} - - {new Date( - (dayEvent.event.start_utc_seconds + - dayEvent.event.duration_minutes * 60) * - 1000 - ).toString()} - {dayEvent.event.name}{dayEvent.event.description} - - {dayEvent.venue.name} - -
    -
    - ))} -
    -
    -
    - ); -}; diff --git a/src/pages/VenueLandingPage/VenueLandingPage.tsx b/src/pages/VenueLandingPage/VenueLandingPage.tsx index fac28852aa..6359c6f426 100644 --- a/src/pages/VenueLandingPage/VenueLandingPage.tsx +++ b/src/pages/VenueLandingPage/VenueLandingPage.tsx @@ -1,45 +1,51 @@ +import React, { useEffect, useState } from "react"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { VenueEvent } from "types/venues"; - -import CountDown from "components/molecules/CountDown"; -import EventPaymentButton from "components/molecules/EventPaymentButton"; -import InformationCard from "components/molecules/InformationCard"; -import SecretPasswordForm from "components/molecules/SecretPasswordForm"; -import AuthenticationModal from "components/organisms/AuthenticationModal"; -import PaymentModal from "components/organisms/PaymentModal"; -import WithNavigationBar from "components/organisms/WithNavigationBar"; import dayjs from "dayjs"; import advancedFormat from "dayjs/plugin/advancedFormat"; -import useConnectCurrentVenue from "hooks/useConnectCurrentVenue"; -import { useSelector } from "hooks/useSelector"; -import { useUser } from "hooks/useUser"; -import { updateTheme } from "pages/VenuePage/helpers"; -import React, { useEffect, useState } from "react"; -import { useFirestoreConnect } from "hooks/useFirestoreConnect"; -import { useVenueId } from "hooks/useVenueId"; +import { isAfter } from "date-fns"; +import { + DEFAULT_VENUE_BANNER, + DEFAULT_VENUE_LOGO, + IFRAME_ALLOW, +} from "settings"; + +import { VenueEvent } from "types/venues"; import { Firestore } from "types/Firestore"; +import { VenueAccessMode } from "types/VenueAcccess"; + import { hasUserBoughtTicketForEvent } from "utils/hasUserBoughtTicket"; import { WithId } from "utils/id"; import { isUserAMember } from "utils/isUserAMember"; -import { getTimeBeforeParty, ONE_MINUTE_IN_SECONDS } from "utils/time"; -import "./VenueLandingPage.scss"; +import { getTimeBeforeParty } from "utils/time"; import { venueEntranceUrl, venueInsideUrl } from "utils/url"; import { currentVenueSelectorData, userPurchaseHistorySelector, venueEventsSelector, } from "utils/selectors"; -import { - DEFAULT_VENUE_BANNER, - DEFAULT_VENUE_LOGO, - IFRAME_ALLOW, -} from "settings"; -import { AuthOptions } from "components/organisms/AuthenticationModal/AuthenticationModal"; +import { eventEndTime } from "utils/event"; import { showZendeskWidget } from "utils/zendesk"; -import { VenueAccessMode } from "types/VenueAcccess"; + +import useConnectCurrentVenue from "hooks/useConnectCurrentVenue"; +import { useSelector } from "hooks/useSelector"; +import { useUser } from "hooks/useUser"; +import { useFirestoreConnect } from "hooks/useFirestoreConnect"; +import { useVenueId } from "hooks/useVenueId"; + +import { updateTheme } from "pages/VenuePage/helpers"; + +import AuthenticationModal from "components/organisms/AuthenticationModal"; +import PaymentModal from "components/organisms/PaymentModal"; +import WithNavigationBar from "components/organisms/WithNavigationBar"; +import { AuthOptions } from "components/organisms/AuthenticationModal/AuthenticationModal"; +import CountDown from "components/molecules/CountDown"; +import EventPaymentButton from "components/molecules/EventPaymentButton"; +import InformationCard from "components/molecules/InformationCard"; +import SecretPasswordForm from "components/molecules/SecretPasswordForm"; + +import "./VenueLandingPage.scss"; export interface VenueLandingPageProps { venue: Firestore["data"]["currentVenue"]; @@ -95,9 +101,7 @@ export const VenueLandingPage: React.FunctionComponent = const { user } = useUser(); const futureOrOngoingVenueEvents = venueEvents?.filter( - (event) => - event.start_utc_seconds + event.duration_minutes * ONE_MINUTE_IN_SECONDS > - new Date().getTime() / 1000 && event.price > 0 + (event) => isAfter(eventEndTime(event), Date.now()) && event.price > 0 ); useEffect(() => { diff --git a/src/pages/VenuePage/VenuePage.tsx b/src/pages/VenuePage/VenuePage.tsx index 9ae571df37..e3a78679b2 100644 --- a/src/pages/VenuePage/VenuePage.tsx +++ b/src/pages/VenuePage/VenuePage.tsx @@ -1,6 +1,7 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { Redirect, useHistory } from "react-router-dom"; import { useTitle } from "react-use"; +import { isBefore } from "date-fns"; import { LOC_UPDATE_FREQ_MS } from "settings"; @@ -16,7 +17,6 @@ import { isUserPurchaseHistoryRequestedSelector, userPurchaseHistorySelector, } from "utils/selectors"; -import { canUserJoinTheEvent, ONE_MINUTE_IN_SECONDS } from "utils/time"; import { clearLocationData, setLocationData, @@ -27,6 +27,7 @@ import { venueEntranceUrl } from "utils/url"; import { showZendeskWidget } from "utils/zendesk"; import { isCompleteProfile, updateProfileEnteredVenueIds } from "utils/profile"; import { isTruthy } from "utils/types"; +import { eventEndTime, isEventStartingSoon } from "utils/event"; import { useConnectCurrentEvent } from "hooks/useConnectCurrentEvent"; import { useConnectUserPurchaseHistory } from "hooks/useConnectUserPurchaseHistory"; @@ -62,7 +63,6 @@ const VenuePage: React.FC = () => { const mixpanel = useMixpanel(); const history = useHistory(); - const [currentTimestamp] = useState(Date.now() / 1000); // const [isAccessDenied, setIsAccessDenied] = useState(false); const { user, profile } = useUser(); @@ -102,10 +102,7 @@ const VenuePage: React.FC = () => { const hasUserBoughtTicket = event && hasUserBoughtTicketForEvent(userPurchaseHistory, event.id); - const isEventFinished = - event && - currentTimestamp > - event.start_utc_seconds + event.duration_minutes * ONE_MINUTE_IN_SECONDS; + const isEventFinished = event && isBefore(eventEndTime(event), Date.now()); const isUserVenueOwner = userId && venue?.owners?.includes(userId); const isMember = @@ -223,7 +220,7 @@ const VenuePage: React.FC = () => { return <>Forbidden; } - if (!canUserJoinTheEvent(event)) { + if (isEventStartingSoon(event)) { return ( >; + export interface User { drinkOfChoice?: string; favouriteRecord?: string; @@ -32,4 +35,5 @@ export interface User { kidsMode: boolean; anonMode: boolean; enteredVenueIds?: string[]; + myPersonalizedSchedule?: MyPersonalizedSchedule; } diff --git a/src/types/venues.ts b/src/types/venues.ts index f3e031840b..2f0987f933 100644 --- a/src/types/venues.ts +++ b/src/types/venues.ts @@ -2,6 +2,8 @@ import { CSSProperties } from "react"; import { HAS_ROOMS_TEMPLATES } from "settings"; +import { WithVenueId } from "utils/id"; + import { EntranceStepConfig } from "./EntranceStep"; import { Poster } from "./posters"; import { Quotation } from "./Quotation"; @@ -312,6 +314,21 @@ export interface VenueEvent { id?: string; } +export interface VenueLocation { + venueId: string; + roomTitle: string; + venueTitle?: string; +} + +export interface LocatedEvents { + location: VenueLocation; + events: PersonalizedVenueEvent[]; +} + +export interface PersonalizedVenueEvent extends WithVenueId { + isSaved: boolean; +} + export const isVenueWithRooms = (venue: AnyVenue): venue is PartyMapVenue => HAS_ROOMS_TEMPLATES.includes(venue.template); diff --git a/src/utils/event.ts b/src/utils/event.ts index 282a09da12..25c367cff9 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -1,4 +1,15 @@ +import { + addMinutes, + areIntervalsOverlapping, + differenceInMinutes, + endOfDay, + fromUnixTime, + isWithinInterval, + startOfDay, +} from "date-fns"; + import { VenueEvent } from "types/venues"; + import { getCurrentTimeInUTCSeconds } from "./time"; export const getCurrentEvent = (roomEvents: VenueEvent[]) => { @@ -12,13 +23,7 @@ export const getCurrentEvent = (roomEvents: VenueEvent[]) => { }; export const isEventLive = (event: VenueEvent) => { - const currentTimeInUTCSeconds = getCurrentTimeInUTCSeconds(); - - return ( - event.start_utc_seconds < currentTimeInUTCSeconds && - event.start_utc_seconds + event.duration_minutes * 60 > - currentTimeInUTCSeconds - ); + return isWithinInterval(Date.now(), getEventInterval(event)); }; export const isEventLiveOrFuture = (event: VenueEvent) => { @@ -42,3 +47,30 @@ export const eventHappeningNow = ( event.start_utc_seconds + event.duration_minutes > currentTimeInUTCSeconds ); }; + +export const eventStartTime = (event: VenueEvent) => + fromUnixTime(event.start_utc_seconds); + +export const eventEndTime = (event: VenueEvent) => + addMinutes(eventStartTime(event), event.duration_minutes); + +export const isEventStartingSoon = (event: VenueEvent) => + differenceInMinutes(eventStartTime(event), Date.now()) <= 60; + +export const getEventInterval = (event: VenueEvent) => ({ + start: eventStartTime(event), + end: eventEndTime(event), +}); + +export const isEventWithinDate = (checkDate: number | Date) => ( + event: VenueEvent +) => { + const checkDateInterval = { + start: startOfDay(checkDate), + end: endOfDay(checkDate), + }; + + const eventInterval = getEventInterval(event); + + return areIntervalsOverlapping(checkDateInterval, eventInterval); +}; diff --git a/src/utils/formatMeasurement.ts b/src/utils/formatMeasurement.ts new file mode 100644 index 0000000000..52c3dd0dc7 --- /dev/null +++ b/src/utils/formatMeasurement.ts @@ -0,0 +1,5 @@ +export const formatMeasurement = (value: number, measureUnit: string) => { + const baseFormatted = `${value} ${measureUnit}`; + + return value === 1 ? baseFormatted : `${baseFormatted}s`; +}; diff --git a/src/utils/time.ts b/src/utils/time.ts index b27b87ced0..2d656514f4 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -1,17 +1,55 @@ -import { format, formatDuration, formatRelative } from "date-fns"; - -import { VenueEvent } from "types/venues"; +import { + differenceInSeconds, + format, + formatDuration, + formatRelative, + fromUnixTime, + getUnixTime, + intervalToDuration, + isBefore, + startOfDay, +} from "date-fns"; +/** + * @deprecated in favor of using date-fns functions + */ export const ONE_SECOND_IN_MILLISECONDS = 1000; + +/** + * @deprecated in favor of using date-fns functions + */ export const ONE_MINUTE_IN_SECONDS = 60; + +/** + * @deprecated in favor of using date-fns functions + */ +export const ONE_HOUR_IN_MINUTES = 60; + +/** + * @deprecated in favor of using date-fns functions + */ export const ONE_HOUR_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 60; + +/** + * @deprecated in favor of using date-fns functions + */ export const ONE_DAY_IN_SECONDS = ONE_HOUR_IN_SECONDS * 24; +/** + * @deprecated in favor of using date-fns functions + */ export const FIVE_MINUTES_MS = 5 * ONE_MINUTE_IN_SECONDS * ONE_SECOND_IN_MILLISECONDS; + +/** + * @deprecated in favor of using date-fns functions + */ export const ONE_HOUR_IN_MILLISECONDS = ONE_SECOND_IN_MILLISECONDS * ONE_HOUR_IN_SECONDS; +/** + * @deprecated in favor of using date-fns functions + */ export const SECONDS_TIMESTAMP_MAX_VALUE = 9999999999; /** @@ -53,57 +91,23 @@ export const secondsToDuration = (totalSeconds: number): Duration => { export const formatSecondsAsDuration = (seconds: number): string => formatDuration(secondsToDuration(seconds)); -const formatMeasurementInString = (value: number, measureUnit: string) => { - const baseFormatted = `${value} ${measureUnit}`; - - if (value === 0) return ""; - if (value === 1) return baseFormatted; - if (value > 1) return `${baseFormatted}s`; -}; - -// @debt quality test this export const getTimeBeforeParty = (startUtcSeconds?: number) => { if (startUtcSeconds === undefined) return "???"; - const secondsBeforeParty = - startUtcSeconds - Date.now() / ONE_SECOND_IN_MILLISECONDS; + const eventStartDate = fromUnixTime(startUtcSeconds); + const now = Date.now(); - if (secondsBeforeParty < 0) { - return 0; - } + if (isBefore(eventStartDate, now)) return 0; - const numberOfCompleteDaysBeforeParty = Math.floor( - secondsBeforeParty / ONE_DAY_IN_SECONDS + return formatDuration( + intervalToDuration({ + start: now, + end: eventStartDate, + }), + { format: ["days", "hours", "minutes"] } ); - - const numberOfCompleteHours = Math.floor( - (secondsBeforeParty % ONE_DAY_IN_SECONDS) / ONE_HOUR_IN_SECONDS - ); - - const numberOfMinutes = Math.floor( - (secondsBeforeParty % ONE_HOUR_IN_SECONDS) / ONE_MINUTE_IN_SECONDS - ); - - const numberOfDaysInString = formatMeasurementInString( - numberOfCompleteDaysBeforeParty, - "day" - ); - const numberOfHoursInString = formatMeasurementInString( - numberOfCompleteHours, - "hour" - ); - const numberOfMinutesInString = formatMeasurementInString( - numberOfMinutes, - "minute" - ); - - return `${numberOfDaysInString} ${numberOfHoursInString} ${numberOfMinutesInString}`; }; -export const canUserJoinTheEvent = (event: VenueEvent) => - event.start_utc_seconds - Date.now() / ONE_SECOND_IN_MILLISECONDS > - ONE_HOUR_IN_SECONDS; - /** * Format UTC seconds as a string representing date. * @@ -116,7 +120,7 @@ export const canUserJoinTheEvent = (event: VenueEvent) => * @see https://date-fns.org/docs/format */ export function formatDate(utcSeconds: number) { - return format(utcSeconds * ONE_SECOND_IN_MILLISECONDS, "MMM do"); + return format(fromUnixTime(utcSeconds), "MMM do"); } /** @@ -150,13 +154,11 @@ export function oneHourAfterTimestamp(timestamp: number) { * @see https://date-fns.org/docs/format */ export function formatUtcSeconds(utcSeconds?: number | null) { - return utcSeconds - ? format(utcSeconds * ONE_SECOND_IN_MILLISECONDS, "p") - : "(unknown)"; + return utcSeconds ? format(fromUnixTime(utcSeconds), "p") : "(unknown)"; } export function getHoursAgoInSeconds(hours: number) { - const nowInSec = Date.now() / ONE_SECOND_IN_MILLISECONDS; + const nowInSec = getUnixTime(Date.now()); return nowInSec - hours * ONE_HOUR_IN_SECONDS; } @@ -181,56 +183,18 @@ export function getDaysAgoInSeconds(days: number) { * @see https://date-fns.org/docs/format */ export const formatHourAndMinute = (utcSeconds: number) => { - return format(utcSeconds * ONE_SECOND_IN_MILLISECONDS, "HH:mm"); + return format(fromUnixTime(utcSeconds), "HH:mm"); }; -export const daysFromEndOfEvent = ( - utcSeconds: number, - durationMinutes: number -) => { - const dateNow = new Date(); - const dateOfFinish = new Date( - (utcSeconds + durationMinutes * 60) * ONE_SECOND_IN_MILLISECONDS - ); - const differenceInTime = dateOfFinish.getTime() - dateNow.getTime(); - const differenceInDays = - differenceInTime / (ONE_SECOND_IN_MILLISECONDS * 3600 * 24); - return Math.round(differenceInDays); -}; +export const getSecondsFromStartOfDay = (utcSeconds: number) => { + const time = fromUnixTime(utcSeconds); -export const daysFromStartOfEvent = (utcSeconds: number) => { - const dateNow = new Date(); - const dateOfStart = new Date(utcSeconds * ONE_SECOND_IN_MILLISECONDS); - const differenceInTime = dateNow.getTime() - dateOfStart.getTime(); - const differenceInDays = - differenceInTime / (ONE_SECOND_IN_MILLISECONDS * 3600 * 24); - return Math.round(differenceInDays); + return differenceInSeconds(time, startOfDay(time)); }; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now // The static Date.now() method returns the number of milliseconds elapsed since January 1, 1970 00:00:00 UTC. -export const getCurrentTimeInUTCSeconds = () => - Date.now() / ONE_SECOND_IN_MILLISECONDS; - -export const roundToNearestHour = (seconds: number) => { - const oneHour = 60 * 60; - return Math.floor(seconds / oneHour) * oneHour; -}; - -/** - * Format UTC seconds as a string representing weekday abbreviation. - * - * @example - * formatDateToWeekday(1618509600) - * // 'Thu' - * - * @param utcSeconds - * - * @see https://date-fns.org/docs/format - */ -export function formatDateToWeekday(utcSeconds: number) { - return format(utcSeconds * ONE_SECOND_IN_MILLISECONDS, "E"); -} +export const getCurrentTimeInUTCSeconds = () => getUnixTime(Date.now()); /** * Format UTC seconds as a string representing relative date from now. @@ -243,13 +207,13 @@ export function formatDateToWeekday(utcSeconds: number) { * * @see https://date-fns.org/docs/formatRelative */ -export const formatUtcSecondsRelativeToNow = (utcSeconds: number) => { - return formatRelative(utcSeconds * ONE_SECOND_IN_MILLISECONDS, Date.now()); -}; +export const formatUtcSecondsRelativeToNow = (utcSeconds: number) => + formatRelative(fromUnixTime(utcSeconds), Date.now()); export const normalizeTimestampToMilliseconds = (timestamp: number) => { const isTimestampInMilliSeconds = timestamp > SECONDS_TIMESTAMP_MAX_VALUE; + // @debt get rid of ONE_SECOND_IN_MILLISECONDS and use date-fns function return isTimestampInMilliSeconds ? timestamp : timestamp * ONE_SECOND_IN_MILLISECONDS;