diff --git a/functions/venue.js b/functions/venue.js index 392549b90e..7d1cb7b9f7 100644 --- a/functions/venue.js +++ b/functions/venue.js @@ -372,11 +372,6 @@ const createBaseUpdateVenueData = (data, doc) => { updated.mapBackgroundImageUrl = data.mapBackgroundImageUrl; } - // @debt do we need to be able to set this here anymore? I think we have a dedicated function for it? - if (data.bannerMessage) { - updated.bannerMessage = data.bannerMessage; - } - if (data.parentId) { updated.parentId = data.parentId; } @@ -904,7 +899,7 @@ exports.adminUpdateBannerMessage = functions.https.onCall( .firestore() .collection("venues") .doc(data.venueId) - .update({ bannerMessage: data.bannerMessage || null }); + .update({ banner: data.banner || null }); } ); diff --git a/src/api/admin.ts b/src/api/admin.ts index eb896ce79e..b6422bc83d 100644 --- a/src/api/admin.ts +++ b/src/api/admin.ts @@ -92,7 +92,6 @@ export type VenueInput = AdvancedVenueInput & columns?: number; width?: number; height?: number; - bannerMessage?: string; parentId?: string; owners?: string[]; chatTitle?: string; diff --git a/src/api/bannerAdmin.ts b/src/api/bannerAdmin.ts index 9df2c4166f..28ba80a055 100644 --- a/src/api/bannerAdmin.ts +++ b/src/api/bannerAdmin.ts @@ -1,13 +1,22 @@ import Bugsnag from "@bugsnag/js"; import firebase from "firebase/app"; -export const makeUpdateBanner = ( - venueId: string, - onError?: (errorMsg: string) => void -) => async (message?: string): Promise => { +import { Banner } from "types/banner"; + +export interface UpdateBannerProps { + venueId: string; + banner?: Banner; + onError?: (errorMsg: string) => void; +} + +export const updateBanner = async ({ + venueId, + banner, + onError = () => {}, +}: UpdateBannerProps): Promise => { const params = { venueId, - bannerMessage: message ?? "", + banner: banner ?? firebase.firestore.FieldValue.delete(), }; await firebase @@ -16,9 +25,9 @@ export const makeUpdateBanner = ( .catch((e) => { Bugsnag.notify(e, (event) => { event.addMetadata("context", { - location: "api/bannerAdmin::makeUpdateBanner", + location: "api/bannerAdmin::updateBanner", venueId, - message, + banner, }); }); onError?.(e.toString()); diff --git a/src/assets/icons/close-icon.svg b/src/assets/icons/close-icon.svg new file mode 100644 index 0000000000..f198c1adab --- /dev/null +++ b/src/assets/icons/close-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/atoms/ConfirmationModal/ConfirmationModal.scss b/src/components/atoms/ConfirmationModal/ConfirmationModal.scss index f87ae71431..bbd9ed2b6c 100644 --- a/src/components/atoms/ConfirmationModal/ConfirmationModal.scss +++ b/src/components/atoms/ConfirmationModal/ConfirmationModal.scss @@ -1,15 +1,17 @@ @import "scss/constants.scss"; .ConfirmationModal { - .modal-content { - background: $modal-background--confirm; - } - &__message { padding: 10px 0 30px 0; } &__buttons { display: flex; + width: 70%; + margin: 0 auto; + } + + .modal-content { + text-align: center; } } diff --git a/src/components/atoms/ConfirmationModal/ConfirmationModal.tsx b/src/components/atoms/ConfirmationModal/ConfirmationModal.tsx index 1db9161907..9eccf81112 100644 --- a/src/components/atoms/ConfirmationModal/ConfirmationModal.tsx +++ b/src/components/atoms/ConfirmationModal/ConfirmationModal.tsx @@ -4,6 +4,7 @@ import { Modal } from "react-bootstrap"; import { isTruthy } from "utils/types"; import { ButtonNG } from "components/atoms/ButtonNG"; +import { ButtonVariant } from "components/atoms/ButtonNG/ButtonNG"; import "./ConfirmationModal.scss"; @@ -11,22 +12,24 @@ interface ConfirmationModalProps { show?: boolean; header?: string; message: string; - no?: string; - yes?: string; - centered?: boolean; + confirmVariant?: ButtonVariant; + cancelBtnLabel?: string; + saveBtnLabel?: string; onConfirm: () => void; onCancel?: () => void; + isCentered?: boolean; } export const ConfirmationModal: React.FC = ({ show, header, message, - no = "No", - yes = "Yes", - centered = true, onConfirm, onCancel, + cancelBtnLabel = "No", + saveBtnLabel = "Yes", + isCentered = false, + confirmVariant = "primary", }) => { const [isVisible, setIsVisible] = useState(true); @@ -53,16 +56,16 @@ export const ConfirmationModal: React.FC = ({ className="ConfirmationModal" show={isShown} onHide={hide} - centered={centered} + centered={isCentered} >
- {hasHeader && {header}} + {hasHeader &&

{header}

}
{message}
- {no} - - {yes} + {cancelBtnLabel} + + {saveBtnLabel}
diff --git a/src/components/molecules/AnnouncementMessage/AnnouncementMessage.scss b/src/components/molecules/AnnouncementMessage/AnnouncementMessage.scss index 95055d4df4..6c1488b382 100644 --- a/src/components/molecules/AnnouncementMessage/AnnouncementMessage.scss +++ b/src/components/molecules/AnnouncementMessage/AnnouncementMessage.scss @@ -3,42 +3,122 @@ $spacing: 15px; $close-icon-width: 20px; $message-margin: $spacing + $close-icon-width + 5px; +$action-button-height: 30px; +$action-button-padding: 6px; +$announce-box-shadow: 0px 2px 20px 5px; -.announcement-container { +.AnnouncementMessage { display: block; - z-index: z(announcement); - position: absolute; - background: $primary; - top: 66px; - left: 50%; - width: 500px; - margin: 0 auto 0 -250px; + background: $announcement-background; + width: $announcement-container-width; + height: auto; + min-height: 70px; transition: left 0.1s linear; + padding: $spacing 0; + backdrop-filter: blur(50px); + text-align: center; + position: fixed; + border-radius: 0px 0px $border-radius--xl $border-radius--xl; + border: 1px solid opaque-white(0.4); + border-top: none; + box-sizing: border-box; + box-shadow: $announce-box-shadow opaque-white(0.35); + left: 0; + right: 0; + margin: 0 auto; + z-index: z(announcement-banner); - &.centered { - left: 50%; + &__admin { + position: absolute; } - padding: $spacing 0; - border-radius: 0 0 20px 20px; - text-align: center; + &--withButton { + min-height: $announcement-container-height; + } + + &__fullscreen { + position: fixed; + border: 1px solid opaque-white(0.4); + z-index: z(announcement-banner); + border-radius: $border-radius--xl; + top: 40%; + border-radius: 16px; - .close-button { - background: transparent; - border: none; - color: $white; + &--admin { + position: relative; + left: 0; + top: 0; + } + } + + &__default-text { + opacity: 0.6; + } + + &__content { + font-size: $font-size--md; + width: 90%; + margin-left: auto; + margin-right: auto; + + p { + margin-bottom: 0px; + } + } + + &__action-button { + display: inline-block; + background-color: $primary--live; + min-width: $announce-button-width; + min-height: $action-button-height; + font-size: $font-size--md; + font-weight: $font-weight--500; + padding: $action-button-padding $spacing--lg; + } + + &__close-button { cursor: pointer; position: absolute; top: $spacing; right: $spacing; + background: url("/assets/icons/close-icon.svg"); + height: 20px; + width: 20px; &:focus, &:hover { color: var(--primary-color); text-decoration: underline; } } -} -.announcement-message { - margin: 0 $message-margin; + &__container { + top: $navbar-height; + left: 0; + right: 0; + width: 100%; + z-index: z(announcement-banner); + display: flex; + justify-content: center; + align-items: flex-start; + position: absolute; + + &--centered { + bottom: 0; + position: fixed; + display: flex; + align-items: center; + background-color: opaque-black(0.6); + } + + &--admin { + position: relative; + height: 210px; + top: 0; + } + + &--canceled { + position: relative; + top: 0; + } + } } diff --git a/src/components/molecules/AnnouncementMessage/AnnouncementMessage.tsx b/src/components/molecules/AnnouncementMessage/AnnouncementMessage.tsx index b73238edfe..864d31a390 100644 --- a/src/components/molecules/AnnouncementMessage/AnnouncementMessage.tsx +++ b/src/components/molecules/AnnouncementMessage/AnnouncementMessage.tsx @@ -1,59 +1,105 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { faTimesCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { useEffect } from "react"; import classNames from "classnames"; -import { useChatSidebarControls } from "hooks/chats/chatSidebar"; +import { isDefined } from "utils/types"; + +import { useConnectCurrentVenueNG } from "hooks/useConnectCurrentVenueNG"; +import { useShowHide } from "hooks/useShowHide"; +import { useVenueId } from "hooks/useVenueId"; import { RenderMarkdown } from "components/organisms/RenderMarkdown"; +import { LinkButton } from "components/atoms/LinkButton"; + import "./AnnouncementMessage.scss"; -type AnnouncementMessageProps = { - message?: string; -}; +export interface AnnouncementMessageProps { + isAnnouncementUserView?: boolean; +} export const AnnouncementMessage: React.FC = ({ - message = "", + isAnnouncementUserView = false, }) => { - const [isVisible, setVisibility] = useState(false); - const { isExpanded } = useChatSidebarControls(); + const { + isShown: isAnnouncementMessageShown, + show: showAnnouncementMessage, + hide: hideAnnouncementMessage, + } = useShowHide(); + + const venueId = useVenueId(); + const { currentVenue: venue } = useConnectCurrentVenueNG(venueId); - const hideAnnouncement = useCallback(() => { - setVisibility(false); - }, []); + const { banner } = venue ?? {}; useEffect(() => { - if (message) { - setVisibility(true); + if (isDefined(banner?.content)) { + showAnnouncementMessage(); } - }, [message]); + }, [banner, showAnnouncementMessage]); + + const isWithButton = banner?.buttonDisplayText && banner?.isActionButton; + + const isAnnouncementCloseable = !banner?.isForceFunnel; + + const containerClasses = classNames("AnnouncementMessage__container", { + "AnnouncementMessage__container--centered": banner?.isFullScreen, + "AnnouncementMessage__container--admin": !isAnnouncementUserView, + "AnnouncementMessage__container--withButton": isWithButton, + }); + + const announcementMessageClasses = classNames("AnnouncementMessage", { + AnnouncementMessage__fullscreen: banner?.isFullScreen, + AnnouncementMessage__admin: !isAnnouncementUserView, + "AnnouncementMessage__fullscreen--admin": + banner?.isFullScreen && !isAnnouncementUserView, + }); - if (!isVisible || !message) return null; + const actionButtonClasses = classNames("AnnouncementMessage__action-button", { + "AnnouncementMessage__action-button-admin": !isAnnouncementUserView, + }); + + const handleBannerModalClose = () => { + if (banner?.isForceFunnel) return; + hideAnnouncementMessage(); + }; + + if (!isAnnouncementUserView && !banner?.content) + return ( +
+ + No announcement + +
+ ); + + if (!banner?.content || !isAnnouncementMessageShown) return null; return ( -
-
- + <> +
+
+
+ +
+ + {isWithButton && banner.buttonUrl && ( + + {banner.buttonDisplayText} + + )} + + {isAnnouncementCloseable && ( + + )} +
- -
+ ); }; - -/** - * @deprecated use named export instead - */ -export default AnnouncementMessage; diff --git a/src/components/molecules/IframeAdmin/IframeAdmin.tsx b/src/components/molecules/IframeAdmin/IframeAdmin.tsx index 0c265818b9..fde8a16630 100644 --- a/src/components/molecules/IframeAdmin/IframeAdmin.tsx +++ b/src/components/molecules/IframeAdmin/IframeAdmin.tsx @@ -11,8 +11,6 @@ interface IframeAdminProps { venue: AnyVenue; } -// @debt This component is almost exactly the same as BannerAdmin, we should refactor them both to use the same generic base component -// BannerAdmin is the 'canonical example' to follow when we do this export const IframeAdmin: React.FC = ({ venueId, venue }) => { const [iframeUrl, setIframeUrl] = useState(""); const [error, setError] = useState(); @@ -35,9 +33,7 @@ export const IframeAdmin: React.FC = ({ venueId, venue }) => {
- + = ({ }) => { const { user, userWithId } = useUser(); const venueId = useVenueId(); - const radioStations = useSelector(radioStationsSelector); const { currentVenue, parentVenue, sovereignVenueId } = useRelatedVenues({ diff --git a/src/components/organisms/AdminVenueView/components/RunTabToolbar/RunTabToolbar.tsx b/src/components/organisms/AdminVenueView/components/RunTabToolbar/RunTabToolbar.tsx index b2e632f731..29d33d60dd 100644 --- a/src/components/organisms/AdminVenueView/components/RunTabToolbar/RunTabToolbar.tsx +++ b/src/components/organisms/AdminVenueView/components/RunTabToolbar/RunTabToolbar.tsx @@ -4,7 +4,7 @@ import { useHistory } from "react-router"; import { useAsyncFn } from "react-use"; import { faPaperPlane } from "@fortawesome/free-solid-svg-icons"; -import { makeUpdateBanner } from "api/bannerAdmin"; +import { updateBanner } from "api/bannerAdmin"; import { venueInsideUrl } from "utils/url"; @@ -45,11 +45,11 @@ export const RunTabToolbar: React.FC = ({ const [ { loading: isUpdatingBanner, error }, - updateBanner, + updateBannerAsync, ] = useAsyncFn(async () => { if (!venueId) return; const bannerMessage = getValues().message; - await makeUpdateBanner(venueId)(bannerMessage); + await updateBanner({ venueId, banner: { content: bannerMessage } }); }, [getValues, venueId]); return ( @@ -68,7 +68,7 @@ export const RunTabToolbar: React.FC = ({ disabled={isUpdatingBanner} iconName={faPaperPlane} iconOnly={true} - onClick={updateBanner} + onClick={updateBannerAsync} /> {error && } diff --git a/src/components/organisms/AppRouter/AppRouter.tsx b/src/components/organisms/AppRouter/AppRouter.tsx index eb60cbbc74..70816a201f 100644 --- a/src/components/organisms/AppRouter/AppRouter.tsx +++ b/src/components/organisms/AppRouter/AppRouter.tsx @@ -145,7 +145,7 @@ export const AppRouter: React.FC = () => { - + diff --git a/src/components/organisms/BannerAdmin/BannerAdmin.scss b/src/components/organisms/BannerAdmin/BannerAdmin.scss new file mode 100644 index 0000000000..aed4d4a0c4 --- /dev/null +++ b/src/components/organisms/BannerAdmin/BannerAdmin.scss @@ -0,0 +1,51 @@ +@import "scss/constants.scss"; + +.BannerAdmin { + width: 100%; + + &__input-text { + text-align: start; + padding: $spacing--sm $spacing--lg; + font-size: $font-size--md; + + &--large { + height: $announcement-banner-admin-height; + } + } + + &__input-container { + margin-bottom: $spacing--lg; + } + + &__button-container { + display: flex; + justify-content: space-between; + margin-top: $spacing--xl; + } + + &__button { + width: $announce-button-width; + } + + &__action-container { + margin-left: 50px; + } + + &__checkbox { + justify-content: start; + margin-bottom: $spacing--lg; + + &--action { + margin-bottom: $spacing--xs; + } + } + + &__checkbox__label { + font-size: $font-size--lg; + } + + &__checkbox__label__disabled { + color: --white; + opacity: 20%; + } +} diff --git a/src/components/organisms/BannerAdmin/BannerAdmin.tsx b/src/components/organisms/BannerAdmin/BannerAdmin.tsx index 2c7c70fd47..a27bdaf278 100644 --- a/src/components/organisms/BannerAdmin/BannerAdmin.tsx +++ b/src/components/organisms/BannerAdmin/BannerAdmin.tsx @@ -1,89 +1,185 @@ -import React, { useCallback, useRef, useState } from "react"; +import React, { useCallback, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { useAsyncFn } from "react-use"; +import classNames from "classnames"; -import { makeUpdateBanner } from "api/bannerAdmin"; +import { updateBanner } from "api/bannerAdmin"; +import { Banner } from "types/banner"; import { AnyVenue } from "types/venues"; +import { useShowHide } from "hooks/useShowHide"; + +import { ButtonNG } from "components/atoms/ButtonNG"; +import { Checkbox } from "components/atoms/Checkbox"; +import { ConfirmationModal } from "components/atoms/ConfirmationModal/ConfirmationModal"; +import { InputField } from "components/atoms/InputField"; + +import "./BannerAdmin.scss"; + interface BannerAdminProps { venueId?: string; venue: AnyVenue; + onClose?: () => void; } -// @debt This component is almost exactly the same as IframeAdmin, we should refactor them both to use the same generic base component -// BannerAdmin is the 'canonical example' to follow when we do this -export const BannerAdmin: React.FC = ({ venueId, venue }) => { - const existingBannerMessage = venue?.bannerMessage ?? ""; - - const textareaFieldRef = useRef(null); - const [error, setError] = useState(); +export const BannerAdmin: React.FC = ({ + venueId, + venue, + onClose, +}) => { + const { + register, + handleSubmit, + errors, + reset, + watch, + setValue, + } = useForm({ + mode: "onChange", + reValidateMode: "onChange", + }); + const isUrlButtonActive = watch( + "isActionButton", + venue?.banner?.isActionButton + ); - const handleInputChange = useCallback(() => { - setError(null); - }, []); + const { + isShown: isShowBannerChangeModal, + show: showBannerChangeModal, + hide: hideBannerChangeModal, + } = useShowHide(); - const updateBannerInFirestore = useCallback( - (msg?: string) => { + const [{ loading: isUpdatingBanner }, saveBanner] = useAsyncFn( + async (banner?: Banner) => { if (!venueId) return; - makeUpdateBanner(venueId, (errorMsg) => setError(errorMsg))(msg); + await updateBanner({ venueId, banner }); + onClose && onClose(); }, - [venueId] + [venueId, onClose] ); - const saveBanner = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); + const clearBanner = useCallback(() => { + showBannerChangeModal(); - if (!textareaFieldRef.current) return; + reset(); + }, [showBannerChangeModal, reset]); - updateBannerInFirestore(textareaFieldRef.current.value); - }, - [updateBannerInFirestore] - ); + const confirmChangeBannerData = useCallback(() => { + saveBanner(); + hideBannerChangeModal(); + }, [saveBanner, hideBannerChangeModal]); + + useEffect(() => { + if (!isUrlButtonActive) { + setValue("isForceFunnel", false); + } + }, [isUrlButtonActive, setValue]); - const clearBanner = useCallback(() => updateBannerInFirestore(""), [ - updateBannerInFirestore, - ]); + const forceFunnelLabelClasses = classNames("BannerAdmin__checkbox__label", { + BannerAdmin__checkbox__label__disabled: !isUrlButtonActive, + }); return ( -
-
-
-
-
- - -