Эх сурвалжийг харах

Merge branch 'feature/basic/structure' into 'main'

Feature/basic/structure

See merge request oliver.deutsch/Scheduler!1
Oliver Deutsch 11 сар өмнө
parent
commit
494bfc32af
43 өөрчлөгдсөн 2487 нэмэгдсэн , 91 устгасан
  1. 2 0
      .env-example
  2. 132 0
      .gitignore
  3. 5 91
      README.md
  4. 60 0
      app.js
  5. 91 0
      bin/www
  6. 9 0
      eslint.config.mjs
  7. 1 0
      mongodb.sh
  8. 26 0
      package.json
  9. 34 0
      passport.js
  10. 10 0
      public/html/404.html
  11. 10 0
      public/html/410.html
  12. 49 0
      public/html/appointments.html
  13. 59 0
      public/html/appointmentscreator.html
  14. 35 0
      public/html/auth.html
  15. 62 0
      public/html/bookings.html
  16. 56 0
      public/html/schedule.html
  17. 11 0
      public/index.html
  18. 135 0
      public/javascripts/appointments.js
  19. 74 0
      public/javascripts/auth.js
  20. 188 0
      public/javascripts/bookings.js
  21. 162 0
      public/javascripts/creator-appointments.js
  22. 8 0
      public/javascripts/index.js
  23. 23 0
      public/javascripts/navbar.js
  24. 114 0
      public/javascripts/schedule.js
  25. 92 0
      public/javascripts/utils.js
  26. 73 0
      public/stylesheets/appointments.css
  27. 85 0
      public/stylesheets/auth.css
  28. 37 0
      public/stylesheets/bookings.css
  29. 70 0
      public/stylesheets/create-appointment.css
  30. 50 0
      public/stylesheets/navbar.css
  31. 78 0
      public/stylesheets/schedule.css
  32. 13 0
      public/stylesheets/screen.css
  33. 54 0
      public/stylesheets/style.css
  34. 65 0
      public/stylesheets/tables.css
  35. 87 0
      routes/appointments.js
  36. 143 0
      routes/auth.js
  37. 144 0
      routes/members.js
  38. 62 0
      schemas/appointmentSchema.js
  39. 23 0
      schemas/bookingSchema.js
  40. 38 0
      utils.js
  41. 6 0
      views/error.pug
  42. 5 0
      views/index.pug
  43. 6 0
      views/layout.pug

+ 2 - 0
.env-example

@@ -0,0 +1,2 @@
+JWT_SECRET=secret_key
+DB_CONNECTION=mongodb://user:pass@localhost:27017/

+ 132 - 0
.gitignore

@@ -0,0 +1,132 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+.idea/
+package-lock.json

+ 5 - 91
README.md

@@ -1,93 +1,7 @@
-# Scheduler
-
-
-
-## Getting started
-
-To make it easy for you to get started with GitLab, here's a list of recommended next steps.
-
-Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
-
-## Add your files
-
-- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
-- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
-
-```
-cd existing_repo
-git remote add origin https://gitlab.hochschule-stralsund.de/oliver.deutsch/Scheduler.git
-git branch -M main
-git push -uf origin main
-```
-
-## Integrate with your tools
-
-- [ ] [Set up project integrations](https://gitlab.hochschule-stralsund.de/oliver.deutsch/Scheduler/-/settings/integrations)
-
-## Collaborate with your team
-
-- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
-- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
-- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
-- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
-- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
-
-## Test and Deploy
-
-Use the built-in continuous integration in GitLab.
-
-- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
-- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
-- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
-- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
-- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
-
-***
-
-# Editing this README
-
-When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
-
-## Suggestions for a good README
-
-Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
-
-## Name
-Choose a self-explaining name for your project.
-
-## Description
-Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
-
-## Badges
-On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
-
-## Visuals
-Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
-
-## Installation
-Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
+# README
+Ersteller: Oliver Deutsch
 
 ## Usage
-Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
-
-## Support
-Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
-
-## Roadmap
-If you have ideas for releases in the future, it is a good idea to list them in the README.
-
-## Contributing
-State if you are open to contributions and what your requirements are for accepting them.
-
-For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
-
-You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
-
-## Authors and acknowledgment
-Show your appreciation to those who have contributed to the project.
-
-## License
-For open source projects, say how it is licensed.
-
-## Project status
-If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
+Rename `.env-example` to `.env` and set the secrets. Then install all the
+required dependencies using `npm install`. After that the project can be 
+started using `npm run dev`.

+ 60 - 0
app.js

@@ -0,0 +1,60 @@
+import createError from "http-errors";
+import express from 'express';
+import path from 'path';
+import cookieParser from 'cookie-parser';
+import logger from 'morgan';
+import mongoose from "mongoose";
+import {passport} from "./passport.js";
+
+import {router as authRouter} from "./routes/auth.js";
+import {router as memberRouter} from "./routes/members.js";
+import {router as appointmentsRouter} from "./routes/appointments.js";
+import {fileURLToPath} from "url";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const app = express();
+
+const DB_CONNECTION_STRING = process.env.DB_CONNECTION.toString();
+
+async function connect() {
+  await mongoose.connect(DB_CONNECTION_STRING)
+      .then(() => console.log('Connected to the database'))
+      .catch(err => console.error('Database connection error:', err));
+}
+
+// view engine setup
+app.set('views', path.join(__dirname, 'views'));
+app.set('view engine', 'pug');
+
+app.use(logger('dev'));
+app.use(express.json());
+app.use(express.urlencoded({ extended: false }));
+app.use(cookieParser());
+app.use(express.static(path.join(__dirname, 'public')));
+app.use(passport.initialize());
+
+app.use('/api/users', memberRouter);
+app.use('/api/schedule', appointmentsRouter);
+app.use('/api/auth', authRouter);
+
+// catch 404 and forward to error handler
+app.use(function(req, res, next) {
+  next(createError(404));
+});
+
+// error handler
+app.use(function(err, req, res, next) {
+  // set locals, only providing error in development
+  res.locals.message = err.message;
+  res.locals.error = req.app.get('env') === 'development' ? err : {};
+
+  // render the error page
+  res.status(err.status || 500);
+  res.render('error');
+});
+
+connect();
+
+export {app};

+ 91 - 0
bin/www

@@ -0,0 +1,91 @@
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+import {app} from '../app.js';
+import debug from 'debug';
+import http from 'http';
+
+/**
+ * Get port from environment and store in Express.
+ */
+
+const port = normalizePort(process.env.PORT || '3000');
+app.set('port', port);
+
+/**
+ * Create HTTP server.
+ */
+
+const server = http.createServer(app);
+
+/**
+ * Listen on provided port, on all network interfaces.
+ */
+
+server.listen(port);
+server.on('error', onError);
+server.on('listening', onListening);
+
+/**
+ * Normalize a port into a number, string, or false.
+ */
+
+function normalizePort(val) {
+  const port = parseInt(val, 10);
+
+  if (isNaN(port)) {
+    // named pipe
+    return val;
+  }
+
+  if (port >= 0) {
+    // port number
+    return port;
+  }
+
+  return false;
+}
+
+/**
+ * Event listener for HTTP server "error" event.
+ */
+
+function onError(error) {
+  if (error.syscall !== 'listen') {
+    throw error;
+  }
+
+  const bind = typeof port === 'string'
+    ? 'Pipe ' + port
+    : 'Port ' + port;
+
+  // handle specific listen errors with friendly messages
+  switch (error.code) {
+    case 'EACCES':
+      console.error(bind + ' requires elevated privileges');
+      process.exit(1);
+      break;
+    case 'EADDRINUSE':
+      console.error(bind + ' is already in use');
+      process.exit(1);
+      break;
+    default:
+      throw error;
+  }
+}
+
+/**
+ * Event listener for HTTP server "listening" event.
+ */
+
+function onListening() {
+  const addr = server.address();
+  const bind = typeof addr === 'string'
+    ? 'pipe ' + addr
+    : 'port ' + addr.port;
+  console.log('Listening on ' + bind);
+  debug('Listening on ' + bind);
+}

+ 9 - 0
eslint.config.mjs

@@ -0,0 +1,9 @@
+import pluginJs from "@eslint/js";
+
+
+/** @type {import('eslint').Linter.Config[]} */
+export default [
+  {languageOptions: {
+  }}, pluginJs.configs.recommended,
+  {ignores: ["views/*", "node_modules/*"]}
+];

+ 1 - 0
mongodb.sh

@@ -0,0 +1 @@
+docker run -p 27017:27017 -v mongodb:/data/db -e MONGODB_INITDB_ROOT_USERNAME=user -e MONGODB_INITDB_ROOT_PASSWORD=pass mongodb/mongodb-community-server:latest

+ 26 - 0
package.json

@@ -0,0 +1,26 @@
+{
+  "name": "scheduler",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "start": "node ./bin/www",
+    "dev": "nodemon ./bin/www"
+  },
+  "dependencies": {
+    "bcrypt": "^5.1.1",
+    "cookie-parser": "~1.4.4",
+    "debug": "~2.6.9",
+    "dotenv": "^16.4.7",
+    "eslinter": "^3.1.0",
+    "express": "^4.21.2",
+    "http-errors": "~1.6.3",
+    "jsonwebtoken": "^9.0.2",
+    "mongoose": "^8.9.0",
+    "morgan": "~1.9.1",
+    "nodemon": "^3.1.9",
+    "passport": "^0.7.0",
+    "passport-jwt": "^4.0.1",
+    "pug": "^3.0.3"
+  }
+}

