feat: slicing page ranking

This commit is contained in:
mphstar 2025-02-26 12:11:20 +07:00
parent 1c1e828315
commit ae17e2d0ba
12 changed files with 540 additions and 32 deletions

39
package-lock.json generated
View File

@ -16,6 +16,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-nice-avatar": "^1.5.0",
"react-router-dom": "^6.26.1",
"react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10",
@ -2069,6 +2070,12 @@
"node": ">= 6"
}
},
"node_modules/chroma-js": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz",
"integrity": "sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==",
"license": "(BSD-3-Clause AND Apache-2.0)"
},
"node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@ -3475,7 +3482,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -3809,6 +3815,17 @@
"node": ">= 0.8.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -3869,6 +3886,26 @@
"react": "*"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-nice-avatar": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/react-nice-avatar/-/react-nice-avatar-1.5.0.tgz",
"integrity": "sha512-sGusqbgWIA4Il6Y0zHEfs4XF+a06etNljhwFYiHIGATDmVVf53Nez7U7GY5EwEz5/xGuUhs6uel5AC5NN/2UPg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.14.3",
"chroma-js": "^2.1.2",
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": ">=16.0.0"
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",

View File

@ -18,6 +18,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-nice-avatar": "^1.5.0",
"react-router-dom": "^6.26.1",
"react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10",

View File

@ -0,0 +1,126 @@
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
const slides = [
{
title: "Nama",
description:
"Masukkan nama Anda untuk memulai. Nama Anda akan digunakan untuk personalisasi pengalaman Anda selama menggunakan aplikasi ini.",
image: "/assets/images/susun_1.svg",
button: "Next",
},
{
title: "Tebak Huruf",
description:
"Tebak huruf sesuai soal yang ditampilkan. Ini adalah latihan untuk meningkatkan keterampilan kognitif dan kecepatan berpikir Anda.",
image: "/assets/images/susun_2.svg",
button: "Next",
},
{
title: "Proses Jawaban",
description:
"Tahan tangan Anda di depan kamera sampai proses selesai dan tampil dialog benar atau tidaknya jawaban.",
image: "/assets/images/susun_3.svg",
button: "Next",
},
{
title: "Score",
description:
"Raih skor tertinggimu dengan menyelesaikan tugas dengan cepat dan tepat. Skor Anda akan dibandingkan dengan pengguna lain di papan peringkat.",
image: "/assets/images/susun_4.svg",
button: "Get Started",
},
];
export default function Carousel() {
const [current, setCurrent] = useState(0);
const nextSlide = () => {
setCurrent((prev) => (prev === slides.length - 1 ? 0 : prev + 1));
};
const prevSlide = () => {
setCurrent((prev) => (prev === 0 ? slides.length - 1 : prev - 1));
};
return (
<div className="relative w-full max-w-lg mx-auto p-6 pb-20 bg-white rounded-xl shadow-lg">
<AnimatePresence mode="wait">
{slides.map(
(slide, index) =>
index === current && (
<motion.div
key={index}
className="flex flex-col items-center text-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.5 }}
>
<img src={slide.image} alt={slide.title} className="h-60" />
{/* <h2 className="mt-4 text-xl font-bold">{slide.title}</h2> */}
<p className="mt-8 text-gray-500 text-sm">
{slide.description}
</p>
{/* <button
onClick={nextSlide}
className="mt-4 bg-blue-500 text-white py-2 px-4 rounded-full shadow-md"
>
{slide.button}
</button> */}
</motion.div>
)
)}
</AnimatePresence>
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
{slides.map((_, index) => (
<button
key={index}
onClick={() => setCurrent(index)}
className={`h-2 w-2 rounded-full ${
index === current ? "bg-blue-500" : "bg-gray-300"
}`}
/>
))}
</div>
<button
onClick={prevSlide}
className="absolute left-0 top-1/2 -translate-y-1/2 bg-base-100 text-gray-600 p-1 rounded-full"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
<button
onClick={nextSlide}
className="absolute right-0 top-1/2 -translate-y-1/2 bg-base-100 text-gray-600 p-1 rounded-full"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</button>
</div>
);
}

View File

