From a7695b57544ca92de0ec668ffc41e1fd094d657b Mon Sep 17 00:00:00 2001 From: "Cross, Liam (UG - Comp Sci & Elec Eng)" <lc01383@surrey.ac.uk> Date: Thu, 21 Mar 2024 12:38:50 +0000 Subject: [PATCH] Add login, register and customer dashboard components from templates and use nginx in docker --- client/Dockerfile | 14 +- client/nginx.conf | 23 ++++ client/package-lock.json | 16 +++ client/package.json | 1 + client/src/App.css | 8 -- client/src/App.scss | 15 ++ client/src/App.tsx | 6 +- client/src/components/.gitkeep | 0 .../CustomerDashboard/CustomerDashboard.scss | 105 ++++++++++++++ .../CustomerDashboard/CustomerDashboard.tsx | 128 ++++++++++++++++++ .../FlightCard/FlightCard.scss | 16 +++ .../FlightCard/FlightCard.tsx | 28 ++++ .../components/CustomerDashboard/avatar.jpg | Bin 0 -> 6031 bytes client/src/components/Login/Login.scss | 12 ++ client/src/components/Login/Login.tsx | 48 +++++++ client/src/components/Register/Register.scss | 12 ++ client/src/components/Register/Register.tsx | 70 ++++++++++ client/src/index.css | 18 --- client/src/index.scss | 78 +++++++++++ client/src/main.tsx | 20 ++- .../CustomerDashboard/CustomerDashboard.ts | 61 +++++++++ 21 files changed, 643 insertions(+), 36 deletions(-) create mode 100644 client/nginx.conf delete mode 100644 client/src/App.css create mode 100644 client/src/App.scss delete mode 100644 client/src/components/.gitkeep create mode 100644 client/src/components/CustomerDashboard/CustomerDashboard.scss create mode 100644 client/src/components/CustomerDashboard/CustomerDashboard.tsx create mode 100644 client/src/components/CustomerDashboard/FlightCard/FlightCard.scss create mode 100644 client/src/components/CustomerDashboard/FlightCard/FlightCard.tsx create mode 100644 client/src/components/CustomerDashboard/avatar.jpg create mode 100644 client/src/components/Login/Login.scss create mode 100644 client/src/components/Login/Login.tsx create mode 100644 client/src/components/Register/Register.scss create mode 100644 client/src/components/Register/Register.tsx delete mode 100644 client/src/index.css create mode 100644 client/src/index.scss create mode 100644 client/src/services/CustomerDashboard/CustomerDashboard.ts diff --git a/client/Dockerfile b/client/Dockerfile index 9f2a70d..4a1664c 100644 --- a/client/Dockerfile +++ b/client/Dockerfile @@ -1,8 +1,8 @@ -FROM node:18-alpine +FROM node:18-alpine AS build WORKDIR /app -COPY package.json . +COPY package.json package-lock.json ./ RUN npm install @@ -10,6 +10,12 @@ COPY . . RUN npm run build -EXPOSE 8080 +FROM nginx:alpine -CMD [ "npm", "run", "preview" ] \ No newline at end of file +COPY --from=build /app/dist /usr/share/nginx/html + +COPY nginx.conf /etc/nginx/nginx.conf + +EXPOSE 4200 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/client/nginx.conf b/client/nginx.conf new file mode 100644 index 0000000..3cdf0cf --- /dev/null +++ b/client/nginx.conf @@ -0,0 +1,23 @@ +events {} + +http { + server { + listen 4200; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + types { + text/html html; + application/javascript js; + text/css css; + image/png png; + image/jpeg jpg; + } + + location / { + try_files $uri $uri/ /index.html; + } + } +} diff --git a/client/package-lock.json b/client/package-lock.json index c840bd2..831fa4a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.51.1", "react-router-dom": "^6.22.3" }, "devDependencies": { @@ -2916,6 +2917,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.51.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.1.tgz", + "integrity": "sha512-ifnBjl+kW0ksINHd+8C/Gp6a4eZOdWyvRv0UBaByShwU8JbVx5hTcTWEcd5VdybvmPTATkVVXk9npXArHmo56w==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", diff --git a/client/package.json b/client/package.json index fc1425e..14ef04f 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,7 @@ "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.51.1", "react-router-dom": "^6.22.3" }, "devDependencies": { diff --git a/client/src/App.css b/client/src/App.css deleted file mode 100644 index 3876ac7..0000000 --- a/client/src/App.css +++ /dev/null @@ -1,8 +0,0 @@ -.wrapper { - display: flex; - flex-direction: column; -} - -.main { - height: 85vh; -} \ No newline at end of file diff --git a/client/src/App.scss b/client/src/App.scss new file mode 100644 index 0000000..859adee --- /dev/null +++ b/client/src/App.scss @@ -0,0 +1,15 @@ +.wrapper { + display: flex; + flex-direction: column; + height: 100vh; +} + +.main { + height: 100%; + overflow-y: scroll; + padding: 2rem 0; +} + +.footer-wrapper { + margin-top: auto; +} \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index fb484f5..dcb35e7 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,7 +1,7 @@ -import './App.css'; import Header from './components/Header/Header'; import Footer from './components/Footer/Footer'; import { Outlet } from 'react-router-dom'; +import './App.scss'; function App() { return ( @@ -11,7 +11,9 @@ function App() { <div className='main'> <Outlet></Outlet> </div> - <Footer></Footer> + <div className='footer-wrapper'> + <Footer></Footer> + </div> </div> </> ); diff --git a/client/src/components/.gitkeep b/client/src/components/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/client/src/components/CustomerDashboard/CustomerDashboard.scss b/client/src/components/CustomerDashboard/CustomerDashboard.scss new file mode 100644 index 0000000..559bc05 --- /dev/null +++ b/client/src/components/CustomerDashboard/CustomerDashboard.scss @@ -0,0 +1,105 @@ +.customer-dashboard { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + gap: 2rem; +} + +.avatar { + width: 150px; +} + +.customer-profile { + display: flex; + flex-direction: row; + gap: 1.5rem; +} + +.bio-card { + padding: 1.5rem 5rem !important; // Sorry +} + +.form-h { + display: flex; + flex-direction: column; + gap: 1rem; + + .form-group-h { + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; + + label { + font-weight: 700; + width: 180px; + } + + input { + font-size: 1rem; + padding: 6px 12px; + border: 1px solid gray; + border-radius: 5px; + width: 300px; + } + + button { + font-size: 1rem; + padding: 6px 12px; + border: 1px solid gray; + border-radius: 5px; + background-color: var(--main); + color: white; + } + + button:hover { + filter: brightness(50%); + cursor: pointer; + } + + button:disabled { + filter: brightness(40%); + } + + button:disabled:hover { + cursor: auto; + } + + span { + overflow: hidden; + color: red; + } + } +} + +.flights-title { + font-size: 2rem; +} + +.flights { + display: flex; + flex-direction: column; + width: 80vw; + gap: 0.5rem; +} + +.flight-list { + display: flex; + gap: 1rem; +} + +.view-more { + font-size: 1rem; + padding: 6px 12px; + border: 1px solid gray; + border-radius: 5px; + background-color: var(--main); + color: white; +} + +.view-more:hover { + filter: brightness(50%); + cursor: pointer; +} \ No newline at end of file diff --git a/client/src/components/CustomerDashboard/CustomerDashboard.tsx b/client/src/components/CustomerDashboard/CustomerDashboard.tsx new file mode 100644 index 0000000..006b7dd --- /dev/null +++ b/client/src/components/CustomerDashboard/CustomerDashboard.tsx @@ -0,0 +1,128 @@ +import { useState } from 'react'; +import { useLoaderData } from 'react-router-dom'; +import { useForm } from 'react-hook-form'; +import { ICustomerDashboardData } from '../../services/CustomerDashboard/CustomerDashboard'; +import FlightCard from './FlightCard/FlightCard'; +import avatar from './avatar.jpg'; +import './CustomerDashboard.scss'; + +interface ICustomerDashboardForm { + name: string; + email: string; + password: string; + confirmPassword: string; +} + +function CustomerDashboard() { + const [error, setError] = useState(''); + const [disabled, setDisabled] = useState(true); + const data = useLoaderData() as ICustomerDashboardData; + const formValues: ICustomerDashboardForm = { + name: data.name, + email: data.email, + password: '', + confirmPassword: '' + }; + const { register, handleSubmit } = useForm<ICustomerDashboardForm>({mode: 'onChange', defaultValues: formValues}); + + const toggleEdit = () => { + setDisabled(!disabled); + }; + + const onSubmit = (formValue: ICustomerDashboardForm) => { + if (formValue.password.length < 7) { + setError('password length must be greater than 7 characters'); + return; + } + + if (formValue.password !== formValue.confirmPassword) { + setError('password and confirm password must match'); + return; + } + + setError(''); + console.log('ready to make update details api call'); + }; + + return ( + <> + <div className='customer-dashboard'> + <div className='customer-profile'> + <div className='customer-profile-bio'> + <div className='card bio-card'> + <div className='flex'> + <img src={avatar} alt='avatar' className='avatar'></img> + <span>{data.name}</span> + <span>Loyal Customer</span> + </div> + </div> + </div> + + <div className='customer-profile-fields'> + <div className='card'> + <form onSubmit={handleSubmit(onSubmit)}> + <div className='form-h'> + <div className='form-group-h'> + <label>Full Name</label> + <input type='text' {...register('name', { required: true, disabled })} /> + </div> + + <div className='form-group-h'> + <label>Email</label> + <input type='email' {...register('email', { required: true, disabled })} /> + </div> + + <div className='form-group-h'> + <label>Password</label> + <input type='password' placeholder='Enter new password' {...register('password', { required: true, disabled })} /> + </div> + + <div className='form-group-h'> + <label>Confirm Password</label> + <input type='password' placeholder='Confirm new password' {...register('confirmPassword', { required: true, disabled })} /> + </div> + + <div className='form-group-h'> + <button type='button' onClick={toggleEdit}>Toggle Edit</button> + <button type='submit' disabled={disabled}>Submit</button> + </div> + + <div className='form-group-h'> + {error && <span>{error}</span>} + </div> + </div> + </form> + </div> + </div> + </div> + + <div className='flights'> + <span className='flights-title'>Upcoming Flights</span> + <div className='flight-list'> + {data.upcomingFlights.length > 0 + ? data.upcomingFlights.map((flight) => { + return <FlightCard key={flight.id} flight={flight}></FlightCard> + }) + : <div>No Upcoming Flights</div>} + </div> + </div> + + <div className='flights'> + <span className='flights-title'>Flights History</span> + <div className='flight-list'> + {data.upcomingFlights.length > 0 + ? data.flightsHistory.map((flight) => { + return <FlightCard key={flight.id} flight={flight}></FlightCard> + }) + : <div>No Flights History</div>} + </div> + <div> + <button type='button' className='view-more'>View More</button> + </div> + </div> + </div> + </> + ); +} + +export default CustomerDashboard; \ No newline at end of file diff --git a/client/src/components/CustomerDashboard/FlightCard/FlightCard.scss b/client/src/components/CustomerDashboard/FlightCard/FlightCard.scss new file mode 100644 index 0000000..639c3a9 --- /dev/null +++ b/client/src/components/CustomerDashboard/FlightCard/FlightCard.scss @@ -0,0 +1,16 @@ +.flight-card { + display: flex; + flex-direction: column; + gap: 1rem; + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; + padding: 1rem; + box-shadow: 0 1px 3px 0 rgba(0,0,0,.1), 0 1px 2px 0 rgba(0,0,0,.06); + width: max-content; +} + +.flight-path { + color: #777; + font-size: 0.8rem; +} \ No newline at end of file diff --git a/client/src/components/CustomerDashboard/FlightCard/FlightCard.tsx b/client/src/components/CustomerDashboard/FlightCard/FlightCard.tsx new file mode 100644 index 0000000..9e20e2b --- /dev/null +++ b/client/src/components/CustomerDashboard/FlightCard/FlightCard.tsx @@ -0,0 +1,28 @@ +import { Link } from 'react-router-dom'; +import './FlightCard.scss'; + +interface IFlight { + id: number; + flightNumber: string; + flightPath: string; + flightPathFull: string; +} + +interface IFlightCard { + flight: IFlight; +} + +function FlightCard({ flight }: IFlightCard) { + return ( + <> + <div className='flight-card'> + <span>{flight.flightNumber}</span> + <span className='flight-path'>{flight.flightPath}</span> + <span>{flight.flightPathFull}</span> + <Link to={'/flights/' + flight.id}>View Flight</Link> + </div> + </> + ); +} + +export default FlightCard; \ No newline at end of file diff --git a/client/src/components/CustomerDashboard/avatar.jpg b/client/src/components/CustomerDashboard/avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6cc1140f400c6e47c6ac1ddf985ee188f92c4ae2 GIT binary patch literal 6031 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 literal 0 HcmV?d00001 diff --git a/client/src/components/Login/Login.scss b/client/src/components/Login/Login.scss new file mode 100644 index 0000000..1d2060b --- /dev/null +++ b/client/src/components/Login/Login.scss @@ -0,0 +1,12 @@ +.login { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.login-card { + width: 20vw; + min-width: 350px; +} \ No newline at end of file diff --git a/client/src/components/Login/Login.tsx b/client/src/components/Login/Login.tsx new file mode 100644 index 0000000..3c3ecf2 --- /dev/null +++ b/client/src/components/Login/Login.tsx @@ -0,0 +1,48 @@ +import { useForm } from 'react-hook-form'; +import { useState } from 'react'; +import './Login.scss'; + +interface ILogin { + email: string; + password: string; +} + +export function Login() { + const [error, setError] = useState(''); + const { register, handleSubmit } = useForm<ILogin>({mode: 'onChange'}); + + const onSubmit = (formValue: ILogin) => { + setError('TODO: remove me once actual errors are implemented'); + console.log('ready to make login api call', formValue); + }; + + return ( + <> + <div className='login'> + <form onSubmit={handleSubmit(onSubmit)}> + <div className='card login-card'> + <div className='form-group'> + <label>Email Address</label> + <input type='email' placeholder='Enter email' {...register('email', { required: true })} /> + </div> + + <div className='form-group'> + <label>Password</label> + <input type='password' placeholder='Enter password' {...register('password', { required: true })} /> + </div> + + <div className='form-group'> + <button type='submit'>Submit</button> + </div> + + <div className='form-group'> + {error && <span>{error}</span>} + </div> + </div> + </form> + </div> + </> + ); +} + +export default Login; \ No newline at end of file diff --git a/client/src/components/Register/Register.scss b/client/src/components/Register/Register.scss new file mode 100644 index 0000000..c5cef15 --- /dev/null +++ b/client/src/components/Register/Register.scss @@ -0,0 +1,12 @@ +.register { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.register-card { + width: 20vw; + min-width: 350px; +} \ No newline at end of file diff --git a/client/src/components/Register/Register.tsx b/client/src/components/Register/Register.tsx new file mode 100644 index 0000000..8c216b6 --- /dev/null +++ b/client/src/components/Register/Register.tsx @@ -0,0 +1,70 @@ +import { useForm } from 'react-hook-form'; +import { useState } from 'react'; +import './Register.scss'; + +interface IRegister { + name: string; + email: string; + password: string; + confirmPassword: string; +} + +export function Register() { + const [error, setError] = useState(''); + const { register, handleSubmit } = useForm<IRegister>({mode: 'onChange'}); + + const onSubmit = (formValue: IRegister) => { + if (formValue.password.length < 7) { + setError('password length must be greater than 6 characters'); + return; + } + + if (formValue.password !== formValue.confirmPassword) { + setError('password and confirm password must match'); + return; + } + + setError(''); + console.log('ready to make register api call'); + }; + + return ( + <> + <div className='register'> + <form onSubmit={handleSubmit(onSubmit)}> + <div className='card register-card'> + <div className='form-group'> + <label>Full Name</label> + <input type='text' placeholder='Full name' {...register('name', { required: true })} /> + </div> + + <div className='form-group'> + <label>Email Address</label> + <input type='email' placeholder='Enter email' {...register('email', { required: true })} /> + </div> + + <div className='form-group'> + <label>Password</label> + <input type='password' placeholder='Enter password' {...register('password', { required: true })} /> + </div> + + <div className='form-group'> + <label>Confirm Password</label> + <input type='password' placeholder='Confirm password' {...register('confirmPassword', { required: true })} /> + </div> + + <div className='form-group'> + <button type='submit'>Submit</button> + </div> + + <div className='form-group'> + {error && <span>{error}</span>} + </div> + </div> + </form> + </div> + </> + ); +} + +export default Register; \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css deleted file mode 100644 index d8a001f..0000000 --- a/client/src/index.css +++ /dev/null @@ -1,18 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Lato&display=swap'); - -/* Unifies font across frontend */ -* { - font-family: "Lato", sans-serif; - font-weight: 400; - font-style: normal; -} - -/* Stops whitespace surrounding page */ -body { - margin: 0; -} - -/* CSS Variables */ -:root { - --main: #000055; -} \ No newline at end of file diff --git a/client/src/index.scss b/client/src/index.scss new file mode 100644 index 0000000..7c138bb --- /dev/null +++ b/client/src/index.scss @@ -0,0 +1,78 @@ +@import url('https://fonts.googleapis.com/css2?family=Lato&display=swap'); + +/* Unifies font across frontend */ +* { + font-family: "Lato", sans-serif; + font-weight: 400; + font-style: normal; +} + +/* Stops whitespace surrounding page */ +body { + margin: 0; +} + +/* CSS Variables */ +:root { + --main: #000055; +} + +/* +** +** Global CSS classes: +** +*/ + +.card { + display: flex; + flex-direction: column; + gap: 1rem; + border: 1px solid gray; + border-radius: 5px; + background-color: white; + padding: 1.5rem; +} + +.form-group { + display: flex; + flex-direction: column; + + label { + font-weight: 700; + margin-bottom: 0.5rem; + } + + input { + font-size: 1rem; + padding: 6px 12px; + border: 1px solid gray; + border-radius: 5px; + } + + button { + font-size: 1rem; + padding: 6px 12px; + border: 1px solid gray; + border-radius: 5px; + background-color: var(--main); + color: white; + } + + button:hover { + filter: brightness(50%); + cursor: pointer; + } + + span { + overflow: hidden; + color: red; + } +} + +.flex { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; +} \ No newline at end of file diff --git a/client/src/main.tsx b/client/src/main.tsx index 8e2d887..ac1959e 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -2,17 +2,29 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import App from './App.tsx'; -import './index.css'; +import Login from './components/Login/Login.tsx'; +import Register from './components/Register/Register.tsx'; +import CustomerDashboard from './components/CustomerDashboard/CustomerDashboard.tsx'; +import { GetCustomerDashboardData } from './services/CustomerDashboard/CustomerDashboard.ts'; +import './index.scss'; const router = createBrowserRouter([ { path: '/', element: <App></App>, children: [ - // TODO: Remove this path { - path: 'test', - element: <div>test</div> + path: 'login', + element: <Login></Login> + }, + { + path: 'register', + element: <Register></Register> + }, + { + path: 'customer-dashboard', + loader: GetCustomerDashboardData, + element: <CustomerDashboard></CustomerDashboard> } ] } diff --git a/client/src/services/CustomerDashboard/CustomerDashboard.ts b/client/src/services/CustomerDashboard/CustomerDashboard.ts new file mode 100644 index 0000000..81c852e --- /dev/null +++ b/client/src/services/CustomerDashboard/CustomerDashboard.ts @@ -0,0 +1,61 @@ + +export interface IFlight { + id: number; + flightNumber: string; + flightPath: string; + flightPathFull: string; +} + +export interface ICustomerDashboardData { + name: string; + email: string; + upcomingFlights: IFlight[]; + flightsHistory: IFlight[]; +} + +export async function GetCustomerDashboardData(): Promise<ICustomerDashboardData> { + return { + name: 'fname lname', + email: 'test@test.com', + upcomingFlights: [ + { + id: 4, + flightNumber: '0004', + flightPath: 'LTN - MLG', + flightPathFull: 'London(LTN) - Spain(MLG)' + }, + { + id: 5, + flightNumber: '0005', + flightPath: 'LTN - MLG', + flightPathFull: 'London(LTN) - Spain(MLG)' + }, + { + id: 6, + flightNumber: '0006', + flightPath: 'LTN - MLG', + flightPathFull: 'London(LTN) - Spain(MLG)' + } + ], + flightsHistory: [ + { + id: 1, + flightNumber: '0001', + flightPath: 'LTN - MLG', + flightPathFull: 'London(LTN) - Spain(MLG)' + }, + { + id: 2, + flightNumber: '0002', + flightPath: 'LTN - MLG', + flightPathFull: 'London(LTN) - Spain(MLG)' + }, + { + id: 3, + flightNumber: '0003', + flightPath: 'LTN - MLG', + flightPathFull: 'London(LTN) - Spain(MLG)' + } + ] + } +} \ No newline at end of file -- GitLab