handling loading + slicing kuis page

This commit is contained in:
mphstar 2025-02-22 18:32:22 +07:00
parent a3a2bd255b
commit 9a0bb1e26e
14 changed files with 82 additions and 33 deletions

7
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "ksuli-sibi", "name": "ksuli-sibi",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@fontsource/poppins": "^5.1.1",
"@mediapipe/tasks-vision": "^0.10.14", "@mediapipe/tasks-vision": "^0.10.14",
"@tensorflow/tfjs": "^4.20.0", "@tensorflow/tfjs": "^4.20.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -842,6 +843,12 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@fontsource/poppins": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@fontsource/poppins/-/poppins-5.1.1.tgz",
"integrity": "sha512-PQVKHGQOK+UpVNPzTFF9Z9ez3CIDkunfvRXGH95hiDoqWJc1shW4JFqe4tEoqnq7RtZaYrw9aWQq58L4eny5xg==",
"license": "OFL-1.1"
},
"node_modules/@humanwhocodes/module-importer": { "node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",

View File

@ -10,6 +10,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@fontsource/poppins": "^5.1.1",
"@mediapipe/tasks-vision": "^0.10.14", "@mediapipe/tasks-vision": "^0.10.14",
"@tensorflow/tfjs": "^4.20.0", "@tensorflow/tfjs": "^4.20.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,6 +1,7 @@
import { Suspense } from "react"; import { Suspense } from "react";
import { Route, Routes } from "react-router-dom"; import { Route, Routes } from "react-router-dom";
import myRoute from "./routes/routes"; import myRoute from "./routes/routes";
import MyLoading from "./components/organisms/MyLoading";
const App = () => { const App = () => {
return ( return (
@ -11,7 +12,7 @@ const App = () => {
key={index} key={index}
path={route.path} path={route.path}
element={ element={
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<MyLoading />}>
<route.component /> <route.component />
</Suspense> </Suspense>
} }

View File

@ -1,6 +1,4 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Link } from "react-router-dom";
type NavLinkProps = { type NavLinkProps = {
href: string; href: string;
name: string; name: string;
@ -9,7 +7,7 @@ type NavLinkProps = {
const NavLink = ({ href, name, isActive }: NavLinkProps) => { const NavLink = ({ href, name, isActive }: NavLinkProps) => {
return ( return (
<Link to={href}> <a href={href}>
<li <li
className={cn( className={cn(
"btn bg-transparent border-none", "btn bg-transparent border-none",
@ -18,7 +16,7 @@ const NavLink = ({ href, name, isActive }: NavLinkProps) => {
> >
{name} {name}
</li> </li>
</Link> </a>
); );
}; };

View File

@ -8,7 +8,7 @@ const HeaderPage = () => {
const navStore = useNavbarStore(); const navStore = useNavbarStore();
return ( return (
<div className="bg-white drop-shadow w-full h-fit sticky top-0 z-[300]"> <div className="bg-white drop-shadow w-full h-fit sticky top-0 z-[300]">
<header className="flex flex-row items-center gap-2 justify-between px-4 py-3 container max-w-[1200px]"> <header className="flex flex-row items-center gap-2 justify-between px-4 py-3 container max-w-[960px]">
<div className="flex gap-1 items-center"> <div className="flex gap-1 items-center">
<img <img
className="w-10" className="w-10"
@ -17,7 +17,7 @@ const HeaderPage = () => {
/> />
<div className="form-control"> <div className="form-control">
<h1 className="font-semibold">K-SULI</h1> <h1 className="font-semibold">K-SULI</h1>
<p className="text-primary">Kedai Susu Tuli</p> <p className="text-primary text-sm">Kedai Susu Tuli</p>
</div> </div>
</div> </div>
<ul <ul

View File

@ -0,0 +1,12 @@
import React from 'react';
const MyLoading: React.FC = () => {
return (
<div className="flex flex-col items-center justify-center h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-t-4 border-b-4 border-primary"></div>
<p className="mt-4 text-lg text-gray-700">Loading...</p>
</div>
);
};
export default MyLoading;

View File

@ -4,9 +4,9 @@ import FooterPage from "../organisms/FooterPage";
const LayoutPage = ({ children }: { children: React.ReactNode }) => { const LayoutPage = ({ children }: { children: React.ReactNode }) => {
return ( return (
<div className="flex flex-col min-h-svh bg-ground"> <div className="flex flex-col min-h-svh bg-white font-poppins">
<HeaderPage /> <HeaderPage />
<main className="flex flex-col flex-1 container max-w-[1200px]"> <main className="flex flex-col flex-1 container max-w-[960px]">
{children} {children}
</main> </main>
<FooterPage /> <FooterPage />

View File

@ -42,13 +42,13 @@ class MediapipeHelper {
detectHands = async () => { detectHands = async () => {
if (this.videoRef.current === null) { if (this.videoRef.current === null) {
console.error("Video is not initialized."); // console.error("Video is not initialized.");
return; return;
} }
if (this.videoRef && this.videoRef.current.readyState >= 2) { if (this.videoRef && this.videoRef.current.readyState >= 2) {
if (!this.handLandmarker) { if (!this.handLandmarker) {
console.error("HandLandmarker is not initialized."); // console.error("HandLandmarker is not initialized.");
return; return;
} }
const detections = this.handLandmarker.detectForVideo( const detections = this.handLandmarker.detectForVideo(
@ -86,7 +86,6 @@ class MediapipeHelper {
} }
} }
requestAnimationFrame(this.detectHands);
}; };
} }

View File

@ -1,3 +1,4 @@
@import "@fontsource/poppins";
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@ -6,4 +7,5 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
@apply font-poppins;
} }

View File

@ -4,6 +4,7 @@ import { FaCircleCheck } from "react-icons/fa6";
import useNavbarStore from "@/stores/NavbarStore"; import useNavbarStore from "@/stores/NavbarStore";
import MediapipeHelper from "@/helper/MediapipeHelper"; import MediapipeHelper from "@/helper/MediapipeHelper";
import DetectionHelper from "@/helper/DetectionHelper"; import DetectionHelper from "@/helper/DetectionHelper";
import MyLoading from "@/components/organisms/MyLoading";
type PredictResult = { type PredictResult = {
abjad: String; abjad: String;
@ -23,13 +24,18 @@ const Home = () => {
const [handPresence, setHandPresence] = useState(false); const [handPresence, setHandPresence] = useState(false);
const onHandDetected = async () => { const onHandDetected = async () => {
if (!mediapipeHelper || !detectionHelper) {
return;
}
mediapipeHelper.detectHands();
const result = mediapipeHelper.getResult(); const result = mediapipeHelper.getResult();
if (result.handPresence) { if (result.handPresence) {
// console.log("Hand Detected"); // console.log("Hand Detected");
setHandPresence(true); setHandPresence(true);
const predict = await detectionHelper.makePrediction(result.finalResult); const predict = await detectionHelper.makePrediction(result.finalResult);
console.log(predict);
if (predict) { if (predict) {
setResultPredict((prevState) => ({ setResultPredict((prevState) => ({
@ -37,7 +43,6 @@ const Home = () => {
...predict, ...predict,
})); }));
} }
} else { } else {
setHandPresence(false); setHandPresence(false);
} }
@ -51,25 +56,20 @@ const Home = () => {
video: true, video: true,
}); });
setLoadCamera(true);
if (videoRef.current) { if (videoRef.current) {
videoRef.current.srcObject = stream; videoRef.current.srcObject = stream;
}
mediapipeHelper = new MediapipeHelper(videoRef); mediapipeHelper = new MediapipeHelper(videoRef);
detectionHelper = new DetectionHelper(); detectionHelper = new DetectionHelper();
}
onHandDetected(); onHandDetected();
} catch (error) { } catch (error) {
console.error("Error accessing webcam:", error); console.error("Error accessing webcam:", error);
} }
}; };
const store = useNavbarStore(); const store = useNavbarStore();
let mediapipeHelper: MediapipeHelper; let mediapipeHelper: MediapipeHelper;
let detectionHelper: DetectionHelper; let detectionHelper: DetectionHelper;
@ -79,12 +79,14 @@ const Home = () => {
startWebcam(); startWebcam();
setLoadCamera(true);
return () => { return () => {
if (videoRef.current) {
(videoRef.current.srcObject as MediaStream)
?.getTracks()
.forEach((track) => {
track.stop();
});
}
}; };
}, []); }, []);
@ -113,7 +115,10 @@ const Home = () => {
></video> ></video>
</div> </div>
) : ( ) : (
<div>Loading...</div> <div className="flex flex-col items-center justify-center flex-1">
<div className="animate-spin rounded-full h-12 w-12 border-t-4 border-b-4 border-primary"></div>
<p className="mt-4 text-lg text-gray-700">Loading...</p>
</div>
)} )}
</div> </div>
</LayoutPage> </LayoutPage>

View File

@ -11,14 +11,35 @@ const Kuis = () => {
return ( return (
<LayoutPage> <LayoutPage>
<div className="flex flex-col flex-1 py-4"> <div className="flex flex-col flex-1 py-4">
<h1 className="font-semibold">Kuis SIBI</h1> <h1 className="font-semibold text-3xl">Ayoo Kuiss</h1>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mt-6"> <p>Be the first!</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-24 md:gap-6 mt-24 md:mt-52 h-full mb-12">
<div className="relative ">
<img
className="absolute w-24 -top-12 left-4 md:left-12"
src="/assets/images/tebak-huruf.png"
alt="Tebak Huruf"
/>
<div className="flex flex-col w-full bg-[#7FAFEF] hover:bg-[#6290cc] rounded-md px-4 md:px-12 py-6 pb-12 text-white pt-32 duration-300">
<h1 className="font-semibold text-4xl">Tebak Huruf</h1>
<p>Tebak huruf dan coba simulasikan</p>
</div>
</div>
<div className="relative ">
<img
className="absolute w-42 -top-12 left-4 md:left-12"
src="/assets/images/menyusun-huruf.png"
alt="Menyusun Huruf"
/>
<div className="flex flex-col w-full bg-[#FF6884] hover:bg-[#d3546b] rounded-md px-4 md:px-12 py-6 pb-12 text-white pt-32 duration-300">
<h1 className="font-semibold text-4xl">Menyusun Huruf</h1>
<p>Susun huruf jadi kata yang tepat</p>
</div>
</div>
</div> </div>
</div> </div>
</LayoutPage> </LayoutPage>
); );
}; };
export default Kuis; export default Kuis;

View File

@ -3,6 +3,9 @@ export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: { theme: {
extend: { extend: {
fontFamily: {
poppins: ["Poppins", "sans-serif"],
},
container: { container: {
center: true, center: true,
padding: "1rem", padding: "1rem",