@magmamath/students-features 1.3.26-rc.7 → 1.3.26-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/commonjs/features/fluency/components/fluency-solving/components/EquationContent.js +39 -18
  2. package/dist/commonjs/features/fluency/components/fluency-solving/components/EquationContent.js.map +1 -1
  3. package/dist/commonjs/features/fluency/components/fluency-solving/components/FluencyCardDeck.js +113 -0
  4. package/dist/commonjs/features/fluency/components/fluency-solving/components/FluencyCardDeck.js.map +1 -0
  5. package/dist/commonjs/features/fluency/components/fluency-solving/components/FluencyEquationCard.js +22 -81
  6. package/dist/commonjs/features/fluency/components/fluency-solving/components/FluencyEquationCard.js.map +1 -1
  7. package/dist/commonjs/features/fluency/components/fluency-solving/components/FluencySolvingCard.js +32 -15
  8. package/dist/commonjs/features/fluency/components/fluency-solving/components/FluencySolvingCard.js.map +1 -1
  9. package/dist/commonjs/features/fluency/components/start-banner/StartBanner.js +15 -29
  10. package/dist/commonjs/features/fluency/components/start-banner/StartBanner.js.map +1 -1
  11. package/dist/commonjs/features/fluency/hooks/useCardSnapAnimation.js +33 -22
  12. package/dist/commonjs/features/fluency/hooks/useCardSnapAnimation.js.map +1 -1
  13. package/dist/commonjs/features/fluency/hooks/useFluencyCardDeckAnimation.js +44 -0
  14. package/dist/commonjs/features/fluency/hooks/useFluencyCardDeckAnimation.js.map +1 -0
  15. package/dist/commonjs/features/fluency/hooks/useFluencySolvingPreset.js +2 -2
  16. package/dist/commonjs/features/fluency/hooks/useFluencySolvingPreset.js.map +1 -1
  17. package/dist/commonjs/lib/components/GradientView.js +57 -0
  18. package/dist/commonjs/lib/components/GradientView.js.map +1 -0
  19. package/dist/module/features/fluency/components/fluency-solving/components/EquationContent.js +40 -19
  20. package/dist/module/features/fluency/components/fluency-solving/components/EquationContent.js.map +1 -1
  21. package/dist/module/features/fluency/components/fluency-solving/components/FluencyCardDeck.js +107 -0
  22. package/dist/module/features/fluency/components/fluency-solving/components/FluencyCardDeck.js.map +1 -0
  23. package/dist/module/features/fluency/components/fluency-solving/components/FluencyEquationCard.js +23 -82
  24. package/dist/module/features/fluency/components/fluency-solving/components/FluencyEquationCard.js.map +1 -1
  25. package/dist/module/features/fluency/components/fluency-solving/components/FluencySolvingCard.js +32 -15
  26. package/dist/module/features/fluency/components/fluency-solving/components/FluencySolvingCard.js.map +1 -1
  27. package/dist/module/features/fluency/components/start-banner/StartBanner.js +16 -30
  28. package/dist/module/features/fluency/components/start-banner/StartBanner.js.map +1 -1
  29. package/dist/module/features/fluency/hooks/useCardSnapAnimation.js +33 -22
  30. package/dist/module/features/fluency/hooks/useCardSnapAnimation.js.map +1 -1
  31. package/dist/module/features/fluency/hooks/useFluencyCardDeckAnimation.js +39 -0
  32. package/dist/module/features/fluency/hooks/useFluencyCardDeckAnimation.js.map +1 -0
  33. package/dist/module/features/fluency/hooks/useFluencySolvingPreset.js +2 -2
  34. package/dist/module/features/fluency/hooks/useFluencySolvingPreset.js.map +1 -1
  35. package/dist/module/lib/components/GradientView.js +51 -0
  36. package/dist/module/lib/components/GradientView.js.map +1 -0
  37. package/dist/typescript/commonjs/features/fluency/components/fluency-solving/components/EquationContent.d.ts +5 -1
  38. package/dist/typescript/commonjs/features/fluency/components/fluency-solving/components/EquationContent.d.ts.map +1 -1
  39. package/dist/typescript/commonjs/features/fluency/components/fluency-solving/components/FluencyCardDeck.d.ts +14 -0
  40. package/dist/typescript/commonjs/features/fluency/components/fluency-solving/components/FluencyCardDeck.d.ts.map +1 -0
  41. package/dist/typescript/commonjs/features/fluency/components/fluency-solving/components/FluencyEquationCard.d.ts +7 -3
  42. package/dist/typescript/commonjs/features/fluency/components/fluency-solving/components/FluencyEquationCard.d.ts.map +1 -1
  43. package/dist/typescript/commonjs/features/fluency/components/fluency-solving/components/FluencySolvingCard.d.ts.map +1 -1
  44. package/dist/typescript/commonjs/features/fluency/components/fluency-solving/fluencySolving.types.d.ts +4 -0
  45. package/dist/typescript/commonjs/features/fluency/components/fluency-solving/fluencySolving.types.d.ts.map +1 -1
  46. package/dist/typescript/commonjs/features/fluency/components/start-banner/StartBanner.d.ts.map +1 -1
  47. package/dist/typescript/commonjs/features/fluency/hooks/useCardSnapAnimation.d.ts +7 -2
  48. package/dist/typescript/commonjs/features/fluency/hooks/useCardSnapAnimation.d.ts.map +1 -1
  49. package/dist/typescript/commonjs/features/fluency/hooks/useFluencyCardDeckAnimation.d.ts +16 -0
  50. package/dist/typescript/commonjs/features/fluency/hooks/useFluencyCardDeckAnimation.d.ts.map +1 -0
  51. package/dist/typescript/commonjs/features/fluency/hooks/useFluencySolvingPreset.d.ts +5 -2
  52. package/dist/typescript/commonjs/features/fluency/hooks/useFluencySolvingPreset.d.ts.map +1 -1
  53. package/dist/typescript/commonjs/lib/components/GradientView.d.ts +18 -0
  54. package/dist/typescript/commonjs/lib/components/GradientView.d.ts.map +1 -0
  55. package/dist/typescript/module/features/fluency/components/fluency-solving/components/EquationContent.d.ts +5 -1
  56. package/dist/typescript/module/features/fluency/components/fluency-solving/components/EquationContent.d.ts.map +1 -1
  57. package/dist/typescript/module/features/fluency/components/fluency-solving/components/FluencyCardDeck.d.ts +14 -0
  58. package/dist/typescript/module/features/fluency/components/fluency-solving/components/FluencyCardDeck.d.ts.map +1 -0
  59. package/dist/typescript/module/features/fluency/components/fluency-solving/components/FluencyEquationCard.d.ts +7 -3
  60. package/dist/typescript/module/features/fluency/components/fluency-solving/components/FluencyEquationCard.d.ts.map +1 -1
  61. package/dist/typescript/module/features/fluency/components/fluency-solving/components/FluencySolvingCard.d.ts.map +1 -1
  62. package/dist/typescript/module/features/fluency/components/fluency-solving/fluencySolving.types.d.ts +4 -0
  63. package/dist/typescript/module/features/fluency/components/fluency-solving/fluencySolving.types.d.ts.map +1 -1
  64. package/dist/typescript/module/features/fluency/components/start-banner/StartBanner.d.ts.map +1 -1
  65. package/dist/typescript/module/features/fluency/hooks/useCardSnapAnimation.d.ts +7 -2
  66. package/dist/typescript/module/features/fluency/hooks/useCardSnapAnimation.d.ts.map +1 -1
  67. package/dist/typescript/module/features/fluency/hooks/useFluencyCardDeckAnimation.d.ts +16 -0
  68. package/dist/typescript/module/features/fluency/hooks/useFluencyCardDeckAnimation.d.ts.map +1 -0
  69. package/dist/typescript/module/features/fluency/hooks/useFluencySolvingPreset.d.ts +5 -2
  70. package/dist/typescript/module/features/fluency/hooks/useFluencySolvingPreset.d.ts.map +1 -1
  71. package/dist/typescript/module/lib/components/GradientView.d.ts +18 -0
  72. package/dist/typescript/module/lib/components/GradientView.d.ts.map +1 -0
  73. package/package.json +1 -1
  74. package/src/features/fluency/components/fluency-solving/components/EquationContent.tsx +41 -25
  75. package/src/features/fluency/components/fluency-solving/components/FluencyCardDeck.tsx +128 -0
  76. package/src/features/fluency/components/fluency-solving/components/FluencyEquationCard.tsx +37 -72
  77. package/src/features/fluency/components/fluency-solving/components/FluencySolvingCard.tsx +32 -13
  78. package/src/features/fluency/components/fluency-solving/fluencySolving.types.ts +5 -0
  79. package/src/features/fluency/components/start-banner/StartBanner.tsx +10 -28
  80. package/src/features/fluency/hooks/useCardSnapAnimation.ts +41 -21
  81. package/src/features/fluency/hooks/useFluencyCardDeckAnimation.ts +49 -0
  82. package/src/features/fluency/hooks/useFluencySolvingPreset.ts +25 -7
  83. package/src/lib/components/GradientView.tsx +63 -0
