first commit
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
import Navbar from "../Layout/Navbar";
|
||||
import bghero1 from "../assets/hero2about.png";
|
||||
import bghero2 from "../assets/hero1about.png";
|
||||
import bghero3 from "../assets/hero3about.png";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Footer from "../Layout/Footer";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const missions = [
|
||||
"Becoming the science and technology excellent center in the field of embedded sensor systems to support biomedical applications based on the Internet of Things (IoT).",
|
||||
"Becoming the science and technology excellent center on development remote health monitoring systems based on Internet of Things (IoT).",
|
||||
"Becoming the science and technology excellent center on Big Data Analytic.",
|
||||
"Becoming the science and technology excellent center on health development of Information and Communication Technology (ICT).",
|
||||
];
|
||||
|
||||
const AboutUs = () => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 8;
|
||||
|
||||
const [partners, setPartners] = useState<Partner[]>([]);
|
||||
const [hasPartner, setHasPartner] = useState(true);
|
||||
const [researchProducts, setResearchProducts] = useState<ResearchProduct[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
type Partner = {
|
||||
id: number;
|
||||
nama_partner: string;
|
||||
image_path: string;
|
||||
};
|
||||
type ResearchProduct = {
|
||||
id: number;
|
||||
nama_project: string;
|
||||
image_path: string;
|
||||
};
|
||||
|
||||
const slides = [
|
||||
{
|
||||
image: bghero1,
|
||||
title: "About Us",
|
||||
description:
|
||||
"Research Center Human Centric Engineering (RC HUMIC), merupakan pusat penelitian Telkom University yang berfokus pada pengembangkan rekayasa teknologi yang berkaitan dengan dukungan aktivitas manusia sehari-hari seperti bidang komputasi, informatika, elektronika, robotik, mekanikal, dan teknik biomedis.",
|
||||
},
|
||||
{
|
||||
image: bghero2,
|
||||
title: "Visi",
|
||||
description:
|
||||
"To become an excellent research center in the field of engineering to improve the human health and prosperity",
|
||||
},
|
||||
{
|
||||
image: bghero3,
|
||||
title: "Misi",
|
||||
description:
|
||||
"1. Becoming the science and technology excellent center in the field of embedded sensor systems to support biomedical applications based on the Internet of Things (IoT). 2. Becoming the science and technology excellent center on development remote health monitoring systems based on Internet of Things (IoT).",
|
||||
},
|
||||
];
|
||||
|
||||
const prevSlide = () => {
|
||||
setCurrentIndex((prev) => (prev === 0 ? slides.length - 1 : prev - 1));
|
||||
};
|
||||
|
||||
const nextSlide = () => {
|
||||
setCurrentIndex((prev) => (prev === slides.length - 1 ? 0 : prev + 1));
|
||||
};
|
||||
|
||||
const currentSlide = slides[currentIndex];
|
||||
|
||||
const indexOfLastItem = currentPage * itemsPerPage;
|
||||
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||
const currentProducts = researchProducts.slice(
|
||||
indexOfFirstItem,
|
||||
indexOfLastItem
|
||||
);
|
||||
const totalPages = Math.ceil(researchProducts.length / itemsPerPage);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${import.meta.env.VITE_API_BASE_URL}/hasil-research-api/get`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setResearchProducts(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Gagal mengambil data hasil research:", error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${import.meta.env.VITE_API_BASE_URL}/partnership-api/get`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.message === "No partnership data") {
|
||||
setHasPartner(false);
|
||||
} else {
|
||||
setPartners(data.data);
|
||||
setHasPartner(true);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to fetch partnership data:", error);
|
||||
setHasPartner(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="body-of-aboutus bg-[#F8F9FA] min-h-screen">
|
||||
{/* Navbar Section */}
|
||||
<div className="navbar-section py-5">
|
||||
<Navbar />
|
||||
</div>
|
||||
|
||||
{/* Hero Section 1 */}
|
||||
<div
|
||||
className="hero-section relative flex flex-col items-start justify-end
|
||||
h-[400px] sm:h-[500px] md:h-[600px] lg:h-[650px]
|
||||
mx-5 sm:mx-[25px] mt-5 rounded-2xl overflow-hidden p-6 sm:p-10 md:p-14 text-white transition-all duration-700 ease-in-out"
|
||||
style={{
|
||||
backgroundImage: `url(${currentSlide.image})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
>
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-black to-transparent opacity-90 z-0"></div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 w-full max-w-full">
|
||||
<div className="container-of-content flex flex-col md:flex-row items-start md:items-end md:justify-between gap-6 md:gap-0">
|
||||
<div className="right-section max-w-full md:max-w-[670px]">
|
||||
<h2 className="font-bold text-2xl sm:text-3xl md:text-4xl">
|
||||
{currentSlide.title}
|
||||
</h2>
|
||||
{/* Mission Control */}
|
||||
{currentSlide.title === "Misi" ? (
|
||||
<ol className="list-decimal pl-5 space-y-2 font-normal text-sm sm:text-base md:text-lg mt-3 text-white">
|
||||
{missions.map((mission, index) => (
|
||||
<li key={index}>{mission}</li>
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
<p className="font-normal text-sm sm:text-base md:text-lg mt-3 text-white">
|
||||
{currentSlide.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Arrow Controls */}
|
||||
<div className="left-section flex flex-row items-center gap-6">
|
||||
<button
|
||||
onClick={prevSlide}
|
||||
className="icon-1-right bg-white rounded-full p-2 sm:p-3 md:p-4 hover:bg-gray-200 transition cursor-pointer"
|
||||
aria-label="Previous Slide"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-6 h-6 sm:w-8 sm:h-8 text-black"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15.75 19.5 8.25 12l7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={nextSlide}
|
||||
className="icon-1-right bg-white rounded-full p-2 sm:p-3 md:p-4 hover:bg-gray-200 transition cursor-pointer"
|
||||
aria-label="Next Slide"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-6 h-6 sm:w-8 sm:h-8 text-black"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m8.25 4.5 7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero Section 2 Partnership */}
|
||||
{hasPartner && (
|
||||
<div className="hero-2-partnership bg-[#FAF0EF] flex flex-col md:flex-row items-center justify-between mt-12 px-5 sm:px-20 py-16">
|
||||
<div className="caption-hero-2 flex flex-col items-start w-full md:w-[540px] mb-8 md:mb-0">
|
||||
<h2 className="font-bold text-2xl sm:text-3xl md:text-4xl">
|
||||
Partnership
|
||||
</h2>
|
||||
<h2 className="font-normal text-sm sm:text-base mt-2">
|
||||
Kami bersama-sama menciptakan solusi yang memberdayakan bisnis dan
|
||||
meningkatkan efisiensi operasional.
|
||||
</h2>
|
||||
</div>
|
||||
<div className="image-hero-2 flex flex-row flex-wrap justify-start md:justify-center items-center gap-6 md:gap-12 max-w-full md:max-w-[700px]">
|
||||
{partners.map((partner) => (
|
||||
<img
|
||||
key={partner.id}
|
||||
src={`${import.meta.env.VITE_API_BASE_URL}${
|
||||
partner.image_path
|
||||
}`}
|
||||
alt={`Partnership ${partner.nama_partner}`}
|
||||
className="w-16 sm:w-20 md:w-24"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hero section 3 */}
|
||||
<div className="hero-3-research-product mt-12 px-5 sm:px-20">
|
||||
<div className="title-container flex flex-row justify-between items-center mb-6">
|
||||
<h2 className="font-bold text-2xl sm:text-3xl md:text-4xl">
|
||||
Hasil Produk Research
|
||||
</h2>
|
||||
<div className="arrow-container flex flex-row items-center gap-6">
|
||||
<button
|
||||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="left bg-[#C3423F] p-2 sm:p-3 rounded-full hover:bg-red-700 transition disabled:opacity-50 cursor-pointer"
|
||||
aria-label="Previous research product"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-5 h-5 sm:w-6 sm:h-6 text-white"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15.75 19.5 8.25 12l7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||
}
|
||||
disabled={currentPage === totalPages}
|
||||
className="right bg-[#C3423F] p-2 sm:p-3 rounded-full hover:bg-red-700 transition disabled:opacity-50 cursor-pointer"
|
||||
aria-label="Next research product"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-5 h-5 sm:w-6 sm:h-6 text-white"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m8.25 4.5 7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero section 3 content */}
|
||||
<div className="hero-3-section-content px-5 sm:px-20 pb-10">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{currentProducts.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-white p-4 rounded-lg shadow-md overflow-hidden flex flex-col cursor-pointer hover:shadow-lg transition"
|
||||
onClick={() => navigate(`/details-product/${item.id}`)}
|
||||
>
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_BASE_URL}${item.image_path}`}
|
||||
alt={item.nama_project}
|
||||
className="w-full h-48 object-cover rounded-lg mb-4"
|
||||
/>
|
||||
<div className="text-start">
|
||||
<p className="font-semibold text-base sm:text-lg line-clamp-2">
|
||||
{item.nama_project}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutUs;
|
||||
@@ -0,0 +1,141 @@
|
||||
import NavbarAdmin from "../Layout/NavbarAdmin";
|
||||
import SidebarAdmin from "../Layout/SidebarAdmin";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
|
||||
const AddPartnershipAdmin = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [namaPartner, setNamaPartner] = useState("");
|
||||
const [thumbnail, setThumbnail] = useState<File | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const handleImageChange = (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const validTypes = ["image/jpeg", "image/png", "image/webp"];
|
||||
if (!validTypes.includes(file.type)) {
|
||||
setError("Format file tidak didukung.");
|
||||
return;
|
||||
}
|
||||
setThumbnail(file);
|
||||
setPreviewUrl(URL.createObjectURL(file));
|
||||
setError("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e?: React.FormEvent | React.MouseEvent) => {
|
||||
e?.preventDefault();
|
||||
|
||||
if (!namaPartner || !thumbnail) {
|
||||
setError("Nama dan thumbnail harus diisi.");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("nama_partner", namaPartner);
|
||||
formData.append("image", thumbnail);
|
||||
|
||||
// Ambil token dari cookie seperti yang kamu minta
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
if (!token) {
|
||||
setError("Token tidak ditemukan. Silakan login kembali.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/partnership-api/add`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
alert("Partnership berhasil ditambahkan!");
|
||||
navigate("/partnership-admin");
|
||||
} else {
|
||||
setError(data.message || "Gagal menambahkan partnership.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError("Terjadi kesalahan saat mengirim data.");
|
||||
}
|
||||
};
|
||||
|
||||
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="header-button flex flex-row justify-between items-center mb-[45px]">
|
||||
<h2 className="text-[24px] font-semibold">Tambah Partnership</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
className="bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white px-6 py-2 rounded-md font-medium"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-8 w-full">
|
||||
<div className="flex flex-row gap-16">
|
||||
<div className="flex flex-col items-start gap-[15px]">
|
||||
<h2 className="text-[16px] font-bold">Nama</h2>
|
||||
<input
|
||||
type="text"
|
||||
value={namaPartner}
|
||||
onChange={(e) => setNamaPartner(e.target.value)}
|
||||
placeholder="Nama Partnership"
|
||||
className="border-2 px-[15px] py-[10px] border-[#A9B5C0] rounded-xl w-[600px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-[15px]">
|
||||
<h2 className="text-[16px] font-bold">Thumbnail</h2>
|
||||
<input
|
||||
type="file"
|
||||
accept=".jpg,.png,.webp"
|
||||
onChange={handleImageChange}
|
||||
className="border-2 px-[15px] py-[10px] border-[#A9B5C0] rounded-xl w-[600px]"
|
||||
/>
|
||||
<p className="text-[#C3423F] text-[12px]">
|
||||
*file harus berformat .jpg .png .webp dengan resolusi maksimal
|
||||
1024 x 500
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-[16px] font-bold mb-2">Preview</h2>
|
||||
{previewUrl && (
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt="Preview"
|
||||
className="w-[800px] h-auto border rounded-xl"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPartnershipAdmin;
|
||||
@@ -0,0 +1,263 @@
|
||||
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 AddProduct = () => {
|
||||
const [posisi, setPosisi] = useState("");
|
||||
const [kelompok, setKelompok] = useState("");
|
||||
const [kelompokOptions, setKelompokOptions] = useState<string[]>([]);
|
||||
const [lokasi, setLokasi] = useState("");
|
||||
const [paid, setPaid] = useState("paid");
|
||||
const [durasiAwal, setDurasiAwal] = useState("");
|
||||
const [durasiAkhir, setDurasiAkhir] = useState("");
|
||||
const [jobdesk, setJobdesk] = useState("");
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
const [isCustomKelompok, setIsCustomKelompok] = useState(false);
|
||||
const [customKelompok, setCustomKelompok] = useState("");
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
axios
|
||||
.get(
|
||||
`${
|
||||
import.meta.env.VITE_API_BASE_URL
|
||||
}/lowongan-magang-api/get/kelompok-all`
|
||||
)
|
||||
.then((res) => {
|
||||
const data = res.data.data;
|
||||
setKelompokOptions(data);
|
||||
if (data.length > 0) {
|
||||
setKelompok(data[0]);
|
||||
setIsCustomKelompok(false);
|
||||
} else {
|
||||
setIsCustomKelompok(true);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Gagal memuat kelompok peminatan:", err);
|
||||
setIsCustomKelompok(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
if (!token) {
|
||||
alert("Token tidak ditemukan.");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("posisi", posisi);
|
||||
formData.append(
|
||||
"kelompok_peminatan",
|
||||
isCustomKelompok || kelompokOptions.length === 0
|
||||
? customKelompok
|
||||
: kelompok
|
||||
);
|
||||
formData.append("lokasi", lokasi);
|
||||
formData.append("jobdesk", jobdesk);
|
||||
formData.append("kualifikasi", getRandomKualifikasi());
|
||||
formData.append("benefit", getRandomBenefit());
|
||||
formData.append("durasi_awal", durasiAwal);
|
||||
formData.append("durasi_akhir", durasiAkhir);
|
||||
formData.append("paid", paid);
|
||||
if (image) formData.append("image", image);
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/lowongan-magang-api/add`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
}
|
||||
);
|
||||
alert("Internship berhasil ditambahkan!");
|
||||
navigate("/lowongan-list");
|
||||
} catch (error) {
|
||||
console.error("Error adding internship:", error);
|
||||
alert("Gagal menambahkan internship.");
|
||||
}
|
||||
};
|
||||
|
||||
const getRandomKualifikasi = () => {
|
||||
const list = [
|
||||
"Menguasai tools desain",
|
||||
"Bisa bekerja dalam tim",
|
||||
"Memiliki laptop pribadi",
|
||||
"Menguasai dasar HTML & CSS",
|
||||
];
|
||||
return list[Math.floor(Math.random() * list.length)];
|
||||
};
|
||||
|
||||
const getRandomBenefit = () => {
|
||||
const list = [
|
||||
"Sertifikat",
|
||||
"Uang transport",
|
||||
"Mentoring langsung",
|
||||
"Kesempatan kerja tetap",
|
||||
];
|
||||
return list[Math.floor(Math.random() * list.length)];
|
||||
};
|
||||
|
||||
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 Lowongan</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">
|
||||
{/* Internship Fields Only */}
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">
|
||||
Kategori Internship
|
||||
</label>
|
||||
|
||||
{/* Jika ada data dari API */}
|
||||
{kelompokOptions.length > 0 && (
|
||||
<select
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
value={kelompok}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setKelompok(value);
|
||||
setIsCustomKelompok(value === "lainnya");
|
||||
}}
|
||||
>
|
||||
{kelompokOptions.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
<option value="lainnya">Lainnya (masukan manual)</option>
|
||||
</select>
|
||||
)}
|
||||
|
||||
{(isCustomKelompok || kelompokOptions.length === 0) && (
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded-lg p-3 mt-2"
|
||||
placeholder="Tulis kategori internship baru"
|
||||
value={customKelompok}
|
||||
onChange={(e) => setCustomKelompok(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Paid</label>
|
||||
<select
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
value={paid}
|
||||
onChange={(e) => setPaid(e.target.value)}
|
||||
>
|
||||
<option value="paid">Paid</option>
|
||||
<option value="unpaid">Unpaid</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Lokasi</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
value={lokasi}
|
||||
onChange={(e) => setLokasi(e.target.value)}
|
||||
placeholder="Onsite,Jakarta"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Nama Internship</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">
|
||||
Tanggal Mulai Oprec
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
value={durasiAwal}
|
||||
onChange={(e) => setDurasiAwal(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">
|
||||
Tanggal Akhir Oprec
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
value={durasiAkhir}
|
||||
onChange={(e) => setDurasiAkhir(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 col-span-2">
|
||||
<label className="text-sm font-medium">Thumbnail / Image</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={jobdesk}
|
||||
onModelChange={(model: any) => setJobdesk(model)}
|
||||
config={{
|
||||
placeholderText: "Tulis deskripsi lowongan...",
|
||||
charCounterCount: true,
|
||||
toolbarSticky: true,
|
||||
heightMin: 200,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddProduct;
|
||||
@@ -0,0 +1,130 @@
|
||||
import { useState } from "react";
|
||||
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";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const AddProductHumic = () => {
|
||||
const [namaProject, setNamaProject] = useState("");
|
||||
const [deskripsi, setDeskripsi] = useState("");
|
||||
const [linkProject, setLinkProject] = useState("");
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("nama_project", namaProject);
|
||||
formData.append("deskripsi", deskripsi);
|
||||
formData.append("link_project", linkProject);
|
||||
if (image) formData.append("image", image);
|
||||
|
||||
try {
|
||||
await axios.post(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/hasil-research-api/add`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
}
|
||||
);
|
||||
alert("Project berhasil ditambahkan!");
|
||||
navigate("/product-list");
|
||||
} catch (error) {
|
||||
console.error("Gagal menambahkan project:", error);
|
||||
alert("Gagal menambahkan project.");
|
||||
}
|
||||
};
|
||||
|
||||
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 Research Project</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 col-span-2">
|
||||
<label className="text-sm font-medium">Nama Project</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
value={namaProject}
|
||||
onChange={(e) => setNamaProject(e.target.value)}
|
||||
placeholder="Masukan Nama Project"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 col-span-2">
|
||||
<label className="text-sm font-medium">Link Project</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
value={linkProject}
|
||||
onChange={(e) => setLinkProject(e.target.value)}
|
||||
placeholder="Masukan Link Disini"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 col-span-2">
|
||||
<label className="text-sm font-medium">Upload Gambar</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>
|
||||
|
||||
{/* Deskripsi Project */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Deskripsi Project</label>
|
||||
<FroalaEditor
|
||||
tag="textarea"
|
||||
model={deskripsi}
|
||||
onModelChange={(model: any) => setDeskripsi(model)}
|
||||
config={{
|
||||
placeholderText: "Tulis deskripsi project di sini...",
|
||||
charCounterCount: true,
|
||||
toolbarSticky: true,
|
||||
heightMin: 200,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddProductHumic;
|
||||
@@ -0,0 +1,92 @@
|
||||
import Footer from "../Layout/Footer";
|
||||
import Navbar from "../Layout/Navbar";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
type ResearchItem = {
|
||||
id: number;
|
||||
image_path: string;
|
||||
nama_project: string;
|
||||
deskripsi: string;
|
||||
link_project: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
const DetailsProduct = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [product, setProduct] = useState<ResearchItem | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
|
||||
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]);
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Gagal fetch data:", err);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [id]);
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center mt-10">Loading...</div>;
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<div className="text-center mt-10 text-red-500">Data tidak ditemukan</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="body-details-of-product bg-[#F8F9FA] min-h-screen">
|
||||
{/* Navbar Section */}
|
||||
<div className="navbar-section py-[20px]">
|
||||
<Navbar />
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="title-of-content flex justify-center mt-[20px] mx-4 md:mx-[20px] mb-[30px] text-center">
|
||||
<h2 className="text-[24px] md:text-[32px] font-bold">
|
||||
{product.nama_project}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="content-container flex flex-col gap-[60px] mx-4 md:mx-[100px]">
|
||||
{/* Image */}
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_BASE_URL}${product.image_path}`}
|
||||
alt={product.nama_project}
|
||||
className="w-full max-h-[500px] object-cover rounded-xl"
|
||||
/>
|
||||
|
||||
{/* Deskripsi */}
|
||||
<div
|
||||
className="paragraf-section prose max-w-none text-[15px] md:text-[16px]"
|
||||
dangerouslySetInnerHTML={{ __html: product.deskripsi }}
|
||||
/>
|
||||
|
||||
{product.link_project && (
|
||||
<a
|
||||
href={product.link_project}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-6 inline-block text-blue-600 underline hover:text-blue-800"
|
||||
>
|
||||
Lihat Project ↗
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailsProduct;
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import SidebarAdmin from "../Layout/SidebarAdmin";
|
||||
import NavbarAdmin from "../Layout/NavbarAdmin";
|
||||
|
||||
const EditKontenAdmin = () => {
|
||||
// const { id } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const tipeQuery = searchParams.get("tipe") || "Internship";
|
||||
const [tipeKonten, setTipeKonten] = useState(tipeQuery);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full overflow-hidden">
|
||||
{/* Navbar */}
|
||||
<NavbarAdmin />
|
||||
|
||||
{/* Sidebar and Content */}
|
||||
<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">Edit Konten</h2>
|
||||
<button className="bg-[#1F4A92] text-white px-6 py-2 rounded-md font-medium">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form className="flex flex-col gap-6 max-w-full">
|
||||
{/* Select Tipe Konten */}
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Tipe Konten</label>
|
||||
<select
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
value={tipeKonten}
|
||||
onChange={(e) => setTipeKonten(e.target.value)}
|
||||
>
|
||||
<option value="Internship">Internship</option>
|
||||
<option value="Product">Product</option>
|
||||
<option value="News">News</option>
|
||||
<option value="Event">Event</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Internship Fields */}
|
||||
{tipeKonten === "Internship" && (
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Kategori</label>
|
||||
<select className="border border-gray-300 rounded-lg p-3">
|
||||
<option value="Design">Design</option>
|
||||
<option value="Programming">Programming</option>
|
||||
<option value="Marketing">Marketing</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Paid / Unpaid</label>
|
||||
<select className="border border-gray-300 rounded-lg p-3">
|
||||
<option value="Paid">Paid</option>
|
||||
<option value="Unpaid">Unpaid</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Lokasi</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
placeholder="Contoh: Bandung"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Nama Internship</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
placeholder="Contoh: UI/UX Designer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Product Fields */}
|
||||
{tipeKonten === "Product" && (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Judul Produk</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
placeholder="Nama produk"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">
|
||||
Upload Thumbnail
|
||||
</label>
|
||||
<input type="file" accept=".jpg,.png,.webm" />
|
||||
<p className="text-red-500 text-xs">
|
||||
* Maksimal ukuran 1024x500 (jpg, png, webm)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* News Fields */}
|
||||
{tipeKonten === "News" && (
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Judul Berita</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
placeholder="Judul berita"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Event Fields */}
|
||||
{tipeKonten === "Event" && (
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Nama Event</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
placeholder="Contoh: Tech Webinar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Tanggal Event</label>
|
||||
<input
|
||||
type="date"
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Common Content Field */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Isi Konten</label>
|
||||
<textarea
|
||||
className="border border-gray-300 rounded-lg p-3 min-h-[150px]"
|
||||
placeholder="Tuliskan isi konten..."
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditKontenAdmin;
|
||||
@@ -0,0 +1,213 @@
|
||||
import NavbarAdmin from "../Layout/NavbarAdmin";
|
||||
import SidebarAdmin from "../Layout/SidebarAdmin";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const EditPartnerships = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
|
||||
const [namaPartner, setNamaPartner] = useState("");
|
||||
const [thumbnail, setThumbnail] = useState<File | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPartnershipData = async () => {
|
||||
if (!id) {
|
||||
setError("ID tidak ditemukan");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/partnership-api/get/${id}`
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
console.log("GET result:", result.data);
|
||||
|
||||
if (response.ok) {
|
||||
setNamaPartner(result.data[0].nama_partner);
|
||||
setPreviewUrl(
|
||||
`${import.meta.env.VITE_API_BASE_URL}` + result.data[0].image_path
|
||||
);
|
||||
} else {
|
||||
setError(result.message || "Gagal memuat data partnership.");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Terjadi kesalahan saat mengambil data.");
|
||||
}
|
||||
};
|
||||
|
||||
fetchPartnershipData();
|
||||
}, [id]);
|
||||
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
const validTypes = ["image/jpeg", "image/png", "image/webp"];
|
||||
if (!validTypes.includes(file.type)) {
|
||||
setError("Format file tidak didukung.");
|
||||
return;
|
||||
}
|
||||
setThumbnail(file);
|
||||
setPreviewUrl(URL.createObjectURL(file));
|
||||
setError("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e?: React.FormEvent | React.MouseEvent) => {
|
||||
e?.preventDefault();
|
||||
|
||||
if (!namaPartner) {
|
||||
setError("Nama partnership harus diisi.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!token || !id) {
|
||||
setError("Token atau ID tidak ditemukan.");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("nama_partner", namaPartner);
|
||||
if (thumbnail) formData.append("image", thumbnail);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/partnership-api/update/${id}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
alert("Partnership berhasil diperbarui!");
|
||||
navigate("/partnership-admin");
|
||||
} else {
|
||||
setError(result.message || "Gagal memperbarui partnership.");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Terjadi kesalahan saat memperbarui data.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!token || !id) {
|
||||
setError("Token atau ID tidak ditemukan.");
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmDelete = confirm(
|
||||
"Apakah kamu yakin ingin menghapus partnership ini?"
|
||||
);
|
||||
if (!confirmDelete) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/partnership-api/delete/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
alert("Partnership berhasil dihapus!");
|
||||
navigate("/partnership-admin");
|
||||
} else {
|
||||
setError(result.message || "Gagal menghapus partnership.");
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Terjadi kesalahan saat menghapus data.");
|
||||
}
|
||||
};
|
||||
|
||||
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="header-button flex justify-between items-center mb-[45px]">
|
||||
<h2 className="text-[24px] font-semibold">Edit Partnership</h2>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
className="bg-[#4CAF50] hover:bg-[#89c58c] cursor-pointer text-white px-6 py-3 rounded-xl w-[200px] font-semibold"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className="bg-[#E53935] hover:bg-[#9e5857] cursor-pointer text-white px-6 py-3 rounded-xl w-[200px] font-semibold"
|
||||
>
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-8 w-full">
|
||||
<div className="flex flex-row gap-16">
|
||||
<div className="flex flex-col items-start gap-[15px]">
|
||||
<h2 className="text-[16px] font-bold">Nama</h2>
|
||||
<input
|
||||
type="text"
|
||||
value={namaPartner}
|
||||
onChange={(e) => setNamaPartner(e.target.value)}
|
||||
placeholder="Nama Partnership"
|
||||
className="border-2 px-[15px] py-[10px] border-[#A9B5C0] rounded-xl w-[600px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-[15px]">
|
||||
<h2 className="text-[16px] font-bold">Thumbnail</h2>
|
||||
<input
|
||||
type="file"
|
||||
accept=".jpg,.png,.webp"
|
||||
onChange={handleImageChange}
|
||||
className="border-2 px-[15px] py-[10px] border-[#A9B5C0] rounded-xl w-[600px]"
|
||||
/>
|
||||
<p className="text-[#C3423F] text-[12px]">
|
||||
*file harus berformat .jpg .png .webp dengan resolusi maksimal
|
||||
1024 x 500
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-[16px] font-bold mb-2">Preview</h2>
|
||||
{previewUrl && (
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt="Preview"
|
||||
className="w-[800px] h-auto border rounded-xl"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditPartnerships;
|
||||
@@ -0,0 +1,165 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } 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 EditProductHumic = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [namaProject, setNamaProject] = useState("");
|
||||
const [deskripsi, setDeskripsi] = useState("");
|
||||
const [linkProject, setLinkProject] = useState("");
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/hasil-research-api/get/${id}`
|
||||
);
|
||||
|
||||
const data = response.data.data;
|
||||
console.log("Response data:", data);
|
||||
|
||||
const project = Array.isArray(data) ? data[0] : data;
|
||||
|
||||
if (project) {
|
||||
setNamaProject(project.nama_project || "");
|
||||
setDeskripsi(project.deskripsi || "");
|
||||
setLinkProject(project.link_project || "");
|
||||
} else {
|
||||
alert("Data project tidak ditemukan.");
|
||||
console.warn("Data kosong:", data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal mengambil data:", error);
|
||||
alert("Gagal mengambil data project.");
|
||||
}
|
||||
};
|
||||
|
||||
if (id) {
|
||||
console.log("ID dari URL:", id);
|
||||
fetchData();
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
if (!token) {
|
||||
alert("Token tidak ditemukan.");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("nama_project", namaProject);
|
||||
formData.append("deskripsi", deskripsi);
|
||||
formData.append("link_project", linkProject);
|
||||
if (image) formData.append("image", image);
|
||||
|
||||
try {
|
||||
await axios.patch(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/hasil-research-api/update/${id}`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
alert("Project berhasil diupdate!");
|
||||
navigate("/product-list");
|
||||
} catch (error) {
|
||||
console.error("Gagal mengupdate project:", error);
|
||||
alert("Gagal mengupdate project.");
|
||||
}
|
||||
};
|
||||
|
||||
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">Edit Research Project</h2>
|
||||
<button
|
||||
className="bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white px-6 py-2 rounded-md font-medium"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Simpan Perubahan
|
||||
</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 col-span-2">
|
||||
<label className="text-sm font-medium">Nama Project</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
value={namaProject}
|
||||
onChange={(e) => setNamaProject(e.target.value)}
|
||||
placeholder="Contoh: Sistem Rekomendasi Tanaman"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 col-span-2">
|
||||
<label className="text-sm font-medium">Link Project</label>
|
||||
<input
|
||||
type="text"
|
||||
className="border border-gray-300 rounded-lg p-3"
|
||||
value={linkProject}
|
||||
onChange={(e) => setLinkProject(e.target.value)}
|
||||
placeholder="https://github.com/..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 col-span-2">
|
||||
<label className="text-sm font-medium">
|
||||
Upload Gambar (Opsional)
|
||||
</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>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">Deskripsi Project</label>
|
||||
<FroalaEditor
|
||||
tag="textarea"
|
||||
model={deskripsi}
|
||||
onModelChange={(model: any) => setDeskripsi(model)}
|
||||
config={{
|
||||
placeholderText: "Tulis deskripsi penelitian di sini...",
|
||||
charCounterCount: true,
|
||||
toolbarSticky: true,
|
||||
heightMin: 200,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditProductHumic;
|
||||
@@ -0,0 +1,143 @@
|
||||
import Footer from "../Layout/Footer";
|
||||
import Navbar from "../Layout/Navbar";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
const InternshipDetails = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const [intern, setIntern] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
axios
|
||||
.get(`${import.meta.env.VITE_API_BASE_URL}/lowongan-magang-api/get`)
|
||||
.then((res) => {
|
||||
const selected = res.data.data.find((item: any) => item.id === id);
|
||||
setIntern(selected);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to fetch internship detail:", err);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [id]);
|
||||
|
||||
if (loading) return <div className="p-10 text-center">Loading...</div>;
|
||||
if (!intern)
|
||||
return <div className="p-10 text-center">Internship not found.</div>;
|
||||
|
||||
return (
|
||||
<div className="bg-[#F8F9FA] min-h-screen flex flex-col">
|
||||
{/* Navbar Section */}
|
||||
<div className="py-5">
|
||||
<Navbar />
|
||||
</div>
|
||||
|
||||
{/* Content Section */}
|
||||
<div className="flex-grow px-4 md:px-10 lg:px-[170px] w-full mt-10">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
|
||||
<div className="flex flex-col items-start">
|
||||
<h2 className="font-bold text-[28px] md:text-[32px]">
|
||||
{intern.posisi}
|
||||
</h2>
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center font-bold gap-4 md:gap-8 mt-5 text-[#292D32]">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Lokasi */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-6 h-6 md:w-7 md:h-7"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<h2 className="text-[16px] md:text-[18px]">{intern.lokasi}</h2>
|
||||
</div>
|
||||
|
||||
{/* Kelompok Peminatan */}
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-6 h-6 md:w-7 md:h-7"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 0 0 .75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 0 0-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0 1 12 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 0 1-.673-.38m0 0A2.18 2.18 0 0 1 3 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 0 1 3.413-.387m7.5 0V5.25A2.25 2.25 0 0 0 13.5 3h-3a2.25 2.25 0 0 0-2.25 2.25v.894m7.5 0a48.667 48.667 0 0 0-7.5 0M12 12.75h.008v.008H12v-.008Z"
|
||||
/>
|
||||
</svg>
|
||||
<h2 className="text-[16px] md:text-[18px]">
|
||||
{intern.kelompok_peminatan}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Paid / Unpaid */}
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-6 h-6 md:w-7 md:h-7"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.25 18.75a60.07 60.07 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 0 1 3 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 0 0-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 0 1-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 0 0 3 15h-.75M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm3 0h.008v.008H18V10.5Zm-12 0h.008v.008H6V10.5Z"
|
||||
/>
|
||||
</svg>
|
||||
<h2 className="text-[16px] md:text-[18px]">
|
||||
{intern.paid === "paid" ? "Paid" : "Unpaid"}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Apply Button */}
|
||||
<div className="self-start md:self-center">
|
||||
<button
|
||||
className="bg-[#C3423F] hover:bg-[#b36462] cursor-pointer text-white text-sm md:text-[14px] font-semibold px-6 py-2 md:px-9 md:py-3 rounded-2xl shadow-xl"
|
||||
onClick={() => navigate(`/register-intern/${intern.id}`)}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Job Description Only */}
|
||||
<div className="mt-10 text-[#5F5F5F]">
|
||||
<div className="mb-6">
|
||||
<div
|
||||
className="text-[16px] font-medium"
|
||||
dangerouslySetInnerHTML={{ __html: intern.jobdesk }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InternshipDetails;
|
||||
@@ -0,0 +1,175 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import NavbarAdmin from "../Layout/NavbarAdmin";
|
||||
import SidebarAdmin from "../Layout/SidebarAdmin";
|
||||
import axios from "axios";
|
||||
|
||||
const InternshipDetailsAdmin = () => {
|
||||
const { id } = useParams(); // id lamaran magang
|
||||
const [applicant, setApplicant] = useState<any>(null);
|
||||
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
useEffect(() => {
|
||||
axios
|
||||
.get(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/lamaran-magang-api/get/${id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
setApplicant(res.data.data[0]);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Gagal fetch detail lamaran", err);
|
||||
});
|
||||
}, [id]);
|
||||
|
||||
const handleStatusUpdate = (status: "diterima" | "ditolak") => {
|
||||
axios
|
||||
.patch(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/lamaran-magang-api/update/${id}`,
|
||||
{ status },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
alert(`Status berhasil diubah menjadi ${status}`);
|
||||
setApplicant((prev: any) => ({ ...prev, status }));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Gagal update status", err);
|
||||
});
|
||||
};
|
||||
|
||||
if (!applicant) return <p className="p-10">Loading...</p>;
|
||||
|
||||
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="container-header flex flex-row items-center justify-between mb-10">
|
||||
<h2 className="text-[24px] font-semibold">Detail Pelamar</h2>
|
||||
{/* Status Indikator */}
|
||||
<div className="mt-10">
|
||||
<span
|
||||
className={`px-4 py-2 rounded-lg text-white font-semibold ${
|
||||
applicant.status === "diterima"
|
||||
? "bg-green-500"
|
||||
: applicant.status === "ditolak"
|
||||
? "bg-red-500"
|
||||
: "bg-gray-500"
|
||||
}`}
|
||||
>
|
||||
{applicant.status.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-start mb-10">
|
||||
<div>
|
||||
<h2 className="text-[24px] font-semibold">
|
||||
{applicant.nama_depan} {applicant.nama_belakang}
|
||||
</h2>
|
||||
<p className="text-[#747474]">{applicant.email}</p>
|
||||
<p className="text-[#747474]">{applicant.kontak}</p>
|
||||
</div>
|
||||
|
||||
{/* Status Button */}
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => handleStatusUpdate("diterima")}
|
||||
className="bg-[#3BB525] hover:bg-green-600 cursor-pointer text-white px-3 py-2 rounded-xl"
|
||||
>
|
||||
Terima
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStatusUpdate("ditolak")}
|
||||
className="bg-[#C3423F] hover:bg-red-600 cursor-pointer text-white px-3 py-2 rounded-xl"
|
||||
>
|
||||
Tolak
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detail */}
|
||||
<div className="flex gap-[200px] mb-12">
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>
|
||||
<span className="font-semibold">Jurusan:</span>{" "}
|
||||
{applicant.jurusan}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Posisi yang dilamar:</span>{" "}
|
||||
{applicant.posisi}
|
||||
</p>
|
||||
<div className="flex gap-2 items-center">
|
||||
<h2 className="font-semibold">Skill:</h2>
|
||||
{applicant.relevant_skills
|
||||
.split(",")
|
||||
.map((skill: string, idx: number) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="text-[#9F9F9F] font-semibold px-3 py-1 border border-[#9F9F9F] rounded-3xl"
|
||||
>
|
||||
{skill.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CV & Portofolio */}
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="font-semibold">CV:</h2>
|
||||
<a
|
||||
href={`${import.meta.env.VITE_API_BASE_URL}${
|
||||
applicant.cv_path
|
||||
}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-[#D78108] text-white px-4 py-2 rounded-xl"
|
||||
>
|
||||
Lihat CV
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="font-semibold">Portofolio:</h2>
|
||||
<a
|
||||
href={`${import.meta.env.VITE_API_BASE_URL}${
|
||||
applicant.portofolio_path
|
||||
}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-[#995010] text-white px-4 py-2 rounded-xl"
|
||||
>
|
||||
Lihat Portofolio
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Motivasi */}
|
||||
<div>
|
||||
<h2 className="font-semibold mb-3">Motivasi</h2>
|
||||
<p className="leading-7">{applicant.motivasi}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InternshipDetailsAdmin;
|
||||
@@ -0,0 +1,265 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import SidebarAdmin from "../Layout/SidebarAdmin";
|
||||
import NavbarAdmin from "../Layout/NavbarAdmin";
|
||||
import axios from "axios";
|
||||
|
||||
type Application = {
|
||||
id: number;
|
||||
nama_depan: string;
|
||||
posisi: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
const InternshipList = () => {
|
||||
const [applications, setApplications] = useState<Application[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const itemsPerPage = 10;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchApplications = () => {
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
axios
|
||||
.get(`${import.meta.env.VITE_API_BASE_URL}/lamaran-magang-api/get`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
setApplications(res.data.data || []);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Gagal mengambil data lamaran:", err);
|
||||
});
|
||||
};
|
||||
|
||||
fetchApplications();
|
||||
|
||||
const interval = setInterval(fetchApplications, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const getStatusStyle = (status: string) => {
|
||||
switch (status) {
|
||||
case "diterima":
|
||||
return "bg-green-500 text-white";
|
||||
case "ditolak":
|
||||
return "bg-red-500 text-white";
|
||||
case "diproses":
|
||||
default:
|
||||
return "bg-yellow-400 text-black";
|
||||
}
|
||||
};
|
||||
|
||||
// Pagination Logic
|
||||
const indexOfLastItem = currentPage * itemsPerPage;
|
||||
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||
|
||||
const filteredApplications = applications.filter(
|
||||
(app) =>
|
||||
app.nama_depan.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
app.posisi.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const currentItems = filteredApplications.slice(
|
||||
indexOfFirstItem,
|
||||
indexOfLastItem
|
||||
);
|
||||
const totalPages = Math.ceil(filteredApplications.length / itemsPerPage);
|
||||
|
||||
const handleExportExcel = async (): Promise<boolean> => {
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/lamaran-magang-api/export`,
|
||||
{
|
||||
responseType: "blob",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const blob = new Blob([response.data], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", "data.xlsx");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Gagal mengekspor data:", error);
|
||||
alert("Gagal mengekspor data. Silakan coba lagi.");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateBatch = async () => {
|
||||
const exportSuccess = await handleExportExcel();
|
||||
|
||||
if (!exportSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmDelete = window.confirm(
|
||||
"Apakah Anda yakin ingin menghapus seluruh data pelamar setelah melakukan ekspor?"
|
||||
);
|
||||
|
||||
if (!confirmDelete) return;
|
||||
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
try {
|
||||
await axios.delete(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/lamaran-magang-api/delete`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setApplications([]);
|
||||
alert("Data pelamar berhasil dihapus setelah diekspor.");
|
||||
} catch (error) {
|
||||
console.error("Gagal menghapus data pelamar:", error);
|
||||
alert("Gagal menghapus data pelamar. Silakan coba lagi.");
|
||||
}
|
||||
};
|
||||
|
||||
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-6">
|
||||
<h2 className="font-semibold text-[24px]">Daftar Pelamar</h2>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari Nama, Posisi"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="px-4 py-2 rounded-2xl w-[20rem] border border-slate-400"
|
||||
/>
|
||||
<button
|
||||
onClick={handleUpdateBatch}
|
||||
className="bg-[#923120] hover:bg-[#8d5050] cursor-pointer text-white px-4 py-2 rounded-lg flex flex-row items-center gap-2"
|
||||
>
|
||||
Update Batch{" "}
|
||||
<span>
|
||||
<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="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportExcel}
|
||||
className="bg-[#227014] hover:bg-green-700 cursor-pointer text-white px-4 py-2 rounded-lg"
|
||||
>
|
||||
Ekspor ke Excel
|
||||
</button>
|
||||
</div>
|
||||
</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">Posisi</th>
|
||||
<th className="px-6 py-3">Status</th>
|
||||
<th className="px-6 py-3">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentItems.map((app, idx) => (
|
||||
<tr key={idx} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-3">{app.nama_depan}</td>
|
||||
<td className="px-6 py-3">{app.posisi}</td>
|
||||
<td className="px-6 py-3">
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full font-semibold text-sm ${getStatusStyle(
|
||||
app.status
|
||||
)}`}
|
||||
>
|
||||
{app.status.charAt(0).toUpperCase() +
|
||||
app.status.slice(1)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-3">
|
||||
<button
|
||||
onClick={() =>
|
||||
navigate(`/InternshipsDetailsAdmin/${app.id}`)
|
||||
}
|
||||
className="bg-blue-500 hover:bg-blue-600 cursor-pointer text-white px-4 py-2 rounded-lg"
|
||||
>
|
||||
Lihat Detail
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{applications.length === 0 && (
|
||||
<p className="text-center text-gray-500 mt-5">Tidak ada data.</p>
|
||||
)}
|
||||
|
||||
{/* Pagination Controls */}
|
||||
<div className="flex justify-center mt-6 gap-2">
|
||||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
className={`px-4 py-2 rounded-lg border ${
|
||||
currentPage === i + 1
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-white text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InternshipList;
|
||||
@@ -0,0 +1,282 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import axios from "axios";
|
||||
import bghero2 from "../assets/hero1about.png";
|
||||
import Navbar from "../Layout/Navbar";
|
||||
import Footer from "../Layout/Footer";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
const Internships = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const initialSearch = searchParams.get("search") || "";
|
||||
const [searchTerm, setSearchTerm] = useState(initialSearch);
|
||||
|
||||
type Internship = {
|
||||
id: string;
|
||||
title: string;
|
||||
location: string;
|
||||
category: string;
|
||||
status: string;
|
||||
statusColor: string;
|
||||
isPaid: boolean;
|
||||
};
|
||||
|
||||
const [internships, setInternships] = useState<Internship[]>([]);
|
||||
const [categories, setCategories] = useState<string[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
axios
|
||||
.get(
|
||||
`${
|
||||
import.meta.env.VITE_API_BASE_URL
|
||||
}/lowongan-magang-api/get/kelompok-all`
|
||||
)
|
||||
.then((res) => {
|
||||
setCategories(res.data.data || []);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Gagal mengambil kategori:", err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchInternships = () => {
|
||||
const url = selectedCategory
|
||||
? `${
|
||||
import.meta.env.VITE_API_BASE_URL
|
||||
}/lowongan-magang-api/get/kelompok/${selectedCategory}`
|
||||
: `${import.meta.env.VITE_API_BASE_URL}/lowongan-magang-api/get`;
|
||||
|
||||
axios
|
||||
.get(url)
|
||||
.then((response) => {
|
||||
const result = response.data.data.map((item: any) => ({
|
||||
id: item.id,
|
||||
title: item.posisi,
|
||||
location: item.lokasi,
|
||||
category: item.kelompok_peminatan,
|
||||
status: item.status_lowongan === "dibuka" ? "Opened" : "Closed",
|
||||
statusColor:
|
||||
item.status_lowongan === "dibuka" ? "#4EAD00" : "#C3423F",
|
||||
isPaid: item.paid === "paid",
|
||||
}));
|
||||
setInternships(result);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to fetch internships:", error);
|
||||
});
|
||||
};
|
||||
|
||||
fetchInternships();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchInternships();
|
||||
}, 15000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [selectedCategory]);
|
||||
|
||||
const filteredInternships = internships.filter(
|
||||
(intern) =>
|
||||
intern.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
intern.location.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const param = searchParams.get("search") || "";
|
||||
setSearchTerm(param);
|
||||
}, [searchParams]);
|
||||
return (
|
||||
<div className="interships-container-body bg-[#F8F9FA] min-h-screen">
|
||||
{/* Hero Section */}
|
||||
<div
|
||||
className="relative bg-cover bg-center h-[90vh] text-white"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(to top, rgba(0, 0, 0, 0.9), rgba(0,0,0,0)), url(${bghero2})`,
|
||||
}}
|
||||
>
|
||||
<div className="absolute py-[20px] top-0 left-0 w-full z-10 text-black">
|
||||
<Navbar />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center h-full px-4 pt-[90px] md:pt-[300px] text-center">
|
||||
<div>
|
||||
<h1 className="text-[32px] md:text-5xl font-bold mb-4">
|
||||
Cari internship
|
||||
</h1>
|
||||
<p className="mb-6 max-w-[700px] mx-auto text-[20px] md:text-base">
|
||||
Berikut merupakan daftar internship yang sedang dibuka. Harap
|
||||
untuk memperhatikan syarat dan ketentuan untuk setiap posisi yang
|
||||
tersedia.
|
||||
</p>
|
||||
|
||||
{/* Search Box */}
|
||||
<div className="search-section bg-white p-[15px] rounded-xl shadow-md w-full mt-[40px] text-black">
|
||||
<form className="flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari program magang atau pelatihan..."
|
||||
className="flex-1 outline-none text-[16px] px-3 py-2"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<svg
|
||||
className="w-7 h-7 text-red-700 cursor-not-allowed"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21 21l-4.35-4.35M17 11A6 6 0 1 0 5 11a6 6 0 0 0 12 0z"
|
||||
/>
|
||||
</svg>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Filter Buttons */}
|
||||
<div className="flex flex-wrap mt-[20px] gap-4 justify-center">
|
||||
{categories.map((category, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() =>
|
||||
setSelectedCategory(
|
||||
selectedCategory === category ? "" : category
|
||||
)
|
||||
}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-semibold border-2 transition ${
|
||||
selectedCategory === category
|
||||
? "bg-[#C3423F] text-white border-[#C3423F]"
|
||||
: "bg-[#FAF0EF] text-[#C3423F] border-[#C3423F] hover:bg-[#C3423F] hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Internship List */}
|
||||
<div className="hero-of-list-internships flex flex-col px-[10px] lg:px-[300px] pt-[50px] gap-[30px] pb-[50px]">
|
||||
{filteredInternships.length === 0 ? (
|
||||
<p className="text-center text-gray-500">Tidak ada data.</p>
|
||||
) : (
|
||||
filteredInternships.map((intern, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`card-1 flex flex-col items-start p-5 lg:p-7 rounded-xl shadow-2xl ${
|
||||
intern.status === "Opened"
|
||||
? "bg-white cursor-pointer hover:shadow-xl transition"
|
||||
: "bg-gray-200 cursor-not-allowed opacity-60"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (intern.status === "Opened") {
|
||||
navigate(`/details/${intern.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="head-indicator flex flex-row justify-between items-center w-full">
|
||||
<h2 className="font-bold text-[18px] lg:text-[24px]">
|
||||
{intern.title}
|
||||
</h2>
|
||||
<h2
|
||||
className="p-2 border-2 text-[12px] lg:text-[14px] font-bold rounded-4xl"
|
||||
style={{
|
||||
backgroundColor:
|
||||
intern.status === "Opened" ? "#F6FFF4" : "#FAF0EF",
|
||||
color: intern.statusColor,
|
||||
borderColor: intern.statusColor,
|
||||
}}
|
||||
>
|
||||
{intern.status}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="caption-content mt-4 flex flex-row items-center gap-4 text-slate-400">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Lokasi */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-5 lg:size-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z"
|
||||
/>{" "}
|
||||
</svg>
|
||||
<h2 className="text-[14px] lg:text-[16px]">
|
||||
{intern.location}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Kategori */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-5 lg:size-6"
|
||||
>
|
||||
{" "}
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 0 0 .75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 0 0-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0 1 12 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 0 1-.673-.38m0 0A2.18 2.18 0 0 1 3 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 0 1 3.413-.387m7.5 0V5.25A2.25 2.25 0 0 0 13.5 3h-3a2.25 2.25 0 0 0-2.25 2.25v.894m7.5 0a48.667 48.667 0 0 0-7.5 0M12 12.75h.008v.008H12v-.008Z"
|
||||
/>{" "}
|
||||
</svg>
|
||||
<h2 className="text-[14px] lg:text-[16px]">
|
||||
{intern.category}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Paid */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-5 lg:size-6"
|
||||
>
|
||||
{" "}
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.25 18.75a60.07 60.07 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 0 1 3 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 0 0-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 0 1-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 0 0 3 15h-.75M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm3 0h.008v.008H18V10.5Zm-12 0h.008v.008H6V10.5Z"
|
||||
/>{" "}
|
||||
</svg>
|
||||
<h2 className="text-[14px] lg:text-[16px]">
|
||||
{intern.isPaid ? "Paid" : "Unpaid"}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Internships;
|
||||
@@ -0,0 +1,227 @@
|
||||
import Navbar from "../Layout/Navbar";
|
||||
import team from "../assets/teamwokr.jpg";
|
||||
import person1 from "../assets/reihan.jpeg";
|
||||
import person2 from "../assets/yohanes.jpeg";
|
||||
import person3 from "../assets/shafa.jpeg";
|
||||
import person4 from "../assets/reinhard.jpeg";
|
||||
import bghero1 from "../assets/hero1.png";
|
||||
import Footer from "../Layout/Footer";
|
||||
import Faq from "../Layout/Faq";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const Landing = () => {
|
||||
const [search, setSearch] = useState("");
|
||||
const navigate = useNavigate();
|
||||
type Internship = {
|
||||
id: string;
|
||||
posisi: string;
|
||||
image_path: string;
|
||||
status_lowongan: string;
|
||||
};
|
||||
|
||||
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (search.trim()) {
|
||||
navigate(`/internships?search=${encodeURIComponent(search)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const [internships, setInternships] = useState<Internship[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${import.meta.env.VITE_API_BASE_URL}/lowongan-magang-api/get`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setInternships(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching internships:", error);
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<div className="body-landing bg-[#F8F9FA] min-h-screen">
|
||||
<div className="navbar-section py-[20px]">
|
||||
<Navbar />
|
||||
</div>
|
||||
|
||||
{/* Hero Section */}
|
||||
<div
|
||||
className="hero-section relative flex flex-col items-start justify-center h-[350px] mx-5 sm:mx-8 lg:mx-[20px] mt-[20px] rounded-2xl overflow-hidden p-5 sm:p-[30px] text-white"
|
||||
style={{
|
||||
backgroundImage: `url(${bghero1})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black opacity-40 z-0"></div>
|
||||
|
||||
<div className="relative z-10 w-full">
|
||||
<h2 className="font-bold text-3xl sm:text-4xl lg:text-[40px] text-white">
|
||||
Tingkatkan keahlian, perluas kesempatan
|
||||
</h2>
|
||||
<h2 className="font-semibold text-base sm:text-lg lg:text-[20px] text-white mt-2">
|
||||
Asah keterampilanmu dan jadilah mahasiswa yang siap bersaing di
|
||||
dunia industri
|
||||
</h2>
|
||||
|
||||
<div className="search-section bg-white p-[15px] rounded-xl shadow-md w-full max-w-lg mt-[40px] text-black">
|
||||
<form onSubmit={handleSearch} className="flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Cari program magang atau pelatihan..."
|
||||
className="flex-1 outline-none text-[16px] px-3 py-2"
|
||||
/>
|
||||
<svg
|
||||
className="w-7 h-7 text-red-700 cursor-pointer"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
onClick={() => {
|
||||
if (search.trim())
|
||||
navigate(
|
||||
`/internships?search=${encodeURIComponent(search)}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21 21l-4.35-4.35M17 11A6 6 0 1 0 5 11a6 6 0 0 0 12 0z"
|
||||
/>
|
||||
</svg>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero 2 Section */}
|
||||
<div className="hero2-section flex flex-col lg:flex-row items-center mx-5 sm:mx-[20px] mt-[50px] rounded-2xl justify-between overflow-hidden relative">
|
||||
<div className="text-section w-full lg:w-[700px] z-10 px-5 sm:px-[30px] py-6 sm:py-[40px]">
|
||||
<h2 className="font-bold text-2xl sm:text-[32px] mb-4">
|
||||
Life at Humic Engineering
|
||||
</h2>
|
||||
<p className="font-medium text-base sm:text-[16px]">
|
||||
Raih pengalaman internship yang bernilai dengan berkontribusi
|
||||
langsung dalam pengembangan solusi teknologi kami. Kembangkan
|
||||
kompetensi Anda melalui keterlibatan nyata dalam proyek IT serta
|
||||
bimbingan langsung dari para expert di bidang Software engineering
|
||||
dan IoT.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="image-section w-full lg:w-[70rem] h-[300px] lg:h-full relative mt-5 lg:mt-0">
|
||||
<img src={team} alt="Team" className="w-full h-full object-cover" />
|
||||
<div className="absolute left-0 top-0 w-full h-full lg:bg-gradient-to-r from-[#F8F9FA] via-white/60 to-transparent "></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero 3 Section */}
|
||||
<div className="hero-3-section flex flex-col lg:flex-row items-center justify-between mx-5 lg:mx-[200px] mt-[60px] gap-10">
|
||||
<div className="relative w-full max-w-[550px] h-[600px]">
|
||||
<img
|
||||
src={person1}
|
||||
alt=""
|
||||
className="absolute top-1/2 left-1/2 w-60 h-80 object-cover rounded-lg shadow-lg transform -translate-x-1/2 -translate-y-1/2 z-10"
|
||||
/>
|
||||
<img
|
||||
src={person4}
|
||||
alt=""
|
||||
className="absolute top-6 right-6 w-32 h-44 object-cover rounded-lg shadow-md z-20"
|
||||
/>
|
||||
<img
|
||||
src={person3}
|
||||
alt=""
|
||||
className="absolute top-0 left-0 w-28 h-40 object-cover rounded-lg shadow-md z-30"
|
||||
/>
|
||||
<img
|
||||
src={person2}
|
||||
alt=""
|
||||
className="absolute bottom-4 left-6 w-32 h-44 object-cover rounded-lg shadow-md z-40"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="content-section flex flex-col items-start w-full lg:w-[650px]">
|
||||
<h2 className="font-bold text-2xl sm:text-[32px]">
|
||||
Kata mereka tentang Humic Engineering
|
||||
</h2>
|
||||
<p className="font-medium text-base sm:text-[16px] mt-[16px]">
|
||||
Humic engineering menjadi tempat yang menyenangkan bagi saya, karena
|
||||
dapat meningkatkan kemampuan saya dalam melakukan desain UI/UX.
|
||||
Banyak pembelajaran yang berharga serta memiliki lingkungan yang
|
||||
suportif
|
||||
</p>
|
||||
<p className="text-[16px] text-[#C3423F] font-bold mt-[20px]">
|
||||
Reinhard, Humic Internship Batch 3 2025
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero 4 Section */}
|
||||
<div className="hero4-section flex flex-col justify-center items-center mt-[50px]">
|
||||
<h2 className="font-bold text-2xl sm:text-[32px] text-center">
|
||||
Jelajahi posisi yang kamu inginkan
|
||||
</h2>
|
||||
|
||||
<div className="card-container mt-[110px] flex flex-wrap items-center justify-center gap-10 sm:gap-[60px] xl:gap-[80px] mx-3 sm:mx-[10px]">
|
||||
{internships.map((intern) => {
|
||||
const isClosed = intern.status_lowongan === "ditutup";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={intern.id}
|
||||
className={`relative w-64 h-64 rounded-xl overflow-hidden shadow-lg transform transition duration-300 ${
|
||||
isClosed
|
||||
? "cursor-not-allowed grayscale"
|
||||
: "cursor-pointer hover:scale-105"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!isClosed) {
|
||||
navigate(`/details/${intern.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Background image */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage: `url(${import.meta.env.VITE_API_BASE_URL}${
|
||||
intern.image_path
|
||||
})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Gradient overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-black opacity-60" />
|
||||
|
||||
{/* Text content */}
|
||||
<div
|
||||
className={`relative z-10 flex flex-col justify-end h-full p-4 ${
|
||||
isClosed ? "text-gray-400" : "text-white"
|
||||
}`}
|
||||
>
|
||||
<div className="font-semibold text-[20px] leading-[30px]">
|
||||
{intern.posisi}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero 5 Section FaQ */}
|
||||
<Faq />
|
||||
|
||||
{/* Footer */}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Landing;
|
||||
@@ -0,0 +1,206 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import SidebarAdmin from "../Layout/SidebarAdmin";
|
||||
import NavbarAdmin from "../Layout/NavbarAdmin";
|
||||
import axios from "axios";
|
||||
|
||||
type Lowongan = {
|
||||
id: string;
|
||||
posisi: string;
|
||||
kelompok_peminatan: string;
|
||||
lokasi: string;
|
||||
};
|
||||
|
||||
const ListLowongan = () => {
|
||||
const [lowongans, setLowongans] = useState<Lowongan[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 10;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
fetchLowongan();
|
||||
}, []);
|
||||
|
||||
const fetchLowongan = () => {
|
||||
axios
|
||||
.get(`${import.meta.env.VITE_API_BASE_URL}/lowongan-magang-api/get`)
|
||||
.then((res) => {
|
||||
setLowongans(res.data.data || []);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Gagal mengambil data lowongan:", err);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
const confirmed = window.confirm("Yakin ingin menghapus lowongan ini?");
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
if (!token) {
|
||||
alert("Token tidak ditemukan. Silakan login ulang.");
|
||||
return;
|
||||
}
|
||||
|
||||
await axios.delete(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/lowongan-magang-api/delete/${id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
alert("Lowongan berhasil dihapus.");
|
||||
fetchLowongan();
|
||||
} catch (err) {
|
||||
console.error("Gagal menghapus:", err);
|
||||
alert("Gagal menghapus lowongan pastikan tidak ada data pelamar yang sedang melamar.");
|
||||
}
|
||||
};
|
||||
|
||||
const indexOfLastItem = currentPage * itemsPerPage;
|
||||
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||
|
||||
const filteredLowongans = lowongans.filter(
|
||||
(low) =>
|
||||
low.posisi.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
low.kelompok_peminatan.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
low.lokasi.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const currentItems = filteredLowongans.slice(
|
||||
indexOfFirstItem,
|
||||
indexOfLastItem
|
||||
);
|
||||
const totalPages = Math.ceil(filteredLowongans.length / itemsPerPage);
|
||||
|
||||
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-6">
|
||||
<h2 className="font-semibold text-[24px]">Daftar Lowongan</h2>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari Posisi, Kelompok, Lokasi"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="px-4 py-2 rounded-2xl w-[20rem] border border-slate-400"
|
||||
/>
|
||||
<button
|
||||
onClick={() => navigate("/add-product-admin")}
|
||||
className="bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white font-semibold px-4 py-2 rounded-lg"
|
||||
>
|
||||
Tambah
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr className="text-left">
|
||||
<th className="px-6 py-3">Posisi</th>
|
||||
<th className="px-6 py-3">Kelompok Peminatan</th>
|
||||
<th className="px-6 py-3">Lokasi</th>
|
||||
<th className="px-6 py-3">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentItems.map((low) => (
|
||||
<tr key={low.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-3">{low.posisi}</td>
|
||||
<td className="px-6 py-3">{low.kelompok_peminatan}</td>
|
||||
<td className="px-6 py-3">{low.lokasi}</td>
|
||||
<td className="px-6 py-3 flex gap-2">
|
||||
{/* Tombol View */}
|
||||
<button
|
||||
className="bg-[#3BE21D] text-white p-2 rounded-xl cursor-pointer"
|
||||
onClick={() =>
|
||||
window.open(`/details/${low.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 Hapus */}
|
||||
<button
|
||||
className="bg-[#E41E1E] text-white p-2 rounded-xl cursor-pointer"
|
||||
onClick={() => handleDelete(low.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>
|
||||
|
||||
{lowongans.length === 0 && (
|
||||
<p className="text-center text-gray-500 mt-5">Tidak ada data.</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-center mt-6 gap-2">
|
||||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
className={`px-4 py-2 rounded-lg border ${
|
||||
currentPage === i + 1
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-white text-gray-700"
|
||||
}`}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListLowongan;
|
||||
@@ -0,0 +1,202 @@
|
||||
import NavbarAdmin from "../Layout/NavbarAdmin";
|
||||
import SidebarAdmin from "../Layout/SidebarAdmin";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
type Project = {
|
||||
id: number;
|
||||
nama_project: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
const ListProduct = () => {
|
||||
const navigate = useNavigate();
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/hasil-research-api/get`
|
||||
);
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
setProjects(result.data);
|
||||
} else {
|
||||
console.error("Gagal mengambil data project:", result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Terjadi kesalahan:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProjects();
|
||||
}, []);
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
const confirmDelete = confirm(
|
||||
"Apakah Anda yakin ingin menghapus project 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}/hasil-research-api/delete/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
alert("Project berhasil dihapus!");
|
||||
setProjects((prev) => prev.filter((project) => project.id !== id));
|
||||
} else {
|
||||
alert(result.message || "Gagal menghapus project.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal menghapus:", error);
|
||||
alert("Terjadi kesalahan saat menghapus project.");
|
||||
}
|
||||
};
|
||||
|
||||
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">Daftar Project</h2>
|
||||
<button
|
||||
className="px-[20px] py-[10px] rounded-xl bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white font-semibold"
|
||||
onClick={() => navigate("/add-product-humic")}
|
||||
>
|
||||
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 Project</th>
|
||||
<th className="px-6 py-3">Tanggal Unggah</th>
|
||||
<th className="px-6 py-3">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{projects.map((project) => (
|
||||
<tr key={project.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-3">{project.nama_project}</td>
|
||||
<td className="px-6 py-3">
|
||||
{new Date(project.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/${project.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/${project.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(project.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>
|
||||
|
||||
{projects.length === 0 && (
|
||||
<p className="text-gray-500 mt-4">Belum ada data project.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListProduct;
|
||||
@@ -0,0 +1,172 @@
|
||||
import logoblack from "../assets/logo.png";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
const LoginAdmin = () => {
|
||||
const navigate = useNavigate();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/auth-api/login`,
|
||||
{ email, password },
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
document.cookie = `token=${response.data.data.token}; path=/;`;
|
||||
navigate("/internships-list");
|
||||
} else {
|
||||
setErrorMsg(response.data.message || "Login failed");
|
||||
}
|
||||
} catch (error: any) {
|
||||
setErrorMsg(
|
||||
error?.response?.data?.message || "Something went wrong"
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="body-container min-h-screen bg-[#F7FAFC]">
|
||||
<div className="container-of-card flex flex-col min-h-screen gap-9 justify-center items-center">
|
||||
<div className="title-logo flex flex-col items-center">
|
||||
<img src={logoblack} className="w-[250px] mb-4" alt="Logo" />
|
||||
</div>
|
||||
|
||||
<div className="form-input bg-white rounded-3xl flex flex-col items-center gap-8 p-[30px] w-[30rem] shadow-2xl">
|
||||
{/* Email */}
|
||||
<div className="username-section flex flex-col w-full gap-2">
|
||||
<div className="title-of-username flex flex-row items-center gap-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-6 text-[#2A4365]"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z"
|
||||
/>
|
||||
</svg>
|
||||
<h2 className="text-[20px] text-[#2A4365]">Email</h2>
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
className="w-full p-4 bg-[#EDF2F7] rounded-xl"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div className="username-section flex flex-col w-full gap-2">
|
||||
<div className="title-of-username flex flex-row items-center gap-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-6 text-[#2A4365]"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<h2 className="text-[20px] text-[#2A4365]">Password</h2>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
className="w-full p-4 bg-[#EDF2F7] rounded-xl"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{errorMsg && (
|
||||
<p className="text-red-600 text-sm font-medium">{errorMsg}</p>
|
||||
)}
|
||||
|
||||
{/* Sign In Button */}
|
||||
<button
|
||||
className="bg-[#2A4365] hover:bg-[#7f9bc3] cursor-pointer text-[20px] text-white w-full p-3 rounded-xl font-medium flex justify-center items-center gap-2"
|
||||
onClick={handleLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<svg
|
||||
className="animate-spin h-5 w-5 text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8v8H4z"
|
||||
></path>
|
||||
</svg>
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
"Sign In"
|
||||
)}
|
||||
</button>
|
||||
|
||||
<h2 className="font-semibold flex flex-row items-center gap-2 text-red-700">
|
||||
{" "}
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-6 text-red-700"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Halaman Khusus Login Petugas HUMIC
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginAdmin;
|
||||
@@ -0,0 +1,120 @@
|
||||
import Navbar from "../Layout/Navbar";
|
||||
import faried from "../assets/faried.jpeg";
|
||||
import reihan from "../assets/reihan.jpeg";
|
||||
import yohan from "../assets/yohanes.jpeg";
|
||||
import shafa from "../assets/shafa.jpeg";
|
||||
import delkano from "../assets/delkano.jpeg";
|
||||
import reinhard from "../assets/reinhard.jpeg";
|
||||
import kakraya from "../assets/kakraya.png";
|
||||
import Footer from "../Layout/Footer";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const OurDeveloper = () => {
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, []);
|
||||
return (
|
||||
<div className="body-of-developer min-h-screen bg-[#F8F9FA] ">
|
||||
{/* Navbar Section */}
|
||||
<div className="nav-section py-[20px]">
|
||||
<Navbar />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="body-of-content flex flex-col px-6 md:px-10 lg:px-16 py-5">
|
||||
<h2 className="text-[32px] font-bold mb-8 text-start">
|
||||
Our Developers
|
||||
</h2>
|
||||
<div className="flex flex-wrap gap-8 justify-center items-center">
|
||||
{[
|
||||
{
|
||||
name: "Taufik",
|
||||
role: "Project Manager",
|
||||
img: kakraya,
|
||||
link: "https://www.linkedin.com/in/rayataufik/",
|
||||
},
|
||||
{
|
||||
name: "Reinhard",
|
||||
role: "Web Designer",
|
||||
img: reinhard,
|
||||
link: "https://www.linkedin.com/in/reinhard-situmeang/",
|
||||
},
|
||||
{
|
||||
name: "Shafa",
|
||||
role: "Mobile Designer",
|
||||
img: shafa,
|
||||
link: "https://www.linkedin.com/in/shafasalmapermana/",
|
||||
},
|
||||
{
|
||||
name: "Faried",
|
||||
role: "Frontend Developer",
|
||||
img: faried,
|
||||
link: "https://www.linkedin.com/in/faried-gunawan/",
|
||||
},
|
||||
{
|
||||
name: "Delkano",
|
||||
role: "Mobile Developer",
|
||||
img: delkano,
|
||||
link: "https://www.linkedin.com/in/delkano-berutu-28b7482a2/",
|
||||
},
|
||||
{
|
||||
name: "Yohanes",
|
||||
role: "Backend Developer",
|
||||
img: yohan,
|
||||
link: "https://www.linkedin.com/in/yohanesjanuarico/",
|
||||
},
|
||||
{
|
||||
name: "Reihan",
|
||||
role: "Backend Developer",
|
||||
img: reihan,
|
||||
link: "https://www.linkedin.com/in/reihanramadhana/",
|
||||
},
|
||||
].map((dev, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white shadow-lg rounded-xl overflow-hidden w-full max-w-[260px] flex flex-col items-center text-center"
|
||||
>
|
||||
<img
|
||||
src={dev.img}
|
||||
alt={dev.name}
|
||||
className="h-60 p-3 rounded-3xl w-full object-cover"
|
||||
/>
|
||||
<div className="p-4">
|
||||
<h3 className="text-[20px] font-semibold">{dev.name}</h3>
|
||||
<p className="text-sm text-gray-500">{dev.role}</p>
|
||||
<a
|
||||
href={dev.link || "#"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-3 flex items-center justify-center gap-2 bg-[#FAF0EF] font-semibold text-[#C3423F] rounded-lg px-4 py-2 hover:bg-[#f2e3e1] transition"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622
|
||||
1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
|
||||
/>
|
||||
</svg>
|
||||
LinkedIn
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Section */}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OurDeveloper;
|
||||
@@ -0,0 +1,193 @@
|
||||
import NavbarAdmin from "../Layout/NavbarAdmin";
|
||||
import SidebarAdmin from "../Layout/SidebarAdmin";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const PartnershipsList = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [partnerships, setPartnerships] = useState<
|
||||
{ id: number; nama_partner: string; image_path: string }[]
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPartnerships = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/partnership-api/get`
|
||||
);
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
setPartnerships(result.data);
|
||||
} else {
|
||||
console.error("Gagal mengambil data partnership:", result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Terjadi kesalahan:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPartnerships();
|
||||
}, []);
|
||||
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
const confirmDelete = confirm(
|
||||
"Apakah Anda yakin ingin menghapus partnership ini?"
|
||||
);
|
||||
if (!confirmDelete) return;
|
||||
|
||||
if (!token) {
|
||||
alert("Token tidak ditemukan. Silakan login ulang.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/partnership-api/delete/${id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
if (response.ok) {
|
||||
alert("Partnership berhasil dihapus!");
|
||||
setPartnerships((prev) => prev.filter((item) => item.id !== id));
|
||||
} else {
|
||||
alert(result.message || "Gagal menghapus partnership.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Terjadi kesalahan saat menghapus data.");
|
||||
console.error("Error delete:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full overflow-hidden">
|
||||
<NavbarAdmin />
|
||||
|
||||
<div className="flex flex-row">
|
||||
<SidebarAdmin />
|
||||
<div className="p-10 bg-white w-full flex-col flex-wrap">
|
||||
<div className="title-container flex flex-row justify-between items-center mb-[50px]">
|
||||
<h2 className="text-[24px] font-semibold">Partnership</h2>
|
||||
|
||||
<div className="right-section flex flex-row items-center gap-3">
|
||||
<button
|
||||
className="px-[20px] py-[10px] rounded-xl bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white font-semibold"
|
||||
onClick={() => navigate("/add-partnership")}
|
||||
>
|
||||
Tambah
|
||||
</button>
|
||||
</div>
|
||||
</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">Icon</th>
|
||||
|
||||
<th className="px-6 py-3">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{partnerships.map((partner) => {
|
||||
const fileName = partner.image_path.split("/").pop();
|
||||
return (
|
||||
<tr key={partner.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-3">{partner.nama_partner}</td>
|
||||
<td className="px-6 py-3">{fileName}</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("/about-us")}
|
||||
>
|
||||
<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] cursor-pointer text-white p-2 rounded-xl"
|
||||
onClick={() =>
|
||||
navigate(`/edit-partnership/${partner.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.931Zm0 0L19.5 7.125M18 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] cursor-pointer text-white p-2 rounded-xl"
|
||||
onClick={() => handleDelete(partner.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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PartnershipsList;
|
||||
@@ -0,0 +1,31 @@
|
||||
import Footer from "../Layout/Footer";
|
||||
import Navbar from "../Layout/Navbar";
|
||||
import doneimg from "../assets/done.png";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const RegisterDone = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div className="body-of-done min-h-screen bg-[#F8F9FA] flex flex-col">
|
||||
{/* Navbar Section */}
|
||||
<div className="nav-section py-5">
|
||||
<Navbar />
|
||||
</div>
|
||||
|
||||
{/* Content Container */}
|
||||
<div className="container-of-content flex-grow flex flex-col justify-center items-center px-4 md:px-0 text-center">
|
||||
<img src={doneimg} alt="Success" className="w-4/5 max-w-[150px] md:max-w-[170px]" />
|
||||
<h2 className="font-semibold text-[20px] md:text-[24px] mt-8">Lamaran diterima</h2>
|
||||
<p className="mt-3 text-[14px] md:text-[16px] font-normal max-w-[600px]">Kami akan segera memeriksa lamaran kamu. Cek email secara berkala untuk informasi selanjutnya.</p>
|
||||
<button className="mt-6 text-white font-bold bg-[#C3423F] hover:bg-[#975553] cursor-pointer px-8 md:px-10 py-3 md:py-4 rounded-2xl shadow-2xl text-sm md:text-base" onClick={() => navigate("/")}>
|
||||
Kembali ke Beranda
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterDone;
|
||||
@@ -0,0 +1,306 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import ReCAPTCHA from "react-google-recaptcha";
|
||||
import axios from "axios";
|
||||
import Footer from "../Layout/Footer";
|
||||
import Navbar from "../Layout/Navbar";
|
||||
|
||||
const RegisterInternships = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const [posisi, setPosisi] = useState<string>("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [recaptchaToken, setRecaptchaToken] = useState<string | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
nama_depan: "",
|
||||
nama_belakang: "",
|
||||
email: "",
|
||||
kontak: "",
|
||||
jurusan: "",
|
||||
motivasi: "",
|
||||
relevant_skills: "",
|
||||
cv: null as File | null,
|
||||
portofolio: null as File | null,
|
||||
});
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.cv || !formData.portofolio) {
|
||||
alert("CV dan Portofolio wajib diunggah!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!recaptchaToken) {
|
||||
alert("Silakan verifikasi reCAPTCHA terlebih dahulu.");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = new FormData();
|
||||
data.append("nama_depan", formData.nama_depan);
|
||||
if (formData.nama_belakang.trim() !== "") {
|
||||
data.append("nama_belakang", formData.nama_belakang);
|
||||
}
|
||||
data.append("email", formData.email);
|
||||
data.append("kontak", formData.kontak);
|
||||
data.append("jurusan", formData.jurusan);
|
||||
data.append("motivasi", formData.motivasi);
|
||||
data.append("relevant_skills", formData.relevant_skills);
|
||||
data.append("g-recaptcha-response", recaptchaToken);
|
||||
if (formData.cv) data.append("cv", formData.cv);
|
||||
if (formData.portofolio) data.append("portofolio", formData.portofolio);
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/lamaran-magang-api/add/${id}`,
|
||||
data,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log("Success:", response.data);
|
||||
navigate("/register-done");
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error(
|
||||
"Error submitting form:",
|
||||
error.response?.data || error.message
|
||||
);
|
||||
alert("Gagal mengirim form. Silakan coba lagi.");
|
||||
} else {
|
||||
console.error("Unexpected error:", error);
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPosisi = async () => {
|
||||
if (!id) {
|
||||
console.warn("⚠️ ID belum tersedia, skip fetchPosisi()");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${
|
||||
import.meta.env.VITE_API_BASE_URL
|
||||
}/lowongan-magang-api/get/id/${id}`
|
||||
);
|
||||
|
||||
console.log("Response posisi:", response.data);
|
||||
|
||||
const posisiValue = response.data?.data?.posisi;
|
||||
if (posisiValue) {
|
||||
setPosisi(posisiValue);
|
||||
} else {
|
||||
console.warn("Tidak ada field posisi di response:", response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal mengambil data posisi:", error);
|
||||
}
|
||||
};
|
||||
fetchPosisi();
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div className="body-of-register min-h-screen bg-[#F8F9FA]">
|
||||
<div className="nav-section py-[20px]">
|
||||
<Navbar />
|
||||
</div>
|
||||
|
||||
<div className="form-input-container flex flex-col text-[#494949] justify-center items-center">
|
||||
<div className="title-of-form">
|
||||
<h2 className="font-bold text-[32px] text-black mt-[40px]">
|
||||
{posisi || "Loading..."}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="w-full max-w-[70rem] px-4 mt-8 grid grid-cols-1 md:grid-cols-2 gap-8"
|
||||
>
|
||||
<div>
|
||||
<label className="block mb-1 font-bold">Nama depan</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
|
||||
placeholder="John"
|
||||
value={formData.nama_depan}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, nama_depan: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 font-bold">Nama belakang</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
|
||||
placeholder="Doe"
|
||||
value={formData.nama_belakang}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, nama_belakang: e.target.value })
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-[#C3423F] mt-1">*not required</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 font-bold">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
|
||||
placeholder="johndoe@email.com"
|
||||
value={formData.email}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, email: e.target.value })
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-[#C3423F] mt-1">*email or SSO</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 font-bold">Kontak</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
|
||||
placeholder="08123456789"
|
||||
value={formData.kontak}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, kontak: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 font-bold">Jurusan</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
|
||||
placeholder="Informatika"
|
||||
value={formData.jurusan}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, jurusan: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 font-bold">Skill</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
|
||||
placeholder="JavaScript,Python,Figma"
|
||||
value={formData.relevant_skills}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, relevant_skills: e.target.value })
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-[#C3423F] mt-1">*separate with commas</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 font-bold">
|
||||
CV (Curriculum Vitae)
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, cv: e.target.files?.[0] || null })
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-[#C3423F] mt-1">
|
||||
*only accept pdf files only
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1 font-bold">Portofolio</label>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 text-black"
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
portofolio: e.target.files?.[0] || null,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-[#C3423F] mt-1">
|
||||
*only accept pdf files only
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block mb-1 font-bold">
|
||||
Motivasi ingin bergabung ke Humic?
|
||||
</label>
|
||||
<textarea
|
||||
className="w-full border-2 border-[#A9B5C0] rounded-md p-2 mt-3 min-h-[120px] text-black"
|
||||
placeholder="Saya ingin belajar lebih banyak mengenai desain antarmuka pengguna..."
|
||||
value={formData.motivasi}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, motivasi: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 flex items-start gap-2">
|
||||
<input type="checkbox" className="mt-1" required />
|
||||
<p className="text-[16px] text-black">
|
||||
Dengan melanjutkan, saya mengonfirmasi bahwa saya telah membaca
|
||||
secara seksama dan menyetujui{" "}
|
||||
<span className="text-[#C3423F] underline">
|
||||
Persyaratan Layanan
|
||||
</span>{" "}
|
||||
dan{" "}
|
||||
<span className="text-[#C3423F] underline">
|
||||
Kebijakan Privasi
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 flex flex-col items-center justify-center gap-10 text-center mt-[40px]">
|
||||
{/* Sisipkan reCAPTCHA */}
|
||||
|
||||
<ReCAPTCHA
|
||||
sitekey={import.meta.env.VITE_RECAPTCHA_SITE_KEY}
|
||||
onChange={(token) => {
|
||||
setRecaptchaToken(token);
|
||||
}}
|
||||
onExpired={() => setRecaptchaToken(null)}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className={`px-9 py-3 rounded-xl cursor-pointer hover:bg-[#975553] font-bold shadow-2xl ${
|
||||
isSubmitting
|
||||
? "bg-gray-400 cursor-not-allowed"
|
||||
: "bg-[#C3423F] text-white"
|
||||
}`}
|
||||
>
|
||||
{isSubmitting ? "Mengirim..." : "Submit"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterInternships;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Outlet, useNavigate } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const PrivateRoute = () => {
|
||||
const navigate = useNavigate();
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
navigate("/humic-20242025-2");
|
||||
}
|
||||
}, [navigate, token]);
|
||||
|
||||
return token ? <Outlet /> : null;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
Reference in New Issue
Block a user