diff --git a/src/Layout/Faq.tsx b/src/Layout/Faq.tsx index eeec409..20375ef 100644 --- a/src/Layout/Faq.tsx +++ b/src/Layout/Faq.tsx @@ -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 = [ - { - question: "Bagaimana cara magang disini?", - answer: "Pilih Internship, Kemudian Lakukan Proses Administrasi", - }, - { - 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", - }, -]; +type FaqItem = { + id: number; + pertanyaan: string; + jawaban: string; + created_at?: string; +}; const Faq = () => { + const location = useLocation(); + const [openIndex, setOpenIndex] = useState(null); + const [faqData, setFaqData] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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) => { 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:
+ // 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 (
+ {/* UI tetap sama persis */}
-

Frequently Asked Questions

+

+ Frequently Asked Questions +

- 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{" "} DM kami

+ + {loading &&

Memuat FAQ...

} + {error &&

{error}

}
+
+ {!loading && !error && faqData.length === 0 && ( +
+

Belum ada FAQ.

+
+ )} + {faqData.map((faq, index) => ( -
- -
-

{faq.answer}

+ +
+
))} diff --git a/src/Layout/NavbarAdmin.tsx b/src/Layout/NavbarAdmin.tsx index ecd9608..f9abf04 100644 --- a/src/Layout/NavbarAdmin.tsx +++ b/src/Layout/NavbarAdmin.tsx @@ -26,8 +26,6 @@ const NavbarAdmin = () => { ); const result = await response.json(); - console.log("Response status:", response.status); - console.log("Response body:", result); if (response.ok && result.data?.nama_depan) { setNamaDepan(result.data.nama_depan); diff --git a/src/Layout/SidebarAdmin.tsx b/src/Layout/SidebarAdmin.tsx index c1e6a79..8e94876 100644 --- a/src/Layout/SidebarAdmin.tsx +++ b/src/Layout/SidebarAdmin.tsx @@ -11,7 +11,7 @@ const SidebarAdmin = () => {
; + 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 PENERIMAAN_COLORS = ["#0088ff", "#00cc00", "#ff3333"]; 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 [loading, setLoading] = useState(true); const [errorMessage, setErrorMessage] = useState(""); + const [statCards, setStatCards] = useState([]); const [universitasData, setUniversitasData] = useState([]); const [penerimaanData, setPenerimaanData] = useState([]); const [negaraData, setNegaraData] = useState([]); + const [, setBatches] = useState([]); + + 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; + 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(() => { const fetchDashboardData = async () => { try { - const response = await fetch( - `${import.meta.env.VITE_API_BASE_URL}/lowongan-magang-api/get` - ); - const result = await response.json(); + setLoading(true); + setErrorMessage(""); - if (!response.ok) { - console.error("Gagal mengambil data dashboard:", result.message); - setErrorMessage(result.message || "Gagal mengambil data dashboard."); + const token = getTokenFromCookie(); + if (!token) { + setErrorMessage( + "Token tidak ditemukan di cookie. Silakan login ulang." + ); setLoading(false); return; } - const data: DashboardApiResponse = result.data; + const [statsRes, positionRes, countryRes, batchRes] = await Promise.all( + [ + api.get( + "/lamaran-magang-api/statistics/dashboard" + ), + api.get( + "/lamaran-magang-api/statistics/position" + ), + api.get( + "/lamaran-magang-api/statistics/country" + ), + api.get("/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[] = [ { title: "Total Pendaftar", - value: data.totalPendaftar, - subtitle: "+200 dari batch sebelumnya", // sesuaikan bila BE sudah support + value: stats.total_pendaftar, + subtitle: "+200 dari batch sebelumnya", icon: , iconColor: "#0088ff", }, { title: "Total Diterima", - value: data.totalDiterima, - subtitle: `${( - (data.totalDiterima / (data.totalPendaftar || 1)) * - 100 - ).toFixed(1)}% acceptance rate`, + value: stats.total_diterima, + subtitle: `${stats.acceptance_rate} acceptance rate`, icon: , iconColor: "#00cc00", }, { title: "Total Ditolak", - value: data.totalDitolak, - subtitle: `${( - (data.totalDitolak / (data.totalPendaftar || 1)) * - 100 - ).toFixed(1)}% rejection rate`, + value: stats.total_ditolak, + subtitle: `${stats.rejection_rate} rejection rate`, icon: , iconColor: "#ff3333", }, { title: "Sedang Diproses", - value: data.totalDiproses, - subtitle: `${( - (data.totalDiproses / (data.totalPendaftar || 1)) * - 100 - ).toFixed(1)}% pending review`, + value: stats.sedang_diproses, + subtitle: `${stats.pending_rate} pending review`, icon: , iconColor: "#ffaa00", }, ]; const penerimaanFromApi: ChartDataItem[] = [ - { name: "Total Pendaftar", value: data.totalPendaftar }, - { name: "Total Diterima", value: data.totalDiterima }, - { name: "Total Ditolak", value: data.totalDitolak }, + { name: "Total Pendaftar", value: stats.total_pendaftar }, + { name: "Total Diterima", value: stats.total_diterima }, + { 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); - setUniversitasData(data.universitas || []); setPenerimaanData(penerimaanFromApi); - setNegaraData(data.negara || []); + setUniversitasData(universitasFromApi); + setNegaraData(negaraFromApi); + setLoading(false); - } catch (error) { - console.error("Terjadi kesalahan saat mengambil data dashboard:", error); - setErrorMessage("Terjadi kesalahan saat mengambil data dashboard."); + } catch (err) { + console.error("Terjadi kesalahan saat mengambil data dashboard:", err); + setErrorMessage(getAxiosErrorMessage(err)); setLoading(false); } }; + console.log("AXIOS BASE URL:", API_BASE_URL); + fetchDashboardData(); - }, []); + }, [api]); return (
@@ -149,7 +315,9 @@ const Dashboard: React.FC = () => { {/* Loading / Error State */} {loading && ( -

Memuat data dashboard...

+

+ Memuat data dashboard... +

)} {!loading && errorMessage && ( @@ -329,9 +497,7 @@ const Dashboard: React.FC = () => { ))} diff --git a/src/Pages/Landing.tsx b/src/Pages/Landing.tsx index 4212137..422c9d6 100644 --- a/src/Pages/Landing.tsx +++ b/src/Pages/Landing.tsx @@ -216,7 +216,9 @@ const Landing = () => {
{/* Hero 5 Section FaQ */} - +
+ +
{/* Footer */}