Update: Layout FE (Left, middle, right)
This commit is contained in:
parent
7687eae2bb
commit
0c5e777273
65
apps/frontend/src/routes/_assessmentLayout.tsx
Normal file
65
apps/frontend/src/routes/_assessmentLayout.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { Navigate, Outlet, createFileRoute } from "@tanstack/react-router";
|
||||||
|
import AppHeader from "../components/AppHeader";
|
||||||
|
import AppNavbar from "../components/AppNavbar";
|
||||||
|
import useAuth from "@/hooks/useAuth";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import fetchRPC from "@/utils/fetchRPC";
|
||||||
|
import client from "@/honoClient";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_assessmentLayout")({
|
||||||
|
component: AssessmentLayout,
|
||||||
|
|
||||||
|
// beforeLoad: ({ location }) => {
|
||||||
|
// if (true) {
|
||||||
|
// throw redirect({
|
||||||
|
// to: "/login",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
|
||||||
|
function AssessmentLayout() {
|
||||||
|
const { isAuthenticated, saveAuthData } = useAuth();
|
||||||
|
|
||||||
|
useQuery({
|
||||||
|
queryKey: ["my-profile"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetchRPC(client.auth["my-profile"].$get());
|
||||||
|
|
||||||
|
saveAuthData({
|
||||||
|
id: response.id,
|
||||||
|
name: response.name,
|
||||||
|
permissions: response.permissions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
enabled: isAuthenticated,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [openNavbar, setNavbarOpen] = useState(true);
|
||||||
|
const toggle = () => {
|
||||||
|
setNavbarOpen(!openNavbar);
|
||||||
|
};
|
||||||
|
|
||||||
|
return isAuthenticated ? (
|
||||||
|
<div className="flex flex-col w-full h-screen overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<AppHeader toggle={toggle} openNavbar={openNavbar} />
|
||||||
|
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<div className="flex h-full w-screen overflow-hidden">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<AppNavbar />
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="relative w-full mt-16 bg-white overflow-auto">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Navigate to="/login" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -25,13 +25,14 @@ import {
|
||||||
import { TbFlagFilled, TbUpload, TbChevronRight, TbChevronUp } from "react-icons/tb";
|
import { TbFlagFilled, TbUpload, TbChevronRight, TbChevronUp } from "react-icons/tb";
|
||||||
import FinishAssessmentModal from "@/modules/assessmentManagement/modals/ConfirmModal";
|
import FinishAssessmentModal from "@/modules/assessmentManagement/modals/ConfirmModal";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import AppHeader from "@/components/AppHeader";
|
||||||
|
|
||||||
const getQueryParam = (param: string) => {
|
const getQueryParam = (param: string) => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
return urlParams.get(param);
|
return urlParams.get(param);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/_dashboardLayout/assessment/")({
|
export const Route = createLazyFileRoute("/_assessmentLayout/assessment/")({
|
||||||
component: AssessmentPage,
|
component: AssessmentPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -446,8 +447,9 @@ export default function AssessmentPage() {
|
||||||
|
|
||||||
{/* LEFT-SIDE */}
|
{/* LEFT-SIDE */}
|
||||||
{/* Aspek dan Sub-Aspek */}
|
{/* Aspek dan Sub-Aspek */}
|
||||||
<Flex direction="column" gap="xs" className="mr-4 w-52">
|
<Flex direction="column" gap="xs" className="w-52">
|
||||||
<div className="space-y-4">
|
<div className="space-y-2">
|
||||||
|
{/* Aspek */}
|
||||||
{aspectsQuery.data?.data
|
{aspectsQuery.data?.data
|
||||||
.filter((aspect) =>
|
.filter((aspect) =>
|
||||||
aspect.subAspects.some((subAspect) =>
|
aspect.subAspects.some((subAspect) =>
|
||||||
|
|
@ -457,13 +459,13 @@ export default function AssessmentPage() {
|
||||||
.map((aspect) => (
|
.map((aspect) => (
|
||||||
<div
|
<div
|
||||||
key={aspect.id}
|
key={aspect.id}
|
||||||
className="p-4 bg-gray-50 rounded-lg shadow-md"
|
className="p-2 "
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="flex justify-between cursor-pointer"
|
className="flex justify-between cursor-pointer"
|
||||||
onClick={() => toggleAspect(aspect.id)}
|
onClick={() => toggleAspect(aspect.id)}
|
||||||
>
|
>
|
||||||
<div className="text-lg text-gray-700">{aspect.name}</div>
|
<div className="text-lg font-bold px-3">{aspect.name}</div>
|
||||||
<div>
|
<div>
|
||||||
{openAspects[aspect.id] ? (
|
{openAspects[aspect.id] ? (
|
||||||
<TbChevronUp size={25} />
|
<TbChevronUp size={25} />
|
||||||
|
|
@ -473,6 +475,7 @@ export default function AssessmentPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Sub-Aspek */}
|
||||||
{openAspects[aspect.id] && (
|
{openAspects[aspect.id] && (
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
{aspect.subAspects
|
{aspect.subAspects
|
||||||
|
|
@ -482,7 +485,7 @@ export default function AssessmentPage() {
|
||||||
.map((subAspect) => (
|
.map((subAspect) => (
|
||||||
<div
|
<div
|
||||||
key={subAspect.id}
|
key={subAspect.id}
|
||||||
className={`flex justify-between text-gray-600 cursor-pointer ${selectedSubAspectId === subAspect.id ? 'font-bold' : ''}`}
|
className={`flex justify-between cursor-pointer p-2 px-6 rounded-sm transition-colors duration-150 ${selectedSubAspectId === subAspect.id ? 'text-black font-medium bg-gray-200' : 'text-gray-500'}`}
|
||||||
onClick={() => setSelectedSubAspectId(subAspect.id)}
|
onClick={() => setSelectedSubAspectId(subAspect.id)}
|
||||||
>
|
>
|
||||||
<div>{subAspect.name}</div>
|
<div>{subAspect.name}</div>
|
||||||
|
|
@ -494,179 +497,182 @@ export default function AssessmentPage() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* Pertanyaan */}
|
{/* Pertanyaan */}
|
||||||
<Stack gap="sm" style={{ flex: 1 }}>
|
<Stack gap="sm" style={{ flex: 1 }}>
|
||||||
<Text className="text-2xl font-bold ml-6">
|
<Text className="text-2xl font-bold ml-6">
|
||||||
Harap menjawab semua pertanyaan yang tersedia
|
Harap menjawab semua pertanyaan yang tersedia
|
||||||
</Text>
|
|
||||||
<Text className="text-gray-400 ml-6">Semua jawaban Anda akan ditinjau</Text>
|
|
||||||
{filteredQuestions.length === 0 ? (
|
|
||||||
<Text color="black" className="text-center p-3">
|
|
||||||
Pertanyaan tidak ada untuk sub-aspek yang dipilih.
|
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
<Text className="text-gray-400 ml-6">Semua jawaban Anda akan ditinjau</Text>
|
||||||
filteredQuestions.map((question: any, index: number) => {
|
{filteredQuestions.length === 0 ? (
|
||||||
const questionId = question.questionId;
|
<Text color="black" className="text-center p-3">
|
||||||
if (!questionId) return null;
|
Pertanyaan tidak ada untuk sub-aspek yang dipilih.
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
filteredQuestions.map((question: any, index: number) => {
|
||||||
|
const questionId = question.questionId;
|
||||||
|
if (!questionId) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={questionId}
|
key={questionId}
|
||||||
ref={(el) => (questionRefs.current[questionId] = el)}
|
ref={(el) => (questionRefs.current[questionId] = el)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Flex justify="space-between" align="flex-start" style={{ width: "100%" }}>
|
<Flex justify="space-between" align="flex-start" style={{ width: "100%" }}>
|
||||||
<Text className="font-bold mr-3">{startIndex + index + 1}.</Text>
|
{/* Question */}
|
||||||
<div className="flex-grow">
|
<Text className="font-bold mx-3 p-1">{startIndex + index + 1}.</Text>
|
||||||
<Text className="font-bold break-words">
|
<div className="flex-grow">
|
||||||
{question.questionText}
|
<Text className="font-bold break-words">
|
||||||
</Text>
|
{question.questionText}
|
||||||
</div>
|
</Text>
|
||||||
|
|
||||||
{/* Action Icon */}
|
|
||||||
<ActionIcon
|
|
||||||
onClick={() => {
|
|
||||||
setFlaggedQuestions((prevFlags) => ({
|
|
||||||
...prevFlags,
|
|
||||||
[questionId]: !prevFlags[questionId],
|
|
||||||
}));
|
|
||||||
toggleFlagMutation.mutate(questionId);
|
|
||||||
}}
|
|
||||||
title={
|
|
||||||
!answers[question.questionId]
|
|
||||||
? "Pilih jawaban terlebih dahulu"
|
|
||||||
: "Tandai"
|
|
||||||
}
|
|
||||||
color={flaggedQuestions[questionId] ? "red" : "white"}
|
|
||||||
style={{
|
|
||||||
border: "1px gray solid",
|
|
||||||
borderRadius: "4px",
|
|
||||||
backgroundColor: flaggedQuestions[questionId] ? "red" : "white",
|
|
||||||
}}
|
|
||||||
disabled={!answers[question.questionId]}
|
|
||||||
>
|
|
||||||
<TbFlagFilled
|
|
||||||
size={20}
|
|
||||||
color={flaggedQuestions[questionId] ? "white" : "black"}
|
|
||||||
style={{
|
|
||||||
padding: "2px",
|
|
||||||
}}
|
|
||||||
|
|
||||||
/>
|
|
||||||
</ActionIcon>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{/* Opsi Radio Button */}
|
|
||||||
{question.options?.length > 0 ? (
|
|
||||||
<div className="ml-6">
|
|
||||||
<Radio.Group value={answers[question.questionId] || ""}>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{question.options.map((option: any) => (
|
|
||||||
<label
|
|
||||||
key={option.optionId}
|
|
||||||
className={`cursor-pointer transition-transform transform hover:scale-105 shadow-md hover:shadow-lg flex items-center border rounded-lg p-4 ${
|
|
||||||
answers[question.questionId] === option.optionId
|
|
||||||
? "bg-blue-500 text-white"
|
|
||||||
: "bg-gray-200 text-black"
|
|
||||||
}`}
|
|
||||||
onClick={() => document.getElementById(option.optionId)?.click()}
|
|
||||||
>
|
|
||||||
<Radio
|
|
||||||
id={option.optionId}
|
|
||||||
className="font-bold"
|
|
||||||
value={option.optionId}
|
|
||||||
label={option.optionText}
|
|
||||||
size="md"
|
|
||||||
radius="xl"
|
|
||||||
style={{ pointerEvents: "none" }}
|
|
||||||
checked={answers[question.questionId] === option.optionId} // Untuk menampilkan jawaban yang sudah dipilih
|
|
||||||
onChange={() => handleAnswerChange(question.questionId, option.optionId)} // Memanggil handleAnswerChange dengan questionId dan optionId
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Radio.Group>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Text color="red">Tidak ada opsi untuk pertanyaan ini.</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Textarea */}
|
|
||||||
<div className="ml-6">
|
|
||||||
<Textarea
|
|
||||||
placeholder="Berikan keterangan terkait jawaban di atas"
|
|
||||||
value={validationInformation[question.questionId] || ""}
|
|
||||||
onChange={(event) => handleTextareaChange(question.questionId, event.currentTarget.value)}
|
|
||||||
disabled={!answers[question.questionId]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* File Upload */}
|
|
||||||
<div className="ml-6">
|
|
||||||
{question.needFile === true && (
|
|
||||||
<div
|
|
||||||
className={`pt-5 pb-5 pr-5 pl-5 border-2 rounded-lg border-dashed ${dragActive ? "bg-gray-100" : "bg-transparent"
|
|
||||||
} shadow-lg`}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDrop={(event) => handleDrop(event, question)} // Mengoper question sebagai argumen
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<Flex align="center" justify="space-between" gap="sm">
|
|
||||||
<TbUpload
|
|
||||||
size={24}
|
|
||||||
style={{ marginLeft: "8px", marginRight: "8px" }}
|
|
||||||
/>
|
|
||||||
<div className="flex-grow text-right">
|
|
||||||
<Text className="font-bold">
|
|
||||||
Klik untuk unggah atau geser file disini
|
|
||||||
</Text>
|
|
||||||
<Text className="text-sm text-gray-400">
|
|
||||||
PNG, JPG, PDF
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={fileInputRef}
|
|
||||||
onChange={(event) => handleFileChange(event, question)} // Mengoper question sebagai argumen
|
|
||||||
style={{ display: "none" }}
|
|
||||||
accept="image/png, image/jpeg, application/pdf"
|
|
||||||
disabled={!answers[question.questionId]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ml-6">
|
{/* Action Icon/Flag */}
|
||||||
{uploadedFiles[question.questionId] && (
|
<ActionIcon
|
||||||
<Stack gap="sm" mt="sm">
|
onClick={() => {
|
||||||
<Text className="font-bold">File yang diunggah:</Text>
|
setFlaggedQuestions((prevFlags) => ({
|
||||||
<Group align="center">
|
...prevFlags,
|
||||||
<Text>{uploadedFiles[question.questionId]?.name}</Text> {/* Tampilkan nama file yang diunggah */}
|
[questionId]: !prevFlags[questionId],
|
||||||
<CloseButton
|
}));
|
||||||
title="Hapus file"
|
toggleFlagMutation.mutate(questionId);
|
||||||
onClick={() => handleRemoveFile(question)} // Mengoper question sebagai argumen
|
}}
|
||||||
|
title={
|
||||||
|
!answers[question.questionId]
|
||||||
|
? "Pilih jawaban terlebih dahulu"
|
||||||
|
: "Tandai"
|
||||||
|
}
|
||||||
|
color={flaggedQuestions[questionId] ? "red" : "white"}
|
||||||
|
style={{
|
||||||
|
margin: "2px 10px",
|
||||||
|
border: "1px gray solid",
|
||||||
|
borderRadius: "4px",
|
||||||
|
backgroundColor: flaggedQuestions[questionId] ? "red" : "white",
|
||||||
|
}}
|
||||||
|
disabled={!answers[question.questionId]}
|
||||||
|
>
|
||||||
|
<TbFlagFilled
|
||||||
|
size={20}
|
||||||
|
color={flaggedQuestions[questionId] ? "white" : "black"}
|
||||||
|
style={{
|
||||||
|
padding: "2px",
|
||||||
|
}}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</ActionIcon>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* Opsi Radio Button */}
|
||||||
|
{question.options?.length > 0 ? (
|
||||||
|
<div className="mx-12">
|
||||||
|
<Radio.Group value={answers[question.questionId] || ""}>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{question.options.map((option: any) => (
|
||||||
|
<label
|
||||||
|
key={option.optionId}
|
||||||
|
className={`cursor-pointer transition-transform transform hover:scale-105 shadow-md hover:shadow-lg flex items-center border rounded-lg p-4 ${
|
||||||
|
answers[question.questionId] === option.optionId
|
||||||
|
? "bg-blue-500 text-white"
|
||||||
|
: "bg-gray-200 text-black"
|
||||||
|
}`}
|
||||||
|
onClick={() => document.getElementById(option.optionId)?.click()}
|
||||||
|
>
|
||||||
|
<Radio
|
||||||
|
id={option.optionId}
|
||||||
|
className="font-bold"
|
||||||
|
value={option.optionId}
|
||||||
|
label={option.optionText}
|
||||||
|
size="md"
|
||||||
|
radius="xl"
|
||||||
|
style={{ pointerEvents: "none" }}
|
||||||
|
checked={answers[question.questionId] === option.optionId} // Untuk menampilkan jawaban yang sudah dipilih
|
||||||
|
onChange={() => handleAnswerChange(question.questionId, option.optionId)} // Memanggil handleAnswerChange dengan questionId dan optionId
|
||||||
/>
|
/>
|
||||||
</Group>
|
</label>
|
||||||
</Stack>
|
))}
|
||||||
|
</div>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Text color="red">Tidak ada opsi untuk pertanyaan ini.</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Garis pembatas setiap soal */}
|
{/* Textarea */}
|
||||||
<div>
|
<div className="mx-12">
|
||||||
<hr className="border-t-2 border-gray-300 ml-6 mx-auto mt-6 mb-6" />
|
<Textarea
|
||||||
</div>
|
placeholder="Berikan keterangan terkait jawaban di atas"
|
||||||
</Stack>
|
value={validationInformation[question.questionId] || ""}
|
||||||
</div>
|
onChange={(event) => handleTextareaChange(question.questionId, event.currentTarget.value)}
|
||||||
);
|
disabled={!answers[question.questionId]}
|
||||||
})
|
/>
|
||||||
)}
|
</div>
|
||||||
</Stack>
|
|
||||||
|
{/* File Upload */}
|
||||||
|
<div className="mx-12">
|
||||||
|
{question.needFile === true && (
|
||||||
|
<div
|
||||||
|
className={`pt-5 pb-5 pr-5 pl-5 border-2 rounded-lg border-dashed ${dragActive ? "bg-gray-100" : "bg-transparent"
|
||||||
|
} shadow-lg`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={(event) => handleDrop(event, question)} // Mengoper question sebagai argumen
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<Flex align="center" justify="space-between" gap="sm">
|
||||||
|
<TbUpload
|
||||||
|
size={24}
|
||||||
|
style={{ marginLeft: "8px", marginRight: "8px" }}
|
||||||
|
/>
|
||||||
|
<div className="flex-grow text-right">
|
||||||
|
<Text className="font-bold">
|
||||||
|
Klik untuk unggah atau geser file disini
|
||||||
|
</Text>
|
||||||
|
<Text className="text-sm text-gray-400">
|
||||||
|
PNG, JPG, PDF
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={(event) => handleFileChange(event, question)} // Mengoper question sebagai argumen
|
||||||
|
style={{ display: "none" }}
|
||||||
|
accept="image/png, image/jpeg, application/pdf"
|
||||||
|
disabled={!answers[question.questionId]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-6">
|
||||||
|
{uploadedFiles[question.questionId] && (
|
||||||
|
<Stack gap="sm" mt="sm">
|
||||||
|
<Text className="font-bold">File yang diunggah:</Text>
|
||||||
|
<Group align="center">
|
||||||
|
<Text>{uploadedFiles[question.questionId]?.name}</Text> {/* Tampilkan nama file yang diunggah */}
|
||||||
|
<CloseButton
|
||||||
|
title="Hapus file"
|
||||||
|
onClick={() => handleRemoveFile(question)} // Mengoper question sebagai argumen
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Garis pembatas setiap soal */}
|
||||||
|
<div>
|
||||||
|
<hr className="border-t-2 border-gray-300 mx-12 mt-6 mb-6" />
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
{/* Navigasi dan Pagination */}
|
{/* Navigasi dan Pagination */}
|
||||||
<Flex direction="column" gap="xs" className="ml-4">
|
<Flex direction="column" gap="xs" className="mx-4">
|
||||||
|
|
||||||
{/* Navigasi (Number of Questions) */}
|
{/* Navigasi (Number of Questions) */}
|
||||||
<div className="grid grid-cols-5 gap-2">
|
<div className="grid grid-cols-5 gap-2">
|
||||||
|
|
@ -9,7 +9,7 @@ const searchParamSchema = z.object({
|
||||||
detail: z.string().default("").optional(),
|
detail: z.string().default("").optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute("/_dashboardLayout/assessment/")({
|
export const Route = createFileRoute("/_assessmentLayout/assessment/")({
|
||||||
validateSearch: searchParamSchema,
|
validateSearch: searchParamSchema,
|
||||||
|
|
||||||
loader: ({ context: { queryClient } }) => {
|
loader: ({ context: { queryClient } }) => {
|
||||||
Loading…
Reference in New Issue
Block a user