first commit

This commit is contained in:
kharismaulia0610 2026-05-22 15:54:20 +07:00
commit 50c8a11748
61 changed files with 6816 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

73
README.md Normal file
View File

@ -0,0 +1,73 @@
# 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 [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## 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 defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// 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,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
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 defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

22
eslint.config.js Normal file
View File

@ -0,0 +1,22 @@
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'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
},
},
])

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>kasir</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3981
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "kasir",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.16.0",
"lucide-react": "^1.14.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router-dom": "^7.14.2"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.5.0",
"eslint": "^10.2.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"postcss": "^8.5.12",
"tailwindcss": "^3.4.19",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.2",
"vite": "^8.0.10"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

1
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

24
public/icons.svg Normal file
View File

@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

184
src/App.css Normal file
View File

@ -0,0 +1,184 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}

34
src/App.tsx Normal file
View File

@ -0,0 +1,34 @@
import { useEffect, useState } from "react";
import { RouterProvider } from "react-router-dom";
import { router } from "./Routes";
function App() {
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
const savedTheme = localStorage.getItem("theme");
if (savedTheme === "dark") {
setDarkMode(true);
document.documentElement.classList.add("dark");
}
}, []);
useEffect(() => {
if (darkMode) {
document.documentElement.classList.add("dark");
localStorage.setItem("theme", "dark");
} else {
document.documentElement.classList.remove("dark");
localStorage.setItem("theme", "light");
}
}, [darkMode]);
return (
<div className="min-h-screen bg-white text-black dark:bg-gray-900 dark:text-white transition-all duration-300">
<RouterProvider router={router} />
</div>
);
}
export default App;

57
src/Routes.tsx Normal file
View File

@ -0,0 +1,57 @@
// src/Routes.tsx
import { createBrowserRouter } from "react-router-dom";
import DashboardLayout from "./layout/DashboardLayout";
// ROUTES
import Dashboard from "./routes/Dashboard";
import ManajemenUsers from "./routes/ManajementUsers";
import MasterData from "./routes/MasterData";
import LoginKasir from "./routes/LoginKasir";
import TransaksiBAST from "./pages/TransaksiBAST";
import Laporan from "./pages/Laporan";
import SettingsPage from "./pages/SettingsPage";
export const router = createBrowserRouter([
/* LOGIN */
{
path: "/login",
element: <LoginKasir />,
},
/* DASHBOARD LAYOUT */
{
path: "/",
element: <DashboardLayout />,
children: [
/* DASHBOARD */
{
index: true,
element: <Dashboard />,
},
/* MANAJEMEN USERS */
{
path: "manajemen-users",
element: <ManajemenUsers />,
},
/* MASTER DATA */
{
path: "master-data",
element: <MasterData />,
},
{
path: "transaksi-bast",
element: <TransaksiBAST />,
},
{
path: "laporan",
element: <Laporan />,
},
{
path: "settings",
element: <SettingsPage />,
},
],
},
]);

BIN
src/assets/Filsafat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
src/assets/alat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 KiB

BIN
src/assets/buku.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

BIN
src/assets/elektronik.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
src/assets/komputer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
src/assets/lampu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/assets/laptop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
src/assets/matematika.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

BIN
src/assets/novel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
src/assets/pensil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

BIN
src/assets/pulang.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
src/assets/pulpen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

BIN
src/assets/setrika.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
src/assets/tas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 KiB

BIN
src/assets/tipex.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

BIN
src/assets/welcome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -0,0 +1,13 @@
export default function About() {
return (
<div className="bg-white rounded-2xl shadow p-6 mb-8">
<h2 className="text-2xl font-bold mb-3">
Tentang Sistem
</h2>
<p className="text-gray-600">
Aplikasi kasir dan peminjaman barang sekolah ini dirancang untuk mempermudah pengelolaan transaksi penjualan, pencatatan data barang, serta proses peminjaman perlengkapan sekolah seperti buku, alat tulis, dan perangkat pendukung lainnya. Dengan sistem yang modern dan terintegrasi, pengguna dapat mengelola stok barang, melakukan transaksi, memantau data peminjaman, serta melihat laporan dengan lebih cepat, akurat, dan aman. Dashboard utama memberikan akses mudah ke seluruh fitur penting sehingga operasional sekolah menjadi lebih terorganisir, efisien, dan profesional.
</p>
</div>
);
}

View File

