Commit a2582eb2 by Manivasagam S

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-essentials",
"@storybook/addon-onboarding",
"@chromatic-com/storybook",
"@storybook/experimental-addon-test"
],
"framework": {
"name": "@storybook/react-vite",
"options": {}
}
};
export default config;
\ No newline at end of file
/** @type { import('@storybook/react').Preview } */
import '../src/index.css';
const preview = {
parameters: {
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#333333' },
{ name: 'primary-bg', value: '#ffffff' }, // White for Primary
{ name: 'secondary-bg', value: '#3444c5' }, // Blue for Secondary
],
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
import { beforeAll } from 'vitest';
import { setProjectAnnotations } from '@storybook/react';
import * as projectAnnotations from './preview';
// This is an important step to apply the right configuration when testing your stories.
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
const project = setProjectAnnotations([projectAnnotations]);
beforeAll(project.beforeAll);
\ 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.
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 },
],
},
},
]
<!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>Vite + React</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": "training-seatbooking",
"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-confetti": "^6.4.0",
"react-confetti-explosion": "^3.0.0",
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.6.0",
"react-toastify": "^11.0.5"
},
"devDependencies": {
"@chromatic-com/storybook": "^3.2.6",
"@eslint/js": "^9.25.0",
"@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-onboarding": "^8.6.14",
"@storybook/blocks": "^8.6.14",
"@storybook/experimental-addon-test": "^8.6.14",
"@storybook/react": "^8.6.14",
"@storybook/react-vite": "^8.6.14",
"@storybook/test": "^8.6.14",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"@vitest/browser": "^3.1.4",
"@vitest/coverage-v8": "^3.1.4",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-storybook": "^0.12.0",
"globals": "^16.0.0",
"json-server": "^1.0.0-beta.3",
"playwright": "^1.52.0",
"prop-types": "^15.8.1",
"storybook": "^8.6.14",
"vite": "^6.3.5",
"vitest": "^3.1.4"
},
"eslintConfig": {
"extends": [
"plugin:storybook/recommended"
]
}
}
<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>
\ No newline at end of file
import axios from "axios";
const API_URL = "http://192.168.1.59:3000/users";
export const getAllUsers = () => axios.get(API_URL);
export const getUserByPhone = (phoneNumber) =>
axios.get(`${API_URL}?phoneNumber=${phoneNumber}`);
export const updateUserReservedSeats = (userId, reservedSeats) =>
axios.patch(`${API_URL}/${userId}`, { reservedSeats });
export const postUser=async(userData)=>{
return await axios.post(API_URL,userData);
}
\ No newline at end of file
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Selectseat } from './Components/Top-level/Seatselect/Selectseat.jsx';
import { Success } from '../src/Components/Top-level/Response/Success/Success.jsx';
import './App.css';
import { AuthPage } from './Pages/AuthPage.jsx';
import { ProtectedRoute } from './auth/ProtectedRoute.jsx';
import { Logoutpage } from './Pages/Logoutpage.jsx';
export const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<AuthPage />} />
<Route
path="/select-seat"
element={
<ProtectedRoute>
<Logoutpage/>
</ProtectedRoute>
}
/>
<Route
path="/success"
element={
<ProtectedRoute>
<Success />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
);
};
import PropTypes from 'prop-types';
import styles from './Button.module.css';
import clsx from 'clsx';
export const Button = ({ variant, size, label, loading, ...props }) => {
return (
<button
className={clsx(
styles.button,
styles[variant],
styles[size],
loading && styles.loading
)}
disabled={loading || props.disabled}
{...props}
>
{loading ? 'Loading...' : label}
</button>
);
};
Button.propTypes = {
variant: PropTypes.oneOf(['primary', 'secondary']).isRequired,
size: PropTypes.oneOf(['sm', 'md', 'lg']),
label: PropTypes.string.isRequired,
loading: PropTypes.bool,
};
Button.defaultProps = {
size: 'md',
loading: false,
};
/*
.primary{
padding: 12px 0;
background-color: #5b9bd5;
color: white;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s ease;
min-width: 25rem;
}
.secondary{
padding: 12px;
min-width: 12rem;
border-radius: 15px;
border: none;
font-size: 18px;
font-weight: 600;
cursor: pointer;
}
.Logout{
padding: 12px;
min-width: 5rem;
border-radius: 15px;
border: none;
font-size: 16px;
font-weight: 600;
cursor: pointer;
position: relative;
right:1rem;
top:10px;
}
.Increment{
padding: 10px;
width: 4.5rem;
}
.Decrement{
padding: 10px;
width: 3.5rem;
} */
.buttoncolumn {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.button {
font-weight: 700;
font-size: 1rem;
border: none;
cursor: pointer;
transition: background-color 0.3s ease;
display: inline-block;
padding:10px;
font-weight: 600;
}
.primary {
background-color: var(--primary);
color: white;
border-radius: 6px;
width: 100%;
}
.primary:hover {
background-color: var(--primary);
}
.primary:disabled {
background-color: #a0c4e3;
cursor: not-allowed;
opacity: 0.7;
}
.secondary {
background-color: var(--secondary);
color: black;
border-radius: 15px;
min-width: 12rem;
}
.secondary:hover {
background-color: #e0e0e0;
}
.secondary:disabled {
background-color: #cdcaca;
cursor: not-allowed;
}
.sm {
font-size: 0.875rem;
padding: 8px 16px;
}
.md {
font-size: 1.2rem;
padding: 12px 20px;
}
.lg {
font-size: 1.125rem;
padding: 16px 24px;
}
.loading{
cursor: wait !important;
}
\ No newline at end of file
import { Button } from './Button';
import styles from '../Buttons/Button.module.css';
export default {
title: 'Base/Button',
component: Button,
argTypes: {
onClick: { action: 'clicked' },
},
};
export const Primary = () => (
<div className={styles.buttoncolumn}>
<Button variant="primary" label="Default" />
<Button variant="primary" label="Loading..." loading="true" />
<Button variant="primary" label="Disabled" disabled />
<Button variant="primary" label="Small" size="sm" />
<Button variant="primary" label="Medium" size="md" />
<Button variant="primary" label="Large" size="lg" />
</div>
);
Primary.parameters = {
backgrounds: { default: 'primary-bg' ,value:"white"}, // white
};
export const Secondary = () => (
<div className={styles.buttoncolumn}>
<Button variant="secondary" label="Default" />
<Button variant="secondary" label="Loading..." loading="true" />
<Button variant="secondary" label="Disabled" disabled />
<Button variant="secondary" label="Small" size="sm" />
<Button variant="secondary" label="Medium" size="md" />
<Button variant="secondary" label="Large" size="lg" />
</div>
);
Secondary.parameters = {
backgrounds: { default: 'secondary-bg' }, // blue
};
import React from 'react'
import styles from "./Input.module.css"
import { ErrorFormatter } from 'storybook/internal/components'
export const Input =({type='text',placeholder='',isInvalid=false,onChange,value='',errorMessage="",variant,...props}) => {
return (
<div >
<input className={`${styles.inputbox} ${isInvalid && styles.invalid}`} type={type} placeholder={placeholder} onChange={onChange} {...props}/>
{errorMessage && <p className={styles.error} style={{
color:'red'
}}>{errorMessage}</p>}
</div>
)
}
.inputbox{
padding: 12px 14px;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 6px;
outline-offset: 2px;
outline-color: #99a2e6;
transition: border-color 0.5s ease;
width:100%;
box-sizing: border-box;
}
/* .count{
padding:10px;
min-width: 10px;
width: 5rem;
} */
.invalid{
border: 1px solid red;
}
.invalid:focus{
outline-color: red;
}
.error{
/* margin-top: 10px; */
font-size: small;
font-family: 'Segoe UI';
text-align: left;
}
\ No newline at end of file
import { background } from "storybook/internal/theming";
import { Input } from "./Input";
export default {
title: "Base/Input",
component: Input,
argTypes: {
onChange: { action: "onChange" },
},
};
export const Default = {
args: {
type: "text",
placeholder: "Enter mobile number",
variant: "inputbox",
},
render: (args) => (
<div>
<p>Without error message:</p>
<Input {...args} />
<p>With error message:</p>
<Input
{...args}
isInvalid={true}
errorMessage="This field is required"
/>
</div>
),
};
import styles from "../Modal/Modal.module.css";
export const Modal = ({
title = "Choose the Number of Seats",
children,
onClose,
}) => {
return (
<div className={styles.modalcontainer}>
<div className={styles.modalcard}>
<h2 className={styles.modalheading}>{title}</h2>
{children}
</div>
</div>
);
};
.modalcontainer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(13, 13, 13, 0.4);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 1rem;
box-sizing: border-box;
}
.modalcard {
background-color: rgb(250, 248, 248);
padding: 2rem;
border-radius: 8px;
text-align: center;
max-width: 30rem;
width: 100%;
margin-inline: 1rem;
position: relative;
z-index: 1001;
box-sizing: border-box;
}
.modalheading {
font-size: 24px;
font-weight: 600;
text-align: center;
color: #1f2937;
margin-bottom: 16px;
}
.modalform {
display: flex;
flex-direction: column;
gap: 16px;
}
.modalselect {
padding: 10px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
outline: none;
transition: border-color 0.3s;
width: 100%;
box-sizing: border-box;
}
.modalselect:focus {
border-color: #2563eb;
}
.modalbutton {
background-color: #2563eb;
color: white;
padding: 10px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
width: 100%;
}
.modalbutton:hover {
background-color: #1d4ed8;
}
import { Modal } from "./Modal";
import { SeatSelectForm } from "../../Form/SeatSelectForm/SeatSelectForm";
// import { action } from 'storybook/actions';
export default {
title: "Base/Modal",
component: Modal,
argTypes: {
title: { control: "text" },
},
};
const availableSeats=7;
const seatOptions = Array.from({ length: availableSeats }).map((_, i) => ({
value: i + 1,
label: `${i + 1} ${i + 1 === 1 ? "seat" : "seats"}`,
}));
const seatCount=1;
export const Default = {
args:{
title:"choose the Number of seats",
onClose:() => console.log("Modal closed"),
children:(availableSeats > 0 ? (
<SeatSelectForm
seatOptions={seatOptions}
seatCount={seatCount}
/>
) : (
<p>No seats available</p>
))
},
};
import styles from "../Screen/Screen.module.css"
export const Screen = ({ width = 600, height = 210 }) => (
<div className={styles.shadow}>
<svg width={width} height={height} viewBox="0 0 300 120" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="lightGradient" x1="0.5" y1="0" x2="0.5" y2="1">
<stop offset="0%" stopColor="#484CCD" stopOpacity="1" />
<stop offset="70%" stopColor="#4347CA" stopOpacity="0.3" />
<stop offset="100%" stopColor="#3E41C8" stopOpacity="0" />
</linearGradient>
</defs>
<path d="M30 60 Q150 36 270 60 L285 130 L10 130 Z" fill="url(#lightGradient)" />
<path d="M30 60 Q150 36 270 60" stroke="#fff" strokeWidth="2" fill="none" strokeLinecap="round" />
</svg>
</div>
);
.shadow{
display: flex;
justify-content: center;
align-items: center;
width: 100%;
min-width: 30rem;
position: relative;
bottom: 1rem;
}
\ No newline at end of file
import { Screen } from "./Screen";
export default{
title:'Shared/Screen',
component:Screen,
parameters: {
backgrounds: {
default: 'blue-bg',
values: [{ name: 'blue-bg', value: '#3444c5' }],
},
}
}
export const Default=(args)=><Screen {...args}/>
\ No newline at end of file
import styles from '../Select/Select.module.css';
export const Select = ({ label, options, value, onChange }) => {
return (
<div className={styles.container}>
{label && <label className={styles.label}>{label}</label>}
<select className={styles.select} value={value} onChange={onChange} >
{options.map((opt, index) => (
<option key={index} value={opt.value} className={styles.option}>
{opt.label}
</option>
))}
</select>
</div>
);
};
.container {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-family: Arial, sans-serif;
}
.label {
font-weight: bold;
}
.select {
padding: 0.5rem;
font-size: 1rem;
border-radius: 4px;
}
import React from 'react';
import { Select } from './Select';
import { action } from '@storybook/addon-actions';
export default {
title: 'Base/Select',
component: Select,
argTypes: {
label: { control: 'text' }, // Makes 'label' editable in Storybook controls
onChange: { action: 'onChange'},
},
};
export const Default = (args) => {
const options = [
{ value: '1', label: '1 seat' },
{ value: '2', label: '2 seats' },
{ value: '3', label: '3 seats' },
];
return (
<Select
{...args}
options={options}
onChange={(e) => {
args.onChange(e);
}}
/>
);
};
Default.args = {
label: 'Choose a seat',
};
import { useState } from "react";
import { Input } from "../../Base/Input/Input";
import { Button } from "../../Base/Buttons/Button";
import styles from "./Login.module.css";
export const Login = ({
loginHeading = "Login With Your Mobile Number",
placeholder = "Enter Mobile Number",
onLogin,
}) => {
const [phoneNumber, setPhoneNumber] = useState("");
const [error, setError] = useState("");
const validatePhoneNumber = (number) => {
const phoneRegex = /^\+?\d{10,15}$/;
return phoneRegex.test(number);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!phoneNumber.trim()) {
setError('Phone number is required');
return;
}
if(phoneNumber.length<10){
setError('phone number contain 10 numbers!!');
return;
}
if (!validatePhoneNumber(phoneNumber)) {
setError('Please enter only numbers');
return;
}
try {
await onLogin?.(phoneNumber);
} catch (err) {
setError('Login failed. Please try again.');
}
};
const handleInputChange = (e) => {
setPhoneNumber(e.target.value);
if (error) {
setError('');
}
};
return (
<div className={styles.formcontainer}>
{/* <div className={styles.leftcontainer}>
<h1 className={styles.text}>{heading}</h1>
<p className={styles.content}>{description}</p>
<button>{buttonLabel}</button>
</div> */}
{/* <h2 className={styles.headerNew}>{loginHeading}</h2> */}
<form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.input}>
<Input
id="phone-number"
type="tel"
value={phoneNumber}
variant="inputbox"
placeholder={placeholder}
onChange={handleInputChange}
isInvalid={!!error}
errorMessage={error}
/>
</div>
<Button variant="primary" label="Login" size="md" />
</form>
</div>
);
};
.formcontainer {
/* max-width: 400px; */
/* margin: 60px auto; */
/* padding: 24px 32px; */
/* border: 1px solid #ddd; */
border-radius: 8px;
/* background-color: #fff; */
/* box-shadow: 0 2px 8px rgba(0,0,0,0.1); */
}
.headerNew {
font-size: 1.7rem;
font-family:'Segoe UI';
color: black;
margin-bottom: 24px;
text-align: center;
}
.form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form input[type="text"]:focus {
border-color: #5b9bd5;
}
.form button:hover {
background-color:rgb(52, 74, 199);
}
import { Login } from "./Login";
export default {
title: "Toplevel/Login",
component: Login,
parameters: {
backgrounds: { default: 'light',value:'F8F8F8' }
},
argTypes: {
onLogin: {
action: "clicked",
},
},
};
export const LoginForm = {
args: {
loginHeading: "Login With Your Mobile Number",
placeholder: "Enter Mobile Number",
},
render: (args) => <Login {...args} />,
};
import styles from "../../Form/SeatSelectForm/SeatSelectForm.module.css"
import {Select} from "../../Base/Select/Select.jsx"
import {Button} from "../../Base/Buttons/Button.jsx"
import PropTypes from "prop-types";
export const SeatSelectForm = ({ seatOptions, seatCount, setSeatCount, onClose }) => {
return (
<form className={styles.modalform} onSubmit={(e) => e.preventDefault()}>
<Select
options={seatOptions}
value={seatCount}
onChange={(e) => setSeatCount(parseInt(e.target.value))}
/>
<Button variant="primary" label="Continue" size="md" onClick={onClose} />
</form>
);
};
SeatSelectForm.propTypes = {
seatOptions: PropTypes.array.isRequired,
seatCount: PropTypes.number.isRequired,
setSeatCount: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
.modalform {
display: flex;
flex-direction: column;
gap: 16px;
min-width: auto;
}
.modalselect {
padding: 10px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
outline: none;
transition: border-color 0.3s;
width: 100%;
box-sizing: border-box;
}
.modalselect:focus {
border-color: #2563eb;
}
.modalbutton {
background-color: #2563eb;
color: white;
padding: 10px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.modalbutton:hover {
background-color: #1d4ed8;
}
import React, { useState } from 'react';
import { Input } from '../../Base/Input/Input';
import { Button } from '../../Base/Buttons/Button';
import styles from './SignUp.module.css';
// import signup from '../../../assets/Signup.png';
import { getUserByPhone, postUser } from '../../../Api/userApi';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
export const SignUp = ({
onSubmit,
label = "SignUp",
namePlaceholder = "Name",
emailPlaceholder = "Email",
phonePlaceholder = "Phone Number",
}) => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
const [errors, setErrors] = useState({ name: '', email: '', phoneNumber: '' });
const handleNameChange = (e) => {
setName(e.target.value);
setErrors((prev) => ({ ...prev, name: '' }));
};
const handleEmailChange = (e) => {
setEmail(e.target.value);
setErrors((prev) => ({ ...prev, email: '' }));
};
const handlePhoneChange = (e) => {
setPhoneNumber(e.target.value);
setErrors((prev) => ({ ...prev, phoneNumber: '' }));
};
const handleSubmit = async (e) => {
e.preventDefault();
const newErrors = {};
if (!name.trim()) newErrors.name = 'Name is required';
if (!email.trim()) {
newErrors.email = 'Email is required';
} else {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
newErrors.email = 'Enter a valid email address';
}
}
if (!phoneNumber.trim()) {
newErrors.phoneNumber = 'Phone number is required';
} else if (phoneNumber.length < 10) {
newErrors.phoneNumber = 'Phone number must contain at least 10 digits';
} else {
const phoneRegex = /^\+?\d{10,15}$/;
if (!phoneRegex.test(phoneNumber)) {
newErrors.phoneNumber = 'Phone number must contain only digits';
}
}
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
try {
const res = await getUserByPhone(phoneNumber);
if (res.data.length > 0) {
setErrors((prev) => ({
...prev,
phoneNumber: 'Phone number already registered',
}));
return;
}
const formData = { name, email, phoneNumber, reservedSeats: [] };
const postRes = await postUser(formData);
setName("");
setEmail("");
setPhoneNumber("");
if (postRes.status === 201) {
toast.success("Account created successfully!");
if (onSubmit) onSubmit(formData);
} else {
toast.error("Something went wrong. Please try again.");
}
} catch (err) {
console.error('Error submitting form:', err);
toast.error("Network error or server not available.");
}
}
};
return (
<div className={styles.SignUpContainer}>
<div className={styles.wrapper}>
{/* <div
className={styles.leftPane}
style={{ backgroundImage: `url(${signup})` }}
></div> */}
<div className={styles.rightPane}>
{/* <h1 className={styles.text}>Create Account</h1> */}
<form onSubmit={handleSubmit} className={styles.form}>
<Input
type="text"
placeholder={namePlaceholder}
value={name}
onChange={handleNameChange}
errorMessage={errors.name}
isInvalid={!!errors.name}
/>
<Input
type="text"
placeholder={emailPlaceholder}
value={email}
onChange={handleEmailChange}
errorMessage={errors.email}
isInvalid={!!errors.email}
/>
<Input
type="text"
placeholder={phonePlaceholder}
value={phoneNumber}
onChange={handlePhoneChange}
errorMessage={errors.phoneNumber}
isInvalid={!!errors.phoneNumber}
/>
<Button variant="primary" label={label} size="md" />
</form>
</div>
</div>
</div>
);
};
.SignUpContainer{
width: 100%;
}
.wrapper{
background-color: white;
}
.rightPane{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
/* padding: 1rem; */
margin: 0 auto;
/* max-width: 30rem; */
border-radius: 2rem;
}
.text {
margin-bottom: 2rem;
color: #00020c;
font-family: 'Segoe UI';
}
.form {
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 100%;
}
import { SignUp } from "./SignUp.jsx";
import React from "react";
export default {
title: "Toplevel/SignUp",
component: SignUp,
parameters: {
backgrounds: {
default: "white",
values: [
{ name: "white", value: "#ffffff" },
],
},
// layout: "fullscreen"
},
argTypes: {
onSubmit: { action: "onSubmit" },
label: { control: "text"},
heading:{control:"text"},
namePlaceholder: { control: "text", defaultValue: "Name" },
emailPlaceholder: { control: "text", defaultValue: "Email" },
phonePlaceholder: { control: "text", defaultValue: "Phone Number" },
},
};
export const SignUpForm = (args) => <SignUp {...args} />;
SignUpForm.args = {
label: "Sign Up",
heading:"Create Account",
namePlaceholder: "Name",
emailPlaceholder: "Email",
phonePlaceholder: "Phone Number"
};
import PropTypes from "prop-types";
import styles from "../Notification/Notification.module.css";
import { Button } from "../../Base/Buttons/Button";
import successImg from "../../../assets/success.png";
import errorImg from "../../../assets/giferror.gif";
import cn from "classnames";
import { AiOutlineLogout } from "react-icons/ai";
export const Notification = ({ title, type, msg, button, }) => {
const selectedImage = type === "success" ? successImg : errorImg;
// const onLogout=()=>{
// window.location.replace("/");
// }
return (
<>
{/* <div className={styles.logoutButton}>
<AiOutlineLogout onClick={onLogout}/>
</div> */}
<div className={styles.container}>
<span className={styles.span}>
<img
src={selectedImage}
alt={type}
className={cn(styles.imageEffect, {
[styles.success]: type === "success",
[styles.error]: type === "error",
})}
/>
</span>
<h2 className={styles.title}>{title}</h2>
<p className={styles.msg}>{msg}</p>
{button && button}
</div>
</>
);
};
Notification.propTypes = {
type: PropTypes.oneOf(["success", "error"]).isRequired,
title: PropTypes.string.isRequired,
msg: PropTypes.string.isRequired,
button: PropTypes.element,
};
.logoutButton{
display: flex;
justify-content: flex-end;
position: relative;
top: 2rem;
right: 1.5rem;
color: white;
font-size: 2rem;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
margin-top:12rem;
text-align: center;
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
animation: fadeIn 0.5s ease-in-out;
width: 100%;
height: 100%;
}
.container:hover {
transform: scale(1.02);
opacity: 0.95;
}
.title {
margin-bottom: 0.5em;
font-size: 1.5rem;
color:white;
font-weight: bold;
font-family: 'Segoe UI';
}
.span {
font-size: 4rem;
margin-bottom: 0.5rem;
}
.success {
color: #2ecc71; /* green */
}
.error {
color: #e74c3c; /* red */
}
.msg {
font-size: 1.5rem;
color:white;
margin-top: 0.5rem;
line-height: 1.5;
font-family: 'Segoe UI';
font-weight: bold;
margin-bottom: 2rem;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.success {
width: 150px;
height: auto;
animation: rotate 1s linear , glow 7s ease-in-out alternate;
}
@keyframes rotate {
0% {
transform: rotate(250deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes glow {
from {
filter: drop-shadow(0 0 5px #ffffff);
}
to {
filter: drop-shadow(0 0 20px #eff0ef);
}
}
.error{
height: 150px;
width: 200px;
}
\ No newline at end of file
import { Notification } from '../Notification/Notification.jsx';
import { Button } from '../../Base/Buttons/Button.jsx';
export default {
title: 'TopLevel/Notification',
component: Notification,
parameters: {
backgrounds: {
default: 'blue-bg',
values: [{ name: 'blue-bg', value: '#3444c5' }],
},
},
argTypes: {
title: { control: 'text' },
msg: { control: 'text' },
type: { control: { type: 'select' }, options: ['success', 'error'] },
button: { control: false }, // button is a JSX element, not editable from controls
},
};
export const Success = (args) => <Notification {...args} />;
Success.args = {
title: 'Success!!!',
type: 'success',
msg: 'Successfully booked your ticket!',
button: (
<Button
variant="secondary"
label="Book more seats"
size="md"
onClick={() => {
console.log("Book more clicked");
}}
/>
),
};
export const Error = (args) => <Notification {...args} />;
Error.args = {
title: 'Failed 😢',
type: 'error',
msg: 'Retry to book your ticket.',
button: (
<Button
variant="secondary"
label="Go back"
size="md"
onClick={() => {
console.log("Go back clicked");
}}
/>
),
};
import { Notification } from "../../Notification/Notification"
export const Error = () => {
const response={
title:'Failed😢',
type:'error',
msg:'Failed to book your ticket',
}
return <Notification {...response} button={<Button variant="secondary"
label="Back Home"
size="md" />}/>
}
import { useEffect, useState } from 'react';
import Confetti from 'react-confetti';
import { Notification } from "../../Notification/Notification";
import { Button } from "../../../Base/Buttons/Button";
export const Success = () => {
const response = {
title: 'Success!!!',
type: 'success',
msg: 'Successfully booked your ticket!',
};
const [showConfetti, setShowConfetti] = useState(true);
const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });
useEffect(() => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
const timer = setTimeout(() => {
setShowConfetti(false);
}, 20000);
return () => clearTimeout(timer);
}, []);
const handleSuccess = () => {
window.location.replace('/select-seat');
};
return (
<>
{showConfetti && (
<Confetti width={windowSize.width} height={windowSize.height} />
)}
<Notification {...response} button={<Button variant="secondary"
label="Book more seats"
size="md" onClick={handleSuccess}/>}/>
<div
style={{ display: "flex", justifyContent: "center", marginTop: "1rem" }}
onClick={handleSuccess}
>
</div>
</>
);
};
import styles from '../Seat-legend/Legend.module.css'
export const Legend = ({
labels = {
selectedLabel: "Selected",
reservedLabel: "Reserved",
availableLabel: "Available",
},
}) => {
const { selectedLabel, reservedLabel, availableLabel } = labels;
return (
<div className={styles.legend}>
<div className={styles.legendItem}>
<span className={`${styles.seat} ${styles.selected}`}></span> {selectedLabel}
</div>
<div className={styles.legendItem}>
<span className={`${styles.seat} ${styles.reserved}`}></span> {reservedLabel}
</div>
<div className={styles.legendItem}>
<span className={`${styles.seat} ${styles.available}`}></span> {availableLabel}
</div>
</div>
);
};
.seat {
display: inline-block;
width: 10px;
height: 10px;
background-color: transparent;
border: 2px solid #ccc;
border-radius: 15px;
margin: 2px;
cursor: pointer;
display: flex;
}
.selected {
background-color: #00e4ff;
border-color: #00e4ff;
}
.reserved {
background-color:rgb(91,99,198);
border-color: rgb(91,99,198);
}
.available {
background-color: transparent;
}
.legend {
display: flex;
justify-content: space-between;
justify-content: center;
align-items: center;
margin:0 auto;
margin-top: 15px;
gap: 35px;
width: 100%;
/*
flex-direction: column; */
}
.legendItem {
display: inline-flex;
align-items: center;
gap: 10px;
font-size: 14px;
color: white;
font-family: 'Segoe UI';
font-weight: 500;
font-size: 17px;
}
@media (min-width:640px) {
.legend{
flex-direction: row;
/* column-gap: 0; */
column-gap: 150px;
justify-content: center;
align-items: center;
}
}
\ No newline at end of file
import { Legend } from "./Legend";
export default {
title: 'Toplevel/SeatLegend',
component: Legend,
parameters: {
backgrounds: {
default: 'blue-bg',
values: [
{ name: 'blue-bg', value: '#3444c5' },
],
},
layout: "centered",
},
};
export const Default = (args) => <Legend {...args} />;
Default.args = {
labels:{
selectedLabel: "Selected",
reservedLabel: "Reserved",
availableLabel: "Available",
}
};
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './Seat.module.css';
export const Seat = ({ seatNo, status, onClick }) => {
const className = cn(styles.seat, {
[styles.noSeat]: status === 'none',
[styles.available]: status === 'available',
[styles.reserved]: status === 'reserved',
[styles.selected]: status === 'selected',
});
return (
<span
className={className}
data-tooltip={seatNo}
role="button"
tabIndex={status === 'available' || status === 'selected' ? 0 : -1}
onClick={onClick}
>
{null}
</span>
);
};
Seat.propTypes = {
seatNo: PropTypes.string.isRequired,
status: PropTypes.oneOf(['none', 'available', 'reserved', 'selected']),
onClick: PropTypes.func,
};
.seat {
display: inline-block;
background-color: transparent;
border: 0.5px solid rgba(164,175,255,255);
width: 27px;
height: 27px;
border-radius: 5px;
margin: 8px;
cursor: pointer;
transition: transform 0.2s ease-in-out;
position: relative;
box-shadow:2px 4px 8px rgba(0,0,0,0.2) ;
}
.available:hover {
background-color: lab(52.35% 1.51 0);
transform: scale(1.1);
}
.selected {
background-color:#01fff7;
border-color:#01fff7 ;
}
.reserved {
background-color:#5b66cb;
}
.reserved:hover {
transform: none;
cursor: not-allowed;
}
.noSeat {
display: inline-block;
width: 30px;
height: 30px;
margin: 5px;
cursor: context-menu;
background: transparent;
border: none;
visibility: hidden;
}
.seat::before,
.seat::after {
visibility: hidden;
opacity: 0;
pointer-events: none;
position: absolute;
left: 50%;
transition: opacity 0.2s;
}
.seat::before {
bottom: 150%;
margin-left: -30px;
padding: 5px;
width: 60px;
border-radius: 3px;
background-color: #f7f4f4;
color: #080808;
content: attr(data-tooltip);
text-align: center;
font-size: 12px;
line-height: 1.2;
}
.seat::after {
bottom: 125%;
margin-left: -5px;
width: 0;
border-top: 5px solid #fffcfc;
border-right: 5px solid transparent;
border-left: 5px solid transparent;
content: ' ';
font-size: 0;
line-height: 0;
}
.seat:hover::before,
.seat:hover::after {
visibility: visible;
opacity: 1;
}
import React from 'react';
import { Seat } from './Seat';
export default {
title: 'Toplevel/Seat',
component: Seat,
parameters:{
backgrounds: {
default: 'blue-bg',
values: [
{ name: 'blue-bg', value: '#3444c5' },
],
},
},
argTypes: {
status: {
control: {
type: 'select',
options: ['available', 'selected', 'reserved'],
},
},
seatNo: { table:{disable:true}},
// onSelect: { action: 'selected seat' },
onClick: { action: 'onClick' },
},
};
const Template = (args) => {
const seats = ["A1"];
return (
<div style={{ display: 'flex',justifyContent:"center",marginTop:"13rem"}}>
{seats.map((seatNo) => (
<Seat key={seatNo} {...args} seatNo={seatNo} />
))}
</div>
);
};
export const Seats = Template.bind({});
Seats.args = {
status: 'available',
seatNo:"A1",
};
import { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Seat } from '../Seat/Seat.jsx';
import styles from './SeatLayout.module.css';
import { seatsData } from '../SeatLayout/SeatsData.js';
const populateSeatsArray = (selectedSeats, allReservedSeats) => {
return seatsData.map(({ row, seats }) =>
seats.map((number) => {
const seatNo = row + number;
if (number === '/') {
return { seatNo, status: 'none' };
} else if (selectedSeats.includes(seatNo)) {
return { seatNo, status: 'selected' };
} else if (allReservedSeats.includes(seatNo)) {
return { seatNo, status: 'reserved' };
} else {
return { seatNo, status: 'available' };
}
})
);
};
export const SeatLayout = ({
initialReservedSeats = [],
allReservedSeats = [],
limit,
onSelectionChange,
onSubmit,
}) => {
const [selectedSeats, setSelectedSeats] = useState(initialReservedSeats);
useEffect(() => {
onSelectionChange?.(selectedSeats);
}, [selectedSeats, onSelectionChange]);
// useEffect(() => {
// setSelectedSeats(initialReservedSeats);
// }, [initialReservedSeats]);
const seatClickHandler = (seat) => {
if (seat.status === 'available' || seat.status === 'selected') {
setSelectedSeats((prevSelected) => {
let newSelected;
let actionType;
if (prevSelected.includes(seat.seatNo)) {
newSelected = prevSelected.filter((s) => s !== seat.seatNo);
actionType = 'unclicked';
} else if (prevSelected.length < limit) {
newSelected = [...prevSelected, seat.seatNo];
actionType = 'clicked';
} else {
return prevSelected;
}
onSubmit?.(actionType);
return newSelected;
});
}
};
const renderedSeats = populateSeatsArray(selectedSeats, allReservedSeats);
console.log(selectedSeats);
return (
<div className={styles.container}>
<div>
{renderedSeats.map((rowSeats, i) => (
<div key={i} className={styles.seatcontainer}>
{rowSeats.map((seat, j) => (
<Seat key={j} onClick={() => seatClickHandler(seat)} {...seat} />
))}
</div>
))}
</div>
</div>
);
};
SeatLayout.propTypes = {
allReservedSeats: PropTypes.arrayOf(PropTypes.string),
initialReservedSeats: PropTypes.arrayOf(PropTypes.string),
limit: PropTypes.number,
onSelectionChange: PropTypes.func,
};
.container{
display: flex;
justify-content: center;
margin-bottom: 5rem;
}
.seatcontainer {
display: flex;
align-items: center;
margin-bottom: 4px;
gap: 8px;
padding: 2px 2px;
}
.seatcontainer:last-child{
margin-left:8px;
}
.seatcontainer:first-child{
margin-left: 4px;
}
/* .seats {
width: 23px;
height: 23px;
margin: 0 2px;
text-align: center;
line-height: 30px;
border-radius: 6px;
border: 2px solid #999;
} */
@media (max-width:640px) {
.seatcontainer{
gap: 3px;
padding: 1px 1px;
}
}
\ No newline at end of file
import { SeatLayout } from "./SeatLayout";
export default {
title: 'Toplevel/SeatLayout',
component: SeatLayout,
parameters:{
backgrounds: {
default: 'blue-bg',
values: [
{ name: 'blue-bg', value: '#3444c5' }, // blue background
],
},
layout:"centered"
},
argTypes: {
seatType: {
control: {
type: 'radio',
},
options: ['available', 'reserved', 'selected']
},
limit: {
control: { type: 'number', min: 1, max: 10 },
},
onSubmit: { action: 'clicked/unclicked' },
onSelectionChange: { action: 'selectedseats' },
},
};
export const Default= ({ seatType, limit, onSubmit, onSelectionChange }) => {
let allReservedSeats = [];
let initialReservedSeats = [];
if (seatType === 'reserved') {
allReservedSeats = ['B1', 'B2', 'B3'];
} else if (seatType === 'selected') {
initialReservedSeats = ['C1', 'C2','C3'];
}
return (
<SeatLayout
key={seatType}
allReservedSeats={allReservedSeats}
initialReservedSeats={initialReservedSeats}
limit={limit}
onSubmit={onSubmit}
onSelectionChange={onSelectionChange}
/>
);
};
Default.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],
],
seatType: 'available',
limit: 3,
};
export const seatsData = [
{
row: 'H',
seats: ['/', 1, 2, 3, '/', 4, 5, 6, '/'],
},
{
row:'G',
seats:[1, 2, 3, 4, '/', 5, 6, 7, 8]
},
{
row: 'F',
seats: [1, 2, 3, 4, '/', 5, 6, 7, 8],
},
{
row: 'E',
seats: [1, 2, 3, 4, '/', 5, 6, 7, 8],
},
{
row: 'D',
seats: [1, 2, 3, 4, '/', 5, 6, 7, 8],
},
{
row: 'C',
seats: [1, 2, 3, 4, '/', 5, 6, 7, 8],
},
{
row: 'B',
seats: [1, 2, 3, 4, '/', 5, 6, 7, 8],
},
{
row: 'A',
seats: ['/', 1, 2, 3, '/', 4, 5, 6, '/'],
},
]
\ No newline at end of file
import { useEffect, useState } from "react";
import { getAllUsers, updateUserReservedSeats } from "../../../Api/userApi";
import { Button } from "../../Base/Buttons/Button";
import { Legend } from "../Seat-legend/Legend";
import { SeatLayout } from "../SeatLayout/SeatLayout";
import { getCurrentUser } from "../../../auth/authService";
import styles from "./Selectseat.module.css";
import "react-toastify/dist/ReactToastify.css";
import { toast, ToastContainer } from "react-toastify";
import { Modal } from "../../Base/Modal/Modal";
import { SeatSelectForm } from "../../Form/SeatSelectForm/SeatSelectForm";
import { Screen } from "../../Base/Screen/Screen";
import { AiOutlineLogout } from "react-icons/ai";
export const Selectseat = ({onLogout}) => {
const [selectedSeats, setSelectedSeats] = useState([]);
const [allReservedSeats, setAllReservedSeats] = useState([]);
const [seatCount, setSeatCount] = useState(1);
const [availableSeats, setAvailableSeats] = useState(0);
const [showSelect, setShowSelect] = useState(true);
useEffect(() => {
getAllUsers()
.then(res => {
const users = res.data;
const reserved = users
.flatMap(u => u.reservedSeats || [])
.filter(seat => seat);
setAllReservedSeats(reserved);
const totalSeats=60;
const available=totalSeats-reserved.length;
setAvailableSeats(available);
})
.catch(err => {
console.error("Error fetching users", err);
toast.error("Failed to load seat data.");
});
}, []);
const confirmHandler = () => {
const user = getCurrentUser();
if (!user || !user.phoneNumber?.trim()) {
toast.warn("User not logged in");
return;
}
if (selectedSeats.length < seatCount) {
toast.info(`Please select ${seatCount} seats`);
return;
}
getAllUsers()
.then(res => {
const users = res.data;
const currentUser = users.find(u => u.phoneNumber === user.phoneNumber);
if (!currentUser) {
toast.error("User not found");
return;
}
const latestReserved = users
.flatMap(u => u.reservedSeats || []);
const conflictSeats = selectedSeats.filter(seat =>
latestReserved.includes(seat)
);
if (conflictSeats.length > 0) {
toast.error(
`The following seat(s) were just taken: ${conflictSeats.join(", ")}`
);
setSelectedSeats([]);
return;
}
const updatedSeats = Array.from(
new Set([...(currentUser.reservedSeats || []), ...selectedSeats])
);
return updateUserReservedSeats(currentUser.id, updatedSeats);
})
.then(res => {
if (res) {
toast.success("Seats confirmed!");
setSelectedSeats([]);
setTimeout(() => {
window.location.replace("/success");
}, 1000);
}
})
.catch(err => {
console.error("Error confirming seats", err);
toast.error("Failed to confirm seats");
setTimeout(() => {
window.location.replace("/error");
}, 1000);
});
};
const handleModal = () => {
setShowSelect(false);
};
const seatOptions = Array.from({ length: availableSeats }).map((_, i) => ({
value: i + 1,
label: `${i + 1} ${i + 1 === 1 ? "seat" : "seats"}`
}));
return (
<>
<div className={styles.logoutButton}>
<AiOutlineLogout onClick={onLogout}/>
</div>
<div className={styles.container}>
{/* <div className={styles.logoutButton}><Button variant="secondary" label="Logout" size="sm" onClick={onLogout}/></div> */}
<h2 className={styles.text}>Choose Seats</h2>
<Screen/>
<div className={styles.seatcontainer}>
<SeatLayout
allReservedSeats={allReservedSeats}
onSelectionChange={setSelectedSeats}
limit={seatCount}
/>
</div>
<div className={styles.button}>
<Button variant="secondary" label="Confirm" size='md' onClick={confirmHandler} />
</div>
<div className={styles.legend}>
<Legend />
</div>
</div>
{showSelect && (
<Modal
title="Choose Number of Seats"
availableSeats={availableSeats}
seatCount={seatCount}
setSeatCount={setSeatCount}
onClose={handleModal}
>
<SeatSelectForm
seatOptions={seatOptions}
seatCount={seatCount}
setSeatCount={setSeatCount}
onClose={handleModal}
/>
</Modal>
)}
<ToastContainer position="top-right" autoClose={3000} />
</>
);
};
body {
background-color: #303fb6;
margin: 0;
font-family: 'Segoe UI';
}
.header {
text-align: center;
margin-top: 2rem;
color: white;
font-size: 1.5em;
position: relative;
left: 30px;
}
.text {
position: relative;
top:1rem;
text-align: center;
margin-top: 1rem;
color: white;
font-size: 2.3em;
padding: 0 1rem;
}
.button {
display: flex;
justify-content: center;
position: relative;
bottom: 5rem;
padding: 0 1rem;
}
/*
.shadow {
display: flex;
justify-content: center;
position: relative;
bottom: 1rem;
padding: 0 1rem;
} */
.seatcontainer {
position: relative;
bottom:4rem;
width: 100%;
overflow-x: auto;
overflow: visible !important;
}
/* Modal container */
.modalcontainer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(13, 13, 13, 0.4);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 1rem;
box-sizing: border-box;
}
.modalcard {
background-color: rgb(250, 248, 248);
padding: 2rem;
border-radius: 8px;
text-align: center;
max-width: 25rem;
width: 100%;
margin-inline: 1rem;
position: relative;
z-index: 1001;
box-sizing: border-box;
}
.modalheading {
font-size: 24px;
font-weight: 600;
text-align: center;
color: #1f2937;
margin-bottom: 16px;
}
.modalform {
display: flex;
flex-direction: column;
gap: 16px;
min-width: auto;
}
.modalselect {
padding: 10px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
outline: none;
transition: border-color 0.3s;
width: 100%;
box-sizing: border-box;
}
.modalselect:focus {
border-color: #2563eb;
}
.modalbutton {
background-color: #2563eb;
color: white;
padding: 10px;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.modalbutton:hover {
background-color: #1d4ed8;
}
.logoutButton{
display: flex;
justify-content: flex-end;
position: relative;
top: 2rem;
right: 1.5rem;
color: white;
font-size: 2rem;
}
/* .legend {
bottom: 2.5rem;
} */
@media (min-width:640px){
.text{
text-align: center;
}
}
\ No newline at end of file
export default {
title: 'Base/Typography'
};
export const TitleAndParagraph = () => (
<div className="container">
<h1>Heading</h1>
<h2>Heading</h2>
<h3>Heading</h3>
<h4>Heading</h4>
<h5>Heading</h5>
<h6>Heading</h6>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo et quae
consequuntur voluptatum quibusdam, autem velit quidem laborum, dignissimos
doloremque ullam eos fugit repellendus maiores eveniet repellat dolor eius
perspiciatis? Lorem ipsum dolor sit amet, consectetur adipisicing elit.
</p>
<p>
Quasi aliquam impedit magnam tempora doloribus voluptatibus, at ea dolores
facilis et quod hic, cumque repellendus quidem expedita explicabo
architecto, possimus minima!
</p>
</div>
);
\ No newline at end of file
import { useState } from "react";
import styles from "./Auth.module.css";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
export const Auth = ({ LoginComponent, SignUpComponent, onLoginSubmit, onSignUpSubmit }) => {
const [isLoginMode, setIsLoginMode] = useState(true);
return (
<div className={styles.container}>
<div className={styles.formCard}>
<h2 className={styles.header}>{isLoginMode ? "Login Form" : "SignUp Form"}</h2>
<div className={styles.tabContainer}>
<button
className={`${styles.tabButton} ${isLoginMode ? styles.activeTab : ""}`}
onClick={() => setIsLoginMode(true)}
>
Login
</button>
<button
className={`${styles.tabButton} ${!isLoginMode ? styles.activeTab : ""}`}
onClick={() => setIsLoginMode(false)}
>
Signup
</button>
</div>
{isLoginMode ? (
<div className={styles.login}>
<LoginComponent onLogin={onLoginSubmit} />
<a href="#" className={styles.forgotLink}>Forgot password?</a>
</div>
) : (
<div className={styles.signup}>
<SignUpComponent onSubmit={() => {
setIsLoginMode(true);
if (onSignUpSubmit) onSignUpSubmit();
}} />
</div>
)}
<div className={styles.signupPrompt}>
{isLoginMode ? "Not a member?" : "Already a member?"}{" "}
<a href="#" onClick={() => setIsLoginMode(!isLoginMode)}>
{isLoginMode ? "Signup now" : "Login"}
</a>
</div>
</div>
<ToastContainer position="top-right" autoClose={3000} />
</div>
);
};
body{
background: rgb(46, 46, 181);
}
.container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: Arial, sans-serif;
}
.formCard {
background: #fff;
padding: 40px 30px;
border-radius: 12px;
width: 350px;
max-width: 400px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
text-align: center;
margin-left: 2rem ;
margin-right: 2rem;
}
.header {
font-size: 24px;
font-weight: bold;
margin-bottom: 25px;
}
.tabContainer {
display: flex;
justify-content: space-between;
background: #f0f0f0;
border-radius: 30px;
margin-bottom: 20px;
padding: 5px;
}
.tabButton {
flex: 1;
padding: 10px;
border: none;
border-radius: 30px;
font-weight: bold;
background: transparent;
cursor: pointer;
transition:background-color 0.3s ease-in-out;
}
.activeTab {
background: linear-gradient(to right, #7e97ee, #1d2bab);
color: white;
}
/* .inputField {
width: 100%;
padding: 15px 15px;
margin: 10px 0;
border-radius: 6px;
border: 1px solid #ccc;
font-size: 14px;
outline: none;
} */
.forgotLink {
display: block;
color: #004e92;
font-size: 13px;
text-align: right;
text-decoration: none;
position: relative;
margin-top: 1rem;
/* bottom: 3rem; */
}
.signupPrompt {
margin-top: 15px;
font-size: 13px;
color: #333;
}
.signupPrompt a {
color: #004e92;
text-decoration: none;
font-weight: bold;
}
.login{
margin-bottom: 1rem;
}
\ No newline at end of file
import { AuthPage } from "../AuthPage"
export default{
title:'Auth/Auth',
component:AuthPage,
argTypes: {
onClick: {
action: "clicked",
},
},
}
export const Default=(args)=><AuthPage {...args}/>
\ No newline at end of file
import { Login} from "../Components/Form/Login/Login.jsx"
import { SignUp } from "../Components/Form/SignUp/SignUp";
import { toast } from "react-toastify";
import { Auth } from "./Auth/Auth.jsx";
export const AuthPage = () => {
const handleLogin = async (phoneNumber) => {
try {
const response = await fetch(`http://192.168.1.59:3000/users?phoneNumber=${phoneNumber}`);
const users = await response.json();
if (users.length > 0) {
const user = users[0];
localStorage.setItem("user", JSON.stringify(user));
window.location.replace("/select-seat");
} else {
toast.warn("User not found. Please register or check the number.");
}
} catch (error) {
console.error("Login error:", error);
toast.error("Failed to login. Please try again.");
}
};
return (
<Auth
LoginComponent={Login}
SignUpComponent={SignUp}
onLoginSubmit={handleLogin}
/>
);
};
import { Selectseat } from "../Components/Top-level/Seatselect/Selectseat";
import { logout } from "../auth/authService";
export const Logoutpage = () => {
const onLogout = () => {
logout();
};
return (
<Selectseat onLogout={onLogout}/>
);
};
import React from 'react';
import { Navigate } from 'react-router-dom';
export const ProtectedRoute = ({ children }) => {
const user = JSON.parse(localStorage.getItem("user"));
if (!user) {
return <Navigate to="/" replace />;
}
return children;
};
export const login = async (phoneNumber) => {
const response = await fetch(`http://localhost:3000/users?phoneNumber=${phoneNumber}`);
const users = await response.json();
if (users.length > 0) {
const user = users[0];
localStorage.setItem("user", JSON.stringify(user));
return user;
} else {
throw new Error("User not found");
}
};
export const getCurrentUser = () => {
const user = localStorage.getItem("user");
return user ? JSON.parse(user) : null;
};
export const logout = () => {
localStorage.removeItem("user");
window.location.replace('/');
return;
};
{
"users": [
{
"id": "1",
"phoneNumber": "9489569937",
"name": "Mani",
"reservedSeats": [
"G3",
"F3",
"F4",
"G2"
]
},
{
"id": "2",
"phoneNumber": "9361775481",
"name": "mani",
"reservedSeats": [
"B8",
"B7",
"B6",
"D5",
"B4",
"B3",
"E7"
]
}
]
}
\ No newline at end of file
*{
margin: 0;
padding: 0;
font-family: 'Segoe UI';
}
/* body{
background-color:#3444c5;
font-family: 'Segoe UI';
} */
:root{
--primary: #4958cf;
--secondary: #f1f1f1;
}
h1,h2,h3,h4,h5,h6,p{
font-family: 'Segoe UI';
}
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import {App} from '../src/App'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
export const generateAllSeats = () => {
const rows = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
const seats = [];
for (let row of rows) {
for (let i = 1; i <= 8; i++) {
seats.push(`${row}${i}`);
}
}
return seats;
};
export const mergeReservedSeats = (existing = [], newSeats = []) => {
return Array.from(new Set([...existing, ...newSeats]));
};
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: true, // same as --host
port: 5173, // or any port you want
}
})
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { defineWorkspace } from 'vitest/config';
import { storybookTest } from '@storybook/experimental-addon-test/vitest-plugin';
const dirname =
typeof __dirname !== 'undefined' ? __dirname : path.dirname(fileURLToPath(import.meta.url));
// More info at: https://storybook.js.org/docs/writing-tests/test-addon
export default defineWorkspace([
'vite.config.js',
{
extends: 'vite.config.js',
plugins: [
// The plugin will run tests for the stories defined in your Storybook config
// See options at: https://storybook.js.org/docs/writing-tests/test-addon#storybooktest
storybookTest({ configDir: path.join(dirname, '.storybook') }),
],
test: {
name: 'storybook',
browser: {
enabled: true,
headless: true,
name: 'chromium',
provider: 'playwright'
},
setupFiles: ['.storybook/vitest.setup.js'],
},
},
]);
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