first commit
@@ -0,0 +1 @@
|
||||
VITE_API_BASE_URL=
|
||||
@@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
.env
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,54 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
extends: [
|
||||
// Remove ...tseslint.configs.recommended and replace with this
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default tseslint.config({
|
||||
plugins: {
|
||||
// Add the react-x and react-dom plugins
|
||||
'react-x': reactX,
|
||||
'react-dom': reactDom,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended typescript rules
|
||||
...reactX.configs['recommended-typescript'].rules,
|
||||
...reactDom.configs.recommended.rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/sec2.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Internify | Humic Engineering</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "internify-project",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.6",
|
||||
"@tiptap/react": "^2.23.0",
|
||||
"@tiptap/starter-kit": "^2.23.0",
|
||||
"axios": "^1.10.0",
|
||||
"froala-editor": "^4.5.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-froala-wysiwyg": "^4.5.2",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.6.0",
|
||||
"tailwindcss": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,60 @@
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
import Landing from "./Pages/Landing";
|
||||
import AboutUs from "./Pages/AboutUs";
|
||||
import Internships from "./Pages/Internships";
|
||||
import InternshipDetails from "./Pages/InternshipDetails";
|
||||
import RegisterInternships from "./Pages/RegisterInternships";
|
||||
import RegisterDone from "./Pages/RegisterDone";
|
||||
import OurDeveloper from "./Pages/OurDeveloper";
|
||||
import LoginAdmin from "./Pages/LoginAdmin";
|
||||
import AddProduct from "./Pages/AddProduct";
|
||||
import InternshipList from "./Pages/InternshipList";
|
||||
import InternshipDetailsAdmin from "./Pages/InternshipDetailsAdmin";
|
||||
import EditKontenAdmin from "./Pages/EditKontenAdmin";
|
||||
import PartnershipsList from "./Pages/PartnershipsList";
|
||||
import AddPartnershipAdmin from "./Pages/AddPartnershipAdmin";
|
||||
import DetailsProduct from "./Pages/DetailsProduct";
|
||||
import PrivateRoute from "./Pages/utils/PrivateRoute";
|
||||
import EditPartnerships from "./Pages/EditPartnerships";
|
||||
import ListLowongan from "./Pages/ListLowongan";
|
||||
import ListProduct from "./Pages/ListProduct";
|
||||
import AddProductHumic from "./Pages/AddProductHumic";
|
||||
import EditProductHumic from "./Pages/EditProductHumic";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Landing />} />
|
||||
<Route path="/about-us" element={<AboutUs />} />
|
||||
<Route path="/internships" element={<Internships />} />
|
||||
<Route path="/details/:id" element={<InternshipDetails />} />
|
||||
<Route path="/register-intern/:id" element={<RegisterInternships />} />
|
||||
<Route path="/register-done" element={<RegisterDone />} />
|
||||
<Route path="/our-developer" element={<OurDeveloper />} />
|
||||
<Route path="/details-product/:id" element={<DetailsProduct />} />
|
||||
<Route path="/humic-20242025-2" element={<LoginAdmin />} />
|
||||
{/* Endpoint khusus admin login langsung di inject ajah */}
|
||||
|
||||
<Route element={<PrivateRoute />}>
|
||||
<Route path="/add-product-admin" element={<AddProduct />} />
|
||||
<Route path="/internships-list" element={<InternshipList />} />
|
||||
<Route path="/lowongan-list" element={<ListLowongan />} />
|
||||
<Route path="/product-list" element={<ListProduct />} />
|
||||
<Route path="/add-product-humic" element={<AddProductHumic />} />
|
||||
<Route path="/edit-product-humic/:id" element={<EditProductHumic />} />
|
||||
<Route path="/kontent-aktif/edit/:id" element={<EditKontenAdmin />} />
|
||||
<Route
|
||||
path="/internships-details-admin/:id"
|
||||
element={<InternshipDetailsAdmin />}
|
||||
/>
|
||||
<Route path="/partnership-admin" element={<PartnershipsList />} />
|
||||
<Route path="/add-partnership" element={<AddPartnershipAdmin />} />
|
||||
<Route path="/edit-partnership/:id" element={<EditPartnerships />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const faqData = [
|
||||
{
|
||||
question: "Bagaimana cara magang disini?",
|
||||
answer: "Pilih Internship, Kemudian Lakukan Proses Administrasi",
|
||||
},
|
||||
{
|
||||
question: "Apakah saya bisa melamar lebih dari satu lowongan?",
|
||||
answer: "Tidak Bisa, Hanya Bisa Melamar 1 Lowongan",
|
||||
},
|
||||
{
|
||||
question: "Bagaimana saya tahu jika lamaran saya diterima atau ditolak?",
|
||||
answer: "Sistem Akan Mengirimkan Pesan Diterima/Ditolak Ke Email Anda",
|
||||
},
|
||||
{
|
||||
question: "Berapa lama durasi magang di Humic?",
|
||||
answer: "Cek Instagram Kita Yah, Disana Tertera Durasi Magangnya",
|
||||
},
|
||||
{
|
||||
question: "Apakah data pribadi saya aman?",
|
||||
answer: "Data Pribadi Dipastikan Aman, Sistem Ini Memiliki Admin Dashboard",
|
||||
},
|
||||
];
|
||||
|
||||
const Faq = () => {
|
||||
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
||||
|
||||
const toggleItem = (index: number) => {
|
||||
setOpenIndex(openIndex === index ? null : index);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="hero-5-section flex flex-col lg:flex-row items-start lg:items-center justify-between mt-25 px-6 md:px-16 lg:px-[150px] pb-[60px] gap-10">
|
||||
<div className="content-of-5 w-full lg:w-[610px]">
|
||||
<h2 className="font-bold text-2xl md:text-3xl">Frequently Asked Questions</h2>
|
||||
<p className="font-medium text-base mt-4">
|
||||
Berikut adalah informasi mengenai pertanyaan umum seputar program magang di Humic, termasuk tata cara pendaftaran dan persyaratan yang perlu dipenuhi. Masih ada yang perlu ditanyakan? Silahkan{" "}
|
||||
<span className="text-[#C3423F] font-semibold">DM</span> kami
|
||||
</p>
|
||||
</div>
|
||||
<div className="faq-content-list space-y-6 w-full lg:w-[700px]">
|
||||
{faqData.map((faq, index) => (
|
||||
<div key={index} className="bg-[#FAF0EF] shadow-md rounded-sm">
|
||||
<button onClick={() => toggleItem(index)} className="w-full flex items-center cursor-pointer justify-between p-4 focus:outline-none">
|
||||
<span className="text-[16px] font-semibold text-left text-[#C3423F]">{faq.question}</span>
|
||||
<span className="text-[20px] font-bold text-[#C3423F]">{openIndex === index ? "-" : "+"}</span>
|
||||
</button>
|
||||
<div className={`px-4 pb-4 text-black transition-all duration-300 ${openIndex === index ? "block" : "hidden"}`}>
|
||||
<p>{faq.answer}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Faq;
|
||||
@@ -0,0 +1,79 @@
|
||||
import humiclogo from "../assets/whiteLogo.png";
|
||||
import { FaInstagram, FaLinkedinIn } from "react-icons/fa";
|
||||
import { MdOutlineEmail } from "react-icons/md";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const Footer = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<footer
|
||||
id="footer"
|
||||
className="footer-container flex flex-col px-6 md:px-16 lg:px-[140px] bg-[#263742] mt-[60px] text-white pt-[60px] pb-[20px]"
|
||||
>
|
||||
<div className="head-of-footer flex flex-col lg:flex-row justify-between gap-10">
|
||||
{/* Right Section */}
|
||||
<div className="right-section flex flex-col items-start">
|
||||
<img src={humiclogo} className="w-[160px]" alt="Humic Logo" />
|
||||
<p className="text-[16px] mt-[20px]">
|
||||
<span className="font-bold text-[16px]">
|
||||
Gedung Kultubai Selatan, Blok F
|
||||
</span>{" "}
|
||||
<br />
|
||||
Jl. Telekomunikasi, Terusan Buah Batu Bandung <br />
|
||||
Jawa Barat, Indonesia. 40257
|
||||
</p>
|
||||
<div className="icons-media flex flex-row items-center gap-4 mt-[40px]">
|
||||
<a
|
||||
href="https://www.instagram.com/fariedgnwn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="instagram p-[10px] border-2 rounded-full"
|
||||
>
|
||||
<FaInstagram className="text-[24px]" />
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/company/humic-engineering/about/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="linkedin p-[10px] border-2 rounded-full"
|
||||
>
|
||||
<FaLinkedinIn className="text-[24px]" />
|
||||
</a>
|
||||
<div className="mail p-[10px] border-2 rounded-full">
|
||||
<MdOutlineEmail className="text-[24px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Left Section */}
|
||||
<div className="left-section flex flex-col sm:flex-row gap-10">
|
||||
<div className="sec-1 flex flex-col gap-3">
|
||||
<p className="font-bold">Internship</p>
|
||||
<p>Beranda</p>
|
||||
<p>Tentang</p>
|
||||
<p>Life at Humic</p>
|
||||
</div>
|
||||
<div className="sec-2 flex flex-col gap-3">
|
||||
<p className="font-bold">Lainnya</p>
|
||||
<p>Humic Website</p>
|
||||
<p>Our Activity</p>
|
||||
<p>Life at Humic</p>
|
||||
<p
|
||||
onClick={() => navigate("/our-developer")}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Our Developer
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="flex flex-col sm:flex-row justify-center items-center mt-[80px] text-center text-sm sm:text-base">
|
||||
© 2025 Humic Research Center
|
||||
<span className="font-bold sm:ml-[10px]">
|
||||
Persyaratan Layanan | Kebijakan Privasi
|
||||
</span>
|
||||
</h2>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useState } from "react";
|
||||
import humiclogo from "../assets/logo.png";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const Navbar = () => {
|
||||
const [openMenu, setOpenMenu] = useState(false);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleContactClick = () => {
|
||||
if (location.pathname === "/") {
|
||||
document.getElementById("footer")?.scrollIntoView({ behavior: "smooth" });
|
||||
} else {
|
||||
navigate("/", { replace: false });
|
||||
setTimeout(() => {
|
||||
document
|
||||
.getElementById("footer")
|
||||
?.scrollIntoView({ behavior: "smooth" });
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="bg-[#D7E1E8] px-4 py-5 mx-5 rounded-2xl shadow-md">
|
||||
<div className="max-w-8xl mx-auto flex items-center justify-between">
|
||||
<img
|
||||
src={humiclogo}
|
||||
alt="Humic Logo"
|
||||
onClick={() => navigate("/")}
|
||||
className="w-[130px] cursor-pointer"
|
||||
/>
|
||||
|
||||
{/* Burger menu */}
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
onClick={() => setOpenMenu(!openMenu)}
|
||||
className="text-gray-800 focus:outline-none"
|
||||
>
|
||||
<svg
|
||||
className="w-7 h-7"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d={
|
||||
openMenu ? "M6 18L18 6M6 6l12 12" : "M4 6h16M4 12h16M4 18h16"
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Desktop menu */}
|
||||
<div className="hidden md:flex items-center gap-6 text-[18px] font-semibold">
|
||||
<button
|
||||
onClick={() => navigate("/about-us")}
|
||||
className="hover:text-blue-600 transition cursor-pointer"
|
||||
>
|
||||
About Us
|
||||
</button>
|
||||
<button
|
||||
onClick={handleContactClick}
|
||||
className="hover:text-blue-600 transition cursor-pointer"
|
||||
>
|
||||
Contact Us
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate("/internships")}
|
||||
className="hover:text-blue-600 transition cursor-pointer"
|
||||
>
|
||||
Internship
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{openMenu && (
|
||||
<div className="md:hidden mt-4 text-[18px] font-semibold space-y-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/about-us");
|
||||
setOpenMenu(false);
|
||||
}}
|
||||
className="block w-full text-left hover:text-blue-600 transition"
|
||||
>
|
||||
About Us
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleContactClick();
|
||||
setOpenMenu(false);
|
||||
}}
|
||||
className="block w-full text-left hover:text-blue-600 transition"
|
||||
>
|
||||
Contact Us
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/internships");
|
||||
setOpenMenu(false);
|
||||
}}
|
||||
className="block w-full text-left hover:text-blue-600 transition"
|
||||
>
|
||||
Internship
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import logo from "../assets/whiteLogo.png";
|
||||
|
||||
const NavbarAdmin = () => {
|
||||
const [namaDepan, setNamaDepan] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const token = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("token="))
|
||||
?.split("=")[1];
|
||||
|
||||
if (!token) return;
|
||||
|
||||
const fetchUserData = async () => {
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_API_BASE_URL}/auth-api/me`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log("Response status:", response.status);
|
||||
console.log("Response body:", result);
|
||||
|
||||
if (response.ok && result.data?.nama_depan) {
|
||||
setNamaDepan(result.data.nama_depan);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal mengambil data user:", error);
|
||||
}
|
||||
};
|
||||
fetchUserData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center w-full justify-between p-3 bg-[#2D3748] px-[50px]">
|
||||
<img src={logo} className="w-[180px]" alt="Logo" />
|
||||
<div className="user-container flex flex-row items-center gap-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-12 text-white"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
/>
|
||||
</svg>
|
||||
<h2 className="text-white text-[16px] font-semibold">
|
||||
Hai, {namaDepan}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavbarAdmin;
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const SidebarAdmin = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
return (
|
||||
<div className="bg-[#EDF2F7] w-[300px] min-h-[87vh] flex flex-col justify-between p-4">
|
||||
<div className="container-sidebar">
|
||||
<h2 className="font-semibold text-[#2C5282] text-[20px]">Menu</h2>
|
||||
<div className="list-of-sidebar flex flex-col p-7 gap-4">
|
||||
<div
|
||||
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
|
||||
location.pathname === "/produc-lList"
|
||||
? "bg-[#D3DFEA] font-bold"
|
||||
: "hover:bg-[#D3DFEA] "
|
||||
}`}
|
||||
onClick={() => navigate("/product-list")}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-8 text-[#2C5282]"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"
|
||||
/>
|
||||
</svg>
|
||||
<h2 className="text-[#2C5282]">Project</h2>
|
||||
</div>
|
||||
<div
|
||||
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
|
||||
location.pathname === "/lowongan-list"
|
||||
? "bg-[#D3DFEA] font-bold"
|
||||
: "hover:bg-[#D3DFEA]"
|
||||
}`}
|
||||
onClick={() => navigate("/lowongan-list")}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-8 text-[#2C5282]"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 10.5v6m3-3H9m4.06-7.19-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<h2 className="text-[#2C5282]">Lowongan</h2>
|
||||
</div>
|
||||
<div
|
||||
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
|
||||
location.pathname === "/partnership-admin"
|
||||
? "bg-[#D3DFEA] font-bold"
|
||||
: "hover:bg-[#D3DFEA]"
|
||||
}`}
|
||||
onClick={() => navigate("/partnership-admin")}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-8 text-[#2C5282]"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<h2 className="text-[#2C5282]">Partnership</h2>
|
||||
</div>
|
||||
<div
|
||||
className={`text-container flex flex-row gap-3 p-3 items-center cursor-pointer rounded-2xl ${
|
||||
location.pathname === "/internships-list"
|
||||
? "bg-[#D3DFEA] font-bold"
|
||||
: "hover:bg-[#D3DFEA]"
|
||||
}`}
|
||||
onClick={() => navigate("/internships-list")}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-8 text-[#2C5282]"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<h2 className="text-[#2C5282]">Pelamar</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="logout flex flex-row items-center cursor-pointer gap-4 p-3 rounded-2xl hover:bg-[#F8D2D2] hover:font-bold mx-7"
|
||||
onClick={() => {
|
||||
document.cookie =
|
||||
"token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;";
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-8 text-[#C3423F]"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15m-3 0-3-3m0 0 3-3m-3 3H15"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<h2 className="text-[#C3423F] text-[16px] font-semibold">Keluar</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarAdmin;
|
||||
@@ -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;
|
||||
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 353 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 3.8 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 445 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 503 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 210 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 750 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@font-face {
|
||||
font-family: JakartaSans;
|
||||
src: url("/fonts/PlusJakartaSans-VariableFont_wght.ttf") format("truetype");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: JakartaSans, sans-serif;
|
||||
color: black;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
declare module "react-google-recaptcha" {
|
||||
import * as React from "react";
|
||||
|
||||
interface ReCAPTCHAProps {
|
||||
sitekey: string;
|
||||
onChange?: (token: string | null) => void;
|
||||
onExpired?: () => void;
|
||||
onErrored?: () => void;
|
||||
theme?: "light" | "dark";
|
||||
size?: "compact" | "normal" | "invisible";
|
||||
tabindex?: number;
|
||||
badge?: "bottomright" | "bottomleft" | "inline";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default class ReCAPTCHA extends React.Component<ReCAPTCHAProps> {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
});
|
||||