@ -4,7 +4,11 @@ const FooterPage = () => {
<div className="flex flex-row justify-center py-2 bg-white">
<p className="text-xs text-center">
&copy; 2024 <span className="font-medium">Kedai Susu Tuli</span> &
Developed by <span className="font-medium">Mphstar</span>
Developed by{" "}
<a href="https://mphstar.me" target="_blank" rel="noopener noreferrer">
<span className="underline font-semibold">Mphstar</span>
</a>{" "}
</p>
</div>
</footer>

View File

@ -22,7 +22,7 @@ const HeaderPage = () => {
</div>
<ul
className={cn(
"flex md:flex-row flex-col items-center justify-center fixed md:static min-h-svh md:min-h-0 w-full md:w-fit bg-white/50 md:bg-transparent md:backdrop-blur-none backdrop-blur-md z-[999] top-0 left-0",
"flex md:flex-row flex-col items-center justify-center fixed md:static min-h-svh md:min-h-0 w-full md:w-fit bg-white/50 md:bg-transparent z-[999] top-0 left-0",
navStore.isOpen ? "translate-y-0" : "-translate-y-full",
"duration-300 ease-in-out md:translate-y-0"
)}

View File

@ -19,15 +19,29 @@ const Kamus = () => {
<h1 className="font-semibold">Kamus SIBI</h1>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mt-6">
{constantKamus.map((item, index) => (
<motion.div
key={index}
initial={{
scale: 0,
opacity: 0,
}}
animate={{
scale: 1,
opacity: 1,
}}
transition={{
delay: index * 0.1,
}}
>
<CardKamus
handleClick={() => {
setSelectedKamus(item);
setShowDialog(true);
}}
key={index}
title={`Abjad ${item.abjad.toUpperCase()}`}
image={`/assets/kamus/${item.abjad}.jpg`}
/>
</motion.div>
))}
</div>
</div>

View File

@ -2,6 +2,7 @@ import LayoutPage from "@/components/templates/LayoutPage";
import useNavbarStore from "@/stores/NavbarStore";
import { useEffect } from "react";
import { Link } from "react-router-dom";
import { motion } from "framer-motion";
const Kuis = () => {
const store = useNavbarStore();
@ -16,7 +17,20 @@ const Kuis = () => {
<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">
<Link to="/kuis/tebak-huruf">
<div className="relative ">
<motion.div
initial={{
scale: 0,
opacity: 0,
}}
animate={{
scale: 1,
opacity: 1,
}}
transition={{
delay: 0.1,
}}
className="relative "
>
<img
className="absolute w-24 -top-12 left-4 md:left-12"
src="/assets/images/tebak-huruf.png"
@ -28,10 +42,23 @@ const Kuis = () => {
</h1>
<p>Tebak huruf dan coba simulasikan</p>
</div>
</div>
</motion.div>
</Link>
<Link to="/kuis/menyusun-huruf">
<div className="relative ">
<motion.div
initial={{
scale: 0,
opacity: 0,
}}
animate={{
scale: 1,
opacity: 1,
}}
transition={{
delay: 0.2,
}}
className="relative "
>
<img
className="absolute w-42 -top-12 left-4 md:left-12"
src="/assets/images/menyusun-huruf.png"
@ -43,7 +70,7 @@ const Kuis = () => {
</h1>
<p>Susun huruf jadi kata yang tepat</p>
</div>
</div>
</motion.div>
</Link>
</div>
</div>

View File

@ -1,9 +1,10 @@
import Carousel from "@/components/organisms/Carousel";
import Carousel from "@/components/organisms/CarouselSusunHuruf";
import LayoutPage from "@/components/templates/LayoutPage";
import useMenyusunHurufStore from "@/stores/MenyusunHurufStore";
import useNavbarStore from "@/stores/NavbarStore";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState } from "react";
import { HiOutlineHome } from "react-icons/hi";
import { IoCloseOutline } from "react-icons/io5";
import { Link, useNavigate } from "react-router-dom";
import Swal from "sweetalert2";
@ -121,7 +122,10 @@ const MenyusunHuruf = () => {
</AnimatePresence>
<div className="flex flex-col flex-1 py-4 relative">
<ul className="flex gap-3 mt-4">
<li className="hover:text-primary cursor-default">Home</li>
<li className="hover:text-primary cursor-default flex gap-2 items-center">
<HiOutlineHome />
<p>Home</p>
</li>
<li>{">"}</li>
<Link to="/kuis" className="hover:text-primary">
Kuis
@ -131,10 +135,33 @@ const MenyusunHuruf = () => {
</ul>
<div className="flex-1 flex flex-col mt-12 md:mt-24 w-full md:w-[500px]">
<h1 className="text-5xl font-semibold">
<motion.h1
initial={{
scale: 0,
opacity: 0,
}}
animate={{
scale: 1,
opacity: 1,
}}
transition={{
delay: 0.2,
}}
className="text-4xl md:text-5xl font-semibold"
>
Start Your <span className="text-primary">Quiz!</span>
</h1>
<div className="flex flex-col gap-3 w-full mt-24 md:mt-36">
</motion.h1>
<motion.div
initial={{
scale: 0,
opacity: 0,
}}
animate={{
scale: 1,
opacity: 1,
}}
className="flex flex-col gap-3 w-full mt-24 md:mt-36"
>
<h1 className="font-semibold text-xl">Masukkan Nama</h1>
<div className="bg-[#F2F2F2] rounded-full pr-4 flex py-2 items-center">
<input
@ -154,13 +181,22 @@ const MenyusunHuruf = () => {
</button>
</div>
<div className="flex items-center gap-2">
<span onClick={() => {
<span
onClick={() => {
setShowDialog(true);
}} className="text-primary btn btn-link px-0">Cara Bermain</span>
}}
className="text-primary btn btn-link px-0"
>
Cara Bermain
</span>
<span className="text-gray-400">|</span>
<span className="text-primary btn btn-link px-0">Lihat Ranking</span>
</div>
<Link to="/kuis/ranking" className="text-primary">
<span className="text-primary btn btn-link px-0">
Lihat Ranking
</span>
</Link>
</div>
</motion.div>
</div>
<img
className="absolute right-0 md:top-36 bottom-0 pointer-events-none"

View File

@ -1,9 +1,13 @@
import LayoutPage from "@/components/templates/LayoutPage";
import useNavbarStore from "@/stores/NavbarStore";
import useTebakHurufStore from "@/stores/TebakHurufStore";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { HiOutlineHome } from "react-icons/hi";
import { Link, useNavigate } from "react-router-dom";
import Swal from "sweetalert2";
import { AnimatePresence, motion } from "framer-motion";
import Carousel from "@/components/organisms/CarouselTebakHuruf";
import { IoCloseOutline } from "react-icons/io5";
const TebakHuruf = () => {
const quizStore = useTebakHurufStore();
@ -123,11 +127,53 @@ const TebakHuruf = () => {
};
}, [quizStore.isFinish]);
const [showDialog, setShowDialog] = useState(false);
return (
<LayoutPage>
<AnimatePresence>
{showDialog && (
<motion.div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center z-[999] justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowDialog(false);
}
}}
>
<motion.div
className="bg-white max-h-[90%] px-8 py-6 rounded-md flex flex-col max-w-[90%] md:min-w-[400px] md:max-w-[400px] w-full"
initial={{ scale: 0.5 }}
animate={{ scale: 1 }}
exit={{ scale: 0.8 }}
>
<div className="flex gap-2 justify-between items-center pb-8">
<h1 className="font-semibold">Cara Bermain</h1>
<button
onClick={() => setShowDialog(false)}
className="hover:bg-base-100 p-3 rounded-md"
>
<IoCloseOutline />
</button>
</div>
<div className="flex flex-col md:flex-row gap-6 overflow-y-auto flex-1">
<Carousel />
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
<div className="flex flex-col flex-1 py-4 relative">
<ul className="flex gap-3 mt-4">
<li className="hover:text-primary cursor-default">Home</li>
<li className="hover:text-primary cursor-default flex gap-2 items-center">
<HiOutlineHome />
<p>Home</p>
</li>
<li>{">"}</li>
<Link to="/kuis" className="hover:text-primary">
Kuis
@ -137,10 +183,33 @@ const TebakHuruf = () => {
</ul>
<div className="flex-1 flex flex-col mt-12 md:mt-24 w-full md:w-[500px]">
<h1 className="text-5xl font-semibold">
<motion.h1
initial={{
scale: 0,
opacity: 0,
}}
animate={{
scale: 1,
opacity: 1,
}}
transition={{
delay: 0.2,
}}
className="text-4xl md:text-5xl font-semibold"
>
Start Your <span className="text-primary">Quiz!</span>
</h1>
<div className="flex flex-col gap-3 w-full mt-24 md:mt-36">
</motion.h1>
<motion.div
initial={{
scale: 0,
opacity: 0,
}}
animate={{
scale: 1,
opacity: 1,
}}
className="flex flex-col gap-3 w-full mt-24 md:mt-36"
>
<h1 className="font-semibold text-xl">Masukkan Nama</h1>
<div className="bg-[#F2F2F2] rounded-full pr-4 flex py-2 items-center">
<input
@ -159,8 +228,23 @@ const TebakHuruf = () => {
Mulai Kuis
</button>
</div>
<span className="text-primary">Lihat Ranking</span>
<div className="flex items-center gap-2">
<span
onClick={() => {
setShowDialog(true);
}}
className="text-primary btn btn-link px-0"
>
Cara Bermain
</span>
<span className="text-gray-400">|</span>
<Link to="/kuis/ranking" className="text-primary">
<span className="text-primary btn btn-link px-0">
Lihat Ranking
</span>
</Link>
</div>
</motion.div>
</div>
<img
className="absolute right-0 md:top-36 bottom-0 pointer-events-none"

View File

@ -0,0 +1,174 @@
import LayoutPage from "@/components/templates/LayoutPage";
import useNavbarStore from "@/stores/NavbarStore";
import { useEffect, useState } from "react";
import { HiOutlineHome } from "react-icons/hi";
import { Link } from "react-router-dom";
import { motion } from "framer-motion";
const data = [
{
id: 1,
name: "Asep",
score: 100,
time: "15-08-2021 12:40",
},
{
id: 2,
name: "Budi",
score: 90,
time: "15-08-2021 12:40",
},
{
id: 3,
name: "Cecep",
score: 80,
time: "15-08-2021 12:40",
},
{
id: 4,
name: "Dedi",
score: 70,
time: "15-08-2021 12:40",
},
{
id: 5,
name: "Euis",
score: 60,
time: "15-08-2021 12:40",
},
{
id: 6,
name: "Fafa",
score: 50,
time: "15-08-2021 12:40",
},
{
id: 7,
name: "Gaga",
score: 40,
time: "15-08-2021 12:40",
},
{
id: 8,
name: "Haha",
score: 30,
time: "15-08-2021 12:40",
},
{
id: 9,
name: "Ii",
score: 20,
time: "15-08-2021 12:40",
},
{
id: 10,
name: "Jaja",
score: 10,
time: "15-08-2021 12:40",
},
];
const Ranking = () => {
const store = useNavbarStore();
const [tabSelected, setTabSelected] = useState(0);
useEffect(() => {
store.setNavSelected("kuis");
}, []);
return (
<LayoutPage>
<div className="flex flex-col flex-1 py-4 relative items-center">
<ul className="flex gap-3 mt-4 w-full">
<li className="hover:text-primary cursor-default flex gap-2 items-center">
<HiOutlineHome />
<p>Home</p>
</li>
<li>{">"}</li>
<Link to="/kuis" className="hover:text-primary">
Kuis
</Link>
<li>{">"}</li>
<li className="font-medium text-gray-500">Ranking</li>
</ul>
<div className="md:max-w-[500px] w-full flex flex-col mt-6 items-center">
<div className="flex items-center">
<button
onClick={() => setTabSelected(0)}
className={`btn btn-link ${
tabSelected === 0 ? "" : "no-underline text-gray-500"
}`}
>
Tebak Huruf
</button>
<button
onClick={() => setTabSelected(1)}
className={`btn btn-link ${
tabSelected === 1 ? "" : "no-underline text-gray-500"
}`}
>
Menyusun Huruf
</button>
</div>
<label className="input input-bordered flex items-center gap-2 w-full mt-6 mb-4">
<input type="text" className="grow" placeholder="Search" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
className="h-4 w-4 opacity-70"
>
<path
fillRule="evenodd"
d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z"
clipRule="evenodd"
/>
</svg>
</label>
{data.map((item, index) => (
<motion.div
initial={{
scale: 0,
opacity: 0,
}}
whileInView={{
scale: 1,
opacity: 1,
transition: { duration: 0.1, delay: index * 0.1 },
}}
key={item.id}
className="flex justify-between mt-4 bg-base-100 hover:bg-base-200 duration-300 ease-in-out px-6 py-4 rounded-md w-full items-center gap-4"
>
<img
className="bg-green-400 rounded-full h-14 w-14 object-cover"
src={`https://api.dicebear.com/9.x/miniavs/svg?seed=${item.name}&backgroundColor=b6e3f4,c0aede,d1d4f9`}
alt=""
/>
<div className="flex flex-col flex-1">
<p className="font-semibold">{item.name}</p>
<p>{item.score} Poin</p>
<p className="text-xs text-gray-500">{item.time}</p>
</div>
<div
className={`w-6 h-6 rounded-full flex justify-center items-center p-5 `}
style={{
backgroundColor: ["#b6e3f4", "#c0aede", "#d1d4f9"][
Math.floor(Math.random() * 3)
],
}}
>
{index + 1}
</div>
</motion.div>
))}
</div>
</div>
</LayoutPage>
);
};
export default Ranking;

View File

@ -36,6 +36,11 @@ const myRoute = [
path: "/kuis/menyusun-huruf/app",
component: lazy(() => import("@/pages/Kuis/MenyusunHuruf/Quiz")),
},
{
title: "Ranking",
path: "/kuis/ranking",
component: lazy(() => import("@/pages/Ranking/Ranking")),
},
];
export default myRoute;