@@ -0,0 +1,128 @@
1
+ import React from 'react'
2
+ import { View, StyleSheet } from 'react-native'
3
+ import Animated from 'react-native-reanimated'
4
+ import { BORDER_RADIUS, COLORS, SHADOWS } from '@magmamath/react-native-ui'
5
+
6
+ import { GradientView } from '../../../../../lib/components/GradientView'
7
+ import { FluencyEquationCard } from './FluencyEquationCard'
8
+ import type { SnapStyles } from '../../../hooks/useCardSnapAnimation'
9
+ import { AttemptStatus } from '../../../fluency.constants'
10
+ import type { FluencyCardDisplayContent } from '../fluencySolving.types'
11
+
12
+ const CARD_GRADIENT_COLORS: Record<AttemptStatus.CORRECT | AttemptStatus.INCORRECT, [string, string]> = {
13
+ [AttemptStatus.CORRECT]: ['#ecfbf3', '#ffffff'],
14
+ [AttemptStatus.INCORRECT]: ['#fff0f1', '#ffffff'],
15
+ }
16
+
17
+ type FluencyCardDeckProps = {
18
+ currentCardContent: FluencyCardDisplayContent
19
+ exitingCardContent: FluencyCardDisplayContent | null
20
+ exitingCardStatus: AttemptStatus | null
21
+ snapStyles: SnapStyles
22
+ onAnswerChange: (text: string) => void
23
+ }
24
+
25
+ const hideAnswer = (cardContent: FluencyCardDisplayContent): FluencyCardDisplayContent => ({
26
+ ...cardContent,
27
+ answer: '',
28
+ })
29
+
30
+ export const FluencyCardDeck = ({
31
+ currentCardContent,
32
+ exitingCardContent,
33
+ exitingCardStatus,
34
+ snapStyles,
35
+ onAnswerChange,
36
+ }: FluencyCardDeckProps) => {
37
+ const backingCardContent = hideAnswer(currentCardContent)
38
+
39
+ return (
40
+ <View style={styles.stack}>
41
+ <Animated.View style={[styles.card, styles.cardFar, snapStyles.far]} />
42
+ <Animated.View style={[styles.card, styles.cardMid, snapStyles.mid]} />
43
+
44
+ <Animated.View style={[styles.card, styles.cardNear, snapStyles.near]}>
45
+ <FluencyEquationCard cardContent={backingCardContent} readOnly />
46
+ </Animated.View>
47
+
48
+ {!exitingCardContent && (
49
+ <Animated.View style={[styles.card, styles.cardFront]}>
50
+ <FluencyEquationCard cardContent={currentCardContent} onAnswerChange={onAnswerChange} />
51
+ </Animated.View>
52
+ )}
53
+
54
+ {exitingCardContent && (
55
+ <Animated.View
56
+ style={[
57
+ styles.card,
58
+ styles.cardFront,
59
+ snapStyles.front,
60
+ exitingCardStatus === AttemptStatus.INCORRECT && styles.cardIncorrect,
61
+ exitingCardStatus === AttemptStatus.CORRECT && styles.cardCorrect,
62
+ ]}
63
+ >
64
+ {(exitingCardStatus === AttemptStatus.CORRECT ||
65
+ exitingCardStatus === AttemptStatus.INCORRECT) && (
66
+ <GradientView
67
+ colors={CARD_GRADIENT_COLORS[exitingCardStatus]}
68
+ style={[StyleSheet.absoluteFill, { borderRadius: BORDER_RADIUS[400] }]}
69
+ pointerEvents="none"
70
+ />
71
+ )}
72
+ <FluencyEquationCard
73
+ cardContent={exitingCardContent}
74
+ status={exitingCardStatus ?? undefined}
75
+ readOnly
76
+ />
77
+ </Animated.View>
78
+ )}
79
+ </View>
80
+ )
81
+ }
82
+
83
+ const styles = StyleSheet.create({
84
+ stack: {
85
+ width: '100%',
86
+ maxWidth: 560,
87
+ },
88
+ card: {
89
+ height: 260,
90
+ borderRadius: BORDER_RADIUS[400],
91
+ backgroundColor: COLORS.NEUTRAL_1,
92
+ ...SHADOWS[5],
93
+ },
94
+ cardFar: {
95
+ position: 'absolute',
96
+ top: 36,
97
+ left: 60,
98
+ right: 60,
99
+ opacity: 0,
100
+ },
101
+ cardMid: {
102
+ position: 'absolute',
103
+ top: 24,
104
+ left: 40,
105
+ right: 40,
106
+ },
107
+ cardNear: {
108
+ position: 'absolute',
109
+ top: 12,
110
+ left: 20,
111
+ right: 20,
112
+ alignItems: 'center',
113
+ justifyContent: 'center',
114
+ ...SHADOWS[5],
115
+ },
116
+ cardFront: {
117
+ alignItems: 'center',
118
+ justifyContent: 'center',
119
+ },
120
+ cardCorrect: {
121
+ borderWidth: 1.5,
122
+ borderColor: COLORS.PRIMARY_GREEN,
123
+ },
124
+ cardIncorrect: {
125
+ borderWidth: 1.5,
126
+ borderColor: COLORS.PRIMARY_RED,
127
+ },
128
+ })
@@ -1,86 +1,51 @@
1
1
  import React from 'react'
