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-window": "^1.8.10",
"sweetalert2": "^11.17.2",
"swr": "^2.3.2",
"tailwind-merge": "^2.5.2",
"zustand": "^4.5.5"
},
@ -2316,6 +2317,15 @@
"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": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -4360,6 +4370,28 @@
"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": {
"version": "2.5.2",
"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-window": "^1.8.10",
"sweetalert2": "^11.17.2",
"swr": "^2.3.2",
"tailwind-merge": "^2.5.2",
"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 useNavbarStore from "@/stores/NavbarStore";
import { useEffect, useState } from "react";
import { ChangeEvent, 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",
},
];
import useSWR from "swr";
import { fetcher } from "@/utils/fetcher";
import { debounce } from "@/utils/debounce";
const Ranking = () => {
const store = useNavbarStore();
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(() => {
store.setNavSelected("kuis");
@ -93,19 +53,25 @@ const Ranking = () => {
<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">
<div className="md:max-w-[500px] w-full flex flex-col mt-6 md:items-center h-full flex-1">
<div className="flex md:items-center gap-3 md:gap-0">
<button
onClick={() => setTabSelected(0)}
className={`btn btn-link ${
onClick={() => {
setTabSelected(0);
setPage(1);
}}
className={`btn btn-link p-0 md:p-2 ${
tabSelected === 0 ? "" : "no-underline text-gray-500"
}`}
>
Tebak Huruf
</button>
<button
onClick={() => setTabSelected(1)}
className={`btn btn-link ${
onClick={() => {
setTabSelected(1);
setPage(1);
}}
className={`btn btn-link p-0 md:p-2 ${
tabSelected === 1 ? "" : "no-underline text-gray-500"
}`}
>
@ -114,7 +80,12 @@ const Ranking = () => {
</div>
<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
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
@ -129,7 +100,45 @@ const Ranking = () => {
</svg>
</label>
{data.map((item, index) => (
{isLoading && (
<div className="flex flex-col items-center justify-center h-full flex-1">
<div className="loader"></div>
<p className="mt-4 text-lg text-gray-700">Loading...</p>
</div>
)}
{error && !isLoading && (
<div className="flex flex-col items-center justify-center h-full flex-1">
<img
src="/assets/images/nodata.svg"
className="h-44"
alt="Error Image"
/>
<p className="mt-4 text-md text-center text-gray-700">
Sedang terjadi error, silakan coba lagi nanti.
</p>
</div>
)}
{data && data.records.length === 0 && !isLoading && (
<div className="flex flex-col items-center justify-center h-full flex-1">
<img
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,
@ -145,13 +154,26 @@ const Ranking = () => {
>
<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`}
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.name}</p>
<p>{item.score} Poin</p>
<p className="text-xs text-gray-500">{item.time}</p>
<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 `}
@ -165,6 +187,50 @@ const Ranking = () => {
</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}
>
<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="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>
</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());
};