Skip to content

Commit 2214bd2

Browse files
Merge branch 'main' of github.com:csesoc/learning-platform
2 parents 75cfa57 + 50ba543 commit 2214bd2

32 files changed

+1094
-20
lines changed

components/ArticlesCarousel.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import React from 'react'
1+
// @ts-nocheck
2+
import React, { useCallback, useEffect, useRef, useState } from 'react'
23
import { Box, styled } from '@modulz/design-system'
34
import { useComposedRefs } from '@radix-ui/react-compose-refs'
45
import { createContext } from '@radix-ui/react-context'
56
import { useCallbackRef } from '@radix-ui/react-use-callback-ref'
67
import { composeEventHandlers } from '@radix-ui/primitive'
78
import debounce from 'lodash.debounce'
8-
import { useCallback, useEffect, useRef, useState } from 'react'
99
import smoothscroll from 'smoothscroll-polyfill'
1010

1111
import { Article } from 'contentlayer/generated'
@@ -298,7 +298,7 @@ export const Carousel = (props) => {
298298
return slidesArray.find(
299299
(slide) => slide.dataset.slideIntersectionRatio !== '0'
300300
)
301-
}
301+
}
302302
})
303303

304304
const handleNextClick = useCallback(() => {
@@ -513,6 +513,7 @@ const CarouselArrowButton = styled('button', {
513513
margin: 0,
514514
border: 0,
515515
padding: 0,
516+
length: 0,
516517

517518
display: 'flex',
518519
position: 'relative',

components/MultiChoice.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Children, cloneElement, ReactElement, ReactNode } from 'react'
2+
import { styled } from '../stitches.config'
3+
import { Flex } from './Flex'
4+
import Answer from './MultiChoiceAnswer'
5+
import Explanation from './MultiChoiceExplanation'
6+
import Question from './MultiChoiceQuestion'
7+
8+
const MultiChoiceBase = styled('div', {
9+
display: 'flex',
10+
flexDirection: 'column',
11+
backgroundColor: '$slate1',
12+
padding: '$2 $6 $6',
13+
borderRadius: '16px',
14+
margin: '$6 0 $6',
15+
boxShadow: '0px 4px 55px -42px rgba(0,0,0,0.74)',
16+
})
17+
18+
interface MultiChoiceProps {
19+
children: ReactNode
20+
}
21+
22+
const MultiChoice = ({ children }: MultiChoiceProps) => {
23+
const childrenArray = Children.toArray(children) as ReactElement[]
24+
const question = childrenArray.find(
25+
(child) => child.type === MultiChoice.Question
26+
)
27+
const answers = childrenArray
28+
.filter((child) => child.type === MultiChoice.Answer)
29+
// assign answer a number based on order, shown as a number circle
30+
.map((answer, index) => cloneElement(answer, { answerNum: index + 1 }))
31+
32+
return (
33+
<MultiChoiceBase>
34+
{question}
35+
<Flex direction="column">{answers}</Flex>
36+
</MultiChoiceBase>
37+
)
38+
}
39+
40+
MultiChoice.Question = Question
41+
MultiChoice.Answer = Answer
42+
MultiChoice.Explanation = Explanation
43+
44+
export default MultiChoice

components/MultiChoiceAnswer.tsx

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {
2+
CheckCircle,
3+
IconProps,
4+
NumberCircleFive,
5+
NumberCircleFour,
6+
NumberCircleOne,
7+
NumberCircleThree,
8+
NumberCircleTwo,
9+
XCircle
10+
} from 'phosphor-react'
11+
import { Children, ReactElement, ReactNode, useState } from 'react'
12+
import { styled } from '../stitches.config'
13+
import { Flex } from './Flex'
14+
import MultiChoice from './MultiChoice'
15+
16+
const COLOR_CORRECT = 'limegreen'
17+
const COLOR_INCORRECT = 'tomato'
18+
19+
const ExplanationBase = styled('div', {
20+
cursor: 'pointer',
21+
padding: '$2',
22+
borderBottom: '1px solid grey',
23+
'&:hover': {
24+
backgroundColor: '$slate3'
25+
}
26+
})
27+
28+
type AnswerProps = {
29+
content: ReactElement
30+
isCorrect: boolean
31+
answerNum: number
32+
children: ReactNode
33+
}
34+
35+
const Answer = ({
36+
content,
37+
isCorrect = false,
38+
answerNum,
39+
children
40+
}: AnswerProps) => {
41+
const [isExpanded, setIsExpanded] = useState(false)
42+
const childrenArray = Children.toArray(children) as ReactElement[]
43+
const answer = childrenArray.filter(
44+
(child) => child.type != MultiChoice.Explanation
45+
)
46+
const explanation = childrenArray.find(
47+
(child) => child.type === MultiChoice.Explanation
48+
)
49+
50+
const handleOnClick = () => {
51+
setIsExpanded((prev) => !prev)
52+
}
53+
54+
// content is used for short answers
55+
// long answers use the children prop
56+
return (
57+
<ExplanationBase onClick={handleOnClick}>
58+
<Flex direction="row" gap="3" align="center">
59+
{renderCircle(isExpanded ? isCorrect : answerNum)}
60+
{content ? <p>{content}</p> : <div>{answer}</div>}
61+
</Flex>
62+
{isExpanded && renderIsCorrect(isCorrect)}
63+
{isExpanded && explanation}
64+
</ExplanationBase>
65+
)
66+
}
67+
68+
export default Answer
69+
70+
const renderIsCorrect = (isCorrect: boolean) => {
71+
if (isCorrect) {
72+
const style = { color: COLOR_CORRECT, fontWeight: 'bold' }
73+
return <p style={style}>Correct!</p>
74+
}
75+
// !isCorrect
76+
const style = { color: COLOR_INCORRECT, fontWeight: 'bold' }
77+
return <p style={style}>Incorrect</p>
78+
}
79+
80+
const renderCircle = (circleType: boolean | number) => {
81+
const iconProps: IconProps = { size: '32px', style: { flexShrink: 0 } }
82+
switch (circleType) {
83+
case true:
84+
return <CheckCircle color={COLOR_CORRECT} weight="fill" {...iconProps} />
85+
case false:
86+
return <XCircle color={COLOR_INCORRECT} weight="fill" {...iconProps} />
87+
case 1:
88+
return <NumberCircleOne {...iconProps} />
89+
case 2:
90+
return <NumberCircleTwo {...iconProps} />
91+
case 3:
92+
return <NumberCircleThree {...iconProps} />
93+
case 4:
94+
return <NumberCircleFour {...iconProps} />
95+
case 5:
96+
return <NumberCircleFive {...iconProps} />
97+
default:
98+
return <XCircle {...iconProps} />
99+
}
100+
}

components/MultiChoiceExplanation.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React, { Children, ReactElement, ReactNode, useState } from 'react'
2+
import { callbackify } from 'util'
3+
import { styled } from '../stitches.config'
4+
5+
const ExplanationBase = styled('div', {
6+
color: 'grey'
7+
})
8+
9+
type ExplanationProps = {
10+
content: ReactElement
11+
children: JSX.Element[]
12+
}
13+
14+
const Explanation = ({ content, children }: ExplanationProps) => {
15+
const [isExpanded, setIsExpanded] = useState(false)
16+
17+
// content is used for short explanations
18+
// long explanations use the children prop
19+
if (content) {
20+
return (
21+
<ExplanationBase>
22+
<p>{content}</p>
23+
</ExplanationBase>
24+
)
25+
}
26+
27+
console.log("children", children)
28+
const traverse = (ele: JSX.Element[] | JSX.Element | string, callback: (ele: string) => void) => {
29+
if (typeof ele === 'string') {
30+
// console.log("ele is string:", ele)
31+
callback(ele);
32+
return;
33+
}
34+
if (Array.isArray(ele)) {
35+
// console.log("ele is array:", ele)
36+
ele.forEach((subEle) => traverse(subEle, callback));
37+
} else if (React.isValidElement(ele)) {
38+
// console.log("ele is react element:", ele)
39+
if (ele.props.hasOwnProperty('children')) {
40+
// Only if the props of this react element has a children prop.
41+
// Might not in some cases, e.g. when ele.type === "img"
42+
traverse((ele as JSX.Element).props.children, callback);
43+
}
44+
}
45+
}
46+
47+
// Traverse the component tree to count total length of text.
48+
let textLength = 0;
49+
traverse(children, (text) => {
50+
textLength += text.length
51+
})
52+
53+
const handleOnClick = (event) => {
54+
// prevents collapsing the parent component
55+
event.stopPropagation()
56+
setIsExpanded((prev) => !prev)
57+
}
58+
59+
// above collapsable threshold
60+
const threshold = 1000
61+
if ((children.length > 1 || textLength > threshold) && !isExpanded) {
62+
let previewTextFound = false
63+
let previewText = ""
64+
65+
// Traverse the component tree and find the first text node.
66+
traverse(children, (text: string) => {
67+
if (!previewTextFound) {
68+
previewText = text
69+
previewTextFound = true
70+
}
71+
})
72+
73+
previewText.substring(0, threshold)
74+
// trims all non-letter characters from end of string
75+
// this ensures the truncated paragraph looks good with the ellipsis (...)
76+
.replace(/[^a-z]+$/gi, '')
77+
78+
return (
79+
<ExplanationBase>
80+
<p>
81+
{previewText}
82+
{'... '}
83+
<a onClick={(event) => handleOnClick(event)}>Read more</a>
84+
</p>
85+
</ExplanationBase>
86+
)
87+
}
88+
89+
return <ExplanationBase>{children}</ExplanationBase>
90+
}
91+
92+
export default Explanation

components/MultiChoiceQuestion.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Children, cloneElement, ReactElement, ReactNode } from 'react'
2+
import { styled } from '../stitches.config'
3+
4+
const QuestionBase = styled('div', {
5+
borderBottom: '1px solid grey'
6+
})
7+
8+
type QuestionProps = {
9+
children: ReactNode
10+
}
11+
12+
const Question = ({ children }: QuestionProps) => {
13+
// a clone of children, except the images are centred
14+
const childrenWithCenteredImages = Children.map(
15+
children as ReactElement[],
16+
(child) => {
17+
if (childIsType(child, 'img')) {
18+
return cloneElement(child, { align: 'center' })
19+
}
20+
return child
21+
}
22+
)
23+
24+
return <QuestionBase>{childrenWithCenteredImages}</QuestionBase>
25+
}
26+
27+
export default Question
28+
29+
// recursively checks if an element or any of its children is the given type
30+
const childIsType = (child: ReactElement, type: string): boolean => {
31+
if (child == null) {
32+
return false
33+
}
34+
const isType = childIsType(child.props?.children, type)
35+
if (child.type === type) {
36+
return true
37+
}
38+
return isType
39+
}

0 commit comments

Comments
 (0)