From 58d998ce4e8c67709baa29012d66086047043fa7 Mon Sep 17 00:00:00 2001 From: Shayne Ahchoon Date: Fri, 28 Nov 2025 09:58:40 +0000 Subject: [PATCH 01/17] WS-HACK: Initial --- src/app/components/Riddle/README.md | 5 + src/app/components/Riddle/index.stories.tsx | 13 ++ src/app/components/Riddle/index.styles.ts | 123 ++++++++++++++++++ src/app/components/Riddle/index.tsx | 131 ++++++++++++++++++++ src/app/components/Riddle/metadata.json | 29 +++++ 5 files changed, 301 insertions(+) create mode 100644 src/app/components/Riddle/README.md create mode 100644 src/app/components/Riddle/index.stories.tsx create mode 100644 src/app/components/Riddle/index.styles.ts create mode 100644 src/app/components/Riddle/index.tsx create mode 100644 src/app/components/Riddle/metadata.json 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/index.stories.tsx b/src/app/components/Riddle/index.stories.tsx new file mode 100644 index 00000000000..9ffacbd58e6 --- /dev/null +++ b/src/app/components/Riddle/index.stories.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import Riddle from '.'; + +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.styles.ts b/src/app/components/Riddle/index.styles.ts new file mode 100644 index 00000000000..97d91902251 --- /dev/null +++ b/src/app/components/Riddle/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', + }, + }), + playArea: ({ spacings, palette }: Theme) => + css({ + color: palette.GHOST, + background: palette.POSTBOX, + flex: 1, + padding: `${spacings.FULL}rem`, + }), + question: ({ palette }: Theme) => + css({ + background: palette.GHOST, + }), + 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: ({ palette, spacings }: Theme) => + css({ + margin: `${spacings.TRIPLE}rem 0 ${2.5}rem`, + }), + hintContainer: ({ spacings, palette }: Theme) => + css({ + cursor: 'pointer', + margin: `${spacings.HALF}rem 0`, + }), + hintSummary: ({ palette }: Theme) => + css({ + listStyle: 'none', + display: 'flex', + }), + hintSummaryText: ({ palette, spacings }: Theme) => + css({ + flexGrow: 1, + padding: `${0.6}rem ${spacings.FULL}rem`, + background: palette.GHOST, + }), + + hintPrice: ({ palette }: Theme) => + css({ + width: `${3.25}rem`, + padding: `${0.6}rem ${1}rem`, + background: palette.GREY_3, + textAlign: 'center', + }), + hint: ({ palette }: Theme) => css({}), + detailsArea: ({ spacings, mq, palette }: Theme) => + css({ + padding: `${spacings.FULL}rem`, + minWidth: `${pixelsToRem(200)}rem`, + backgroundColor: `${palette.GREY_2}`, + [mq.GROUP_2_MAX_WIDTH]: { + minWidth: '100%', + }, + }), + detail: ({ palette }: Theme) => css({}), +}; diff --git a/src/app/components/Riddle/index.tsx b/src/app/components/Riddle/index.tsx new file mode 100644 index 00000000000..f3d7230674e --- /dev/null +++ b/src/app/components/Riddle/index.tsx @@ -0,0 +1,131 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/react'; +import style from './index.styles'; +import Text from '../Text'; + +export type HintData = { + title: string; + hintText: string; +}; + +export type GameData = { + utcExpire: string; + question: string; + hint1: HintData; + hint2: HintData; + answer: string; +}; + +export type CachedGameData = { + goes: number; + credits: number; +}; + +export default () => { + const gameData = { + utcExpire: '500-25-26', + 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 an...', + hintText: 'Begins with an s', + }, + hint2: { + title: '', + hintText: 'Begins with an s', + }, + answer: 'submarine', + } as GameData; + + const { question, hint1, hint2, answer } = gameData; + + return ( +
+
+ + {question} + +
+
+ + + C 100 + + + {hint1.title.length > 0 ? hint1.title : 'Hint 1'} + + + + {hint1.hintText} + +
+
+ + + C 350 + + + {hint2.title.length > 0 ? hint2.title.length : 'Hint 2'} + + + + {hint2.hintText} + +
+
+ + + C 2500 + + + Answer + + + + {answer} + +
+
+
+
+ +
+
+ +
+
+
+
+ Expires in: + + + +
+
+ Goes: + 4/5 +
+
+ Credits: + C 700 +
+
+
+ ); +}; 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" + } + } +} From abdd24b199349c756790f725a7a7d2e4c9f16c99 Mon Sep 17 00:00:00 2001 From: Shayne Ahchoon Date: Mon, 1 Dec 2025 10:00:21 +0000 Subject: [PATCH 02/17] WS-HACK: Initial --- src/app/components/Riddle/index.styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/Riddle/index.styles.ts b/src/app/components/Riddle/index.styles.ts index 97d91902251..022c38e1e96 100644 --- a/src/app/components/Riddle/index.styles.ts +++ b/src/app/components/Riddle/index.styles.ts @@ -104,7 +104,7 @@ export default { hintPrice: ({ palette }: Theme) => css({ - width: `${3.25}rem`, + width: `${3}rem`, padding: `${0.6}rem ${1}rem`, background: palette.GREY_3, textAlign: 'center', From 094f158900f8fcd8d95fb8c09423f8057e7111f3 Mon Sep 17 00:00:00 2001 From: Shayne Ahchoon Date: Wed, 3 Dec 2025 10:25:20 +0000 Subject: [PATCH 03/17] ELECTION-285: Add BDD test for welsh scoreboard. --- src/app/components/Riddle/data.ts | 30 +++++++++++++++++++++++ src/app/components/Riddle/index.styles.ts | 14 ++++++++--- src/app/components/Riddle/index.tsx | 6 ++--- 3 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 src/app/components/Riddle/data.ts diff --git a/src/app/components/Riddle/data.ts b/src/app/components/Riddle/data.ts new file mode 100644 index 00000000000..77e02139875 --- /dev/null +++ b/src/app/components/Riddle/data.ts @@ -0,0 +1,30 @@ +export default [ + { + utcExpire: '500-25-26', + 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 an...', + hintText: 'Begins with an s', + }, + hint2: { + title: '', + hintText: 'Begins with an s', + }, + answer: 'submarine', + }, + { + utcExpire: '500-25-26', + 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 an...', + hintText: 'Begins with an s', + }, + hint2: { + title: '', + hintText: 'Begins with an s', + }, + answer: 'submarine', + }, +]; diff --git a/src/app/components/Riddle/index.styles.ts b/src/app/components/Riddle/index.styles.ts index 022c38e1e96..ed0843a0dff 100644 --- a/src/app/components/Riddle/index.styles.ts +++ b/src/app/components/Riddle/index.styles.ts @@ -87,6 +87,7 @@ export default { }), hintContainer: ({ spacings, palette }: Theme) => css({ + position: 'relative', cursor: 'pointer', margin: `${spacings.HALF}rem 0`, }), @@ -101,15 +102,22 @@ export default { padding: `${0.6}rem ${spacings.FULL}rem`, background: palette.GHOST, }), - hintPrice: ({ palette }: Theme) => css({ - width: `${3}rem`, + width: `${3.5}rem`, padding: `${0.6}rem ${1}rem`, background: palette.GREY_3, textAlign: 'center', }), - hint: ({ palette }: Theme) => css({}), + // hintAnswerContainer: ({ palette, spacings }: Theme) => css({}), + hintAnswerText: ({ palette, spacings }: Theme) => + css({ + position: 'absolute', + top: 0, + right: 0, + left: `${5.5}rem`, + padding: `${0.6}rem ${spacings.FULL}rem`, + }), detailsArea: ({ spacings, mq, palette }: Theme) => css({ padding: `${spacings.FULL}rem`, diff --git a/src/app/components/Riddle/index.tsx b/src/app/components/Riddle/index.tsx index f3d7230674e..d696b6d8570 100644 --- a/src/app/components/Riddle/index.tsx +++ b/src/app/components/Riddle/index.tsx @@ -59,7 +59,7 @@ export default () => { {hint1.title.length > 0 ? hint1.title : 'Hint 1'} - + {hint1.hintText} @@ -76,7 +76,7 @@ export default () => { {hint2.title.length > 0 ? hint2.title.length : 'Hint 2'} - + {hint2.hintText} @@ -93,7 +93,7 @@ export default () => { Answer - + {answer} From 732119ed51c9340f5962e14ad4eb60dbbd6aa62c Mon Sep 17 00:00:00 2001 From: Shayne Ahchoon Date: Wed, 3 Dec 2025 16:57:22 +0000 Subject: [PATCH 04/17] HACK-25: Add style --- src/app/components/Riddle/index.styles.ts | 4 ++ src/app/components/Riddle/index.tsx | 73 +++++++---------------- 2 files changed, 26 insertions(+), 51 deletions(-) diff --git a/src/app/components/Riddle/index.styles.ts b/src/app/components/Riddle/index.styles.ts index ed0843a0dff..ac062eee3aa 100644 --- a/src/app/components/Riddle/index.styles.ts +++ b/src/app/components/Riddle/index.styles.ts @@ -101,6 +101,9 @@ export default { flexGrow: 1, padding: `${0.6}rem ${spacings.FULL}rem`, background: palette.GHOST, + 'details:open &': { + display: 'none', + }, }), hintPrice: ({ palette }: Theme) => css({ @@ -117,6 +120,7 @@ export default { right: 0, left: `${5.5}rem`, padding: `${0.6}rem ${spacings.FULL}rem`, + background: palette.CHALK, }), detailsArea: ({ spacings, mq, palette }: Theme) => css({ diff --git a/src/app/components/Riddle/index.tsx b/src/app/components/Riddle/index.tsx index d696b6d8570..f3d3e03763f 100644 --- a/src/app/components/Riddle/index.tsx +++ b/src/app/components/Riddle/index.tsx @@ -6,6 +6,7 @@ import Text from '../Text'; export type HintData = { title: string; hintText: string; + price: number; }; export type GameData = { @@ -21,6 +22,24 @@ export type CachedGameData = { credits: number; }; +const Hint = ({ title, hintText, price = 250 }: HintData) => { + return ( +
+ + + C{price} + + + {title.length > 0 ? title : 'Hint'} + + + + {hintText} + +
+ ); +}; + export default () => { const gameData = { utcExpire: '500-25-26', @@ -46,57 +65,9 @@ export default () => { {question}
-
- - - C 100 - - - {hint1.title.length > 0 ? hint1.title : 'Hint 1'} - - - - {hint1.hintText} - -
-
- - - C 350 - - - {hint2.title.length > 0 ? hint2.title.length : 'Hint 2'} - - - - {hint2.hintText} - -
-
- - - C 2500 - - - Answer - - - - {answer} - -
+ + +
From 0f95cc3db4691377a69be7e67965973cfe76dccf Mon Sep 17 00:00:00 2001 From: Shayne Ahchoon Date: Wed, 10 Dec 2025 16:49:04 +0000 Subject: [PATCH 05/17] HACK-25: Add style --- .../{ => Components/Card}/index.styles.ts | 43 +- .../Riddle/Components/Card/index.tsx | 106 +++ .../Riddle/Components/Detail/index.styles.ts | 34 + .../Riddle/Components/Detail/index.tsx | 25 + .../DevControlPanel/index.styles.ts | 38 + .../Components/DevControlPanel/index.tsx | 46 ++ .../Riddle/Components/Hint/index.styles.ts | 65 ++ .../Riddle/Components/Hint/index.tsx | 49 ++ .../Riddle/Components/ReadMeter.tsx | 28 + .../Riddle/LocalStorageProvider/index.tsx | 100 +++ .../Riddle/RiddleProvider copy/index.tsx | 52 ++ .../Riddle/RiddleProvider/index.tsx | 121 ++++ src/app/components/Riddle/data.ts | 662 +++++++++++++++++- src/app/components/Riddle/index.stories.tsx | 10 +- src/app/components/Riddle/index.tsx | 97 +-- 15 files changed, 1339 insertions(+), 137 deletions(-) rename src/app/components/Riddle/{ => Components/Card}/index.styles.ts (70%) create mode 100644 src/app/components/Riddle/Components/Card/index.tsx create mode 100644 src/app/components/Riddle/Components/Detail/index.styles.ts create mode 100644 src/app/components/Riddle/Components/Detail/index.tsx create mode 100644 src/app/components/Riddle/Components/DevControlPanel/index.styles.ts create mode 100644 src/app/components/Riddle/Components/DevControlPanel/index.tsx create mode 100644 src/app/components/Riddle/Components/Hint/index.styles.ts create mode 100644 src/app/components/Riddle/Components/Hint/index.tsx create mode 100644 src/app/components/Riddle/Components/ReadMeter.tsx create mode 100644 src/app/components/Riddle/LocalStorageProvider/index.tsx create mode 100644 src/app/components/Riddle/RiddleProvider copy/index.tsx create mode 100644 src/app/components/Riddle/RiddleProvider/index.tsx diff --git a/src/app/components/Riddle/index.styles.ts b/src/app/components/Riddle/Components/Card/index.styles.ts similarity index 70% rename from src/app/components/Riddle/index.styles.ts rename to src/app/components/Riddle/Components/Card/index.styles.ts index ac062eee3aa..3afe199bfcd 100644 --- a/src/app/components/Riddle/index.styles.ts +++ b/src/app/components/Riddle/Components/Card/index.styles.ts @@ -81,55 +81,20 @@ export default { }, }, }), - hintsArea: ({ palette, spacings }: Theme) => + hintsArea: ({ spacings }: Theme) => css({ margin: `${spacings.TRIPLE}rem 0 ${2.5}rem`, }), - hintContainer: ({ spacings, palette }: Theme) => - css({ - position: 'relative', - cursor: 'pointer', - margin: `${spacings.HALF}rem 0`, - }), - hintSummary: ({ palette }: Theme) => - css({ - listStyle: 'none', - display: 'flex', - }), - 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: `${3.5}rem`, - padding: `${0.6}rem ${1}rem`, - background: palette.GREY_3, - textAlign: 'center', - }), - // hintAnswerContainer: ({ palette, spacings }: Theme) => css({}), - hintAnswerText: ({ palette, spacings }: Theme) => - css({ - position: 'absolute', - top: 0, - right: 0, - left: `${5.5}rem`, - padding: `${0.6}rem ${spacings.FULL}rem`, - background: palette.CHALK, - }), 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', }, }), - detail: ({ palette }: Theme) => css({}), }; 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..3f64ff5f48a --- /dev/null +++ b/src/app/components/Riddle/Components/Card/index.tsx @@ -0,0 +1,106 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/react'; +import { useMemo, useState, useEffect, use } from 'react'; +import Text from '../../../Text'; +import style from './index.styles'; +import Hint, { HintData } from '../Hint'; +import Detail from '../Detail'; +import { RiddleContext } from '../../RiddleProvider'; +import { LocalStorageContext } from '../../LocalStorageProvider'; + +export type GameData = { + expire: string; + question: string; + hint1: HintData; + hint2: HintData; + answer: 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]; +}; + +export default () => { + const { gameData, devTime } = use(RiddleContext); + const { goes, coins } = use(LocalStorageContext); + const { question, hint1, hint2, answer, expire } = gameData; + + 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]); + + 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}`; + + return ( +
+
+ + {question} + +
+ + + +
+
+
+ +
+
+ +
+
+
+ + + +
+
+ ); +}; 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..874ec04c552 --- /dev/null +++ b/src/app/components/Riddle/Components/DevControlPanel/index.styles.ts @@ -0,0 +1,38 @@ +import { css, Theme } from '@emotion/react'; + +export default { + title: ({ spacings }: Theme) => + css({ + display: 'inline-block', + marginTop: `${spacings.DOUBLE}rem`, + padding: `${spacings.HALF}rem`, + }), + 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}`, + }, + }), +}; 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..1bff9408bb1 --- /dev/null +++ b/src/app/components/Riddle/Components/DevControlPanel/index.tsx @@ -0,0 +1,46 @@ +/** @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 { forceTimeInc24: forceTimeInc, forceTimeDec24: forceTimeDec } = + use(RiddleContext); + + return ( + <> + + Developer Tools + +
+ + +
+ + ); +}; 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..ebc762d977a --- /dev/null +++ b/src/app/components/Riddle/Components/Hint/index.styles.ts @@ -0,0 +1,65 @@ +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.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', + display: 'none', + 'details:open &': { + display: 'block', + }, + }), + 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..0b438c7443e --- /dev/null +++ b/src/app/components/Riddle/Components/Hint/index.tsx @@ -0,0 +1,49 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/react'; +import { useState } from 'react'; +import Text from '../../../Text'; +import style from './index.styles'; + +export type HintData = { + title: string; + hintText: string; + price?: number; + boughtPrefix?: string; +}; + +export default ({ + title, + hintText, + boughtPrefix = 'Hint', + price = 250, +}: HintData) => { + const [paidStatus, setPaidStatus] = useState(false); + const priceText = `🪙 ${price}`; + return ( +
+ { + setPaidStatus(() => true); + element.currentTarget.blur(); + }} + {...(paidStatus && { tabIndex: -1 })} + > + + {paidStatus ? boughtPrefix : priceText} + + + {title.length > 0 ? title : 'Hint'} + +
+ + Paid + +
+
+ + {hintText} + +
+ ); +}; diff --git a/src/app/components/Riddle/Components/ReadMeter.tsx b/src/app/components/Riddle/Components/ReadMeter.tsx new file mode 100644 index 00000000000..c26b05ba998 --- /dev/null +++ b/src/app/components/Riddle/Components/ReadMeter.tsx @@ -0,0 +1,28 @@ +/** @jsx jsx */ +import { use, useEffect } from 'react'; +import { jsx } from '@emotion/react'; +import Text from '../../Text'; +import style from './index.styles'; +import { LocalStorageContext } from '../LocalStorageProvider'; + +export default () => { + const { addCoins } = use(LocalStorageContext); + + useEffect(() => { + const listener = (event: Event) => { + console.log('CHECK THIS EVENT', event); + addCoins(10); + }; + console.log('CHECK'); + document.addEventListener('scroll', listener); + return () => { + document.removeEventListener('scroll', listener); + }; + }, []); + + return ( +
+ READ METER: +
+ ); +}; diff --git a/src/app/components/Riddle/LocalStorageProvider/index.tsx b/src/app/components/Riddle/LocalStorageProvider/index.tsx new file mode 100644 index 00000000000..5d802d7a25b --- /dev/null +++ b/src/app/components/Riddle/LocalStorageProvider/index.tsx @@ -0,0 +1,100 @@ +import React, { + createContext, + PropsWithChildren, + useMemo, + useState, +} from 'react'; + +export type LocalStorage = { + goes: number; + coins: number; + paidFor: boolean[]; + addCoins: (amount: number) => void; + reduceCoins: (amount: number) => void; + reduceGoes: () => void; + updatePaidFor: React.Dispatch>; +}; + +export const LocalStorageContext = createContext( + {} as LocalStorage, +); + +const DATA_KEY = 'ws_bbc_riddle'; + +const getLocalData = () => { + const localStorageData = window.localStorage[DATA_KEY]; + if (localStorageData) { + const parsedData = JSON.parse(localStorageData); + return { + goes: parsedData.goes ?? 5, + coins: parsedData.coins ?? 0, + }; + } + + return { + goes: 5, + coins: 0, + }; +}; + +const setLocalData = ({ goes, coins }: { goes?: number; coins?: number }) => { + const { goes: localGoes, coins: localCoins } = getLocalData(); + const updatedData = { + goes: goes ?? localGoes, + coins: coins ?? localCoins, + }; + const toStore = JSON.stringify(updatedData); + window.localStorage.setItem(DATA_KEY, toStore); +}; + +export default ({ children }: PropsWithChildren) => { + const { goes: initialGoes, coins: initialCoins } = getLocalData(); + + const [coins, updateCoins] = useState(initialCoins); + const [goes, updateGoes] = useState(initialGoes); + const [paidFor, updatePaidFor] = useState([false, false, false]); + + const addCoins = (amount: number) => { + updateCoins(prevAmount => { + const newAmount = prevAmount + amount; + setLocalData({ coins: newAmount }); + return newAmount; + }); + }; + const reduceCoins = (amount: number) => { + updateCoins(prevAmount => { + const newAmount = prevAmount - amount; + setLocalData({ coins: newAmount }); + return newAmount; + }); + }; + const reduceGoes = () => { + updateGoes(prevGoes => { + const newGoes = prevGoes - 1; + setLocalData({ goes: newGoes }); + return newGoes; + }); + }; + + // TO DO: Reset goes during transition period. + // TO DO: Reset paid for. + + const value = useMemo( + () => ({ + coins, + goes, + addCoins, + reduceCoins, + reduceGoes, + paidFor, + updatePaidFor, + }), + [coins, goes, paidFor], + ); + + return ( + + {children} + + ); +}; diff --git a/src/app/components/Riddle/RiddleProvider copy/index.tsx b/src/app/components/Riddle/RiddleProvider copy/index.tsx new file mode 100644 index 00000000000..d8dcfcac0a8 --- /dev/null +++ b/src/app/components/Riddle/RiddleProvider copy/index.tsx @@ -0,0 +1,52 @@ +import React, { + createContext, + Dispatch, + PropsWithChildren, + SetStateAction, + useMemo, + useState, +} from 'react'; +import { GameData } from '../Components/Card'; +import data from '../data'; + +export enum GameState { + PLAY, + CLOSED, + FAILED, + WINNER, +} + +export type RiddleGameState = { + gameState: GameState; + gameData: GameData; + updateGameState: Dispatch>; + nextGame: () => void; + prevGame: () => void; +}; + +export const RiddleContext = createContext( + {} as RiddleGameState, +); + +export default ({ children }: PropsWithChildren) => { + const [gameState, updateGameState] = useState(GameState.PLAY); + const [gameIndex, updateIndex] = useState(0); + + const nextGame = () => { + updateIndex(0); + }; + const prevGame = () => { + updateIndex(0); + }; + + const gameData = data[gameIndex]; + + const value = useMemo( + () => ({ gameState, updateGameState, nextGame, prevGame, gameData }), + [gameData, gameState], + ); + + return ( + {children} + ); +}; diff --git a/src/app/components/Riddle/RiddleProvider/index.tsx b/src/app/components/Riddle/RiddleProvider/index.tsx new file mode 100644 index 00000000000..b9e7fe807e7 --- /dev/null +++ b/src/app/components/Riddle/RiddleProvider/index.tsx @@ -0,0 +1,121 @@ +import React, { + createContext, + Dispatch, + PropsWithChildren, + SetStateAction, + useEffect, + useMemo, + useState, +} from 'react'; +import { GameData } from '../Components/Card'; +import data from '../data'; + +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', +} as GameData; + +export enum GameState { + PLAY, + CLOSED, + FAILED, + WINNER, +} + +export type RiddleGameState = { + gameState: GameState; + gameData: GameData; + updateGameState: Dispatch>; + devTime: Date; + forceTimeInc24: () => void; + forceTimeDec24: () => 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 [devTime, updateDevTime] = useState(new Date()); + const initialIndex = findCurrGameIndex(); + const [gameIndex, updateIndex] = useState(initialIndex); + const gameData = gameIndex > -1 ? data[gameIndex] : defaultGameData; + const initialGameState = gameIndex > -1 ? GameState.PLAY : GameState.CLOSED; + 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); + updateIndex(nextGameIndex); + } + }, 1000); + + return () => clearInterval(timer); + }, [devTime, gameData.expire, gameState]); + + 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); + + updateIndex(nextGameIndex); + updateDevTime(updatedDate); + }; + + return { + gameState, + updateGameState, + forceTimeInc24, + forceTimeDec24, + gameData, + devTime, + }; + }, [devTime, gameData, gameState]); + + return ( + {children} + ); +}; diff --git a/src/app/components/Riddle/data.ts b/src/app/components/Riddle/data.ts index 77e02139875..84dfdf69130 100644 --- a/src/app/components/Riddle/data.ts +++ b/src/app/components/Riddle/data.ts @@ -1,6 +1,6 @@ export default [ { - utcExpire: '500-25-26', + 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: { @@ -13,18 +13,666 @@ export default [ }, answer: 'submarine', }, + { - utcExpire: '500-25-26', + 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'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?", + 'I\u2019m tall when I\u2019m young, and I\u2019m short when I\u2019m old. What am I?', hint1: { - title: 'Begins with an...', - hintText: 'Begins with an s', + title: 'Begins with', + hintText: 'c', }, hint2: { - title: '', - hintText: 'Begins with an s', + 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 index 9ffacbd58e6..379069d8eb1 100644 --- a/src/app/components/Riddle/index.stories.tsx +++ b/src/app/components/Riddle/index.stories.tsx @@ -1,5 +1,8 @@ import React from 'react'; import Riddle from '.'; +import LocalStorageProvider from './LocalStorageProvider'; +import ReadMeter from './Components/ReadMeter'; +import DevControlPanel from './Components/DevControlPanel'; const Component = () => ( @@ -10,4 +13,9 @@ export default { Component, }; -export const Example = () => \ No newline at end of file +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 index f3d3e03763f..01a09c63290 100644 --- a/src/app/components/Riddle/index.tsx +++ b/src/app/components/Riddle/index.tsx @@ -1,102 +1,19 @@ /** @jsx jsx */ import { jsx } from '@emotion/react'; -import style from './index.styles'; -import Text from '../Text'; - -export type HintData = { - title: string; - hintText: string; - price: number; -}; - -export type GameData = { - utcExpire: string; - question: string; - hint1: HintData; - hint2: HintData; - answer: string; -}; +import Card from './Components/Card'; +import RiddleProvider from './RiddleProvider'; +import DevControlPanel from './Components/DevControlPanel'; export type CachedGameData = { goes: number; credits: number; }; -const Hint = ({ title, hintText, price = 250 }: HintData) => { - return ( -
- - - C{price} - - - {title.length > 0 ? title : 'Hint'} - - - - {hintText} - -
- ); -}; - export default () => { - const gameData = { - utcExpire: '500-25-26', - 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 an...', - hintText: 'Begins with an s', - }, - hint2: { - title: '', - hintText: 'Begins with an s', - }, - answer: 'submarine', - } as GameData; - - const { question, hint1, hint2, answer } = gameData; - return ( -
-
- - {question} - -
- - - -
-
-
- -
-
- -
-
-
-
- Expires in: - - - -
-
- Goes: - 4/5 -
-
- Credits: - C 700 -
-
-
+ + + + ); }; From 632ccdc603d5d8a88e6250b2bf67af9c6a1a967f Mon Sep 17 00:00:00 2001 From: Shayne Ahchoon Date: Thu, 11 Dec 2025 10:22:21 +0000 Subject: [PATCH 06/17] HACK-25: Add style --- .../Riddle/Components/DevControlPanel/index.styles.ts | 1 + .../Riddle/Components/DevControlPanel/index.tsx | 10 +++++++++- .../components/Riddle/LocalStorageProvider/index.tsx | 9 +++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/app/components/Riddle/Components/DevControlPanel/index.styles.ts b/src/app/components/Riddle/Components/DevControlPanel/index.styles.ts index 874ec04c552..acf665bbcab 100644 --- a/src/app/components/Riddle/Components/DevControlPanel/index.styles.ts +++ b/src/app/components/Riddle/Components/DevControlPanel/index.styles.ts @@ -33,6 +33,7 @@ export default { 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 index 1bff9408bb1..e434088033a 100644 --- a/src/app/components/Riddle/Components/DevControlPanel/index.tsx +++ b/src/app/components/Riddle/Components/DevControlPanel/index.tsx @@ -17,7 +17,7 @@ const Option = ({ title, children }: PropsWithChildren<{ title: string }>) => { }; export default () => { - const { addCoins } = use(LocalStorageContext); + const { addCoins, addGoes } = use(LocalStorageContext); const { forceTimeInc24: forceTimeInc, forceTimeDec24: forceTimeDec } = use(RiddleContext); @@ -39,6 +39,14 @@ export default () => { +
diff --git a/src/app/components/Riddle/LocalStorageProvider/index.tsx b/src/app/components/Riddle/LocalStorageProvider/index.tsx index 5d802d7a25b..daac216148d 100644 --- a/src/app/components/Riddle/LocalStorageProvider/index.tsx +++ b/src/app/components/Riddle/LocalStorageProvider/index.tsx @@ -10,6 +10,7 @@ export type LocalStorage = { coins: number; paidFor: boolean[]; addCoins: (amount: number) => void; + addGoes: () => void; reduceCoins: (amount: number) => void; reduceGoes: () => void; updatePaidFor: React.Dispatch>; @@ -61,6 +62,13 @@ export default ({ children }: PropsWithChildren) => { return newAmount; }); }; + const addGoes = () => { + updateGoes(() => { + const newAmount = 5; + setLocalData({ goes: newAmount }); + return newAmount; + }); + }; const reduceCoins = (amount: number) => { updateCoins(prevAmount => { const newAmount = prevAmount - amount; @@ -84,6 +92,7 @@ export default ({ children }: PropsWithChildren) => { coins, goes, addCoins, + addGoes, reduceCoins, reduceGoes, paidFor, From 95697372123f1164014004fdafe74c0f5ec04cf8 Mon Sep 17 00:00:00 2001 From: Shayne Ahchoon Date: Thu, 11 Dec 2025 16:25:55 +0000 Subject: [PATCH 07/17] HACK-25: Add style --- .../DevControlPanel/index.styles.ts | 8 ++- .../Components/DevControlPanel/index.tsx | 12 +++-- .../Riddle/LocalStorageProvider/index.tsx | 50 +++++++++++++++---- 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/app/components/Riddle/Components/DevControlPanel/index.styles.ts b/src/app/components/Riddle/Components/DevControlPanel/index.styles.ts index acf665bbcab..ccdf4d7d501 100644 --- a/src/app/components/Riddle/Components/DevControlPanel/index.styles.ts +++ b/src/app/components/Riddle/Components/DevControlPanel/index.styles.ts @@ -3,9 +3,13 @@ import { css, Theme } from '@emotion/react'; export default { title: ({ spacings }: Theme) => css({ - display: 'inline-block', + display: 'block', marginTop: `${spacings.DOUBLE}rem`, - padding: `${spacings.HALF}rem`, + }), + date: () => + css({ + fontFamily: '"Lucida Console", "Courier New", monospace', + display: 'block', }), container: () => css({ diff --git a/src/app/components/Riddle/Components/DevControlPanel/index.tsx b/src/app/components/Riddle/Components/DevControlPanel/index.tsx index e434088033a..5115d10ee04 100644 --- a/src/app/components/Riddle/Components/DevControlPanel/index.tsx +++ b/src/app/components/Riddle/Components/DevControlPanel/index.tsx @@ -18,14 +18,20 @@ const Option = ({ title, children }: PropsWithChildren<{ title: string }>) => { export default () => { const { addCoins, addGoes } = use(LocalStorageContext); - const { forceTimeInc24: forceTimeInc, forceTimeDec24: forceTimeDec } = - use(RiddleContext); + const { + devTime, + forceTimeInc24: forceTimeInc, + forceTimeDec24: forceTimeDec, + } = use(RiddleContext); return ( <> Developer Tools + + Sys Date: {devTime.toDateString()} +
+ + )}
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/Hint/index.styles.ts b/src/app/components/Riddle/Components/Hint/index.styles.ts index ebc762d977a..114fb4d5fc0 100644 --- a/src/app/components/Riddle/Components/Hint/index.styles.ts +++ b/src/app/components/Riddle/Components/Hint/index.styles.ts @@ -27,7 +27,7 @@ export default { hintSummaryText: ({ palette, spacings }: Theme) => css({ flexGrow: 1, - padding: `${0.6}rem ${spacings.FULL}rem`, + padding: `0 ${spacings.FULL}rem`, background: palette.GHOST, 'details:open &': { display: 'none', @@ -53,6 +53,14 @@ export default { display: 'block', }, }), + notEnough: () => + css({ + position: 'absolute', + top: 0, + insetInlineStart: 0, + background: 'yellow', + textAlign: 'center', + }), hintAnswerText: ({ spacings }: Theme) => css({ position: 'absolute', diff --git a/src/app/components/Riddle/Components/Hint/index.tsx b/src/app/components/Riddle/Components/Hint/index.tsx index 0b438c7443e..291d78010e4 100644 --- a/src/app/components/Riddle/Components/Hint/index.tsx +++ b/src/app/components/Riddle/Components/Hint/index.tsx @@ -1,36 +1,50 @@ /** @jsx jsx */ import { jsx } from '@emotion/react'; -import { useState } from '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; - boughtPrefix?: string; + paidSymbol?: string; }; export default ({ title, hintText, - boughtPrefix = 'Hint', + paidSymbol = 'Hint', price = 250, -}: HintData) => { - const [paidStatus, setPaidStatus] = useState(false); + 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 ( -
+
{ - setPaidStatus(() => true); - element.currentTarget.blur(); + onClick={event => { + buyHint(index, price); + setIsInvoked(true); + event.currentTarget.blur(); }} - {...(paidStatus && { tabIndex: -1 })} + {...((paidStatus || !isAffordable) && { tabIndex: -1 })} > - {paidStatus ? boughtPrefix : priceText} + {paidStatus ? paidSymbol : priceText} {title.length > 0 ? title : 'Hint'} @@ -40,6 +54,13 @@ export default ({ 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..923e5900707 --- /dev/null +++ b/src/app/components/Riddle/Components/HintButton/index.tsx @@ -0,0 +1,65 @@ +/** @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 priceText = `🪙 ${price}`; + const paidStatus = paidHints[index]; + + const isAffordable = price <= coins; + return ( +
+ +
+ ); +}; diff --git a/src/app/components/Riddle/LocalStorageProvider/index.tsx b/src/app/components/Riddle/LocalStorageProvider/index.tsx index 09e5a3a3509..8df9bd8ea05 100644 --- a/src/app/components/Riddle/LocalStorageProvider/index.tsx +++ b/src/app/components/Riddle/LocalStorageProvider/index.tsx @@ -13,7 +13,7 @@ export type LocalStorage = { addGoes: () => void; reduceCoins: (amount: number) => void; reduceGoes: () => void; - buyHint: (amount: number) => void; + buyHint: (amount: number, price: number) => void; resetHints: () => void; }; @@ -96,30 +96,41 @@ export default ({ children }: PropsWithChildren) => { return newAmount; }); }; - const reduceGoes = () => { - updateGoes(prevGoes => { - const newGoes = prevGoes - 1; - setLocalData({ goes: newGoes }); - return newGoes; - }); - }; - const buyHint = (index: number) => { - updatePaidHints(prevBoughtHints => { - const updatedBoughtHints = [...prevBoughtHints]; - updatedBoughtHints[index] = true; + + const resetHints = () => { + updatePaidHints(() => { + const updatedBoughtHints = [false, false, false]; setLocalData({ paidHints: updatedBoughtHints }); return updatedBoughtHints; }); }; - const resetHints = () => { - updatePaidHints([false, false, false]); - }; // TO DO: Reset goes during transition period. // TO DO: Reset paid for. + 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 value = useMemo( - () => ({ + return { coins, goes, addCoins, @@ -129,9 +140,8 @@ export default ({ children }: PropsWithChildren) => { paidHints, buyHint, resetHints, - }), - [coins, goes, paidHints], - ); + }; + }, [coins, goes, paidHints]); return ( diff --git a/src/app/components/Riddle/RiddleProvider copy/index.tsx b/src/app/components/Riddle/RiddleProvider copy/index.tsx deleted file mode 100644 index d8dcfcac0a8..00000000000 --- a/src/app/components/Riddle/RiddleProvider copy/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { - createContext, - Dispatch, - PropsWithChildren, - SetStateAction, - useMemo, - useState, -} from 'react'; -import { GameData } from '../Components/Card'; -import data from '../data'; - -export enum GameState { - PLAY, - CLOSED, - FAILED, - WINNER, -} - -export type RiddleGameState = { - gameState: GameState; - gameData: GameData; - updateGameState: Dispatch>; - nextGame: () => void; - prevGame: () => void; -}; - -export const RiddleContext = createContext( - {} as RiddleGameState, -); - -export default ({ children }: PropsWithChildren) => { - const [gameState, updateGameState] = useState(GameState.PLAY); - const [gameIndex, updateIndex] = useState(0); - - const nextGame = () => { - updateIndex(0); - }; - const prevGame = () => { - updateIndex(0); - }; - - const gameData = data[gameIndex]; - - const value = useMemo( - () => ({ gameState, updateGameState, nextGame, prevGame, gameData }), - [gameData, gameState], - ); - - return ( - {children} - ); -}; diff --git a/src/app/components/Riddle/RiddleProvider/index.tsx b/src/app/components/Riddle/RiddleProvider/index.tsx index b9e7fe807e7..70cf60c4658 100644 --- a/src/app/components/Riddle/RiddleProvider/index.tsx +++ b/src/app/components/Riddle/RiddleProvider/index.tsx @@ -3,12 +3,14 @@ import React, { 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', @@ -27,19 +29,21 @@ const defaultGameData = { } as GameData; export enum GameState { - PLAY, - CLOSED, - FAILED, - WINNER, + 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) => void; }; export const RiddleContext = createContext( @@ -59,6 +63,7 @@ const findCurrGameIndex = (forcedDate?: Date) => { }; export default ({ children }: PropsWithChildren) => { + const { resetHints, goes, reduceGoes, addGoes } = use(LocalStorageContext); const [devTime, updateDevTime] = useState(new Date()); const initialIndex = findCurrGameIndex(); const [gameIndex, updateIndex] = useState(initialIndex); @@ -82,12 +87,18 @@ export default ({ children }: PropsWithChildren) => { updatedTime.getTime() > expiryTime.getTime() ) { const nextGameIndex = findCurrGameIndex(updatedTime); + const updatedGameState = + nextGameIndex > -1 ? GameState.PLAY : GameState.CLOSED; + + resetHints(); updateIndex(nextGameIndex); + updateGameState(updatedGameState); + addGoes(); } }, 1000); return () => clearInterval(timer); - }, [devTime, gameData.expire, gameState]); + }, [addGoes, devTime, gameData.expire, gameState, resetHints]); const value = useMemo(() => { const forceTimeInc24 = () => { @@ -100,9 +111,29 @@ export default ({ children }: PropsWithChildren) => { const updatedDateStamp = devTime.setDate(devTime.getDate() - 1); const updatedDate = new Date(updatedDateStamp); const nextGameIndex = findCurrGameIndex(updatedDate); + const updatedGameState = + nextGameIndex > -1 ? GameState.PLAY : GameState.CLOSED; - updateIndex(nextGameIndex); updateDevTime(updatedDate); + resetHints(); + updateIndex(nextGameIndex); + updateGameState(updatedGameState); + addGoes(); + }; + + const submitAttempt = (submitString: string) => { + 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); + } + reduceGoes(); + } }; return { @@ -112,8 +143,19 @@ export default ({ children }: PropsWithChildren) => { forceTimeDec24, gameData, devTime, + gameIndex, + submitAttempt, }; - }, [devTime, gameData, gameState]); + }, [ + addGoes, + devTime, + gameData, + gameIndex, + gameState, + goes, + reduceGoes, + resetHints, + ]); return ( {children} diff --git a/src/app/components/Riddle/data.ts b/src/app/components/Riddle/data.ts index 84dfdf69130..6675b7eca85 100644 --- a/src/app/components/Riddle/data.ts +++ b/src/app/components/Riddle/data.ts @@ -4,16 +4,15 @@ export default [ 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 an...', - hintText: 'Begins with an s', + title: 'Begins with', + hintText: 's', }, hint2: { title: '', - hintText: 'Begins with an s', + 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?', From 25529f270a762cdfdaeb77541d2a2946e30727cf Mon Sep 17 00:00:00 2001 From: Shayne Ahchoon Date: Mon, 15 Dec 2025 13:03:25 +0000 Subject: [PATCH 09/17] HACK-25: Add style --- .../Riddle/Components/Card/index.styles.ts | 8 +- .../Riddle/Components/Card/index.tsx | 58 +- .../Components/DevControlPanel/index.tsx | 11 +- .../Riddle/Components/HintButton/index.tsx | 6 +- .../Riddle/LocalStorageProvider/index.tsx | 24 +- .../Riddle/RiddleProvider/index.tsx | 53 +- src/app/components/Riddle/data.ts | 661 ++++++----------- src/app/components/Riddle/data_original.js | 677 ++++++++++++++++++ 8 files changed, 1034 insertions(+), 464 deletions(-) create mode 100644 src/app/components/Riddle/data_original.js diff --git a/src/app/components/Riddle/Components/Card/index.styles.ts b/src/app/components/Riddle/Components/Card/index.styles.ts index 18f283f90eb..bdffe06310a 100644 --- a/src/app/components/Riddle/Components/Card/index.styles.ts +++ b/src/app/components/Riddle/Components/Card/index.styles.ts @@ -16,10 +16,14 @@ export default { }), playArea: ({ spacings, palette }: Theme) => css({ + flex: 1, color: palette.GHOST, background: palette.POSTBOX, padding: `${spacings.FULL}rem`, - minHeight: '23.2rem', + }), + fixedHeight: () => + css({ + minHeight: '13.5rem', }), question: ({ palette }: Theme) => css({ @@ -33,7 +37,7 @@ export default { }), answerHeading: ({ palette, spacings }: Theme) => css({ - marginTop: `${spacings.TRIPLE}rem`, + margin: `${spacings.TRIPLE}rem 0`, color: palette.WHITE, }), didYouKnow: ({ palette }: Theme) => diff --git a/src/app/components/Riddle/Components/Card/index.tsx b/src/app/components/Riddle/Components/Card/index.tsx index a010e19baa8..f47931eb2f8 100644 --- a/src/app/components/Riddle/Components/Card/index.tsx +++ b/src/app/components/Riddle/Components/Card/index.tsx @@ -16,6 +16,7 @@ export type GameData = { hint1: HintData; hint2: HintData; answer: string; + funFact: string; }; const getTimeDiff = (a: Date, b: Date) => { @@ -29,12 +30,23 @@ const getTimeDiff = (a: Date, b: Date) => { 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 } = - use(RiddleContext); + const { + gameData, + devTime, + gameIndex, + submitAttempt, + gameState, + revealAnswer, + } = use(RiddleContext); const { goes, coins } = use(LocalStorageContext); const inputRef = useRef(null); - const { question, hint1, hint2, answer, expire } = gameData; + const { question, hint1, hint2, answer, expire, funFact } = gameData; const expiryDate = new Date(expire); const currTime = devTime; @@ -62,9 +74,8 @@ export default () => { {question} - {gameState === GameState.PLAY && ( - <> +
@@ -74,6 +85,9 @@ export default () => { price={2500} paidSymbol="Answer" index={2} + onClickFn={() => { + revealAnswer(2500); + }} />
@@ -102,22 +116,34 @@ export default () => {
- +
)} {gameState === GameState.WINNER && ( - <> +
- {answer} + {capitalise(answer)} - - Did you know? Unlike diesel-electric submarines, which need to - surface or snorkel to recharge batteries, nuclear submarines use a - nuclear reactor to generate power, allowing them to produce their - own oxygen and fresh water. The main limiting factor for how long - they can stay underwater is food supply for the crew, not fuel or - air. + + {funFact} - +
+ )} + {gameState === GameState.FAILED && ( +
+ + {`You've run out of attempts!`} + + { + revealAnswer(2500); + }} + /> +
)}
diff --git a/src/app/components/Riddle/Components/DevControlPanel/index.tsx b/src/app/components/Riddle/Components/DevControlPanel/index.tsx index 5115d10ee04..9e5c17df522 100644 --- a/src/app/components/Riddle/Components/DevControlPanel/index.tsx +++ b/src/app/components/Riddle/Components/DevControlPanel/index.tsx @@ -17,11 +17,13 @@ const Option = ({ title, children }: PropsWithChildren<{ title: string }>) => { }; export default () => { - const { addCoins, addGoes } = use(LocalStorageContext); + const { addCoins } = use(LocalStorageContext); const { devTime, forceTimeInc24: forceTimeInc, forceTimeDec24: forceTimeDec, + gameState, + devOptionResetGoes, } = use(RiddleContext); return ( @@ -30,7 +32,10 @@ export default () => { Developer Tools - Sys Date: {devTime.toDateString()} + Sim Date: {devTime.toDateString()} + + + Game State: {gameState}