+ 34 - 0
passport.js

@@ -0,0 +1,34 @@
+import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
+import passport from 'passport';
+import {User} from "./routes/auth.js";
+import dotenv from 'dotenv';
+
+// Geheimschlüssel für JWT
+// aus einem ENV File laden
+
+dotenv.config();
+
+const SECRET_KEY = process.env.JWT_SECRET.toString();
+
+const opts = {
+    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
+    secretOrKey: SECRET_KEY
+};
+
+passport.use(
+    new JwtStrategy(opts, async (jwt_payload, done) => {
+        try {
+            const user = await User.findById(jwt_payload.id);
+            if (user) {
+                return done(null, user);  // Successfully authenticated user
+            } else {
+                return done(null, false);  // No user found
+            }
+        } catch (err) {
+            return done(err, false);  // Authentication error
+        }
+    })
+);
+
+
+export { passport, SECRET_KEY };

+ 10 - 0
public/html/404.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Error 404</title>
+</head>
+<body>
+<h1>Error 404: NOT FOUND</h1>
+</body>
+</html>

+ 10 - 0
public/html/410.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Gone</title>
+</head>
+<body>
+<h1>appointment registration is closed</h1>
+</body>
+</html>

+ 49 - 0
public/html/appointments.html

@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Appointment Table</title>
+    <script type="module" src="../javascripts/utils.js" async></script>
+    <script type="module" src="../javascripts/appointments.js" async></script>
+    <script type="module" src="../javascripts/navbar.js" async></script>
+    <link rel="stylesheet" href="../stylesheets/style.css">
+    <link rel="stylesheet" href="../stylesheets/screen.css">
+    <link rel="stylesheet" href="../stylesheets/navbar.css">
+    <link rel="stylesheet" href="../stylesheets/tables.css">
+    <link rel="stylesheet" href="../stylesheets/appointments.css">
+</head>
+<body>
+<nav class="navbar">
+    <div class="navbar-brand">Scheduler</div>
+    <ul class="navbar-links">
+        <li><a href="/html/appointments.html">Appointments</a></li>
+        <li><a href="/html/appointmentscreator.html">Create</a></li>
+        <li><a href="#" id="logout-button">Logout</a></li>
+    </ul>
+</nav>
+<h1 class="msg-screen-size">Please turn your screen or switch to a device with a larger screen</h1>
+<div class="content hidden" id="content">
+    <div class="container">
+        <h2 class="heading">Appointments</h2>
+        <table>
+            <thead>
+            <tr>
+                <th>Title</th>
+                <th>Description</th>
+                <th>Due Date</th>
+                <th>Place</th>
+                <th>Time Span</th>
+                <th>Share</th>
+                <th>Participants</th>
+                <th>Edit</th>
+                <th>Delete</th>
+            </tr>
+            </thead>
+            <tbody id="appointment-table-body">
+            </tbody>
+        </table>
+    </div>
+</div>
+</body>
+</html>

+ 59 - 0
public/html/appointmentscreator.html

@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Create Appointment</title>
+    <script type="module" src="../javascripts/utils.js" async></script>
+    <script type="module" src="../javascripts/navbar.js" async></script>
+    <script type="module" src="../javascripts/creator-appointments.js" async></script>
+    <link rel="stylesheet" href="../stylesheets/navbar.css">
+    <link rel="stylesheet" href="../stylesheets/style.css">
+    <link rel="stylesheet" href="../stylesheets/create-appointment.css">
+</head>
+
+<body>
+<nav class="navbar">
+    <div class="navbar-brand">Scheduler</div>
+    <ul class="navbar-links">
+        <li><a href="/html/appointments.html">Appointments</a></li>
+        <li><a href="/html/appointmentscreator.html">Create</a></li>
+        <li><a href="#" id="logout-button">Logout</a></li>
+    </ul>
+</nav>
+<div class="content">
+    <div class="container">
+        <h2 id="form-title">Create Appointment</h2>
+        <div id="appointment-form">
+            <div class="input-group">
+                <label for="title">Title</label>
+                <input type="text" id="title" placeholder="Enter title" required>
+            </div>
+            <div class="input-group">
+                <label for="description">Description</label>
+                <textarea id="description" placeholder="Enter description" rows="4" required></textarea>
+            </div>
+            <div class="input-group">
+                <label for="dueDate">Due Date</label>
+                <input type="date" id="dueDate" required>
+            </div>
+            <div class="input-group">
+                <label for="startDate">Start Date</label>
+                <input type="datetime-local" id="startDate" required>
+            </div>
+            <div class="input-group">
+                <label for="endDate">End Date</label>
+                <input type="datetime-local" id="endDate" required>
+            </div>
+            <div class="input-group">
+                <label for="place">Place</label>
+                <input type="text" id="place" placeholder="Enter location" required>
+            </div>
+            <button type="submit" class="btn-submit" id="appointment-btn">Create Appointment</button>
+            <p class="msg-error" id="appointment-error"></p>
+            <p class="msg-success" id="appointment-success"></p>
+        </div>
+    </div>
+</div>
+</body>
+</html>

+ 35 - 0
public/html/auth.html

@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Authorization</title>
+    <script type="module" src="../javascripts/utils.js" async></script>
+    <script type="module" src="../javascripts/auth.js" async></script>
+    <link rel="stylesheet" href="../stylesheets/style.css">
+    <link rel="stylesheet" href="../stylesheets/navbar.css">
+    <link rel="stylesheet" href="../stylesheets/auth.css">
+</head>
+<body>
+<div class="content">
+<div class="container">
+    <div id="login-form" class="form">
+        <h2>Authorization</h2>
+        <div>
+            <div class="input-group">
+                <label for="login-email">Email</label>
+                <input type="email" id="login-email" placeholder="Enter your email" required>
+            </div>
+            <div class="input-group">
+                <label for="login-password">Password</label>
+                <input type="password" id="login-password" placeholder="Enter your password" required>
+            </div>
+            <button class="btn" id="btn-login">Login</button>
+            <button class="btn" id="btn-register">Register</button>
+            <p class="msg-error" id="login-error"></p>
+        </div>
+    </div>
+</div>
+</div>
+</body>
+</html>

+ 62 - 0
public/html/bookings.html

@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Appointment Details</title>
+    <script type="module" src="../javascripts/navbar.js" async></script>
+    <script type="module" src="../javascripts/bookings.js" async></script>
+    <script type="module" src="../javascripts/utils.js" async></script>
+    <link rel="stylesheet" href="../stylesheets/style.css">
+    <link rel="stylesheet" href="../stylesheets/screen.css">
+    <link rel="stylesheet" href="../stylesheets/navbar.css">
+    <link rel="stylesheet" href="../stylesheets/tables.css">
+    <link rel="stylesheet" href="../stylesheets/bookings.css">
+</head>
+<body>
+<nav class="navbar">
+    <div class="navbar-brand">Scheduler</div>
+    <ul class="navbar-links">
+        <li><a href="/html/appointments.html">Appointments</a></li>
+        <li><a href="/html/appointmentscreator.html">Create</a></li>
+        <li><a href="#" id="logout-button">Logout</a></li>
+    </ul>
+</nav>
+<h1 class="msg-screen-size">Please turn your screen or switch to a device with a larger screen</h1>
+<div class="content">
+<div class="container">
+    <h1>Appointment Details</h1>
+
+    <!-- Appointment Information -->
+    <div class="appointment-info">
+    </div>
+
+    <!-- Participants Table -->
+    <h2>Participants</h2>
+    <div class="table-wrapper">
+        <table>
+            <thead>
+            <tr>
+                <th>First Name</th>
+                <th>Last Name</th>
+                <th>Email</th>
+            </tr>
+            </thead>
+            <tbody id="participants-table-body">
+            <tr>
+                <td>John</td>
+                <td>Doe</td>
+                <td>johndoe@example.com</td>
+            </tr>
+            <tr>
+                <td>Jane</td>
+                <td>Smith</td>
+                <td>janesmith@example.com</td>
+            </tr>
+            </tbody>
+        </table>
+    </div>
+</div>
+</div>
+</body>
+</html>

+ 56 - 0
public/html/schedule.html

