From 7c401c172e726938399542bb95f043407877e65a Mon Sep 17 00:00:00 2001 From: Basile Spaenlehauer Date: Wed, 11 Dec 2024 15:29:49 +0100 Subject: [PATCH] feat: add preview mode (#533) * feat: add preview component * fix: display in copyright text * fix: test --- cypress/e2e/player/island.cy.ts | 5 +- src/config/selectors.ts | 2 +- .../landing/footer/CopyrightText.stories.tsx | 14 ++++ src/modules/landing/footer/CopyrightText.tsx | 31 ++++++++ src/modules/landing/footer/Footer.tsx | 5 +- .../landing/preview/PreviewModeContext.tsx | 74 +++++++++++++++++++ .../navigationIsland/NavigationIsland.tsx | 4 +- src/routes/__root.tsx | 6 +- src/routes/_landing.tsx | 10 ++- src/routes/_landing/features.tsx | 10 ++- 10 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 src/modules/landing/footer/CopyrightText.stories.tsx create mode 100644 src/modules/landing/footer/CopyrightText.tsx create mode 100644 src/modules/landing/preview/PreviewModeContext.tsx diff --git a/cypress/e2e/player/island.cy.ts b/cypress/e2e/player/island.cy.ts index 13986323f..bafe21618 100644 --- a/cypress/e2e/player/island.cy.ts +++ b/cypress/e2e/player/island.cy.ts @@ -7,7 +7,7 @@ import { import { ITEM_CHATBOX_BUTTON_ID, ITEM_MAP_BUTTON_ID, - NAVIGATION_ISLAND_ID, + NAVIGATION_ISLAND_CLASSNAME, buildDocumentId, buildTreeItemClass, } from '../../../src/config/selectors'; @@ -118,6 +118,7 @@ describe('Island', () => { }); }); + // test is flaky it('Shows only one island when folder contains shortcut', () => { const items = getFolderWithShortcutFixture(); const parent = items[0]; @@ -128,7 +129,7 @@ describe('Island', () => { 'contain', getDocumentExtra(documentTarget.extra as DocumentItemExtra).content, ); - cy.get(`#${NAVIGATION_ISLAND_ID}`) + cy.get(`.${NAVIGATION_ISLAND_CLASSNAME}`) .should('be.visible') .and('have.length', 1); }); diff --git a/src/config/selectors.ts b/src/config/selectors.ts index cee146ad5..d34ddf0cb 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -179,7 +179,7 @@ export const FORBIDDEN_CONTENT_CONTAINER_ID = 'forbiddenContentContainer'; export const USER_SWITCH_SIGN_IN_BUTTON_ID = 'userSwitchSignInButton'; -export const NAVIGATION_ISLAND_ID = 'navigationIsland'; +export const NAVIGATION_ISLAND_CLASSNAME = 'navigationIsland'; export const ITEM_CHATBOX_BUTTON_ID = 'itemChatboxButton'; export const ITEM_MAP_BUTTON_ID = 'itemMapButton'; export const ITEM_PINNED_BUTTON_ID = 'itemPinnedButton'; diff --git a/src/modules/landing/footer/CopyrightText.stories.tsx b/src/modules/landing/footer/CopyrightText.stories.tsx new file mode 100644 index 000000000..896e60d01 --- /dev/null +++ b/src/modules/landing/footer/CopyrightText.stories.tsx @@ -0,0 +1,14 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { CopyrightText } from './CopyrightText'; + +const meta = { + component: CopyrightText, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default = { + args: {}, +} satisfies Story; diff --git a/src/modules/landing/footer/CopyrightText.tsx b/src/modules/landing/footer/CopyrightText.tsx new file mode 100644 index 000000000..5d2d98fca --- /dev/null +++ b/src/modules/landing/footer/CopyrightText.tsx @@ -0,0 +1,31 @@ +import { useState } from 'react'; + +import { Typography } from '@mui/material'; + +import { usePreviewMode } from '~landing/preview/PreviewModeContext'; + +export function CopyrightText() { + const [clickCounter, setClickCounter] = useState(0); + const { togglePreview, isEnabled: isPreviewEnabled } = usePreviewMode(); + + const handleClick = () => { + if (clickCounter === 2) { + togglePreview(); + // reset counter + setClickCounter(0); + } else { + setClickCounter((s) => s + 1); + } + }; + return ( + + © Graasp 2014 - {new Date().getFullYear()} + {isPreviewEnabled ? ' (preview)' : ''} + + ); +} diff --git a/src/modules/landing/footer/Footer.tsx b/src/modules/landing/footer/Footer.tsx index 0c3bfae23..832eb5b60 100644 --- a/src/modules/landing/footer/Footer.tsx +++ b/src/modules/landing/footer/Footer.tsx @@ -8,6 +8,7 @@ import LanguageSwitch from '@/components/ui/LanguageSwitch'; import { NS } from '@/config/constants'; import { OnChangeLangProp } from '@/types'; +import { CopyrightText } from './CopyrightText'; import { FooterSection } from './FooterSection'; import { FacebookIcon, @@ -191,9 +192,7 @@ export function Footer({ onChangeLang }: Readonly): JSX.Element { alignItems="center" justifyContent="center" > - - © Graasp 2014 - {new Date().getFullYear()} - + diff --git a/src/modules/landing/preview/PreviewModeContext.tsx b/src/modules/landing/preview/PreviewModeContext.tsx new file mode 100644 index 000000000..63d23b36f --- /dev/null +++ b/src/modules/landing/preview/PreviewModeContext.tsx @@ -0,0 +1,74 @@ +import { + ReactNode, + createContext, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; + +const PREVIEW_STORAGE_KEY = 'graasp-preview'; + +type PreviewContextType = { + togglePreview: () => void; + isEnabled: boolean; +}; +const PreviewContext = createContext({ + isEnabled: false, + togglePreview: () => console.error('no Preview context present'), +}); + +const isPreviewEnabled = () => { + const isPresent = localStorage.getItem(PREVIEW_STORAGE_KEY) != null; + return isPresent; +}; + +export function PreviewContextProvider({ + children, +}: Readonly<{ children: ReactNode }>) { + const [isEnabled, setIsEnabled] = useState(isPreviewEnabled()); + + useEffect(() => { + function listenForStorageChanges(event: StorageEvent) { + if (event.key === PREVIEW_STORAGE_KEY) { + // sync the local state + setIsEnabled(isPreviewEnabled()); + } + // discard the event otherwise + } + window.addEventListener('storage', listenForStorageChanges); + return () => window.removeEventListener('storage', listenForStorageChanges); + }, []); + + const value = useMemo( + () => ({ + togglePreview: () => { + if (isPreviewEnabled()) { + window.localStorage.removeItem(PREVIEW_STORAGE_KEY); + setIsEnabled(false); + } else { + window.localStorage.setItem(PREVIEW_STORAGE_KEY, 'enabled'); + setIsEnabled(true); + } + }, + isEnabled, + }), + [isEnabled], + ); + + return ( + {children} + ); +} + +export function usePreviewMode() { + return useContext(PreviewContext); +} + +export function Preview({ children }: { children: ReactNode }) { + const { isEnabled } = usePreviewMode(); + if (isEnabled) { + return children; + } + return null; +} diff --git a/src/modules/player/navigationIsland/NavigationIsland.tsx b/src/modules/player/navigationIsland/NavigationIsland.tsx index 99aab55d6..cbe32d2f1 100644 --- a/src/modules/player/navigationIsland/NavigationIsland.tsx +++ b/src/modules/player/navigationIsland/NavigationIsland.tsx @@ -1,6 +1,6 @@ import { Box, Stack } from '@mui/material'; -import { NAVIGATION_ISLAND_ID } from '@/config/selectors'; +import { NAVIGATION_ISLAND_CLASSNAME } from '@/config/selectors'; import useChatButton from './ChatButton'; import useGeolocationButton from './GeolocationButton'; @@ -26,7 +26,7 @@ const NavigationIslandBox = (): JSX.Element | null => { return ( ()({ component: RootComponent, notFoundComponent: NotFoundComponent, @@ -32,7 +34,9 @@ function RootComponent() { return ( - + + + {import.meta.env.MODE !== 'test' && } diff --git a/src/routes/_landing.tsx b/src/routes/_landing.tsx index c154b4681..46730a76d 100644 --- a/src/routes/_landing.tsx +++ b/src/routes/_landing.tsx @@ -19,6 +19,7 @@ import { OnChangeLangProp } from '@/types'; import { Footer } from '~landing/footer/Footer'; import { RightHeader } from '~landing/header/RightHeader'; +import { usePreviewMode } from '~landing/preview/PreviewModeContext'; export const Route = createFileRoute('/_landing')({ component: RouteComponent, @@ -30,7 +31,7 @@ function RouteComponent() { const { isMobile } = useMobileView(); const { fill: primary } = useButtonColor('primary'); const { mutate } = mutations.useEditCurrentMember(); - + const { isEnabled: isPreviewEnabled } = usePreviewMode(); const onChangeLang: OnChangeLangProp = (lang: string) => { if (isAuthenticated) { mutate({ extra: { lang } }); @@ -76,7 +77,12 @@ function RouteComponent() { {!isMobile && ( - Graasp + Graasp{' '} + {isPreviewEnabled ? ( + preview + ) : ( + '' + )} )} diff --git a/src/routes/_landing/features.tsx b/src/routes/_landing/features.tsx index 14ad15e9e..e4ea50bee 100644 --- a/src/routes/_landing/features.tsx +++ b/src/routes/_landing/features.tsx @@ -4,7 +4,9 @@ import { BlendedLearningSection } from '~landing/features/BlendedLearningSection import { GraaspFeaturesSection } from '~landing/features/GraaspFeaturesSection'; import { PlatformOverviewSection } from '~landing/features/PlatformOverviewSection'; import { TitleSection } from '~landing/features/TitleSection'; +import { PricingPlansSection } from '~landing/features/pricing/PricingPlansSection'; import { NewsLetter } from '~landing/home/NewsLetter'; +import { Preview } from '~landing/preview/PreviewModeContext'; export const Route = createFileRoute('/_landing/features')({ component: RouteComponent, @@ -17,8 +19,12 @@ function RouteComponent() { - {/* */} - {/* */} + + + + {/* + + */} );