diff --git a/package-lock.json b/package-lock.json index 739d041..e76f6c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@mediapipe/tasks-vision": "^0.10.14", "@tensorflow/tfjs": "^4.20.0", "clsx": "^2.1.1", + "motion": "^12.4.7", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.3.0", @@ -2815,6 +2816,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.4.7", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.7.tgz", + "integrity": "sha512-VhrcbtcAMXfxlrjeHPpWVu2+mkcoR31e02aNSR7OUS/hZAciKa8q6o3YN2mA1h+jjscRsSyKvX6E1CiY/7OLMw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.4.5", + "motion-utils": "^12.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3317,6 +3345,47 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion": { + "version": "12.4.7", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.4.7.tgz", + "integrity": "sha512-mhegHAbf1r80fr+ytC6OkjKvIUegRNXKLWNPrCN2+GnixlNSPwT03FtKqp9oDny1kNcLWZvwbmEr+JqVryFrcg==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.4.7", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.4.5", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.4.5.tgz", + "integrity": "sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.0.0" + } + }, + "node_modules/motion-utils": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz", + "integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4360,6 +4429,12 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index a85dd3a..33cb24f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@mediapipe/tasks-vision": "^0.10.14", "@tensorflow/tfjs": "^4.20.0", "clsx": "^2.1.1", + "motion": "^12.4.7", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.3.0", diff --git a/public/assets/gif/betul.gif b/public/assets/gif/betul.gif new file mode 100644 index 0000000..bdc19ee Binary files /dev/null and b/public/assets/gif/betul.gif differ diff --git a/public/assets/gif/salah.gif b/public/assets/gif/salah.gif new file mode 100644 index 0000000..bc039af Binary files /dev/null and b/public/assets/gif/salah.gif differ diff --git a/public/assets/images/overlay-bg.png b/public/assets/images/overlay-bg.png new file mode 100644 index 0000000..88088cb Binary files /dev/null and b/public/assets/images/overlay-bg.png differ diff --git a/src/components/molecules/ProgressBar.tsx b/src/components/molecules/ProgressBar.tsx new file mode 100644 index 0000000..e4696a6 --- /dev/null +++ b/src/components/molecules/ProgressBar.tsx @@ -0,0 +1,17 @@ +import { motion } from "framer-motion"; + +type ProgressBarProps = { + progress: number; // nilai antara 0 - 100 +}; + +export default function ProgressBar({ progress }: ProgressBarProps) { + return ( +
+ +
+ ); +} diff --git a/src/components/organisms/MyLoading.tsx b/src/components/organisms/MyLoading.tsx index da7fefd..230241b 100644 --- a/src/components/organisms/MyLoading.tsx +++ b/src/components/organisms/MyLoading.tsx @@ -3,7 +3,7 @@ import React from 'react'; const MyLoading: React.FC = () => { return (
-
+

Loading...

); diff --git a/src/index.css b/src/index.css index 909e743..33d0abd 100644 --- a/src/index.css +++ b/src/index.css @@ -8,4 +8,24 @@ padding: 0; box-sizing: border-box; @apply font-poppins; -} \ No newline at end of file +} + +.loader { + width: 24px; + height: 24px; + border: 3px solid black; + border-bottom-color: transparent; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; + } + + @keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } \ No newline at end of file diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 7ca7af8..2b8c2da 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -37,6 +37,7 @@ const Home = () => { if (videoRef.current) { videoRef.current.srcObject = stream; } + setLoadCamera(true); // setLoadCamera(true); await initializeHandDetection(); @@ -145,7 +146,7 @@ const Home = () => { loadModel(); startWebcam(); - setLoadCamera(true); + return () => { if (handLandmarker) { @@ -180,7 +181,7 @@ const Home = () => { ) : (
-
+

Loading...

)} diff --git a/src/pages/Kuis.tsx b/src/pages/Kuis.tsx index eae45a7..b1d2fba 100644 --- a/src/pages/Kuis.tsx +++ b/src/pages/Kuis.tsx @@ -1,6 +1,7 @@ import LayoutPage from "@/components/templates/LayoutPage"; import useNavbarStore from "@/stores/NavbarStore"; import { useEffect } from "react"; +import { Link } from "react-router-dom"; const Kuis = () => { const store = useNavbarStore(); @@ -11,31 +12,39 @@ const Kuis = () => { return (
-

Ayoo Kuiss

+

Ayoo Kuiss

Be the first!

-
- Tebak Huruf -
-

Tebak Huruf

-

Tebak huruf dan coba simulasikan

+ +
+ Tebak Huruf +
+

+ Tebak Huruf +

+

Tebak huruf dan coba simulasikan

+
-
-
- Menyusun Huruf -
-

Menyusun Huruf

-

Susun huruf jadi kata yang tepat

+ + +
+ Menyusun Huruf +
+

+ Menyusun Huruf +

+

Susun huruf jadi kata yang tepat

+
-
+
diff --git a/src/pages/Kuis/MenyusunHuruf/MenyusunHuruf.tsx b/src/pages/Kuis/MenyusunHuruf/MenyusunHuruf.tsx new file mode 100644 index 0000000..990922b --- /dev/null +++ b/src/pages/Kuis/MenyusunHuruf/MenyusunHuruf.tsx @@ -0,0 +1,66 @@ +import LayoutPage from "@/components/templates/LayoutPage"; +import useMenyusunHurufStore from "@/stores/MenyusunHurufStore"; +import { Link } from "react-router-dom"; + +const MenyusunHuruf = () => { + const quizStore = useMenyusunHurufStore(); + + const shuffleArray = (array: any[]) => { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + }; + + return ( + +
+
    +
  • Home
  • +
  • {">"}
  • + + Kuis + +
  • {">"}
  • +
  • Menyusun Huruf
  • +
+ +
+

+ Start Your Quiz! +

+
+

Masukkan Nama

+
+ + + + +
+ Lihat Ranking +
+
+ Overlay Background +
+
+ ); +}; + +export default MenyusunHuruf; diff --git a/src/pages/Kuis/MenyusunHuruf/Quiz.tsx b/src/pages/Kuis/MenyusunHuruf/Quiz.tsx new file mode 100644 index 0000000..6cdcc2c --- /dev/null +++ b/src/pages/Kuis/MenyusunHuruf/Quiz.tsx @@ -0,0 +1,327 @@ +import LayoutPage from "@/components/templates/LayoutPage"; +import { useEffect, useRef, useState } from "react"; +import * as tf from "@tensorflow/tfjs"; +import { FilesetResolver, HandLandmarker } from "@mediapipe/tasks-vision"; +import calcLandmarkList from "@/utils/CalculateLandmark"; +import preProcessLandmark from "@/utils/PreProcessLandmark"; +import { abjads } from "@/utils/ConvertResult"; +import useNavbarStore from "@/stores/NavbarStore"; +import ProgressBar from "@/components/molecules/ProgressBar"; +import { MdOutlineQuiz } from "react-icons/md"; +import useMenyusunHurufStore from "@/stores/MenyusunHurufStore"; +import { useNavigate } from "react-router-dom"; + +// type PredictResult = { +// abjad: String; +// acc: String; +// }; + +const Quiz = () => { + const quizStore = useMenyusunHurufStore(); + + useEffect(() => { + if (!quizStore.session) { + window.location.href = "/kuis/menyusun-huruf"; + } + }, []); + + const videoRef = useRef(null); + const [loadCamera, setLoadCamera] = useState(false); + const canvasRef = useRef(null); + + // const [setResultPredict] = useState({ + // abjad: "", + // acc: "", + // }); + + const [showAnswer, setShowAnswer] = useState(false); + + let model: tf.LayersModel; + let handLandmarker: HandLandmarker; + + const [handPresence, setHandPresence] = useState(false); + + const startWebcam = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + }); + + if (videoRef.current) { + videoRef.current.srcObject = stream; + } + + setLoadCamera(true); + + // setLoadCamera(true); + await initializeHandDetection(); + } catch (error) { + console.error("Error accessing webcam:", error); + } + }; + + const loadModel = async () => { + setLoadCamera(false); + try { + const lm = await tf.loadLayersModel("/model/model.json"); + model = lm; + + const emptyInput = tf.tensor2d([[0, 0]]); + + model.predict(emptyInput) as tf.Tensor; + + setLoadCamera(true); + } catch (error) { + // console.error("Error loading model:", error); + } + }; + + const initializeHandDetection = async () => { + try { + const vision = await FilesetResolver.forVisionTasks( + "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm" + ); + handLandmarker = await HandLandmarker.createFromOptions(vision, { + baseOptions: { + modelAssetPath: `https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task`, + }, + numHands: 2, + runningMode: "VIDEO", + }); + + detectHands(); + } catch (error) { + console.error("Error initializing hand detection:", error); + } + }; + + const [progress, setProgress] = useState(0); + const [answer, setAnswer] = useState(""); + + let tempAnswer = ""; + let noSoal = 0; + let startTimer: number = 0; + let answerTime: number = 0; + const navigate = useNavigate(); + + const [resultAnswer, setResultAnswer] = useState({ + minutes: 0, + seconds: 0, + }); + + const makePrediction = async (finalResult: any) => { + if (startTimer === 0) { + startTimer = Date.now(); // Mulai waktu saat pertama kali prediksi + } + + const input = tf.tensor2d([finalResult]); + + // Melakukan prediksi + const prediction = model.predict(input) as tf.Tensor; + + const result = prediction.dataSync(); + + const maxEntry = Object.entries(result).reduce((max, entry) => { + const [, value] = entry; + return value > max[1] ? entry : max; + }); + + // maxEntry sekarang berisi [key, value] dengan nilai terbesar + const [maxKey] = maxEntry; + + // const percentageValue = (maxValue * 100).toFixed(2) + "%"; + + let currentResult = abjads[parseInt(maxKey)]; + + // Hapus tensor + input.dispose(); + prediction.dispose(); + + if (tempAnswer.length == 0) { + const firstChar = quizStore.listSoal[noSoal].charAt(0); + console.log(noSoal); + + if (currentResult === firstChar) { + tempAnswer += currentResult; + } + } + + if ( + currentResult === quizStore.listSoal[noSoal].charAt(tempAnswer.length) + ) { + tempAnswer += currentResult; + } + + setAnswer(tempAnswer); + + if (tempAnswer === quizStore.listSoal[noSoal]) { + setShowAnswer(true); + + const elapsedTime = (Date.now() - startTimer) / 1000; + + const minutes = Math.floor(elapsedTime / 60); + const seconds = Math.floor(elapsedTime % 60); + + setResultAnswer({ + minutes, + seconds, + }); + + startTimer = 0; + tempAnswer = ""; + setAnswer(""); + + setTimeout(() => { + setShowAnswer(false); + setResultAnswer({ + minutes: 0, + seconds: 0, + }); + }, 3000); + + quizStore.setSoalIndex(noSoal + 1); + + answerTime = answerTime += elapsedTime; + + noSoal++; + + if (noSoal === 10) { + quizStore.setTime(answerTime); + quizStore.setSession(false); + + navigate("/kuis/menyusun-huruf/"); + } + } + }; + + const detectHands = async () => { + if (showAnswer) { + return; + } + + if (videoRef.current && videoRef.current.readyState >= 2) { + const detections = handLandmarker.detectForVideo( + videoRef.current, + performance.now() + ); + + setHandPresence(detections.handedness.length > 0); + // Assuming detections.landmarks is an array of landmark objects + if (detections.landmarks) { + if (detections.handednesses.length > 0) { + // console.log(detections); + + if (detections.handednesses[0][0].displayName === "Right") { + const landm = detections.landmarks[0].map((landmark) => landmark); + + const calt = calcLandmarkList(videoRef.current, landm); + const finalResult = preProcessLandmark(calt); + + makePrediction(finalResult); + } else { + setHandPresence(false); + setProgress(0); + } + } else { + setProgress(0); + } + } + } + requestAnimationFrame(detectHands); + }; + + const store = useNavbarStore(); + + useEffect(() => { + store.setNavSelected("kuis"); + + loadModel(); + startWebcam(); + + return () => { + if (handLandmarker) { + handLandmarker.close(); + } + }; + }, []); + + return ( + +
+
+ Jawaban Betul +

+ Kamu berhasil menyusun dalam {resultAnswer.minutes} Menit{" "} + {resultAnswer.seconds} Detik +

+
+
+ +
+ +

+ Soal: {quizStore.soalIndex + 1} / 10 +

+
+ +
+ {loadCamera ? ( +
+ {!showAnswer && ( +
+
+

+ Susun huruf "{quizStore.listSoal[quizStore.soalIndex]}" +

+
+ {answer.length > 0 && ( +
+

+ {answer} +

+
+ )} +
+ )} + {handPresence && !showAnswer && ( +
+
+
+ +

Tahan Tangan..

+
+ +
+
+ )} + + +
+ ) : ( +
+
+

Loading...

+
+ )} +
+
+ ); +}; + +export default Quiz; diff --git a/src/pages/Kuis/TebakHuruf/Quiz.tsx b/src/pages/Kuis/TebakHuruf/Quiz.tsx new file mode 100644 index 0000000..219ff02 --- /dev/null +++ b/src/pages/Kuis/TebakHuruf/Quiz.tsx @@ -0,0 +1,266 @@ +import LayoutPage from "@/components/templates/LayoutPage"; +import { useEffect, useRef, useState } from "react"; +import * as tf from "@tensorflow/tfjs"; +import { FilesetResolver, HandLandmarker } from "@mediapipe/tasks-vision"; +import calcLandmarkList from "@/utils/CalculateLandmark"; +import preProcessLandmark from "@/utils/PreProcessLandmark"; +import ConvertResult from "@/utils/ConvertResult"; +import useNavbarStore from "@/stores/NavbarStore"; +import ProgressBar from "@/components/molecules/ProgressBar"; +import { MdOutlineQuiz } from "react-icons/md"; + +// type PredictResult = { +// abjad: String; +// acc: String; +// }; + +const Quiz = () => { + const videoRef = useRef(null); + const [loadCamera, setLoadCamera] = useState(false); + const canvasRef = useRef(null); + + // const [setResultPredict] = useState({ + // abjad: "", + // acc: "", + // }); + + const [showAnswer, setShowAnswer] = useState(false); + + let model: tf.LayersModel; + let handLandmarker: HandLandmarker; + + const [handPresence, setHandPresence] = useState(false); + + const startWebcam = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + }); + + if (videoRef.current) { + videoRef.current.srcObject = stream; + } + + setLoadCamera(true); + + // setLoadCamera(true); + await initializeHandDetection(); + } catch (error) { + console.error("Error accessing webcam:", error); + } + }; + + const loadModel = async () => { + setLoadCamera(false); + try { + const lm = await tf.loadLayersModel("/model/model.json"); + model = lm; + + const emptyInput = tf.tensor2d([[0, 0]]); + + model.predict(emptyInput) as tf.Tensor; + + setLoadCamera(true); + } catch (error) { + // console.error("Error loading model:", error); + } + }; + + const initializeHandDetection = async () => { + try { + const vision = await FilesetResolver.forVisionTasks( + "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm" + ); + handLandmarker = await HandLandmarker.createFromOptions(vision, { + baseOptions: { + modelAssetPath: `https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task`, + }, + numHands: 2, + runningMode: "VIDEO", + }); + + detectHands(); + } catch (error) { + console.error("Error initializing hand detection:", error); + } + }; + + let previousResult: string[] = []; + const [progress, setProgress] = useState(0); + + const makePrediction = async (finalResult: any) => { + const input = tf.tensor2d([finalResult]); + + // Melakukan prediksi + const prediction = model.predict(input) as tf.Tensor; + + const result = prediction.dataSync(); + + const maxEntry = Object.entries(result).reduce((max, entry) => { + const [, value] = entry; + return value > max[1] ? entry : max; + }); + + // maxEntry sekarang berisi [key, value] dengan nilai terbesar + const [maxKey] = maxEntry; + + // const percentageValue = (maxValue * 100).toFixed(2) + "%"; + + // setResultPredict({ + // abjad: ConvertResult(parseInt(maxKey)), + // acc: percentageValue, + // }); + + let currentResult = ConvertResult(parseInt(maxKey)); + + // Hapus tensor + input.dispose(); + prediction.dispose(); + + if ( + previousResult.length > 0 && + previousResult[previousResult.length - 1] === currentResult + ) { + previousResult.push(currentResult); + setProgress((prev) => prev + 10); + } else { + previousResult = [currentResult]; + setProgress(10); + } + + if (previousResult.length == 11) { + setShowAnswer(true); + + previousResult = []; + setProgress(0); + + setTimeout(() => { + setShowAnswer(false); + }, 2000); + } + + // console.log(previousResult); + }; + + const detectHands = async () => { + if (showAnswer) { + return; + } + + if (videoRef.current && videoRef.current.readyState >= 2) { + const detections = handLandmarker.detectForVideo( + videoRef.current, + performance.now() + ); + + setHandPresence(detections.handedness.length > 0); + // Assuming detections.landmarks is an array of landmark objects + if (detections.landmarks) { + if (detections.handednesses.length > 0) { + // console.log(detections); + + if (detections.handednesses[0][0].displayName === "Right") { + const landm = detections.landmarks[0].map((landmark) => landmark); + + const calt = calcLandmarkList(videoRef.current, landm); + const finalResult = preProcessLandmark(calt); + + makePrediction(finalResult); + } else { + setHandPresence(false); + setProgress(0); + previousResult = []; + } + } else { + setProgress(0); + previousResult = []; + } + } + } + requestAnimationFrame(detectHands); + }; + + const store = useNavbarStore(); + + useEffect(() => { + store.setNavSelected("kuis"); + + loadModel(); + startWebcam(); + + return () => { + if (handLandmarker) { + handLandmarker.close(); + } + }; + }, []); + + return ( + +
+
+ Jawaban Salah +

A

+

+ Jawaban kamu Salah +

+
+
+ +
+ +

Soal: 1 / 10

+
+ +
+ {loadCamera ? ( +
+ {!showAnswer && ( +
+

+ Tebak Huruf K +

+
+ )} + {handPresence && !showAnswer && ( +
+
+
+ +

Tahan Tangan..

+
+ +
+
+ )} + + +
+ ) : ( +
+
+

Loading...

+
+ )} +
+
+ ); +}; + +export default Quiz; diff --git a/src/pages/Kuis/TebakHuruf/TebakHuruf.tsx b/src/pages/Kuis/TebakHuruf/TebakHuruf.tsx new file mode 100644 index 0000000..62ab3b5 --- /dev/null +++ b/src/pages/Kuis/TebakHuruf/TebakHuruf.tsx @@ -0,0 +1,49 @@ +import LayoutPage from "@/components/templates/LayoutPage"; +import { Link } from "react-router-dom"; + +const TebakHuruf = () => { + return ( + +
+
    +
  • Home
  • +
  • {">"}
  • + + Kuis + +
  • {">"}
  • +
  • Tebak Huruf
  • +
+ +
+

+ Start Your Quiz! +

+
+

Masukkan Nama

+
+ + + + +
+ Lihat Ranking +
+
+ Overlay Background +
+
+ ); +}; + +export default TebakHuruf; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 5cfd21d..220f833 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -1,26 +1,41 @@ import { lazy } from "react"; -const Home = lazy(() => import("@/pages/Home")); -const Kamus = lazy(() => import("@/pages/Kamus")); -const Kuis = lazy(() => import("@/pages/Kuis")); - const myRoute = [ - { - "title": "Home", - "path": "/", - "component": Home, - }, - { - "title": "Kamus", - "path": "/kamus", - "component": Kamus - }, - { - "title": "Kuis", - "path": "/kuis", - "component": Kuis - }, + { + title: "Home", + path: "/", + component: lazy(() => import("@/pages/Home")), + }, + { + title: "Kamus", + path: "/kamus", + component: lazy(() => import("@/pages/Kamus")), + }, + { + title: "Kuis", + path: "/kuis", + component: lazy(() => import("@/pages/Kuis")), + }, + { + title: "Tebak Huruf", + path: "/kuis/tebak-huruf", + component: lazy(() => import("@/pages/Kuis/TebakHuruf/TebakHuruf")), + }, + { + title: "Start Quiz", + path: "/kuis/tebak-huruf/app", + component: lazy(() => import("@/pages/Kuis/TebakHuruf/Quiz")), + }, + { + title: "Menyusun Huruf", + path: "/kuis/menyusun-huruf", + component: lazy(() => import("@/pages/Kuis/MenyusunHuruf/MenyusunHuruf")), + }, + { + title: "Start Quiz", + path: "/kuis/menyusun-huruf/app", + component: lazy(() => import("@/pages/Kuis/MenyusunHuruf/Quiz")), + }, +]; -] - -export default myRoute; \ No newline at end of file +export default myRoute; diff --git a/src/stores/MenyusunHurufStore.ts b/src/stores/MenyusunHurufStore.ts new file mode 100644 index 0000000..fad161e --- /dev/null +++ b/src/stores/MenyusunHurufStore.ts @@ -0,0 +1,38 @@ +import { create } from "zustand"; + +const soal = [ + "INDONESIA", + "KUCING", + "SIBI", + "MAHASISWA", + "KSULI", + "INFORMATIKA", + "CODING", + "WHATSAPP", + "INSTAGRAM", + "TEMAN", +]; + +type MenyusunHurufType = { + listSoal: string[]; + setListSoal: (listSoal: any[]) => void; + soalIndex: number; + setSoalIndex: (index: number) => void; + time: number; + setTime: (time: number) => void; + session: boolean; + setSession: (session: boolean) => void; +}; + +const useMenyusunHurufStore = create((set) => ({ + listSoal: soal, + setListSoal: (listSoal) => set({ listSoal: listSoal }), + soalIndex: 0, + setSoalIndex: (index) => set({ soalIndex: index }), + time: 0, + setTime: (time) => set({ time: time }), + session: false, + setSession: (session) => set({ session: session }), +})); + +export default useMenyusunHurufStore; diff --git a/src/stores/TebakHurufStore.ts b/src/stores/TebakHurufStore.ts new file mode 100644 index 0000000..9a870c2 --- /dev/null +++ b/src/stores/TebakHurufStore.ts @@ -0,0 +1,33 @@ +import { create } from "zustand"; + +type JawabanType = { + jawaban: string; + isCorrect: boolean; +}; + +type TebakHurufType = { + listSoal: any[]; + setListSoal: (listSoal: any[]) => void; + soalIndex: number; + setSoalIndex: (index: number) => void; + score: number; + setScore: (score: number) => void; + jawaban: JawabanType[]; + setJawaban: (jawaban: JawabanType[]) => void; + addJawaban: (jawaban: JawabanType) => void; +}; + +const useTebakHurufStore = create((set) => ({ + listSoal: [], + setListSoal: (listSoal) => set({ listSoal: listSoal }), + soalIndex: 0, + setSoalIndex: (index) => set({ soalIndex: index }), + score: 0, + setScore: (score) => set({ score: score }), + jawaban: [], + setJawaban: (jawaban) => set({ jawaban: jawaban }), + addJawaban: (jawaban) => + set((state) => ({ jawaban: [...state.jawaban, jawaban] })), +})); + +export default useTebakHurufStore;