fixed FAQ view button in ListFAQ, ongoing: Dashboard routing to BE
This commit is contained in:
+128
-31
@@ -1,53 +1,150 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
const faqData = [
|
type FaqItem = {
|
||||||
{
|
id: number;
|
||||||
question: "Bagaimana cara magang disini?",
|
pertanyaan: string;
|
||||||
answer: "Pilih Internship, Kemudian Lakukan Proses Administrasi",
|
jawaban: string;
|
||||||
},
|
created_at?: string;
|
||||||
{
|
};
|
||||||
question: "Apakah saya bisa melamar lebih dari satu lowongan?",
|
|
||||||
answer: "Tidak Bisa, Hanya Bisa Melamar 1 Lowongan",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Bagaimana saya tahu jika lamaran saya diterima atau ditolak?",
|
|
||||||
answer: "Sistem Akan Mengirimkan Pesan Diterima/Ditolak Ke Email Anda",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Berapa lama durasi magang di Humic?",
|
|
||||||
answer: "Cek Instagram Kita Yah, Disana Tertera Durasi Magangnya",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: "Apakah data pribadi saya aman?",
|
|
||||||
answer: "Data Pribadi Dipastikan Aman, Sistem Ini Memiliki Admin Dashboard",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const Faq = () => {
|
const Faq = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||||
|
const [faqData, setFaqData] = useState<FaqItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const faqIdFromUrl = useMemo(() => {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const raw = params.get("faq");
|
||||||
|
const n = raw ? Number(raw) : NaN;
|
||||||
|
return Number.isFinite(n) ? n : null;
|
||||||
|
}, [location.search]);
|
||||||
|
|
||||||
const toggleItem = (index: number) => {
|
const toggleItem = (index: number) => {
|
||||||
setOpenIndex(openIndex === index ? null : index);
|
setOpenIndex(openIndex === index ? null : index);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchFaq = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = document.cookie
|
||||||
|
.split("; ")
|
||||||
|
.find((row) => row.startsWith("token="))
|
||||||
|
?.split("=")[1];
|
||||||
|
|
||||||
|
const response = await axios.get(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}/faq-api/get`,
|
||||||
|
{
|
||||||
|
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data?.status) {
|
||||||
|
const items: FaqItem[] = Array.isArray(response.data.data)
|
||||||
|
? response.data.data
|
||||||
|
: [];
|
||||||
|
setFaqData(items);
|
||||||
|
} else {
|
||||||
|
setFaqData([]);
|
||||||
|
setError(response.data?.message || "Gagal mengambil data FAQ.");
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Gagal mengambil data FAQ:", err);
|
||||||
|
setFaqData([]);
|
||||||
|
setError(
|
||||||
|
err?.response?.data?.message ||
|
||||||
|
"Terjadi kesalahan saat mengambil data FAQ."
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchFaq();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Auto-scroll ke section FAQ kalau ada #faq
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.hash === "#faq") {
|
||||||
|
// wrapper ada di Landing: <div id="faq"><Faq/></div>
|
||||||
|
// ini memastikan scroll halus
|
||||||
|
const el = document.getElementById("faq");
|
||||||
|
el?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
}
|
||||||
|
}, [location.hash]);
|
||||||
|
|
||||||
|
// Auto-open FAQ tertentu kalau ?faq=ID ada dan data sudah ke-load
|
||||||
|
useEffect(() => {
|
||||||
|
if (!faqIdFromUrl) return;
|
||||||
|
if (faqData.length === 0) return;
|
||||||
|
|
||||||
|
const idx = faqData.findIndex((f) => f.id === faqIdFromUrl);
|
||||||
|
if (idx !== -1) {
|
||||||
|
setOpenIndex(idx);
|
||||||
|
|
||||||
|
// Optional: setelah open, pastikan tetap terlihat
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = document.getElementById("faq");
|
||||||
|
el?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}, [faqIdFromUrl, faqData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hero-5-section flex flex-col lg:flex-row items-start lg:items-center justify-between mt-25 px-6 md:px-16 lg:px-[150px] pb-[60px] gap-10">
|
<div className="hero-5-section flex flex-col lg:flex-row items-start lg:items-center justify-between mt-25 px-6 md:px-16 lg:px-[150px] pb-[60px] gap-10">
|
||||||
|
{/* UI tetap sama persis */}
|
||||||
<div className="content-of-5 w-full lg:w-[610px]">
|
<div className="content-of-5 w-full lg:w-[610px]">
|
||||||
<h2 className="font-bold text-2xl md:text-3xl">Frequently Asked Questions</h2>
|
<h2 className="font-bold text-2xl md:text-3xl">
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
<p className="font-medium text-base mt-4">
|
<p className="font-medium text-base mt-4">
|
||||||
Berikut adalah informasi mengenai pertanyaan umum seputar program magang di Humic, termasuk tata cara pendaftaran dan persyaratan yang perlu dipenuhi. Masih ada yang perlu ditanyakan? Silahkan{" "}
|
Berikut adalah informasi mengenai pertanyaan umum seputar program magang
|
||||||
|
di Humic, termasuk tata cara pendaftaran dan persyaratan yang perlu
|
||||||
|
dipenuhi. Masih ada yang perlu ditanyakan? Silahkan{" "}
|
||||||
<span className="text-[#C3423F] font-semibold">DM</span> kami
|
<span className="text-[#C3423F] font-semibold">DM</span> kami
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{loading && <p className="mt-4 text-sm text-gray-600">Memuat FAQ...</p>}
|
||||||
|
{error && <p className="mt-4 text-sm text-red-600">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="faq-content-list space-y-6 w-full lg:w-[700px]">
|
<div className="faq-content-list space-y-6 w-full lg:w-[700px]">
|
||||||
|
{!loading && !error && faqData.length === 0 && (
|
||||||
|
<div className="bg-[#FAF0EF] shadow-md rounded-sm p-4">
|
||||||
|
<p className="text-sm text-gray-700">Belum ada FAQ.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{faqData.map((faq, index) => (
|
{faqData.map((faq, index) => (
|
||||||
<div key={index} className="bg-[#FAF0EF] shadow-md rounded-sm">
|
<div
|
||||||
<button onClick={() => toggleItem(index)} className="w-full flex items-center cursor-pointer justify-between p-4 focus:outline-none">
|
key={faq.id ?? index}
|
||||||
<span className="text-[16px] font-semibold text-left text-[#C3423F]">{faq.question}</span>
|
className="bg-[#FAF0EF] shadow-md rounded-sm"
|
||||||
<span className="text-[20px] font-bold text-[#C3423F]">{openIndex === index ? "-" : "+"}</span>
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleItem(index)}
|
||||||
|
className="w-full flex items-center cursor-pointer justify-between p-4 focus:outline-none"
|
||||||
|
>
|
||||||
|
<span className="text-[16px] font-semibold text-left text-[#C3423F]">
|
||||||
|
{faq.pertanyaan}
|
||||||
|
</span>
|
||||||
|
<span className="text-[20px] font-bold text-[#C3423F]">
|
||||||
|
{openIndex === index ? "-" : "+"}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div className={`px-4 pb-4 text-black transition-all duration-300 ${openIndex === index ? "block" : "hidden"}`}>
|
|
||||||
<p>{faq.answer}</p>
|
<div
|
||||||
|
className={`px-4 pb-4 text-black transition-all duration-300 ${
|
||||||
|
openIndex === index ? "block" : "hidden"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: faq.jawaban }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ const NavbarAdmin = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
console.log("Response status:", response.status);
|
|
||||||
console.log("Response body:", result);
|
|
||||||
|
|
||||||
if (response.ok && result.data?.nama_depan) {
|
if (response.ok && result.data?.nama_depan) {
|
||||||
setNamaDepan(result.data.nama_depan);
|
setNamaDepan(result.data.nama_depan);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const SidebarAdmin = () => {
|
|||||||
<div className="list-of-sidebar flex flex-col p-7 gap-4">
|
<div className="list-of-sidebar flex flex-col p-7 gap-4">
|
||||||
<div
|
<div
|
||||||
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
|
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
|
||||||
location.pathname === "/product-list"
|
location.pathname === "/dashboard"
|
||||||
? "bg-[#D3DFEA] font-bold"
|
? "bg-[#D3DFEA] font-bold"
|
||||||
: "hover:bg-[#D3DFEA] "
|
: "hover:bg-[#D3DFEA] "
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
+213
-47
@@ -1,6 +1,14 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
|
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
|
||||||
import { Search, ChevronDown, Users, UserCheck, UserX, Clock } from "lucide-react";
|
import {
|
||||||
|
Search,
|
||||||
|
ChevronDown,
|
||||||
|
Users,
|
||||||
|
UserCheck,
|
||||||
|
UserX,
|
||||||
|
Clock,
|
||||||
|
} from "lucide-react";
|
||||||
|
import axios, { AxiosError } from "axios";
|
||||||
import NavbarAdmin from "../Layout/NavbarAdmin";
|
import NavbarAdmin from "../Layout/NavbarAdmin";
|
||||||
import SidebarAdmin from "../Layout/SidebarAdmin";
|
import SidebarAdmin from "../Layout/SidebarAdmin";
|
||||||
|
|
||||||
@@ -17,104 +25,262 @@ type StatCard = {
|
|||||||
iconColor: string;
|
iconColor: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DashboardApiResponse = {
|
// ====== BE RESPONSE TYPES ======
|
||||||
totalPendaftar: number;
|
type DashboardStatsResponse = {
|
||||||
totalDiterima: number;
|
status: boolean;
|
||||||
totalDitolak: number;
|
data: {
|
||||||
totalDiproses: number;
|
total_pendaftar: number;
|
||||||
universitas: ChartDataItem[];
|
total_diterima: number;
|
||||||
negara: ChartDataItem[];
|
total_ditolak: number;
|
||||||
|
sedang_diproses: number;
|
||||||
|
acceptance_rate: string;
|
||||||
|
rejection_rate: string;
|
||||||
|
pending_rate: string;
|
||||||
|
};
|
||||||
|
message: string;
|
||||||
|
code?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PositionStatsResponse = {
|
||||||
|
status: boolean;
|
||||||
|
data: Array<{
|
||||||
|
posisi: string;
|
||||||
|
total: number;
|
||||||
|
diterima: number;
|
||||||
|
ditolak: number;
|
||||||
|
}>;
|
||||||
|
message?: string;
|
||||||
|
code?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CountryStatsResponse = {
|
||||||
|
status: boolean;
|
||||||
|
data: Array<{
|
||||||
|
negara: string;
|
||||||
|
total: number;
|
||||||
|
diterima: number;
|
||||||
|
ditolak: number;
|
||||||
|
diproses: number;
|
||||||
|
}>;
|
||||||
|
message?: string;
|
||||||
|
code?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BatchResponse = {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data: Array<{
|
||||||
|
id: number;
|
||||||
|
batch_number: number;
|
||||||
|
is_active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const UNIVERSITAS_COLORS = ["#001a66", "#334d99", "#6699cc", "#99b3ff"];
|
const UNIVERSITAS_COLORS = ["#001a66", "#334d99", "#6699cc", "#99b3ff"];
|
||||||
const PENERIMAAN_COLORS = ["#0088ff", "#00cc00", "#ff3333"];
|
const PENERIMAAN_COLORS = ["#0088ff", "#00cc00", "#ff3333"];
|
||||||
const NEGARA_COLORS = ["#cc3333", "#dd6666", "#ee9999", "#f0b3b3", "#f5cccc"];
|
const NEGARA_COLORS = ["#cc3333", "#dd6666", "#ee9999", "#f0b3b3", "#f5cccc"];
|
||||||
|
|
||||||
|
const API_BASE_URL =
|
||||||
|
import.meta.env.VITE_API_BASE_URL?.trim() ||
|
||||||
|
"https://internify-ruddy.vercel.app";
|
||||||
|
|
||||||
const Dashboard: React.FC = () => {
|
const Dashboard: React.FC = () => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
const [statCards, setStatCards] = useState<StatCard[]>([]);
|
const [statCards, setStatCards] = useState<StatCard[]>([]);
|
||||||
const [universitasData, setUniversitasData] = useState<ChartDataItem[]>([]);
|
const [universitasData, setUniversitasData] = useState<ChartDataItem[]>([]);
|
||||||
const [penerimaanData, setPenerimaanData] = useState<ChartDataItem[]>([]);
|
const [penerimaanData, setPenerimaanData] = useState<ChartDataItem[]>([]);
|
||||||
const [negaraData, setNegaraData] = useState<ChartDataItem[]>([]);
|
const [negaraData, setNegaraData] = useState<ChartDataItem[]>([]);
|
||||||
|
|
||||||
|
const [, setBatches] = useState<BatchResponse["data"]>([]);
|
||||||
|
|
||||||
|
const getTokenFromCookie = () => {
|
||||||
|
return document.cookie
|
||||||
|
.split("; ")
|
||||||
|
.find((row) => row.startsWith("token="))
|
||||||
|
?.split("=")[1];
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = useMemo(() => {
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.interceptors.request.use((config) => {
|
||||||
|
const token = getTokenFromCookie();
|
||||||
|
if (token) {
|
||||||
|
config.headers = config.headers ?? {};
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getAxiosErrorMessage = (err: unknown) => {
|
||||||
|
if (!axios.isAxiosError(err)) {
|
||||||
|
return "Terjadi kesalahan saat mengambil data dashboard.";
|
||||||
|
}
|
||||||
|
|
||||||
|
const axErr = err as AxiosError<any>;
|
||||||
|
const status = axErr.response?.status;
|
||||||
|
|
||||||
|
const msgFromBe =
|
||||||
|
axErr.response?.data?.message ||
|
||||||
|
axErr.response?.data?.error ||
|
||||||
|
axErr.message;
|
||||||
|
|
||||||
|
if (status === 401) {
|
||||||
|
return msgFromBe || "Unauthorized (401). Silakan login ulang.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return msgFromBe || `Request gagal (status ${status ?? "unknown"}).`;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDashboardData = async () => {
|
const fetchDashboardData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
setLoading(true);
|
||||||
`${import.meta.env.VITE_API_BASE_URL}/lowongan-magang-api/get`
|
setErrorMessage("");
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
const token = getTokenFromCookie();
|
||||||
console.error("Gagal mengambil data dashboard:", result.message);
|
if (!token) {
|
||||||
setErrorMessage(result.message || "Gagal mengambil data dashboard.");
|
setErrorMessage(
|
||||||
|
"Token tidak ditemukan di cookie. Silakan login ulang."
|
||||||
|
);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: DashboardApiResponse = result.data;
|
const [statsRes, positionRes, countryRes, batchRes] = await Promise.all(
|
||||||
|
[
|
||||||
|
api.get<DashboardStatsResponse>(
|
||||||
|
"/lamaran-magang-api/statistics/dashboard"
|
||||||
|
),
|
||||||
|
api.get<PositionStatsResponse>(
|
||||||
|
"/lamaran-magang-api/statistics/position"
|
||||||
|
),
|
||||||
|
api.get<CountryStatsResponse>(
|
||||||
|
"/lamaran-magang-api/statistics/country"
|
||||||
|
),
|
||||||
|
api.get<BatchResponse>("/api/batch"),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const statsJson = statsRes.data;
|
||||||
|
const positionJson = positionRes.data;
|
||||||
|
const countryJson = countryRes.data;
|
||||||
|
const batchJson = batchRes.data;
|
||||||
|
|
||||||
|
if (!statsJson?.status) {
|
||||||
|
setErrorMessage(
|
||||||
|
statsJson?.message ||
|
||||||
|
"Gagal mengambil data dashboard (statistics/dashboard)."
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!positionJson?.status) {
|
||||||
|
setErrorMessage(
|
||||||
|
positionJson?.message ||
|
||||||
|
"Gagal mengambil data dashboard (statistics/position)."
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!countryJson?.status) {
|
||||||
|
setErrorMessage(
|
||||||
|
countryJson?.message ||
|
||||||
|
"Gagal mengambil data dashboard (statistics/country)."
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batchJson?.success) {
|
||||||
|
setBatches(batchJson.data || []);
|
||||||
|
} else {
|
||||||
|
console.warn("Fetch batch gagal:", batchJson?.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = statsJson.data;
|
||||||
|
const positions = positionJson.data || [];
|
||||||
|
const countries = countryJson.data || [];
|
||||||
|
|
||||||
const statCardsFromApi: StatCard[] = [
|
const statCardsFromApi: StatCard[] = [
|
||||||
{
|
{
|
||||||
title: "Total Pendaftar",
|
title: "Total Pendaftar",
|
||||||
value: data.totalPendaftar,
|
value: stats.total_pendaftar,
|
||||||
subtitle: "+200 dari batch sebelumnya", // sesuaikan bila BE sudah support
|
subtitle: "+200 dari batch sebelumnya",
|
||||||
icon: <Users size={24} />,
|
icon: <Users size={24} />,
|
||||||
iconColor: "#0088ff",
|
iconColor: "#0088ff",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Total Diterima",
|
title: "Total Diterima",
|
||||||
value: data.totalDiterima,
|
value: stats.total_diterima,
|
||||||
subtitle: `${(
|
subtitle: `${stats.acceptance_rate} acceptance rate`,
|
||||||
(data.totalDiterima / (data.totalPendaftar || 1)) *
|
|
||||||
100
|
|
||||||
).toFixed(1)}% acceptance rate`,
|
|
||||||
icon: <UserCheck size={24} />,
|
icon: <UserCheck size={24} />,
|
||||||
iconColor: "#00cc00",
|
iconColor: "#00cc00",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Total Ditolak",
|
title: "Total Ditolak",
|
||||||
value: data.totalDitolak,
|
value: stats.total_ditolak,
|
||||||
subtitle: `${(
|
subtitle: `${stats.rejection_rate} rejection rate`,
|
||||||
(data.totalDitolak / (data.totalPendaftar || 1)) *
|
|
||||||
100
|
|
||||||
).toFixed(1)}% rejection rate`,
|
|
||||||
icon: <UserX size={24} />,
|
icon: <UserX size={24} />,
|
||||||
iconColor: "#ff3333",
|
iconColor: "#ff3333",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Sedang Diproses",
|
title: "Sedang Diproses",
|
||||||
value: data.totalDiproses,
|
value: stats.sedang_diproses,
|
||||||
subtitle: `${(
|
subtitle: `${stats.pending_rate} pending review`,
|
||||||
(data.totalDiproses / (data.totalPendaftar || 1)) *
|
|
||||||
100
|
|
||||||
).toFixed(1)}% pending review`,
|
|
||||||
icon: <Clock size={24} />,
|
icon: <Clock size={24} />,
|
||||||
iconColor: "#ffaa00",
|
iconColor: "#ffaa00",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const penerimaanFromApi: ChartDataItem[] = [
|
const penerimaanFromApi: ChartDataItem[] = [
|
||||||
{ name: "Total Pendaftar", value: data.totalPendaftar },
|
{ name: "Total Pendaftar", value: stats.total_pendaftar },
|
||||||
{ name: "Total Diterima", value: data.totalDiterima },
|
{ name: "Total Diterima", value: stats.total_diterima },
|
||||||
{ name: "Total Ditolak", value: data.totalDitolak },
|
{ name: "Total Ditolak", value: stats.total_ditolak },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const universitasFromApi: ChartDataItem[] = positions.map((p) => ({
|
||||||
|
name: p.posisi,
|
||||||
|
value: p.total,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const negaraFromApi: ChartDataItem[] = countries.map((c) => ({
|
||||||
|
name: c.negara,
|
||||||
|
value: c.total,
|
||||||
|
}));
|
||||||
|
|
||||||
setStatCards(statCardsFromApi);
|
setStatCards(statCardsFromApi);
|
||||||
setUniversitasData(data.universitas || []);
|
|
||||||
setPenerimaanData(penerimaanFromApi);
|
setPenerimaanData(penerimaanFromApi);
|
||||||
setNegaraData(data.negara || []);
|
setUniversitasData(universitasFromApi);
|
||||||
|
setNegaraData(negaraFromApi);
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error("Terjadi kesalahan saat mengambil data dashboard:", error);
|
console.error("Terjadi kesalahan saat mengambil data dashboard:", err);
|
||||||
setErrorMessage("Terjadi kesalahan saat mengambil data dashboard.");
|
setErrorMessage(getAxiosErrorMessage(err));
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log("AXIOS BASE URL:", API_BASE_URL);
|
||||||
|
|
||||||
fetchDashboardData();
|
fetchDashboardData();
|
||||||
}, []);
|
}, [api]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen w-full overflow-hidden">
|
<div className="min-h-screen w-full overflow-hidden">
|
||||||
@@ -149,7 +315,9 @@ const Dashboard: React.FC = () => {
|
|||||||
|
|
||||||
{/* Loading / Error State */}
|
{/* Loading / Error State */}
|
||||||
{loading && (
|
{loading && (
|
||||||
<p className="text-center text-gray-600">Memuat data dashboard...</p>
|
<p className="text-center text-gray-600">
|
||||||
|
Memuat data dashboard...
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && errorMessage && (
|
{!loading && errorMessage && (
|
||||||
@@ -329,9 +497,7 @@ const Dashboard: React.FC = () => {
|
|||||||
<Cell
|
<Cell
|
||||||
key={`negara-cell-${index}`}
|
key={`negara-cell-${index}`}
|
||||||
fill={
|
fill={
|
||||||
NEGARA_COLORS[
|
NEGARA_COLORS[index % NEGARA_COLORS.length]
|
||||||
index % NEGARA_COLORS.length
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -216,7 +216,9 @@ const Landing = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hero 5 Section FaQ */}
|
{/* Hero 5 Section FaQ */}
|
||||||
<Faq />
|
<div id="faq">
|
||||||
|
<Faq />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ const ListFaq = () => {
|
|||||||
<button
|
<button
|
||||||
className="bg-[#3BE21D] cursor-pointer text-white p-2 rounded-xl"
|
className="bg-[#3BE21D] cursor-pointer text-white p-2 rounded-xl"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
window.open(`/details-product/${faq.id}`, "_blank")
|
window.open(`/?faq=${faq.id}#faq`, "_blank")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
Reference in New Issue
Block a user