24/02/2025

This commit is contained in:
damarrsyh 2025-02-24 15:59:45 +07:00
parent 3fd2931404
commit 11d418602a
23 changed files with 8991 additions and 0 deletions

24
.gitignore vendored Normal file
View File

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

38
eslint.config.js Normal file
View File

@ -0,0 +1,38 @@
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
settings: { react: { version: '18.3' } },
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

17
index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Poppins Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

8496
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "queue-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@reduxjs/toolkit": "^2.6.0",
"axios": "^1.7.9",
"bcryptjs": "^3.0.2",
"bootstrap": "^5.3.3",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2",
"react": "^19.0.0",
"react-bootstrap": "^2.10.9",
"react-dom": "^19.0.0",
"react-player": "^2.16.0",
"react-redux": "^9.2.0",
"react-router-dom": "^6.29.0",
"redux": "^5.0.1"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^1.3.2",
"eslint": "^9.19.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0",
"vite": "^6.1.1"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

View File

@ -0,0 +1,70 @@
import { Container, Row, Col, Card } from "react-bootstrap";
import ReactPlayer from "react-player";
const sampleTickets = [
{ number: "A1", counter: "1" },
{ number: "A2", counter: "2" },
{ number: "A3", counter: "3" },
{ number: "A4", counter: "4" },
{ number: "A5", counter: "5" },
{ number: "A6", counter: "6" },
{ number: "A7", counter: "7" },
{ number: "A8", counter: "8" },
{ number: "A9", counter: "9" },
{ number: "A10", counter: "10" },
];
const QueueDisplay = () => {
return (
<Container fluid className="p-3" style={{ overflowX: "hidden" }}>
<Row className="mb-3 g-0">
{/* Informasi Bisnis & Nomor Antrian Utama */}
<Col md={4} className="px-3 d-flex flex-column justify-content-between">
<div className="mb-3 p-3 text-white bg-dark text-center" style={{ borderRadius: "10px" }}>
<h4 className="mb-1">Jagowebdev.com</h4>
<p className="mb-1">Jl. Zebra III No. 32, Pedurungan Kidul, Semarang</p>
<p>Telp: 08561363962</p>
</div>
<Card className="flex-grow-1 d-flex align-items-center justify-content-center text-center"
style={{ borderRadius: "10px", padding: "20px", width: "100%", minHeight: "250px" }}>
<Card.Body>
<h5 className="mb-2"><strong>NOMOR ANTRIAN</strong></h5>
<h1 style={{ fontSize: "80px", fontWeight: "bold", margin: "10px 0" }}>A0</h1>
</Card.Body>
</Card>
</Col>
{/* Video Player */}
<Col md={8} className="px-3 d-flex align-items-stretch">
<div className="w-100 d-flex">
<ReactPlayer
url="https://www.youtube.com/watch?v=FaU8BkqmXzo&pp=ygULc3RvY2sgdmlkZW8%3D"
controls
width="100%"
height="100%"
className="react-player"
style={{ borderRadius: "10px", overflow: "hidden", flexGrow: 1 }}
/>
</div>
</Col>
</Row>
{/* Loket Antrian */}
<Row className="text-center d-flex flex-wrap justify-content-center row-cols-2 row-cols-md-3 row-cols-lg-5 g-0 px-2">
{sampleTickets.map((ticket, index) => (
<Col key={index} className="d-flex justify-content-center p-2">
<Card className="d-flex align-items-center justify-content-center text-center flex-grow-1"
style={{ borderRadius: "10px", width: "100%", minHeight: "150px" }}>
<Card.Body>
<Card.Title>Nomor Antrian</Card.Title>
<h2 style={{ fontSize: "40px", fontWeight: "bold" }}>{ticket.number}</h2>
</Card.Body>
</Card>
</Col>
))}
</Row>
</Container>
);
};
export default QueueDisplay;

View File