@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Attending Appointment</title>
+    <script type="module" src="../javascripts/utils.js" async></script>
+    <script type="module" src="../javascripts/schedule.js" async></script>
+    <link rel="stylesheet" href="../stylesheets/style.css">
+    <link rel="stylesheet" href="../stylesheets/navbar.css">
+    <link rel="stylesheet" href="../stylesheets/schedule.css">
+</head>
+<body>
+<div class="content">
+<div class="container">
+    <h1 class=heading>Attending an appointment</h1>
+    <h3 id="title"></h3>
+    <h4 id="place"></h4>
+    <p id="description"></p>
+    <div>
+        <div class="input-group">
+            <label for="email">Email</label>
+            <input type="email" id="email" name="email" placeholder="johndoe@example.com" required>
+        </div>
+        <div class="input-group">
+            <label for="firstname">First Name</label>
+            <input type="text" id="firstname" name="firstname" placeholder="John" required>
+        </div>
+        <div class="input-group">
+            <label for="lastname">Last Name</label>
+            <input type="text" id="lastname" name="lastname" placeholder="Doe" required>
+        </div>
+        <!-- Preset Due Date -->
+        <div class="input-group">
+            <label for="due-date">Due Date</label>
+            <input type="text" id="due-date" name="dueDate" readonly>
+        </div>
+        <!-- Preset Appointment Date -->
+        <div class="input-group">
+            <label for="appointment-date">Appointment Date</label>
+            <input type="text" id="appointment-date" name="appointmentDate" readonly>
+        </div>
+        <!-- Preset Time Span -->
+        <div class="input-group">
+            <label for="time-span">Time Span</label>
+            <input type="text" id="time-span" name="timeSpan" value="10:00 AM to 12:00 PM" readonly>
+        </div>
+        <button type="submit" id="btn-attend" class="btn-submit">Attend</button>
+
+    </div>
+    <p class="msg-error" id="appointment-error"></p>
+    <p class="msg-success" id="appointment-success"></p>
+</div>
+</div>
+</body>
+</html>

+ 11 - 0
public/index.html

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>Title</title>
+    <script src="javascripts/index.js" async></script>
+</head>
+<body>
+
+</body>
+</html>

+ 135 - 0
public/javascripts/appointments.js

@@ -0,0 +1,135 @@
+"use strict";
+
+import {fetchWithToken, getTimeSpan, parseOrRedirect} from "./utils.js";
+
+let last_clicked = "";
+let click_counter = 0;
+
+/**
+ * Adds an event listener to window load to fetch and render appointments.
+ */
+window.addEventListener("load", async function () {
+    await fetchAppointments();
+    document.getElementById("content").classList.remove("hidden");
+});
+
+
+
+/**
+ * Renders appointment data into the table body of an HTML table.
+ * @param {Array<Object>} data - Array of appointment objects containing title, description, dueDate, place, startDate, and endDate.
+ */
+function renderAppointments(data) {
+    const tableBody = document.getElementById('appointment-table-body');
+    tableBody.innerHTML = '';
+    data.forEach(appointment => {
+        const row = createAppointmentRow(appointment);
+
+        // Append the row to the table body
+        tableBody.appendChild(row);
+    });
+}
+
+/**
+ * Creates a row in the appointment table for each appointment.
+ * @param {Object} appointment - Appointment object containing title, description, dueDate, place, startDate, and endDate.
+ * @returns {HTMLTableRowElement} The newly created table row element.
+ */
+function createAppointmentRow(appointment) {
+    const row = document.createElement('tr');
+    const titleCell = document.createElement('td');
+    const descriptionCell = document.createElement('td');
+    const placeCell = document.createElement('td');
+    const dueDateCell = document.createElement('td');
+    const timeSpanCell = document.createElement('td');
+    const shareCell = document.createElement('td');
+    const participantCell = document.createElement('td');
+    const editCell = document.createElement('td');
+    const deleteCell = document.createElement('td');
+    const shareATag = document.createElement('a');
+    const participantATag = document.createElement('a');
+    const editATag = document.createElement('a');
+    const deleteButton = document.createElement('button');
+
+    const searchParams = new URLSearchParams({
+        appointment: appointment._id,
+    });
+
+    participantATag.href = '/html/bookings.html?' + searchParams.toString();
+    participantATag.innerText = 'participants';
+    participantATag.classList.add('participants-link');
+    editATag.href = '/html/appointmentscreator.html?' + searchParams.toString();
+    shareATag.href = '/html/schedule.html?' + searchParams.toString();
+    shareATag.innerText = 'share';
+    shareATag.classList.add('share-link');
+    editATag.innerText = 'edit';
+    editATag.classList.add('edit-link');
+    deleteButton.onclick = async () => {
+        if (last_clicked !== appointment._id) {
+            click_counter += 1;
+            last_clicked = appointment._id;
+            return;
+        }
+
+        await deleteAppointment(appointment._id);
+    }
+    deleteButton.innerText = 'delete';
+
+    shareCell.appendChild(shareATag);
+    participantCell.appendChild(participantATag);
+    editCell.appendChild(editATag);
+    deleteCell.appendChild(deleteButton);
+
+    titleCell.textContent = appointment.title;
+    descriptionCell.textContent = appointment.description;
+    dueDateCell.textContent = new Date(appointment.dueDate).toLocaleDateString();
+    placeCell.textContent = appointment.place;
+
+    timeSpanCell.textContent = getTimeSpan(appointment.startDate, appointment.endDate);
+    timeSpanCell.classList.add('time-span');
+
+    // Append cells to the row
+    row.appendChild(titleCell);
+    row.appendChild(descriptionCell);
+    row.appendChild(dueDateCell);
+    row.appendChild(placeCell);
+    row.appendChild(timeSpanCell);
+    row.appendChild(shareCell);
+    row.appendChild(participantCell);
+    row.appendChild(editCell);
+    row.appendChild(deleteCell);
+
+    // Append the row to the table body
+    return row;
+}
+
+/**
+ * Deletes an appointment from the server and updates the UI.
+ * @param {string} appointmentId - ID of the appointment to delete.
+ */
+async function deleteAppointment(appointmentId) {
+    const response = await fetchWithToken(`/api/users/delete`, {
+        method: 'DELETE',
+        body: JSON.stringify({appointmentId: appointmentId})
+    });
+    await parseOrRedirect(response);
+
+    if (response.ok) {
+        await fetchAppointments();
+    }
+}
+
+/**
+ * Fetches appointments from the server and updates the UI.
+ */
+async function fetchAppointments() {
+    const response = await fetchWithToken("/api/users/appointments");
+
+    const js = await parseOrRedirect(response);
+
+    if (js.length === 0) return;
+    renderAppointments(js);
+}
+
+
+

+ 74 - 0
public/javascripts/auth.js

@@ -0,0 +1,74 @@
+"use strict";
+
+import {fetchRegular} from "./utils.js";
+
+const emailInput = document.getElementById("login-email");
+const passwordInput = document.getElementById("login-password");
+const errorMessageParagraph = document.getElementById("login-error");
+const submitButtonLogin = document.getElementById("btn-login");
+const submitButtonRegister = document.getElementById("btn-register");
+
+/**
+ * Add button event listeners, for login and register
+ */
+window.addEventListener("load", function () {
+    submitButtonLogin.addEventListener("click", async function () {
+        await login();
+    })
+
+    submitButtonRegister.addEventListener("click", async function () {
+        await register();
+    })
+});
+
+
+/**
+ * Function to handle sending authentication requests.
+ *
+ * @param {string} type - The type of request, either 'login' or 'register'.
+ */
+async function sendAuthRequest(type) {
+    const response = await fetchRegular(
+        "/api/auth/" + type,
+        {
+            method: "POST",
+            headers: {
+                "Content-Type": "application/json", // Set the content type to JSON
+            },
+            body: JSON.stringify(
+                {
+                    email: emailInput.value,
+                    password: passwordInput.value,
+                }
+            ),
+        }
+    )
+    const response_json = await response.json();
+
+    if (response.status === 200) {
+        localStorage.setItem("token", response_json["token"]);
+        window.location.replace("/html/appointments.html");
+    } else {
+        if (response_json["error"]) {
+            errorMessageParagraph.innerText = response_json["error"];
+        } else {
+            errorMessageParagraph.innerText = response_json["message"];
+        }
+    }
+
+
+}
+
+/**
+ * Function to handle user login.
+ */
+async function login() {
+    await sendAuthRequest("login");
+}
+
+/**
+ * Function to handle user registration.
+ */
+async function register() {
+    await sendAuthRequest("register");
+}

+ 188 - 0
public/javascripts/bookings.js

