feat: quiz tebak huruf

This commit is contained in:
mphstar 2025-02-25 20:26:23 +07:00
parent 3cde38dd5e
commit 7c37026804
6 changed files with 264 additions and 61 deletions

View File

@ -10,7 +10,7 @@ const NavLink = ({ href, name, isActive }: NavLinkProps) => {
<a href={href}> <a href={href}>
<li <li
className={cn( className={cn(
"btn bg-transparent border-none", "btn bg-transparent shadow-none border-none",
isActive ? "text-primary" : "" isActive ? "text-primary" : ""
)} )}
> >

View File

@ -1,11 +1,49 @@
import LayoutPage from "@/components/templates/LayoutPage"; import LayoutPage from "@/components/templates/LayoutPage";
import useMenyusunHurufStore from "@/stores/MenyusunHurufStore"; import useMenyusunHurufStore from "@/stores/MenyusunHurufStore";
import useNavbarStore from "@/stores/NavbarStore";
import { useEffect } from "react"; import { useEffect } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
const MenyusunHuruf = () => { const MenyusunHuruf = () => {
const quizStore = useMenyusunHurufStore(); const quizStore = useMenyusunHurufStore();
const store = useNavbarStore();
useEffect(() => {
store.setNavSelected("kuis");
}, []);
const saveData = async () => {
try {
await fetch("https://ksuli-api.deno.dev/proses-kuis", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer KSULI_TOKEN_321`,
},
body: JSON.stringify({
kategori_id: "rec_cuum7c5qrj60bgubcjog",
person_name: quizStore.name,
score: parseInt(quizStore.time.toString()),
}),
});
Swal.close();
} catch (error) {
console.error("Error saving data:", error);
Swal.fire({
icon: "error",
title: "Oops...",
text: "Something went wrong while saving your data!",
});
}
Swal.fire({
icon: "success",
title: "Kuis telah selesai",
text: `Anda menyelesaikan kuis dalam waktu ${quizStore.time} detik`,
});
};
const shuffleArray = (array: any[]) => { const shuffleArray = (array: any[]) => {
for (let i = array.length - 1; i > 0; i--) { for (let i = array.length - 1; i > 0; i--) {
@ -19,11 +57,7 @@ const MenyusunHuruf = () => {
useEffect(() => { useEffect(() => {
if (quizStore.isFinish) { if (quizStore.isFinish) {
Swal.fire({ saveData();
icon: "success",
title: "Kuis telah selesai",
text: `Anda menyelesaikan kuis dalam waktu ${quizStore.time} detik`,
});
} }
}, [quizStore.isFinish]); }, [quizStore.isFinish]);

View File

@ -6,7 +6,6 @@ import calcLandmarkList from "@/utils/CalculateLandmark";
import preProcessLandmark from "@/utils/PreProcessLandmark"; import preProcessLandmark from "@/utils/PreProcessLandmark";
import { abjads } from "@/utils/ConvertResult"; import { abjads } from "@/utils/ConvertResult";
import useNavbarStore from "@/stores/NavbarStore"; import useNavbarStore from "@/stores/NavbarStore";
import ProgressBar from "@/components/molecules/ProgressBar";
import { MdOutlineQuiz } from "react-icons/md"; import { MdOutlineQuiz } from "react-icons/md";
import useMenyusunHurufStore from "@/stores/MenyusunHurufStore"; import useMenyusunHurufStore from "@/stores/MenyusunHurufStore";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@ -94,7 +93,6 @@ const Quiz = () => {
} }
}; };
const [progress, setProgress] = useState(0);
const [answer, setAnswer] = useState(""); const [answer, setAnswer] = useState("");
let tempAnswer = ""; let tempAnswer = "";
@ -196,7 +194,6 @@ const Quiz = () => {
Swal.showLoading(); Swal.showLoading();
}, },
}); });
await saveData(parseInt(answerTime.toString()));
navigate("/kuis/menyusun-huruf/"); navigate("/kuis/menyusun-huruf/");
} }
@ -204,32 +201,6 @@ const Quiz = () => {
} }
}; };
const saveData = async (time: number) => {
try {
await fetch("https://ksuli-api.deno.dev/proses-kuis", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer KSULI_TOKEN_321`,
},
body: JSON.stringify({
kategori_id: "rec_cuum7c5qrj60bgubcjog",
person_name: quizStore.name,
score: time,
}),
});
Swal.close();
} catch (error) {
console.error("Error saving data:", error);
Swal.fire({
icon: "error",
title: "Oops...",
text: "Something went wrong while saving your data!",
});
}
};
const detectHands = async () => { const detectHands = async () => {
if (showAnswer) { if (showAnswer) {
return; return;
@ -256,10 +227,8 @@ const Quiz = () => {
makePrediction(finalResult); makePrediction(finalResult);
} else { } else {
setHandPresence(false); setHandPresence(false);
setProgress(0);
} }
} else { } else {
setProgress(0);
} }
} }
} }
@ -316,13 +285,13 @@ const Quiz = () => {
{!showAnswer && ( {!showAnswer && (
<div className="top-6 left-6 absolute flex flex-col gap-2"> <div className="top-6 left-6 absolute flex flex-col gap-2">
<div className="flex gap-2 items-center bg-white text-black rounded-md drop-shadow px-3 py-2"> <div className="flex gap-2 items-center bg-white text-black rounded-md drop-shadow px-3 py-2">
<h1 className="text-2xl font-semibold text-center"> <h1 className="text-xs md:text-2xl font-semibold text-center">
Susun huruf "{quizStore.listSoal[quizStore.soalIndex]}" Susun huruf "{quizStore.listSoal[quizStore.soalIndex]}"
</h1> </h1>
</div> </div>
{answer.length > 0 && ( {answer.length > 0 && (
<div className="flex gap-2 items-center bg-white text-black w-fit rounded-md drop-shadow px-3 py-2"> <div className="flex gap-2 items-center bg-white text-black w-fit rounded-md drop-shadow px-3 py-2">
<h1 className="text-2xl font-semibold text-center"> <h1 className="text-xs md:text-2xl font-semibold text-center">
{answer} {answer}
</h1> </h1>
</div> </div>
@ -334,9 +303,7 @@ const Quiz = () => {
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="loader"></span> <span className="loader"></span>
<h1>Tahan Tangan..</h1>
</div> </div>
<ProgressBar progress={progress} />
</div> </div>
</div> </div>
)} )}

View File

@ -4,10 +4,13 @@ import * as tf from "@tensorflow/tfjs";
import { FilesetResolver, HandLandmarker } from "@mediapipe/tasks-vision"; import { FilesetResolver, HandLandmarker } from "@mediapipe/tasks-vision";
import calcLandmarkList from "@/utils/CalculateLandmark"; import calcLandmarkList from "@/utils/CalculateLandmark";
import preProcessLandmark from "@/utils/PreProcessLandmark"; import preProcessLandmark from "@/utils/PreProcessLandmark";
import ConvertResult from "@/utils/ConvertResult"; import ConvertResult, { abjads } from "@/utils/ConvertResult";
import useNavbarStore from "@/stores/NavbarStore"; import useNavbarStore from "@/stores/NavbarStore";
import ProgressBar from "@/components/molecules/ProgressBar"; import ProgressBar from "@/components/molecules/ProgressBar";
import { MdOutlineQuiz } from "react-icons/md"; import { MdOutlineQuiz } from "react-icons/md";
import useTebakHurufStore from "@/stores/TebakHurufStore";
import { useNavigate } from "react-router-dom";
import Swal from "sweetalert2";
// type PredictResult = { // type PredictResult = {
// abjad: String; // abjad: String;
@ -85,6 +88,9 @@ const Quiz = () => {
let previousResult: string[] = []; let previousResult: string[] = [];
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
let noSoal = 0;
let isLoading = false;
const navigate = useNavigate();
const makePrediction = async (finalResult: any) => { const makePrediction = async (finalResult: any) => {
const input = tf.tensor2d([finalResult]); const input = tf.tensor2d([finalResult]);
@ -127,13 +133,45 @@ const Quiz = () => {
} }
if (previousResult.length == 11) { if (previousResult.length == 11) {
isLoading = true;
if (abjads[parseInt(maxKey)] === quizStore.listSoal[noSoal]) {
quizStore.addJawaban({
jawaban: abjads[parseInt(maxKey)],
isCorrect: true,
});
} else {
quizStore.addJawaban({
jawaban: abjads[parseInt(maxKey)],
isCorrect: false,
});
}
setShowAnswer(true); setShowAnswer(true);
previousResult = [];
setProgress(0);
setTimeout(() => { setTimeout(() => {
isLoading = false;
setShowAnswer(false); setShowAnswer(false);
setProgress(0);
noSoal++;
previousResult = [];
quizStore.setSoalIndex(noSoal);
if (noSoal === quizStore.listSoal.length) {
quizStore.setSession(false);
quizStore.setIsFinish(true);
Swal.fire({
title: "Loading",
text: "Proses menyimpan data...",
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
},
});
navigate("/kuis/tebak-huruf");
}
}, 2000); }, 2000);
} }
@ -163,7 +201,9 @@ const Quiz = () => {
const calt = calcLandmarkList(videoRef.current, landm); const calt = calcLandmarkList(videoRef.current, landm);
const finalResult = preProcessLandmark(calt); const finalResult = preProcessLandmark(calt);
if (!isLoading) {
makePrediction(finalResult); makePrediction(finalResult);
}
} else { } else {
setHandPresence(false); setHandPresence(false);
setProgress(0); setProgress(0);
@ -179,6 +219,15 @@ const Quiz = () => {
}; };
const store = useNavbarStore(); const store = useNavbarStore();
const quizStore = useTebakHurufStore();
console.log(quizStore.jawaban);
useEffect(() => {
if (!quizStore.session) {
window.location.href = "/kuis/tebak-huruf";
}
}, []);
useEffect(() => { useEffect(() => {
store.setNavSelected("kuis"); store.setNavSelected("kuis");
@ -205,33 +254,48 @@ const Quiz = () => {
<div className="rounded-md px-3 py-2 text-white flex flex-col justify-center items-center gap-3"> <div className="rounded-md px-3 py-2 text-white flex flex-col justify-center items-center gap-3">
<img <img
className="h-56" className="h-56"
src="/assets/gif/salah.gif" src={`/assets/gif/${
alt="Jawaban Salah" quizStore.jawaban[quizStore.soalIndex]?.isCorrect
? "betul"
: "salah"
}.gif`}
alt={`Jawaban ${
quizStore.jawaban[quizStore.soalIndex]?.isCorrect
? "Benar"
: "Salah"
}`}
/> />
<p className="text-center text-6xl font-bold">A</p> <p className="text-center text-6xl font-bold">
{quizStore.jawaban[quizStore.soalIndex]?.jawaban}
</p>
<h1 className="text-2xl font-semibold text-center"> <h1 className="text-2xl font-semibold text-center">
Jawaban kamu Salah Jawaban kamu{" "}
{quizStore.jawaban[quizStore.soalIndex]?.isCorrect
? "Benar"
: "Salah"}
</h1> </h1>
</div> </div>
</div> </div>
<div className="flex items-center gap-2 mt-4"> <div className="flex items-center gap-2 mt-4">
<MdOutlineQuiz size={18} /> <MdOutlineQuiz size={18} />
<h1 className="text-xl font-semibold">Soal: 1 / 10</h1> <h1 className="text-xl font-semibold">
Soal: {quizStore.soalIndex + 1} / {quizStore.listSoal.length}
</h1>
</div> </div>
<div className="flex flex-col flex-1 py-4"> <div className="flex flex-col flex-1 py-4">
{loadCamera ? ( {loadCamera ? (
<div className="rounded-md overflow-hidden relative"> <div className="rounded-md overflow-hidden relative">
{!showAnswer && ( {!showAnswer && (
<div className="top-6 left-6 absolute flex gap-2 items-center bg-white text-black rounded-md drop-shadow px-3 py-2"> <div className="md:top-6 top-3 left-3 md:left-6 absolute flex gap-2 items-center bg-white text-black rounded-md drop-shadow px-3 py-2">
<h1 className="text-2xl font-semibold text-center"> <h1 className="md:text-2xl font-semibold text-center">
Tebak Huruf K Tebak Huruf {quizStore.listSoal[quizStore.soalIndex]}
</h1> </h1>
</div> </div>
)} )}
{handPresence && !showAnswer && ( {handPresence && !showAnswer && (
<div className="top-6 right-6 absolute flex gap-2 items-center bg-white text-black rounded-md drop-shadow px-3 py-2 w-fit"> <div className="bottom-3 md:bottom-auto md:top-6 right-3 md:right-6 absolute flex gap-2 items-center bg-white text-black rounded-md drop-shadow px-3 py-2 w-fit">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="loader"></span> <span className="loader"></span>

View File

@ -1,7 +1,128 @@
import LayoutPage from "@/components/templates/LayoutPage"; import LayoutPage from "@/components/templates/LayoutPage";
import { Link } from "react-router-dom"; import useNavbarStore from "@/stores/NavbarStore";
import useTebakHurufStore from "@/stores/TebakHurufStore";
import { useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import Swal from "sweetalert2";
const TebakHuruf = () => { const TebakHuruf = () => {
const quizStore = useTebakHurufStore();
const store = useNavbarStore();
useEffect(() => {
store.setNavSelected("kuis");
}, []);
const shuffleSoal = () => {
const arr = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
];
const shuffledArr = arr.sort(() => 0.5 - Math.random()).slice(0, 10);
return shuffledArr;
};
const saveData = async () => {
let score = 0;
let benar = 0;
let salah = 0;
try {
quizStore.jawaban.forEach((jawaban) => {
if (jawaban.isCorrect) {
score += 10;
benar += 1;
} else {
salah += 1;
}
});
await fetch("https://ksuli-api.deno.dev/proses-kuis", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer KSULI_TOKEN_321`,
},
body: JSON.stringify({
kategori_id: "rec_cuum78tqrj678tmbcjh0",
person_name: quizStore.name,
score: score,
}),
});
Swal.close();
} catch (error) {
console.error("Error saving data:", error);
Swal.fire({
icon: "error",
title: "Oops...",
text: "Something went wrong while saving your data!",
});
}
Swal.fire({
icon: "success",
title: "Kuis telah selesai",
html: `
<p>Anda menyelesaikan kuis dengan score: ${score}</p>
<p>Jawaban benar: ${benar}</p>
<p>Jawaban salah: ${salah}</p>
`,
});
};
const router = useNavigate();
const ProsesKuis = () => {
if (quizStore.name === "") {
Swal.fire({
icon: "error",
title: "Oops...",
text: "Isi nama terlebih dahulu",
});
return;
}
quizStore.setSoalIndex(0);
quizStore.setSession(true);
quizStore.setListSoal(shuffleSoal());
router("/kuis/tebak-huruf/app");
};
useEffect(() => {
if (quizStore.isFinish) {
saveData();
}
return () => {
quizStore.setIsFinish(false);
};
}, [quizStore.isFinish]);
return ( return (
<LayoutPage> <LayoutPage>
<div className="flex flex-col flex-1 py-4 relative"> <div className="flex flex-col flex-1 py-4 relative">
@ -25,13 +146,18 @@ const TebakHuruf = () => {
<input <input
placeholder="Name.." placeholder="Name.."
type="text" type="text"
value={quizStore.name}
onChange={(e) => quizStore.setName(e.target.value)}
className="bg-transparent outline-none p-2 px-8 flex-1 w-full" className="bg-transparent outline-none p-2 px-8 flex-1 w-full"
/> />
<Link to="/kuis/tebak-huruf/app"> <button
<button className="bg-blue-500 hover:bg-blue-700 text-white px-3 py-2 rounded-full whitespace-nowrap"> onClick={() => {
ProsesKuis();
}}
className="bg-blue-500 hover:bg-blue-700 text-white px-3 py-2 rounded-full whitespace-nowrap"
>
Mulai Kuis Mulai Kuis
</button> </button>
</Link>
</div> </div>
<span className="text-primary">Lihat Ranking</span> <span className="text-primary">Lihat Ranking</span>
</div> </div>

View File

@ -15,6 +15,12 @@ type TebakHurufType = {
jawaban: JawabanType[]; jawaban: JawabanType[];
setJawaban: (jawaban: JawabanType[]) => void; setJawaban: (jawaban: JawabanType[]) => void;
addJawaban: (jawaban: JawabanType) => void; addJawaban: (jawaban: JawabanType) => void;
session: boolean;
setSession: (session: boolean) => void;
name: string;
setName: (name: string) => void;
isFinish: boolean;
setIsFinish: (isFinish: boolean) => void;
}; };
const useTebakHurufStore = create<TebakHurufType>((set) => ({ const useTebakHurufStore = create<TebakHurufType>((set) => ({
@ -28,6 +34,12 @@ const useTebakHurufStore = create<TebakHurufType>((set) => ({
setJawaban: (jawaban) => set({ jawaban: jawaban }), setJawaban: (jawaban) => set({ jawaban: jawaban }),
addJawaban: (jawaban) => addJawaban: (jawaban) =>
set((state) => ({ jawaban: [...state.jawaban, jawaban] })), set((state) => ({ jawaban: [...state.jawaban, jawaban] })),
session: false,
setSession: (session) => set({ session: session }),
name: "",
setName: (name) => set({ name: name }),
isFinish: false,
setIsFinish: (isFinish) => set({ isFinish: isFinish }),
})); }));
export default useTebakHurufStore; export default useTebakHurufStore;