@ -0,0 +1,135 @@
import { Container, Card, Button, Row, Col, Form, } from 'react-bootstrap';
import { useState } from 'react';
import ReactPlayer from 'react-player';
const services = [
{id: "TSP1", name: "test1"},
{id: "TSP2", name: "test2"},
{id: "TSP3", name: "test3"},
{id: "TSP4", name: "test4"},
{id: "TSP5", name: "test5"},
{id: "TSP6", name: "test6"}
];
const ServiceSelection = () => {
const [selectedService, setSelectedService] = useState (null);
const [name, setName] = useState("");
const [phone, setPhone] = useState("");
const [ticket, setTicket] = useState (null);
const handleServiceSelect = (serviceId) => {
setSelectedService(services.find(service => service.id === serviceId));
}
const handleSubmit = (e) => {
e.preventDefault();
if (selectedService && name && phone) {
const queueNumber = `${selectedService.id}-${Math.floor(1000 + Math.random() * 9000)}`;
const newTicket = {
number : queueNumber,
serviceId : selectedService.id,
service : selectedService.name,
name,
phone
};
setTicket(newTicket);
setTimeout(() => window.print(), 500);
}
}
return (
<Container>
{!ticket ? (
<Row className='vh-100'>
<Col md={6} className='d-flex flex-column'>
<h3 className='my-4'>Pilih Layanan</h3>
<Row className='flex-grow-1'>
{services.map(service => {
const isSelected = selectedService?.id === service.id;
return (
<Col key={service.id} md={6} className='mb-3'>
<Card className={`h-100 ${isSelected ? "bg-primary text-white border-primary shadow-sm" : "bg-light"}`}>
<Card.Body className='d-flex flex-column justify-content-between'>
<Card.Title className='text-center'>{service.name}</Card.Title>
<Button
variant={isSelected ? "light" : "outline-primary"}
onClick={() => handleServiceSelect(service.id)}>
{isSelected ? "Dipilih" : "Pilih Layanan"}
</Button>
</Card.Body>
</Card>
</Col>
)
})}
</Row>
</Col>
<Col md={6} className='d-flex flex-column'>
<h3 className='my-4'>Masukan Data Diri</h3>
<Card className='flex-grow-1'>
<Card.Body className='d-flex flex-column'>
<Form onSubmit={handleSubmit} className='flex-grow-1 d-flex flex-column'>
<Form.Group className='mb-3' controlId='formName'>
<Form.Label>Nama</Form.Label>
<Form.Control
type='text'
placeholder='@example: pandawa'
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</Form.Group>
<Form.Group className='mb-3' controlId='formPhone'>
<Form.Label>No Telepon</Form.Label>
<Form.Control
type='text'
placeholder='@example: 08xxxxxxxxxxx'
value={phone}
onChange={(e) => setPhone(e.target.value)}
required
/>
</Form.Group>
<Button variant='primary' type='submit'>Submit</Button>
</Form>
</Card.Body>
</Card>
<div className='d-flex flex-grow-1 my-3'>
<ReactPlayer
url="https://www.youtube.com/watch?v=FaU8BkqmXzo&pp=ygULc3RvY2sgdmlkZW8%3D"
controls
width="100%"
height="400px"
/>
</div>
</Col>
</Row>
) : (
<Container className="d-flex justify-content-center align-items-center vh-100">
<Card className="p-4 text-center" style={{ maxWidth: "400px", width: "100%" }}>
<Card.Body>
<Card.Title className="fw-bold fs-3">Tiket Antrian</Card.Title>
<hr />
<Card.Text className="fw-bold text-uppercase fs-2 bg-light p-3 rounded">
{ticket.number}
</Card.Text>
<Card.Text>
<strong>Layanan :</strong> {ticket.service}
</Card.Text>
<Card.Text>
<strong>Nama :</strong> {ticket.name}
</Card.Text>
<Card.Text>
<strong>No Telepon :</strong> {ticket.phone}
</Card.Text>
<Button variant="success" onClick={() => window.print()}>
Cetak Tiket
</Button>
</Card.Body>
</Card>
</Container>
)}
</Container>
)
}
export default ServiceSelection

11
src/main.jsx Normal file
View File

@ -0,0 +1,11 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import AppRoutes from './routes/AppRoutes.jsx'
import 'bootstrap/dist/css/bootstrap.min.css'
import './styles/style.css'
createRoot(document.getElementById('root')).render(
<StrictMode>
<AppRoutes/>
</StrictMode>,
)