@@ -0,0 +1,188 @@
+/**
+ * Import necessary utility functions from utils.js
+ */
+import {fetchRegular, fetchWithToken, getTimeSpan, parseOrRedirect} from "./utils.js";
+
+/**
+ * Event listener for the window load event.
+ * Retrieves the appointment ID from the URL query parameters and fetches the appointment details.
+ */
+window.addEventListener("load", async function () {
+    /**
+     * Get the URL query parameters
+     */
+    const urlParams = new URLSearchParams(window.location.search);
+    const appointment = urlParams.get('appointment');
+
+    /**
+     * If no appointment ID is found, redirect to the 404 page
+     */
+    if (!appointment) {
+        window.location.replace("/html/404.html");
+    }
+
+    /**
+     * Fetch the appointment details and bookings
+     */
+    await getAppointment(appointment);
+    await fetchBookings(appointment);
+});
+
+/**
+ * Displays the appointment details in the appointment-info container.
+ * @param {Object} appointmentData - The appointment data object
+ */
+function displayAppointmentDetails(appointmentData) {
+    /**
+     * Retrieve the appointment-info container
+     */
+    const appointmentInfo = document.querySelector('.appointment-info');
+
+    /**
+     * Clear existing content
+     */
+    while (appointmentInfo.firstChild) {
+        appointmentInfo.removeChild(appointmentInfo.firstChild);
+    }
+
+    /**
+     * Create and append the updated appointment content
+     */
+    const title = document.createElement('p');
+    const titleLabel = document.createElement('strong');
+    titleLabel.textContent = 'Title: ';
+    title.appendChild(titleLabel);
+    title.appendChild(document.createTextNode(appointmentData.title));
+
+    const description = document.createElement('p');
+    const descriptionLabel = document.createElement('strong');
+    descriptionLabel.textContent = 'Description: ';
+    description.appendChild(descriptionLabel);
+    description.appendChild(document.createTextNode(appointmentData.description));
+
+    const place = document.createElement('p');
+    const placeLabel = document.createElement('strong');
+    placeLabel.textContent = 'Place: ';
+    place.appendChild(placeLabel);
+    place.appendChild(document.createTextNode(appointmentData.place));
+
+    const dueDate = document.createElement('p');
+    const dueDateLabel = document.createElement('strong');
+    dueDateLabel.textContent = 'Due Date: ';
+    dueDate.appendChild(dueDateLabel);
+    dueDate.appendChild(document.createTextNode(new Date(appointmentData.dueDate).toLocaleDateString()));
+
+    const timeSpan = document.createElement('p');
+    const timeSpanLabel = document.createElement('strong');
+    timeSpanLabel.textContent = 'Time Span: ';
+    timeSpan.appendChild(timeSpanLabel);
+    timeSpan.appendChild(document.createTextNode(getTimeSpan(appointmentData.startDate, appointmentData.endDate)));
+
+    /**
+     * Append all the updated elements
+     */
+    appointmentInfo.appendChild(title);
+    appointmentInfo.appendChild(description);
+    appointmentInfo.appendChild(place);
+    appointmentInfo.appendChild(dueDate);
+    appointmentInfo.appendChild(timeSpan);
+}
+
+/**
+ * Fetches the appointment details from the API.
+ * @param {string} appointment - The appointment ID
+ */
+async function getAppointment(appointment) {
+    /**
+     * Set the request options
+     */
+    const options = {method: 'GET', headers: {'User-Agent': 'insomnia/10.0.0'}};
+
+    /**
+     * Set the query parameters
+     */
+    const searchParams = new URLSearchParams({
+        appointmentId: appointment,
+        isUser: true
+    });
+
+    /**
+     * Fetch the appointment details
+     */
+    const response = await fetchRegular(`/api/schedule?` + searchParams.toString(), options)
+        .catch(err => console.error('error:' + err));
+
+    /**
+     * Parse the response and display the appointment details
+     */
+    const js = await parseOrRedirect(response);
+    displayAppointmentDetails(js);
+}
+
+/**
+ * Renders the bookings data in the participants-table-body container.
+ * @param {Array<Object>} data - The bookings data array
+ */
+function renderBookings(data) {
+    /**
+     * Retrieve the participants-table-body container
+     */
+    const tableBody = document.getElementById('participants-table-body');
+    tableBody.innerHTML = ''; // Clear existing rows
+
+    /**
+     * Iterate over the bookings data and create table rows
+     */
+    data.forEach(participant => {
+        /**
+         * Create a new table row
+         */
+        const row = document.createElement('tr');
+
+        /**
+         * Create table cells for first name, last name, and email
+         */
+        const firstNameCell = document.createElement('td');
+        const lastNameCell = document.createElement('td');
+        const emailCell = document.createElement('td');
+
+        /**
+         * Populate cell content with participant data
+         */
+        firstNameCell.textContent = participant.firstname;
+        lastNameCell.textContent = participant.lastname;
+        emailCell.textContent = participant.email;
+
+        /**
+         * Append cells to the row
+         */
+        row.appendChild(firstNameCell);
+        row.appendChild(lastNameCell);
+        row.appendChild(emailCell);
+
+        /**
+         * Append the row to the table body
+         */
+        tableBody.appendChild(row);
+    });
+}
+
+/**
+ * Fetches the bookings data from the API.
+ * @param {string} appointmentId - The appointment ID
+ */
+async function fetchBookings(appointmentId) {
+    /**
+     * Set the query parameters
+     */
+    const searchParams = new URLSearchParams({
+        appointmentId: appointmentId,
+    });
+
+    /**
+     * Fetch the bookings data
+     */
+    const response = await fetchWithToken('/api/users/bookings?' + searchParams.toString());
+    const js = await parseOrRedirect(response);
+    renderBookings(js);
+}

+ 162 - 0
public/javascripts/creator-appointments.js

@@ -0,0 +1,162 @@
+/**
+ * Appointment management script.
+ * Handles loading, creating, and editing appointments.
+ */
+
+"use strict";
+
+import {fetchRegular, fetchWithToken, parseOrRedirect} from "./utils.js";
+
+/**
+ * DOM elements for appointment form and error/success messages.
+ */
+const appointmentErrorParagraph = document.getElementById("appointment-error");
+const appointmentSuccessParagraph = document.getElementById("appointment-success");
+const titleInput = document.getElementById('title');
+const descriptionInput = document.getElementById('description');
+const dueDateInput = document.getElementById('dueDate');
+const startDateInput = document.getElementById('startDate');
+const endDateInput = document.getElementById('endDate');
+const placeInput = document.getElementById('place');
+
+/**
+ * Event listener for window load event.
+ * Checks if an appointment ID is provided in the URL query string.
+ * If an appointment ID is found, loads the appointment data and updates the form.
+ */
+window.addEventListener("load", async function () {
+    const urlParams = new URLSearchParams(window.location.search);
+    const appointment = urlParams.get('appointment');
+
+    if (appointment) {
+        // Load appointment data
+        await loadAppointment(appointment);
+        document.getElementById("appointment-btn").innerText = "Edit Appointment";
+        document.getElementById("form-title").innerText = "Edit Appointment";
+    }
+
+    /**
+     * Event listener for appointment button click event.
+     * Creates or edits an appointment based on the presence of an appointment ID.
+     */
+    document.getElementById("appointment-btn").addEventListener('click', async () => {
+        if (!appointment) {
+            await createAppointment();
+        } else {
+            // Modify appointment
+            await editAppointment(appointment);
+        }
+    })
+});
+
+/**
+ * Displays appointment data in the form.
+ * @param {Object} data - Appointment data object.
+ */
+async function displayAppointment(data) {
+    titleInput.value = data.title;
+    descriptionInput.value = data.description;
+    dueDateInput.value = new Date(data.dueDate).toISOString().split('T')[0];
+    startDateInput.value = new Date(data.startDate).toISOString().slice(0, 16);
+    endDateInput.value = new Date(data.endDate).toISOString().slice(0, 16);
+    placeInput.value = data.place;
+}
+
+/**
+ * Loads appointment data from the server.
+ * @param {string} appointment - Appointment ID.
+ */
+async function loadAppointment(appointment) {
+    let options = {method: 'GET', headers: {'User-Agent': 'insomnia/10.0.0'}};
+
+    console.log(window.location.host);
+
+    const searchParams = new URLSearchParams({
+        appointmentId: appointment,
+        isUser: true
+    });
+
+    const response = await fetchRegular("/api/schedule?" + searchParams.toString(), options)
+        .catch(err => console.error('error:' + err));
+
+    const js = await parseOrRedirect(response);
+    await displayAppointment(js);
+}
+
+/**
+ * Retrieves form data as an object.
+ * @returns {Object} Form data object.
+ */
+function getParameters() {
+    const title = titleInput.value;
+    const description = descriptionInput.value;
+    const dueDate = dueDateInput.value;
+    const startDate = startDateInput.value;
+    const endDate = endDateInput.value;
+    const place = placeInput.value;
+
+    return {title, description, dueDate, startDate, endDate, place};
+}
+
+/**
+ * Edits an existing appointment.
+ * @param {string} appointment - Appointment ID.
+ */
+async function editAppointment(appointment) {
+    await setAppointment("modify", appointment);
+}
+
+/**
+ * Creates a new appointment.
+ */
+async function createAppointment() {
+    await setAppointment("create", "");
+}
+
+/**
+ * Sets an appointment (create or modify).
+ * @param {string} endpoint - API endpoint (create or modify).
+ * @param {string} appointment - Appointment ID (optional).
+ */
+async function setAppointment(endpoint, appointment) {
+    const {title, description, dueDate, startDate, endDate, place} = getParameters();
+
+    if (!title || !description || !dueDate || !startDate || !endDate) {
+        appointmentSuccessParagraph.innerText = "";
+        appointmentErrorParagraph.innerText = "Not all fields filled";
+        return;
+    }
+
+    const response = await fetchWithToken(`/api/users/${endpoint}`, {
+        method: 'POST',
+        body: JSON.stringify({
+            title: title,
+            description: description,
+            dueDate: dueDate,
+            startDate: startDate,
+            endDate: endDate,
+            place: place,
+            appointmentId: appointment
+        })
+    });
+    const js = await response.json();
+
+    if (!response.ok) {
+        appointmentSuccessParagraph.innerText = "";
+        appointmentErrorParagraph.innerText = js["message"];
+    } else {
+        const searchParams = new URLSearchParams({
+            appointment: js["id"],
+        });
+
+        const path = '/html/schedule.html?' + searchParams.toString();
+        appointmentErrorParagraph.innerText = "";
+        appointmentSuccessParagraph.innerText = "Appointment created successfully. The share link " +
+            "was copied to your clipboard";
+        if (endpoint === "modify") {
+            appointmentSuccessParagraph.innerText = "Appointment edited successfully. The share link " +
+                "was copied to your clipboard";
+        }
+        await navigator.clipboard.writeText(`${window.location.origin}${path}`);
+    }
+}

