Commit 5e7a4928 by Sujeeth AV

Initial commit

parents
# 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?
*storybook.log
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
"stories": [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
],
"addons": ["@storybook/addon-onboarding"],
"framework": {
"name": "@storybook/react-vite",
"options": {}
}
};
export default config;
\ No newline at end of file
import 'react-toastify/dist/ReactToastify.css';
import '../global.css';
/** @type { import('@storybook/react-vite').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
\ No newline at end of file
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
{
"seatLayout": [
[
0,
"A1",
"A2",
"A3",
0,
"A4",
"A5",
"A6",
0
],
[
"B1",
"B2",
"B3",
"B4",
0,
"B5",
"B6",
"B7",
"B8"
],
[
"C1",
"C2",
"C3",
"C4",
0,
"C5",
"C6",
"C7",
"C8"
],
[
"D1",
"D2",
"D3",
"D4",
0,
"D5",
"D6",
"D7",
"D8"
],
[
"E1",
"E2",
"E3",
"E4",
0,
"E5",
"E6",
"E7",
"E8"
],
[
"F1",
"F2",
"F3",
"F4",
0,
"F5",
"F6",
"F7",
"F8"
],
[
"G1",
"G2",
"G3",
"G4",
0,
"G5",
"G6",
"G7",
"G8"
],
[
0,
"H1",
"H2",
"H3",
0,
"H4",
"H5",
"H7",
0
]
],
"users": [
{
"id": "9500403346",
"Mobile": "9500403346",
"Name": "sujeeth"
},
{
"id": "7418601450",
"Mobile": "7418601450",
"Name": "ravi"
},
{
"id": "9087654321",
"Mobile": "9087654321",
"Name": "oggy"
},
{
"id": "7894561230",
"Mobile": "7894561230",
"Name": "khan"
},
{
"id": "8097654321",
"Mobile": "8097654321",
"Name": "john"
},
{
"id": "9677655288",
"Mobile": "9677655288",
"Name": "anitha"
},
{
"id": "9597003337",
"Mobile": "9597003337",
"Name": "venkatesh"
},
{
"id": "6379757115",
"Mobile": "6379757115",
"Name": "Shaganaz "
}
],
"seats": [
{
"userId": "9087654321",
"seats": [
"D4"
],
"status": "booked",
"id": "9192"
},
{
"userId": "9500403346",
"seats": [
"A1",
"A2"
],
"status": "booked",
"id": "7685"
},
{
"id": "4d7f",
"userId": "7418601450",
"seats": [
"D1",
"D2",
"D3"
],
"status": "booked"
},
{
"userId": "8097654321",
"seats": [
"E6",
"E7"
],
"status": "booked",
"id": "164e"
},
{
"userId": "9677655288",
"seats": [
"H1"
],
"status": "booked",
"id": "aa34"
},
{
"userId": "9597003337",
"seats": [
"E1",
"E2"
],
"status": "booked",
"id": "9213"
},
{
"userId": "6379757115",
"seats": [
"B1",
"B2"
],
"status": "booked",
"id": "89cd"
},
{
"id": "7ace",
"userId": null,
"seats": [
"B3",
"B4",
"E5",
"E8"
],
"status": "booked"
},
{
"id": "ab59",
"userId": null,
"seats": [
"B3",
"B4",
"E5",
"E8"
],
"status": "booked"
}
]
}
\ No newline at end of file
import js from '@eslint/js'
import globals from 'globals'
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',
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]
@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Reddit+Sans:ital,wght@0,200..900;1,200..900&display=swap");
* {
margin: 0;
padding: 0;
}
body {
background-color: #303fb6;
font-family: "Times New Roman", Times, serif;
min-height: 50svh;
height: 100svh;
display: flex;
align-items: center;
justify-content: center;
}
h2 {
font-size: 1.5rem;
font-weight: 700;
}
input {
font-size: 1rem;
font-weight: 500;
}
button {
outline: none;
border-radius: 9px;
font-size: 1rem;
font-weight: 550;
cursor: pointer;
width: 100%;
}
.Toastify__toast {
min-width: 250px !important;
max-width: 300px !important;
padding: 10px 20px !important;
font-size: 0.7rem !important;
}
.Toastify__close-button {
position: absolute;
left: 107px;
top: 53%;
transform: translateY(-50%);
}
@media (min-width: 768px) {
input {
width: 100%;
}
}
<!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" />
<title>Seat-Booking</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "seat",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"axios": "^1.9.0",
"classnames": "^2.5.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.6.2",
"react-toastify": "^11.0.5"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@storybook/addon-onboarding": "^9.0.4",
"@storybook/react-vite": "^9.0.4",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-storybook": "^9.0.4",
"globals": "^16.0.0",
"prop-types": "^15.8.1",
"storybook": "^9.0.4",
"vite": "^6.3.5"
},
"eslintConfig": {
"extends": [
"plugin:storybook/recommended"
]
},
"overrides": {
"storybook": "$storybook"
}
}
import axios from "axios";
const BASE_URL = "http://192.168.1.64:3001";
export const loginUser = async (mobile) => {
const res = await axios.get(`${BASE_URL}/users`, {
params: { Mobile: mobile }
});
console.log("Login User API response:", res.data);
return res.data.length ? res.data[0] : null;
};
export const saveSeat=async({userId,seats})=>{
const response=await axios.post(`${BASE_URL}/seats`,{
userId,
seats,
status:'booked',
});
return response.data;
}
export const getSeat=async()=>{
const response= await axios.get(`${BASE_URL}/seats`);
return response.data;
}
export const deleteSeat=async(id)=>{
const response=await axios.delete(`${BASE_URL}/seats/${id}`,);
return response.data;
}
export const getSeatLayout=async()=>{
const response=await axios.get(`${BASE_URL}/seatLayout`);
return response.data;
}
export const getReservedSeats=async()=>{
const response=await axios.get(`${BASE_URL}/seats`);
const allSeats=response.data;
const bookedIDs=allSeats
.filter((seatObj)=>seatObj.status === 'booked').flatMap((seatObj)=>seatObj.seats);
return bookedIDs
}
export const register = async ({ mobile,name }) => {
const response = await axios.post(`${BASE_URL}/users`, {id:mobile, Mobile: mobile,Name:name, });
return response.data;
};
export const updateSeat = async ({ id, seats,userId}) => {
const response = await axios.put(`${BASE_URL}/seats/${id}`, { userId, seats,status:"booked" });
return response.data;
};
export const getUserReservedSeats = async (userId) => {
const response = await axios.get(`${BASE_URL}/seats`, {
params: { userId },
});
const record=response.data.find(seatObj=> seatObj.status === 'booked');
return record || null;
};
import { Routes, Route } from "react-router-dom";
import "../global.css";
import SeatBooking from "./pages/SeatBooking";
import { SeatLimit } from "./pages/SeatLimit";
import { Summary } from "./components/Response/Index";
import { LoginPage } from "./pages/Forms";
import { ContextProvider } from "./context/Index";
function App() {
return (
<ContextProvider>
<Routes>
<Route path="/signup" element={<LoginPage />} />
<Route path="/" element={<LoginPage />} />
<Route path="/seat-limit" element={<SeatLimit />} />
<Route path="/booking" element={<SeatBooking />} />
<Route path="booking-summary" element={<Summary />} />
</Routes>
</ContextProvider>
);
}
export default App;
import styles from "./Button.module.css";
export const Button = ({
backgroundColor,
children,
className,
onClick,
...props
}) => {
return (
<button
style={{ backgroundColor }}
className={`${styles.btn} ${className || ""}`}
onClick={onClick}
{...props}
>
{children}
</button>
);
};
.btn {
border: 1px solid grey;
height: 3rem;
border-radius: 6px;
padding: 1rem;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
import { Button } from "./Button";
export default {
title: "Base/Button",
component: Button,
tags: ["autodocs"],
};
export const button = {
args: {
children: "Submit",
},
};
import styles from "./Styles.module.css";
export const Input = ({ value, onChange, placeholder, type, ...props }) => {
return (
<div className={styles.input}>
<input
value={value}
placeholder={placeholder}
type={type}
onChange={onChange}
{...props}
/>
</div>
);
};
import { Input } from "./Input";
export default {
title: 'Base/Input',
component: Input,
tags: ['autodocs'],
};
export const input={
render:(args)=><Input {...args}/>,
args:{
placeholder:'Enter the Text',
}
}
export const number={
render:(args)=><Input {...args}/>,
args:{
placeholder:'Enter Your Number',
}
}
input {
padding: 1rem;
border-radius: 0.3rem;
border: 1px solid rgb(190, 190, 190);
box-sizing: border-box;
font-size: 1rem;
width: 100%;
margin-bottom: 1rem;
}
input:focus {
outline: none;
}
.input input {
display: flex;
flex-direction: start;
}
import React, { useState } from "react";
import { Header } from "../../Layout/Card/Header";
import { Input } from "../../Form/form/Input";
import { Button } from "../../Form/Button/Button";
import styles from "./Login.module.css";
import { toast, ToastContainer } from "react-toastify";
import Home from "../../../Assets/Login.png";
import { Link } from "react-router-dom";
import { Sign } from "../SignUp/Sign";
export const Login = ({ onClick, onSwitch }) => {
const [value, setValue] = useState("");
const [islesser, setIsLesser] = useState(false);
const InputChange = (e) => {
setValue(e.target.value);
};
const HandleButton = (e) => {
e.preventDefault();
if (value.length < 10) {
setIsLesser(true);
toast.error("Mobile Number is less than 10");
} else {
console.log("onClick prop:", onClick);
onClick({ mobile: value });
setIsLesser(false);
setValue("");
}
};
return (
<>
<div className={styles.container}>
<div className={`${styles.back} ${styles.round}`}>
<Header title="Login Page" Color="#000" className={styles.head} />
<img src={Home} className={styles.img} />
<div className={styles.form}>
<div className={styles.inp}>
<Input
placeholder="Enter Your Number"
type="text"
value={value}
onChange={InputChange}
required
/>
</div>
{islesser && (
<span className={styles.span}>Please enter 10 digits</span>
)}
<div className={styles.btn}>
<Button
children="Submit"
onClick={HandleButton}
disabled={value.trim().length === 0}
/>
<div className={styles.foot}>
Don't have an account?{" "}
<span
style={{ color: "blue", cursor: "pointer" }}
onClick={onSwitch}
>
Sign Up
</span>
</div>
</div>
</div>
</div>
<ToastContainer />
</div>
</>
);
};
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap");
@import url("https://fonts.googleapis.com/icon?family=Material+Icons");
body {
font-family: "Poppins", sans-serif;
}
.container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.back {
background-color: #f7fafc;
padding: 2rem;
border-radius: 0.75rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
width: 100%;
max-width: 28rem;
text-align: center;
}
.round {
border-radius: 0.75rem;
}
.head {
font-size: 1.875rem;
font-weight: 700;
color: #2d3748;
margin-bottom: 2rem;
}
.img {
width: 16rem;
height: auto;
margin-bottom: 2rem;
}
.form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.inp {
margin-bottom: 0.3rem;
}
.span {
color: #e53e3e;
font-size: 0.75rem;
text-align: left;
margin-top: -0.5rem;
margin-bottom: 1rem;
}
.btn {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.foot {
font-size: 0.875rem;
color: #4a5568;
margin-top: 1rem;
}
import { Login } from "./Login";
export default {
title: "Forms/Login",
component: Login,
tags: ["autodocs"],
argTypes: {
onClick: { action: "User" },
onChange: { action: "change" },
},
parameters: {
layout: "centered",
},
};
export const login = {
render: (args) => <Login {...args} />,
args: {
value: "",
onLogin: (user) => {
console.log("login", user);
},
},
};
import React, { useState } from "react";
import { Input } from "../../Form/form/Input";
import { Button } from "../../Form/Button/Button";
import { Header } from "../../Layout/Card/Header";
import style from "./Styles.module.css";
import { ToastContainer } from "react-toastify";
import { toast } from "react-toastify";
export const Sign = ({ onClick, onChange, onSwitch }) => {
const [num, setNum] = useState("");
const [name, setName] = useState("");
const Number = (e) => {
const updated = e.target.value;
if (onChange) onChange(updated);
console.log("executed");
if (/^\d{0,10}$/.test(updated)) {
setNum(updated);
}
};
const Name = (e) => {
const User = e.target.value;
setName(User);
if (onChange) {
onChange(e);
}
};
const handleNewUser = () => {
console.log("signup page worked");
if (!num || !name) {
toast.error("Please enter both Name and Number ");
return;
}
if (!/^[6-9]\d{9}$/.test(num)) {
toast.info("Please enter a valid 10-digit number");
return;
}
const confirmSignUp = window.confirm("Are you sure you want to Sign Up?");
if (!confirmSignUp) return;
console.log("Calling onClick:", { mobile: num, name });
if (onClick) {
onClick({ mobile: num, name });
} else {
console.log("onclick not defined in sign");
}
setName("");
setNum("");
if (onSwitch) {
onSwitch();
}
};
return (
<>
<div className={style.container}>
<div className={style.block}>
<div className={style.image}>
<Header title="Sign-Up Page" Color="#000" className={style.head} />
</div>
<div className={style.signcontainer}>
<div className={style.Inpbtn}>
<Input
placeholder="Enter Your Mobile Number"
value={num}
onChange={Number}
type="text"
pattern="[6-9][0-9]{9}"
min="0"
/>
</div>
<div className={style.Inpname}>
<Input
placeholder="Enter Your Name"
value={name}
onChange={Name}
type="text"
maxLength={255}
/>
</div>
<div className={style.signbtn}>
<Button
children="Submit"
size="medium"
primary="true"
onClick={handleNewUser}
/>
</div>
</div>
</div>
<ToastContainer />
</div>
</>
);
};
import { Sign } from "./Sign";
export default {
title: "Forms/SignUp",
component: Sign,
argTypes: {
onClick: { action: "Sign-Up" },
},
parameters: {
layout: "centered",
},
};
export const signUp = (args) => <Sign {...args} />;
.container {
font-family: "Poppins", sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 0;
margin: 0;
}
.block {
background-color: #ffffff;
border-radius: 1rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
padding: 1.2rem;
max-width: 600px;
width: 100%;
}
.image {
text-align: center;
margin-bottom: 1rem;
}
.head {
font-size: 2rem;
font-weight: bold;
color: #1f2937;
}
.signcontainer {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.Inpbtn,
.Inpname {
margin-bottom: 0.4rem;
}
.signbtn {
display: flex;
justify-content: center;
}
.signbtn button {
width: 100%;
padding: 0.75rem 1rem;
border: none;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 500;
color: #ffffff;
background-color: #6366f1;
cursor: pointer;
transition: background-color 0.2s ease;
}
.signbtn button:hover {
background-color: #4f46e5;
}
@media (min-width: 768px) {
.container {
padding: 2rem;
}
.block {
max-width: 400px;
padding: 2rem;
}
}
// import styles from './Styles.module.css';
export const Header=({title,Color,className})=>{
return(
<div className={className}>
<h2 style={{color:Color}}>{title}</h2>
</div>
)
}
import { Header } from "./Header";
export default {
title: "Title/title",
component: Header,
argTypes: {},
parameters: {
layout: "center",
},
};
export const title = {
render: (args) => <Header {...args} />,
args: {
title: "Choose Your Seats",
Color: "#fff",
},
};
import styles from './Legend.module.css'
export const Legend=(className,...props)=>{
return(
<ul className={`${styles.legend} ${className}`}{...props}>
<li><span className={`${styles.dot} ${styles.selected}`}></span>Selected</li>
<li><span className={`${styles.dot} ${styles.reserved}`}></span>Reserved</li>
<li><span className={`${styles.dot} ${styles.available}`}></span>Available</li>
</ul>
);
};
\ No newline at end of file
.legend {
margin-top: 3rem;
list-style: none;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 1rem;
color: #fff;
}
.legend li span {
display: inline-block;
height: 11px;
width: 11px;
border-radius: 50%;
}
.dot {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
}
.selected {
background-color: rgba(3, 229, 223, 255);
}
.reserved {
background-color: #c5ccf8;
}
.available {
border: 1px solid #fff;
border-radius: 50%;
}
li {
margin-top: 2px;
}
@media (min-width: 768px) {
.legend {
margin-top: 3rem;
list-style: none;
display: flex;
flex-direction: row;
justify-content: center;
gap: 6rem;
color: #fff;
}
}
import React from "react";
import styles from "./Styles.module.css";
import { useLocation, useNavigate } from "react-router-dom";
import { Button } from "../Form/Button/Button";
import { IoMdLogOut } from "react-icons/io";
import Failed from "../../Assets/Failed.png";
import QRCode from "../../Assets/QRcode.png";
import Ticket from "../../Assets/Success.png";
export const Summary = ({ success: propSuccess }) => {
const navigate = useNavigate();
const location = useLocation();
const currentDateTime = new Date().toLocaleString();
const success = propSuccess ?? location.state?.success;
const onLogout = () => {
console.log("exit");
navigate("/");
};
return (
<div>
{success === true ? (
<div className={styles.success}>
{console.log("Worked")}
<img src={Ticket} alt="Success" className={styles.symbol} />
<h2 className={styles.hd}>🎉 Hooray!!! Successfully Booked</h2>
<img src={QRCode} alt="QR-Code" className={styles.qr} />
<p className={styles.timeStamp}>Booked On: {currentDateTime}</p>
<div className={styles.logout}>
<Button onClick={onLogout}>
<IoMdLogOut className={styles.logoutIcon} />
</Button>
</div>
</div>
) : (
<div className={styles.error}>
{console.log("Failed")}
<img src={Failed} alt="Failed" className={styles.symbol} />
<h2>Something Went Wrong</h2>
<p className={styles.timeStamp}>Booked On: {currentDateTime}</p>
<div className={styles.logout}>
<Button onClick={onLogout}>
<IoMdLogOut className={styles.logoutIcon} />
</Button>
</div>
</div>
)}
</div>
);
};
import { BrowserRouter } from "react-router-dom";
import { Summary } from "./Index";
export default {
title: "Response/Response",
component: Summary,
tags: ["autodocs"],
argTypes: {
success: { control: "boolean" },
},
parameters: {
layout: "centered",
},
decorators: [
(Story) => (
<BrowserRouter>
<Story />
</BrowserRouter>
),
],
};
export const SuccessState = {
args: {
success: true,
},
};
export const ErrorState = {
args: {
success: false,
},
};
.success,
.error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
text-align: center;
min-height: 80vh;
background-color: #ffffff;
}
.symbol {
width: 380px;
height: 300px;
margin-bottom: 1.5rem;
}
.hd {
font-size: 1.5rem;
font-weight: 700;
color: #28a745; /* Success Green */
margin-bottom: 1rem;
}
.error .hd {
color: #dc3545; /* Error Red */
}
.qr {
width: 160px;
height: auto;
margin: 1rem 0;
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 0.5rem;
}
.timeStamp {
font-size: 0.9rem;
color: #555;
margin: 1rem 0;
}
.logout {
margin-top: 1.5rem;
}
.logoutIcon {
font-size: 1.2rem;
color: #000;
}
import React, { useState } from "react";
import style from "./Styles.module.css";
import { Button } from "../../Form/Button/Button";
import { Input } from "../../Form/form/Input";
import { Header } from "../../Layout/Card//Header";
import { toast } from "react-toastify";
const Counter = ({ onSubmit, className, onChange }) => {
const [count, setCount] = useState(0);
console.log(onSubmit);
const Num = (e) => {
const value = Number(e.target.value);
console.log("number", value);
setCount(value);
if (onChange) {
onChange({ limit: value });
}
};
const Submit = () => {
const number = parseInt(count, 10);
if (number > 0) {
alert(`Selected Seats,${count}`);
onSubmit?.(number);
} else if (number === 0) {
toast.info("Please select seat");
} else {
toast.info("no seats selected");
}
};
return (
<>
<div className={style.container}>
<Header
title={`You have selected ${count} seats.`}
Color="#000"
className={style.hd}
/>
<div className={style.block}>
<Input
placeholder="Select the required seats"
type="number"
value={count}
onChange={Num}
min="0"
disabled={count >= 60}
/>
</div>
<div className={style.submitbtn}>
<Button
children="Submit"
onClick={Submit}
className={`${style.submit} ${className}`}
/>
</div>
</div>
</>
);
};
export default Counter;
import Counter from "./Counter";
export default {
title: "Top Level",
component: Counter,
parameters: {
layout: "centered",
},
argTypes: {
onClick: { action: "Seat-Selecteds" },
},
};
export const SeatLimit = {
render: (args) => <Counter {...args} />,
};
body {
font-family: "Poppins", sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem;
margin: 0;
}
.container {
width: 100%;
max-width: 15rem;
background-color: #ffffff;
padding: 1.5rem;
border-radius: 1rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.hd {
text-align: center;
margin-bottom: 1.5rem;
font-size: 1.125rem;
font-weight: 500;
color: #000000;
}
.block {
margin-bottom: 1.5rem;
}
.block input[type="number"] {
width: 100%;
padding: 0.75rem 1rem;
text-align: center;
font-size: 1.125rem;
color: #374151;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.block input[type="number"]:focus {
outline: none;
border-color: #4f46e5;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.4);
}
.submitbtn {
margin-top: 1rem;
}
.submit {
width: 100%;
background-color: #4f46e5;
color: #ffffff;
font-weight: 600;
padding: 0.75rem 1rem;
border: none;
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
transition: transform 0.15s ease, background-color 0.15s ease,
box-shadow 0.15s ease;
cursor: pointer;
}
.submit:hover {
background-color: #4338ca;
transform: scale(1.05);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.submit:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.5);
}
@media (min-width: 768px) {
.container {
max-width: 28rem;
}
}
import React, { useState } from 'react'
import { Header } from '../../Layout/Card/Header'
import {Screen} from '../Screen/Screen';
import { Index } from '../Seatposition/Index';
import styles from './Styles.module.css'
import { Button } from '../../Form/Button/Button';
import { Legend } from '../../Layout/Legend/Legend';
export const Layout = () => {
const[selectedseat,SetSelectedSeat]=useState([]);
const Confirm=()=>{
if(selectedseat.length > 0){
alert('Are You Sure To Book Tickets');
}
else{
alert("Choose Minimun One Ticket To Proceed");
}
}
return (
<>
<Header className={styles.head} title="Choose Your Seats" Color="white"/>
<div className={styles.center}>
<div className={styles.screen}>
<Screen/>
</div>
<div className={styles.seat}>
<Index selectedseat={selectedseat} SetSelectedseat={SetSelectedSeat} className='seats'/>
</div>
</div>
<div className={styles.btn}>
<Button backgroundColor="white"
label="Confirm"
primary={true}
onClick={Confirm}
/>
</div>
<div className='legend'>
<Legend/>
</div>
</>
)
}
* {
margin: 0;
padding: 0;
}
body {
width: 100%;
height: 60svh;
}
.head {
text-align: center;
margin-top: 25rem;
}
.seat {
overflow: visible;
position: relative;
}
.seat::before {
content: attr(data-tooltip);
position: relative;
bottom: 120%;
left: 50%;
transform: translateX(-50%);
color: #fff;
padding: 3px 6px;
font-size: 12px;
white-space: nowrap;
border-radius: 4px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease-in-out;
z-index: 10;
}
.seat:hover::before {
opacity: 1;
}
.screen {
display: flex;
justify-content: center;
margin-bottom: -5rem;
position: relative;
}
.center {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
width: 100%;
}
.btn {
margin-top: 4rem;
display: flex;
justify-content: center;
height: 3rem;
}
button {
height: 3rem;
width: 25%;
border-radius: 10px;
cursor: pointer;
font-weight: 700;
}
.legend {
display: flex;
justify-content: center;
align-items: center;
}
.log {
display: flex;
justify-content: end;
}
import React from "react";
import style from "./Styles.module.css";
export const Screen = () => {
return (
<svg
width="550"
height="210"
viewBox="0 0 300 140"
xmlns="http://www.w3.org/2000/svg"
className={style.screen}
>
<defs>
<linearGradient id="lightGradient" x1="0.5" y1="0" x2="0.5" y2="1">
<stop offset="0%" stopColor="#484ccf" />
<stop offset="70%" stopColor="#4145c8" />
<stop offset="100%" stopColor="#3b3fc4" />
</linearGradient>
<filter id="blur" x="-25%" y="-25%" width="150%" height="150%">
<feGaussianBlur stdDeviation="6" />
</filter>
</defs>
<path
d="M30 60 Q150 45 270 60 L285 130 L10 130 Z"
fill="url(#lightGradient)"
/>
<path
d="M30 60 Q150 45 270 60"
stroke="#fff"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
);
};
import styles from "./Seat.module.css";
import PropTypes from "prop-types";
import cn from "classnames";
export const Seat = ({ seatNo, status, onChange, ...props }) => {
const handleChange = () => {
if (status === "available" || status === "selected") {
onChange?.({
seatNo,
status,
});
}
};
const className = cn(styles.seat, {
[styles.available]: status === "available",
[styles.reserved]: status === "reserved",
[styles.selected]: status === "selected",
[styles.myseat]: status === "myseat",
});
return (
<span
className={className}
data-tooltip={seatNo}
onClick={(e) => handleChange(e)}
{...props}
/>
);
};
Seat.propTypes = {
seatNo: PropTypes.string.isRequired,
status: PropTypes.oneOf(["available", "reserved", "selected", "myseat"]),
onChange: PropTypes.func,
};
.seat {
display: inline-block;
width: 18px;
height: 18px;
border: 1px solid #fff;
border-radius: 4px;
margin: 5px;
cursor: pointer;
}
.available {
background-color: rgba(52, 68, 197, 255);
}
.reserved {
background-color: #c5ccf8;
}
.selected {
background-color: #01fff7;
}
.myseat {
background-color: #01fff7;
cursor: default;
pointer-events: none;
border: 2px solid #004080;
color: white;
}
.noSeat {
background-color: transparent;
border: none;
}
[data-tooltip] {
position: relative;
cursor: pointer;
}
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: 105%;
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: #fff;
padding: 5px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
z-index: 1;
}
[data-tooltip]:hover::after {
opacity: 1;
}
@media (min-width: 768px) {
.seat {
display: inline-block;
width: 25px;
height: 25px;
border: 1px solid #fff;
border-radius: 4px;
margin: 5px;
cursor: pointer;
}
}
import { Seat as SeatComponent } from "./Seat";
export default {
title: "Top Level/Seat",
component: SeatComponent,
tags: ["autodocs"],
argTypes: {
onChange: { action: "OnChange" },
status: {
options: ["available", "reserved", "selected"],
control: { type: "radio" },
},
},
parameters: {
layout: "centered",
},
};
export const Seat = {
args: {
seatNo: "A1",
status: "available",
},
};
import { useState, useEffect } from "react";
import { Seat } from "../Seat/Seat.jsx";
import styles from "./Styles.module.css";
import { toast, ToastContainer } from "react-toastify";
import { Button } from "../../Form/Button/Button.jsx";
export const SeatSelectionComponent = ({
reservedSeats = [],
selectedSeats = [],
seatLimit,
onClick,
seats = [],
}) => {
const [selectedseats, setSelectedSeats] = useState([...selectedSeats]);
useEffect(() => {
setSelectedSeats([...selectedSeats]);
}, [selectedSeats]);
const controlSeat = (seat) => {
if (reservedSeats.includes(seat)) {
toast.info("You can't select this seat because it already reserved!!");
return;
}
if (!selectedseats.includes(seat) && selectedseats.length < seatLimit) {
setSelectedSeats([...selectedseats, seat]);
} else if (selectedseats.includes(seat)) {
setSelectedSeats(selectedseats.filter((e) => e !== seat));
} else {
toast.info(`You can't select more than ${seatLimit} seat(s).`);
}
};
const ConfirmSeat = (e) => {
e.preventDefault();
if (selectedseats.length < seatLimit) {
toast.error(`Please select atleast ${seatLimit} seat(s).`);
} else {
console.log("Triggered onChange with:", { seats: selectedseats });
onClick({
seats: selectedseats,
clearSelection: () => setSelectedSeats([]),
});
}
};
const isSeatDisabled = (seat) =>
reservedSeats.includes(seat) && !selectedSeats.includes(seat);
return (
<div className={styles.container}>
<div className={styles["seat-container"]}>
{seats?.map((row, rowIndex) => (
<div key={rowIndex} className={styles.seat}>
{row?.map((seatNo, seatIndex) => (
<div
key={seatIndex}
className={seatNo === 0 ? styles["remove"] : ""}
>
<Seat
seatNo={seatNo}
onChange={() => {
if (!reservedSeats.includes(seatNo) && seatNo !== 0) {
controlSeat(seatNo);
}
}}
status={
selectedseats.includes(seatNo)
? "selected"
: reservedSeats.includes(seatNo)
? "reserved"
: "available"
}
disabled={isSeatDisabled(seatNo)}
/>
</div>
))}
</div>
))}
<div className={styles.btn}>
<Button
children="Confirm"
backgroundColor="#fff"
onClick={ConfirmSeat}
/>
</div>
</div>
<ToastContainer />
</div>
);
};
SeatSelectionComponent.defaultProps = {
seats: [],
reservedSeats: [],
selectedSeats: [],
};
import { SeatSelectionComponent } from "./Index";
export default {
title: "Top-Level/Seat-Selection",
component: SeatSelectionComponent,
parameters: {
layout: "centered",
},
argTypes: {
onClick: { action: "onClick" },
},
};
export const SeatSelection = {
args:{
seats:[
[0, "A1", "A2", "A3", 0, "A4", "A5", "A6", 0],
["B1", "B2", "B3", "B4", 0, "B5", "B6", "B7", "B8"],
["C1", "C2", "C3", "C4", 0, "C5", "C6", "C7", "C8"],
["D1", "D2", "D3", "D4", 0, "D5", "D6", "D7", "D8"],
["E1", "E2", "E3", "E4", 0, "E5", "E6", "E7", "E8"],
["F1", "F2", "F3", "F4", 0, "F5", "F6", "F7", "F8"],
["G1", "G2", "G3", "G4", 0, "G5", "G6", "G7", "G8"],
[0, "H1", "H2", "H3", 0, "H4", "H5", "H7", 0],
],
seatLimit:2,
reservedSeats:["A1"],
selectedSeats:[],
},
};
.container {
display: flex;
flex-direction: column;
gap: 30px;
margin-top: 2rem;
}
.seatcontainer {
display: grid;
grid-template-columns: repeat(9, 30px);
grid-template-rows: repeat(8, 5px);
place-content: center;
}
.seat {
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
user-select: none;
transition: background-color 0.2s ease;
padding: 0.5rem;
gap: 1px;
}
.seat::before {
content: attr(data-tooltip);
position: relative;
bottom: 120%;
left: 50%;
transform: translateX(-50%);
color: #fff;
padding: 3px 6px;
font-size: 12px;
white-space: nowrap;
border-radius: 4px;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease-in-out;
z-index: 10;
}
.seat:hover::before {
opacity: 1;
}
.available {
border: 1px solid #fff;
background-color: transparent;
}
.selected {
background-color: #01fff7;
}
.reserved {
background-color: #c5ccf8;
cursor: not-allowed;
opacity: 0.7;
pointer-events: none;
}
.remove {
visibility: hidden;
}
.btn {
margin-top: 2rem;
}
.btn button {
display: flex;
justify-content: center;
border-radius: 1rem;
max-width: 45%;
margin: 0 auto;
}
import { createContext, useContext, useEffect, useState } from "react";
import * as api from "../Api/Api";
import { toast } from "react-toastify";
import { useNavigate } from "react-router-dom";
export const AppContext = createContext();
export const useAppContext = () => useContext(AppContext);
export function ContextProvider({ children }) {
const [userSeats, setUserSeats] = useState([]);
const [reservedSeats, setReservedSeats] = useState([]);
const [seatLayout, setSeatLayout] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [seatLimit, setSeatLimit] = useState(
Number(sessionStorage.getItem("seatLimit")) || 0
);
const navigate = useNavigate();
console.log(reservedSeats);
const loginOrRegister = async ({ mobile, name }) => {
try {
const user = await api.loginUser(mobile);
if (!user) {
console.log("registering:", mobile, name);
const response = await api.register({ mobile, name });
console.log("Registered:", response);
sessionStorage.setItem("userId", response?.id);
await getAllReservedSeats();
navigate("/seat-limit");
return response;
} else {
sessionStorage.setItem("userId", user?.id);
const response = await getAllReservedSeats();
const userSelectedSeats = response.find((e) => e.userId == user?.id);
sessionStorage.setItem(
"seatLimit",
userSelectedSeats?.seats?.length || 0
);
navigate("/booking");
return user;
}
} catch (err) {
toast.error(err.message);
}
};
const addOrUpdateSeats = async ({ id, seats }) => {
try {
const validSeats = seats.filter((s) => typeof s === "string" && s.trim());
if (validSeats.length === 0) {
toast.error("No valid seats selected.");
return;
}
const latestReserved = await api.getReservedSeats();
const conflictingSeats = validSeats.filter(
(seat) => latestReserved.includes(seat) && !userSeats.includes(seat)
);
if (conflictingSeats.length > 0) {
toast.error(
`Seats already taken: ${conflictingSeats.join(
", "
)}. Please choose different seats.`
);
return;
}
let response;
if (!userSeats.length) {
response = await api.saveSeat({ id, seats: validSeats });
setReservedSeats((prev) => [...prev, ...validSeats]);
setUserSeats(validSeats);
} else {
response = await api.updateSeat({ id, seats: validSeats });
setUserSeats(response.seats);
}
const [allReserved, userReserved] = await Promise.all([
api.getReservedSeats(),
api.getUserReservedSeats(id),
]);
setReservedSeats(allReserved);
setUserSeats(userReserved);
} catch (err) {
toast.error(err.message || "Something went wrong.");
}
};
const getAllReservedSeats = async () => {
setIsLoading(false);
try {
const userId = sessionStorage.getItem("userId");
const reserved = await api.getReservedSeats();
const userReserved = await api.getUserReservedSeats(userId);
setReservedSeats(reserved);
setUserSeats(userReserved);
} catch (err) {
toast.error(err.message);
}
};
const getSeatLayout = async () => {
setIsLoading(false);
try {
const response = await api.getSeatLayout();
setSeatLayout(response);
setIsLoading(true);
} catch (err) {
toast.error(err.message);
}
};
useEffect(() => {
const storedLimit = Number(sessionStorage.getItem("seatLimit")) || 0;
if (storedLimit !== seatLimit) {
setSeatLimit(storedLimit);
}
getAllReservedSeats();
}, []);
console.log("state reserved", reservedSeats);
console.log("Selected Seats:", reservedSeats.length);
const value = {
userSeats,
reservedSeats,
isLoading,
seatLayout,
loginOrRegister,
addOrUpdateSeats,
getAllReservedSeats,
getSeatLayout,
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
import { StrictMode } from "react";
import App from "./App.jsx";
import { BrowserRouter } from "react-router-dom";
import ReactDOM from "react-dom/client";
ReactDOM.createRoot(document.getElementById("root")).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
import { useEffect, useState } from "react";
import { useAppContext } from "../context/Index";
import { Login } from "../components/Forms/Login/Login";
import { Sign } from "../components/Forms/SignUp/Sign";
import styles from "./Styles.module.css";
import { useNavigate } from "react-router-dom";
export const LoginPage = () => {
const navigate = useNavigate();
const { loginOrRegister } = useAppContext();
const [showLogin, setShowLogin] = useState(true);
useEffect(() => {
sessionStorage.clear();
}, []);
const handlelogin = async ({ mobile }) => {
await loginOrRegister({ mobile });
navigate("/seat-limit");
};
const handleSignup = async ({ name, mobile }) => {
const res = await loginOrRegister({ name, mobile });
console.log("registered", res);
setShowLogin(true);
};
return (
<div className={styles.login}>
{showLogin ? (
<Login onClick={handlelogin} onSwitch={() => setShowLogin(false)} />
) : (
<Sign onClick={handleSignup} onSwitch={() => setShowLogin(true)} />
)}
</div>
);
};
import { useState, useEffect } from "react";
import { Header } from "../components/Layout/Card/Header";
import {
getSeatLayout,
getReservedSeats,
getUserReservedSeats,
saveSeat,
updateSeat,
} from "../Api/Api";
import { Legend } from "../components/Layout/Legend/Legend";
import { SeatSelectionComponent } from "../components/Top-level/SeatSelection/Index";
import style from "./Styles.module.css";
import { useNavigate } from "react-router-dom";
import { Screen } from "../components/Top-level/Screen/Screen";
import { toast } from "react-toastify";
function SeatBooking() {
const navigate = useNavigate();
const userId =
sessionStorage.getItem("userId") || localStorage.getItem("userId");
const storedLimit = sessionStorage.getItem("seatLimit");
const initialLimit = storedLimit ? Number(storedLimit) : 0;
const [seatLayout, setSeatLayout] = useState([]);
const [reservedSeats, setReservedSeats] = useState([]);
const [userReservedSeats, setUserReservedSeats] = useState([]);
const [userSeatRecordId, setUserSeatRecordId] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
async function fetchSeatData() {
try {
setLoading(true);
setError(null);
const [layoutData, allReserved, userRecord] = await Promise.all([
getSeatLayout(),
getReservedSeats(),
userId ? getUserReservedSeats(userId) : Promise.resolve(null),
]);
if (isMounted) {
setSeatLayout(layoutData);
setReservedSeats(allReserved);
if (userRecord) {
setUserReservedSeats(userRecord.seats || []);
setUserSeatRecordId(userRecord.id || null);
}
}
} catch (err) {
if (isMounted) {
setError("Failed to load seat data. Please try again.");
console.error("Error fetching seat data:", err);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
}
fetchSeatData();
return () => {
isMounted = false;
};
}, [userId]);
const handleConfirm = async ({ seats, clearSelection }) => {
try {
const validSeats = seats.filter((s) => typeof s === "string" && s.trim());
if (validSeats.length < initialLimit) {
toast.info(`Please select ${initialLimit} seat(s).`);
return;
}
const latestReservedSeats = await getReservedSeats();
const conflictingSeats = validSeats.filter(
(seat) =>
latestReservedSeats.includes(seat) &&
!userReservedSeats.includes(seat)
);
if (conflictingSeats.length > 0) {
toast.error(
`The following seat(s) were just taken:${conflictingSeats.join(",")}`
);
return;
}
let response;
if (userSeatRecordId) {
response = await updateSeat({
id: userSeatRecordId,
userId,
seats: validSeats,
});
} else {
response = await saveSeat({ userId, seats: validSeats });
setUserSeatRecordId(response.id);
}
const [allReserved, userRecord] = await Promise.all([
getReservedSeats(),
getUserReservedSeats(userId),
]);
setReservedSeats(allReserved);
setUserReservedSeats(userRecord?.seats || []);
sessionStorage.setItem("selectedSeats", JSON.stringify(validSeats));
if (typeof clearSelection === "function") clearSelection();
navigate("/booking-summary", { state: { success: true } });
} catch (error) {
console.error("Error saving seats:", error);
toast.error("Failed to reserve seats. Please try again.");
}
};
if (loading) {
return (
<div className={style.loadingContainer}>
<div className={style.spinner}></div>
<p style={{ color: "#fff" }}>Loading seat data...</p>
</div>
);
}
if (error) {
return (
<div className={style.errorContainer}>
<p>{error}</p>
<button onClick={() => window.location.reload()}>Retry</button>
</div>
);
}
return (
<div className={style.pageContainer}>
<div className={style.head}>
<Header title="Choose Your Seats" Color="#fff" />
</div>
<div className={style.screen}>
<Screen className={style.mainscreen} />
</div>
<div className={style.container}>
<SeatSelectionComponent
key={userSeatRecordId || "new-user"}
seats={seatLayout}
reservedSeats={reservedSeats.filter(
(s) => !userReservedSeats.includes(s)
)}
selectedSeats={userReservedSeats}
seatLimit={initialLimit}
userId={userId}
onClick={handleConfirm}
/>
</div>
<div className={style.footer}>
<Legend />
</div>
</div>
);
}
export default SeatBooking;
import React from "react";
import Counter from "../components/Top-level/Counter/Counter";
import { useAppContext } from "../context/Index";
import styles from "./Styles.module.css";
export const SeatLimit = () => {
const totalSeats = 60;
const { reservedSeats } = useAppContext();
const Seat = totalSeats - reservedSeats.length;
const handleSeat = (seatCount) => {
console.log("handleSeat called with seatCount:", seatCount);
sessionStorage.setItem("seatLimit", seatCount);
window.location.replace("/booking");
};
return (
<>
<p className={styles.line}>Remaining Seats Available:{Seat}</p>
<Counter onSubmit={handleSeat} />
</>
);
};
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
@import url("https://fonts.googleapis.com/icon?family=Material+Icons");
.head {
display: flex;
justify-content: center;
text-align: center;
margin-top: 0;
margin-bottom: 1.2rem;
}
.container {
display: flex;
justify-content: center;
align-items: center;
}
.login {
min-height: 20rem;
}
.logout {
display: flex;
justify-content: end;
height: 0;
align-items: center;
margin-left: 20%;
}
.footer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.screen {
display: flex;
justify-content: center;
margin-bottom: -7rem;
position: relative;
min-width: 20rem;
}
.line {
font-size: 1rem;
color: #c7d2fe;
font-weight: 500;
margin-bottom: 1rem;
text-align: center;
font-family: "Inter", sans-serif;
}
.line p {
text-align: center;
}
@media (min-width: 768px) {
.container {
display: flex;
justify-content: center;
align-items: center;
overflow-x: auto;
}
.mainscreen {
width: 550;
height: 210;
}
.login {
min-width: 40rem;
}
}
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: true,
port: 5173
}
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment