add dashboard, feedback, faq

This commit is contained in:
Billy Marcello
2025-11-22 19:21:04 +07:00
parent fcdbf7d664
commit 96ee371975
16 changed files with 1626 additions and 64 deletions
+10
View File
@@ -20,6 +20,11 @@ import ListLowongan from "./Pages/ListLowongan";
import ListProduct from "./Pages/ListProduct";
import AddProductHumic from "./Pages/AddProductHumic";
import EditProductHumic from "./Pages/EditProductHumic";
import ListFeedback from "./Pages/ListFeedback";
import AddFeedback from "./Pages/AddFeedback";
import ListFaq from "./Pages/ListFaq";
import AddFaq from "./Pages/AddFAQ";
import Dashboard from "./Pages/Dashboard";
function App() {
return (
@@ -51,6 +56,11 @@ function App() {
<Route path="/partnership-admin" element={<PartnershipsList />} />
<Route path="/add-partnership" element={<AddPartnershipAdmin />} />
<Route path="/edit-partnership/:id" element={<EditPartnerships />} />
<Route path="/feedback-list" element={<ListFeedback />} />
<Route path="/add-feedback" element={<AddFeedback />} />
<Route path="/faq-list" element={<ListFaq />} />
<Route path="/add-faq" element={<AddFaq />} />
<Route path="/dashboard" element={<Dashboard />} />
</Route>
</Routes>
</Router>
+8 -2
View File
@@ -52,9 +52,15 @@ const Footer = () => {
<p>Tentang</p>
<p>Life at Humic</p>
</div>
<div className="sec-2 flex flex-col gap-3">
<div className="sec-2 flex flex-col gap-3 hover:cursor-pointer">
<p className="font-bold">Lainnya</p>
<p>Humic Website</p>
<p
onClick={() =>
window.open("https://humic.telkomuniversity.ac.id", "_blank")
}
>
Humic Website
</p>
<p>Our Activity</p>
<p>Life at Humic</p>
<p
+92 -1
View File
@@ -11,7 +11,37 @@ const SidebarAdmin = () => {
<div className="list-of-sidebar flex flex-col p-7 gap-4">
<div
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
location.pathname === "/produc-lList"
location.pathname === "/product-list"
? "bg-[#D3DFEA] font-bold"
: "hover:bg-[#D3DFEA] "
}`}
onClick={() => navigate("/dashboard")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
className="size-8 text-[#2C5282]"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3 L5 8 A4 4 0 0 0 3 11 L3 15 A4 4 0 0 0 7 19 L17 19 A4 4 0 0 0 21 15 L21 11 A4 4 0 0 0 19 8 L12 3 Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 13 v4"
/>
</svg>
<h2 className="text-[#2C5282]">Dashboard</h2>
</div>
<div
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
location.pathname === "/product-list"
? "bg-[#D3DFEA] font-bold"
: "hover:bg-[#D3DFEA] "
}`}
@@ -108,6 +138,67 @@ const SidebarAdmin = () => {
<h2 className="text-[#2C5282]">Pelamar</h2>
</div>
<div
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
location.pathname === "/feedbacks-list"
? "bg-[#D3DFEA] font-bold"
: "hover:bg-[#D3DFEA]"
}`}
onClick={() => navigate("/feedback-list")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-8 text-[#2C5282]"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 4h16a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-5l-3 3-3-3H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
/>
<circle cx="8" cy="10" r="1.2" fill="currentColor" />
<circle cx="12" cy="10" r="1.2" fill="currentColor" />
<circle cx="16" cy="10" r="1.2" fill="currentColor" />
</svg>
<h2 className="text-[#2C5282]">Feedback</h2>
</div>
<div
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
location.pathname === "/faq-list"
? "bg-[#D3DFEA] font-bold"
: "hover:bg-[#D3DFEA]"
}`}
onClick={() => navigate("/faq-list")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-8 text-[#2C5282]"
>
{/* Balon chat */}
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 4h16a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-5l-3 3-3-3H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z"
/>
{/* Tanda tanya */}
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.75 9.5a2.25 2.25 0 1 1 4.5 0c0 1.25-.75 1.75-1.5 1.85s-1 1-1 1.75"
/>
<circle cx="12" cy="16" r="0.6" fill="currentColor" />
</svg>
<h2 className="text-[#2C5282]">FAQ</h2>
</div>
</div>
</div>
+114
View File
@@ -0,0 +1,114 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import SidebarAdmin from "../Layout/SidebarAdmin";
import NavbarAdmin from "../Layout/NavbarAdmin";
import "froala-editor/js/plugins.pkgd.min.js";
import "froala-editor/css/froala_editor.pkgd.min.css";
import "froala-editor/css/froala_style.min.css";
import FroalaEditor from "react-froala-wysiwyg";
import axios from "axios";
const AddFaq = () => {
const [questions, setQuestions] = useState("");
const [answers, setAnswers] = useState("");
const navigate = useNavigate();
const handleSubmit = async () => {
const token = document.cookie
.split("; ")
.find((row) => row.startsWith("token="))
?.split("=")[1];
if (!token) {
alert("Token tidak ditemukan.");
return;
}
if (!questions || !answers) {
alert("Semua field wajib diisi.");
return;
}
try {
const response = await axios.post(
`${import.meta.env.VITE_API_BASE_URL}/faq-api/add`,
{
pertanyaan: questions,
jawaban: answers,
},
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
if (response.data.status) {
alert("Faq berhasil ditambahkan!");
navigate("/faq-list");
} else {
alert(response.data.message || "Gagal menambahkan faq.");
}
} catch (error: any) {
if (error.response) {
console.log("Error response data:", error.response.data);
}
console.error("Error adding faq:", error);
alert("Terjadi kesalahan saat menambahkan faq.");
}
};
return (
<div className="min-h-screen w-full overflow-hidden">
<NavbarAdmin />
<div className="flex flex-row">
<SidebarAdmin />
<div className="p-10 bg-white flex-col flex-wrap w-full">
<div className="flex justify-between items-center mb-8">
<h2 className="text-xl font-semibold">
Tambah Frequently Asked Questions
</h2>
<button
className="bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white px-6 py-2 rounded-md font-medium"
onClick={handleSubmit}
>
Submit
</button>
</div>
<form className="flex flex-col gap-6 max-w-full">
{/* Questions */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Questions</label>
<input
type="text"
className="border border-gray-300 rounded-lg p-3"
value={questions}
onChange={(e) => setQuestions(e.target.value)}
placeholder="Tulis pertanyaan kamu..."
/>
</div>
{/* Answers */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Answers</label>
<FroalaEditor
tag="textarea"
model={answers}
onModelChange={(model: any) => setAnswers(model)}
config={{
placeholderText: "Tulis jawaban kamu...",
charCounterCount: true,
toolbarSticky: true,
heightMin: 200,
}}
/>
</div>
</form>
</div>
</div>
</div>
);
};
export default AddFaq;
+178
View File
@@ -0,0 +1,178 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import SidebarAdmin from "../Layout/SidebarAdmin";
import NavbarAdmin from "../Layout/NavbarAdmin";
import "froala-editor/js/plugins.pkgd.min.js";
import "froala-editor/css/froala_editor.pkgd.min.css";
import "froala-editor/css/froala_style.min.css";
import FroalaEditor from "react-froala-wysiwyg";
import axios from "axios";
const AddFeedback = () => {
const [posisi, setPosisi] = useState("");
const [universitas, setUniversitas] = useState("");
const [nama, setNama] = useState("");
const [image, setImage] = useState<File | null>(null);
const [batch, setBatch] = useState("");
const [feedback, setFeedbacks] = useState("");
const [year, setYear] = useState("");
const navigate = useNavigate();
const handleSubmit = async () => {
const token = document.cookie
.split("; ")
.find((row) => row.startsWith("token="))
?.split("=")[1];
if (!token) {
alert("Token tidak ditemukan.");
return;
}
if (!nama || !universitas || !posisi || !batch || !year || !feedback) {
alert("Semua field wajib diisi.");
return;
}
try {
const formData = new FormData();
formData.append("nama", nama);
formData.append("universitas", universitas);
formData.append("posisi", posisi);
formData.append("batch", batch);
formData.append("tahun", year);
formData.append("pesan", feedback);
if (image) formData.append("image", image);
const response = await axios.post(
`${import.meta.env.VITE_API_BASE_URL}/feedback-api/add`,
formData,
{
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
},
}
);
if (response.data.status) {
alert("Feedback berhasil ditambahkan!");
navigate("/feedback-list");
} else {
alert(response.data.message || "Gagal menambahkan feedback.");
}
} catch (error: any) {
console.error("Error adding feedback:", error);
alert("Terjadi kesalahan saat menambahkan feedback.");
}
};
return (
<div className="min-h-screen w-full overflow-hidden">
<NavbarAdmin />
<div className="flex flex-row">
<SidebarAdmin />
<div className="p-10 bg-white flex-col flex-wrap w-full">
<div className="flex justify-between items-center mb-8">
<h2 className="text-xl font-semibold">Tambah Feedback</h2>
<button
className="bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white px-6 py-2 rounded-md font-medium"
onClick={handleSubmit}
>
Submit
</button>
</div>
<form className="flex flex-col gap-6 max-w-full">
<div className="grid grid-cols-2 gap-6">
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Nama</label>
<input
type="text"
className="border border-gray-300 rounded-lg p-3"
value={nama}
onChange={(e) => setNama(e.target.value)}
placeholder="John Doe"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Universitas</label>
<input
type="text"
className="border border-gray-300 rounded-lg p-3"
value={universitas}
onChange={(e) => setUniversitas(e.target.value)}
placeholder="Telkom University Bandung"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Posisi</label>
<input
type="text"
className="border border-gray-300 rounded-lg p-3"
value={posisi}
onChange={(e) => setPosisi(e.target.value)}
placeholder="UI/UX Designer"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Batch</label>
<input
type="text"
className="border border-gray-300 rounded-lg p-3"
value={batch}
onChange={(e) => setBatch(e.target.value)}
placeholder="5"
/>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Tahun</label>
<input
type="text"
className="border border-gray-300 rounded-lg p-3"
value={year}
onChange={(e) => setYear(e.target.value)}
placeholder="2025"
/>
</div>
<div className="flex flex-col gap-2 col-span-2">
<label className="text-sm font-medium">Image anda</label>
<input
type="file"
accept="image/*"
onChange={(e) =>
setImage(e.target.files ? e.target.files[0] : null)
}
className="border border-gray-300 rounded-lg p-3"
/>
</div>
</div>
{/* Isi Konten */}
<div className="flex flex-col gap-2">
<label className="text-sm font-medium">Isi Konten</label>
<FroalaEditor
tag="textarea"
model={feedback}
onModelChange={(model: any) => setFeedbacks(model)}
config={{
placeholderText: "Tulis feedback kamu...",
charCounterCount: true,
toolbarSticky: true,
heightMin: 200,
}}
/>
</div>
</form>
</div>
</div>
</div>
);
};
export default AddFeedback;
+372
View File
@@ -0,0 +1,372 @@
import React, { useEffect, useState } from "react";
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
import { Search, ChevronDown, Users, UserCheck, UserX, Clock } from "lucide-react";
import NavbarAdmin from "../Layout/NavbarAdmin";
import SidebarAdmin from "../Layout/SidebarAdmin";
type ChartDataItem = {
name: string;
value: number;
};
type StatCard = {
title: string;
value: number;
subtitle: string;
icon: React.ReactNode;
iconColor: string;
};
type DashboardApiResponse = {
totalPendaftar: number;
totalDiterima: number;
totalDitolak: number;
totalDiproses: number;
universitas: ChartDataItem[];
negara: ChartDataItem[];
};
const UNIVERSITAS_COLORS = ["#001a66", "#334d99", "#6699cc", "#99b3ff"];
const PENERIMAAN_COLORS = ["#0088ff", "#00cc00", "#ff3333"];
const NEGARA_COLORS = ["#cc3333", "#dd6666", "#ee9999", "#f0b3b3", "#f5cccc"];
const Dashboard: React.FC = () => {
const [loading, setLoading] = useState(true);
const [errorMessage, setErrorMessage] = useState("");
const [statCards, setStatCards] = useState<StatCard[]>([]);
const [universitasData, setUniversitasData] = useState<ChartDataItem[]>([]);
const [penerimaanData, setPenerimaanData] = useState<ChartDataItem[]>([]);
const [negaraData, setNegaraData] = useState<ChartDataItem[]>([]);
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();
if (!response.ok) {
console.error("Gagal mengambil data dashboard:", result.message);
setErrorMessage(result.message || "Gagal mengambil data dashboard.");
setLoading(false);
return;
}
const data: DashboardApiResponse = result.data;
const statCardsFromApi: StatCard[] = [
{
title: "Total Pendaftar",
value: data.totalPendaftar,
subtitle: "+200 dari batch sebelumnya", // sesuaikan bila BE sudah support
icon: <Users size={24} />,
iconColor: "#0088ff",
},
{
title: "Total Diterima",
value: data.totalDiterima,
subtitle: `${(
(data.totalDiterima / (data.totalPendaftar || 1)) *
100
).toFixed(1)}% acceptance rate`,
icon: <UserCheck size={24} />,
iconColor: "#00cc00",
},
{
title: "Total Ditolak",
value: data.totalDitolak,
subtitle: `${(
(data.totalDitolak / (data.totalPendaftar || 1)) *
100
).toFixed(1)}% rejection rate`,
icon: <UserX size={24} />,
iconColor: "#ff3333",
},
{
title: "Sedang Diproses",
value: data.totalDiproses,
subtitle: `${(
(data.totalDiproses / (data.totalPendaftar || 1)) *
100
).toFixed(1)}% pending review`,
icon: <Clock size={24} />,
iconColor: "#ffaa00",
},
];
const penerimaanFromApi: ChartDataItem[] = [
{ name: "Total Pendaftar", value: data.totalPendaftar },
{ name: "Total Diterima", value: data.totalDiterima },
{ name: "Total Ditolak", value: data.totalDitolak },
];
setStatCards(statCardsFromApi);
setUniversitasData(data.universitas || []);
setPenerimaanData(penerimaanFromApi);
setNegaraData(data.negara || []);
setLoading(false);
} catch (error) {
console.error("Terjadi kesalahan saat mengambil data dashboard:", error);
setErrorMessage("Terjadi kesalahan saat mengambil data dashboard.");
setLoading(false);
}
};
fetchDashboardData();
}, []);
return (
<div className="min-h-screen w-full overflow-hidden">
<NavbarAdmin />
<div className="flex flex-row">
<SidebarAdmin />
<div className="w-full flex-col flex-wrap bg-white p-10">
{/* Header Section */}
<div className="mb-[50px] flex flex-row items-center justify-between">
<h2 className="text-[24px] font-semibold">Dashboard</h2>
<div className="flex items-center gap-4">
{/* Batch Filter */}
<button className="flex items-center gap-2 rounded-xl border border-gray-300 bg-white px-[20px] py-[10px] text-gray-700 hover:bg-gray-50">
Batch 3 - 2025
<ChevronDown size={18} />
</button>
{/* Search Box */}
<div className="flex items-center gap-2 rounded-xl border border-gray-300 bg-white px-[20px] py-[10px]">
<Search size={18} className="text-gray-400" />
<input
type="text"
placeholder="Nama, Jurusan,"
className="w-52 text-sm text-gray-700 placeholder-gray-400 outline-none"
/>
</div>
</div>
</div>
{/* Loading / Error State */}
{loading && (
<p className="text-center text-gray-600">Memuat data dashboard...</p>
)}
{!loading && errorMessage && (
<p className="text-center text-red-500">{errorMessage}</p>
)}
{!loading && !errorMessage && (
<>
{/* Stat Cards */}
<div className="mb-8 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
{statCards.map((card, index) => (
<div
key={index}
className="rounded-lg border border-gray-300 bg-white p-6"
>
<div className="mb-4 flex items-center gap-3">
<div
className="rounded-lg bg-gray-100 p-2"
style={{ color: card.iconColor }}
>
{card.icon}
</div>
<h3 className="text-sm font-medium text-blue-600">
{card.title}
</h3>
</div>
<div className="mb-2">
<p className="text-2xl font-bold text-gray-900">
{card.value}
</p>
</div>
<p className="text-xs" style={{ color: card.iconColor }}>
{card.subtitle}
</p>
</div>
))}
</div>
{/* Charts Section */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* Universitas Chart */}
<div className="rounded-lg border border-gray-300 bg-white p-6">
<h2 className="mb-6 text-lg font-bold text-gray-900">
Universitas
</h2>
<div className="mb-6 flex justify-center">
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie
data={universitasData}
cx="50%"
cy="50%"
innerRadius={0}
outerRadius={100}
paddingAngle={2}
dataKey="value"
>
{universitasData.map((_, index) => (
<Cell
key={`universitas-cell-${index}`}
fill={
UNIVERSITAS_COLORS[
index % UNIVERSITAS_COLORS.length
]
}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
{/* Legend */}
<div className="space-y-3 text-sm">
{universitasData.map((item, index) => (
<div
key={`universitas-legend-${index}`}
className="flex items-center justify-between"
>
<div className="flex items-center gap-2">
<div
className="h-3 w-3 rounded"
style={{
backgroundColor:
UNIVERSITAS_COLORS[
index % UNIVERSITAS_COLORS.length
],
}}
/>
<span className="text-gray-700">{item.name}</span>
</div>
<span className="text-sm font-semibold text-gray-900">
{item.value}%
</span>
</div>
))}
</div>
</div>
{/* Right Column - Penerimaan & Negara */}
<div className="space-y-6">
{/* Penerimaan Chart */}
<div className="rounded-lg border border-gray-300 bg-white p-6">
<h2 className="mb-6 text-lg font-bold text-gray-900">
Penerimaan
</h2>
<div className="mb-6 flex justify-center">
<ResponsiveContainer width="100%" height={180}>
<PieChart>
<Pie
data={penerimaanData}
cx="50%"
cy="50%"
innerRadius={0}
outerRadius={70}
paddingAngle={2}
dataKey="value"
>
{penerimaanData.map((_, index) => (
<Cell
key={`penerimaan-cell-${index}`}
fill={
PENERIMAAN_COLORS[
index % PENERIMAAN_COLORS.length
]
}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
{/* Legend */}
<div className="space-y-2 text-sm">
{penerimaanData.map((item, index) => (
<div
key={`penerimaan-legend-${index}`}
className="flex items-center gap-2"
>
<div
className="h-3 w-3 rounded"
style={{
backgroundColor:
PENERIMAAN_COLORS[
index % PENERIMAAN_COLORS.length
],
}}
/>
<span className="text-gray-700">{item.name}</span>
</div>
))}
</div>
</div>
{/* Negara Chart */}
<div className="rounded-lg border border-gray-300 bg-white p-6">
<h2 className="mb-6 text-lg font-bold text-gray-900">
Negara
</h2>
<div className="mb-6 flex justify-center">
<ResponsiveContainer width="100%" height={180}>
<PieChart>
<Pie
data={negaraData}
cx="50%"
cy="50%"
innerRadius={0}
outerRadius={70}
paddingAngle={2}
dataKey="value"
>
{negaraData.map((_, index) => (
<Cell
key={`negara-cell-${index}`}
fill={
NEGARA_COLORS[
index % NEGARA_COLORS.length
]
}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
{/* Legend */}
<div className="space-y-2 text-sm">
{negaraData.map((item, index) => (
<div
key={`negara-legend-${index}`}
className="flex items-center gap-2"
>
<div
className="h-3 w-3 rounded"
style={{
backgroundColor:
NEGARA_COLORS[index % NEGARA_COLORS.length],
}}
/>
<span className="text-gray-700">{item.name}</span>
</div>
))}
</div>
</div>
</div>
</div>
</>
)}
</div>
</div>
</div>
);
};
export default Dashboard;
+6 -2
View File
@@ -23,8 +23,10 @@ const DetailsProduct = () => {
fetch(`${import.meta.env.VITE_API_BASE_URL}/hasil-research-api/get/${id}`)
.then((res) => res.json())
.then((data) => {
if (data && data.data && data.data.length > 0) {
setProduct(data.data[0]);
if (data && data.data) {
setProduct(data.data);
} else {
setProduct(null);
}
setLoading(false);
})
@@ -43,6 +45,8 @@ const DetailsProduct = () => {
<div className="text-center mt-10 text-red-500">Data tidak ditemukan</div>
);
}
console.log(`${import.meta.env.VITE_API_BASE_URL}${product.image_path}`);
return (
<div className="body-details-of-product bg-[#F8F9FA] min-h-screen">
+3 -3
View File
@@ -144,9 +144,9 @@ const EditPartnerships = () => {
<div className="flex flex-row">
<SidebarAdmin />
<div className="p-10 bg-white flex-col flex-wrap w-full">
<div className="header-button flex justify-between items-center mb-[45px]">
<div className="flex justify-between items-center mb-[45px]">
<h2 className="text-[24px] font-semibold">Edit Partnership</h2>
<div className="flex gap-4">
<div className="flex gap-4 pr-10">
<button
type="button"
onClick={handleSubmit}
@@ -165,7 +165,7 @@ const EditPartnerships = () => {
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-8 w-full">
<div className="flex flex-row gap-16">
<div className="flex flex-row gap-10">
<div className="flex flex-col items-start gap-[15px]">
<h2 className="text-[16px] font-bold">Nama</h2>
<input
+4 -4
View File
@@ -57,7 +57,7 @@ const Internships = () => {
title: item.posisi,
location: item.lokasi,
category: item.kelompok_peminatan,
status: item.status_lowongan === "dibuka" ? "Opened" : "Closed",
status: item.status_lowongan === "dibuka" ? "Open" : "Closed",
statusColor:
item.status_lowongan === "dibuka" ? "#4EAD00" : "#C3423F",
isPaid: item.paid === "paid",
@@ -171,12 +171,12 @@ const Internships = () => {
<div
key={index}
className={`card-1 flex flex-col items-start p-5 lg:p-7 rounded-xl shadow-2xl ${
intern.status === "Opened"
intern.status === "Open"
? "bg-white cursor-pointer hover:shadow-xl transition"
: "bg-gray-200 cursor-not-allowed opacity-60"
}`}
onClick={() => {
if (intern.status === "Opened") {
if (intern.status === "Open") {
navigate(`/details/${intern.id}`);
}
}}
@@ -189,7 +189,7 @@ const Internships = () => {
className="p-2 border-2 text-[12px] lg:text-[14px] font-bold rounded-4xl"
style={{
backgroundColor:
intern.status === "Opened" ? "#F6FFF4" : "#FAF0EF",
intern.status === "Open" ? "#F6FFF4" : "#FAF0EF",
color: intern.statusColor,
borderColor: intern.statusColor,
}}
+208
View File
@@ -0,0 +1,208 @@
import NavbarAdmin from "../Layout/NavbarAdmin";
import SidebarAdmin from "../Layout/SidebarAdmin";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
type Faq = {
id: number;
pertanyaan: string;
jawaban: string;
created_at: string;
};
const ListFaq = () => {
const navigate = useNavigate();
const [faq, setFaq] = useState<Faq[]>([]);
useEffect(() => {
const fetchFaq = async () => {
try {
const response = await fetch(
`${import.meta.env.VITE_API_BASE_URL}/faq-api/get`
);
const result = await response.json();
if (response.ok) {
setFaq(result.data);
} else {
console.error("Gagal mengambil data FAQ:", result.message);
}
} catch (error) {
console.error("Terjadi kesalahan:", error);
}
};
fetchFaq();
}, []);
const handleDelete = async (id: number) => {
const confirmDelete = confirm("Apakah Anda yakin ingin menghapus FAQ ini?");
if (!confirmDelete) return;
const token = document.cookie
.split("; ")
.find((row) => row.startsWith("token="))
?.split("=")[1];
if (!token) {
alert("Token tidak ditemukan. Silakan login ulang.");
return;
}
try {
const response = await fetch(
`${import.meta.env.VITE_API_BASE_URL}/faq-api/delete/${id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const result = await response.json();
if (response.ok) {
alert("Feedback berhasil dihapus!");
setFaq((prev) => prev.filter((faq) => faq.id !== id));
} else {
alert(result.message || "Gagal menghapus FAQ.");
}
} catch (error) {
console.error("Gagal menghapus:", error);
alert("Terjadi kesalahan saat menghapus FAQ.");
}
};
return (
<div className="min-h-screen w-full overflow-hidden">
<NavbarAdmin />
<div className="flex flex-row">
<SidebarAdmin />
<div className="p-10 bg-white flex-col flex-wrap w-full">
<div className="flex flex-row justify-between items-center mb-[50px]">
<h2 className="text-[24px] font-semibold">
Frequently Asked Questions
</h2>
<button
className="px-[20px] py-[10px] rounded-xl bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white font-semibold"
onClick={() => navigate("/add-faq")}
>
Tambah
</button>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead>
<tr className="text-left">
<th className="px-6 py-3">Questions</th>
<th className="px-6 py-3">Answers</th>
<th className="px-6 py-3">Tanggal Unggah</th>
<th className="px-6 py-3">Aksi</th>
</tr>
</thead>
<tbody>
{faq.map((faq) => (
<tr key={faq.id} className="hover:bg-gray-50">
<td className="px-6 py-3">{faq.pertanyaan}</td>
<td className="px-6 py-3">
<div dangerouslySetInnerHTML={{ __html: faq.jawaban }} />
</td>
<td className="px-6 py-3">
{new Date(faq.created_at).toLocaleDateString()}
</td>
<td className="px-6 py-3 flex gap-2">
{/* Tombol View */}
<button
className="bg-[#3BE21D] cursor-pointer text-white p-2 rounded-xl"
onClick={() =>
window.open(`/details-product/${faq.id}`, "_blank")
}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</button>
{/* Tombol Edit */}
<button
className="bg-[#2CAEFF] text-white cursor-pointer p-2 rounded-xl"
onClick={() => navigate(`/edit-faq/${faq.id}`)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 7.125 18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
/>
</svg>
</button>
{/* Tombol Hapus */}
<button
className="bg-[#E41E1E] text-white cursor-pointer p-2 rounded-xl"
onClick={() => handleDelete(faq.id)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
{faq.length === 0 && (
<p className="text-gray-500 mt-4 text-center">
Belum ada data FAQ.
</p>
)}
</div>
</div>
</div>
</div>
);
};
export default ListFaq;
+207
View File
@@ -0,0 +1,207 @@
import NavbarAdmin from "../Layout/NavbarAdmin";
import SidebarAdmin from "../Layout/SidebarAdmin";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
type Feedback = {
id: number;
nama: string;
image: string;
created_at: string;
};
const ListFeedback = () => {
const navigate = useNavigate();
const [feedbacks, setFeedbacks] = useState<Feedback[]>([]);
useEffect(() => {
const fetchFeedbacks = async () => {
try {
const response = await fetch(
`${import.meta.env.VITE_API_BASE_URL}/feedback-api/get`
);
const result = await response.json();
if (response.ok) {
setFeedbacks(result.data);
} else {
console.error("Gagal mengambil data feedback:", result.message);
}
} catch (error) {
console.error("Terjadi kesalahan:", error);
}
};
fetchFeedbacks();
}, []);
const handleDelete = async (id: number) => {
const confirmDelete = confirm(
"Apakah Anda yakin ingin menghapus feedback ini?"
);
if (!confirmDelete) return;
const token = document.cookie
.split("; ")
.find((row) => row.startsWith("token="))
?.split("=")[1];
if (!token) {
alert("Token tidak ditemukan. Silakan login ulang.");
return;
}
try {
const response = await fetch(
`${import.meta.env.VITE_API_BASE_URL}/feedback-api/delete/${id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const result = await response.json();
if (response.ok) {
alert("Feedback berhasil dihapus!");
setFeedbacks((prev) => prev.filter((feedback) => feedback.id !== id));
} else {
alert(result.message || "Gagal menghapus feedback.");
}
} catch (error) {
console.error("Gagal menghapus:", error);
alert("Terjadi kesalahan saat menghapus feedback.");
}
};
return (
<div className="min-h-screen w-full overflow-hidden">
<NavbarAdmin />
<div className="flex flex-row">
<SidebarAdmin />
<div className="p-10 bg-white flex-col flex-wrap w-full">
<div className="flex flex-row justify-between items-center mb-[50px]">
<h2 className="text-[24px] font-semibold">Feedback</h2>
<button
className="px-[20px] py-[10px] rounded-xl bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white font-semibold"
onClick={() => navigate("/add-feedback")}
>
Tambah
</button>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead>
<tr className="text-left">
<th className="px-6 py-3">Nama</th>
<th className="px-6 py-3">Foto</th>
<th className="px-6 py-3">Tanggal Unggah</th>
<th className="px-6 py-3">Aksi</th>
</tr>
</thead>
<tbody>
{feedbacks.map((feedback) => (
<tr key={feedback.id} className="hover:bg-gray-50">
<td className="px-6 py-3">{feedback.nama}</td>
<td className="px-6 py-3">
<img src={feedback.image} alt={feedback.nama} />
</td>
<td className="px-6 py-3">
{new Date(feedback.created_at).toLocaleDateString()}
</td>
<td className="px-6 py-3 flex gap-2">
{/* Tombol View */}
<button
className="bg-[#3BE21D] cursor-pointer text-white p-2 rounded-xl"
onClick={() =>
window.open(`/details-product/${feedback.id}`, "_blank")
}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</button>
{/* Tombol Edit */}
<button
className="bg-[#2CAEFF] text-white cursor-pointer p-2 rounded-xl"
onClick={() =>
navigate(`/edit-product-humic/${feedback.id}`)
}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 7.125 18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
/>
</svg>
</button>
{/* Tombol Hapus */}
<button
className="bg-[#E41E1E] text-white cursor-pointer p-2 rounded-xl"
onClick={() => handleDelete(feedback.id)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
{feedbacks.length === 0 && (
<p className="text-gray-500 mt-4 text-center">Belum ada data feedback.</p>
)}
</div>
</div>
</div>
</div>
);
};
export default ListFeedback;
+1 -1
View File
@@ -190,7 +190,7 @@ const ListProduct = () => {
</table>
{projects.length === 0 && (
<p className="text-gray-500 mt-4">Belum ada data project.</p>
<p className="text-gray-500 mt-4 text-center">Belum ada data project.</p>
)}
</div>
</div>
+49 -3
View File
@@ -17,7 +17,10 @@ const RegisterInternships = () => {
nama_belakang: "",
email: "",
kontak: "",
universitas: "",
negara: "",
jurusan: "",
batch: "",
motivasi: "",
relevant_skills: "",
cv: null as File | null,
@@ -44,7 +47,10 @@ const RegisterInternships = () => {
}
data.append("email", formData.email);
data.append("kontak", formData.kontak);
data.append("universitas", formData.universitas);
data.append("negara", formData.negara);
data.append("jurusan", formData.jurusan);
data.append("batch", formData.batch);
data.append("motivasi", formData.motivasi);
data.append("relevant_skills", formData.relevant_skills);
data.append("g-recaptcha-response", recaptchaToken);
@@ -131,12 +137,13 @@ const RegisterInternships = () => {
<input
type="text"
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
placeholder="John"
placeholder="Muhammad Budi"
value={formData.nama_depan}
onChange={(e) =>
setFormData({ ...formData, nama_depan: e.target.value })
}
/>
<p className="text-sm text-[#C3423F] mt-1">*required</p>
</div>
<div>
@@ -144,13 +151,13 @@ const RegisterInternships = () => {
<input
type="text"
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
placeholder="Doe"
placeholder="Pekerti"
value={formData.nama_belakang}
onChange={(e) =>
setFormData({ ...formData, nama_belakang: e.target.value })
}
/>
<p className="text-sm text-[#C3423F] mt-1">*not required</p>
<p className="text-sm text-[#C3423F] mt-1">*required</p>
</div>
<div>
@@ -179,6 +186,32 @@ const RegisterInternships = () => {
}
/>
</div>
<div>
<label className="block mb-1 font-bold">Universitas</label>
<input
type="text"
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
placeholder="Telkom University Bandung"
value={formData.universitas}
onChange={(e) =>
setFormData({ ...formData, universitas: e.target.value })
}
/>
</div>
<div>
<label className="block mb-1 font-bold">Negara</label>
<input
type="text"
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
placeholder="Indonesia"
value={formData.negara}
onChange={(e) =>
setFormData({ ...formData, negara: e.target.value })
}
/>
</div>
<div>
<label className="block mb-1 font-bold">Jurusan</label>
@@ -207,6 +240,19 @@ const RegisterInternships = () => {
<p className="text-sm text-[#C3423F] mt-1">*separate with commas</p>
</div>
<div>
<label className="block mb-1 font-bold">Batch</label>
<input
type="text"
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
placeholder="1"
value={formData.batch}
onChange={(e) =>
setFormData({ ...formData, batch: e.target.value })
}
/>
</div>
<div>
<label className="block mb-1 font-bold">
CV (Curriculum Vitae)