+ 8 - 0
public/javascripts/index.js

@@ -0,0 +1,8 @@
+"use strict";
+
+/**
+ * Redirect to appointment.html when page root is visited
+ */
+window.addEventListener("load", function() {
+    window.location.replace("/html/appointments.html");
+});

+ 23 - 0
public/javascripts/navbar.js

@@ -0,0 +1,23 @@
+/**
+ * Logout functionality.
+ *
+ * This script is responsible for removing the user token from local storage and redirecting to the authentication page upon logout click.
+ */
+
+"use strict";
+
+/**
+ * Add an event listener to the window's load event.
+ * This function will be executed when the document has finished loading.
+ */
+window.addEventListener("load", function() {
+    document.getElementById("logout-button").addEventListener("click", logout);
+});
+
+/**
+ * Removes the user token from local storage and redirects to the authentication page.
+ */
+function logout() {
+    window.localStorage.removeItem('token');
+    window.location.replace("/html/auth.html");
+}

+ 114 - 0
public/javascripts/schedule.js

@@ -0,0 +1,114 @@
+"use strict";
+
+/**
+ * Import required functions from utils.js
+ */
+import { fetchRegular, getTimeSpan, parseOrRedirect } from "./utils.js";
+
+let appointment;
+const errorParagraph = document.getElementById('appointment-error');
+const successParagraph = document.getElementById('appointment-success');
+
+/**
+ * Event listener for window load
+ */
+window.addEventListener("load", async function () {
+    document.getElementById("btn-attend").addEventListener("click", async () => {
+        await attendEvent();
+    })
+    
+    const urlParams = new URLSearchParams(window.location.search);
+    appointment = urlParams.get('appointment');
+    
+    if (!appointment) {
+        window.location.replace("/html/404.html");
+    }
+
+    await getAppointment();
+});
+
+/**
+ * Get appointment details from API
+ *
+ * @async
+ * @returns {Promise<void>}
+ */
+async function getAppointment() {
+    let options = { method: 'GET', headers: {'User-Agent': 'insomnia/10.0.0'} };
+
+    const searchParams = new URLSearchParams({
+        appointmentId: appointment,
+
+    });
+
+    const response = await fetchRegular('/api/schedule?' + searchParams.toString(), options)
+        .catch(err => console.error('error:' + err));
+
+    const js = await parseOrRedirect(response);
+    displayAppointmentDetails(js);
+}
+
+/**
+ * Display appointment details on page
+ *
+ * @param {object} appointment - Appointment data from API
+ */
+function displayAppointmentDetails(appointment) {
+    /**
+     * Update element values with appointment details
+     */
+    document.getElementById('appointment-date').innerText = appointment._id;
+    document.getElementById('title').innerText = appointment.title;
+    document.getElementById('description').innerText = appointment.description;
+
+    const dueDate = new Date(appointment.dueDate).toLocaleDateString();
+    const startDate = new Date(appointment.startDate);
+    const endDate = new Date(appointment.endDate);
+    const startDateFormatted = startDate.toLocaleString();
+
+    document.getElementById('due-date').value = dueDate;
+    document.getElementById('appointment-date').value = startDateFormatted;
+    document.getElementById('time-span').value = getTimeSpan(startDate, endDate);
+
+    document.getElementById('place').innerText = appointment.place;
+}
+
+/**
+ * Attend event by creating a new schedule entry in API
+ *
+ * @async
+ * @returns {Promise<void>}
+ */
+async function attendEvent() {
+    const email = document.getElementById('email').value;
+    const firstname = document.getElementById('firstname').value;
+    const lastname = document.getElementById('lastname').value;
+
+    if (!email || !firstname || !lastname) {
+
+        errorParagraph.innerText = "Fill in all fields.";
+        return;
+    }
+
+    const response = await fetchRegular("/api/schedule/create",
+        {
+            method: 'POST',
+            headers: {'Content-Type': 'application/json'},
+            body: JSON.stringify({
+                appointment: appointment,
+                email: email,
+                firstname: firstname,
+                lastname: lastname
+            })
+        });
+
+    const js = await response.json();
+
+    if (response.status !== 200) {
+        errorParagraph.innerText = js["message"];
+        successParagraph.innerText = "";
+    } else {
+        errorParagraph.innerText = "";
+        successParagraph.innerText = "Registered successfully.";
+    }
+}

+ 92 - 0
public/javascripts/utils.js

@@ -0,0 +1,92 @@
+/**
+ * @file utils.js
+ * @description Utility functions for making API requests and handling responses.
+ */
+
+"use strict";
+
+/**
+ * Fetches a resource from the server with authentication token.
+ *
+ * @async
+ * @param {string} url - The URL of the resource to fetch.
+ * @param {Object} options - Options for the fetch request (e.g. method, body).
+ * @returns {Promise<Object>} A promise resolving to the response data.
+ */
+async function fetchWithToken(url, options) {
+    const authToken = localStorage.getItem('token');
+    return await fetch(url, {...options, headers: {
+            'Authorization': `Bearer ${authToken}`,
+            'Content-Type': 'application/json',
+            'Cache-Control': 'no-cache, no-store',
+        }});
+}
+
+/**
+ * Fetches a resource from the server without authentication token.
+ *
+ * @async
+ * @param {string} url - The URL of the resource to fetch.
+ * @param {Object} options - Options for the fetch request (e.g. method, body).
+ * @returns {Promise<Object>} A promise resolving to the response data.
+ */
+async function fetchRegular(url, options) {
+    return await fetch(url, {...options, headers: {
+            'Content-Type': 'application/json',
+            'Cache-Control': 'no-cache, no-store',
+        }});
+}
+
+/**
+ * Formats a time as HH:MM string.
+ *
+ * @param {Date} date - The date to format.
+ * @returns {string} The formatted time string (e.g. "09:00").
+ */
+function formatTime(date) {
+    const hours = date.getHours().toString().padStart(2, '0');
+    const minutes = date.getMinutes().toString().padStart(2, '0');
+    return `${hours}:${minutes}`;
+}
+
+/**
+ * Gets the time span between two dates.
+ *
+ * @param {Date} start - The start date.
+ * @param {Date} end - The end date.
+ * @returns {string} The time span string (e.g. "09:00 to 10:00").
+ */
+function getTimeSpan(start, end) {
+    return `${formatTime(new Date(start))} to ${formatTime(new Date(end))}`;
+}
+
+/**
+ * Parses the response data and redirects if necessary.
+ *
+ * @async
+ * @param {Response} response - The server response.
+ * @returns {Promise<Object>} A promise resolving to the parsed response data, or null if a redirect was taken.
+ */
+async function parseOrRedirect(response) {
+    if (response.status === 401) {
+        // Handle unauthorized request (redirect to login page)
+        window.location.replace("/html/auth.html");
+        return;
+    }
+
+    if (response.status === 410) {
+        // Handle deprecated resource (redirect to deprecated page)
+        window.location.replace("/html/410.html");
+        return;
+    }
+
+    const js = await response.json();
+    if (!response.ok) {
+        // Handle non-200 status code (redirect to error page)
+        window.location.replace("/html/404.html");
+    }
+
+    return js;
+}
+
+export { fetchWithToken, getTimeSpan, parseOrRedirect, fetchRegular };