2
- import { useUnit } from 'effector-react'
3
- import { View, StyleSheet } from 'react-native'
4
- import Animated from 'react-native-reanimated'
5
- import { BORDER_RADIUS, COLORS, SHADOWS } from '@magmamath/react-native-ui'
2
+
6
3
  import { EquationContent } from './EquationContent'
7
- import { useFluency } from '../../../context/FluencyContext'
8
- import type { SnapStyles } from '../../../hooks/useCardSnapAnimation'
4
+ import type { FluencyCardDisplayContent, SolvingFact } from '../fluencySolving.types'
5
+ import type { AttemptStatus, FluencyMode } from '../../../fluency.constants'
9
6
 
10
- type FluencyEquationCardProps = {
11
- animationStyles: SnapStyles
7
+ type DisplayedEquation = {
8
+ leftOperand: number
9
+ rightOperand: number
10
+ mode?: FluencyMode
12
11
  }
13
12
 
14
- export const FluencyEquationCard = ({ animationStyles }: FluencyEquationCardProps) => {
15
- const { model } = useFluency()
16
- const { answer, fact } = useUnit({
17
- answer: model.solving.$answer,
18
- fact: model.solving.$current,
19
- })
13
+ type FluencyEquationCardProps = {
14
+ cardContent: FluencyCardDisplayContent
15
+ readOnly?: boolean
16
+ status?: AttemptStatus
17
+ onAnswerChange?: (text: string) => void
18
+ }
20
19
 
20
+ const getDisplayedEquation = (fact: SolvingFact | null): DisplayedEquation => {
21
21
  const left = fact?.numbers[0] ?? 0
22
22
  const right = fact?.numbers[1] ?? 0
23
23
 
24
- const updateAnswer = (text: string) => {
25
- const digits = text.replace(/\D/g, '')
26
- model.solving.setAnswer(digits || null)
24
+ return {
25
+ leftOperand: left,
26
+ rightOperand: right,
27
+ mode: fact?.mode,
27
28
  }
29
+ }
28
30
 
29
- return (
30
- <View style={styles.stack}>
31
- <Animated.View style={[styles.card, styles.cardFar, animationStyles.far]} />
32
- <Animated.View style={[styles.card, styles.cardMid, animationStyles.mid]} />
31
+ export const FluencyEquationCard = ({
32
+ cardContent,
33
+ readOnly = false,
34
+ status,
35
+ onAnswerChange,
36
+ }: FluencyEquationCardProps) => {
37
+ const equation = getDisplayedEquation(cardContent.fact)
33
38
 
34
- <Animated.View style={[styles.card, styles.cardNear, animationStyles.near]}>
35
- <EquationContent leftOperand={left} rightOperand={right} readOnly />
36
- </Animated.View>
37
- <Animated.View style={[styles.card, styles.cardFront, animationStyles.front]}>
38
- <EquationContent
39
- leftOperand={left}
40
- rightOperand={right}
41
- value={answer ?? ''}
42
- onChange={updateAnswer}
43
- />
44
- </Animated.View>
45
- </View>
39
+ return (
40
+ <EquationContent
41
+ leftOperand={equation.leftOperand}
42
+ rightOperand={equation.rightOperand}
43
+ mode={equation.mode}
44
+ value={cardContent.answer}
45
+ readOnly={readOnly}
46
+ status={status}
47
+ correctAnswer={cardContent.fact?.answer}
48
+ onChange={onAnswerChange}
49
+ />
46
50
  )
47
51
  }
48
-
49
- const styles = StyleSheet.create({
50
- stack: {
51
- width: '100%',
52
- maxWidth: 560,
53
- },
54
- card: {
55
- height: 260,
56
- borderRadius: BORDER_RADIUS[400],
57
- backgroundColor: COLORS.NEUTRAL_1,
58
- ...SHADOWS[5],
59
- },
60
- cardFar: {
61
- position: 'absolute',
62
- top: 36,
63
- left: 60,
64
- right: 60,
65
- opacity: 0,
66
- },
67
- cardMid: {
68
- position: 'absolute',
69
- top: 24,
70
- left: 40,
71
- right: 40,
72
- },
73
- cardNear: {
74
- position: 'absolute',
75
- top: 12,
76
- left: 20,
77
- right: 20,
78
- alignItems: 'center',
79
- justifyContent: 'center',
80
- ...SHADOWS[5],
81
- },
82
- cardFront: {
83
- alignItems: 'center',
84
- justifyContent: 'center',
85
- },
86
- })
@@ -1,31 +1,44 @@
1
1
  import React from 'react'
2
2
  import { View, StyleSheet } from 'react-native'
3
- import { SPACING } from '@magmamath/react-native-ui'
4
3
  import { useUnit } from 'effector-react/effector-react.mjs'
4
+ import { SPACING } from '@magmamath/react-native-ui'
5
+
5
6
  import { FluencyNumpad } from './FluencyNumpad'
6
- import { FluencyEquationCard } from './FluencyEquationCard'
7
+ import { FluencyCardDeck } from './FluencyCardDeck'
7
8
  import { useFluency } from '../../../context/FluencyContext'
8
- import { useCardSnapAnimation } from '../../../hooks/useCardSnapAnimation'
9
+ import { useFluencyCardDeckAnimation } from '../../../hooks/useFluencyCardDeckAnimation'
9
10
  import { useSuccessSound } from '../../../hooks/useSuccessSound'
11
+ import { AttemptStatus } from '../../../fluency.constants'
12
+ import type { SnapDirection } from '../../../hooks/useCardSnapAnimation'
10
13
 
11
14
  export const FluencySolvingCard = () => {
12
15
  const { model } = useFluency()
13
- const { answer, fact } = useUnit({
14
- answer: model.solving.$answer,
15
- fact: model.solving.$current,
16
+ const { currentAnswer, currentFact } = useUnit({
17
+ currentAnswer: model.solving.$answer,
18
+ currentFact: model.solving.$current,
16
19
  })
17
- const animation = useCardSnapAnimation()
20
+ const cardDeckAnimation = useFluencyCardDeckAnimation()
18
21
  const successSound = useSuccessSound()
19
22
 
23
+ const currentCardContent = { fact: currentFact, answer: currentAnswer ?? '' }
24
+
25
+ const updateAnswer = (text: string) => {
26
+ const digits = text.replace(/\D/g, '')
27
+ model.solving.setAnswer(digits || null)
28
+ }
29
+
20
30
  const submit = () => {
21
- if (!fact) return
31
+ if (!currentFact) return
32
+
33
+ const isCorrect = currentAnswer !== null && Number(currentAnswer) === currentFact.answer
34
+ const direction: SnapDirection = isCorrect ? 'right' : 'left'
35
+ const status = isCorrect ? AttemptStatus.CORRECT : AttemptStatus.INCORRECT
36
+
37
+ if (!cardDeckAnimation.startCardExit({ cardContent: currentCardContent, direction, status }))
38
+ return
22
39
 
23
- const isCorrect = answer !== null && Number(answer) === fact.answer
24
40
  if (isCorrect) {
25
41
  successSound.play()
26
- animation.trigger('right')
27
- } else {
28
- animation.trigger('left')
29
42
  }
30
43
 
31
44
  model.solving.submit()
@@ -34,7 +47,13 @@ export const FluencySolvingCard = () => {
34
47
  return (
35
48
  <View>
36
49
  <View style={styles.wrapper}>
37
- <FluencyEquationCard animationStyles={animation.snapStyles} />
50
+ <FluencyCardDeck
51
+ currentCardContent={currentCardContent}
52
+ exitingCardContent={cardDeckAnimation.exitingCardContent}
53
+ exitingCardStatus={cardDeckAnimation.exitingCardStatus}
54
+ snapStyles={cardDeckAnimation.snapStyles}
55
+ onAnswerChange={updateAnswer}
56
+ />
38
57
  </View>
39
58
  <FluencyNumpad onSubmit={submit} />
40
59
  </View>
@@ -14,6 +14,11 @@ export type SolvingFact = {
14
14
  correctCount: number
15
15
  }
16
16
 
17
+ export type FluencyCardDisplayContent = {
18
+ fact: SolvingFact | null
19
+ answer: string
20
+ }
21
+
17
22
  export type QuestionOutcome = {
18
23
  factId: string
19
24
  status: AttemptStatus
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { View, StyleSheet, Platform } from 'react-native'
2
+ import { View, StyleSheet } from 'react-native'
3
3
  import {
4
4
  Button,
5
5
  ButtonColor,
@@ -13,37 +13,12 @@ import {
13
13
  FONT_FAMILY,
14
14
  BORDER_RADIUS,
15
15
  } from '@magmamath/react-native-ui'
16
+ import { GradientView } from '../../../../lib/components/GradientView'
16
17
  import { BannerFlashcards } from './BannerFlashcards'
17
18
  import { useFluency } from '../../context/FluencyContext'
18
19
  import { useText } from '../../../../i18n/i18n'
19
20
  import { FlashIcon } from '../../assets/FlashIcon'
20
21
 
21
- const GradientBackground = Platform.select({
22
- web: () => (
23
- <View
24
- style={[
25
- StyleSheet.absoluteFill,
26
- {
27
- backgroundImage: 'linear-gradient(90deg, #FFFFFF 23.1%, #FFF7E9 81.73%)',
28
- pointerEvents: 'none',
29
- } as any,
30
- ]}
31
- />
32
- ),
33
- default: () => {
34
- const { LinearGradient } = require('expo-linear-gradient')
35
- return (
36
- <LinearGradient
37
- colors={['#FFFFFF', '#FFF7E9']}
38
- locations={[0.231, 0.8173]}
39
- start={{ x: 0, y: 0 }}
40
- end={{ x: 1, y: 0 }}
41
- style={[StyleSheet.absoluteFill, { pointerEvents: 'none' }]}
42
- />
43
- )
44
- },
45
- })!
46
-
47
22
  export const StartBanner = () => {
48
23
  const t = useText()
49
24
  const { model, onStartPress } = useFluency()
@@ -51,7 +26,14 @@ export const StartBanner = () => {
51
26
  return (
52
27
  <View style={styles.container}>
53
28
  <View style={styles.gradientClip}>
54
- <GradientBackground />
29
+ <GradientView
30
+ colors={['#FFFFFF', '#FFF7E9']}
31
+ locations={[0.231, 0.8173]}
32
+ start={{ x: 0, y: 0 }}
33
+ end={{ x: 1, y: 0 }}
34
+ style={StyleSheet.absoluteFill}
35
+ pointerEvents="none"
36
+ />
55
37
  </View>
56
38
 
57
39
  <View style={styles.buttonContainer}>
@@ -21,6 +21,14 @@ export type SnapStyles = {
21
21
  far: AnimatedStyle<ViewStyle>
22
22
  }
23
23
 
24
+ type UseCardSnapAnimationParams = {
25
+ onComplete?: () => void
26
+ }
27
+
28
+ const SHAKE_STEP_DURATION = 60
29
+
30
+ const SNAP_SUCCESS_DURATION = 300
31
+
24
32
  const SNAP = {
25
33
  duration: 240,
26
34
  easing: Easing.bezier(0.6, 0, 0.4, 1),
@@ -35,15 +43,16 @@ const STEP_UP = {
35
43
  easing: Easing.bezier(0.3, 0.9, 0.2, 1),
36
44
  } as const
37
45
 
38
- // Initial positions of each backing layer: [top, side]
39
46
  const L = [
40
47
  { top: 12, side: 20 },
41
48
  { top: 24, side: 40 },
42
49
  { top: 36, side: 60 },
43
50
  ]
44
51
 
45
- export const useCardSnapAnimation = () => {
52
+ export const useCardSnapAnimation = (params: UseCardSnapAnimationParams = {}) => {
46
53
  const isAnimating = useRef(false)
54
+ const onCompleteRef = useRef(params.onComplete)
55
+ onCompleteRef.current = params.onComplete
47
56
 
48
57
  const translateX = useSharedValue(0)
49
58
  const translateY = useSharedValue(0)
@@ -87,13 +96,12 @@ export const useCardSnapAnimation = () => {
87
96
  [l3Top, l3Side, l3Opacity],
88
97
  )
89
98
 
90
- const reset = () => {
91
- translateX.value = 0
92
- translateY.value = 0
93
- rotate.value = 0
94
- scale.value = 1
95
- opacity.value = 1
99
+ const complete = () => {
100
+ isAnimating.current = false
101
+ onCompleteRef.current?.()
102
+ }
96
103
 
104
+ const resetBackingCards = () => {
97
105
  l1Top.value = L[0].top
98
106
  l1Side.value = L[0].side
99
107
  l2Top.value = L[1].top
@@ -101,12 +109,18 @@ export const useCardSnapAnimation = () => {
101
109
  l3Top.value = L[2].top
102
110
  l3Side.value = L[2].side
103
111
  l3Opacity.value = 0
112
+ }
104
113
 
105
- isAnimating.current = false
114
+ const resetFrontCard = () => {
115
+ translateX.value = 0
116
+ translateY.value = 0
117
+ rotate.value = 0
118
+ scale.value = 1
119
+ opacity.value = 1
106
120
  }
107
121
 
108
- const snap = (toX: number, toRotate: number) => {
109
- const snapConfig = { duration: SNAP.duration, easing: SNAP.easing }
122
+ const snap = (toX: number, toRotate: number, duration: number = SNAP.duration) => {
123
+ const snapConfig = { duration, easing: SNAP.easing }
110
124
  const stepConfig = { duration: STEP_UP.duration, easing: STEP_UP.easing }
111
125
 
112
126
  translateX.value = withTiming(toX, snapConfig)
@@ -115,7 +129,10 @@ export const useCardSnapAnimation = () => {
115
129
  scale.value = withTiming(SNAP.scale, snapConfig)
116
130
  opacity.value = withTiming(0, snapConfig)
117
131
 
118
- l1Top.value = withDelay(STEP_UP.delay, withTiming(0, stepConfig, () => runOnJS(reset)()))
132
+ l1Top.value = withDelay(
133
+ STEP_UP.delay,
134
+ withTiming(0, stepConfig, () => runOnJS(complete)()),
135
+ )
119
136
  l1Side.value = withDelay(STEP_UP.delay, withTiming(0, stepConfig))
120
137
  l2Top.value = withDelay(STEP_UP.delay, withTiming(L[0].top, stepConfig))
121
138
  l2Side.value = withDelay(STEP_UP.delay, withTiming(L[0].side, stepConfig))
@@ -125,26 +142,29 @@ export const useCardSnapAnimation = () => {
125
142
  }
126
143
 
127
144
  const trigger = (direction: SnapDirection) => {
128
- if (isAnimating.current) return
145
+ if (isAnimating.current) return false
129
146
  isAnimating.current = true
147
+ resetFrontCard()
148
+ resetBackingCards()
130
149
 
131
150
  const screenWidth = Dimensions.get('window').width
132
151
  const toX = direction === 'right' ? screenWidth * 1.6 : -screenWidth * 1.6
133
152
  const toRotate = direction === 'right' ? SNAP.rotate : -SNAP.rotate
134
153
 
135
154
  if (direction === 'right') {
136
- snap(toX, toRotate)
137
- return
155
+ snap(toX, toRotate, SNAP_SUCCESS_DURATION)
156
+ return true
138
157
  }
139
158
 
140
159
  translateX.value = withSequence(
141
- withTiming(-10, { duration: 60 }),
142
- withTiming(10, { duration: 60 }),
143
- withTiming(-6, { duration: 60 }),
144
- withTiming(6, { duration: 60 }),
145
- withTiming(0, { duration: 60 }, () => runOnJS(snap)(toX, toRotate)),
160
+ withTiming(-10, { duration: SHAKE_STEP_DURATION }),
161
+ withTiming(10, { duration: SHAKE_STEP_DURATION }),
162
+ withTiming(-6, { duration: SHAKE_STEP_DURATION }),
163
+ withTiming(6, { duration: SHAKE_STEP_DURATION }),
164
+ withTiming(0, { duration: SHAKE_STEP_DURATION }, () => runOnJS(snap)(toX, toRotate)),
146
165
  )
166
+ return true
147
167
  }
148
168
 
149
- return { snapStyles: { front, near, mid, far } as SnapStyles, trigger }
169
+ return { snapStyles: { front, near, mid, far } as SnapStyles, trigger, resetBackingCards }
150
170
  }
@@ -0,0 +1,49 @@
1
+ import { useCallback, useLayoutEffect, useState } from 'react'
2
+
3
+ import { useCardSnapAnimation, type SnapDirection } from './useCardSnapAnimation'
4
+ import { type AttemptStatus } from '../fluency.constants'
5
+ import type { FluencyCardDisplayContent } from '../components/fluency-solving/fluencySolving.types'
6
+
7
+ type StartCardExitParams = {
8
+ cardContent: FluencyCardDisplayContent
9
+ direction: SnapDirection
10
+ status: AttemptStatus
11
+ }
12
+
13
+ export const useFluencyCardDeckAnimation = () => {
14
+ const [exitingCardContent, setExitingCardContent] = useState<FluencyCardDisplayContent | null>(
15
+ null,
16
+ )
17
+ const [exitingCardStatus, setExitingCardStatus] = useState<AttemptStatus | null>(null)
18
+ const [shouldResetBackingCards, setShouldResetBackingCards] = useState(false)
19
+
20
+ const clearExitingCard = useCallback(() => {
21
+ setExitingCardContent(null)
22
+ setExitingCardStatus(null)
23
+ setShouldResetBackingCards(true)
24
+ }, [])
25
+
26
+ const animation = useCardSnapAnimation({ onComplete: clearExitingCard })
27
+
28
+ useLayoutEffect(() => {
29
+ if (!shouldResetBackingCards || exitingCardContent) return
30
+
31
+ animation.resetBackingCards()
32
+ setShouldResetBackingCards(false)
33
+ })
34
+
35
+ const startCardExit = ({ cardContent, direction, status }: StartCardExitParams) => {
36
+ if (!animation.trigger(direction)) return false
37
+
38
+ setExitingCardContent(cardContent)
39
+ setExitingCardStatus(status)
40
+ return true
41
+ }
42
+
43
+ return {
44
+ exitingCardContent,
45
+ exitingCardStatus,
46
+ snapStyles: animation.snapStyles,
47
+ startCardExit,
48
+ }
49
+ }
@@ -1,11 +1,15 @@
1
1
  import { useUnit } from 'effector-react'
2
2
  import { COLORS } from '@magmamath/react-native-ui'
3
3
  import type { ButtonColor } from '@magmamath/react-native-ui'
4
- import { FLUENCY_MODE_PRESETS, FluencyMasteryLevel } from '../fluency.constants'
4
+ import { FLUENCY_MODE_PRESETS, FluencyMasteryLevel, type FluencyMode } from '../fluency.constants'
5
5
  import type { LegendItem } from '../fluency.types'
6
6
  import { useFluency } from '../context/FluencyContext'
7
7
  import { useText } from '../../../i18n/i18n'
8
8
 
9
+ type UseFluencySolvingPresetParams = {
10
+ mode?: FluencyMode
11
+ }
12
+
9
13
  type UseFluencySolvingPresetResult = {
10
14
  symbol: string
11
15
  label: string
@@ -18,21 +22,35 @@ type UseFluencySolvingPresetResult = {
18
22
  legend: { items: LegendItem[] }
19
23
  }
20
24
 
21
- export const useFluencySolvingPreset = (): UseFluencySolvingPresetResult => {
25
+ export const useFluencySolvingPreset = (
26
+ params?: UseFluencySolvingPresetParams,
27
+ ): UseFluencySolvingPresetResult => {
22
28
  const t = useText()
23
29
  const { model } = useFluency()
24
30
  const { matrixMode, current } = useUnit({
25
31
  matrixMode: model.matrix.$mode,
26
32
  current: model.solving.$current,
27
33
  })
28
- const preset = FLUENCY_MODE_PRESETS[current?.mode ?? matrixMode]
34
+ const preset = FLUENCY_MODE_PRESETS[params?.mode ?? current?.mode ?? matrixMode]
29
35
 
30
36
  const legendItems: LegendItem[] = [
31
37
  { label: t('student.fluency.notStarted'), color: COLORS.NEUTRAL_2, withBorder: true },
32
- { label: t('student.fluency.masteryLearning'), color: preset.matrix.levelColors[FluencyMasteryLevel.LEARNING] },
33
- { label: t('student.fluency.masteryGrowing'), color: preset.matrix.levelColors[FluencyMasteryLevel.GROWING] },
34
- { label: t('student.fluency.masteryStrong'), color: preset.matrix.levelColors[FluencyMasteryLevel.STRONG] },
35
- { label: t('student.fluency.masteryMastered'), color: preset.matrix.levelColors[FluencyMasteryLevel.MASTERED] },
38
+ {
39
+ label: t('student.fluency.masteryLearning'),
40
+ color: preset.matrix.levelColors[FluencyMasteryLevel.LEARNING],
41
+ },
42
+ {
43
+ label: t('student.fluency.masteryGrowing'),
44
+ color: preset.matrix.levelColors[FluencyMasteryLevel.GROWING],
45
+ },
46
+ {
47
+ label: t('student.fluency.masteryStrong'),
48
+ color: preset.matrix.levelColors[FluencyMasteryLevel.STRONG],
49
+ },
50
+ {
51
+ label: t('student.fluency.masteryMastered'),
52
+ color: preset.matrix.levelColors[FluencyMasteryLevel.MASTERED],
53
+ },
36
54
  ]
37
55
 
38
56
  return {