beta/src/components/MDX/Challenges/Challenges.tsx (222 lines of code) (raw):

/* * Copyright (c) Facebook, Inc. and its affiliates. */ import * as React from 'react'; import cn from 'classnames'; import {Button} from 'components/Button'; import {H2} from 'components/MDX/Heading'; import {H4} from 'components/MDX/Heading'; import {Navigation} from './Navigation'; import {IconHint} from '../../Icon/IconHint'; import {IconSolution} from '../../Icon/IconSolution'; import {IconArrowSmall} from '../../Icon/IconArrowSmall'; interface ChallengesProps { children: React.ReactElement[]; isRecipes?: boolean; titleText?: string; titleId?: string; } export interface ChallengeContents { id: string; name: string; order: number; content: React.ReactNode; solution: React.ReactNode; hint?: React.ReactNode; } const parseChallengeContents = ( children: React.ReactElement[] ): ChallengeContents[] => { const contents: ChallengeContents[] = []; if (!children) { return contents; } let challenge: Partial<ChallengeContents> = {}; let content: React.ReactElement[] = []; React.Children.forEach(children, (child) => { const {props} = child; switch (props.mdxType) { case 'Solution': { challenge.solution = child; challenge.content = content; contents.push(challenge as ChallengeContents); challenge = {}; content = []; break; } case 'Hint': { challenge.hint = child; break; } case 'h3': { challenge.order = contents.length + 1; challenge.name = props.children; challenge.id = props.id; break; } default: { content.push(child); } } }); return contents; }; export function Challenges({ children, isRecipes, titleText = isRecipes ? 'Try out some examples' : 'Try out some challenges', titleId = isRecipes ? 'examples' : 'challenges', }: ChallengesProps) { const challenges = parseChallengeContents(children); const scrollAnchorRef = React.useRef<HTMLDivElement>(null); const [showHint, setShowHint] = React.useState(false); const [showSolution, setShowSolution] = React.useState(false); const [activeChallenge, setActiveChallenge] = React.useState( challenges[0].id ); const handleChallengeChange = (challengeId: string) => { setShowHint(false); setShowSolution(false); setActiveChallenge(challengeId); }; const toggleHint = () => { if (showSolution && !showHint) { setShowSolution(false); } setShowHint((hint) => !hint); }; const toggleSolution = () => { if (showHint && !showSolution) { setShowHint(false); } setShowSolution((solution) => !solution); }; const currentChallenge = challenges.find(({id}) => id === activeChallenge); if (currentChallenge === undefined) { throw new TypeError('currentChallenge should always exist'); } const nextChallenge = challenges.find(({order}) => { return order === currentChallenge.order + 1; }); const Heading = isRecipes ? H4 : H2; return ( <div className="max-w-7xl mx-auto py-4"> <div className={cn( 'border-gray-10 bg-card dark:bg-card-dark shadow-inner rounded-none -mx-5 sm:mx-auto sm:rounded-lg' )}> <div ref={scrollAnchorRef} className="py-2 px-5 sm:px-8 pb-0 md:pb-0"> <Heading id={titleId} className={cn( 'mb-2 leading-10 relative', isRecipes ? 'text-xl text-purple-50 dark:text-purple-30' : 'text-3xl text-link' )}> {titleText} </Heading> {challenges.length > 1 && ( <Navigation currentChallenge={currentChallenge} challenges={challenges} handleChange={handleChallengeChange} isRecipes={isRecipes} /> )} </div> <div className="p-5 sm:py-8 sm:px-8"> <div key={activeChallenge}> <h3 className="text-xl text-primary dark:text-primary-dark mb-2"> <div className="font-bold block md:inline"> {isRecipes ? 'Example' : 'Challenge'} {currentChallenge.order}{' '} of {challenges.length} <span className="text-primary dark:text-primary-dark">: </span> </div> {currentChallenge.name} </h3> <>{currentChallenge.content}</> </div> <div className="flex justify-between items-center mt-4"> {currentChallenge.hint ? ( <div> <Button className="mr-2" onClick={toggleHint} active={showHint}> <IconHint className="mr-1.5" />{' '} {showHint ? 'Hide hint' : 'Show hint'} </Button> <Button className="mr-2" onClick={toggleSolution} active={showSolution}> <IconSolution className="mr-1.5" />{' '} {showSolution ? 'Hide solution' : 'Show solution'} </Button> </div> ) : ( !isRecipes && ( <Button className="mr-2" onClick={toggleSolution} active={showSolution}> <IconSolution className="mr-1.5" />{' '} {showSolution ? 'Hide solution' : 'Show solution'} </Button> ) )} {nextChallenge && ( <Button className={cn( isRecipes ? 'bg-purple-50 border-purple-50 hover:bg-purple-50 focus:bg-purple-50 active:bg-purple-50' : 'bg-link dark:bg-link-dark' )} onClick={() => { setActiveChallenge(nextChallenge.id); setShowSolution(false); }} active> Next {isRecipes ? 'Example' : 'Challenge'} <IconArrowSmall displayDirection="right" className="block ml-1.5" /> </Button> )} </div> {showHint && currentChallenge.hint} {showSolution && ( <div className="mt-6"> <h3 className="text-2xl font-bold text-primary dark:text-primary-dark"> Solution </h3> {currentChallenge.solution} <div className="flex justify-between items-center mt-4"> <Button onClick={() => setShowSolution(false)}> Close solution </Button> {nextChallenge && ( <Button className={cn( isRecipes ? 'bg-purple-50' : 'bg-link dark:bg-link-dark' )} onClick={() => { setActiveChallenge(nextChallenge.id); setShowSolution(false); if (scrollAnchorRef.current) { scrollAnchorRef.current.scrollIntoView({ block: 'start', behavior: 'smooth', }); } }} active> Next Challenge <IconArrowSmall displayDirection="right" className="block ml-1.5" /> </Button> )} </div> </div> )} </div> </div> </div> ); }