feat: get ranking

This commit is contained in:
mphstar 2025-02-26 17:16:35 +07:00
parent ae17e2d0ba
commit 46978cbde4
6 changed files with 223 additions and 104 deletions

32
package-lock.json generated
View File

@ -21,6 +21,7 @@
"react-virtualized-auto-sizer": "^1.0.24", "react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10", "react-window": "^1.8.10",
"sweetalert2": "^11.17.2", "sweetalert2": "^11.17.2",
"swr": "^2.3.2",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.5.2",
"zustand": "^4.5.5" "zustand": "^4.5.5"
}, },
@ -2316,6 +2317,15 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/didyoumean": { "node_modules/didyoumean": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -4360,6 +4370,28 @@
"url": "https://github.com/sponsors/limonte" "url": "https://github.com/sponsors/limonte"
} }
}, },
"node_modules/swr": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.2.tgz",
"integrity": "sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA==",
"license": "MIT",
"dependencies": {
"dequal": "^2.0.3",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/swr/node_modules/use-sync-external-store": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/tailwind-merge": { "node_modules/tailwind-merge": {
"version": "2.5.2", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz",

View File

@ -23,6 +23,7 @@
"react-virtualized-auto-sizer": "^1.0.24", "react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10", "react-window": "^1.8.10",
"sweetalert2": "^11.17.2", "sweetalert2": "^11.17.2",
"swr": "^2.3.2",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.5.2",
"zustand": "^4.5.5" "zustand": "^4.5.5"
}, },

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -1,77 +1,37 @@
import LayoutPage from "@/components/templates/LayoutPage"; import LayoutPage from "@/components/templates/LayoutPage";
import useNavbarStore from "@/stores/NavbarStore"; import useNavbarStore from "@/stores/NavbarStore";
import { useEffect, useState } from "react"; import { ChangeEvent, useEffect, useState } from "react";
import { HiOutlineHome } from "react-icons/hi"; import { HiOutlineHome } from "react-icons/hi";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import useSWR from "swr";
const data = [ import { fetcher } from "@/utils/fetcher";
{ import { debounce } from "@/utils/debounce";
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 Ranking = () => {
const store = useNavbarStore(); const store = useNavbarStore();
const [tabSelected, setTabSelected] = useState(0); const [tabSelected, setTabSelected] = useState(0);
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
const size = 20;
const { data, error, isLoading } = useSWR(
`https://ksuli-api.deno.dev/ranking?page=${page}&search=${search}&size=${size}&kategori_id=${
tabSelected == 0 ? "rec_cuum78tqrj678tmbcjh0" : "rec_cuum7c5qrj60bgubcjog"
}`,
fetcher
);
const handleSearch = debounce((term) => {
setSearch(term);
}, 500);
const handleChangeSearch = (e: ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setPage(1);
handleSearch(value);
};
useEffect(() => { useEffect(() => {
store.setNavSelected("kuis"); store.setNavSelected("kuis");
@ -93,19 +53,25 @@ const Ranking = () => {
<li className="font-medium text-gray-500">Ranking</li> <li className="font-medium text-gray-500">Ranking</li>
</ul> </ul>
<div className="md:max-w-[500px] w-full flex flex-col mt-6 items-center"> <div className="md:max-w-[500px] w-full flex flex-col mt-6 md:items-center h-full flex-1">
<div className="flex items-center"> <div className="flex md:items-center gap-3 md:gap-0">
<button <button
onClick={() => setTabSelected(0)} onClick={() => {
className={`btn btn-link ${ setTabSelected(0);
setPage(1);
}}
className={`btn btn-link p-0 md:p-2 ${
tabSelected === 0 ? "" : "no-underline text-gray-500" tabSelected === 0 ? "" : "no-underline text-gray-500"
}`} }`}
> >
Tebak Huruf Tebak Huruf
</button> </button>
<button <button
onClick={() => setTabSelected(1)} onClick={() => {
className={`btn btn-link ${ setTabSelected(1);
setPage(1);
}}
className={`btn btn-link p-0 md:p-2 ${
tabSelected === 1 ? "" : "no-underline text-gray-500" tabSelected === 1 ? "" : "no-underline text-gray-500"
}`} }`}
> >
@ -114,7 +80,12 @@ const Ranking = () => {
</div> </div>
<label className="input input-bordered flex items-center gap-2 w-full mt-6 mb-4"> <label className="input input-bordered flex items-center gap-2 w-full mt-6 mb-4">
<input type="text" className="grow" placeholder="Search" /> <input
onChange={handleChangeSearch}
type="text"
className="grow"
placeholder="Search"
/>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
@ -129,42 +100,137 @@ const Ranking = () => {
</svg> </svg>
</label> </label>
{data.map((item, index) => ( {isLoading && (
<motion.div <div className="flex flex-col items-center justify-center h-full flex-1">
initial={{ <div className="loader"></div>
scale: 0, <p className="mt-4 text-lg text-gray-700">Loading...</p>
opacity: 0, </div>
}} )}
whileInView={{
scale: 1, {error && !isLoading && (
opacity: 1, <div className="flex flex-col items-center justify-center h-full flex-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 <img
className="bg-green-400 rounded-full h-14 w-14 object-cover" src="/assets/images/nodata.svg"
src={`https://api.dicebear.com/9.x/miniavs/svg?seed=${item.name}&backgroundColor=b6e3f4,c0aede,d1d4f9`} className="h-44"
alt="" alt="Error Image"
/> />
<div className="flex flex-col flex-1"> <p className="mt-4 text-md text-center text-gray-700">
<p className="font-semibold">{item.name}</p> Sedang terjadi error, silakan coba lagi nanti.
<p>{item.score} Poin</p> </p>
<p className="text-xs text-gray-500">{item.time}</p> </div>
</div> )}
<div
className={`w-6 h-6 rounded-full flex justify-center items-center p-5 `} {data && data.records.length === 0 && !isLoading && (
style={{ <div className="flex flex-col items-center justify-center h-full flex-1">
backgroundColor: ["#b6e3f4", "#c0aede", "#d1d4f9"][ <img
Math.floor(Math.random() * 3) src="/assets/images/nodata.svg"
], className="h-44"
}} alt="Error Image"
/>
<p className="mt-4 text-md text-center text-gray-700">
Data tidak ditemukan
</p>
</div>
)}
{data &&
data.records
.sort((a: any, b: any) =>
tabSelected === 0 ? b.score - a.score : a.score - b.score
)
.map((item: any, index: number) => (
<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.person_name}&backgroundColor=b6e3f4,c0aede,d1d4f9`}
alt=""
/>
<div className="flex flex-col flex-1">
<p className="font-semibold">{item.person_name}</p>
<div className="flex items-center flex-wrap">
<p className="text-sm">
{item.score} {tabSelected == 0 ? "Point" : "Detik"}
</p>
<div className="w-2"></div>
{index < 3 && (
<p className="text-xs text-primary">
Top Score {index + 1}
</p>
)}
</div>
<p className="text-xs text-gray-500">
{new Date(item.xata.createdAt).toLocaleString()}
</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>
))}
{data && (
<div className="flex justify-center mt-6 gap-4">
<button
onClick={() => setPage((prev) => Math.max(prev - 1, 1))}
className="btn btn-primary btn-xs"
disabled={page === 1}
> >
{index + 1} <svg
</div> xmlns="http://www.w3.org/2000/svg"
</motion.div> fill="none"
))} viewBox="0 0 24 24"
stroke="currentColor"
className="w-3 h-3"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
<button
onClick={() => setPage((prev) => prev + 1)}
className="btn btn-primary btn-xs"
disabled={!data.meta.page.more}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="w-3 h-3"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</button>
</div>
)}
</div> </div>
</div> </div>
</LayoutPage> </LayoutPage>

10
src/utils/debounce.ts Normal file
View File

@ -0,0 +1,10 @@
export const debounce = <T extends (...args: any[]) => any>(
func: T,
delay: number
): ((...args: Parameters<T>) => void) => {
let timeoutId: ReturnType<typeof setTimeout>;
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
};

9
src/utils/fetcher.ts Normal file
View File

@ -0,0 +1,9 @@
export const fetcher = (url: RequestInfo, options: RequestInit = {}) => {
const token = "KSULI_TOKEN_321"; // Replace with your actual token
const headers = {
...options.headers,
Authorization: `Bearer ${token}`,
};
return fetch(url, { ...options, headers }).then((res) => res.json());
};