+ 73 - 0
public/stylesheets/appointments.css

@@ -0,0 +1,73 @@
+:root {
+    --primary-color: #ff6b6b;
+    --edit-link-color: #54ccac;
+    --share-link-color: #7ccc54;
+    --participants-link-color: #cc54c6;
+    --hover-color: #5750d9;
+    --dark-hover-color: #e05555;
+}
+
+.content {
+    box-sizing: border-box;
+}
+
+.container {
+    width: 90%;
+    max-width: 1200px;
+    padding: 20px;
+    border-radius: 10px;
+    overflow-x: auto; /* Ensure content is scrollable on smaller screens */
+}
+
+/* Modern A-Tag Styling */
+td a {
+    text-decoration: none;
+    padding: 8px 12px;
+    border-radius: 4px;
+    color: white;
+    font-size: 14px;
+    transition: background-color 0.3s ease, transform 0.2s ease;
+}
+
+td a.edit-link {
+    background-color: var(--edit-link-color);
+}
+
+td a.share-link {
+    background-color: var(--share-link-color);
+}
+
+td a.participants-link {
+    background-color: var(--participants-link-color);
+}
+
+td a:hover {
+    background-color: var(--hover-color);
+    transform: translateY(-2px);
+}
+
+/* Modern Button Styling */
+td button {
+    padding: 8px 12px;
+    border: none;
+    border-radius: 4px;
+    font-size: 14px;
+    color: white;
+    background-color: var(--primary-color);
+    cursor: pointer;
+    transition: background-color 0.3s ease, transform 0.2s ease;
+}
+
+td button:hover {
+    background-color: var(--dark-hover-color);
+    transform: translateY(-2px);
+}
+
+h2 {
+    margin-bottom: 20px;
+}
+
+/* Ensure table stays scrollable on smaller screens */
+.container {
+    overflow-x: auto;
+}

+ 85 - 0
public/stylesheets/auth.css

@@ -0,0 +1,85 @@
+:root {
+    --input-bg-color: #e0e0e0;
+    --input-focus-border: #6c63ff;
+    --input-placeholder-color: #888;
+    --button-hover-background: #5750d9;
+    --button-hover-text: white;
+}
+
+/* General Reset */
+
+body {
+    display: grid;
+    min-height: 100vh;
+}
+
+.container {
+    background: white;
+    padding: 20px;
+    border-radius: 8px;
+    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+    width: 100%;
+    max-width: 400px;
+    display: grid;
+    grid-template-rows: auto 1fr;
+    gap: 20px;
+}
+
+.form {
+    display: grid;
+    grid-template-rows: repeat(4, auto);
+    gap: 15px;
+}
+
+h2 {
+    text-align: center;
+    color: #333;
+    margin-bottom: 10px;
+}
+
+.input-group {
+    display: grid;
+    grid-template-rows: auto auto;
+    gap: 10px;
+    margin-top: 10px;
+}
+
+.input-group label {
+    font-size: 14px;
+    color: #555;
+}
+
+.input-group input {
+    padding: 10px;
+    border: 1px solid var(--input-bg-color);
+    border-radius: 4px;
+    font-size: 14px;
+    transition: border-color 0.3s, box-shadow 0.3s;
+}
+
+.input-group input:focus {
+    border-color: var(--input-focus-border);
+    outline: none;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+button {
+    margin-top: 40px;
+    padding: 10px;
+    transition: background-color 0.3s ease;
+}
+
+button:hover {
+    background-color: var(--button-hover-background);
+    color: var(--button-hover-text);
+}
+
+#btn-register {
+    background: var(--input-bg-color);
+    color: #333;
+    margin-top: -10px; /* Adjust spacing between buttons */
+}
+
+#btn-register:hover {
+    background: #cacaca;
+}

+ 37 - 0
public/stylesheets/bookings.css

@@ -0,0 +1,37 @@
+:root {
+    --appointment-bg-color: white;
+    --container-border-radius: 8px;
+    --container-box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+    --font-size: 16px;
+    --text-color: #555;
+    --line-height: 1.6;
+}
+
+/* General Reset */
+.content {
+    box-sizing: border-box;
+    background-color: #f4f4f9;
+}
+
+/* Container */
+.container {
+    background: var(--appointment-bg-color);
+    padding: 20px 30px;
+    border-radius: var(--container-border-radius);
+    box-shadow: var(--container-box-shadow);
+    width: 100%;
+    max-width: 800px;
+    display: grid;
+    gap: 10px;
+}
+
+/* Appointment Info */
+.appointment-info {
+    display: grid;
+    font-size: var(--font-size);
+    color: var(--text-color);
+}
+
+.appointment-info p {
+    line-height: var(--line-height);
+}

+ 70 - 0
public/stylesheets/create-appointment.css

@@ -0,0 +1,70 @@
+:root {
+    --input-bg-color: #e0e0e0;
+    --input-focus-border: #6c63ff;
+    --button-hover-background: #5750d9;
+}
+
+.content {
+    min-height: 80vh;
+    box-sizing: border-box;
+    min-width: 80%;
+}
+
+body {
+    background-color: #f4f4f9;
+}
+
+.btn-submit {
+    margin-top: 10px;
+    padding: 12px;
+    padding-inline: 30px;
+}
+
+textarea {
+    resize: none;
+}
+
+.container {
+    background: white;
+    padding: 20px 40px;
+    border-radius: 8px;
+    width: 80%;
+    max-width: 500px;
+    display: grid;
+    gap: 20px;
+}
+
+#appointment-form {
+    display: grid;
+    gap: 15px;
+    margin-inline: auto;
+    min-width: 95%;
+}
+
+.input-group {
+    display: grid;
+    gap: 5px;
+}
+
+.input-group label {
+    font-size: 14px;
+    color: #555;
+}
+
+.input-group input {
+    width: 100%;
+    padding: 10px;
+    border: 1px solid var(--input-bg-color);
+    border-radius: 4px;
+    font-size: 14px;
+    transition: border-color 0.3s;
+}
+
+.input-group input:focus {
+    border-color: var(--input-focus-border);
+    outline: none;
+}
+
+.btn-submit:hover {
+    background: var(--button-hover-background);
+}

+ 50 - 0
public/stylesheets/navbar.css

@@ -0,0 +1,50 @@
+:root {
+    --navbar-bg-color: white;
+    --navbar-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+    --brand-font-size: 1.5rem;
+    --brand-color: #333;
+    --link-font-size: 1rem;
+    --link-color: #333;
+    --hover-link-color: #6c63ff;
+}
+
+/* Navbar Styling */
+.navbar {
+    position: sticky;
+    top: 0; /* Stick to the top */
+    z-index: 1000; /* Ensures the navbar stays above other elements */
+    display: grid;
+    grid-template-columns: auto 1fr auto;
+    align-items: center;
+    background-color: var(--navbar-bg-color);
+    padding: 10px 20px;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+    width: 100%;
+    max-width: 96%;
+    border-radius: 8px;
+}
+
+.navbar-brand {
+    font-size: var(--brand-font-size);
+    font-weight: bold;
+    color: var(--brand-color);
+}
+
+.navbar-links {
+    display: grid;
+    grid-template-columns: repeat(4, auto);
+    gap: 20px;
+    list-style: none;
+    justify-self: end;
+}
+
+.navbar-links a {
+    text-decoration: none;
+    color: var(--link-color);
+    font-size: var(--link-font-size);
+    transition: color 0.3s ease;
+}
+
+.navbar-links a:hover {
+    color: var(--hover-link-color);
+}

+ 78 - 0
public/stylesheets/schedule.css

@@ -0,0 +1,78 @@
+:root {
+    --container-bg-color: white;
+    --border-radius-val: 8px;
+    --box-shadow-val: 0 4px 6px rgba(0, 0, 0, 0.1);
+    --input-width: 100%;
+    --input-padding: 10px;
+    --input-border-color: #ccc;
+    --input-focus-border-color: #6c63ff;
+    --btn-submit-bg-hover: #5750d9;
+    --input-group-readonly-color: #f9f9f9;
+    --input-label-color: #555;
+}
+
+.container * {
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+}
+
+.content {
+    min-height: 100vh;
+}
+
+.container {
+    background-color: var(--container-bg-color);
+    padding: 20px 30px;
+    border-radius: var(--border-radius-val);
+    box-shadow: var(--box-shadow-val);
+    width: 100%;
+    max-width: 500px;
+    display: grid;
+    gap: 20px;
+}
+
+form {
+    display: grid;
+    gap: 15px;
+}
+
+.input-group {
+    display: grid;
+    gap: 5px;
+}
+
+.input-group label {
+    font-size: 14px;
+    color: var(--input-label-color);
+}
+
+.input-group input {
+    width: var(--input-width);
+    padding: var(--input-padding);
+    border: 1px solid var(--input-border-color);
+    border-radius: 4px;
+    font-size: 14px;
+    transition: border-color 0.3s;
+}
+
+/* For readonly inputs */
+.input-group input[readonly] {
+    background-color: var(--input-group-readonly-color);
+    cursor: not-allowed;
+}
+
+.input-group input:focus {
+    border-color: var(--input-focus-border-color);
+    outline: none;
+}
+
+.btn-submit {
+    margin-top: 10px;
+    padding: 12px;
+    padding-inline: 30px;
+}
+
+.btn-submit:hover {
+    background-color: var(--btn-submit-bg-hover);
+}

