diff --git a/scripts/bundleSize/bundleSizeConfig.js b/scripts/bundleSize/bundleSizeConfig.js index e0d0e1f505c..4c8356b484e 100644 --- a/scripts/bundleSize/bundleSizeConfig.js +++ b/scripts/bundleSize/bundleSizeConfig.js @@ -10,4 +10,4 @@ export const VARIANCE = 5; export const MIN_SIZE = 916; -export const MAX_SIZE = 1267; +export const MAX_SIZE = 1286; diff --git a/src/app/components/Heading/index.tsx b/src/app/components/Heading/index.tsx index 2cab92d37cd..63a43d5be35 100644 --- a/src/app/components/Heading/index.tsx +++ b/src/app/components/Heading/index.tsx @@ -1,6 +1,6 @@ /** @jsx jsx */ -import React, { FC, HTMLAttributes, ForwardedRef, forwardRef } from 'react'; +import React, { HTMLAttributes, ForwardedRef, forwardRef } from 'react'; import { jsx } from '@emotion/react'; import { GelFontSize, FontVariant } from '../../models/types/theming'; @@ -30,7 +30,7 @@ const sizes: Sizes = { h4: 'greatPrimer', }; -const Heading: FC = forwardRef( +const Heading = forwardRef( ( { children, diff --git a/src/app/components/Riddle/Components/Card/index.styles.ts b/src/app/components/Riddle/Components/Card/index.styles.ts new file mode 100644 index 00000000000..6ba16f29e46 --- /dev/null +++ b/src/app/components/Riddle/Components/Card/index.styles.ts @@ -0,0 +1,123 @@ +import pixelsToRem from '#app/utilities/pixelsToRem'; +import { css, Theme } from '@emotion/react'; + +// border: `${pixelsToRem(7)}rem solid transparent`, +// borderImage: `url(https://www.dropbox.com/scl/fi/5p4y98asfvgrxksxlgwqs/back23.png?rlkey=cu9mmwhnxq5vph4lvcacy2v15&st=z3pwywc4&raw=1) 33% round`, +// borderRadius: `${pixelsToRem(7)}rem`, + +export default { + container: ({ mq }: Theme) => + css({ + display: 'flex', + flexWrap: 'wrap', + [mq.GROUP_2_MAX_WIDTH]: { + flexDirection: 'column-reverse', + }, + }), + hidden: () => + css({ + display: 'none', + }), + playArea: ({ spacings, palette }: Theme) => + css({ + flex: 1, + color: palette.GHOST, + background: palette.POSTBOX, + padding: `${spacings.FULL}rem`, + }), + fixedHeight: () => + css({ + minHeight: '13.5rem', + }), + question: ({ palette }: Theme) => + css({ + background: palette.GHOST, + }), + heading: ({ palette, spacings }: Theme) => + css({ + color: palette.WHITE, + textAlign: 'end', + marginBottom: `${spacings.FULL}rem`, + }), + answerHeading: ({ palette, spacings }: Theme) => + css({ + margin: `${spacings.TRIPLE}rem 0`, + color: palette.WHITE, + }), + didYouKnow: ({ palette }: Theme) => + css({ + color: palette.WHITE, + }), + inputContainer: ({ spacings }: Theme) => + css({ + display: 'flex', + alignTracks: 'flex-end', + marginBottom: `${spacings.FULL}rem`, + }), + inputUnderline: () => + css({ + display: 'flex', + alignTracks: 'flex-end', + position: 'relative', + }), + underline: ({ palette }: Theme) => + css({ + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: '0.1rem', + backgroundColor: palette.GHOST, + }), + input: ({ fontSizes, fontVariants, palette }: Theme) => + css({ + ...fontSizes.greatPrimer, + ...fontVariants.serifLight, + color: palette.GHOST, + outline: 0, + border: 0, + background: palette.POSTBOX, + '&::placeholder': { + ...fontVariants.serifLight, + fontStyle: 'oblique', + color: palette.GHOST, + }, + '&:focus + div': { + height: '0.20rem', + backgroundColor: palette.GHOST, + }, + }), + submitButton: ({ palette, spacings }: Theme) => + css({ + cursor: 'pointer', + padding: `${spacings.FULL}rem`, + marginInlineStart: `${spacings.FULL}rem`, + border: `${0.15}rem solid ${palette.GHOST}`, + background: palette.POSTBOX, + '& span': { + color: palette.GHOST, + }, + '&:hover': { + background: palette.GHOST, + '& span': { + color: palette.BLACK, + }, + }, + }), + hintsArea: ({ spacings }: Theme) => + css({ + margin: `${spacings.TRIPLE}rem 0 ${2.5}rem`, + }), + detailsArea: ({ spacings, mq, palette }: Theme) => + css({ + padding: `${spacings.FULL}rem`, + minWidth: `${pixelsToRem(200)}rem`, + backgroundColor: `${palette.GREY_2}`, + display: 'grid', + gridTemplateColumns: '1fr', + [mq.GROUP_2_MAX_WIDTH]: { + minWidth: '100%', + gridTemplateColumns: '1fr 1fr 1fr', + }, + }), +}; diff --git a/src/app/components/Riddle/Components/Card/index.tsx b/src/app/components/Riddle/Components/Card/index.tsx new file mode 100644 index 00000000000..5e49e2cd7d4 --- /dev/null +++ b/src/app/components/Riddle/Components/Card/index.tsx @@ -0,0 +1,185 @@ +/** @jsxFrag React.Fragment */ +/** @jsx jsx */ +import { jsx } from '@emotion/react'; +import { use, useLayoutEffect, useRef, useState } from 'react'; +import Heading from '#app/components/Heading'; +import Text from '../../../Text'; +import style from './index.styles'; +import Hint, { HintData } from '../HintButton'; +import Detail from '../Detail'; +import { GameState, RiddleContext } from '../../RiddleProvider'; +import { LocalStorageContext } from '../../LocalStorageProvider'; + +export type GameData = { + expire: string; + question: string; + hint1: HintData; + hint2: HintData; + answer: string; + funFact: string; +}; + +const getTimeDiff = (a: Date, b: Date) => { + const timeDelta = a.getTime() - b.getTime(); + const secondsDelta = timeDelta / 1000; + const minutesDelta = Math.floor(timeDelta / (1000 * 60)); + const hoursToGo = Math.floor(timeDelta / (1000 * 60 * 60)); + const minutesToGo = minutesDelta - hoursToGo * 60; + const secondsToGo = Math.floor(secondsDelta - minutesDelta * 60); + + return [hoursToGo, minutesToGo, secondsToGo]; +}; + +const capitalise = (str: string) => { + if (!str) return ''; + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); +}; + +export default () => { + const { + gameData, + devTime, + gameIndex, + submitAttempt, + gameState, + revealAnswer, + } = use(RiddleContext); + const { goes, coins } = use(LocalStorageContext); + const inputRef = useRef(null); + const { question, hint1, hint2, answer, expire, funFact } = gameData; + const failedMessageRef = useRef(null); + const winnerMessageRef = useRef(null); + const [isInvoked, setIsInvoked] = useState(false); + const expiryDate = new Date(expire); + const currTime = devTime; + const [hour, minute, second] = getTimeDiff(expiryDate, currTime); + + let timeString = `-`; + if (hour > -1) { + timeString = `${hour < 10 ? '0' : ''}${hour}h ${minute < 10 ? '0' : ''}${minute}m ${second < 10 ? '0' : ''}${second}s`; + } + + const goesString = `${goes}/5`; + const coinsString = `🪙 ${coins}`; + + useLayoutEffect(() => { + if (isInvoked) { + if (gameState === GameState.WINNER) { + winnerMessageRef.current?.focus(); + } + if (gameState === GameState.FAILED) { + failedMessageRef.current?.focus(); + } + setIsInvoked(false); + } + }, [isInvoked, gameState]); + + return ( +
+
+ + Riddle of the day + + + {question} + + {gameState === GameState.PLAY && ( +
+
+ + + { + revealAnswer(2500); + setIsInvoked(true); + }} + /> +
+
+
+ +
+
+ + +
+ )} + {gameState === GameState.WINNER && ( +
+ + {capitalise(answer)} + + + {funFact} + +
+ )} + {gameState === GameState.FAILED && ( +
+ + {`You've run out of attempts!`} + + { + revealAnswer(2500); + setIsInvoked(true); + }} + /> +
+ )} +
+
+ + + +
+
+ ); +}; diff --git a/src/app/components/Riddle/Components/Card/usefulStuff.txt b/src/app/components/Riddle/Components/Card/usefulStuff.txt new file mode 100644 index 00000000000..9a470816a5d --- /dev/null +++ b/src/app/components/Riddle/Components/Card/usefulStuff.txt @@ -0,0 +1,28 @@ + + // const [initialTime, expiryTime] = useMemo(() => { + // const currDate = devTime; + // const expiryDate = new Date(expire); + // return [currDate, expiryDate]; + // }, [devTime, expire]); + + // const [initialHourDelta, initialMinuteDelta, initialSecondDelta] = + // getTimeDiff(expiryTime, initialTime); + + // const [hour, setHour] = useState(initialHourDelta); + // const [minute, setMinute] = useState(initialMinuteDelta); + // const [second, setSecond] = useState(initialSecondDelta); + + // useEffect(() => { + // const timer = setInterval(() => { + // const currTime = devTime; + // const [hoursToGo, minutesToGo, secondsToGo] = getTimeDiff( + // expiryTime, + // currTime, + // ); + // setHour(hoursToGo); + // setMinute(minutesToGo); + // setSecond(secondsToGo); + // }, 500); + + // return () => clearInterval(timer); + // }, [initialTime, expiryTime, devTime]); diff --git a/src/app/components/Riddle/Components/Detail/index.styles.ts b/src/app/components/Riddle/Components/Detail/index.styles.ts new file mode 100644 index 00000000000..dfb032fb551 --- /dev/null +++ b/src/app/components/Riddle/Components/Detail/index.styles.ts @@ -0,0 +1,34 @@ +import { css, Theme } from '@emotion/react'; + +export default { + detailContainer: ({ mq }: Theme) => + css({ + alignContent: 'center', + textAlign: 'center', + [mq.GROUP_2_MAX_WIDTH]: { + alignContent: 'start', + }, + }), + detailLabel: ({ palette, spacings, fontSizes, fontVariants, mq }: Theme) => + css({ + display: 'inline-block', + background: palette.BLACK, + color: palette.WHITE, + marginBottom: `${spacings.FULL}rem`, + ...fontVariants.sansBold, + ...fontSizes.greatPrimer, + [mq.GROUP_2_MAX_WIDTH]: { + ...fontSizes.minion, + }, + }), + detailContent: ({ spacings, fontSizes, fontVariants, mq }: Theme) => + css({ + display: 'inline-block', + marginBottom: `${spacings.FULL}rem`, + ...fontVariants.serifLight, + ...fontSizes.doublePica, + [mq.GROUP_2_MAX_WIDTH]: { + ...fontSizes.brevier, + }, + }), +}; diff --git a/src/app/components/Riddle/Components/Detail/index.tsx b/src/app/components/Riddle/Components/Detail/index.tsx new file mode 100644 index 00000000000..49b9aa6700c --- /dev/null +++ b/src/app/components/Riddle/Components/Detail/index.tsx @@ -0,0 +1,25 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/react'; +import Text from '../../../Text'; +import style from './index.styles'; + +export default ({ + as, + label, + content, +}: { + as?: string; + label: string; + content: string; +}) => { + return ( +
+ {label} +
+ + {content} + +
+
+ ); +}; diff --git a/src/app/components/Riddle/Components/DevControlPanel/index.styles.ts b/src/app/components/Riddle/Components/DevControlPanel/index.styles.ts new file mode 100644 index 00000000000..ccdf4d7d501 --- /dev/null +++ b/src/app/components/Riddle/Components/DevControlPanel/index.styles.ts @@ -0,0 +1,43 @@ +import { css, Theme } from '@emotion/react'; + +export default { + title: ({ spacings }: Theme) => + css({ + display: 'block', + marginTop: `${spacings.DOUBLE}rem`, + }), + date: () => + css({ + fontFamily: '"Lucida Console", "Courier New", monospace', + display: 'block', + }), + container: () => + css({ + display: 'flex', + flexWrap: 'wrap', + }), + optionContainer: ({ spacings, palette }: Theme) => + css({ + fontFamily: '"Lucida Console", "Courier New", monospace', + width: '11rem', + display: 'flex', + flexWrap: 'wrap', + alignContent: 'start', + justifyContent: 'center', + '& > span': { + fontFamily: '"Lucida Console", "Courier New", monospace', + textAlign: 'center', + width: '100%', + padding: `${spacings.HALF}rem`, + background: `${palette.BLACK}`, + color: `${palette.WHITE}`, + }, + '& button': { + height: '3rem', + width: '100%', + background: `${palette.BLACK}`, + color: `${palette.WHITE}`, + cursor: 'pointer', + }, + }), +}; diff --git a/src/app/components/Riddle/Components/DevControlPanel/index.tsx b/src/app/components/Riddle/Components/DevControlPanel/index.tsx new file mode 100644 index 00000000000..9e5c17df522 --- /dev/null +++ b/src/app/components/Riddle/Components/DevControlPanel/index.tsx @@ -0,0 +1,65 @@ +/** @jsx jsx */ +/* @jsxFrag React.Fragment */ +import React, { PropsWithChildren, use } from 'react'; +import { jsx } from '@emotion/react'; +import Text from '../../../Text'; +import style from './index.styles'; +import { LocalStorageContext } from '../../LocalStorageProvider'; +import { RiddleContext } from '../../RiddleProvider'; + +const Option = ({ title, children }: PropsWithChildren<{ title: string }>) => { + return ( +
+ {title} + {children} +
+ ); +}; + +export default () => { + const { addCoins } = use(LocalStorageContext); + const { + devTime, + forceTimeInc24: forceTimeInc, + forceTimeDec24: forceTimeDec, + gameState, + devOptionResetGoes, + } = use(RiddleContext); + + return ( + <> + + Developer Tools + + + Sim Date: {devTime.toDateString()} + + + Game State: {gameState} + +
+ + +
+ + ); +}; diff --git a/src/app/components/Riddle/Components/Hint/index.styles.ts b/src/app/components/Riddle/Components/Hint/index.styles.ts new file mode 100644 index 00000000000..114fb4d5fc0 --- /dev/null +++ b/src/app/components/Riddle/Components/Hint/index.styles.ts @@ -0,0 +1,73 @@ +import { css, Theme } from '@emotion/react'; +import { focusIndicatorThickness } from '../../../ThemeProvider/focusIndicator'; + +export default { + hintContainer: ({ spacings }: Theme) => + css({ + position: 'relative', + cursor: 'pointer', + margin: `${spacings.HALF}rem 0`, + '&[disabled]': { + pointerEvents: 'none', + userSelect: 'none', + }, + }), + hintSummary: ({ palette }: Theme) => + css({ + listStyle: 'none', + display: 'flex', + '&:focus-visible': { + outline: `${focusIndicatorThickness} solid ${palette.BLACK}`, + boxShadow: `0 0 0 ${focusIndicatorThickness} ${palette.WHITE}`, + outlineOffset: `${focusIndicatorThickness}`, + position: 'relative', + zIndex: '1', + }, + }), + hintSummaryText: ({ palette, spacings }: Theme) => + css({ + flexGrow: 1, + padding: `0 ${spacings.FULL}rem`, + background: palette.GHOST, + 'details:open &': { + display: 'none', + }, + }), + hintPrice: ({ palette }: Theme) => + css({ + width: `${4}rem`, + padding: `${0.6}rem ${1}rem`, + background: palette.GREY_3, + textAlign: 'center', + }), + paidIcon: () => + css({ + position: 'absolute', + top: 0, + insetInlineStart: 0, + width: `${2.5}rem`, + background: 'cyan', + textAlign: 'center', + display: 'none', + 'details:open &': { + display: 'block', + }, + }), + notEnough: () => + css({ + position: 'absolute', + top: 0, + insetInlineStart: 0, + background: 'yellow', + textAlign: 'center', + }), + hintAnswerText: ({ spacings }: Theme) => + css({ + position: 'absolute', + top: 0, + insetInlineEnd: 0, + insetInlineStart: `${6}rem`, + padding: `${0.6}rem ${spacings.FULL}rem`, + background: '#FEFAE0', + }), +}; diff --git a/src/app/components/Riddle/Components/Hint/index.tsx b/src/app/components/Riddle/Components/Hint/index.tsx new file mode 100644 index 00000000000..291d78010e4 --- /dev/null +++ b/src/app/components/Riddle/Components/Hint/index.tsx @@ -0,0 +1,70 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/react'; +import { use, useState } from 'react'; +import Text from '../../../Text'; +import style from './index.styles'; +import { LocalStorageContext } from '../../LocalStorageProvider'; + +export type HintData = { + title: string; + hintText: string; + price?: number; + paidSymbol?: string; +}; + +export default ({ + title, + hintText, + paidSymbol = 'Hint', + price = 250, + index, +}: HintData & { index: number }) => { + const { paidHints, buyHint, coins } = use(LocalStorageContext); + const [isInvoked, setIsInvoked] = useState(false); + const priceText = `🪙 ${price}`; + const paidStatus = paidHints[index]; + + const isAffordable = price <= coins; + return ( +
+ { + buyHint(index, price); + setIsInvoked(true); + event.currentTarget.blur(); + }} + {...((paidStatus || !isAffordable) && { tabIndex: -1 })} + > + + {paidStatus ? paidSymbol : priceText} + + + {title.length > 0 ? title : 'Hint'} + +
+ + Paid + +
+ {!paidStatus && !isAffordable && ( +
+ + Not enough credits + +
+ )} +
+ + {hintText} + +
+ ); +}; diff --git a/src/app/components/Riddle/Components/HintButton/index.styles.ts b/src/app/components/Riddle/Components/HintButton/index.styles.ts new file mode 100644 index 00000000000..f461d9422d7 --- /dev/null +++ b/src/app/components/Riddle/Components/HintButton/index.styles.ts @@ -0,0 +1,66 @@ +import { css, Theme } from '@emotion/react'; +import { focusIndicatorThickness } from '../../../ThemeProvider/focusIndicator'; + +export default { + hintContainer: ({ spacings }: Theme) => + css({ + position: 'relative', + cursor: 'pointer', + margin: `${spacings.HALF}rem 0`, + }), + hintButton: ({ palette }: Theme) => + css({ + all: 'unset', + display: 'flex', + width: '100%', + '&:focus-visible': { + outline: `${focusIndicatorThickness} solid ${palette.BLACK}`, + boxShadow: `0 0 0 ${focusIndicatorThickness} ${palette.WHITE}`, + outlineOffset: `${focusIndicatorThickness}`, + position: 'relative', + zIndex: '1', + }, + }), + hintSummaryText: ({ palette, spacings }: Theme) => + css({ + flexGrow: 1, + padding: `${0.6}rem ${spacings.FULL}rem`, + background: palette.GHOST, + 'details:open &': { + display: 'none', + }, + }), + hintPrice: ({ palette }: Theme) => + css({ + width: `${4}rem`, + padding: `${0.6}rem ${1}rem`, + background: palette.GREY_3, + textAlign: 'center', + }), + paidIcon: () => + css({ + position: 'absolute', + top: 0, + insetInlineStart: 0, + width: `${2.5}rem`, + background: 'cyan', + textAlign: 'center', + }), + notEnough: () => + css({ + position: 'absolute', + top: 0, + insetInlineStart: 0, + background: 'yellow', + textAlign: 'center', + }), + hintAnswerText: ({ spacings }: Theme) => + css({ + position: 'absolute', + top: 0, + insetInlineEnd: 0, + insetInlineStart: `${6}rem`, + padding: `${0.6}rem ${spacings.FULL}rem`, + background: '#FEFAE0', + }), +}; diff --git a/src/app/components/Riddle/Components/HintButton/index.tsx b/src/app/components/Riddle/Components/HintButton/index.tsx new file mode 100644 index 00000000000..a30930d006d --- /dev/null +++ b/src/app/components/Riddle/Components/HintButton/index.tsx @@ -0,0 +1,69 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/react'; +import { use } from 'react'; +import Text from '../../../Text'; +import style from './index.styles'; +import { LocalStorageContext } from '../../LocalStorageProvider'; + +export type HintData = { + title: string; + hintText: string; + price?: number; + paidSymbol?: string; +}; + +export default ({ + title, + hintText, + paidSymbol = 'Hint', + price = 500, + index, + onClickFn, +}: HintData & { index: number; onClickFn?: () => void }) => { + const { paidHints, buyHint, coins } = use(LocalStorageContext); + const priceText = `🪙 ${price}`; + const paidStatus = paidHints[index]; + + const isAffordable = price <= coins; + return ( +
+ +
+ ); +}; diff --git a/src/app/components/Riddle/Components/Placeholder/index.styles.ts b/src/app/components/Riddle/Components/Placeholder/index.styles.ts new file mode 100644 index 00000000000..1ae1b77307b --- /dev/null +++ b/src/app/components/Riddle/Components/Placeholder/index.styles.ts @@ -0,0 +1,14 @@ +import pixelsToRem from '#app/utilities/pixelsToRem'; +import { css, Theme } from '@emotion/react'; + +export default { + placeholder: ({ mq, palette }: Theme) => + css({ + background: palette.GREY_2, + width: '100%', + height: `${pixelsToRem(504)}rem`, + [mq.GROUP_2_MAX_WIDTH]: { + height: `${pixelsToRem(572)}rem`, + }, + }), +}; diff --git a/src/app/components/Riddle/Components/Placeholder/index.tsx b/src/app/components/Riddle/Components/Placeholder/index.tsx new file mode 100644 index 00000000000..48f0b88aaf1 --- /dev/null +++ b/src/app/components/Riddle/Components/Placeholder/index.tsx @@ -0,0 +1,7 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/react'; +import style from './index.styles'; + +export default () => { + return
; +}; diff --git a/src/app/components/Riddle/Components/ReadMeter/index.styles.ts b/src/app/components/Riddle/Components/ReadMeter/index.styles.ts new file mode 100644 index 00000000000..ff77d74754d --- /dev/null +++ b/src/app/components/Riddle/Components/ReadMeter/index.styles.ts @@ -0,0 +1,26 @@ +import { css, Theme } from '@emotion/react'; + +export default { + container: ({ palette, spacings }: Theme) => + css({ + position: 'fixed', + display: 'inline-block', + background: palette.POSTBOX, + textAlign: 'center', + width: '10rem', + padding: `${spacings.FULL}rem 0`, + bottom: '0.75rem', + insetInlineEnd: '0.75rem', + zIndex: 5, + }), + heading: ({ palette }: Theme) => + css({ + background: palette.WHITE, + }), + guage: ({ palette, spacings }: Theme) => + css({ + color: palette.WHITE, + display: 'block', + marginTop: `${spacings.HALF}rem`, + }), +}; diff --git a/src/app/components/Riddle/Components/ReadMeter/index.tsx b/src/app/components/Riddle/Components/ReadMeter/index.tsx new file mode 100644 index 00000000000..c5879080b92 --- /dev/null +++ b/src/app/components/Riddle/Components/ReadMeter/index.tsx @@ -0,0 +1,63 @@ +/** @jsx jsx */ +import { use, useEffect, useState } from 'react'; +import { jsx } from '@emotion/react'; +import onClient from '#app/lib/utilities/onClient'; +import Text from '../../../Text'; +import style from './index.styles'; +import { LocalStorageContext } from '../../LocalStorageProvider'; + +const ReadMeter = ({ wordCount = 0 }: { wordCount?: number }) => { + const { coins, addCoins } = use(LocalStorageContext); + const [message, setMessage] = useState(null); + + useEffect(() => { + let prevTime = new Date().getTime(); + let prevScrollDepth = window.scrollY; + + const RATE_OF_SCROLL_THRESHOLD = 300; + const listener = () => { + const documentHeight = document.body.scrollHeight; + const currTime = new Date().getTime(); + const currScrollDepth = window.scrollY; + const scrollDelta = currScrollDepth - prevScrollDepth; + const rateOfScroll = (scrollDelta / (currTime - prevTime)) * 1000; + + const wordsPerLine = wordCount / documentHeight; + const wordsReadInScroll = Math.ceil(scrollDelta * wordsPerLine); + + if (rateOfScroll > 0 && rateOfScroll <= RATE_OF_SCROLL_THRESHOLD) { + addCoins(wordsReadInScroll); + } else if (rateOfScroll > 0) { + setMessage('TOO FAST!'); + setTimeout(() => { + setMessage(null); + }, 2000); + } + + prevTime = currTime; + prevScrollDepth = currScrollDepth; + }; + + document.addEventListener('scrollend', listener); + return () => { + document.removeEventListener('scrollend', listener); + }; + }, [addCoins, wordCount]); + + return ( +
+ + CREDITS + + + {message ?? coins} + +
+ ); +}; + +export default ({ wordCount }: { wordCount?: number }) => { + const ReadMeterWithProvider = ; + + return onClient() ? ReadMeterWithProvider : null; +}; diff --git a/src/app/components/Riddle/LocalStorageProvider/index.tsx b/src/app/components/Riddle/LocalStorageProvider/index.tsx new file mode 100644 index 00000000000..3f674d30932 --- /dev/null +++ b/src/app/components/Riddle/LocalStorageProvider/index.tsx @@ -0,0 +1,173 @@ +import onClient from '#app/lib/utilities/onClient'; +import React, { + createContext, + PropsWithChildren, + useMemo, + useState, +} from 'react'; + +export type LocalStorage = { + goes: number; + coins: number; + paidHints: boolean[]; + addCoins: (amount: number) => void; + addGoes: () => void; + reduceCoins: (amount: number) => void; + reduceGoes: () => void; + buyHint: (amount: number, price: number) => void; + resetHints: () => void; + isWinner: boolean; + updateWinnerState: (bool: boolean) => void; +}; + +export const LocalStorageContext = createContext( + {} as LocalStorage, +); + +const DATA_KEY = 'ws_bbc_riddle'; + +const getLocalData = () => { + if (onClient()) { + const localStorageData = window.localStorage[DATA_KEY]; + if (localStorageData) { + const parsedData = JSON.parse(localStorageData); + return { + goes: parsedData.goes ?? 5, + coins: parsedData.coins ?? 0, + paidHints: parsedData.paidHints ?? [false, false, false], + isWinner: parsedData.isWinner ?? false, + }; + } + } + + return { + goes: 5, + coins: 0, + paidHints: [false, false, false], + isWinner: false, + }; +}; + +const setLocalData = ({ + goes, + coins, + paidHints, + isWinner, +}: { + goes?: number; + coins?: number; + paidHints?: boolean[]; + isWinner?: boolean; +}) => { + if (onClient()) { + const { + goes: localGoes, + coins: localCoins, + paidHints: localPaidHints, + isWinner: localIsWinner, + } = getLocalData(); + const updatedData = { + goes: goes ?? localGoes, + coins: coins ?? localCoins, + paidHints: paidHints ?? localPaidHints, + isWinner: isWinner ?? localIsWinner, + }; + const toStore = JSON.stringify(updatedData); + window.localStorage.setItem(DATA_KEY, toStore); + } +}; + +export default ({ children }: PropsWithChildren) => { + const { + goes: initialGoes, + coins: initialCoins, + paidHints: initialPaidHints, + isWinner: initialIsWinner, + } = getLocalData(); + + const [coins, updateCoins] = useState(initialCoins); + const [goes, updateGoes] = useState(initialGoes); + const [paidHints, updatePaidHints] = useState(initialPaidHints); + const [isWinner, updateIsWinner] = useState(initialIsWinner); + + const addCoins = (amount: number) => { + updateCoins(prevAmount => { + const newAmount = prevAmount + amount; + setLocalData({ coins: newAmount }); + return newAmount; + }); + }; + const addGoes = () => { + updateGoes(() => { + const newAmount = 5; + setLocalData({ goes: newAmount }); + return newAmount; + }); + }; + const reduceCoins = (amount: number) => { + updateCoins(prevAmount => { + const newAmount = prevAmount - amount; + setLocalData({ coins: newAmount }); + return newAmount; + }); + }; + + const resetHints = () => { + updatePaidHints(() => { + const updatedBoughtHints = [false, false, false]; + setLocalData({ paidHints: updatedBoughtHints }); + return updatedBoughtHints; + }); + }; + + const value = useMemo(() => { + const buyHint = (index: number, price: number) => { + if (price <= coins) { + reduceCoins(price); + updatePaidHints(prevBoughtHints => { + const updatedBoughtHints = [...prevBoughtHints]; + updatedBoughtHints[index] = true; + setLocalData({ paidHints: updatedBoughtHints }); + return updatedBoughtHints; + }); + } + }; + + const reduceGoes = () => { + if (goes > 0) { + updateGoes(prevGoes => { + const newGoes = prevGoes - 1; + setLocalData({ goes: newGoes }); + return newGoes; + }); + } + }; + + const updateWinnerState = (bool: boolean) => { + updateIsWinner(() => { + setLocalData({ isWinner: bool }); + return bool; + }); + }; + + return { + coins, + goes, + addCoins, + addGoes, + reduceCoins, + reduceGoes, + paidHints, + buyHint, + resetHints, + isWinner, + updateWinnerState, + }; + }, [coins, goes, isWinner, paidHints]); + + return ( + + {children} + + ); +}; diff --git a/src/app/components/Riddle/README.md b/src/app/components/Riddle/README.md new file mode 100644 index 00000000000..5242ed633d4 --- /dev/null +++ b/src/app/components/Riddle/README.md @@ -0,0 +1,5 @@ +## Description + +This component renders the estimated read time for an article, by using the `readTime` parameter passed into it. + +If no `readTime` is supplied nothing is rendered. \ No newline at end of file diff --git a/src/app/components/Riddle/RiddleProvider/index.tsx b/src/app/components/Riddle/RiddleProvider/index.tsx new file mode 100644 index 00000000000..9448d72a195 --- /dev/null +++ b/src/app/components/Riddle/RiddleProvider/index.tsx @@ -0,0 +1,215 @@ +import React, { + createContext, + Dispatch, + PropsWithChildren, + SetStateAction, + use, + useEffect, + useMemo, + useState, +} from 'react'; +import { GameData } from '../Components/Card'; +import data from '../data'; +import { LocalStorageContext } from '../LocalStorageProvider'; + +const defaultGameData = { + expire: '2024-12-31T23:59:59+00:00', + question: 'Game Closed', + hint1: { + title: 'Game Closed', + hintText: 'Game Closed', + price: 0, + }, + hint2: { + title: 'Game Closed', + hintText: 'Game Closed', + price: 0, + }, + answer: 'Game Closed', + funFact: 'Game Closed', +} as GameData; + +export enum GameState { + PLAY = 'PLAY', + CLOSED = 'CLOSED', + FAILED = 'FAILED', + WINNER = 'WINNER', +} + +export type RiddleGameState = { + gameIndex: number; + gameState: GameState; + gameData: GameData; + updateGameState: Dispatch>; + devTime: Date; + forceTimeInc24: () => void; + forceTimeDec24: () => void; + submitAttempt: (str: string) => GameState; + revealAnswer: (price: number) => void; + devOptionResetGoes: () => void; +}; + +export const RiddleContext = createContext( + {} as RiddleGameState, +); + +const findCurrGameIndex = (forcedDate?: Date) => { + return data.findIndex(riddleData => { + const currTime = forcedDate ?? new Date(); + const currEpoch = currTime.getTime(); + const riddleExpiry = new Date(riddleData.expire).getTime(); + if (currEpoch < riddleExpiry) { + return true; + } + return false; + }); +}; + +export default ({ children }: PropsWithChildren) => { + const { + resetHints, + goes, + reduceGoes, + addGoes, + isWinner, + updateWinnerState, + coins, + } = use(LocalStorageContext); + const [devTime, updateDevTime] = useState(new Date()); + const initialIndex = findCurrGameIndex(); + const [gameIndex, updateIndex] = useState(initialIndex); + const gameData = gameIndex > -1 ? data[gameIndex] : defaultGameData; + let initialGameState = gameIndex > -1 ? GameState.PLAY : GameState.CLOSED; + if (goes <= 0 && isWinner === false) { + initialGameState = GameState.FAILED; + } else if (isWinner === true) { + initialGameState = GameState.WINNER; + } + const [gameState, updateGameState] = useState(initialGameState); + + useEffect(() => { + const timer = setInterval(() => { + const devCurrTime = new Date(devTime); + const updatedTimeStamp = devCurrTime.setSeconds( + devCurrTime.getSeconds() + 1, + ); + + const updatedTime = new Date(updatedTimeStamp); + const expiryTime = new Date(gameData.expire); + updateDevTime(updatedTime); + + if ( + gameState !== GameState.CLOSED && + updatedTime.getTime() > expiryTime.getTime() + ) { + const nextGameIndex = findCurrGameIndex(updatedTime); + const updatedGameState = + nextGameIndex > -1 ? GameState.PLAY : GameState.CLOSED; + + resetHints(); + updateIndex(nextGameIndex); + updateGameState(updatedGameState); + updateWinnerState(false); + addGoes(); + } + }, 1000); + + return () => clearInterval(timer); + }, [ + addGoes, + devTime, + gameData.expire, + gameState, + resetHints, + updateWinnerState, + ]); + + const value = useMemo(() => { + const forceTimeInc24 = () => { + const updatedDateStamp = devTime.setDate(devTime.getDate() + 1); + const updatedDate = new Date(updatedDateStamp); + updateDevTime(updatedDate); + }; + + const forceTimeDec24 = () => { + const updatedDateStamp = devTime.setDate(devTime.getDate() - 1); + const updatedDate = new Date(updatedDateStamp); + const nextGameIndex = findCurrGameIndex(updatedDate); + const updatedGameState = + nextGameIndex > -1 ? GameState.PLAY : GameState.CLOSED; + + updateDevTime(updatedDate); + resetHints(); + updateIndex(nextGameIndex); + updateGameState(updatedGameState); + updateWinnerState(false); + addGoes(); + }; + + const submitAttempt = (submitString: string) => { + let updatedGameState = GameState.PLAY; + if (goes > 0) { + const sanitised = submitString + .toLowerCase() // Convert to lowercase + .replace(/[^a-z0-9]/g, ''); + + const { answer } = gameData; + const myRegex = new RegExp(answer, 'gi'); + if (myRegex.exec(sanitised) !== null) { + updateGameState(GameState.WINNER); + updateWinnerState(true); + updatedGameState = GameState.WINNER; + } else if (myRegex.exec(sanitised) === null && goes === 1) { + updateGameState(GameState.FAILED); + updateWinnerState(false); + updatedGameState = GameState.FAILED; + } + reduceGoes(); + } + + return updatedGameState; + }; + + const revealAnswer = (price: number) => { + if (price <= coins) { + updateGameState(GameState.WINNER); + updateWinnerState(true); + } + }; + + const devOptionResetGoes = () => { + addGoes(); + if (gameState !== GameState.CLOSED) { + updateGameState(GameState.PLAY); + } + }; + + return { + gameState, + updateGameState, + forceTimeInc24, + forceTimeDec24, + gameData, + devTime, + gameIndex, + submitAttempt, + revealAnswer, + devOptionResetGoes, + }; + }, [ + addGoes, + coins, + devTime, + gameData, + gameIndex, + gameState, + goes, + reduceGoes, + resetHints, + updateWinnerState, + ]); + + return ( + {children} + ); +}; diff --git a/src/app/components/Riddle/data.ts b/src/app/components/Riddle/data.ts new file mode 100644 index 00000000000..5ff74d9f381 --- /dev/null +++ b/src/app/components/Riddle/data.ts @@ -0,0 +1,466 @@ +export default [ + { + expire: '2025-12-14T23:59:59+00:00', + question: + "I'm surrounded by water, but I never drink. I can swim for miles, but I never breathe. I have only one eye, but I never blink. What am I?", + hint1: { title: 'Begins with', hintText: 's' }, + hint2: { title: '', hintText: 'It has a propeller' }, + answer: 'submarine', + funFact: + 'Submarines use ballast tanks to control buoyancy, letting them dive and surface.', + }, + { + expire: '2025-12-15T23:59:59+00:00', + question: 'What has to be broken before you can use it?', + hint1: { title: 'Begins with', hintText: 'e' }, + hint2: { title: 'Type of', hintText: 'food' }, + answer: 'egg', + funFact: + 'Eggshell color (white, brown, blue/green) is set by hen genetics—not nutrition.', + }, + { + expire: '2025-12-16T23:59:59+00:00', + question: 'I’m tall when I’m young, and I’m short when I’m old. What am I?', + hint1: { title: 'Begins with', hintText: 'c' }, + hint2: { title: 'Type of', hintText: 'object' }, + answer: 'candle', + funFact: + 'The blue base of a candle flame is the hottest zone where wax vapour burns most completely.', + }, + // REPLACED: original answer was “all of them” + { + expire: '2025-12-17T23:59:59+00:00', + question: 'Which month has the fewest days?', + hint1: { title: 'Begins with', hintText: 'f' }, + hint2: { title: 'Type of', hintText: 'month' }, + answer: 'february', + funFact: + 'February uniquely has 28 days in common years and 29 in leap years.', + }, + { + expire: '2025-12-18T23:59:59+00:00', + question: 'What is full of holes but still holds water?', + hint1: { title: 'Begins with', hintText: 's' }, + hint2: { title: 'Type of', hintText: 'cleaning tool' }, + answer: 'sponge', + funFact: + 'Sea sponges are simple animals; many household sponges are made from cellulose or foam.', + }, + // REPLACED: original answer was “are you asleep yet?” + { + expire: '2025-12-19T23:59:59+00:00', + question: 'What wakes you up with a buzz but has no mouth?', + hint1: { title: 'Begins with', hintText: 'a' }, + hint2: { title: 'Type of', hintText: 'device' }, + answer: 'alarm', + funFact: + '“Alarm” traces to Italian “all’arma” (“to arms”), later generalised to warning signals.', + }, + // REPLACED: original answer was “the future” + { + expire: '2025-12-20T23:59:59+00:00', + question: 'What is always ahead but cannot be seen?', + hint1: { title: 'Begins with', hintText: 'f' }, + hint2: { title: 'Type of', hintText: 'concept' }, + answer: 'future', + funFact: + 'In grammar, the future tense marks actions that have not yet occurred.', + }, + // REPLACED: original answer was “no stairs” + { + expire: '2025-12-21T23:59:59+00:00', + question: 'A house with only one level and no stairs is called what?', + hint1: { title: 'Begins with', hintText: 'b' }, + hint2: { title: 'Type of', hintText: 'house style' }, + answer: 'bungalow', + funFact: + '“Bungalow” originated from Bengali “bangla,” referring to a single-storey house style.', + }, + { + expire: '2025-12-22T23:59:59+00:00', + question: 'What can you break, even if you never pick it up or touch it?', + hint1: { title: 'Begins with', hintText: 'p' }, + hint2: { title: 'Type of', hintText: 'concept' }, + answer: 'promise', + funFact: + 'To “keep your word” means to honour a promise—no physical object required.', + }, + { + expire: '2025-12-23T23:59:59+00:00', + question: 'What goes up but never comes down?', + hint1: { title: 'Begins with', hintText: 'a' }, + hint2: { title: 'Type of', hintText: 'measure' }, + answer: 'age', + funFact: + 'Age increases with time; birthdays mark the passage of another year.', + }, + // REPLACED: original answer was “he was bald” + { + expire: '2025-12-24T23:59:59+00:00', + question: 'What word describes a person with no hair on their head?', + hint1: { title: 'Begins with', hintText: 'b' }, + hint2: { title: 'Type of', hintText: 'condition' }, + answer: 'bald', + funFact: + 'Male pattern baldness is strongly influenced by genetics and hormones.', + }, + { + expire: '2025-12-25T23:59:59+00:00', + question: 'What gets wet while drying?', + hint1: { title: 'Begins with', hintText: 't' }, + hint2: { title: 'Type of', hintText: 'object' }, + answer: 'towel', + funFact: + 'Microfiber towels dry fast because their many fine fibres greatly increase surface area.', + }, + // REPLACED: original answer was “your word” + { + expire: '2025-12-26T23:59:59+00:00', + question: 'What can you keep without holding in your hands?', + hint1: { title: 'Begins with', hintText: 's' }, + hint2: { title: 'Type of', hintText: 'concept' }, + answer: 'secret', + funFact: + 'A secret is information intentionally kept hidden; sharing it ends the secrecy.', + }, + { + expire: '2025-12-27T23:59:59+00:00', + question: 'I shave every day, but my beard stays the same. What am I?', + hint1: { title: 'Begins with', hintText: 'b' }, + hint2: { title: 'Type of', hintText: 'profession' }, + answer: 'barber', + funFact: + 'Barber poles’ red-and-white stripes recall historic bloodletting once done by barbers.', + }, + { + expire: '2025-12-28T23:59:59+00:00', + question: + 'You see me once in June, twice in November, and not at all in May. What am I?', + hint1: { title: 'Begins with', hintText: 't' }, + hint2: { title: 'Type of', hintText: 'letter' }, + answer: 'e', + funFact: '“E” is the most common letter in English words and texts.', + }, + { + expire: '2025-12-29T23:59:59+00:00', + question: 'I have branches, but no fruit, trunk or leaves. What am I?', + hint1: { title: 'Begins with', hintText: 'b' }, + hint2: { title: 'Type of', hintText: 'place' }, + answer: 'bank', + funFact: + '“Bank” derives from Italian “banco,” a bench used by moneylenders.', + }, + { + expire: '2025-12-30T23:59:59+00:00', + question: 'What can’t talk but will reply when spoken to?', + hint1: { title: 'Begins with', hintText: 'e' }, + hint2: { title: 'Type of', hintText: 'phenomenon' }, + answer: 'echo', + funFact: + 'Echoes occur when sound reflects back to the listener from a surface.', + }, + { + expire: '2025-12-31T23:59:59+00:00', + question: 'The more of me you take, the more you leave behind. What am I?', + hint1: { title: 'Begins with', hintText: 'f' }, + hint2: { title: 'Type of', hintText: 'trace' }, + answer: 'footsteps', + funFact: + 'Footsteps leave prints and patterns that can reveal stride and direction.', + }, + { + expire: '2026-01-01T23:59:59+00:00', + question: + 'I’m light as a feather, yet the strongest person can’t hold me for five minutes. What am I?', + hint1: { title: 'Begins with', hintText: 'b' }, + hint2: { title: 'Type of', hintText: 'body function' }, + answer: 'breath', + funFact: + 'Breathing is regulated automatically by the brainstem (medulla and pons).', + }, + { + expire: '2026-01-02T23:59:59+00:00', + question: 'What has many keys but can’t open a single lock?', + hint1: { title: 'Begins with', hintText: 'p' }, + hint2: { title: 'Type of', hintText: 'instrument' }, + answer: 'piano', + funFact: + 'A standard piano has 88 keys spanning over seven octaves plus a minor third.', + }, + { + expire: '2026-01-03T23:59:59+00:00', + question: 'What has legs but doesn’t walk?', + hint1: { title: 'Begins with', hintText: 't' }, + hint2: { title: 'Type of', hintText: 'furniture' }, + answer: 'table', + funFact: 'Tables provide a raised flat surface for dining, work, and play.', + }, + { + expire: '2026-01-04T23:59:59+00:00', + question: 'What runs but never walks, has a bed but never sleeps?', + hint1: { title: 'Begins with', hintText: 'r' }, + hint2: { title: 'Type of', hintText: 'nature' }, + answer: 'river', + funFact: + 'A river’s “bed” is the channel it flows through; the “banks” confine its sides.', + }, + { + expire: '2026-01-05T23:59:59+00:00', + question: 'What has a head, a tail, but no body?', + hint1: { title: 'Begins with', hintText: 'c' }, + hint2: { title: 'Type of', hintText: 'object' }, + answer: 'coin', + funFact: 'Coins have an obverse (“heads”) and reverse (“tails”).', + }, + { + expire: '2026-01-06T23:59:59+00:00', + question: 'What has words but never speaks?', + hint1: { title: 'Begins with', hintText: 'b' }, + hint2: { title: 'Type of', hintText: 'object' }, + answer: 'book', + funFact: + 'Books are made of bound pages along a spine and may be printed or digital.', + }, + { + expire: '2026-01-07T23:59:59+00:00', + question: 'What has one eye but can’t see?', + hint1: { title: 'Begins with', hintText: 'n' }, + hint2: { title: 'Type of', hintText: 'object' }, + answer: 'needle', + funFact: 'The “eye” of a sewing needle is the hole that holds the thread.', + }, + { + expire: '2026-01-08T23:59:59+00:00', + question: 'What has a neck but no head?', + hint1: { title: 'Begins with', hintText: 'b' }, + hint2: { title: 'Type of', hintText: 'container' }, + answer: 'bottle', + funFact: + 'Glass bottles can be recycled indefinitely without losing quality.', + }, + { + expire: '2026-01-09T23:59:59+00:00', + question: 'What has hands but can’t clap?', + hint1: { title: 'Begins with', hintText: 'c' }, + hint2: { title: 'Type of', hintText: 'object' }, + answer: 'clock', + funFact: + 'Analogue clocks use “hands” to indicate hours, minutes, and seconds.', + }, + { + expire: '2026-01-10T23:59:59+00:00', + question: + 'What has cities, but no houses; forests, but no trees; and water, but no fish?', + hint1: { title: 'Begins with', hintText: 'm' }, + hint2: { title: 'Type of', hintText: 'representation' }, + answer: 'map', + funFact: + 'Map projections trade off distortions of area, shape, distance, and direction.', + }, + { + expire: '2026-01-11T23:59:59+00:00', + question: 'What can travel around the world while staying in a corner?', + hint1: { title: 'Begins with', hintText: 's' }, + hint2: { title: 'Type of', hintText: 'mail' }, + answer: 'stamp', + funFact: + 'Postage stamps affixed to letters serve as proof of payment for delivery.', + }, + { + expire: '2026-01-12T23:59:59+00:00', + question: 'What has an ear but cannot hear?', + hint1: { title: 'Begins with', hintText: 'c' }, + hint2: { title: 'Type of', hintText: 'place' }, + answer: 'cornfield', + funFact: + 'In botany, an “ear” is the seed-bearing part of cereal plants like corn.', + }, + { + expire: '2026-01-13T23:59:59+00:00', + question: 'What has teeth but doesn’t bite?', + hint1: { title: 'Begins with', hintText: 'c' }, + hint2: { title: 'Type of', hintText: 'object' }, + answer: 'comb', + funFact: 'Combs detangle hair and come in fine and wide-tooth varieties.', + }, + { + expire: '2026-01-14T23:59:59+00:00', + question: 'What has a heart that doesn’t beat?', + hint1: { title: 'Begins with', hintText: 'a' }, + hint2: { title: 'Type of', hintText: 'plant' }, + answer: 'artichoke', + funFact: + 'An artichoke’s edible “heart” is the tender core of its flower bud.', + }, + // REPLACED: original answer was “garbage truck” + { + expire: '2026-01-15T23:59:59+00:00', + question: 'What has four wheels and moves without an engine?', + hint1: { title: 'Begins with', hintText: 's' }, + hint2: { title: 'Type of', hintText: 'vehicle' }, + answer: 'skateboard', + funFact: + 'Skateboards roll on four small wheels and are propelled by the rider’s push.', + }, + { + expire: '2026-01-16T23:59:59+00:00', + question: 'What has an endless supply of letters but starts empty?', + hint1: { title: 'Begins with', hintText: 'm' }, + hint2: { title: 'Type of', hintText: 'object' }, + answer: 'mailbox', + funFact: 'Mailbox flags signal outgoing mail to a carrier.', + }, + { + expire: '2026-01-17T23:59:59+00:00', + question: 'What has a thumb and four fingers but is not alive?', + hint1: { title: 'Begins with', hintText: 'g' }, + hint2: { title: 'Type of', hintText: 'clothing' }, + answer: 'glove', + funFact: + 'Gloves protect hands and can be made of leather, latex, nitrile, or fabric.', + }, + { + expire: '2026-01-18T23:59:59+00:00', + question: 'What has a spine but no bones?', + hint1: { title: 'Begins with', hintText: 'b' }, + hint2: { title: 'Type of', hintText: 'object' }, + answer: 'book', + funFact: 'A book’s “spine” holds pages together and displays the title.', + }, + { + expire: '2026-01-19T23:59:59+00:00', + question: 'What has a face and two hands but no arms or legs?', + hint1: { title: 'Begins with', hintText: 'c' }, + hint2: { title: 'Type of', hintText: 'object' }, + answer: 'clock', + funFact: + 'Mechanical clocks keep time using oscillators like pendulums or balance wheels.', + }, + { + expire: '2026-01-20T23:59:59+00:00', + question: + 'What has an end but no beginning, a home but no family, and a space without room?', + hint1: { title: 'Begins with', hintText: 'k' }, + hint2: { title: 'Type of', hintText: 'device' }, + answer: 'keyboard', + funFact: + 'The QWERTY layout was designed to reduce jams in early typewriters.', + }, + { + expire: '2026-01-21T23:59:59+00:00', + question: 'What has a ring but no finger?', + hint1: { title: 'Begins with', hintText: 't' }, + hint2: { title: 'Type of', hintText: 'device' }, + answer: 'telephone', + funFact: + '“Telephone” combines Greek roots: “tele” (far) + “phone” (voice/sound).', + }, + { + expire: '2026-01-22T23:59:59+00:00', + question: 'What has a bark but no bite?', + hint1: { title: 'Begins with', hintText: 't' }, + hint2: { title: 'Type of', hintText: 'plant' }, + answer: 'tree', + funFact: 'Through photosynthesis, trees absorb CO₂ and release oxygen.', + }, + { + expire: '2026-01-23T23:59:59+00:00', + question: 'What has wings but cannot fly?', + hint1: { title: 'Begins with', hintText: 'p' }, + hint2: { title: 'Type of', hintText: 'animal' }, + answer: 'penguin', + funFact: + 'Penguins “fly” underwater using stiff flipper-like wings to propel themselves.', + }, + { + expire: '2026-01-24T23:59:59+00:00', + question: 'What has a lock but no key?', + hint1: { title: 'Begins with', hintText: 'h' }, + hint2: { title: 'Type of', hintText: 'body part' }, + answer: 'hair', + funFact: + 'Hair is primarily made of keratin, the same protein found in nails and feathers.', + }, + { + expire: '2026-01-25T23:59:59+00:00', + question: 'What has a horn but does not honk?', + hint1: { title: 'Begins with', hintText: 'r' }, + hint2: { title: 'Type of', hintText: 'animal' }, + answer: 'rhinoceros', + funFact: 'A rhino’s horn is made of keratin—like hair and fingernails.', + }, + { + expire: '2026-01-26T23:59:59+00:00', + question: 'What has a foot but no legs?', + hint1: { title: 'Begins with', hintText: 'r' }, + hint2: { title: 'Type of', hintText: 'measure' }, + answer: 'ruler', + funFact: + 'A “foot” is a unit equal to 12 inches, commonly marked on rulers.', + }, + { + expire: '2026-01-27T23:59:59+00:00', + question: 'What has a head but no brain?', + hint1: { title: 'Begins with', hintText: 'p' }, + hint2: { title: 'Type of', hintText: 'object' }, + answer: 'pin', + funFact: 'Safety pins add a clasp and spring to fasten items securely.', + }, + { + expire: '2026-01-28T23:59:59+00:00', + question: 'What has a shell but no pearl?', + hint1: { title: 'Begins with', hintText: 'w' }, + hint2: { title: 'Type of', hintText: 'food' }, + answer: 'walnut', + funFact: + 'Botanically, walnuts are drupes; the edible part is the seed inside the shell.', + }, + { + expire: '2026-01-29T23:59:59+00:00', + question: + 'What has a key but no lock, space but no room, and you can enter but not go in?', + hint1: { title: 'Begins with', hintText: 'k' }, + hint2: { title: 'Type of', hintText: 'device' }, + answer: 'keyboard', + funFact: + 'On a keyboard, the “space” bar adds a blank character—no physical room required.', + }, + { + expire: '2026-01-30T23:59:59+00:00', + question: 'What has a face but no eyes, mouth, or nose?', + hint1: { title: 'Begins with', hintText: 'c' }, + hint2: { title: 'Type of', hintText: 'object' }, + answer: 'clock', + funFact: 'Atomic clocks are today’s most accurate timekeepers.', + }, + { + expire: '2026-01-31T23:59:59+00:00', + question: + "I'm surrounded by water, but I never drink. I can swim for miles, but I never breathe. I have only one eye, but I never blink. What am I?", + hint1: { title: 'Begins with', hintText: 's' }, + hint2: { title: 'Type of', hintText: 'vehicle' }, + answer: 'submarine', + funFact: + 'Periscopes let submarines observe the surface while remaining submerged.', + }, + { + expire: '2026-02-01T23:59:59+00:00', + question: + 'What has roots that nobody sees, is taller than trees, up, up it goes, and yet never grows?', + hint1: { title: 'Begins with', hintText: 'm' }, + hint2: { title: 'Type of', hintText: 'geography' }, + answer: 'mountain', + funFact: + 'Mountains form mainly through tectonic uplift and volcanic activity.', + }, + { + expire: '2026-02-02T23:59:59+00:00', + question: + 'I can fly without wings. I can cry without eyes. Wherever I go, darkness follows me. What am I?', + hint1: { title: 'Begins with', hintText: 'c' }, + hint2: { title: 'Type of', hintText: 'weather' }, + answer: 'cloud', + funFact: + 'Clouds consist of tiny water droplets or ice crystals suspended in air.', + }, +]; diff --git a/src/app/components/Riddle/data_original.js b/src/app/components/Riddle/data_original.js new file mode 100644 index 00000000000..6675b7eca85 --- /dev/null +++ b/src/app/components/Riddle/data_original.js @@ -0,0 +1,677 @@ +export default [ + { + expire: '2025-12-14T23:59:59+00:00', + question: + "I'm surrounded by water, but I never drink. I can swim for miles, but I never breathe. I have only one eye, but I never blink. What am I?", + hint1: { + title: 'Begins with', + hintText: 's', + }, + hint2: { + title: '', + hintText: 'It has a propeller', + }, + answer: 'submarine', + }, + { + expire: '2025-12-15T23:59:59+00:00', + question: 'What has to be broken before you can use it?', + hint1: { + title: 'Begins with', + hintText: 'e', + }, + hint2: { + title: 'Type of', + hintText: 'food', + }, + answer: 'egg', + }, + { + expire: '2025-12-16T23:59:59+00:00', + question: + 'I\u2019m tall when I\u2019m young, and I\u2019m short when I\u2019m old. What am I?', + hint1: { + title: 'Begins with', + hintText: 'c', + }, + hint2: { + title: 'Type of', + hintText: 'object', + }, + answer: 'candle', + }, + { + expire: '2025-12-17T23:59:59+00:00', + question: 'What month of the year has 28 days?', + hint1: { + title: 'Begins with', + hintText: 'a', + }, + hint2: { + title: 'Type of', + hintText: 'time fact', + }, + answer: 'all of them', + }, + { + expire: '2025-12-18T23:59:59+00:00', + question: 'What is full of holes but still holds water?', + hint1: { + title: 'Begins with', + hintText: 's', + }, + hint2: { + title: 'Type of', + hintText: 'cleaning tool', + }, + answer: 'sponge', + }, + { + expire: '2025-12-19T23:59:59+00:00', + question: 'What question can you never answer yes to?', + hint1: { + title: 'Begins with', + hintText: 'a', + }, + hint2: { + title: 'Type of', + hintText: 'question', + }, + answer: 'are you asleep yet?', + }, + { + expire: '2025-12-20T23:59:59+00:00', + question: 'What is always in front of you but can\u2019t be seen?', + hint1: { + title: 'Begins with', + hintText: 't', + }, + hint2: { + title: 'Type of', + hintText: 'concept', + }, + answer: 'the future', + }, + { + expire: '2025-12-21T23:59:59+00:00', + question: + 'There\u2019s a one-story house in which everything is yellow. What color are the stairs?', + hint1: { + title: 'Begins with', + hintText: 'n', + }, + hint2: { + title: 'Type of', + hintText: 'trick', + }, + answer: 'no stairs', + }, + { + expire: '2025-12-22T23:59:59+00:00', + question: 'What can you break, even if you never pick it up or touch it?', + hint1: { + title: 'Begins with', + hintText: 'p', + }, + hint2: { + title: 'Type of', + hintText: 'concept', + }, + answer: 'promise', + }, + { + expire: '2025-12-23T23:59:59+00:00', + question: 'What goes up but never comes down?', + hint1: { + title: 'Begins with', + hintText: 'a', + }, + hint2: { + title: 'Type of', + hintText: 'measure', + }, + answer: 'age', + }, + { + expire: '2025-12-24T23:59:59+00:00', + question: + 'A man who was outside in the rain without an umbrella or hat didn\u2019t get a single hair on his head wet. Why?', + hint1: { + title: 'Begins with', + hintText: 'h', + }, + hint2: { + title: 'Type of', + hintText: 'condition', + }, + answer: 'he was bald', + }, + { + expire: '2025-12-25T23:59:59+00:00', + question: 'What gets wet while drying?', + hint1: { + title: 'Begins with', + hintText: 't', + }, + hint2: { + title: 'Type of', + hintText: 'object', + }, + answer: 'towel', + }, + { + expire: '2025-12-26T23:59:59+00:00', + question: 'What can you keep after giving to someone?', + hint1: { + title: 'Begins with', + hintText: 'y', + }, + hint2: { + title: 'Type of', + hintText: 'concept', + }, + answer: 'your word', + }, + { + expire: '2025-12-27T23:59:59+00:00', + question: 'I shave every day, but my beard stays the same. What am I?', + hint1: { + title: 'Begins with', + hintText: 'b', + }, + hint2: { + title: 'Type of', + hintText: 'profession', + }, + answer: 'barber', + }, + { + expire: '2025-12-28T23:59:59+00:00', + question: + 'You see me once in June, twice in November, and not at all in May. What am I?', + hint1: { + title: 'Begins with', + hintText: 't', + }, + hint2: { + title: 'Type of', + hintText: 'letter', + }, + answer: 'the letter e', + }, + { + expire: '2025-12-29T23:59:59+00:00', + question: 'I have branches, but no fruit, trunk or leaves. What am I?', + hint1: { + title: 'Begins with', + hintText: 'b', + }, + hint2: { + title: 'Type of', + hintText: 'place', + }, + answer: 'bank', + }, + { + expire: '2025-12-30T23:59:59+00:00', + question: 'What can\u2019t talk but will reply when spoken to?', + hint1: { + title: 'Begins with', + hintText: 'e', + }, + hint2: { + title: 'Type of', + hintText: 'phenomenon', + }, + answer: 'echo', + }, + { + expire: '2025-12-31T23:59:59+00:00', + question: 'The more of me you take, the more you leave behind. What am I?', + hint1: { + title: 'Begins with', + hintText: 'f', + }, + hint2: { + title: 'Type of', + hintText: 'trace', + }, + answer: 'footsteps', + }, + { + expire: '2026-01-01T23:59:59+00:00', + question: + 'I\u2019m light as a feather, yet the strongest person can\u2019t hold me for five minutes. What am I?', + hint1: { + title: 'Begins with', + hintText: 'b', + }, + hint2: { + title: 'Type of', + hintText: 'body function', + }, + answer: 'breath', + }, + { + expire: '2026-01-02T23:59:59+00:00', + question: 'What has many keys but can\u2019t open a single lock?', + hint1: { + title: 'Begins with', + hintText: 'p', + }, + hint2: { + title: 'Type of', + hintText: 'instrument', + }, + answer: 'piano', + }, + { + expire: '2026-01-03T23:59:59+00:00', + question: 'What has legs but doesn\u2019t walk?', + hint1: { + title: 'Begins with', + hintText: 't', + }, + hint2: { + title: 'Type of', + hintText: 'furniture', + }, + answer: 'table', + }, + { + expire: '2026-01-04T23:59:59+00:00', + question: 'What runs but never walks, has a bed but never sleeps?', + hint1: { + title: 'Begins with', + hintText: 'r', + }, + hint2: { + title: 'Type of', + hintText: 'nature', + }, + answer: 'river', + }, + { + expire: '2026-01-05T23:59:59+00:00', + question: 'What has a head, a tail, but no body?', + hint1: { + title: 'Begins with', + hintText: 'c', + }, + hint2: { + title: 'Type of', + hintText: 'object', + }, + answer: 'coin', + }, + { + expire: '2026-01-06T23:59:59+00:00', + question: 'What has words but never speaks?', + hint1: { + title: 'Begins with', + hintText: 'b', + }, + hint2: { + title: 'Type of', + hintText: 'object', + }, + answer: 'book', + }, + { + expire: '2026-01-07T23:59:59+00:00', + question: 'What has one eye but can\u2019t see?', + hint1: { + title: 'Begins with', + hintText: 'n', + }, + hint2: { + title: 'Type of', + hintText: 'object', + }, + answer: 'needle', + }, + { + expire: '2026-01-08T23:59:59+00:00', + question: 'What has a neck but no head?', + hint1: { + title: 'Begins with', + hintText: 'b', + }, + hint2: { + title: 'Type of', + hintText: 'container', + }, + answer: 'bottle', + }, + { + expire: '2026-01-09T23:59:59+00:00', + question: 'What has hands but can\u2019t clap?', + hint1: { + title: 'Begins with', + hintText: 'c', + }, + hint2: { + title: 'Type of', + hintText: 'object', + }, + answer: 'clock', + }, + { + expire: '2026-01-10T23:59:59+00:00', + question: + 'What has cities, but no houses; forests, but no trees; and water, but no fish?', + hint1: { + title: 'Begins with', + hintText: 'm', + }, + hint2: { + title: 'Type of', + hintText: 'representation', + }, + answer: 'map', + }, + { + expire: '2026-01-11T23:59:59+00:00', + question: 'What can travel around the world while staying in a corner?', + hint1: { + title: 'Begins with', + hintText: 's', + }, + hint2: { + title: 'Type of', + hintText: 'mail', + }, + answer: 'stamp', + }, + { + expire: '2026-01-12T23:59:59+00:00', + question: 'What has an ear but cannot hear?', + hint1: { + title: 'Begins with', + hintText: 'c', + }, + hint2: { + title: 'Type of', + hintText: 'place', + }, + answer: 'cornfield', + }, + { + expire: '2026-01-13T23:59:59+00:00', + question: 'What has teeth but doesn\u2019t bite?', + hint1: { + title: 'Begins with', + hintText: 'c', + }, + hint2: { + title: 'Type of', + hintText: 'object', + }, + answer: 'comb', + }, + { + expire: '2026-01-14T23:59:59+00:00', + question: 'What has a heart that doesn\u2019t beat?', + hint1: { + title: 'Begins with', + hintText: 'a', + }, + hint2: { + title: 'Type of', + hintText: 'plant', + }, + answer: 'artichoke', + }, + { + expire: '2026-01-15T23:59:59+00:00', + question: 'What has four wheels and flies?', + hint1: { + title: 'Begins with', + hintText: 'g', + }, + hint2: { + title: 'Type of', + hintText: 'vehicle', + }, + answer: 'garbage truck', + }, + { + expire: '2026-01-16T23:59:59+00:00', + question: 'What has an endless supply of letters but starts empty?', + hint1: { + title: 'Begins with', + hintText: 'm', + }, + hint2: { + title: 'Type of', + hintText: 'object', + }, + answer: 'mailbox', + }, + { + expire: '2026-01-17T23:59:59+00:00', + question: 'What has a thumb and four fingers but is not alive?', + hint1: { + title: 'Begins with', + hintText: 'g', + }, + hint2: { + title: 'Type of', + hintText: 'clothing', + }, + answer: 'glove', + }, + { + expire: '2026-01-18T23:59:59+00:00', + question: 'What has a spine but no bones?', + hint1: { + title: 'Begins with', + hintText: 'b', + }, + hint2: { + title: 'Type of', + hintText: 'object', + }, + answer: 'book', + }, + { + expire: '2026-01-19T23:59:59+00:00', + question: 'What has a face and two hands but no arms or legs?', + hint1: { + title: 'Begins with', + hintText: 'c', + }, + hint2: { + title: 'Type of', + hintText: 'object', + }, + answer: 'clock', + }, + { + expire: '2026-01-20T23:59:59+00:00', + question: + 'What has an end but no beginning, a home but no family, and a space without room?', + hint1: { + title: 'Begins with', + hintText: 'k', + }, + hint2: { + title: 'Type of', + hintText: 'device', + }, + answer: 'keyboard', + }, + { + expire: '2026-01-21T23:59:59+00:00', + question: 'What has a ring but no finger?', + hint1: { + title: 'Begins with', + hintText: 't', + }, + hint2: { + title: 'Type of', + hintText: 'device', + }, + answer: 'telephone', + }, + { + expire: '2026-01-22T23:59:59+00:00', + question: 'What has a bark but no bite?', + hint1: { + title: 'Begins with', + hintText: 't', + }, + hint2: { + title: 'Type of', + hintText: 'plant', + }, + answer: 'tree', + }, + { + expire: '2026-01-23T23:59:59+00:00', + question: 'What has wings but cannot fly?', + hint1: { + title: 'Begins with', + hintText: 'p', + }, + hint2: { + title: 'Type of', + hintText: 'animal', + }, + answer: 'penguin', + }, + { + expire: '2026-01-24T23:59:59+00:00', + question: 'What has a lock but no key?', + hint1: { + title: 'Begins with', + hintText: 'h', + }, + hint2: { + title: 'Type of', + hintText: 'body part', + }, + answer: 'hair', + }, + { + expire: '2026-01-25T23:59:59+00:00', + question: 'What has a horn but does not honk?', + hint1: { + title: 'Begins with', + hintText: 'r', + }, + hint2: { + title: 'Type of', + hintText: 'animal', + }, + answer: 'rhinoceros', + }, + { + expire: '2026-01-26T23:59:59+00:00', + question: 'What has a foot but no legs?', + hint1: { + title: 'Begins with', + hintText: 'r', + }, + hint2: { + title: 'Type of', + hintText: 'measure', + }, + answer: 'ruler', + }, + { + expire: '2026-01-27T23:59:59+00:00', + question: 'What has a head but no brain?', + hint1: { + title: 'Begins with', + hintText: 'p', + }, + hint2: { + title: 'Type of', + hintText: 'object', + }, + answer: 'pin', + }, + { + expire: '2026-01-28T23:59:59+00:00', + question: 'What has a shell but no pearl?', + hint1: { + title: 'Begins with', + hintText: 'w', + }, + hint2: { + title: 'Type of', + hintText: 'food', + }, + answer: 'walnut', + }, + { + expire: '2026-01-29T23:59:59+00:00', + question: + 'What has a key but no lock, space but no room, and you can enter but not go in?', + hint1: { + title: 'Begins with', + hintText: 'k', + }, + hint2: { + title: 'Type of', + hintText: 'device', + }, + answer: 'keyboard', + }, + { + expire: '2026-01-30T23:59:59+00:00', + question: 'What has a face but no eyes, mouth, or nose?', + hint1: { + title: 'Begins with', + hintText: 'c', + }, + hint2: { + title: 'Type of', + hintText: 'object', + }, + answer: 'clock', + }, + { + expire: '2026-01-31T23:59:59+00:00', + question: + 'I\u2019m surrounded by water, but I never drink. I can swim for miles, but I never breathe. I have only one eye, but I never blink. What am I?', + hint1: { + title: 'Begins with', + hintText: 's', + }, + hint2: { + title: 'Type of', + hintText: 'vehicle', + }, + answer: 'submarine', + }, + { + expire: '2026-02-01T23:59:59+00:00', + question: + 'What has roots that nobody sees, is taller than trees, up, up it goes, and yet never grows?', + hint1: { + title: 'Begins with', + hintText: 'm', + }, + hint2: { + title: 'Type of', + hintText: 'geography', + }, + answer: 'mountain', + }, + { + expire: '2026-02-02T23:59:59+00:00', + question: + 'I can fly without wings. I can cry without eyes. Wherever I go, darkness follows me. What am I?', + hint1: { + title: 'Begins with', + hintText: 'c', + }, + hint2: { + title: 'Type of', + hintText: 'weather', + }, + answer: 'cloud', + }, +]; diff --git a/src/app/components/Riddle/index.stories.tsx b/src/app/components/Riddle/index.stories.tsx new file mode 100644 index 00000000000..223396e2e2a --- /dev/null +++ b/src/app/components/Riddle/index.stories.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import Riddle from '.'; +import ReadMeter from './Components/ReadMeter'; + +const Component = () => ( + +); + +export default { + title: 'Components/MyRiddleGame', + Component, +}; + +export const Example = () => ( + <> + + + +) \ No newline at end of file diff --git a/src/app/components/Riddle/index.tsx b/src/app/components/Riddle/index.tsx new file mode 100644 index 00000000000..ae0e6dabaac --- /dev/null +++ b/src/app/components/Riddle/index.tsx @@ -0,0 +1,23 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/react'; +import onClient from '#app/lib/utilities/onClient'; +import Card from './Components/Card'; +import RiddleProvider from './RiddleProvider'; +import DevControlPanel from './Components/DevControlPanel'; +import Placeholder from './Components/Placeholder'; + +export type CachedGameData = { + goes: number; + credits: number; +}; + +export default () => { + const Game = ( + + + + + ); + + return onClient() ? Game : ; +}; diff --git a/src/app/components/Riddle/metadata.json b/src/app/components/Riddle/metadata.json new file mode 100644 index 00000000000..3b99a8e0082 --- /dev/null +++ b/src/app/components/Riddle/metadata.json @@ -0,0 +1,29 @@ +{ + "alpha": true, + "lastUpdated": { + "day": 23, + "month": "Jul", + "year": 2025 + }, + "uxAccessibilityDoc": { + "done": false, + "reference": { + "url": "https://www.figma.com/design/xIOunUThBXwKijvaF5HS7X/Read-Time-designs?node-id=446-34909&p=f&t=wOPIXu3CST8CMuDp-0", + "label": "Screen Reader UX" + } + }, + "acceptanceCriteria": { + "done": false, + "reference": { + "url": "https://paper.dropbox.com/doc/Read-Time-for-Homepage-OJs-Experiment--CtZgjG_Huelwx4b87JzFBgRaAg-padKUYgd2fRw3zcS0ag3d", + "label": "Accessibility Acceptance Criteria" + } + }, + "swarm": { + "done": false, + "reference": { + "url": "https://paper.dropbox.com/doc/A11Y-Swarm-Article-Read-Time-Homepage-Experiment--CtYNO6jbnVAYw7CkKv4HCr2NAg-zLTlvrNpD89QQl3UzAWsj", + "label": "Accessibility Swarm Notes" + } + } +} diff --git a/src/app/pages/ArticlePage/ArticlePage.tsx b/src/app/pages/ArticlePage/ArticlePage.tsx index 050ef04bf8f..fbc37166052 100644 --- a/src/app/pages/ArticlePage/ArticlePage.tsx +++ b/src/app/pages/ArticlePage/ArticlePage.tsx @@ -49,6 +49,9 @@ import { Recommendation } from '#app/models/types/onwardJourney'; import ScrollablePromo from '#components/ScrollablePromo'; import Recommendations from '#app/components/Recommendations'; import { ReadTimeArticleExperiment as ReadTime } from '#app/components/ReadTime'; +import ReadMeter from '#app/components/Riddle/Components/ReadMeter'; +import Riddle from '#app/components/Riddle'; +import LocalStorageProvider from '#app/components/Riddle/LocalStorageProvider'; import ElectionBanner from './ElectionBanner'; import ImageWithCaption from '../../components/ImageWithCaption'; import AdContainer from '../../components/Ad'; @@ -248,7 +251,7 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { const allowAdvertising = pageData?.metadata?.allowAdvertising ?? false; const adcampaign = pageData?.metadata?.adCampaignKeyword; - + const wordCount = pageData?.metadata?.stats?.wordCount; const { metadata: { atiAnalytics }, mostRead: mostReadInitialData, @@ -390,116 +393,120 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { const authors = bylineLinkedData?.map(data => data?.authorName).join(','); return ( -
- - - - - - - {allowAdvertising && ( - - )} - -
-
-
- +
+ + + + + + + {allowAdvertising && ( + + )} + +
+
+ +
+ + + +
+ + {showTopics && ( + + )} + - -
- - {showTopics && ( - + {!isApp && !isPGL && ( + )} - + {!isApp && !isPGL && ( + -
- {!isApp && !isPGL && ( - )}
- {!isApp && !isPGL && ( - - )} -
+ ); }; diff --git a/src/app/pages/HomePage/HomePage.tsx b/src/app/pages/HomePage/HomePage.tsx index eb9d6a0f0ff..9f2848e48f1 100644 --- a/src/app/pages/HomePage/HomePage.tsx +++ b/src/app/pages/HomePage/HomePage.tsx @@ -7,6 +7,7 @@ import useOptimizelyVariation, { ExperimentType, } from '#app/hooks/useOptimizelyVariation'; import OptimizelyPageMetrics from '#app/components/OptimizelyPageMetrics'; +import Riddle from '#app/components/Riddle'; import ATIAnalytics from '../../components/ATIAnalytics'; import { Curation, @@ -112,6 +113,12 @@ const HomePage = ({ pageData }: HomePageProps) => {
+
+
+ +
+
+ {curations.map( ( { diff --git a/src/app/pages/HomePage/index.styles.tsx b/src/app/pages/HomePage/index.styles.tsx index 554756659d6..449e35e9495 100644 --- a/src/app/pages/HomePage/index.styles.tsx +++ b/src/app/pages/HomePage/index.styles.tsx @@ -31,6 +31,16 @@ const styles = { margin: `${spacings.QUINTUPLE}rem 0`, }, }), + riddleMaxWidth: () => + css({ + flexGrow: 1, + maxWidth: '800px', + }), + riddleContainer: () => + css({ + display: 'flex', + justifyContent: 'center', + }), }; export default styles;