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 (
);
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 = () => {
) : (
)}
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 dan coba simulasikan
+
+
+

+
+
+ Tebak Huruf
+
+
Tebak huruf dan coba simulasikan
+
-
-
-

-
-
Menyusun Huruf
-
Susun huruf jadi kata yang tepat
+
+
+
+

+
+
+ 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
+
+
+

+
+
+ );
+};
+
+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 (
+
+
+
+

+
+ 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..
+
+
+
+
+ )}
+
+
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+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 (
+
+
+
+

+
A
+
+ Jawaban kamu Salah
+
+
+
+
+
+
+
Soal: 1 / 10
+
+
+
+ {loadCamera ? (
+
+ {!showAnswer && (
+
+
+ Tebak Huruf K
+
+
+ )}
+ {handPresence && !showAnswer && (
+
+
+
+
+
Tahan Tangan..
+
+
+
+
+ )}
+
+
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+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
+
+
+

+
+
+ );
+};
+
+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;