@ -0,0 +1,61 @@
import novel from "../../assets/novel.png";
import elektronik from "../../assets/elektronik.png";
import alat from "../../assets/alat.png";
export default function Content() {
const items = [
{
name: "novel",
image: novel,
function:
"rekreatif (hiburan), didaktif (mendidik), moralitas (penanaman nilai baik-buruk), religius (ajaran agama), dan estetis (keindahan bahasa/cerita).",
},
{
name: "elektronik",
image: elektronik,
function:
"Elektronik digunakan untuk mempermudah pekerjaan manusia, memberikan hiburan, membantu komunikasi, serta meningkatkan efisiensi dan keamanan dalam kehidupan sehari-hari.",
},
{
name: "Alat Tulis",
image: alat ,
function:
"mencatat informasi, mendukung administrasi perkantoran, menunjang kegiatan belajar-mengajar, serta memfasilitasi ekspresi kreatif seperti menggambar dan merancang.",
},
];
return (
<div className="space-y-8 bg-white p-6 rounded-2xl">
<h2 className="text-3xl font-bold text-black mb-6 border-b-4 border-blue-600 pb-2">
Barang Barang
</h2>
{items.map((item, index) => (
<div
key={index}
className="bg-black text-white rounded-2xl shadow-lg p-6 flex items-center gap-8 border-l-4 border-blue-600"
>
{/* KIRI - PENJELASAN */}
<div className="flex-1">
<h3 className="text-2xl font-bold text-blue-400 mb-3">
{item.name}
</h3>
<p className="text-gray-300 leading-relaxed">
{item.function}
</p>
</div>
{/* KANAN - FOTO */}
<div className="w-72">
<img
src={item.image}
alt={item.name}
className="w-full h-56 object-cover rounded-xl shadow border-2 border-blue-500"
/>
</div>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,87 @@
// src/components/dashboard/Hero.tsx
export default function Hero() {
return (
<div className="bg-blue-50 rounded-3xl p-8 shadow-sm border border-blue-100 mb-8">
{/* HEADER */}
<div className="flex justify-between items-start mb-8">
<div>
<p className="text-gray-500 text-lg">
Selamat datang kembali 👋
</p>
<h1 className="text-5xl font-extrabold text-blue-700 mt-2">
Dashboard
</h1>
</div>
{/* PROFILE */}
<div className="flex items-center gap-3 bg-white rounded-full px-4 py-2 shadow border">
<div className="w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center text-white font-bold">
BU
</div>
<div>
<h3 className="font-semibold text-black">
Bu Sari
</h3>
<p className="text-gray-500 text-sm">
Admin
</p>
</div>
</div>
</div>
{/* STAT CARDS */}
<div className="grid grid-cols-4 gap-6">
<div className="bg-white rounded-2xl p-6 shadow border">
<p className="text-gray-500 mb-2">
Permintaan Hari Ini
</p>
<h2 className="text-4xl font-bold text-blue-700">
27
</h2>
<p className="text-green-600 mt-2">
+5 dari kemarin
</p>
</div>
<div className="bg-white rounded-2xl p-6 shadow border">
<p className="text-gray-500 mb-2">
Menunggu Persetujuan
</p>
<h2 className="text-4xl font-bold text-blue-700">
5
</h2>
<p className="text-yellow-600 mt-2">
Perlu tindakan segera
</p>
</div>
<div className="bg-white rounded-2xl p-6 shadow border">
<p className="text-gray-500 mb-2">
Total Barang
</p>
<h2 className="text-4xl font-bold text-blue-700">
48
</h2>
<p className="text-green-600 mt-2">
3 stok hampir habis
</p>
</div>
<div className="bg-white rounded-2xl p-6 shadow border">
<p className="text-gray-500 mb-2">
Pengguna Aktif
</p>
<h2 className="text-4xl font-bold text-blue-700">
214
</h2>
<p className="text-green-600 mt-2">
Siswa & petugas
</p>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,45 @@
import type { LucideIcon } from 'lucide-react';
interface Props {
label: string;
name: string;
type: string;
icon: LucideIcon;
placeholder: string;
showEye?: boolean;
toggleEye?: () => void;
eyeIcon?: LucideIcon;
// --- TAMBAHKAN DUA INI ---
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
export default function LoginInput({
label, name, type, icon: Icon, placeholder, showEye, toggleEye, eyeIcon: EyeIcon,
value, onChange // <--- Terima props di sini
}: Props) {
return (
<div className="mb-4">
<label className="block text-gray-400 text-sm mb-1">{label}:</label>
<div className="relative group">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-500 group-focus-within:text-[#4A90E2]">
<Icon size={20} />
</div>
<input
name={name}
type={type}
placeholder={placeholder}
// --- PASANG DI SINI ---
value={value}
onChange={onChange}
className="w-full bg-[#3D444D] border border-gray-600 rounded-lg pl-11 pr-10 py-3 text-white focus:outline-none focus:border-[#4A90E2] transition-all placeholder:text-gray-500"
/>
{showEye && toggleEye && EyeIcon && (
<button type="button" onClick={toggleEye} className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-white">
<EyeIcon size={20} />
</button>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,10 @@
export default function MyButton({ text, type = "button" }: { text: string; type?: "submit" | "button" }) {
return (
<button
type={type}
className="w-full bg-[#4A90E2] hover:bg-[#357ABD] text-white font-semibold py-3 rounded-lg transition-all active:scale-95 shadow-lg mt-4"
>
{text}
</button>
);
}

View File

@ -0,0 +1,173 @@
// src/components/manajemenUsers/EditUserModal.tsx
import { useState } from "react";
import type { UserType } from "../../routes/ManajementUsers";
type Props = {
user: UserType;
onClose: () => void;
onSave: (
user: UserType
) => void;
onDelete: (
id: string
) => void;
};
export default function EditUserModal({
user,
onClose,
onSave,
onDelete,
}: Props) {
/* STATE */
const [nama, setNama] =
useState(user.nama);
const [role, setRole] =
useState(user.role);
const [kelas, setKelas] =
useState(user.kelas);
const [status, setStatus] =
useState(user.status);
/* SAVE */
const handleSubmit = () => {
const updatedUser: UserType =
{
...user,
nama,
role,
kelas:
role === "Guru"
? "-"
: kelas,
status,
};
onSave(updatedUser);
};
return (
<div className="fixed inset-0 bg-black/50 flex justify-center items-center z-50">
{/* MODAL BOX */}
<div className="bg-white w-[450px] rounded-3xl p-6 shadow-2xl">
{/* HEADER */}
<h2 className="text-2xl font-bold text-center mb-6">
Edit User
</h2>
{/* FORM */}
<div className="space-y-4">
{/* NAMA */}
<input
type="text"
value={nama}
onChange={(e) =>
setNama(
e.target.value
)
}
className="w-full border rounded-xl p-3"
placeholder="Nama User"
/>
{/* ROLE */}
<select
value={role}
onChange={(e) =>
setRole(
e.target.value
)
}
className="w-full border rounded-xl p-3"
>
<option value="Siswa">
Siswa
</option>
<option value="Guru">
Guru
</option>
</select>
{/* KELAS */}
{role ===
"Siswa" && (
<input
type="text"
value={kelas}
onChange={(e) =>
setKelas(
e.target.value
)
}
className="w-full border rounded-xl p-3"
placeholder="Kelas"
/>
)}
{/* STATUS */}
<select
value={status}
onChange={(e) =>
setStatus(
e.target.value
)
}
className="w-full border rounded-xl p-3"
>
<option value="Aktif">
Aktif
</option>
<option value="Nonaktif">
Nonaktif
</option>
</select>
</div>
{/* BUTTON */}
<div className="flex justify-between mt-8">
{/* DELETE */}
<button
onClick={() =>
onDelete(
user.id
)
}
className="bg-red-600 text-white px-5 py-2 rounded-xl hover:bg-red-700 transition"
>
Hapus
</button>
<div className="flex gap-3">
{/* CANCEL */}
<button
onClick={onClose}
className="bg-gray-300 px-5 py-2 rounded-xl hover:bg-gray-400 transition"
>
Batal
</button>
{/* SAVE */}
<button
onClick={
handleSubmit
}
className="bg-blue-600 text-white px-5 py-2 rounded-xl hover:bg-blue-700 transition"
>
Simpan
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,128 @@
// src/components/manajemenUsers/TambahUserModal.tsx
import { useState } from "react";
type UserType = {
id: string;
nama: string;
role: string;
kelas: string;
status: string;
};
type Props = {
onClose: () => void;
onSave: (
user: UserType
) => void;
};
export default function TambahUserModal({
onClose,
onSave,
}: Props) {
const [nama, setNama] =
useState("");
const [role, setRole] =
useState("Siswa");
const [kelas, setKelas] =
useState("");
const handleSubmit = () => {
if (!nama) return;
const newUser: UserType = {
id: `#${Date.now()}`,
nama,
role,
kelas:
role === "Guru"
? "-"
: kelas,
status: "Aktif",
};
onSave(newUser);
};
return (
<div className="fixed inset-0 bg-black/50 flex justify-center items-center z-50">
<div className="bg-white w-[420px] rounded-3xl p-6 shadow-2xl">
{/* HEADER */}
<h2 className="text-xl font-bold text-center mb-5">
Tambah User
</h2>
{/* FORM */}
<div className="space-y-4">
{/* NAMA */}
<input
type="text"
placeholder="Nama User"
value={nama}
onChange={(e) =>
setNama(
e.target.value
)
}
className="w-full border rounded-xl p-3"
/>
{/* ROLE */}
<select
value={role}
onChange={(e) =>
setRole(
e.target.value
)
}
className="w-full border rounded-xl p-3"
>
<option value="Siswa">
Siswa
</option>
<option value="Guru">
Guru
</option>
</select>
{/* KELAS */}
{role === "Siswa" && (
<input
type="text"
placeholder="Kelas"
value={kelas}
onChange={(e) =>
setKelas(
e.target.value
)
}
className="w-full border rounded-xl p-3"
/>
)}
</div>
{/* BUTTON */}
<div className="flex justify-end gap-3 mt-6">
<button
onClick={onClose}
className="px-4 py-2 rounded-xl bg-gray-300 hover:bg-gray-400"
>
Batal
</button>
<button
onClick={handleSubmit}
className="px-4 py-2 rounded-xl bg-blue-600 text-white hover:bg-blue-700"
>
Simpan
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,83 @@
// src/components/manajemenUsers/UserFilter.tsx
type Props = {
selectedRole: string;
setSelectedRole: (
role: string
) => void;
search: string;
setSearch: (
search: string
) => void;
onTambahUser: () => void;
};
export default function UsersFilter({
selectedRole,
setSelectedRole,
search,
setSearch,
onTambahUser,
}: Props) {
const roleList = [
"Semua",
"Siswa",
"Guru",
];
return (
<div className="bg-white rounded-3xl shadow-lg p-6 mb-8">
{/* ATAS */}
<div className="flex justify-between items-center mb-4">
{/* FILTER ROLE */}
<div className="flex gap-3 flex-wrap">
{roleList.map((role) => (
<button
key={role}
onClick={() =>
setSelectedRole(
role
)
}
className={`px-4 py-2 rounded-xl font-medium transition ${
selectedRole ===
role
? "bg-blue-600 text-white"
: "bg-gray-100 hover:bg-gray-200"
}`}
>
{role}
</button>
))}
</div>
{/* TAMBAH USER */}
<button
onClick={onTambahUser}
className="bg-blue-600 text-white px-5 py-2 rounded-2xl hover:bg-blue-700 transition shadow-md"
>
+ Tambah User
</button>
</div>
{/* SEARCH */}
<div>
<input
type="text"
placeholder="Cari user..."
value={search}
onChange={(e) =>
setSearch(
e.target.value
)
}
className="w-full border rounded-2xl p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,21 @@
// src/components/manajemenUsers/UserHero.tsx
export default function UsersHero() {
return (
<div className="bg-gradient-to-r from-blue-600 to-blue-400 rounded-3xl p-8 text-white shadow-xl mb-8">
{/* TITLE */}
<h1 className="text-3xl font-bold mb-2">
Manajemen Users
</h1>
{/* DESKRIPSI */}
<p className="text-blue-100 text-lg">
Kelola data siswa dan guru sekolah,
atur status pengguna,
serta pantau seluruh aktivitas user
dalam sistem kasir & peminjaman.
</p>
</div>
);
}

View File

@ -0,0 +1,135 @@
// src/components/manajemenUsers/UserTable.tsx
import type { UserType } from "../../routes/ManajementUsers";
type Props = {
users: UserType[];
selectedRole: string;
search: string;
onEdit: (
user: UserType
) => void;
onDelete: (
id: string
) => void;
};
export default function UsersTable({
users,
selectedRole,
search,
onEdit,
onDelete,
}: Props) {
/* FILTER */
const filteredUsers =
users.filter((user) => {
const matchRole =
selectedRole === "Semua" ||
user.role === selectedRole;
const matchSearch =
user.nama
.toLowerCase()
.includes(
search.toLowerCase()
);
return (
matchRole &&
matchSearch
);
});
return (
<div className="bg-white rounded-3xl shadow-lg p-6 overflow-x-auto">
<table className="w-full text-left">
{/* HEADER */}
<thead>
<tr className="border-b text-gray-700">
<th className="p-3">ID</th>
<th className="p-3">Nama</th>
<th className="p-3">Role</th>
<th className="p-3">Kelas</th>
<th className="p-3">Status</th>
<th className="p-3 text-center min-w-[180px]">
Aksi
</th>
</tr>
</thead>
{/* BODY */}
<tbody>
{filteredUsers.map(
(user) => (
<tr
key={user.id}
className="border-b hover:bg-gray-50"
>
<td className="p-3">
{user.id}
</td>
<td className="p-3 font-medium">
{user.nama}
</td>
<td className="p-3">
{user.role}
</td>
<td className="p-3">
{user.kelas}
</td>
<td className="p-3">
<span
className={`px-3 py-1 rounded-full text-sm ${
user.status === "Aktif"
? "bg-green-100 text-green-700"
: "bg-red-100 text-red-700"
}`}
>
{user.status}
</span>
</td>
{/* AKSI */}
<td className="p-3">
<div className="flex justify-center gap-3">
{/* EDIT */}
<button
onClick={() =>
onEdit(user)
}
className="bg-blue-600 text-white px-4 py-2 rounded-xl hover:bg-blue-700 transition"
>
Edit
</button>
{/* HAPUS */}
<button
onClick={() =>
onDelete(
user.id
)
}
className="bg-red-600 text-white px-4 py-2 rounded-xl hover:bg-red-700 transition"
>
Hapus
</button>
</div>
</td>
</tr>
)
)}
</tbody>
</table>
</div>
);
}

View File

@ -0,0 +1,86 @@
// src/components/masterData/BarangFilter.tsx
type Props = {
selectedKategori: string;
setSelectedKategori: (
kategori: string
) => void;
search: string;
setSearch: (
search: string
) => void;
onTambahBarang: () => void;
};
export default function BarangFilter({
selectedKategori,
setSelectedKategori,
search,
setSearch,
onTambahBarang,
}: Props) {
const kategoriList = [
"Semua",
"Buku",
"Elektronik",
"Alat Tulis",
];
return (
<div className="bg-white rounded-3xl shadow-lg p-6 mb-8">
{/* ATAS */}
<div className="flex justify-between items-center mb-4">
{/* FILTER KATEGORI */}
<div className="flex gap-3 flex-wrap">
{kategoriList.map(
(kategori) => (
<button
key={kategori}
onClick={() =>
setSelectedKategori(
kategori
)
}
className={`px-4 py-2 rounded-xl font-medium transition ${
selectedKategori ===
kategori
? "bg-blue-600 text-white"
: "bg-gray-100 hover:bg-gray-200"
}`}
>
{kategori}
</button>
)
)}
</div>
{/* TAMBAH BARANG */}
<button
onClick={onTambahBarang}
className="bg-blue-600 text-white px-5 py-2 rounded-2xl hover:bg-blue-700 transition shadow-md"
>
+ Tambah Barang
</button>
</div>
{/* SEARCH */}
<div>
<input
type="text"
placeholder="Cari barang..."
value={search}
onChange={(e) =>
setSearch(
e.target.value
)
}
className="w-full border rounded-2xl p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
);
}

View File

@ -0,0 +1,90 @@
// src/components/masterData/BarangGrid.tsx
type Barang = {
id: number;
nama: string;
foto: string;
kondisi: string;
disetujui: boolean;
kategori: string;
waktu: string;
};
type Props = {
barangList: Barang[];
onEdit: (barang: Barang) => void;
};
export default function BarangGrid({
barangList,
onEdit,
}: Props) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{barangList.map((barang) => (
<div
key={barang.id}
className="bg-white rounded-3xl shadow-lg p-4 hover:shadow-2xl transition"
>
{/* FOTO 1:1 */}
<div className="aspect-square rounded-2xl overflow-hidden bg-gray-100 mb-4">
<img
src={barang.foto}
alt={barang.nama}
className="w-full h-full object-cover"
/>
</div>
{/* NAMA */}
<h3 className="text-lg font-bold mb-2">
{barang.nama}
</h3>
{/* KATEGORI */}
<p className="text-sm text-gray-500 mb-1">
Kategori: {barang.kategori}
</p>
{/* KONDISI */}
<p className="text-sm mb-1">
Kondisi:{" "}
<span className="font-medium">
{barang.kondisi}
</span>
</p>
{/* STATUS */}
<p className="text-sm mb-1">
Status:{" "}
<span
className={`font-semibold ${
barang.disetujui
? "text-green-600"
: "text-red-500"
}`}
>
{barang.disetujui
? "Disetujui"
: "Belum Disetujui"}
</span>
</p>
{/* WAKTU */}
<p className="text-xs text-gray-400 mb-4">
Update: {barang.waktu}
</p>
{/* BUTTON */}
<button
onClick={() =>
onEdit(barang)
}
className="w-full bg-blue-600 text-white py-2 rounded-2xl hover:bg-blue-700 transition"
>
Edit Barang
</button>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,14 @@
export default function BarangHero() {
return (
<div className="bg-gradient-to-r from-black via-blue-900 to-blue-600 rounded-3xl p-8 text-white mb-8">
<h1 className="text-4xl font-bold mb-2">
Kelola Barang Sekolah
</h1>
<p className="text-gray-200">
Atur stok, harga, dan data barang sekolah
dengan mudah.
</p>
</div>
);
}

View File

@ -0,0 +1,153 @@
// src/components/masterData/EditBarangModal.tsx
import { useState } from "react";
type BarangType = {
id: number;
nama: string;
foto: string;
kondisi: string;
disetujui: boolean;
kategori: string;
waktu: string;
};
type Props = {
barang: BarangType;
onClose: () => void;
onSave: (
barang: BarangType
) => void;
};
export default function EditBarangModal({
barang,
onClose,
onSave,
}: Props) {
const [kondisi, setKondisi] =
useState(
barang.kondisi
);
const [disetujui, setDisetujui] =
useState(
barang.disetujui
);
const handleSubmit = () => {
onSave({
...barang,
kondisi,
disetujui,
waktu:
new Date().toLocaleTimeString(
"id-ID"
),
});
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white w-[420px] rounded-3xl shadow-2xl p-6">
{/* HEADER */}
<h2 className="text-xl font-bold text-center mb-5">
Edit Barang
</h2>
{/* FOTO */}
<div className="w-28 h-28 mx-auto rounded-2xl overflow-hidden mb-4 bg-gray-100">
<img
src={barang.foto}
alt={barang.nama}
className="w-full h-full object-cover"
/>
</div>
{/* NAMA */}
<p className="text-center font-semibold mb-2">
{barang.nama}
</p>
{/* WAKTU */}
<p className="text-center text-sm text-gray-500 mb-4">
Update:
{barang.waktu}
</p>
{/* FORM */}
<div className="space-y-4">
{/* KONDISI */}
<div>
<label className="block text-sm font-medium mb-1">
Kondisi
</label>
<select
value={kondisi}
onChange={(e) =>
setKondisi(
e.target.value
)
}
className="w-full border rounded-xl p-3"
>
<option value="Bagus">
Bagus
</option>
<option value="Kurang Bagus">
Kurang Bagus
</option>
</select>
</div>
{/* STATUS */}
<div>
<label className="block text-sm font-medium mb-1">
Status Persetujuan
</label>
<select
value={
disetujui
? "true"
: "false"
}
onChange={(e) =>
setDisetujui(
e.target.value ===
"true"
)
}
className="w-full border rounded-xl p-3"
>
<option value="true">
Disetujui
</option>
<option value="false">
Belum Disetujui
</option>
</select>
</div>
</div>
{/* BUTTON */}
<div className="flex justify-end gap-3 mt-6">
<button
onClick={onClose}
className="px-4 py-2 rounded-xl bg-gray-300 hover:bg-gray-400"
>
Batal
</button>
<button
onClick={handleSubmit}
className="px-4 py-2 rounded-xl bg-blue-600 text-white hover:bg-blue-700"
>
Simpan
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,211 @@
// src/components/masterData/TambahBarang.tsx
import { useState } from "react";
type BarangType = {
id: number;
nama: string;
foto: string;
kondisi: string;
disetujui: boolean;
kategori: string;
waktu: string;
};
type Props = {
onClose: () => void;
onTambah: (
barang: BarangType
) => void;
};
export default function TambahBarang({
onClose,
onTambah,
}: Props) {
/* STATE */
const [nama, setNama] =
useState("");
const [kategori, setKategori] =
useState("Buku");
const [kondisi, setKondisi] =
useState("Bagus");
const [foto, setFoto] =
useState("");
/* LIST KATEGORI SAMA DENGAN FILTER */
const kategoriList = [
"Buku",
"Elektronik",
"Alat Tulis",
];
/* UPLOAD FOTO */
const handleFotoChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const file =
e.target.files?.[0];
if (file) {
const imageUrl =
URL.createObjectURL(
file
);
setFoto(imageUrl);
}
};
/* SIMPAN */
const handleSubmit = () => {
if (!nama || !foto) return;
const barangBaru: BarangType = {
id: Date.now(),
nama,
foto,
kondisi,
disetujui: true,
kategori,
waktu:
new Date().toLocaleTimeString(
"id-ID"
),
};
onTambah(barangBaru);
};
return (
<div className="fixed inset-0 bg-black/50 flex justify-center items-center z-50 p-4">
<div className="bg-white w-full max-w-lg rounded-3xl p-8 shadow-2xl">
{/* HEADER */}
<h2 className="text-2xl font-bold text-center mb-8 text-gray-800">
Tambah Barang Baru
</h2>
{/* FORM */}
<div className="space-y-5">
{/* NAMA */}
<div>
<label className="block text-sm font-medium mb-2 text-gray-700">
Nama Barang
</label>
<input
type="text"
placeholder="Masukkan nama barang"
value={nama}
onChange={(e) =>
setNama(
e.target.value
)
}
className="w-full border rounded-2xl p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* KATEGORI */}
<div>
<label className="block text-sm font-medium mb-2 text-gray-700">
Kategori
</label>
<select
value={kategori}
onChange={(e) =>
setKategori(
e.target.value
)
}
className="w-full border rounded-2xl p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{kategoriList.map(
(item) => (
<option
key={item}
value={item}
>
{item}
</option>
)
)}
</select>
</div>
{/* KONDISI */}
<div>
<label className="block text-sm font-medium mb-2 text-gray-700">
Kondisi
</label>
<select
value={kondisi}
onChange={(e) =>
setKondisi(
e.target.value
)
}
className="w-full border rounded-2xl p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="Bagus">
Bagus
</option>
<option value="Kurang Bagus">
Kurang Bagus
</option>
</select>
</div>
{/* FOTO */}
<div>
<label className="block text-sm font-medium mb-2 text-gray-700">
Upload Foto Barang
</label>
<input
type="file"
accept="image/*"
onChange={
handleFotoChange
}
className="w-full border rounded-2xl p-3"
/>
</div>
{/* PREVIEW FOTO */}
{foto && (
<div className="flex justify-center pt-2">
<div className="w-44 h-44 rounded-3xl overflow-hidden border-2 border-gray-200 shadow-md">
<img
src={foto}
alt="Preview"
className="w-full h-full object-cover"
/>
</div>
</div>
)}
</div>
{/* BUTTON */}
<div className="flex justify-end gap-4 mt-8">
<button
onClick={onClose}
className="px-6 py-3 rounded-2xl bg-gray-300 hover:bg-gray-400 transition font-medium"
>
Batal
</button>
<button
onClick={handleSubmit}
className="px-6 py-3 rounded-2xl bg-blue-600 text-white hover:bg-blue-700 transition font-medium shadow-md"
>
Simpan
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,110 @@
// src/components/sidebar/Sidebar.tsx
import { NavLink } from "react-router-dom";
import {
LayoutDashboard,
BookOpen,
ClipboardList,
Settings,
Menu,
} from "lucide-react";
type SidebarProps = {
isOpen: boolean;
setIsOpen: (value: boolean) => void;
};
const menuItems = [
{
name: "Dashboard",
path: "/",
icon: <LayoutDashboard size={20} />,
},
{
name: "Manajemen Users",
path: "/manajemen-users",
icon: <BookOpen size={20} />,
},
{
name: "Master Data (Kelola Barang)",
path: "/master-data",
icon: <ClipboardList size={20} />,
},
{
name: "Transaksi BAST",
path: "/transaksi-bast",
icon: <Settings size={20} />,
},
{
name: "Laporan",
path: "/laporan",
icon: <Settings size={20} />,
},
{
name: "Settings",
path: "/settings",
icon: <Settings size={20} />,
},
];
export default function Sidebar({
isOpen,
setIsOpen,
}: SidebarProps) {
return (
<aside
className={`fixed top-0 left-0 min-h-screen bg-white text-black dark:bg-black dark:text-white shadow-2xl transition-all duration-300 z-40
${isOpen ? "w-64" : "w-20"}`}
>
{/* HEADER */}
<div className="p-4 border-b border-blue-700 flex items-center justify-between">
{isOpen && (
<div>
<h1 className="text-xl font-extrabold text-blue-500">
SCHOOL LOAN
</h1>
<p className="text-xs text-gray-400">
Sistem Peminjaman
</p>
</div>
)}
{/* TOGGLE BUTTON */}
<button
onClick={() => setIsOpen(!isOpen)}
className="bg-blue-600 p-2 rounded-lg text-white"
>
<Menu size={20} />
</button>
</div>
{/* MENU */}
<nav className="p-4 space-y-3">
{menuItems.map((item, index) => (
<NavLink
key={index}
to={item.path}
className={({ isActive }) =>
`flex items-center ${
isOpen ? "justify-start" : "justify-center"
} gap-3 px-4 py-3 rounded-xl transition-all ${
isActive
? "bg-blue-600 text-white"
: "text-gray-700 dark:text-gray-300 hover:bg-blue-900 hover:text-white"
}`
}
>
{item.icon}
{isOpen && <span>{item.name}</span>}
</NavLink>
))}
</nav>
{/* FOOTER */}
{isOpen && (
<div className="absolute bottom-4 left-4 text-xs text-gray-400">
© 2026 School Loan
</div>
)}
</aside>
);
}

3
src/index.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,31 @@
// src/layout/DashboardLayout.tsx
import { useState } from "react";
import { Outlet } from "react-router-dom";
import Sidebar from "../components/sidebar/Sidebar";
export default function DashboardLayout() {
const [isOpen, setIsOpen] =
useState(true);
return (
<div className="flex min-h-screen bg-[#f4f8ff]">
{/* SIDEBAR */}
<Sidebar
isOpen={isOpen}
setIsOpen={setIsOpen}
/>
{/* MAIN CONTENT */}
<main
className={`flex-1 transition-all duration-300 ${
isOpen
? "ml-64"
: "ml-20"
}`}
>
<Outlet />
</main>
</div>
);
}

10
src/main.tsx Normal file
View File

@ -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>,
)

14
src/pages/Laporan.tsx Normal file
View File

@ -0,0 +1,14 @@
export default function Laporan() {
return (
<div className="p-8 min-h-screen bg-gray-100 dark:bg-gray-900 text-black dark:text-white transition-all duration-300">
<h1 className="text-4xl font-bold mb-6">
Laporan
</h1>
<div className="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow transition-all duration-300">
<p>Total Transaksi: 24</p>
<p>Total Barang: 120</p>
</div>
</div>
);
}

81
src/pages/Settings.tsx Normal file
View File

@ -0,0 +1,81 @@
export default function Settings() {
const settingsMenu = [
"Profil Toko",
"Manajemen User",
"Ganti Password",
"Printer Struk",
"Pengaturan Pajak",
"Kategori Barang",
"Backup Data",
"Tentang Aplikasi",
];
return (
<div className="p-8 bg-[#f4f7fe] min-h-screen">
<div className="mb-8">
<h1 className="text-5xl font-bold text-gray-800">
Settings
</h1>
<p className="text-gray-500 mt-2">
Pengaturan aplikasi kasir
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
{settingsMenu.map((item, index) => (
<div
key={index}
className="
bg-white
rounded-3xl
p-6
shadow-md
hover:shadow-xl
transition
duration-300
cursor-pointer
border
"
>
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold text-gray-800">
{item}
</h2>
<p className="text-gray-500 text-sm mt-2">
Klik untuk membuka pengaturan
</p>
</div>
<div className="
w-12
h-12
rounded-2xl
bg-blue-100
flex
items-center
justify-center
text-2xl
x
">
⚙️
</div>
</div>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,54 @@
import { useEffect, useState } from "react";
const Settings = () => {
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
const savedTheme = localStorage.getItem("theme");
if (savedTheme === "dark") {
setDarkMode(true);
}
}, []);
const toggleTheme = () => {
const newTheme = !darkMode;
setDarkMode(newTheme);
if (newTheme) {
document.documentElement.classList.add("dark");
localStorage.setItem("theme", "dark");
} else {
document.documentElement.classList.remove("dark");
localStorage.setItem("theme", "light");
}
};
return (
<div className="min-h-screen p-6 bg-gray-100 dark:bg-gray-900 text-black dark:text-white transition-all duration-300">
<h1 className="text-3xl font-bold mb-6">
Settings
</h1>
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<h2 className="text-xl font-semibold mb-4">
Pengaturan Tema
</h2>
<button
onClick={toggleTheme}
className={`px-5 py-3 rounded-xl font-semibold transition-all ${
darkMode
? "bg-yellow-400 text-black"
: "bg-blue-600 text-white"
}`}
>
{darkMode ? "Mode Terang ☀️" : "Mode Gelap 🌙"}
</button>
</div>
</div>
);
};
export default Settings;

109
src/pages/TransaksiBAST.tsx Normal file
View File

@ -0,0 +1,109 @@
export default function TransaksiBAST() {
const dataBAST = [
{
id: "BAST-001",
nama: "Sulthan",
barang: "Laptop ASUS",
tanggal: "12 Mei 2026",
status: "Dipinjam",
},
{
id: "BAST-002",
nama: "Zahir",
barang: "Proyektor Epson",
tanggal: "13 Mei 2026",
status: "Dikembalikan",
},
{
id: "BAST-003",
nama: "Akbar",
barang: "Komputer Lab",
tanggal: "14 Mei 2026",
status: "Dipinjam",
},
];
return (
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 p-8 transition-all">
{/* HEADER */}
<div className="bg-white dark:bg-gray-800 rounded-3xl p-8 shadow-lg mb-8">
<h1 className="text-4xl font-bold text-black dark:text-white mb-2">
Transaksi BAST
</h1>
<p className="text-gray-500 dark:text-gray-300">
Riwayat peminjaman barang sekolah
</p>
</div>
{/* TABLE */}
<div className="bg-white dark:bg-gray-800 rounded-3xl shadow-lg p-6 overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b dark:border-gray-700">
<th className="text-left py-4 text-black dark:text-white">
No BAST
</th>
<th className="text-left text-black dark:text-white">
Peminjam
</th>
<th className="text-left text-black dark:text-white">
Barang
</th>
<th className="text-left text-black dark:text-white">
Tanggal
</th>
<th className="text-left text-black dark:text-white">
Status
</th>
</tr>
</thead>
<tbody>
{dataBAST.map((item) => (
<tr
key={item.id}
className="border-b dark:border-gray-700"
>
<td className="py-4 text-gray-700 dark:text-gray-200">
{item.id}
</td>
<td className="text-gray-700 dark:text-gray-200">
{item.nama}
</td>
<td className="text-gray-700 dark:text-gray-200">
{item.barang}
</td>
<td className="text-gray-700 dark:text-gray-200">
{item.tanggal}
</td>
<td>
<span
className={`px-3 py-1 rounded-full text-sm font-medium ${
item.status ===
"Dipinjam"
? "bg-yellow-100 text-yellow-700"
: "bg-green-100 text-green-700"
}`}
>
{item.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

0
src/root.tsx Normal file
View File

13
src/routes/Dashboard.tsx Normal file
View File

@ -0,0 +1,13 @@
import Hero from "../components/dashboard/Hero";
import About from "../components/dashboard/About";
import Content from "../components/dashboard/Content";
export default function Dashboard() {
return (
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 p-6 transition-all duration-300">
<Hero />
<About />
<Content />
</div>
);
}

109
src/routes/LoginKasir.tsx Normal file
View File

@ -0,0 +1,109 @@
import { useState, useEffect } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router';
import { Lock, User, ShieldAlert, Eye, EyeOff } from 'lucide-react';
import LoginInput from '../components/login/LoginInput';
const LoginKasir = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
useEffect(() => {
localStorage.clear();
}, []);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const response = await axios.post('http://localhost:5000/api/login', {
username: username,
password: password
});
// Simpan data sesuai Data Dictionary
localStorage.setItem('token', response.data.token);
localStorage.setItem('user_role', response.data.role);
localStorage.setItem('user_name', response.data.nama_lengkap);
alert('Selamat Datang, ' + response.data.nama_lengkap);
navigate('/dashboard');
} catch (err: any) {
setError(err.response?.data?.message || 'Gagal terhubung ke server');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-[#1A1D21] flex items-center justify-center p-4">
<div className="max-w-md w-full bg-[#24272C] rounded-2xl p-8 shadow-2xl border border-white/5">
{/* Header */}
<div className="text-center mb-8">
<div className="bg-blue-500/10 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
<Lock className="text-blue-500" size={32} />
</div>
<h2 className="text-2xl font-bold text-white">Portal BAST</h2>
<p className="text-gray-400 text-sm mt-2">Silahkan masuk ke akun anda</p>
</div>
{/* Notif Error */}
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-3 rounded-lg mb-6 flex items-center gap-3 text-sm">
<ShieldAlert size={18} />
{error}
</div>
)}
<form onSubmit={handleLogin}>
<LoginInput
label="Username"
name="username"
type="text"
icon={User}
placeholder="Masukkan username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<LoginInput
label="Password"
name="password"
type={showPassword ? 'text' : 'password'}
icon={Lock}
placeholder="••••••••"
showEye={true}
toggleEye={() => setShowPassword(!showPassword)}
eyeIcon={showPassword ? EyeOff : Eye}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
type="submit"
disabled={loading}
className="w-full mt-6 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:cursor-not-allowed text-white font-semibold py-3 rounded-xl shadow-lg shadow-blue-500/20 transition-all transform active:scale-[0.98]"
>
{loading ? 'Menghubungkan...' : 'Masuk Sekarang'}
</button>
</form>
<p className="text-center text-gray-500 text-xs mt-8">
&copy; 2026 WinterAccess - Login Kasir
</p>
</div>
</div>
);
};
export default LoginKasir;

View File

@ -0,0 +1,166 @@
// src/routes/ManajemenUsers.tsx
import { useState } from "react";
// COMPONENTS
import UsersHero from "../components/manajemenUsers/UserHero";
import UsersFilter from "../components/manajemenUsers/UserFilter";
import UsersTable from "../components/manajemenUsers/UserTable";
import TambahUserModal from "../components/manajemenUsers/TambahUserModal";
import EditUserModal from "../components/manajemenUsers/EditUserModal";
export type UserType = {
id: string;
nama: string;
role: string;
kelas: string;
status: string;
};
export default function ManajemenUsers() {
/* FILTER ROLE */
const [selectedRole, setSelectedRole] =
useState("Semua");
/* SEARCH */
const [search, setSearch] =
useState("");
/* MODAL TAMBAH */
const [showTambahModal, setShowTambahModal] =
useState(false);
/* MODAL EDIT */
const [selectedUser, setSelectedUser] =
useState<UserType | null>(null);
/* DATA USER */
const [users, setUsers] =
useState<UserType[]>([
{
id: "#S001",
nama: "Sulthan",
role: "Siswa",
kelas: "XI RPL",
status: "Aktif",
},
{
id: "#S002",
nama: "Zahir",
role: "Siswa",
kelas: "XI RPL",
status: "Aktif",
},
{
id: "#G001",
nama: "Pak Budi",
role: "Guru",
kelas: "-",
status: "Aktif",
},
]);
/* TAMBAH USER */
const handleTambahUser = (
newUser: UserType
) => {
setUsers((prev) => [
...prev,
newUser,
]);
setShowTambahModal(false);
};
/* SAVE EDIT */
const handleSaveUser = (
updatedUser: UserType
) => {
setUsers((prev) =>
prev.map((user) =>
user.id === updatedUser.id
? updatedUser
: user
)
);
setSelectedUser(null);
};
/* DELETE */
const handleDeleteUser = (
id: string
) => {
setUsers((prev) =>
prev.filter(
(user) =>
user.id !== id
)
);
setSelectedUser(null);
};
return (
<div className="min-h-screen bg-[#f4f8ff] p-6">
{/* HERO */}
<UsersHero />
{/* FILTER */}
<UsersFilter
selectedRole={selectedRole}
setSelectedRole={
setSelectedRole
}
search={search}
setSearch={setSearch}
onTambahUser={() =>
setShowTambahModal(
true
)
}
/>
{/* TABLE */}
<UsersTable
users={users}
selectedRole={selectedRole}
search={search}
onEdit={setSelectedUser}
onDelete={
handleDeleteUser
}
/>
{/* MODAL TAMBAH */}
{showTambahModal && (
<TambahUserModal
onClose={() =>
setShowTambahModal(
false
)
}
onSave={handleTambahUser}
/>
)}
{/* MODAL EDIT */}
{selectedUser && (
<EditUserModal
user={selectedUser}
onClose={() =>
setSelectedUser(
null
)
}
onSave={handleSaveUser}
onDelete={
handleDeleteUser
}
/>
)}
</div>
);
}

272
src/routes/MasterData.tsx Normal file
View File

@ -0,0 +1,272 @@
// src/routes/MasterData.tsx
import { useState } from "react";
// IMPORT GAMBAR
import matematika from "../assets/matematika.png";
import pulang from "../assets/pulang.png";
import buku from "../assets/buku.png";
import filsafat from "../assets/Filsafat.png";
import laptop from "../assets/laptop.png";
import lampu from "../assets/lampu.png";
import komputer from "../assets/komputer.png";
import setrika from "../assets/setrika.png";
import tas from "../assets/tas.png";
import pensil from "../assets/pensil.png";
import pulpen from "../assets/pulpen.png";
import tipex from "../assets/tipex.png";
// COMPONENTS
import BarangHero from "../components/masterData/BarangHero";
import BarangFilter from "../components/masterData/BarangFilter";
import BarangGrid from "../components/masterData/BarangGrid";
import EditBarangModal from "../components/masterData/EditBarangModal";
import TambahBarang from "../components/masterData/TambahBarang";
// TYPE
export type BarangType = {
id: number;
nama: string;
foto: string;
kondisi: string;
disetujui: boolean;
kategori: string;
waktu: string;
};
export default function MasterData() {
/* MODAL EDIT */
const [selectedBarang, setSelectedBarang] =
useState<BarangType | null>(null);
/* MODAL TAMBAH */
const [showTambah, setShowTambah] =
useState(false);
/* FILTER */
const [selectedKategori, setSelectedKategori] =
useState("Semua");
/* SEARCH */
const [search, setSearch] =
useState("");
/* DATA */
const [barangList, setBarangList] =
useState<BarangType[]>([
{
id: 1,
nama: "Buku Matematika",
foto: matematika,
kondisi: "Bagus",
disetujui: true,
kategori: "Buku",
waktu: "08:00 WIB",
},
{
id: 2,
nama: "Buku Pulang",
foto: pulang,
kondisi: "Kurang Bagus",
disetujui: false,
kategori: "Buku",
waktu: "08:15 WIB",
},
{
id: 3,
nama: "Buku",
foto: buku,
kondisi: "Bagus",
disetujui: true,
kategori: "Buku",
waktu: "08:30 WIB",
},
{
id: 4,
nama: "Filsafat",
foto: filsafat,
kondisi: "Bagus",
disetujui: true,
kategori: "Buku",
waktu: "09:00 WIB",
},
{
id: 5,
nama: "Laptop",
foto: laptop,
kondisi: "Bagus",
disetujui: true,
kategori: "Elektronik",
waktu: "09:15 WIB",
},
{
id: 6,
nama: "Komputer",
foto: komputer,
kondisi: "Bagus",
disetujui: true,
kategori: "Elektronik",
waktu: "09:30 WIB",
},
{
id: 7,
nama: "Lampu",
foto: lampu,
kondisi: "Bagus",
disetujui: false,
kategori: "Elektronik",
waktu: "10:00 WIB",
},
{
id: 8,
nama: "Setrika",
foto: setrika,
kondisi: "Bagus",
disetujui: true,
kategori: "Elektronik",
waktu: "10:15 WIB",
},
{
id: 9,
nama: "Tas",
foto: tas,
kondisi: "Bagus",
disetujui: true,
kategori: "Alat Tulis",
waktu: "10:30 WIB",
},
{
id: 10,
nama: "Pensil",
foto: pensil,
kondisi: "Bagus",
disetujui: true,
kategori: "Alat Tulis",
waktu: "11:00 WIB",
},
{
id: 11,
nama: "Pulpen",
foto: pulpen,
kondisi: "Bagus",
disetujui: false,
kategori: "Alat Tulis",
waktu: "11:15 WIB",
},
{
id: 12,
nama: "Tipe X",
foto: tipex,
kondisi: "Bagus",
disetujui: true,
kategori: "Alat Tulis",
waktu: "11:30 WIB",
},
]);
/* TAMBAH BARANG */
const handleTambahBarang = (
barangBaru: BarangType
) => {
setBarangList((prev) => [
...prev,
barangBaru,
]);
setShowTambah(false);
};
/* FILTER + SEARCH */
const filteredBarang =
barangList.filter((barang) => {
const matchKategori =
selectedKategori === "Semua" ||
barang.kategori ===
selectedKategori;
const matchSearch =
barang.nama
.toLowerCase()
.includes(
search.toLowerCase()
);
return (
matchKategori && matchSearch
);
});
/* SAVE EDIT */
const handleSave = (
updatedBarang: BarangType
) => {
setBarangList((prev) =>
prev.map((item) =>
item.id === updatedBarang.id
? {
...updatedBarang,
waktu:
new Date().toLocaleTimeString(
"id-ID"
),
}
: item
)
);
setSelectedBarang(null);
};
return (
<div className="min-h-screen bg-[#f4f8ff] p-6">
{/* HERO */}
<BarangHero />
{/* FILTER */}
<BarangFilter
selectedKategori={
selectedKategori
}
setSelectedKategori={
setSelectedKategori
}
search={search}
setSearch={setSearch}
onTambahBarang={() =>
setShowTambah(true)
}
/>
{/* GRID */}
<BarangGrid
barangList={filteredBarang}
onEdit={setSelectedBarang}
/>
{/* MODAL EDIT */}
{selectedBarang && (
<EditBarangModal
barang={selectedBarang}
onClose={() =>
setSelectedBarang(
null
)
}
onSave={handleSave}
/>
)}
{/* MODAL TAMBAH */}
{showTambah && (
<TambahBarang
onClose={() =>
setShowTambah(false)
}
onTambah={
handleTambahBarang
}
/>
)}
</div>
);
}

15
tailwind.config.js Normal file
View File

@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'class',
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

25
tsconfig.app.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
tsconfig.node.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

7
vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})