View File

@ -0,0 +1,11 @@
import QueueDisplay from "../../components/Display/QueueDisplay"
const QueueDisplayPage = () => {
return (
<>
<QueueDisplay/>
</>
)
}
export default QueueDisplayPage

View File

@ -0,0 +1,11 @@
import ServiceSelection from "../../components/Display/ServiceSelection"
const QueueMenuPage = () => {
return (
<>
<ServiceSelection/>
</>
)
}
export default QueueMenuPage

View File

@ -0,0 +1,10 @@
const QueueSettingsDisplayPage = () => {
return (
<div>
</div>
)
}
export default QueueSettingsDisplayPage

View File

@ -0,0 +1,10 @@
const QueueSettingsMenuPage = () => {
return (
<div>
</div>
)
}
export default QueueSettingsMenuPage

View File

@ -0,0 +1,10 @@
const CallQueuePage = () => {
return (
<div>
</div>
)
}
export default CallQueuePage

View File

@ -0,0 +1,10 @@
const QueueListPage = () => {
return (
<div>
</div>
)
}
export default QueueListPage

View File

@ -0,0 +1,10 @@
const QueueReportPage = () => {
return (
<div>
</div>
)
}
export default QueueReportPage

19
src/redux/queueSlice.js Normal file
View File

@ -0,0 +1,19 @@
import { createSlice } from "@reduxjs/toolkit";
const queueSlice = createSlice({
name: "queue",
initialState: {
tickets: [],
},
reducers: {
addTicket: (state, action) => {
state.tickets.push(action.payload);
},
removeTicket: (state) => {
state.tickets.shift();
}
}
});
export const {addTicket, removeTicket } = queueSlice.actions;
export default queueSlice.reducer;

12
src/redux/store.js Normal file
View File

@ -0,0 +1,12 @@
import { configureStore } from "@reduxjs/toolkit";
import queueReducer from "./queueSlice";
import themeReducer from "./themeSlice";
const store = configureStore({
reducer: {
queue: queueReducer,
theme: themeReducer,
}
});
export default store;

16
src/redux/themeSlice.js Normal file
View File

@ -0,0 +1,16 @@
import { createSlice } from '@reduxjs/toolkit';
const themeSlice = createSlice({
name: "theme",
initialState: {
darkMode: false,
},
reducers: {
toggleTheme: (state) => {
state.darkMode = !state.darkMode;
}
}
});
export const {toggleTheme} = themeSlice.actions;
export default themeSlice.reducer;

29
src/routes/AppRoutes.jsx Normal file
View File

@ -0,0 +1,29 @@
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"
import QueueDisplayPage from "../pages/Display/QueueDisplayPage"
import QueueSettingsDisplayPage from "../pages/Display/QueueSettingsDisplayPage"
import QueueSettingsMenuPage from "../pages/Display/QueueSettingsMenuPage"
import QueueMenuPage from "../pages/Display/QueueMenuPage"
import CallQueuePage from "../pages/Queue/CallQueuePage"
import QueueListPage from "../pages/Queue/QueueListPage"
import QueueReportPage from "../pages/Queue/QueueReportPage"
const AppRoutes = () => {
return (
<Router>
<Routes>
{/* Queue Pages */}
<Route path="/" element={<QueueListPage/>}/>
<Route path="/queue-list" element={<QueueListPage/>}/>
<Route path="/call-queue" element={<CallQueuePage/>}/>
<Route path="/queue-report" element={<QueueReportPage/>}/>
{/* Display Pages */}
<Route path="/queue-menu-settings" element={<QueueSettingsMenuPage/>}/>
<Route path="/queue-menu" element={<QueueMenuPage/>}/>
<Route path="/queue-display-settings" element={<QueueSettingsDisplayPage/>}/>
<Route path="/queue-display" element={<QueueDisplayPage/>}/>
</Routes>
</Router>
)
}
export default AppRoutes

3
src/styles/style.css Normal file
View File

@ -0,0 +1,3 @@
:root {
--bs-body-font-family: 'Poppins', sans-serif;
}

7
vite.config.js Normal file
View File

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