+ 13 - 0
public/stylesheets/screen.css

@@ -0,0 +1,13 @@
+.msg-screen-size {
+    visibility: hidden;
+}
+
+@media (max-width: 400px) {
+    .msg-screen-size {
+        visibility: visible;
+    }
+
+    .content, .navbar {
+        visibility: hidden;
+    }
+}

+ 54 - 0
public/stylesheets/style.css

@@ -0,0 +1,54 @@
+:root {
+  --primary-color: #6c63ff;
+  --background-color: #f4f4f9;
+  --text-color: #333;
+  --box-shadow-color: 0 4px 6px rgba(0, 0, 0, 0.1);
+  --error-message-color: red;
+  --success-message-color: green;
+}
+
+body {
+  background-color: var(--background-color);
+}
+
+.hidden {
+  visibility: hidden;
+}
+
+.content {
+  margin: 0;
+  padding: 0;
+  font-family: Arial, sans-serif;
+  display: grid;
+  place-items: center;
+}
+
+.heading {
+  text-align: center;
+  color: var(--text-color);
+}
+
+.container {
+  box-shadow: var(--box-shadow-color);
+}
+
+button {
+  border: none;
+  background: var(--primary-color);
+  color: white;
+  font-size: 16px;
+  cursor: pointer;
+  border-radius: 4px;
+  transition: background 0.3s;
+}
+
+.msg-error {
+  color: var(--error-message-color);
+  margin-top: 10px;
+}
+
+.msg-success {
+  color: var(--success-message-color);
+  margin-top: 10px;
+}
+

+ 65 - 0
public/stylesheets/tables.css

@@ -0,0 +1,65 @@
+
+/* Table Styling */
+.table-wrapper {
+    overflow-x: auto;
+}
+
+table {
+    width: 100%;
+    border-collapse: collapse;
+    text-align: left;
+}
+
+th, td {
+    padding: 12px;
+    border: 1px solid #ddd;
+}
+
+th {
+    background-color: #6c63ff;
+    color: white;
+    font-weight: normal;
+}
+
+tr:nth-child(even) {
+    background-color: #f9f9f9;
+}
+
+tr:hover {
+    background-color: #f1f1f1;
+}
+
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+    body {
+        height: auto;
+        padding: 20px;
+    }
+
+    h2 {
+        font-size: 1.5rem;
+    }
+
+    table {
+        font-size: 14px;
+    }
+
+    th, td {
+        padding: 10px;
+    }
+}
+
+@media (max-width: 480px) {
+    table {
+        font-size: 10px;
+    }
+
+    th, td {
+        padding: 8px;
+    }
+
+    th {
+        font-size: 0.9rem;
+    }
+}

+ 87 - 0
routes/appointments.js

@@ -0,0 +1,87 @@
+/**
+ * Import required dependencies.
+ */
+import express from 'express';
+import Appointment from "../schemas/appointmentSchema.js";
+import Booking from "../schemas/bookingSchema.js";
+import {isDueDatePassed, isValidEmail} from "../utils.js";
+
+/**
+ * Create an Express router for handling appointment-related routes.
+ */
+const router = express.Router();
+
+/**
+ * Create a new appointment and associate it with a booking.
+ */
+router.post('/create', async function(req, res, next) {
+    const { appointment, email, firstname, lastname } = req.body;
+
+    const appointmentId = appointment;
+
+    if (!appointmentId || !firstname || !lastname || !email) {
+        res.status(400).json({ 'message': 'Parameters are incomplete' });
+        return;
+    }
+
+    if (!isValidEmail(email)) {
+        res.status(400).json({ 'message': 'Email is invalid' });
+        return;
+    }
+
+    try {
+        const existingAppointment = await Appointment.findOne({ _id: appointmentId });
+        if (!existingAppointment) {
+            return res.status(404).json({ message: 'appointment does not exist' });
+        }
+
+        if (isDueDatePassed(existingAppointment["dueDate"])) {
+            return res.status(401).json({ message: 'appointment registration is closed' });
+        }
+
+        const duplicateBooking = await Booking.findOne({ appointment: appointmentId, email: email })
+            .catch(err => {
+                console.log(err);
+            });
+
+        if (duplicateBooking) {
+            return res.status(404).json({ message: 'already registered' });
+        }
+
+        const newBooking = new Booking({ appointment, email, firstname, lastname });
+
+        newBooking.save()
+            .then(doc => res.json({ "success": true }))
+            .catch(err => res.status(500).json({ 'error': err }));
+
+    } catch (err) {
+        // Handle errors
+        console.error(err);
+        return res.status(500).json({ error: 'Internal Server Error' });
+    }
+});
+
+/**
+ * Retrieve an appointment by ID.
+ */
+router.get('/', function (req, res, next) {
+    const {appointmentId, isUser} = req.query;
+    Appointment.findOne({ _id: appointmentId })
+        .then(appointment => {
+            if (appointment === null) {
+                res.status(404).json({'message': "Appointment not found"});
+            } else {
+                // Check if a registered user accesses this endpoint
+                if (isDueDatePassed(appointment["dueDate"]) && !isUser) {
+                    return res.status(410).json({ message: 'appointment registration is closed' });
+                } else {
+                    res.json(appointment);
+                }
+            }
+        })
+        .catch(err => {
+            res.status(404).json({'message': "Appointment not found"});
+        });
+});
+
+export { router };

+ 143 - 0
routes/auth.js

@@ -0,0 +1,143 @@
+import express from 'express';
+import { mongoose } from 'mongoose';
+import bcrypt from "bcrypt";
+import jwt from "jsonwebtoken";
+import {SECRET_KEY} from "../passport.js";
+import {isValidEmail} from "../utils.js";
+
+/**
+ * Create an Express router instance.
+ *
+ * @type {Express.Router}
+ */
+const router = express.Router();
+
+/**
+ * Set the salt rounds for password hashing (10).
+ *
+ * @type {number}
+ */
+const saltRounds = 10;
+
+/**
+ * Define a Mongoose schema for users.
+ *
+ * @typedef {Object} UserSchema
+ * @property {String} email - The user's email address.
+ * @property {String} password - The user's password.
+ */
+
+/**
+ * Create a new Mongoose model from the user schema.
+ *
+ * @type {Model<UserSchema>}
+ */
+const userSchema = new mongoose.Schema({
+    /**
+     * The user's email address.
+     *
+     * @type {String}
+     * @required
+     */
+    email: {
+        type: String,
+        required: true
+    },
+    /**
+     * The user's password.
+     *
+     * @type {String}
+     * @required
+     */
+    password: {
+        type: String,
+        required: true
+    }
+})
+const User = mongoose.model('User', userSchema, 'users');
+
+/**
+ * Generate a JWT token for the given user.
+ *
+ * @param {Object} user - The user object.
+ * @returns {String} The generated JWT token.
+ */
+function generateToken(user) {
+    const payload = { id: user._id };
+    return jwt.sign(payload, SECRET_KEY, {expiresIn: '6h'});
+}
+
+/**
+ * Handle a login request.
+ */
+router.post('/login', function (req, res) {
+    const email = req.body.email;
+    const password = req.body.password;
+
+    if (!password || !email) {
+        return res.status(400).json({ message: 'parameters invalid' });
+    }
+
+    if (!isValidEmail(email)) {
+        return res.status(400).json({ message: 'email is invalid' });
+    }
+
+    User.findOne({ email: email })
+        .then(user => {
+            if (!user) {
+                return res.status(422).json({ message: 'user not found' });
+            }
+            console.log(user);
+            bcrypt.compare(password, user.password, function (err, result) {
+                if (result) {
+                    res.json({ token: generateToken(user) });
+                } else {
+                    res.status(401).json({ error: "Wrong password" });
+                }
+            });
+        })
+        .catch(err => {
+            console.log(err);
+            res.status(500).json({ error: err.message }); 
+        });
+});
+
+/**
+ * Handle a registration request.
+ */
+router.post('/register', async function (req, res) {
+    const email = req.body.email;
+    const password = req.body.password;
+
+    if (!password || !email) {
+        return res.status(400).json({ message: 'parameters invalid' });
+    }
+
+    if (!isValidEmail(email)) {
+        return res.status(400).json({ message: 'email is invalid' });
+    }
+
+    try {
+        const existingUser = await User.findOne({ email: email });
+        if (existingUser) {
+            return res.status(403).json({ message: 'user already exists' });
+        }
+
+        // Hash the password
+        bcrypt.hash(password, saltRounds, async function (err, hash) {
+            if (err) {
+                return res.status(500).json({ error: err.message });
+            } else if (hash) {
+                await User.collection.insertOne({ email: email, password: hash });
+                const user = await User.findOne({ email: email });
+  
+                return res.json({ token: generateToken(user) });
+            }
+        });
+    } catch (err) {
+        console.error(err);
+        return res.status(500).json({ error: 'Internal Server Error' });
+    }
+});
+
+export { router, User };

