first commit
|
|
@ -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?
|
||||
|
|
@ -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...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
@ -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>
|
||||
|
|
@ -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?
|
||||
|
|
@ -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...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.16.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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
|
|
@ -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 |
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { RouterProvider } from "react-router-dom";
|
||||
import { router } from "./Routes";
|
||||
|
||||
function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// src/Routes.tsx
|
||||
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import DashboardLayout from "./layout/DashboardLayout";
|
||||
|
||||
// ROUTES
|
||||
import Dashboard from "./routes/Dashboard";
|
||||
import MasterData from "./routes/MasterData";
|
||||
import LoginKasir from "./routes/LoginKasir";
|
||||
import TransaksiBAST from "./routes/transaksiBAST";
|
||||
import Settings from "./routes/Settings";
|
||||
import Bahasa from "./routes/bahasa";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
/* LOGIN */
|
||||
{
|
||||
path: "/login",
|
||||
element: <LoginKasir />,
|
||||
},
|
||||
|
||||
/* DASHBOARD */
|
||||
{
|
||||
path: "/",
|
||||
element: <DashboardLayout />,
|
||||
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Dashboard />,
|
||||
},
|
||||
|
||||
{
|
||||
path: "master-data",
|
||||
element: <MasterData />,
|
||||
},
|
||||
|
||||
{
|
||||
path: "transaksiBAST",
|
||||
element: <TransaksiBAST />,
|
||||
},
|
||||
|
||||
{
|
||||
path: "settings",
|
||||
element: <Settings />,
|
||||
},
|
||||
|
||||
{
|
||||
path: "bahasa",
|
||||
element: <Bahasa />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 757 KiB |
|
After Width: | Height: | Size: 750 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 821 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 523 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
|
@ -0,0 +1,14 @@
|
|||
import lang from "../../languages/language";
|
||||
export default function About() {
|
||||
return (
|
||||
<div className="bg-white rounded-2xl shadow p-6 mb-8">
|
||||
<h2 className="text-2xl font-bold mb-3">
|
||||
{lang.about}
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-600">
|
||||
{lang.aboutDesc}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import novel from "../../assets/novel.png";
|
||||
import elektronik from "../../assets/elektronik.png";
|
||||
import alat from "../../assets/alat.png";
|
||||
|
||||
import lang from "../../languages/language";
|
||||
|
||||
export default function Content() {
|
||||
const items = [
|
||||
{
|
||||
name: lang.novelName,
|
||||
image: novel,
|
||||
function: lang.novelDesc,
|
||||
},
|
||||
|
||||
{
|
||||
name: lang.electronicsName,
|
||||
image: elektronik,
|
||||
function: lang.electronicsDesc,
|
||||
},
|
||||
|
||||
{
|
||||
name: lang.stationeryName,
|
||||
image: alat,
|
||||
function: lang.stationeryDesc,
|
||||
},
|
||||
];
|
||||
|
||||
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">
|
||||
{lang.contentTitle}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
// src/components/dashboard/Hero.tsx
|
||||
|
||||
import lang from "../../languages/language";
|
||||
|
||||
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">
|
||||
|
||||
{/* LEFT */}
|
||||
<div>
|
||||
<p className="text-gray-500 text-lg">
|
||||
{lang.welcome}
|
||||
</p>
|
||||
|
||||
<h1 className="text-5xl font-extrabold text-blue-700 mt-2">
|
||||
{lang.dashboard}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* PROFILE */}
|
||||
<div className="flex items-center gap-3 bg-white rounded-full px-4 py-2 shadow border hover:shadow-lg transition">
|
||||
|
||||
{/* AVATAR */}
|
||||
<div className="w-12 h-12 bg-blue-600 rounded-full flex items-center justify-center text-white font-bold text-lg">
|
||||
Y
|
||||
</div>
|
||||
|
||||
{/* USER INFO */}
|
||||
<div>
|
||||
<h3 className="font-semibold text-black">
|
||||
Yudha
|
||||
</h3>
|
||||
|
||||
<p className="text-gray-500 text-sm">
|
||||
{lang.users}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* STAT CARDS */}
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
|
||||
{/* CARD 1 */}
|
||||
<div className="bg-white rounded-2xl p-6 shadow border hover:shadow-lg transition">
|
||||
<p className="text-gray-500 mb-2">
|
||||
{lang.todayRequest}
|
||||
</p>
|
||||
|
||||
<h2 className="text-4xl font-bold text-blue-700">
|
||||
27
|
||||
</h2>
|
||||
|
||||
<p className="text-green-600 mt-2">
|
||||
{lang.fromYesterday}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CARD 2 */}
|
||||
<div className="bg-white rounded-2xl p-6 shadow border hover:shadow-lg transition">
|
||||
<p className="text-gray-500 mb-2">
|
||||
{lang.waitingApproval}
|
||||
</p>
|
||||
|
||||
<h2 className="text-4xl font-bold text-blue-700">
|
||||
5
|
||||
</h2>
|
||||
|
||||
<p className="text-yellow-600 mt-2">
|
||||
{lang.needAction}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CARD 3 */}
|
||||
<div className="bg-white rounded-2xl p-6 shadow border hover:shadow-lg transition">
|
||||
<p className="text-gray-500 mb-2">
|
||||
{lang.totalItems}
|
||||
</p>
|
||||
|
||||
<h2 className="text-4xl font-bold text-blue-700">
|
||||
48
|
||||
</h2>
|
||||
|
||||
<p className="text-green-600 mt-2">
|
||||
{lang.lowStock}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CARD 4 */}
|
||||
<div className="bg-white rounded-2xl p-6 shadow border hover:shadow-lg transition">
|
||||
<p className="text-gray-500 mb-2">
|
||||
{lang.users}
|
||||
</p>
|
||||
|
||||
<h2 className="text-4xl font-bold text-blue-700">
|
||||
214
|
||||
</h2>
|
||||
|
||||
<p className="text-green-600 mt-2">
|
||||
{lang.studentsStaff}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// src/components/manajemenUsers/UsersFilter.tsx
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export default function UsersFilter() {
|
||||
const [selectedRole, setSelectedRole] =
|
||||
useState("Semua");
|
||||
|
||||
const roles = ["Semua", "Siswa"];
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 mb-6">
|
||||
{roles.map((role) => (
|
||||
<button
|
||||
key={role}
|
||||
onClick={() => setSelectedRole(role)}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||
selectedRole === role
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-white border hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{role}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
export default function UsersHero() {
|
||||
return (
|
||||
<div className="bg-blue-50 rounded-3xl p-8 shadow-sm border border-blue-100 mb-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-gray-500 text-lg">
|
||||
Kelola data pengguna sekolah 👥
|
||||
</p>
|
||||
<h1 className="text-5xl font-extrabold text-blue-700 mt-2">
|
||||
Manajemen Users
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<button className="bg-blue-600 text-white px-6 py-3 rounded-xl font-semibold shadow hover:bg-blue-700 transition">
|
||||
+ Tambah User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
// src/components/manajemenUsers/UsersTable.tsx
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export default function UsersTable() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: "#S001",
|
||||
nama: "Sulthan",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
{
|
||||
id: "#S002",
|
||||
nama: "Zahir",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
{
|
||||
id: "#S003",
|
||||
nama: "Arief",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Pending",
|
||||
},
|
||||
{
|
||||
id: "#S004",
|
||||
nama: "Muhammad",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
{
|
||||
id: "#S005",
|
||||
nama: "Fajar",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
{
|
||||
id: "#S006",
|
||||
nama: "Akbar",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
{
|
||||
id: "#S007",
|
||||
nama: "Yudha",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
{
|
||||
id: "#S008",
|
||||
nama: "Heru",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
];
|
||||
|
||||
// FILTER SEARCH
|
||||
const filteredUsers = users.filter((user) =>
|
||||
user.nama
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-3xl shadow-lg p-6 border">
|
||||
{/* HEADER */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold text-black">
|
||||
Data Siswa
|
||||
</h2>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari nama siswa..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="border border-gray-300 rounded-xl px-4 py-2 w-72 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* TABLE HEADER */}
|
||||
<div className="grid grid-cols-6 bg-gray-100 p-4 rounded-xl font-semibold text-gray-700 mb-4">
|
||||
<div>ID</div>
|
||||
<div>Nama</div>
|
||||
<div>Role</div>
|
||||
<div>Kelas</div>
|
||||
<div>Status</div>
|
||||
<div>Aksi</div>
|
||||
</div>
|
||||
|
||||
{/* TABLE BODY */}
|
||||
<div className="space-y-3">
|
||||
{filteredUsers.map((user, index) => (
|
||||
<div
|
||||
key={user.id}
|
||||
className={`grid grid-cols-6 items-center p-4 rounded-xl border ${
|
||||
index % 2 === 0
|
||||
? "bg-white"
|
||||
: "bg-blue-50"
|
||||
}`}
|
||||
>
|
||||
<div>{user.id}</div>
|
||||
<div>{user.nama}</div>
|
||||
<div>{user.role}</div>
|
||||
<div>{user.kelas}</div>
|
||||
|
||||
<div>
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-sm ${
|
||||
user.status === "Aktif"
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-yellow-100 text-yellow-700"
|
||||
}`}
|
||||
>
|
||||
{user.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button className="bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700">
|
||||
Edit
|
||||
</button>
|
||||
|
||||
<button className="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600">
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* EMPTY */}
|
||||
{filteredUsers.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Siswa tidak ditemukan.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// src/components/masterData/BarangFilter.tsx
|
||||
|
||||
import lang from "../../languages/language";
|
||||
|
||||
type Props = {
|
||||
selectedKategori: string;
|
||||
|
||||
setSelectedKategori: (
|
||||
kategori: string
|
||||
) => void;
|
||||
};
|
||||
|
||||
export default function BarangFilter({
|
||||
selectedKategori,
|
||||
setSelectedKategori,
|
||||
}: Props) {
|
||||
const kategoriList = [
|
||||
lang.all,
|
||||
lang.book,
|
||||
lang.electronics,
|
||||
lang.stationery,
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 mb-8 flex-wrap">
|
||||
{kategoriList.map((kategori) => (
|
||||
<button
|
||||
key={kategori}
|
||||
onClick={() =>
|
||||
setSelectedKategori(kategori)
|
||||
}
|
||||
className={`px-5 py-2 rounded-xl font-medium transition ${
|
||||
selectedKategori === kategori
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-white border border-gray-300 text-black hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{kategori}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// src/components/masterData/BarangGrid.tsx
|
||||
|
||||
import lang from "../../languages/language";
|
||||
|
||||
type Barang = {
|
||||
id: number;
|
||||
nama: string;
|
||||
foto: string;
|
||||
kondisi: string;
|
||||
tersedia: boolean;
|
||||
kategori: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
barangList: Barang[];
|
||||
onEdit: (barang: Barang) => void;
|
||||
};
|
||||
|
||||
export default function BarangGrid({
|
||||
barangList,
|
||||
onEdit,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
{barangList.map((barang) => (
|
||||
<div
|
||||
key={barang.id}
|
||||
onClick={() => onEdit(barang)}
|
||||
className="bg-white rounded-3xl shadow-lg p-4 cursor-pointer hover:shadow-2xl transition"
|
||||
>
|
||||
{/* FOTO DINAMIS BERDASARKAN KATEGORI */}
|
||||
<div
|
||||
className={`w-full overflow-hidden rounded-2xl mb-4 bg-gray-100 ${
|
||||
barang.kategori === lang.electronics
|
||||
? "aspect-square"
|
||||
: "aspect-[2/3]"
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={barang.foto}
|
||||
alt={barang.nama}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* NAMA */}
|
||||
<h2 className="text-lg font-bold text-black mb-2">
|
||||
{barang.nama}
|
||||
</h2>
|
||||
|
||||
{/* KATEGORI */}
|
||||
<p className="text-sm text-blue-600 font-medium mb-1">
|
||||
{barang.kategori}
|
||||
</p>
|
||||
{/* KONDISI */}
|
||||
<p className="text-gray-600">
|
||||
{lang.condition}: {barang.kondisi}
|
||||
</p>
|
||||
|
||||
{/* STATUS */}
|
||||
<p
|
||||
className={`text-sm font-medium ${
|
||||
barang.tersedia
|
||||
? "text-green-600"
|
||||
: "text-red-500"
|
||||
}`}
|
||||
>
|
||||
{barang.tersedia
|
||||
? lang.available
|
||||
: lang.notAvailable}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import lang from "../../languages/language";
|
||||
|
||||
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">
|
||||
{lang.manageItems}
|
||||
</h1>
|
||||
<p className="text-gray-200">
|
||||
{lang.manageItemsDesc}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
// src/components/masterData/EditBarangModal.tsx
|
||||
|
||||
import { useState } from "react";
|
||||
import lang from "../../languages/language";
|
||||
|
||||
type BarangType = {
|
||||
id: number;
|
||||
nama: string;
|
||||
foto: string;
|
||||
kondisi: string;
|
||||
tersedia: boolean;
|
||||
kategori: 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 [tersedia, setTersedia] =
|
||||
useState(barang.tersedia);
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSave({
|
||||
...barang,
|
||||
kondisi,
|
||||
tersedia,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
{/* MODAL BOX */}
|
||||
<div className="bg-white w-[420px] rounded-3xl shadow-2xl p-5">
|
||||
{/* HEADER */}
|
||||
<h2 className="text-xl font-bold text-center mb-4">
|
||||
{lang.editItem}
|
||||
</h2>
|
||||
|
||||
{/* FOTO */}
|
||||
<div className="w-28 h-28 mx-auto rounded-2xl overflow-hidden mb-3 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-1">
|
||||
{barang.nama}
|
||||
</p>
|
||||
|
||||
{/* KATEGORI */}
|
||||
<p className="text-center text-sm text-blue-600 mb-4">
|
||||
{barang.kategori}
|
||||
</p>
|
||||
|
||||
{/* FORM */}
|
||||
<div className="space-y-3">
|
||||
{/* KONDISI */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
{lang.condition}
|
||||
</label>
|
||||
<select
|
||||
value={kondisi}
|
||||
onChange={(e) =>
|
||||
setKondisi(
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
className="w-full border rounded-lg p-2"
|
||||
>
|
||||
<option value={lang.good}>
|
||||
{lang.good}
|
||||
</option>
|
||||
<option value={lang.lessGood}>
|
||||
{lang.lessGood}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* STATUS */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
{lang.status}
|
||||
</label>
|
||||
<select
|
||||
value={
|
||||
tersedia
|
||||
? "true"
|
||||
: "false"
|
||||
}
|
||||
onChange={(e) =>
|
||||
setTersedia(
|
||||
e.target.value ===
|
||||
"true"
|
||||
)
|
||||
}
|
||||
className="w-full border rounded-lg p-2"
|
||||
>
|
||||
<option value="true">
|
||||
{lang.available}
|
||||
</option>
|
||||
<option value="false">
|
||||
{lang.notAvailable}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BUTTON */}
|
||||
<div className="flex justify-end gap-3 mt-5">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-lg bg-gray-300 hover:bg-gray-400"
|
||||
>
|
||||
{lang.cancel}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700"
|
||||
>
|
||||
{lang.borrow}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700"
|
||||
>
|
||||
{lang.return}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
// src/components/sidebar/Sidebar.tsx
|
||||
|
||||
import { NavLink } from "react-router-dom";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
ClipboardList,
|
||||
Settings,
|
||||
Menu,
|
||||
Globe,
|
||||
} from "lucide-react";
|
||||
|
||||
type SidebarProps = {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (value: boolean) => void;
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/",
|
||||
icon: <LayoutDashboard size={22} />,
|
||||
},
|
||||
|
||||
{
|
||||
name: "Master Barang",
|
||||
path: "/master-data",
|
||||
icon: <ClipboardList size={22} />,
|
||||
},
|
||||
|
||||
{
|
||||
name: "Settings",
|
||||
path: "/settings",
|
||||
icon: <Settings size={22} />,
|
||||
},
|
||||
|
||||
{
|
||||
name: "Bahasa",
|
||||
path: "/bahasa",
|
||||
icon: <Globe size={22} />,
|
||||
},
|
||||
|
||||
{
|
||||
name: "transaksiBAST",
|
||||
path: "/transaksiBAST",
|
||||
icon: <ClipboardList size={22} />,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Sidebar({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}: SidebarProps) {
|
||||
return (
|
||||
<aside
|
||||
className={`fixed top-0 left-0 h-screen bg-black text-white shadow-2xl transition-all duration-300 z-50
|
||||
${isOpen ? "w-64" : "w-20"}`}
|
||||
>
|
||||
{/* HEADER */}
|
||||
<div className="h-24 px-4 border-b border-gray-800 flex items-center justify-between">
|
||||
{isOpen && (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-blue-500 leading-none">
|
||||
SCHOOL
|
||||
<br />
|
||||
LOAN
|
||||
</h1>
|
||||
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
Sistem Peminjaman
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TOGGLE */}
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="bg-blue-600 hover:bg-blue-700 transition p-3 rounded-2xl"
|
||||
>
|
||||
<Menu size={22} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* MENU */}
|
||||
<nav className="mt-6 px-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-4 px-5 py-4 rounded-2xl transition-all duration-200 ${
|
||||
isActive
|
||||
? "bg-blue-600 text-white shadow-lg"
|
||||
: "text-gray-300 hover:bg-gray-900 hover:text-white"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{item.icon}
|
||||
|
||||
{isOpen && (
|
||||
<span className="text-lg font-medium">
|
||||
{item.name}
|
||||
</span>
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* FOOTER */}
|
||||
{isOpen && (
|
||||
<div className="absolute bottom-5 left-5 text-sm text-gray-500">
|
||||
© 2026 School Loan
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
|
@ -0,0 +1,425 @@
|
|||
const language =
|
||||
localStorage.getItem("language") || "id";
|
||||
|
||||
const lang = {
|
||||
// HERO
|
||||
welcome:
|
||||
language === "id"
|
||||
? "Selamat datang kembali 👋"
|
||||
: "Welcome back 👋",
|
||||
|
||||
dashboard:
|
||||
language === "id"
|
||||
? "Dashboard"
|
||||
: "Dashboard",
|
||||
|
||||
users:
|
||||
language === "id"
|
||||
? "Pengguna"
|
||||
: "Users",
|
||||
|
||||
todayRequest:
|
||||
language === "id"
|
||||
? "Permintaan Hari Ini"
|
||||
: "Today's Requests",
|
||||
|
||||
fromYesterday:
|
||||
language === "id"
|
||||
? "+5 dari kemarin"
|
||||
: "+5 from yesterday",
|
||||
|
||||
waitingApproval:
|
||||
language === "id"
|
||||
? "Menunggu Persetujuan"
|
||||
: "Waiting Approval",
|
||||
|
||||
needAction:
|
||||
language === "id"
|
||||
? "Perlu tindakan segera"
|
||||
: "Need immediate action",
|
||||
|
||||
totalItems:
|
||||
language === "id"
|
||||
? "Total Barang"
|
||||
: "Total Items",
|
||||
|
||||
lowStock:
|
||||
language === "id"
|
||||
? "3 stok hampir habis"
|
||||
: "3 items almost out of stock",
|
||||
|
||||
activeUsers:
|
||||
language === "id"
|
||||
? "Pengguna Aktif"
|
||||
: "Active Users",
|
||||
|
||||
studentsStaff:
|
||||
language === "id"
|
||||
? "Siswa & petugas"
|
||||
: "Students & staff",
|
||||
|
||||
// ABOUT
|
||||
about:
|
||||
language === "id"
|
||||
? "Tentang Sistem"
|
||||
: "About System",
|
||||
|
||||
aboutDesc:
|
||||
language === "id"
|
||||
? "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."
|
||||
|
||||
: "This school cashier and borrowing system application is designed to simplify sales transaction management, inventory recording, and borrowing processes for school equipment such as books, stationery, and other supporting tools. With a modern and integrated system, users can manage stock, perform transactions, monitor borrowing data, and view reports more quickly, accurately, and securely. The main dashboard provides easy access to all important features, making school operations more organized, efficient, and professional.",
|
||||
|
||||
// CONTENT
|
||||
contentTitle:
|
||||
language === "id"
|
||||
? "Barang Barang"
|
||||
: "Items",
|
||||
|
||||
novelName:
|
||||
language === "id"
|
||||
? "Novel"
|
||||
: "Novel",
|
||||
|
||||
novelDesc:
|
||||
language === "id"
|
||||
? "rekreatif (hiburan), didaktif (mendidik), moralitas (penanaman nilai baik-buruk), religius (ajaran agama), dan estetis (keindahan bahasa/cerita)."
|
||||
|
||||
: "recreational (entertainment), educational, moral values development, religious teachings, and aesthetic appreciation through storytelling.",
|
||||
|
||||
electronicsName:
|
||||
language === "id"
|
||||
? "Elektronik"
|
||||
: "Electronics",
|
||||
|
||||
electronicsDesc:
|
||||
language === "id"
|
||||
? "Elektronik digunakan untuk mempermudah pekerjaan manusia, memberikan hiburan, membantu komunikasi, serta meningkatkan efisiensi dan keamanan dalam kehidupan sehari-hari."
|
||||
|
||||
: "Electronics are used to simplify human tasks, provide entertainment, support communication, and improve efficiency and safety in daily life.",
|
||||
|
||||
stationeryName:
|
||||
language === "id"
|
||||
? "Alat Tulis"
|
||||
: "Stationery",
|
||||
|
||||
stationeryDesc:
|
||||
language === "id"
|
||||
? "mencatat informasi, mendukung administrasi perkantoran, menunjang kegiatan belajar-mengajar, serta memfasilitasi ekspresi kreatif seperti menggambar dan merancang."
|
||||
|
||||
: "used for recording information, supporting office administration, assisting learning activities, and facilitating creative expression such as drawing and designing.",
|
||||
|
||||
// MASTER DATA
|
||||
all:
|
||||
language === "id"
|
||||
? "Semua"
|
||||
: "All",
|
||||
|
||||
good:
|
||||
language === "id"
|
||||
? "Bagus"
|
||||
: "Good",
|
||||
|
||||
lessGood:
|
||||
language === "id"
|
||||
? "Kurang Bagus"
|
||||
: "Less Good",
|
||||
|
||||
book:
|
||||
language === "id"
|
||||
? "Buku"
|
||||
: "Book",
|
||||
|
||||
electronics:
|
||||
language === "id"
|
||||
? "Elektronik"
|
||||
: "Electronics",
|
||||
|
||||
stationery:
|
||||
language === "id"
|
||||
? "Alat Tulis"
|
||||
: "Stationery",
|
||||
|
||||
//BARANG GRID
|
||||
condition:
|
||||
language === "id"
|
||||
? "Kondisi"
|
||||
: "Condition",
|
||||
|
||||
available:
|
||||
language === "id"
|
||||
? "Bisa Dipinjam"
|
||||
: "Available",
|
||||
|
||||
notAvailable:
|
||||
language === "id"
|
||||
? "Tidak Tersedia"
|
||||
: "Not Available",
|
||||
|
||||
//BARANG HERO
|
||||
manageItems:
|
||||
language === "id"
|
||||
? "Kelola Barang Sekolah"
|
||||
: "Manage School Items",
|
||||
|
||||
manageItemsDesc:
|
||||
language === "id"
|
||||
? "Atur stok, harga, dan data barang sekolah dengan mudah."
|
||||
|
||||
: "Manage stock, prices, and school item data easily.",
|
||||
|
||||
//EDIT BARANG
|
||||
editItem:
|
||||
language === "id"
|
||||
? "Edit Barang"
|
||||
: "Edit Item",
|
||||
|
||||
status:
|
||||
language === "id"
|
||||
? "Status"
|
||||
: "Status",
|
||||
|
||||
cancel:
|
||||
language === "id"
|
||||
? "Batal"
|
||||
: "Cancel",
|
||||
|
||||
borrow:
|
||||
language === "id"
|
||||
? "Pinjam"
|
||||
: "Borrow",
|
||||
|
||||
return:
|
||||
language === "id"
|
||||
? "Kembalikan"
|
||||
: "Return",
|
||||
|
||||
//setting
|
||||
profileUser:
|
||||
language === "id"
|
||||
? "Profile User"
|
||||
: "User Profile",
|
||||
|
||||
manageAccount:
|
||||
language === "id"
|
||||
? "Kelola profile akun anda"
|
||||
: "Manage your account profile",
|
||||
|
||||
light:
|
||||
language === "id"
|
||||
? "☀️ Terang"
|
||||
: "☀️ Light",
|
||||
|
||||
dark:
|
||||
language === "id"
|
||||
? "🌙 Gelap"
|
||||
: "🌙 Dark",
|
||||
|
||||
menuSettings:
|
||||
language === "id"
|
||||
? "Menu Settings"
|
||||
: "Settings Menu",
|
||||
|
||||
myProfile:
|
||||
language === "id"
|
||||
? "Profil Saya"
|
||||
: "My Profile",
|
||||
|
||||
changePassword:
|
||||
language === "id"
|
||||
? "Ubah Password"
|
||||
: "Change Password",
|
||||
|
||||
notification:
|
||||
language === "id"
|
||||
? "Notifikasi"
|
||||
: "Notifications",
|
||||
|
||||
security:
|
||||
language === "id"
|
||||
? "Keamanan"
|
||||
: "Security",
|
||||
|
||||
languageMenu:
|
||||
language === "id"
|
||||
? "Bahasa"
|
||||
: "Language",
|
||||
|
||||
theme:
|
||||
language === "id"
|
||||
? "Tema"
|
||||
: "Theme",
|
||||
|
||||
loginActivity:
|
||||
language === "id"
|
||||
? "Aktivitas Login"
|
||||
: "Login Activity",
|
||||
|
||||
help:
|
||||
language === "id"
|
||||
? "Bantuan"
|
||||
: "Help",
|
||||
|
||||
logout:
|
||||
language === "id"
|
||||
? "Logout"
|
||||
: "Logout",
|
||||
|
||||
profilePhoto:
|
||||
language === "id"
|
||||
? "Foto Profil"
|
||||
: "Profile Photo",
|
||||
|
||||
changePhoto:
|
||||
language === "id"
|
||||
? "Ubah Foto"
|
||||
: "Change Photo",
|
||||
|
||||
photoInfo:
|
||||
language === "id"
|
||||
? "PNG, JPG maksimal 2MB"
|
||||
: "PNG, JPG max 2MB",
|
||||
|
||||
fullName:
|
||||
language === "id"
|
||||
? "Nama Lengkap"
|
||||
: "Full Name",
|
||||
|
||||
phone:
|
||||
language === "id"
|
||||
? "No. Telepon"
|
||||
: "Phone Number",
|
||||
|
||||
address:
|
||||
language === "id"
|
||||
? "Alamat"
|
||||
: "Address",
|
||||
|
||||
role:
|
||||
language === "id"
|
||||
? "Role / Jabatan"
|
||||
: "Role / Position",
|
||||
|
||||
saveChanges:
|
||||
language === "id"
|
||||
? "Simpan Perubahan"
|
||||
: "Save Changes",
|
||||
|
||||
reset:
|
||||
language === "id"
|
||||
? "Reset"
|
||||
: "Reset",
|
||||
|
||||
accountInfo:
|
||||
language === "id"
|
||||
? "Informasi Akun"
|
||||
: "Account Information",
|
||||
|
||||
accountId:
|
||||
language === "id"
|
||||
? "ID Akun"
|
||||
: "Account ID",
|
||||
|
||||
lastLogin:
|
||||
language === "id"
|
||||
? "Terakhir Login"
|
||||
: "Last Login",
|
||||
|
||||
device:
|
||||
language === "id"
|
||||
? "Perangkat"
|
||||
: "Device",
|
||||
|
||||
activitySummary:
|
||||
language === "id"
|
||||
? "Ringkasan Aktivitas"
|
||||
: "Activity Summary",
|
||||
|
||||
totalLogin:
|
||||
language === "id"
|
||||
? "Total Login"
|
||||
: "Total Logins",
|
||||
|
||||
successfulLogin:
|
||||
language === "id"
|
||||
? "Login Berhasil"
|
||||
: "Successful Login",
|
||||
|
||||
failedLogin:
|
||||
language === "id"
|
||||
? "Login Gagal"
|
||||
: "Failed Login",
|
||||
|
||||
times:
|
||||
language === "id"
|
||||
? "kali"
|
||||
: "times",
|
||||
|
||||
//TRANSAKSI BAST
|
||||
bastTransaction:
|
||||
language === "id"
|
||||
? "Transaksi BAST"
|
||||
: "BAST Transactions",
|
||||
|
||||
bastDesc:
|
||||
language === "id"
|
||||
? "Data transaksi peminjaman barang sekolah"
|
||||
: "School item borrowing transaction data",
|
||||
|
||||
searchTransaction:
|
||||
language === "id"
|
||||
? "Cari transaksi..."
|
||||
: "Search transactions...",
|
||||
|
||||
addBast:
|
||||
language === "id"
|
||||
? "Tambah BAST"
|
||||
: "Add BAST",
|
||||
|
||||
receiver:
|
||||
language === "id"
|
||||
? "Penerima"
|
||||
: "Receiver",
|
||||
|
||||
item:
|
||||
language === "id"
|
||||
? "Barang"
|
||||
: "Item",
|
||||
|
||||
date:
|
||||
language === "id"
|
||||
? "Tanggal"
|
||||
: "Date",
|
||||
|
||||
time:
|
||||
language === "id"
|
||||
? "Jam"
|
||||
: "Time",
|
||||
|
||||
action:
|
||||
language === "id"
|
||||
? "Aksi"
|
||||
: "Action",
|
||||
|
||||
agree:
|
||||
language === "id"
|
||||
? "Setuju"
|
||||
: "Approved",
|
||||
|
||||
disagree:
|
||||
language === "id"
|
||||
? "Tidak Setuju"
|
||||
: "Rejected",
|
||||
|
||||
detail:
|
||||
language === "id"
|
||||
? "Detail"
|
||||
: "Detail",
|
||||
|
||||
showingData:
|
||||
language === "id"
|
||||
? "Menampilkan 1 sampai 5 dari 15 data"
|
||||
: "Showing 1 to 5 of 15 data",
|
||||
};
|
||||
|
||||
export default lang;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,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 p-6">
|
||||
<Hero />
|
||||
<About />
|
||||
<Content />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,360 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
ClipboardList,
|
||||
Filter,
|
||||
MapPin,
|
||||
Package,
|
||||
CheckCircle,
|
||||
} from "lucide-react";
|
||||
|
||||
type Barang = {
|
||||
id: number;
|
||||
nama: string;
|
||||
kategori: string;
|
||||
lokasi: string;
|
||||
kondisi: string;
|
||||
statusBarang: string;
|
||||
statusBAST: string;
|
||||
riwayat: string;
|
||||
};
|
||||
|
||||
export default function LaporanMonitoring() {
|
||||
const [filterKategori, setFilterKategori] =
|
||||
useState("Semua");
|
||||
|
||||
const [filterLokasi, setFilterLokasi] =
|
||||
useState("Semua");
|
||||
|
||||
const [
|
||||
filterStatusBarang,
|
||||
setFilterStatusBarang,
|
||||
] = useState("Semua");
|
||||
|
||||
const [
|
||||
filterStatusBAST,
|
||||
setFilterStatusBAST,
|
||||
] = useState("Semua");
|
||||
|
||||
const dataBarang: Barang[] = [
|
||||
{
|
||||
id: 1,
|
||||
nama: "Laptop ASUS",
|
||||
kategori: "Elektronik",
|
||||
lokasi: "Lab Komputer",
|
||||
kondisi: "Bagus",
|
||||
statusBarang: "Dipinjam",
|
||||
statusBAST: "Selesai",
|
||||
riwayat:
|
||||
"Diserahkan ke siswa kelas XII",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
nama: "Proyektor Epson",
|
||||
kategori: "Elektronik",
|
||||
lokasi: "Ruang Guru",
|
||||
kondisi: "Baik",
|
||||
statusBarang: "Tersedia",
|
||||
statusBAST: "Pending",
|
||||
riwayat:
|
||||
"Belum diserahterimakan",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
nama: "Buku Matematika",
|
||||
kategori: "Buku",
|
||||
lokasi: "Perpustakaan",
|
||||
kondisi: "Bagus",
|
||||
statusBarang: "Tersedia",
|
||||
statusBAST: "Selesai",
|
||||
riwayat:
|
||||
"Sudah diterima siswa",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nama: "Kursi Lipat",
|
||||
kategori: "Furniture",
|
||||
lokasi: "Gudang",
|
||||
kondisi: "Kurang Bagus",
|
||||
statusBarang: "Rusak",
|
||||
statusBAST: "Proses",
|
||||
riwayat:
|
||||
"Sedang diperbaiki",
|
||||
},
|
||||
];
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
return dataBarang.filter((item) => {
|
||||
return (
|
||||
(filterKategori === "Semua" ||
|
||||
item.kategori ===
|
||||
filterKategori) &&
|
||||
(filterLokasi === "Semua" ||
|
||||
item.lokasi ===
|
||||
filterLokasi) &&
|
||||
(filterStatusBarang ===
|
||||
"Semua" ||
|
||||
item.statusBarang ===
|
||||
filterStatusBarang) &&
|
||||
(filterStatusBAST ===
|
||||
"Semua" ||
|
||||
item.statusBAST ===
|
||||
filterStatusBAST)
|
||||
);
|
||||
});
|
||||
}, [
|
||||
filterKategori,
|
||||
filterLokasi,
|
||||
filterStatusBarang,
|
||||
filterStatusBAST,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-6">
|
||||
{/* HEADER */}
|
||||
<div className="bg-white rounded-3xl shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<ClipboardList
|
||||
className="text-blue-600"
|
||||
size={30}
|
||||
/>
|
||||
|
||||
<h1 className="text-3xl font-bold text-gray-800">
|
||||
Laporan & Monitoring BAST
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-500">
|
||||
Monitoring barang, status
|
||||
BAST, lokasi barang, dan
|
||||
riwayat serah terima.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* FILTER */}
|
||||
<div className="bg-white rounded-3xl shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center gap-2 mb-5">
|
||||
<Filter className="text-blue-600" />
|
||||
|
||||
<h2 className="text-xl font-semibold">
|
||||
Filter Laporan
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<select
|
||||
value={filterKategori}
|
||||
onChange={(e) =>
|
||||
setFilterKategori(
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
className="border rounded-xl p-3"
|
||||
>
|
||||
<option>Semua</option>
|
||||
<option>Buku</option>
|
||||
<option>Elektronik</option>
|
||||
<option>Furniture</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={filterLokasi}
|
||||
onChange={(e) =>
|
||||
setFilterLokasi(
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
className="border rounded-xl p-3"
|
||||
>
|
||||
<option>Semua</option>
|
||||
<option>Perpustakaan</option>
|
||||
<option>Lab Komputer</option>
|
||||
<option>Ruang Guru</option>
|
||||
<option>Gudang</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={filterStatusBarang}
|
||||
onChange={(e) =>
|
||||
setFilterStatusBarang(
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
className="border rounded-xl p-3"
|
||||
>
|
||||
<option>Semua</option>
|
||||
<option>Tersedia</option>
|
||||
<option>Dipinjam</option>
|
||||
<option>Rusak</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={filterStatusBAST}
|
||||
onChange={(e) =>
|
||||
setFilterStatusBAST(
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
className="border rounded-xl p-3"
|
||||
>
|
||||
<option>Semua</option>
|
||||
<option>Selesai</option>
|
||||
<option>Pending</option>
|
||||
<option>Proses</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CARD */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 mb-6">
|
||||
<div className="bg-white p-5 rounded-3xl shadow-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<Package className="text-blue-600" />
|
||||
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Total Barang
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold">
|
||||
{filteredData.length}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-5 rounded-3xl shadow-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle className="text-green-600" />
|
||||
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm">
|
||||
BAST Selesai
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold">
|
||||
{
|
||||
filteredData.filter(
|
||||
(item) =>
|
||||
item.statusBAST ===
|
||||
"Selesai"
|
||||
).length
|
||||
}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-5 rounded-3xl shadow-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<MapPin className="text-orange-500" />
|
||||
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Lokasi Aktif
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-bold">
|
||||
{
|
||||
new Set(
|
||||
filteredData.map(
|
||||
(item) =>
|
||||
item.lokasi
|
||||
)
|
||||
).size
|
||||
}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TABLE */}
|
||||
<div className="bg-white rounded-3xl shadow-lg overflow-hidden">
|
||||
<div className="p-5 border-b">
|
||||
<h2 className="text-xl font-semibold text-gray-700">
|
||||
Data Laporan Barang
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-blue-600 text-white">
|
||||
<tr>
|
||||
<th className="p-4">
|
||||
Nama Barang
|
||||
</th>
|
||||
|
||||
<th className="p-4">
|
||||
Kategori
|
||||
</th>
|
||||
|
||||
<th className="p-4">
|
||||
Lokasi
|
||||
</th>
|
||||
|
||||
<th className="p-4">
|
||||
Kondisi
|
||||
</th>
|
||||
|
||||
<th className="p-4">
|
||||
Status Barang
|
||||
</th>
|
||||
|
||||
<th className="p-4">
|
||||
Status BAST
|
||||
</th>
|
||||
|
||||
<th className="p-4">
|
||||
Riwayat
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{filteredData.map(
|
||||
(item) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className="border-b hover:bg-gray-50"
|
||||
>
|
||||
<td className="p-4 font-medium">
|
||||
{item.nama}
|
||||
</td>
|
||||
|
||||
<td className="p-4">
|
||||
{item.kategori}
|
||||
</td>
|
||||
|
||||
<td className="p-4">
|
||||
{item.lokasi}
|
||||
</td>
|
||||
|
||||
<td className="p-4">
|
||||
{item.kondisi}
|
||||
</td>
|
||||
|
||||
<td className="p-4">
|
||||
{
|
||||
item.statusBarang
|
||||
}
|
||||
</td>
|
||||
|
||||
<td className="p-4">
|
||||
{
|
||||
item.statusBAST
|
||||
}
|
||||
</td>
|
||||
|
||||
<td className="p-4">
|
||||
{item.riwayat}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { useState } from "react";
|
||||
|
||||
export default function LoginKasir() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
function handleLogin(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
// username & password sederhana
|
||||
if (username === "users" && password === "users") {
|
||||
localStorage.setItem("isLogin", "true");
|
||||
|
||||
window.location.href = "/";
|
||||
} else {
|
||||
alert("Username atau Password salah");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: "100vh",
|
||||
background: "#f1f5f9",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleLogin}
|
||||
style={{
|
||||
background: "white",
|
||||
padding: "40px",
|
||||
borderRadius: "20px",
|
||||
width: "350px",
|
||||
boxShadow: "0 5px 20px rgba(0,0,0,0.1)",
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginBottom: "30px",
|
||||
}}
|
||||
>
|
||||
Login users
|
||||
</h1>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "14px",
|
||||
marginBottom: "15px",
|
||||
borderRadius: "10px",
|
||||
border: "1px solid #ccc",
|
||||
}}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "14px",
|
||||
marginBottom: "20px",
|
||||
borderRadius: "10px",
|
||||
border: "1px solid #ccc",
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "14px",
|
||||
border: "none",
|
||||
borderRadius: "10px",
|
||||
background: "#2563eb",
|
||||
color: "white",
|
||||
fontWeight: "bold",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
|
||||
<p
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: "20px",
|
||||
color: "#6b7280",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
username: users <br />
|
||||
password: users
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
// src/routes/ManajemenUsers.tsx
|
||||
|
||||
// COMPONENTS
|
||||
import UsersHero from "../components/manajemenUsers/UserHero";
|
||||
import UsersFilter from "../components/manajemenUsers/UserFilter";
|
||||
import UsersTable from "../components/manajemenUsers/UserTable";
|
||||
|
||||
export default function ManajemenUsers() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#f4f8ff] p-6">
|
||||
{/* HERO */}
|
||||
<UsersHero />
|
||||
|
||||
{/* FILTER */}
|
||||
<UsersFilter />
|
||||
|
||||
{/* TABLE */}
|
||||
<UsersTable />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
// src/routes/MasterData.tsx
|
||||
|
||||
import { useState } from "react";
|
||||
import lang from "../languages/language";
|
||||
|
||||
// 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";
|
||||
|
||||
// TYPE
|
||||
type BarangType = {
|
||||
id: number;
|
||||
nama: string;
|
||||
foto: string;
|
||||
kondisi: string;
|
||||
tersedia: boolean;
|
||||
kategori: string;
|
||||
};
|
||||
|
||||
export default function MasterData() {
|
||||
// STATE MODAL
|
||||
const [selectedBarang, setSelectedBarang] =
|
||||
useState<BarangType | null>(null);
|
||||
|
||||
// STATE FILTER
|
||||
const [selectedKategori, setSelectedKategori] =
|
||||
useState(lang.all);
|
||||
|
||||
// DATA BARANG
|
||||
const [barangList, setBarangList] = useState<
|
||||
BarangType[]
|
||||
>([
|
||||
{
|
||||
id: 1,
|
||||
nama:
|
||||
localStorage.getItem("language") === "id"
|
||||
? "Buku Matematika"
|
||||
: "Math Book",
|
||||
foto: matematika,
|
||||
kondisi: lang.good,
|
||||
tersedia: true,
|
||||
kategori: lang.book,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
nama:
|
||||
localStorage.getItem("language") === "id"
|
||||
? "Buku Pulang"
|
||||
: "Going Home Book",
|
||||
foto: pulang,
|
||||
kondisi: lang.lessGood,
|
||||
tersedia: false,
|
||||
kategori: lang.book,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
nama:
|
||||
localStorage.getItem("language") === "id"
|
||||
? "Buku Sebuah Mimpi"
|
||||
: "A Dream Book",
|
||||
foto: buku,
|
||||
kondisi: lang.good,
|
||||
tersedia: true,
|
||||
kategori: lang.book,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nama:
|
||||
localStorage.getItem("language") === "id"
|
||||
? "Filsafat: Sebuah Pengantar"
|
||||
: "Philosophy: An Introduction",
|
||||
foto: filsafat,
|
||||
kondisi: lang.good,
|
||||
tersedia: true,
|
||||
kategori: lang.book,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
nama:
|
||||
localStorage.getItem("language") === "id"
|
||||
? "Laptop"
|
||||
: "Laptop",
|
||||
foto: laptop,
|
||||
kondisi: lang.good,
|
||||
tersedia: true,
|
||||
kategori: lang.electronics,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
nama:
|
||||
localStorage.getItem("language") === "id"
|
||||
? "Komputer"
|
||||
: "Computer",
|
||||
foto: komputer,
|
||||
kondisi: lang.good,
|
||||
tersedia: true,
|
||||
kategori: lang.electronics,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
nama:
|
||||
localStorage.getItem("language") === "id"
|
||||
? "Lampu"
|
||||
: "Lamp",
|
||||
foto: lampu,
|
||||
kondisi: lang.good,
|
||||
tersedia: true,
|
||||
kategori: lang.electronics,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
nama:
|
||||
localStorage.getItem("language") === "id"
|
||||
? "Setrika"
|
||||
: "Iron",
|
||||
foto: setrika,
|
||||
kondisi: lang.good,
|
||||
tersedia: true,
|
||||
kategori: lang.electronics,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
nama:
|
||||
localStorage.getItem("language") === "id"
|
||||
? "Tas"
|
||||
: "Bag",
|
||||
foto: tas,
|
||||
kondisi: lang.good,
|
||||
tersedia: true,
|
||||
kategori: lang.stationery,
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
nama:
|
||||
localStorage.getItem("language") === "id"
|
||||
? "Pensil"
|
||||
: "Pencil",
|
||||
foto: pensil,
|
||||
kondisi: lang.good,
|
||||
tersedia: true,
|
||||
kategori: lang.stationery,
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
nama:
|
||||
localStorage.getItem("language") === "id"
|
||||
? "Pulpen"
|
||||
: "Pen",
|
||||
foto: pulpen,
|
||||
kondisi: lang.good,
|
||||
tersedia: true,
|
||||
kategori: lang.stationery,
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
nama:
|
||||
localStorage.getItem("language") === "id"
|
||||
? "Tipe X"
|
||||
: "Correction Pen",
|
||||
foto: tipex,
|
||||
kondisi: lang.good,
|
||||
tersedia: true,
|
||||
kategori: lang.stationery,
|
||||
}
|
||||
]);
|
||||
|
||||
// FILTER DATA
|
||||
const filteredBarang = barangList.filter(
|
||||
(barang) =>
|
||||
selectedKategori === lang.all ||
|
||||
barang.kategori === selectedKategori
|
||||
);
|
||||
|
||||
// SAVE EDIT
|
||||
const handleSave = (
|
||||
updatedBarang: BarangType
|
||||
) => {
|
||||
setBarangList((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === updatedBarang.id
|
||||
? updatedBarang
|
||||
: item
|
||||
)
|
||||
);
|
||||
|
||||
setSelectedBarang(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#f4f8ff] p-6">
|
||||
{/* HERO */}
|
||||
<BarangHero />
|
||||
|
||||
{/* FILTER */}
|
||||
<BarangFilter
|
||||
selectedKategori={selectedKategori}
|
||||
setSelectedKategori={setSelectedKategori}
|
||||
/>
|
||||
|
||||
{/* GRID */}
|
||||
<BarangGrid
|
||||
barangList={filteredBarang}
|
||||
onEdit={setSelectedBarang}
|
||||
/>
|
||||
|
||||
{/* MODAL */}
|
||||
{selectedBarang && (
|
||||
<EditBarangModal
|
||||
barang={selectedBarang}
|
||||
onClose={() =>
|
||||
setSelectedBarang(null)
|
||||
}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,333 @@
|
|||
import { useState } from "react";
|
||||
import lang from "../languages/language";
|
||||
import {
|
||||
User,
|
||||
Lock,
|
||||
Bell,
|
||||
Shield,
|
||||
Globe,
|
||||
Palette,
|
||||
History,
|
||||
HelpCircle,
|
||||
LogOut,
|
||||
Camera,
|
||||
} from "lucide-react";
|
||||
|
||||
export default function Settings() {
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`min-h-screen p-6 transition-all duration-300 ${
|
||||
darkMode
|
||||
? "bg-[#0F172A] text-white"
|
||||
: "bg-gray-100 text-black"
|
||||
}`}
|
||||
>
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold flex items-center gap-3">
|
||||
<User className="text-purple-600" size={35} />
|
||||
{lang.profileUser}
|
||||
</h1>
|
||||
|
||||
<p
|
||||
className={`mt-2 ${
|
||||
darkMode ? "text-gray-300" : "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{lang.manageAccount}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* BUTTON DARK MODE */}
|
||||
<button
|
||||
onClick={() => setDarkMode(!darkMode)}
|
||||
className={`px-5 py-3 rounded-2xl font-semibold transition-all ${
|
||||
darkMode
|
||||
? "bg-yellow-400 text-black"
|
||||
: "bg-[#0F172A] text-white"
|
||||
}`}
|
||||
>
|
||||
{darkMode
|
||||
? lang.light
|
||||
: lang.dark}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* CONTENT */}
|
||||
<div className="grid grid-cols-12 gap-6">
|
||||
|
||||
{/* LEFT MENU */}
|
||||
<div
|
||||
className={`col-span-3 rounded-3xl p-5 shadow-sm ${
|
||||
darkMode ? "bg-[#1E293B]" : "bg-white"
|
||||
}`}
|
||||
>
|
||||
<h2 className="font-bold text-xl mb-5">
|
||||
{lang.menuSettings}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
|
||||
<button className="w-full flex items-center gap-3 bg-blue-50 text-blue-600 p-4 rounded-2xl font-semibold">
|
||||
<User size={20} />
|
||||
{lang.myProfile}
|
||||
</button>
|
||||
|
||||
<button className="w-full flex items-center gap-3 p-4 rounded-2xl hover:bg-gray-100 hover:text-black">
|
||||
<Lock size={20} />
|
||||
{lang.changePassword}
|
||||
</button>
|
||||
|
||||
<button className="w-full flex items-center gap-3 p-4 rounded-2xl hover:bg-gray-100 hover:text-black">
|
||||
<Bell size={20} />
|
||||
{lang.notification}
|
||||
</button>
|
||||
|
||||
<button className="w-full flex items-center gap-3 p-4 rounded-2xl hover:bg-gray-100 hover:text-black">
|
||||
<Shield size={20} />
|
||||
{lang.security}
|
||||
</button>
|
||||
|
||||
<button className="w-full flex items-center gap-3 p-4 rounded-2xl hover:bg-gray-100 hover:text-black">
|
||||
<Globe size={20} />
|
||||
{lang.languageMenu}
|
||||
</button>
|
||||
|
||||
<button className="w-full flex items-center gap-3 p-4 rounded-2xl hover:bg-gray-100 hover:text-black">
|
||||
<Palette size={20} />
|
||||
{lang.theme}
|
||||
</button>
|
||||
|
||||
<button className="w-full flex items-center gap-3 p-4 rounded-2xl hover:bg-gray-100 hover:text-black">
|
||||
<History size={20} />
|
||||
{lang.loginActivity}
|
||||
</button>
|
||||
|
||||
<button className="w-full flex items-center gap-3 p-4 rounded-2xl hover:bg-gray-100 hover:text-black">
|
||||
<HelpCircle size={20} />
|
||||
{lang.help}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="border-t mt-8 pt-5 border-gray-600">
|
||||
<button className="flex items-center gap-3 text-red-500 font-semibold">
|
||||
<LogOut size={20} />
|
||||
{lang.logout}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CENTER */}
|
||||
<div
|
||||
className={`col-span-6 rounded-3xl p-6 shadow-sm ${
|
||||
darkMode ? "bg-[#1E293B]" : "bg-white"
|
||||
}`}
|
||||
>
|
||||
{/* PROFILE */}
|
||||
<div className="mb-8">
|
||||
|
||||
<h2 className="font-bold text-lg mb-4">
|
||||
{lang.profilePhoto}
|
||||
</h2>
|
||||
|
||||
<div className="flex items-center gap-5">
|
||||
|
||||
<div className="w-24 h-24 rounded-full bg-blue-600 flex items-center justify-center text-white text-5xl font-bold">
|
||||
Y
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
<button className="flex items-center gap-2 border px-4 py-2 rounded-xl">
|
||||
<Camera size={18} />
|
||||
{lang.changePhoto}
|
||||
</button>
|
||||
|
||||
<p
|
||||
className={`text-sm mt-2 ${
|
||||
darkMode
|
||||
? "text-gray-300"
|
||||
: "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{lang.photoInfo}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FORM */}
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
|
||||
<div>
|
||||
<label className="font-semibold text-sm">
|
||||
{lang.fullName}
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="Yudha"
|
||||
className="w-full border p-4 rounded-2xl mt-2 text-black"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-semibold text-sm">
|
||||
{lang.phone}
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="081234567890"
|
||||
className="w-full border p-4 rounded-2xl mt-2 text-black"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-semibold text-sm">
|
||||
email
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="email"
|
||||
defaultValue="yudha@gmail.com"
|
||||
className="w-full border p-4 rounded-2xl mt-2 text-black"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-semibold text-sm">
|
||||
{lang.address}
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
defaultValue="Jl. Merdeka No.123"
|
||||
className="w-full border p-4 rounded-2xl mt-2 text-black"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-semibold text-sm">
|
||||
{lang.role}
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="Users Dashboard"
|
||||
className="w-full border p-4 rounded-2xl mt-2 text-black"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="font-semibold text-sm">
|
||||
Username
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="yudha_users"
|
||||
className="w-full border p-4 rounded-2xl mt-2 text-black"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* BUTTON */}
|
||||
<div className="flex gap-4 mt-8">
|
||||
|
||||
<button className="bg-blue-600 text-white px-6 py-3 rounded-2xl font-semibold">
|
||||
{lang.saveChanges}
|
||||
</button>
|
||||
|
||||
<button className="border px-6 py-3 rounded-2xl font-semibold">
|
||||
{lang.reset}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT */}
|
||||
<div className="col-span-3 space-y-6">
|
||||
|
||||
{/* INFO */}
|
||||
<div
|
||||
className={`rounded-3xl p-5 shadow-sm ${
|
||||
darkMode ? "bg-[#1E293B]" : "bg-white"
|
||||
}`}
|
||||
>
|
||||
<h2 className="font-bold text-xl mb-5">
|
||||
{lang.accountInfo}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-5 text-sm">
|
||||
|
||||
<div>
|
||||
<p className="text-gray-400">{lang.accountId}</p>
|
||||
<p className="font-semibold">USR-0001</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-gray-400">{lang.lastLogin}</p>
|
||||
<p className="font-semibold">
|
||||
15 Mei 2026
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-gray-400">{lang.device}</p>
|
||||
<p className="font-semibold">
|
||||
Chrome - Windows
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-gray-400">Role</p>
|
||||
<p className="font-semibold">
|
||||
Users Dashboard
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AKTIVITAS */}
|
||||
<div
|
||||
className={`rounded-3xl p-5 shadow-sm ${
|
||||
darkMode ? "bg-[#1E293B]" : "bg-white"
|
||||
}`}
|
||||
>
|
||||
<h2 className="font-bold text-xl mb-5">
|
||||
{lang.activitySummary}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
|
||||
<div className="flex justify-between">
|
||||
<span>{lang.totalLogin}</span>
|
||||
<span className="font-bold">23 {lang.times}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-green-600">
|
||||
<span>{lang.successfulLogin}</span>
|
||||
<span className="font-bold">22 {lang.times}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-red-500">
|
||||
<span>{lang.failedLogin}</span>
|
||||
<span className="font-bold">1 {lang.times}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
import {
|
||||
FileText,
|
||||
Search,
|
||||
Eye,
|
||||
Download,
|
||||
Package,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
ShoppingBag,
|
||||
} from "lucide-react";
|
||||
|
||||
export default function TransaksiBASTUsers() {
|
||||
const transaksi = [
|
||||
{
|
||||
no: "BAST-001",
|
||||
tanggal: "15 Mei 2026",
|
||||
barang: "Laptop ASUS ROG",
|
||||
status: "Dipinjam",
|
||||
},
|
||||
{
|
||||
no: "BAST-002",
|
||||
tanggal: "16 Mei 2026",
|
||||
barang: "Mouse Logitech",
|
||||
status: "Dibeli",
|
||||
},
|
||||
{
|
||||
no: "BAST-003",
|
||||
tanggal: "17 Mei 2026",
|
||||
barang: "Monitor Samsung",
|
||||
status: "Dipinjam",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-6">
|
||||
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold flex items-center gap-3">
|
||||
<FileText className="text-blue-600" size={35} />
|
||||
Transaksi BAST Users
|
||||
</h1>
|
||||
|
||||
<p className="text-gray-500 mt-2">
|
||||
Riwayat transaksi barang users
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button className="bg-blue-600 text-white px-5 py-3 rounded-2xl font-semibold">
|
||||
+ Tambah Transaksi
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* CARD */}
|
||||
<div className="grid grid-cols-4 gap-5 mb-8">
|
||||
|
||||
<div className="bg-white rounded-3xl p-5 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400">
|
||||
Total Transaksi
|
||||
</p>
|
||||
|
||||
<h2 className="text-3xl font-bold mt-2">
|
||||
120
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Package className="text-blue-600" size={40} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-3xl p-5 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400">
|
||||
Dipinjam
|
||||
</p>
|
||||
|
||||
<h2 className="text-3xl font-bold mt-2">
|
||||
85
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Clock className="text-yellow-500" size={40} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-3xl p-5 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400">
|
||||
Dibeli
|
||||
</p>
|
||||
|
||||
<h2 className="text-3xl font-bold mt-2">
|
||||
35
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ShoppingBag className="text-green-600" size={40} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-3xl p-5 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-gray-400">
|
||||
Selesai
|
||||
</p>
|
||||
|
||||
<h2 className="text-3xl font-bold mt-2">
|
||||
118
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<CheckCircle className="text-purple-600" size={40} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* TABLE */}
|
||||
<div className="bg-white rounded-3xl p-6 shadow-sm">
|
||||
|
||||
{/* TOP */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
|
||||
<h2 className="text-2xl font-bold">
|
||||
Data Transaksi
|
||||
</h2>
|
||||
|
||||
<div className="relative">
|
||||
<Search
|
||||
className="absolute left-4 top-4 text-gray-400"
|
||||
size={18}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari transaksi..."
|
||||
className="border pl-11 pr-4 py-3 rounded-2xl outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TABLE */}
|
||||
<div className="overflow-x-auto">
|
||||
|
||||
<table className="w-full">
|
||||
|
||||
<thead>
|
||||
<tr className="border-b text-left text-gray-500">
|
||||
|
||||
<th className="pb-4">No BAST</th>
|
||||
<th className="pb-4">Tanggal</th>
|
||||
<th className="pb-4">Barang</th>
|
||||
<th className="pb-4">Status</th>
|
||||
<th className="pb-4">Aksi</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
{transaksi.map((item, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className="border-b hover:bg-gray-50"
|
||||
>
|
||||
|
||||
<td className="py-5 font-semibold">
|
||||
{item.no}
|
||||
</td>
|
||||
|
||||
<td>{item.tanggal}</td>
|
||||
|
||||
<td>{item.barang}</td>
|
||||
|
||||
<td>
|
||||
<span
|
||||
className={`px-4 py-2 rounded-xl text-sm font-semibold ${
|
||||
item.status === "Dipinjam"
|
||||
? "bg-yellow-100 text-yellow-700"
|
||||
: "bg-green-100 text-green-700"
|
||||
}`}
|
||||
>
|
||||
{item.status}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div className="flex gap-3">
|
||||
|
||||
<button className="bg-blue-100 text-blue-600 p-3 rounded-xl">
|
||||
<Eye size={18} />
|
||||
</button>
|
||||
|
||||
<button className="bg-green-100 text-green-600 p-3 rounded-xl">
|
||||
<Download size={18} />
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
))}
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import { Globe } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function Bahasa() {
|
||||
const [language, setLanguage] = useState<"id" | "en">(
|
||||
() => {
|
||||
return (
|
||||
(localStorage.getItem("language") as "id" | "en") ||
|
||||
"id"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const changeLanguage = (lang: "id" | "en") => {
|
||||
setLanguage(lang);
|
||||
localStorage.setItem("language", lang);
|
||||
};
|
||||
|
||||
const text = {
|
||||
id: {
|
||||
title: "Pengaturan Bahasa",
|
||||
desc: "Pilih bahasa aplikasi yang ingin digunakan",
|
||||
indo: "Bahasa Indonesia",
|
||||
indoDesc: "Gunakan aplikasi dalam bahasa Indonesia",
|
||||
engDesc: "Gunakan aplikasi dalam bahasa Inggris",
|
||||
save: "Simpan Bahasa",
|
||||
},
|
||||
|
||||
en: {
|
||||
title: "Language Settings",
|
||||
desc: "Choose the application language",
|
||||
indo: "Indonesian",
|
||||
indoDesc: "Use application in Indonesian",
|
||||
engDesc: "Use application in English",
|
||||
save: "Save Language",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#f5f6fa] p-6">
|
||||
<div className="bg-white rounded-3xl p-8 shadow-sm max-w-3xl">
|
||||
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="bg-blue-100 p-3 rounded-2xl">
|
||||
<Globe className="text-blue-600" size={28} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">
|
||||
{text[language].title}
|
||||
</h1>
|
||||
|
||||
<p className="text-gray-500">
|
||||
{text[language].desc}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
|
||||
{/* Indonesia */}
|
||||
<div
|
||||
onClick={() => changeLanguage("id")}
|
||||
className={`rounded-2xl p-5 flex items-center justify-between cursor-pointer border ${
|
||||
language === "id"
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<h2 className="font-semibold text-lg">
|
||||
{text[language].indo}
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-500 text-sm">
|
||||
{text[language].indoDesc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
checked={language === "id"}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* English */}
|
||||
<div
|
||||
onClick={() => changeLanguage("en")}
|
||||
className={`rounded-2xl p-5 flex items-center justify-between cursor-pointer border ${
|
||||
language === "en"
|
||||
? "border-blue-500 bg-blue-50"
|
||||
: "border-gray-200"
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<h2 className="font-semibold text-lg">
|
||||
English
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-500 text-sm">
|
||||
{text[language].engDesc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
checked={language === "en"}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="mt-8 bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-2xl font-semibold transition">
|
||||
{text[language].save}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
import { Search, Plus, Eye } from "lucide-react";
|
||||
import lang from "../languages/language";
|
||||
|
||||
export default function TransaksiBAST() {
|
||||
const dataBAST = [
|
||||
{
|
||||
id: "BAST-001",
|
||||
penerima: "Lab Komputer",
|
||||
barang: "Laptop ASUS",
|
||||
tanggal: "12 Mei 2026",
|
||||
jam: "08:30 WIB",
|
||||
status: "Setuju",
|
||||
},
|
||||
{
|
||||
id: "BAST-002",
|
||||
penerima: "Ruang Guru",
|
||||
barang: "Proyektor Epson",
|
||||
tanggal: "12 Mei 2026",
|
||||
jam: "10:15 WIB",
|
||||
status: "Tidak Setuju",
|
||||
},
|
||||
{
|
||||
id: "BAST-003",
|
||||
penerima: "Perpustakaan",
|
||||
barang: "Buku Matematika",
|
||||
tanggal: "13 Mei 2026",
|
||||
jam: "09:00 WIB",
|
||||
status: "Setuju",
|
||||
},
|
||||
{
|
||||
id: "BAST-004",
|
||||
penerima: "Kelas 10 IPA",
|
||||
barang: "Mic Wireless",
|
||||
tanggal: "13 Mei 2026",
|
||||
jam: "11:20 WIB",
|
||||
status: "Tidak Setuju",
|
||||
},
|
||||
{
|
||||
id: "BAST-005",
|
||||
penerima: "Lab Bahasa",
|
||||
barang: "Headset Logitech",
|
||||
tanggal: "14 Mei 2026",
|
||||
jam: "08:45 WIB",
|
||||
status: "Setuju",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-8 bg-[#f3f4f6] min-h-screen">
|
||||
<div className="bg-white rounded-[35px] p-8 shadow-sm">
|
||||
{/* HEADER */}
|
||||
<div className="flex justify-between items-center flex-wrap gap-4 mb-8">
|
||||
<div>
|
||||
<h1 className="text-6xl font-bold text-black">
|
||||
{lang.bastTransaction}
|
||||
</h1>
|
||||
|
||||
<p className="text-gray-500 text-xl mt-3">
|
||||
{lang.bastDesc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ACTION */}
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
{/* SEARCH */}
|
||||
<div className="flex items-center border border-gray-300 bg-white px-5 py-4 rounded-2xl">
|
||||
<Search className="text-gray-400" size={22} />
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder={lang.searchTransaction}
|
||||
className="outline-none ml-3 text-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* BUTTON */}
|
||||
<button className="bg-blue-600 hover:bg-blue-700 transition text-white px-7 py-4 rounded-2xl flex items-center gap-3 text-lg font-medium">
|
||||
<Plus size={22} />
|
||||
{lang.addBast}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TABLE */}
|
||||
<div className="overflow-x-auto rounded-[30px] border border-gray-200">
|
||||
<table className="w-full">
|
||||
{/* HEAD */}
|
||||
<thead className="bg-blue-600 text-white">
|
||||
<tr>
|
||||
<th className="p-6 text-left text-lg">No BAST</th>
|
||||
<th className="p-6 text-left text-lg">{lang.receiver}</th>
|
||||
<th className="p-6 text-left text-lg">{lang.item}</th>
|
||||
<th className="p-6 text-left text-lg">{lang.date}</th>
|
||||
<th className="p-6 text-left text-lg">{lang.time}</th>
|
||||
<th className="p-6 text-left text-lg">Status</th>
|
||||
<th className="p-6 text-left text-lg">{lang.action}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{/* BODY */}
|
||||
<tbody>
|
||||
{dataBAST.map((item, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className="border-b hover:bg-gray-50 transition"
|
||||
>
|
||||
<td className="p-6 font-semibold text-lg">
|
||||
{item.id}
|
||||
</td>
|
||||
|
||||
<td className="p-6 text-lg">
|
||||
{item.penerima}
|
||||
</td>
|
||||
|
||||
<td className="p-6 text-lg">
|
||||
{item.barang}
|
||||
</td>
|
||||
|
||||
<td className="p-6 text-lg">
|
||||
{item.tanggal}
|
||||
</td>
|
||||
|
||||
<td className="p-6 text-lg">
|
||||
{item.jam}
|
||||
</td>
|
||||
|
||||
{/* STATUS */}
|
||||
<td className="p-6">
|
||||
{item.status === lang.agree ? (
|
||||
<span className="bg-green-100 text-green-700 px-5 py-2 rounded-full text-sm font-semibold">
|
||||
{lang.agree}
|
||||
</span>
|
||||
) : (
|
||||
<span className="bg-red-100 text-red-600 px-5 py-2 rounded-full text-sm font-semibold">
|
||||
{lang.disagree}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* BUTTON */}
|
||||
<td className="p-6">
|
||||
<button className="border border-blue-500 text-blue-600 hover:bg-blue-600 hover:text-white transition px-5 py-3 rounded-2xl flex items-center gap-2">
|
||||
<Eye size={18} />
|
||||
{lang.detail}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div className="flex justify-between items-center mt-8 flex-wrap gap-4">
|
||||
<p className="text-gray-500 text-lg">
|
||||
{lang.showingData}
|
||||
</p>
|
||||
|
||||
{/* PAGINATION */}
|
||||
<div className="flex gap-3">
|
||||
<button className="w-12 h-12 rounded-2xl border hover:bg-gray-100 text-lg">
|
||||
{"<"}
|
||||
</button>
|
||||
|
||||
<button className="w-12 h-12 rounded-2xl bg-blue-600 text-white text-lg">
|
||||
1
|
||||
</button>
|
||||
|
||||
<button className="w-12 h-12 rounded-2xl border hover:bg-gray-100 text-lg">
|
||||
2
|
||||
</button>
|
||||
|
||||
<button className="w-12 h-12 rounded-2xl border hover:bg-gray-100 text-lg">
|
||||
3
|
||||
</button>
|
||||
|
||||
<button className="w-12 h-12 rounded-2xl border hover:bg-gray-100 text-lg">
|
||||
{">"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
|
|
@ -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.16.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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
After Width: | Height: | Size: 9.3 KiB |
|
|
@ -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 |
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { RouterProvider } from "react-router-dom";
|
||||
import { router } from "./Routes";
|
||||
|
||||
function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// src/Routes.tsx
|
||||
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import DashboardLayout from "./layout/DashboardLayout";
|
||||
|
||||
// ROUTES
|
||||
import UserDashboard from "./routes/Dashboard";
|
||||
import MasterData from "./routes/MasterData";
|
||||
import LoginKasir from "./routes/LoginKasir";
|
||||
import Settings from "./routes/Settings";
|
||||
import Transaksi from "./routes/transaksi";
|
||||
import BAST from "./routes/BAST";
|
||||
import Laporan from "./routes/Laporan";
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
/* LOGIN */
|
||||
{
|
||||
path: "/",
|
||||
element: <LoginKasir />,
|
||||
},
|
||||
|
||||
/* DASHBOARD */
|
||||
{
|
||||
path: "/userDashboard",
|
||||
element: <DashboardLayout />,
|
||||
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <UserDashboard />,
|
||||
},
|
||||
|
||||
{
|
||||
path: "master-data",
|
||||
element: <MasterData />,
|
||||
},
|
||||
|
||||
{
|
||||
path: "BAST",
|
||||
element: <BAST />,
|
||||
},
|
||||
|
||||
{
|
||||
path: "settings",
|
||||
element: <Settings />,
|
||||
},
|
||||
|
||||
{
|
||||
path: "Transaksi",
|
||||
element: <Transaksi />,
|
||||
},
|
||||
|
||||
{
|
||||
path: "Laporan",
|
||||
element: <Laporan />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 757 KiB |
|
After Width: | Height: | Size: 750 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 821 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 523 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
// src/components/dashboard/Hero.tsx
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Hero() {
|
||||
// State untuk menyimpan data profile secara dinamis
|
||||
const [userData, setUserData] = useState({ nama_lengkap: "User", role: "User" });
|
||||
|
||||
// State untuk menyimpan angka statistik dari database
|
||||
const [stats, setStats] = useState({
|
||||
permintaanHariIni: 0,
|
||||
menungguPersetujuan: 0,
|
||||
totalBarang: 0,
|
||||
stokHampirHabis: 0,
|
||||
totalPengguna: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// 1. AMBIL DATA USER YANG LOGIN (SINKRONISASI NYA DI SINI YA BANG)
|
||||
const localName = localStorage.getItem("user_name");
|
||||
const localRole = localStorage.getItem("user_role");
|
||||
const sessionUser = sessionStorage.getItem("user");
|
||||
|
||||
if (sessionUser) {
|
||||
// Jika ada data di session storage (Objek utuh)
|
||||
try {
|
||||
const parsedUser = JSON.parse(sessionUser);
|
||||
setUserData({
|
||||
nama_lengkap: parsedUser.nama_lengkap || "User",
|
||||
role: parsedUser.role || "User"
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else if (localName) {
|
||||
// Jika data session kosong, ambil dari local storage pecahan milik login Abang
|
||||
setUserData({
|
||||
nama_lengkap: localName,
|
||||
role: localRole || "User"
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Ambil data Barang dari backend untuk Card 3
|
||||
fetch("http://localhost:5000/api/barang")
|
||||
.then((res) => res.json())
|
||||
.then((resData) => {
|
||||
if (resData.success && Array.isArray(resData.data)) {
|
||||
const daftarBarang = resData.data;
|
||||
const hampirHabis = daftarBarang.filter((b: any) => b.stok <= 3).length;
|
||||
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
totalBarang: daftarBarang.length,
|
||||
stokHampirHabis: hampirHabis,
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error("Gagal mengambil data barang:", err));
|
||||
|
||||
// 3. Ambil data BAST dari backend untuk Card 1 dan Card 2
|
||||
fetch("http://localhost:5000/api/bast")
|
||||
.then((res) => res.json())
|
||||
.then((resData) => {
|
||||
if (resData.success && Array.isArray(resData.data)) {
|
||||
const daftarBast = resData.data;
|
||||
|
||||
// Hitung data BAST yang statusnya masih 'Menunggu' (Card 2)
|
||||
const menunggu = daftarBast.filter(
|
||||
(b: any) => b.status_serah === "Menunggu" || b.status_terima === "Menunggu"
|
||||
).length;
|
||||
|
||||
// Hitung data BAST yang dibuat HARI INI (Card 1)
|
||||
const hariIniWIB = new Date().toLocaleDateString("id-ID");
|
||||
const totalHariIni = daftarBast.filter((b: any) => {
|
||||
if (!b.dibuat_pada) return false;
|
||||
const tanggalBast = new Date(b.dibuat_pada).toLocaleDateString("id-ID");
|
||||
return tanggalBast === hariIniWIB;
|
||||
}).length;
|
||||
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
menungguPersetujuan: menunggu,
|
||||
permintaanHariIni: totalHariIni
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error("Gagal mengambil data BAST:", err));
|
||||
|
||||
// 4. Ambil data Users dari backend untuk Card 4
|
||||
fetch("http://localhost:5000/api/users")
|
||||
.then((res) => res.json())
|
||||
.then((resData) => {
|
||||
if (resData.success && Array.isArray(resData.data)) {
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
totalPengguna: resData.data.length
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error("Gagal mengambil data users:", err));
|
||||
}, []);
|
||||
|
||||
// Mengambil huruf pertama untuk avatar icon (kasih pengaman biar ga crash pas loading)
|
||||
const inisialNama = userData.nama_lengkap ? userData.nama_lengkap.charAt(0).toUpperCase() : "U";
|
||||
|
||||
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">
|
||||
|
||||
{/* LEFT */}
|
||||
<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 hover:shadow-lg transition">
|
||||
|
||||
{/* AVATAR */}
|
||||
<div className="w-12 h-12 bg-blue-600 rounded-full flex items-center justify-center text-white font-bold text-lg">
|
||||
{inisialNama}
|
||||
</div>
|
||||
|
||||
{/* USER INFO */}
|
||||
<div>
|
||||
<h3 className="font-semibold text-black">
|
||||
{userData.nama_lengkap}
|
||||
</h3>
|
||||
<p className="text-gray-500 text-sm">
|
||||
{userData.role}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* STAT CARDS */}
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
|
||||
{/* CARD 1 */}
|
||||
<div className="bg-white rounded-2xl p-6 shadow border hover:shadow-lg transition">
|
||||
<p className="text-gray-500 mb-2">
|
||||
Permintaan Hari Ini
|
||||
</p>
|
||||
<h2 className="text-4xl font-bold text-blue-700">
|
||||
{stats.permintaanHariIni}
|
||||
</h2>
|
||||
<p className="text-green-600 mt-2">
|
||||
Dokumen BAST baru
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CARD 2 */}
|
||||
<div className="bg-white rounded-2xl p-6 shadow border hover:shadow-lg transition">
|
||||
<p className="text-gray-500 mb-2">
|
||||
Menunggu Persetujuan
|
||||
</p>
|
||||
<h2 className="text-4xl font-bold text-blue-700">
|
||||
{stats.menungguPersetujuan}
|
||||
</h2>
|
||||
<p className="text-yellow-600 mt-2">
|
||||
Perlu tindakan segera
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CARD 3 */}
|
||||
<div className="bg-white rounded-2xl p-6 shadow border hover:shadow-lg transition">
|
||||
<p className="text-gray-500 mb-2">
|
||||
Total Barang
|
||||
</p>
|
||||
<h2 className="text-4xl font-bold text-blue-700">
|
||||
{stats.totalBarang}
|
||||
</h2>
|
||||
<p className="text-green-600 mt-2">
|
||||
{stats.stokHampirHabis} stok hampir habis
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CARD 4 */}
|
||||
<div className="bg-white rounded-2xl p-6 shadow border hover:shadow-lg transition">
|
||||
<p className="text-gray-500 mb-2">
|
||||
Total Pengguna
|
||||
</p>
|
||||
<h2 className="text-4xl font-bold text-blue-700">
|
||||
{stats.totalPengguna}
|
||||
</h2>
|
||||
<p className="text-green-600 mt-2">
|
||||
Siswa & petugas terdaftar
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// src/components/manajemenUsers/UsersFilter.tsx
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export default function UsersFilter() {
|
||||
const [selectedRole, setSelectedRole] =
|
||||
useState("Semua");
|
||||
|
||||
const roles = ["Semua", "Siswa"];
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 mb-6">
|
||||
{roles.map((role) => (
|
||||
<button
|
||||
key={role}
|
||||
onClick={() => setSelectedRole(role)}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition ${
|
||||
selectedRole === role
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-white border hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
{role}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
export default function UsersHero() {
|
||||
return (
|
||||
<div className="bg-blue-50 rounded-3xl p-8 shadow-sm border border-blue-100 mb-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-gray-500 text-lg">
|
||||
Kelola data pengguna sekolah 👥
|
||||
</p>
|
||||
<h1 className="text-5xl font-extrabold text-blue-700 mt-2">
|
||||
Manajemen Users
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<button className="bg-blue-600 text-white px-6 py-3 rounded-xl font-semibold shadow hover:bg-blue-700 transition">
|
||||
+ Tambah User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
// src/components/manajemenUsers/UsersTable.tsx
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export default function UsersTable() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: "#S001",
|
||||
nama: "Sulthan",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
{
|
||||
id: "#S002",
|
||||
nama: "Zahir",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
{
|
||||
id: "#S003",
|
||||
nama: "Arief",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Pending",
|
||||
},
|
||||
{
|
||||
id: "#S004",
|
||||
nama: "Muhammad",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
{
|
||||
id: "#S005",
|
||||
nama: "Fajar",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
{
|
||||
id: "#S006",
|
||||
nama: "Akbar",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
{
|
||||
id: "#S007",
|
||||
nama: "Yudha",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
{
|
||||
id: "#S008",
|
||||
nama: "Heru",
|
||||
role: "Siswa",
|
||||
kelas: "XI RPL",
|
||||
status: "Aktif",
|
||||
},
|
||||
];
|
||||
|
||||
// FILTER SEARCH
|
||||
const filteredUsers = users.filter((user) =>
|
||||
user.nama
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-3xl shadow-lg p-6 border">
|
||||
{/* HEADER */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold text-black">
|
||||
Data Siswa
|
||||
</h2>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari nama siswa..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="border border-gray-300 rounded-xl px-4 py-2 w-72 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* TABLE HEADER */}
|
||||
<div className="grid grid-cols-6 bg-gray-100 p-4 rounded-xl font-semibold text-gray-700 mb-4">
|
||||
<div>ID</div>
|
||||
<div>Nama</div>
|
||||
<div>Role</div>
|
||||
<div>Kelas</div>
|
||||
<div>Status</div>
|
||||
<div>Aksi</div>
|
||||
</div>
|
||||
|
||||
{/* TABLE BODY */}
|
||||
<div className="space-y-3">
|
||||
{filteredUsers.map((user, index) => (
|
||||
<div
|
||||
key={user.id}
|
||||
className={`grid grid-cols-6 items-center p-4 rounded-xl border ${
|
||||
index % 2 === 0
|
||||
? "bg-white"
|
||||
: "bg-blue-50"
|
||||
}`}
|
||||
>
|
||||
<div>{user.id}</div>
|
||||
<div>{user.nama}</div>
|
||||
<div>{user.role}</div>
|
||||
<div>{user.kelas}</div>
|
||||
|
||||
<div>
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-sm ${
|
||||
user.status === "Aktif"
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-yellow-100 text-yellow-700"
|
||||
}`}
|
||||
>
|
||||
{user.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button className="bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700">
|
||||
Edit
|
||||
</button>
|
||||
|
||||
<button className="bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600">
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* EMPTY */}
|
||||
{filteredUsers.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
Siswa tidak ditemukan.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
// src/components/masterData/BarangFilter.tsx
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
type Props = {
|
||||
selectedKategori: string;
|
||||
setSelectedKategori: (kategori: string) => void;
|
||||
searchQuery: string;
|
||||
setSearchQuery: (query: string) => void;
|
||||
selectedStatus: string;
|
||||
setSelectedStatus: (status: string) => void;
|
||||
};
|
||||
|
||||
type KategoriType = {
|
||||
kategori_id: number;
|
||||
nama_kategori: string;
|
||||
};
|
||||
|
||||
export default function BarangFilter({
|
||||
selectedKategori,
|
||||
setSelectedKategori,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
selectedStatus,
|
||||
setSelectedStatus,
|
||||
}: Props) {
|
||||
// Data cadangan kategori sesuai gambar dashboard admin
|
||||
const [kategoriList, setKategoriList] = useState<string[]>([
|
||||
"Semua",
|
||||
"Buku dan Modul",
|
||||
"Alat_Kebersihan",
|
||||
"Peralatan Bengkel",
|
||||
"Elektronik",
|
||||
"Alat Tulis",
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("http://localhost:5000/api/kategori")
|
||||
.then((res) => res.json())
|
||||
.then((resData) => {
|
||||
if (resData.success && Array.isArray(resData.data) && resData.data.length > 0) {
|
||||
const namaKategoriDariDb = resData.data.map((k: KategoriType) => k.nama_kategori);
|
||||
setKategoriList(["Semua", ...namaKategoriDariDb]);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.warn("Menggunakan kategori default.", err));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 mb-8">
|
||||
{/* BARIS 1: INPUT SEARCH DAN DROPDOWN STATUS */}
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
{/* INPUT SEARCH */}
|
||||
<div className="flex-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari nama atau kode barang..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-4 pr-4 py-3 bg-white border border-gray-300 rounded-xl text-black focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-sm font-medium"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* DROPDOWN FILTER STATUS KONDISI SESUAI DATABASE */}
|
||||
<div className="w-full md:w-52">
|
||||
<select
|
||||
value={selectedStatus}
|
||||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-white border border-gray-300 rounded-xl text-black focus:outline-none focus:ring-2 focus:ring-blue-500 shadow-sm font-semibold"
|
||||
>
|
||||
<option value="Semua">Semua Kondisi</option>
|
||||
<option value="Baik">Kondisi: Baik</option>
|
||||
<option value="Rusak Ringan">Kondisi: Rusak Ringan</option>
|
||||
<option value="Rusak Berat">Kondisi: Rusak Berat</option>
|
||||
<option value="Hilang">Kondisi: Hilang</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BARIS 2: TOMBOL FILTER KATEGORI */}
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
{kategoriList.map((kategori) => (
|
||||
<button
|
||||
key={kategori}
|
||||
type="button"
|
||||
onClick={() => setSelectedKategori(kategori)}
|
||||
className={`px-4 py-2 rounded-xl font-medium text-sm transition ${
|
||||
selectedKategori === kategori
|
||||
? "bg-blue-600 text-white shadow-sm"
|
||||
: "bg-white border border-gray-200 text-gray-700 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
{kategori}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
type BarangType = {
|
||||
barang_id: number;
|
||||
kode_barang: string;
|
||||
nama_barang: string;
|
||||
foto_barang: string;
|
||||
status_barang: string;
|
||||
stok: number;
|
||||
nama_kategori: string;
|
||||
nama_lokasi: string;
|
||||
qr_image?: string;
|
||||
};
|
||||
|
||||
type BarangGridProps = {
|
||||
barangList: BarangType[];
|
||||
onPinjam: (barang: BarangType) => void;
|
||||
};
|
||||
|
||||
export default function BarangGrid({ barangList, onPinjam }: BarangGridProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6 mt-6">
|
||||
{barangList.map((barang) => (
|
||||
<div
|
||||
key={barang.barang_id}
|
||||
className="bg-white rounded-2xl p-4 shadow border hover:shadow-lg transition flex flex-col justify-between"
|
||||
>
|
||||
<div>
|
||||
{/* FOTO BARANG */}
|
||||
<div className="w-full h-48 bg-gray-100 rounded-xl overflow-hidden mb-4 border">
|
||||
<img
|
||||
src={barang.foto_barang || "https://via.placeholder.com/150"}
|
||||
alt={barang.nama_barang}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* INFO BARANG */}
|
||||
<span className="px-2.5 py-1 bg-blue-50 text-blue-700 rounded-md text-xs font-semibold uppercase">
|
||||
{barang.nama_kategori}
|
||||
</span>
|
||||
|
||||
<h3 className="font-bold text-gray-800 text-lg mt-2 line-clamp-1">
|
||||
{barang.nama_barang}
|
||||
</h3>
|
||||
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Kode: {barang.kode_barang} | Lokasi: {barang.nama_lokasi}
|
||||
</p>
|
||||
|
||||
<div className="flex justify-between items-center mt-4 pt-3 border-t">
|
||||
<div>
|
||||
<p className="text-xs text-gray-400">Status</p>
|
||||
<span className={`text-sm font-bold ${barang.status_barang === 'Baik' ? 'text-green-600' : 'text-red-500'}`}>
|
||||
{barang.status_barang}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-gray-400">Stok Tersedia</p>
|
||||
<p className="text-sm font-bold text-gray-700">{barang.stok} Pcs</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TOMBOL AKSI PINJAM */}
|
||||
<button
|
||||
onClick={() => onPinjam(barang)}
|
||||
className="w-full mt-4 bg-blue-600 text-white py-2.5 rounded-xl font-semibold hover:bg-blue-700 transition shadow-sm"
|
||||
>
|
||||
Pinjam Barang
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
// src/components/masterData/EditBarangModal.tsx
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
type BarangType = {
|
||||
id: number;
|
||||
nama: string;
|
||||
foto: string;
|
||||
kondisi: string;
|
||||
tersedia: boolean;
|
||||
kategori: 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 [tersedia, setTersedia] =
|
||||
useState(barang.tersedia);
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSave({
|
||||
...barang,
|
||||
kondisi,
|
||||
tersedia,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
{/* MODAL BOX */}
|
||||
<div className="bg-white w-[420px] rounded-3xl shadow-2xl p-5">
|
||||
{/* HEADER */}
|
||||
<h2 className="text-xl font-bold text-center mb-4">
|
||||
Edit Barang
|
||||
</h2>
|
||||
|
||||
{/* FOTO */}
|
||||
<div className="w-28 h-28 mx-auto rounded-2xl overflow-hidden mb-3 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-1">
|
||||
{barang.nama}
|
||||
</p>
|
||||
|
||||
{/* KATEGORI */}
|
||||
<p className="text-center text-sm text-blue-600 mb-4">
|
||||
{barang.kategori}
|
||||
</p>
|
||||
|
||||
{/* FORM */}
|
||||
<div className="space-y-3">
|
||||
{/* 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-lg p-2"
|
||||
>
|
||||
<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
|
||||
</label>
|
||||
<select
|
||||
value={
|
||||
tersedia
|
||||
? "true"
|
||||
: "false"
|
||||
}
|
||||
onChange={(e) =>
|
||||
setTersedia(
|
||||
e.target.value ===
|
||||
"true"
|
||||
)
|
||||
}
|
||||
className="w-full border rounded-lg p-2"
|
||||
>
|
||||
<option value="true">
|
||||
Bisa Dipinjam
|
||||
</option>
|
||||
<option value="false">
|
||||
Tidak Tersedia
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BUTTON */}
|
||||
<div className="flex justify-end gap-3 mt-5">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-lg bg-gray-300 hover:bg-gray-400"
|
||||
>
|
||||
Batal
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700"
|
||||
>
|
||||
pinjam
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700"
|
||||
>
|
||||
kembalikan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||