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