+ 144 - 0
routes/members.js

@@ -0,0 +1,144 @@
+import express from 'express';
+import passport from "passport";
+import {mongoose} from "mongoose";
+import Appointment from "../schemas/appointmentSchema.js";
+import Booking from "../schemas/bookingSchema.js";
+import {validateDates} from "../utils.js";
+
+/**
+ * Router for handling appointments.
+ */
+const router = express.Router();
+
+/**
+ * Middleware to authenticate with passport
+ */
+router.use(passport.authenticate('jwt', { session: false }));
+
+/**
+ * Middleware to pass user ID to req.userId if it exists in the request.
+ */
+router.use((req, res, next) => {
+    if (req.user && req.user.id) {
+        req.userId = req.user.id; // Pass user ID to req.userId
+    }
+    next();
+});
+
+/**
+ * Create a new appointment.
+ */
+router.post('/create', function(req, res, next) {
+    const user = req.userId;
+    const { title, description, dueDate, place, startDate, endDate } = req.body;
+
+    if (!title || title.length === 0 || !dueDate || !description || !startDate || !endDate || !place) {
+        res.status(400).json({ 'message': 'Empty parameter' });
+        return;
+    }
+
+    if (!validateDates(res, startDate, endDate, dueDate)) {
+        return;
+    }
+
+    const newTask = new Appointment({ user, title, description, dueDate, place, startDate, endDate });
+
+    newTask.save()
+        .then(doc => res.json({ "id": doc._id }))
+        .catch(err => res.status(500).json({ 'message': err }));
+});
+
+/**
+ * Modify an existing appointment.
+ */
+router.post('/modify', function (req, res, next) {
+    const user = req.userId;
+
+    const { appointmentId, title, description, dueDate, place, startDate, endDate } = req.body;
+    if (!title || !dueDate || !description || !startDate || !endDate || !place || !appointmentId) {
+        res.status(400).json({ 'error': 'Empty parameter' });
+        return;
+    }
+
+    if (!validateDates(res, startDate, endDate, dueDate)) {
+        return;
+    }
+
+    Appointment.updateOne(
+        { _id: appointmentId, user: user },
+        { $set: { title, description, dueDate, place, startDate, endDate } }
+    )
+        .then(result => {
+            if (result.modifiedCount === 1) {
+                console.log("Successfully updated the document.");
+                res.json({ "id": appointmentId });
+            } else {
+                console.log("Document not updated");
+                res.status(500).json({ 'message': 'Document has not changed' });
+            }
+        })
+        .catch(err => res.status(404).json({ 'message': 'Not found' }));
+});
+
+/**
+ * Delete an appointment.
+ */
+router.delete('/delete', function (req, res, next) {
+    const user = req.userId;
+    const {appointmentId} = req.body;
+    if (!appointmentId) {
+        res.status(400).json({ 'message': 'Invalid parameter' });
+        return;
+    }
+
+    Appointment.deleteOne({ _id: appointmentId, user: user })
+        .then(result => {
+            if (result.deletedCount === 1) {
+                res.json({ 'success': true });
+            } else {
+                res.status(404).json({ 'message': 'Not found' });
+            }
+        })
+        .catch(err => res.status(500).json({ 'message': err }));
+});
+
+/**
+ * Get all appointments for the authenticated user.
+ */
+router.get('/appointments', function (req, res, next) {
+    const user = req.userId;
+    Appointment.find({ user: user })
+        .then(appointments => {
+            res.json(appointments);
+        })
+        .catch(err => {
+            res.status(404).json({'message': "Appointments not found"});
+        });
+});
+
+/**
+ * Get all bookings for a specific appointment.
+ */
+router.get('/bookings', function (req, res, next) {
+    const { appointmentId } = req.query;
+
+    if (!appointmentId) {
+        res.status(400).json({ 'message': 'Empty parameter' });
+        return;
+    }
+
+    Booking.find({ appointment: appointmentId })
+        .then(bookings => {
+            if (bookings === null) {
+                res.status(404).json({'message': "Appointment not found"});
+            } else {
+                res.json(bookings);
+            }
+        })
+        .catch(err => {
+            res.status(404).json({'message': "Appointment not found"});
+        });
+
+});
+
+export { router };

+ 62 - 0
schemas/appointmentSchema.js

@@ -0,0 +1,62 @@
+import mongoose from "mongoose";
+
+/**
+ * Define a schema for an appointment using mongoose.
+ *
+ * @description This schema represents an appointment between a user and a service provider.
+ * @typedef {Object} AppointmentSchema
+ * @property {String} user - The ID of the user making the appointment (e.g. "user123").
+ * @property {String} title - A brief title for the appointment (e.g. " haircut").
+ * @property {String} description - A longer description of the appointment.
+ * @property {Date} dueDate - The date and time when the appointment is scheduled.
+ * @property {String} place - The location where the appointment will take place.
+ * @property {Date} startDate - The start date and time of the appointment.
+ * @property {Date} endDate - The end date and time of the appointment.
+ */
+
+/**
+ * Create a mongoose model for appointments using the defined schema.
+ *
+ * @description This model allows you to interact with the appointments collection in your MongoDB database.
+ * @typedef {Object} Appointment
+ * @static
+ */
+const appointmentSchema = new mongoose.Schema({
+    user: {
+        type: String,
+        required: true
+    },
+    title: {
+        type: String,
+        required: true
+    },
+    description: {
+        type: String,
+        required: true
+    },
+    dueDate: {
+        type: Date,
+        required: true
+    },
+    place: {
+        type: String,
+        required: true
+    },
+    startDate: {
+        type: Date,
+        required: true
+    },
+    endDate: {
+        type: Date,
+        required: true
+    }
+});
+
+/**
+ * Create a mongoose model for appointments and name it "Appointment".
+ *
+ * @description This model allows you to interact with the appointments collection in your MongoDB database.
+ */
+const Appointment = mongoose.model('Appointment', appointmentSchema, 'appointment');
+
+export default Appointment;

+ 23 - 0
schemas/bookingSchema.js

@@ -0,0 +1,23 @@
+import mongoose from "mongoose";
+
+const bookingSchema = new mongoose.Schema({
+    appointment: {
+        type: String,
+        required: true
+    },
+    email: {
+        type: String,
+        required: true
+    },
+    firstname: {
+        type: String,
+        required: true
+    },
+    lastname: {
+        type: String,
+        required: true
+    }
+});
+const Booking = mongoose.model('Booking', bookingSchema, 'booking');
+
+export default Booking;

+ 38 - 0
utils.js

@@ -0,0 +1,38 @@
+
+function isValidEmail(email) {
+    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+    return emailRegex.test(email);
+}
+
+function isDueDatePassed(dueDate) {
+    const now = new Date();
+    const dueDateTime = new Date(dueDate);
+    return dueDateTime < now;
+}
+
+function isDatePassed(dateA, dateB) {
+    const dueDateA = new Date(dateA);
+    const dueDateB = new Date(dateB);
+    return dueDateA < dueDateB;
+}
+
+function validateDates(res, startDate, endDate, dueDate) {
+    if (isDatePassed(startDate, dueDate)) {
+        res.status(400).json({ 'message': 'Due date is after start date' });
+        return false;
+    }
+
+    if (isDueDatePassed(dueDate)) {
+        res.status(400).json({ 'message': 'Due date is in the past' });
+        return false;
+    }
+
+    if (isDatePassed(endDate, startDate)) {
+        res.status(400).json({ 'message': 'End date is before start date' });
+        return false;
+    }
+
+    return true;
+}
+
+export {isValidEmail, isDueDatePassed, isDatePassed, validateDates};

+ 6 - 0
views/error.pug

@@ -0,0 +1,6 @@
+extends layout
+
+block content
+  h1= message
+  h2= error.status
+  pre #{error.stack}

+ 5 - 0
views/index.pug

@@ -0,0 +1,5 @@
+extends layout
+
+block content
+  h1= title
+  p Welcome to #{title}

+ 6 - 0
views/layout.pug

@@ -0,0 +1,6 @@
+doctype html
+html
+  head
+    title= title
+  body
+    block content