diff --git a/.env b/.env
old mode 100644
new mode 100755
index f92bf9c9fe269667b7d97c1a674e32738d735b72..64c8d4b9a6eb8967d150f2f0b378984c809fce91
--- a/.env
+++ b/.env
@@ -1,2 +1,2 @@
-VITE_APP_NAME=ZBH Portal
-VITE_PAGE_AFTER_LOGIN=/
+VITE_APP_NAME=ZBH Portal
+VITE_PAGE_AFTER_LOGIN=/
diff --git a/README.md b/README.md
index 6f4ae14d429df397714b5f1747b9c812d5859b13..2eb9443b2a6ee05c3f651b0735f4ffa12c0a9297 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,16 @@ It's build to fulfill the style requirements of the University of Hamburg.
 git clone git@gitlab.rrz.uni-hamburg.de:zbhai/ragchat-api.git
 cd ragchat-api
 npm i
+cp ./.env.template.local ./.env.development.local
+cp ./.env.template.local ./.env.production.local
+# fill envs with production and/or devel values
 ```
 
+
 # Sources
 - [RAGChat-API](https://gitlab.rrz.uni-hamburg.de/zbhai/ragchat-api)
 - [PM2](https://pm2.keymetrics.io/)
 
 # Roadmap
-- [ ] create a basic frame
 - [ ] create forms using rhf
 - [ ] add form validations via zod
\ No newline at end of file
diff --git a/README_tmp.html b/README_tmp.html
index 5f967927b24c892a0360597347c826a7b1028f69..0c3616ef036c2fdc73250132a7640ede05b0c98a 100644
--- a/README_tmp.html
+++ b/README_tmp.html
@@ -378,6 +378,9 @@ It's build to fulfill the style requirements of the University of Hamburg.</p>
 <pre class="hljs"><code><div>git clone git@gitlab.rrz.uni-hamburg.de:zbhai/ragchat-api.git
 cd ragchat-api
 npm i
+cp ./.env.template.local ./.env.development.local
+cp ./.env.template.local ./.env.production.local
+# fill envs with production and/or devel values
 </div></code></pre>
 <h1 id="sources">Sources</h1>
 <ul>
@@ -386,9 +389,8 @@ npm i
 </ul>
 <h1 id="roadmap">Roadmap</h1>
 <ul>
-<li><input type="checkbox" id="checkbox0"><label for="checkbox0">create a basic frame</label></li>
-<li><input type="checkbox" id="checkbox1"><label for="checkbox1">create forms using rhf</label></li>
-<li><input type="checkbox" id="checkbox2"><label for="checkbox2">add form validations via zod</label></li>
+<li><input type="checkbox" id="checkbox0"><label for="checkbox0">create forms using rhf</label></li>
+<li><input type="checkbox" id="checkbox1"><label for="checkbox1">add form validations via zod</label></li>
 </ul>
 
 </body>
diff --git a/package-lock.json b/package-lock.json
index b901065ff477c239a95a3ebbf4aeefd54b887e99..cf180e712b315a06b67cd243d0279cb8d654c9f4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,12 +8,19 @@
       "name": "ragchat-frontend",
       "version": "0.0.0",
       "dependencies": {
+        "@hookform/resolvers": "^3.6.0",
+        "axios": "^1.7.2",
         "hamburger-react": "^2.5.1",
         "react": "^18.3.1",
         "react-dom": "^18.3.1",
         "react-helmet-async": "^2.0.5",
+        "react-hook-form": "^7.52.0",
         "react-icons": "^5.2.1",
-        "react-router-dom": "^6.24.0"
+        "react-router-dom": "^6.24.0",
+        "react-toastify": "^10.0.5",
+        "tailwind-merge": "^2.3.0",
+        "validator": "^13.12.0",
+        "zod": "^3.23.8"
       },
       "devDependencies": {
         "@types/react": "^18.3.3",
@@ -362,6 +369,18 @@
         "@babel/core": "^7.0.0-0"
       }
     },
+    "node_modules/@babel/runtime": {
+      "version": "7.24.7",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz",
+      "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==",
+      "license": "MIT",
+      "dependencies": {
+        "regenerator-runtime": "^0.14.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
     "node_modules/@babel/template": {
       "version": "7.24.7",
       "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
@@ -881,6 +900,15 @@
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
+    "node_modules/@hookform/resolvers": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.6.0.tgz",
+      "integrity": "sha512-UBcpyOX3+RR+dNnqBd0lchXpoL8p4xC21XP8H6Meb8uve5Br1GCnmg0PcBoKKqPKgGu9GHQ/oygcmPrQhetwqw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react-hook-form": "^7.0.0"
+      }
+    },
     "node_modules/@humanwhocodes/config-array": {
       "version": "0.11.14",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -1904,6 +1932,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
     "node_modules/autoprefixer": {
       "version": "10.4.19",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
@@ -1958,6 +1992,17 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/axios": {
+      "version": "1.7.2",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
+      "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2162,6 +2207,15 @@
         "node": ">= 6"
       }
     },
+    "node_modules/clsx": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+      "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/color-convert": {
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -2179,6 +2233,18 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/commander": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -2380,6 +2446,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/didyoumean": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -3144,6 +3219,26 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/follow-redirects": {
+      "version": "1.15.6",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+      "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/for-each": {
       "version": "0.3.3",
       "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -3171,6 +3266,20 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/form-data": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/fraction.js": {
       "version": "4.3.7",
       "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -4231,6 +4340,27 @@
         "node": ">=8.6"
       }
     },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -4889,6 +5019,12 @@
         "react-is": "^16.13.1"
       }
     },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
     "node_modules/punycode": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -4965,6 +5101,22 @@
         "react": "^16.6.0 || ^17.0.0 || ^18.0.0"
       }
     },
+    "node_modules/react-hook-form": {
+      "version": "7.52.0",
+      "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.0.tgz",
+      "integrity": "sha512-mJX506Xc6mirzLsmXUJyqlAI3Kj9Ph2RhplYhUVffeOQSnubK2uVqBFOBJmvKikvbFV91pxVXmDiR+QMF19x6A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.22.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/react-hook-form"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17 || ^18 || ^19"
+      }
+    },
     "node_modules/react-icons": {
       "version": "5.2.1",
       "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
@@ -5023,6 +5175,19 @@
         "react-dom": ">=16.8"
       }
     },
+    "node_modules/react-toastify": {
+      "version": "10.0.5",
+      "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz",
+      "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==",
+      "license": "MIT",
+      "dependencies": {
+        "clsx": "^2.1.0"
+      },
+      "peerDependencies": {
+        "react": ">=18",
+        "react-dom": ">=18"
+      }
+    },
     "node_modules/read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -5068,6 +5233,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/regenerator-runtime": {
+      "version": "0.14.1",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+      "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+      "license": "MIT"
+    },
     "node_modules/regexp.prototype.flags": {
       "version": "1.5.2",
       "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
@@ -5688,6 +5859,19 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/tailwind-merge": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz",
+      "integrity": "sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.1"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/dcastil"
+      }
+    },
     "node_modules/tailwindcss": {
       "version": "3.4.4",
       "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz",
@@ -5978,6 +6162,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/validator": {
+      "version": "13.12.0",
+      "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
+      "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
     "node_modules/vite": {
       "version": "5.3.2",
       "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz",
@@ -6334,6 +6527,15 @@
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
+    },
+    "node_modules/zod": {
+      "version": "3.23.8",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
+      "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
     }
   }
 }
diff --git a/package.json b/package.json
index 723c6e77d3aa3e488a18592d87587c21355c595a..c775795dce78b41586d151303469ab2a004c8231 100644
--- a/package.json
+++ b/package.json
@@ -11,12 +11,19 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@hookform/resolvers": "^3.6.0",
+    "axios": "^1.7.2",
     "hamburger-react": "^2.5.1",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "react-helmet-async": "^2.0.5",
+    "react-hook-form": "^7.52.0",
     "react-icons": "^5.2.1",
-    "react-router-dom": "^6.24.0"
+    "react-router-dom": "^6.24.0",
+    "react-toastify": "^10.0.5",
+    "tailwind-merge": "^2.3.0",
+    "validator": "^13.12.0",
+    "zod": "^3.23.8"
   },
   "devDependencies": {
     "@types/react": "^18.3.3",
diff --git a/src/App.jsx b/src/App.jsx
index 2075f13e105e32caaa8a2588bf111a5d01ecc55e..3fce18b9747496c6f115d701b5bd9a5de02e3c36 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,6 +1,7 @@
 import { createBrowserRouter, RouterProvider } from 'react-router-dom';
 import { sitemap } from "./routes/Sitemap";
 import { HelmetProvider } from 'react-helmet-async';
+import AuthState from "./contexts/Auth/AuthState";
 
 
 function App() {
@@ -8,7 +9,9 @@ function App() {
 
   return (
     <HelmetProvider>
-      <RouterProvider router={pages} />
+      <AuthState>
+        <RouterProvider router={pages} />
+      </AuthState>
     </HelmetProvider>
 
   );
diff --git a/src/assets/css/tailwind.presets.min.css b/src/assets/css/tailwind.presets.min.css
index c630ac15b1800210a718c35c907ce6d03e64f0db..c647438847d6457cd12dc38e0866373d41ae6b6e 100644
--- a/src/assets/css/tailwind.presets.min.css
+++ b/src/assets/css/tailwind.presets.min.css
@@ -1 +1 @@
-@tailwind base;@tailwind components;@tailwind utilities;@layer base{img,svg,video,canvas,audio,iframe,embed,object{display:inline;vertical-align:middle}*{@apply border-border}body{@apply bg-background text-foreground}}/*# sourceMappingURL=tailwind.presets.min.css.map */
\ No newline at end of file
+@tailwind base;@tailwind components;@tailwind utilities;@layer base{.conceal{@apply opacity-0 h-0 w-0 p-0 m-0 overflow-hidden}label,details{@apply block w-full pb-1 relative cursor-pointer disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-60 lg:min-w-xs lg:w-[calc(1/2*100%-(1*1rem/2))] xl:w-[calc((1/4*100%)-(3*1rem/4))]}img,svg,video,canvas,audio,iframe,embed,object{display:inline;vertical-align:middle}:root{--background: 0 0% 100%;--foreground: 0 0% 3.9%;--card: 0 0% 100%;--card-foreground: 0 0% 3.9%;--popover: 0 0% 100%;--popover-foreground: 0 0% 3.9%;--primary: 204 98% 37%;--primary-foreground: 0 0% 98%;--secondary: 0 0% 96.1%;--secondary-foreground: 0 0% 9%;--muted: 0 0% 96.1%;--muted-foreground: 0 0% 45.1%;--accent: 0 0% 96.1%;--accent-foreground: 0 0% 9%;--destructive: 353 100% 44%;--destructive-foreground: 0 0% 98%;--border: 0 0% 89.8%;--input: 0 0% 89.8%;--ring: 0 0% 3.9%;--radius: 0.5rem}.dark{--background: 0 0% 3.9%;--foreground: 0 0% 98%;--card: 0 0% 3.9%;--card-foreground: 0 0% 98%;--popover: 0 0% 3.9%;--popover-foreground: 0 0% 98%;--primary: 0 0% 98%;--primary-foreground: 0 0% 9%;--secondary: 0 0% 14.9%;--secondary-foreground: 0 0% 98%;--muted: 0 0% 14.9%;--muted-foreground: 0 0% 63.9%;--accent: 0 0% 14.9%;--accent-foreground: 0 0% 98%;--destructive: 0 62.8% 30.6%;--destructive-foreground: 0 0% 98%;--border: 0 0% 14.9%;--input: 0 0% 14.9%;--ring: 0 0% 83.1%}*{@apply border-border}body{@apply bg-background text-foreground}}/*# sourceMappingURL=tailwind.presets.min.css.map */
\ No newline at end of file
diff --git a/src/assets/css/tailwind.presets.min.css.map b/src/assets/css/tailwind.presets.min.css.map
index 83dac443bc2aa8cbed5b40d0a278694aa092dc9d..a24d0a12751ab3d9a3e954ecc7903e41bf7d0904 100644
--- a/src/assets/css/tailwind.presets.min.css.map
+++ b/src/assets/css/tailwind.presets.min.css.map
@@ -1 +1 @@
-{"version":3,"sources":["../sass/tailwind.presets.scss"],"names":[],"mappings":"AAAA,cAAA,CACA,oBAAA,CACA,mBAAA,CAEA,YACE,+CAQE,cAAA,CACA,qBAAA,CAEF,EACE,oBAAA,CAEF,KACE,oCAAA,CAAA","file":"tailwind.presets.min.css"}
\ No newline at end of file
+{"version":3,"sources":["../sass/tailwind.presets.scss"],"names":[],"mappings":"AAAA,cAAA,CACA,oBAAA,CACA,mBAAA,CACA,YACE,SACE,gDAAA,CAEF,cAEE,6MAAA,CAEF,+CAQE,cAAA,CACA,qBAAA,CAEF,MACE,uBAAA,CACA,uBAAA,CAEA,iBAAA,CACA,4BAAA,CAEA,oBAAA,CACA,+BAAA,CAGA,sBAAA,CACA,8BAAA,CAEA,uBAAA,CACA,+BAAA,CAEA,mBAAA,CACA,8BAAA,CAEA,oBAAA,CACA,4BAAA,CAEA,2BAAA,CACA,kCAAA,CAEA,oBAAA,CACA,mBAAA,CACA,iBAAA,CAEA,gBAAA,CAGF,MACE,uBAAA,CACA,sBAAA,CAEA,iBAAA,CACA,2BAAA,CAEA,oBAAA,CACA,8BAAA,CAEA,mBAAA,CACA,6BAAA,CAEA,uBAAA,CACA,gCAAA,CAEA,mBAAA,CACA,8BAAA,CAEA,oBAAA,CACA,6BAAA,CAEA,4BAAA,CACA,kCAAA,CAEA,oBAAA,CACA,mBAAA,CACA,kBAAA,CAGF,EACE,oBAAA,CAEF,KACE,oCAAA,CAAA","file":"tailwind.presets.min.css"}
\ No newline at end of file
diff --git a/src/assets/sass/tailwind.presets.scss b/src/assets/sass/tailwind.presets.scss
index e360c8fd4070d4114ecf22b1915a63e6a371de1a..2e5d3a45faedb7ec6173b7f7781b8f0000f0bce7 100644
--- a/src/assets/sass/tailwind.presets.scss
+++ b/src/assets/sass/tailwind.presets.scss
@@ -1,8 +1,14 @@
 @tailwind base;
 @tailwind components;
 @tailwind utilities;
-
 @layer base {
+  .conceal {
+    @apply opacity-0 h-0 w-0 p-0 m-0 overflow-hidden;
+  }
+  label,
+  details {
+    @apply block w-full pb-1 relative cursor-pointer disabled:cursor-not-allowed disabled:pointer-events-none disabled:opacity-60 lg:min-w-xs lg:w-[calc(1/2*100%-(1*1rem/2))] xl:w-[calc((1/4*100%)-(3*1rem/4))];
+  }
   img,
   svg,
   video,
@@ -14,6 +20,69 @@
     display: inline;
     vertical-align: middle;
   }
+  :root {
+    --background: 0 0% 100%;
+    --foreground: 0 0% 3.9%;
+
+    --card: 0 0% 100%;
+    --card-foreground: 0 0% 3.9%;
+
+    --popover: 0 0% 100%;
+    --popover-foreground: 0 0% 3.9%;
+
+    // --primary: 0 0% 9%;
+    --primary: 204 98% 37%;
+    --primary-foreground: 0 0% 98%;
+
+    --secondary: 0 0% 96.1%;
+    --secondary-foreground: 0 0% 9%;
+
+    --muted: 0 0% 96.1%;
+    --muted-foreground: 0 0% 45.1%;
+
+    --accent: 0 0% 96.1%;
+    --accent-foreground: 0 0% 9%;
+
+    --destructive: 353 100% 44%;
+    --destructive-foreground: 0 0% 98%;
+
+    --border: 0 0% 89.8%;
+    --input: 0 0% 89.8%;
+    --ring: 0 0% 3.9%;
+
+    --radius: 0.5rem;
+  }
+
+  .dark {
+    --background: 0 0% 3.9%;
+    --foreground: 0 0% 98%;
+
+    --card: 0 0% 3.9%;
+    --card-foreground: 0 0% 98%;
+
+    --popover: 0 0% 3.9%;
+    --popover-foreground: 0 0% 98%;
+
+    --primary: 0 0% 98%;
+    --primary-foreground: 0 0% 9%;
+
+    --secondary: 0 0% 14.9%;
+    --secondary-foreground: 0 0% 98%;
+
+    --muted: 0 0% 14.9%;
+    --muted-foreground: 0 0% 63.9%;
+
+    --accent: 0 0% 14.9%;
+    --accent-foreground: 0 0% 98%;
+
+    --destructive: 0 62.8% 30.6%;
+    --destructive-foreground: 0 0% 98%;
+
+    --border: 0 0% 14.9%;
+    --input: 0 0% 14.9%;
+    --ring: 0 0% 83.1%;
+  }
+
   * {
     @apply border-border;
   }
diff --git a/src/components/boxes/ConfirmBox.jsx b/src/components/boxes/ConfirmBox.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..1df510d3685bf83e9795cd24127fd7d7254c239d
--- /dev/null
+++ b/src/components/boxes/ConfirmBox.jsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import {
+  Dialog,
+  DialogClose,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+  DialogTrigger,
+} from "@/components/ui/dialog";
+import { Button } from '../ui/button';
+import Heading from '../font/Heading';
+
+function ConfirmBox({ confirmDialog, closeDialog, item, handleProceed, ...props }) {
+  // #################################
+  // HOOKS
+  // #################################
+
+  // #################################
+  // FUNCTIONS
+  // #################################
+
+  // #################################
+  // OUTPUT
+  // #################################
+  return (
+    <Dialog open={confirmDialog.open} onOpenChange={closeDialog}>
+      <DialogContent className="sm:max-w-md">
+        <DialogHeader>
+          <DialogTitle>Remove</DialogTitle>
+          <DialogDescription>
+            You are about to remove the following item:<br />
+            <span className='text-lg'>{JSON.stringify(confirmDialog.displayName)}</span>
+          </DialogDescription>
+        </DialogHeader>
+        Proceed?
+        <DialogFooter className="sm:justify-start gap-2">
+          <DialogClose asChild>
+            <Button>
+              Abort
+            </Button>
+          </DialogClose>
+          <DialogClose asChild>
+            <Button variant="destructive" onClick={handleProceed}>
+              remove
+            </Button>
+          </DialogClose>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  );
+}
+
+export default React.memo(ConfirmBox);
\ No newline at end of file
diff --git a/src/components/boxes/Infobox.jsx b/src/components/boxes/Infobox.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..4b259140cb2fdf83064d4ef0b8fb3902ce7b5e74
--- /dev/null
+++ b/src/components/boxes/Infobox.jsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { twMerge } from 'tailwind-merge';
+
+function Infobox({ children, className, type, ...props }) {
+  // #################################
+  // HOOKS
+  // #################################
+
+  // #################################
+  // FUNCTIONS
+  // #################################
+  // ### MERGE CLASSNAMES
+  // static class names every item should have
+  let staticClassName = 'box-border border border-UhhBlue p-4 my-4 md:inline-block';
+  // dynamic class names depending on the type
+  const typeClassName = () => {
+    switch (type) {
+      case 'warning': {
+        return 'border-orange-300 bg-orange-100/20';
+      }
+      case 'alert': {
+        return 'border-UhhRed bg-red-100/20';
+      }
+      default: {
+        return '';
+      }
+    }
+  };
+  // merge static and dynamic class names with className from props
+  const mergedClassName = twMerge(staticClassName, typeClassName(), className);
+
+  // #################################
+  // OUTPUT
+  // #################################
+  return (
+    <div>
+      <div className={mergedClassName}>{children}</div>
+    </div>
+  );
+}
+
+export default React.memo(Infobox);
\ No newline at end of file
diff --git a/src/components/boxes/Tooltip.jsx b/src/components/boxes/Tooltip.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..0d0044ce7238376a8922a382c54bf3a4d7ef8b0a
--- /dev/null
+++ b/src/components/boxes/Tooltip.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { RiQuestionLine } from 'react-icons/ri';
+
+function Tooltip({ children, ...props }) {
+  // #################################
+  // HOOKS
+  // #################################
+
+  // #################################
+  // FUNCTIONS
+  // #################################
+
+  // #################################
+  // OUTPUT
+  // #################################
+  return (
+    <span className='group ml-2'>
+      <RiQuestionLine className='text-UhhBlue hover:cursor-help' />
+      <div className='absolute invisible group-hover:visible group-hover:z-50 bg-UhhWhite border border-UhhBlue p-2'>
+        {children}
+      </div>
+    </span>
+  );
+}
+
+export default React.memo(Tooltip);
\ No newline at end of file
diff --git a/src/components/font/Heading.jsx b/src/components/font/Heading.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..3516ae105b7008c1647e4ba3c79fa6cf7012ec27
--- /dev/null
+++ b/src/components/font/Heading.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { twMerge } from 'tailwind-merge';
+
+function Heading({ level = 1, children, className, ...props }) {
+  // #################################
+  // HOOKS
+  // #################################
+
+  // #################################
+  // FUNCTIONS
+  // #################################
+  const Tag = `h${level}`;
+
+  // ### MERGE CLASSNAMES
+  // static class names every item should have
+  const staticClassName = '';
+  // dynamic class names depending on the type
+  const typeClassName = () => {
+    switch (level) {
+      case '1': {
+        return 'font-UhhBC text-4xl my-8';
+      }
+      case '2': {
+        return 'font-UhhSLC text-3xl leading-7';
+      }
+      case '3': {
+        return 'font-UhhSLC text-3xl';
+      }
+      case '4': {
+        return 'font-UhhB text-2xl py-6';
+      }
+      case '5': {
+        return 'font-UhhSLC text-lg';
+      }
+      case '6': {
+        return 'font-UhhSLC';
+      }
+    }
+  };
+  // merge static and dynamic class names with className from props
+  const mergedClassName = twMerge(staticClassName, typeClassName(), className);
+
+
+  // #################################
+  // OUTPUT
+  // #################################
+  return (
+    <>
+      <Tag className={mergedClassName}>{children}</Tag>
+    </>
+  );
+}
+
+export default React.memo(Heading);
\ No newline at end of file
diff --git a/src/components/form/Fieldset.jsx b/src/components/form/Fieldset.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..d5d298e9f6f274b58742e553f21fddef5464280c
--- /dev/null
+++ b/src/components/form/Fieldset.jsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { twMerge } from 'tailwind-merge';
+
+function Fieldset({ title, className, type, children, ...props }) {
+  // #################################
+  // HOOKS
+  // #################################
+
+  // #################################
+  // FUNCTIONS
+  // #################################
+  // ### MERGE CLASSNAMES
+  // static class names every item should have
+  const staticClassName = 'mb-8 p-1 border-t border-UhhLightGray';
+  // dynamic class names depending on the type
+  const typeClassName = () => {
+    switch (type) {
+      default: {
+        return '';
+      }
+    }
+  };
+  // merge static and dynamic class names with className from props
+  const mergedClassName = twMerge(staticClassName, typeClassName(), className);
+
+  // #################################
+  // OUTPUT
+  // #################################
+  return (
+    <fieldset className={mergedClassName}>
+      <legend className='ml-2 text-xs text-UhhGrey'>{title}</legend>
+      {children}
+    </fieldset>
+  );
+}
+
+export default React.memo(Fieldset);
\ No newline at end of file
diff --git a/src/components/form/File.jsx b/src/components/form/File.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..77036a913dd026aa8c62fd8097a7c8f3e4bb4f2e
--- /dev/null
+++ b/src/components/form/File.jsx
@@ -0,0 +1,111 @@
+import React, { useEffect, useId, useState } from 'react';
+import { useFormContext, useWatch } from 'react-hook-form';
+import { RiEdit2Line, RiDeleteBinLine } from 'react-icons/ri';
+import api from '../../utils/AxiosConfig';
+import { mergeBackendValidation } from '../../utils/ErrorHandling';
+import { capitalizeFirstLetter } from '../../utils';
+
+function File({ name, currentfile, title, deletefnc, ...props }) {
+  // #################################
+  // HOOKS
+  // #################################
+  // ### Prepare form
+  const {
+    register,
+    control,
+    setError,
+    formState: { errors }
+  } = useFormContext();
+
+  const id = useId();
+
+  const watchFile = useWatch({
+    control,
+    name
+  });
+
+  // ### PREVIEW
+  const [mimetype, setMimetype] = useState();
+  const [blob, setBlob] = useState();
+
+
+  //### FILE PREVIEW
+  useEffect(() => {
+    if (watchFile) {
+      const reader = new FileReader();
+      reader.onloadend = () => {
+        // set mimetype to decide which media element shall be rendered
+        setMimetype((watchFile[0].type).split('/')[0]);
+        // set blob for media element
+        setBlob(reader.result);
+      };
+      if (watchFile[0]) reader.readAsDataURL(watchFile[0]);
+    };
+  }, [watchFile]);
+
+  // ### GET FILE
+  useEffect(() => {
+    const getFile = async () => {
+      try {
+        await api.post('/files/download', { path: currentfile.path }, { responseType: 'blob' }).then(result => {
+          let objectURL = URL.createObjectURL(result.data);
+          // set mimetype to decide which media element shall be rendered
+          setMimetype((currentfile?.mimetype).split('/')[0]);
+          // set blob for media element
+          setBlob(objectURL);
+        });
+      } catch (error) {
+        mergeBackendValidation(error.response.status, error.response.data, setError);
+      }
+    };
+    getFile();
+  }, [currentfile]);
+
+  // #################################
+  // FUNCTIONS
+  // #################################
+
+
+  // #################################
+  // OUTPUT
+  // #################################
+  return (
+    <label htmlFor={id}>
+      {capitalizeFirstLetter(title)}
+
+      <input {...register(name)}
+        id={id}
+        aria-invalid={errors[name] ? 'true' : 'false'}
+        className='hidden aria-[invalid=true]:border-UhhRed'
+        autoComplete='off'
+        {...props}
+      />
+      <div className="relative bg-UhhWhite p-1 max-w-fit border border-UhhGrey">
+        {mimetype === 'video' ?
+          <video controls>
+            <source src={blob} type="video/mp4" />
+          </video>
+          :
+          <img src={blob} alt={`file for ${title}`} className="max-h-48 object-cover" title={currentfile.originalname} />
+        }
+
+        {!watchFile &&
+          <div className="absolute inset-0 flex justify-evenly group items-center text-UhhWhite ">
+            <span className="w-full h-full text-center flex justify-center items-center bg-UhhBlue opacity-0 group-hover:opacity-70 hover:!opacity-100 transition-all">
+              <RiEdit2Line className="inset-0" title="click to select a new media" />
+            </span>
+
+            <span onClick={deletefnc} className="w-full h-full text-center flex justify-center items-center bg-UhhRed opacity-0 group-hover:opacity-70 hover:!opacity-100 transition-all">
+              <RiDeleteBinLine className="inset-0 pointer-events-none" title="click to delete this media" />
+            </span>
+          </div>
+        }
+
+      </div>
+      {watchFile && <p className="validationNote mt-2 text-UhhBlue text-xs overflow-auto">Preview only. Save to make change permanent.</p>}
+      <p className="validationNote mt-2 text-UhhRed text-xs overflow-auto"></p>
+    </label>
+  );
+}
+
+export default React.memo(File);
\ No newline at end of file
diff --git a/src/components/form/FlatListEdit copy.jsx b/src/components/form/FlatListEdit copy.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..f2e35c02f6f9a75dd9927d14a1337c5163367124
--- /dev/null
+++ b/src/components/form/FlatListEdit copy.jsx	
@@ -0,0 +1,154 @@
+import React, { useEffect, useState } from 'react';
+import Textarea from './Textarea';
+import { Button } from '../ui/button';
+import ConfirmBox from '../boxes/ConfirmBox';
+import { useFieldArray } from 'react-hook-form';
+import { RiDeleteBin5Line } from 'react-icons/ri';
+import {
+  Table,
+  TableBody,
+  TableCaption,
+  TableCell,
+  TableRow,
+} from "@/components/ui/table";
+import THead from '../../components/table/THead';
+import { useTable } from '@/contexts/Table/TableState';
+
+function FlatListEdit({ methods, initialItems, fieldName, keyName, validator, tableCols, ...props }) {
+  // #################################
+  // HOOKS
+  // #################################
+
+  // ### DEFINE TABLE
+  const { tableState, initTable, replaceItems, removeItem } = useTable();
+
+  useEffect(() => {
+    // initialize table state
+    initTable({ tableCols, baseData: initialItems });
+    replace(initialItems);
+  }, [initialItems]);
+
+
+  // ### CONFIRM DIALOG
+  const [confirmDialog, setConfirmDialog] = useState({ open: false, item: {} });
+
+  // ### USE FIELD ARRAY TO STORE ITEMS
+  const { fields, remove, replace } = useFieldArray({
+    control: methods.control,
+    name: fieldName,
+  });
+
+
+  // #################################
+  // FUNCTIONS
+  // #################################
+
+  // ### ADD ITEMS FROM TEXTAREA
+  const handleAddItems = (e) => {
+    // clear errors
+    methods.clearErrors('addItem');
+    // fetch values from textarea and split new lines & csv into array
+    const addItems = methods.getValues('addItem').split(/[\n,]/);
+    // return on empty input
+    if (addItems.length === 1 && addItems[0] === '') return;
+
+    // get valid entries
+    // trim spaces from each input
+    addItems.forEach((input, idx) => addItems[idx] = input.trim());
+    // filter array for valid mail addresses using zod
+    const validInputs = addItems.filter((input, idx) => {
+      const isMail = validator(input);
+      return isMail.success ?? keyName;
+    });
+    console.log('validInputs', validInputs);
+
+    // handle invalid inputs
+    // strip off valid entries from input to get remaining invalid inputs
+    const invalidInputs = addItems.filter(keyName => !validInputs.includes(keyName));
+    // set input value to remaining invalid entries
+    methods.setValue('addItem', invalidInputs.join('\n'));
+    if (invalidInputs.length > 0) {
+      // set error message on input
+      methods.setError('addItem', { message: 'invalid entries remaining' });
+    }
+
+    // handle valid inputs
+    // get existing items from fieldarray
+    const extistingItems = methods.getValues(fieldName);
+    // flatten fieldarray to array of strings only to merge with new inputs
+    let flatArray = extistingItems.map(field => field[keyName]);
+    // add valid inputs to existing items avoiding douplettes
+    flatArray = ([... new Set([...flatArray, ...validInputs])]);
+    // sort & convert flat array back into object
+    const wholeItemsObject = flatArray.sort().map(input => ({ [keyName]: input }));
+    // replace whole table data (invisible)
+    replaceItems(wholeItemsObject);
+    // replace whole fieldarray, which will be submitted to backend
+    replace(wholeItemsObject);
+    // set focus back to input field, which is only used for display
+    methods.setFocus('addItem');
+  };
+
+
+  // ### REMOVE ITEM FROM ARRAY
+  const handleRemoveItem = (idx) => {
+    // delete from fieldarray, which will be submitted to backend
+    remove(idx);
+    // delete from tableData, which is only used for display
+    replaceItems(methods.getValues(fieldName));
+  };
+
+  // #################################
+  // OUTPUT
+  // #################################
+  if (!tableState.tableData) return <div>loading...</div>;
+
+
+  return (
+    <>
+      {JSON.stringify(tableState.orderBy)}
+      {/* add Owner */}
+      <Textarea name='addItem' title={fieldName} tooltip={props.tooltip}>
+        <Button type='button' variant="default" onClick={handleAddItems} className='px-4 h-16 self-center'>add</Button>
+      </Textarea>
+      {/* List */}
+      <Table>
+        <TableCaption>{tableState.tableData.length} of {tableState.baseData.length}</TableCaption>
+        <THead actionCol={true} />
+        <TableBody>
+          {tableState.tableData && tableState.tableData.map((item, idx) => {
+            return <TableRow key={idx}>
+              <TableCell>
+                {item[keyName]}
+              </TableCell >
+              <TableCell>
+                <Button
+                  type="button"
+                  onClick={() => setConfirmDialog({ open: true, idToDelete: idx, displayName: item[keyName] })}
+                  variant='destructive'
+                  size='icon'
+                  title='delete from list'
+                >
+                  <RiDeleteBin5Line />
+                </Button>
+              </TableCell>
+            </TableRow>;
+          })}
+        </TableBody>
+      </Table>
+
+      {fields && fields.map((item, idx) => {
+        return <div key={idx}>
+          <input className='w-full h-full bg-transparent' type='hidden' key={item.id} {...methods.register(`${fieldName}.${idx}.${keyName}`)} disabled={true} />
+        </div>;
+      })}
+
+
+
+      {/* confirmDialog */}
+      <ConfirmBox confirmDialog={confirmDialog} closeDialog={() => setConfirmDialog({ ...confirmDialog, open: false })} handleProceed={() => { handleRemoveItem(confirmDialog.idToDelete); }} />
+    </>
+  );
+}
+
+export default React.memo(FlatListEdit);;
\ No newline at end of file
diff --git a/src/components/form/FlatListEdit.jsx b/src/components/form/FlatListEdit.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..54a954c269c1c1d6d0a552230d5197bb52a4abcc
--- /dev/null
+++ b/src/components/form/FlatListEdit.jsx
@@ -0,0 +1,118 @@
+import React, { useEffect, useState } from 'react';
+import Textarea from './Textarea';
+import { Button } from '../ui/button';
+import ConfirmBox from '../boxes/ConfirmBox';
+import CustomTable from '../table/CustomTable';
+import { useFieldArray } from 'react-hook-form';
+
+
+function FlatListEdit({ initialItems = [], fieldName = '', tooltip = '', columns = [], methods, validator, keyName, confirmDialog, setConfirmDialog }) {
+  // #################################
+  // HOOKS
+  // #################################
+
+  // store current data in state
+  const [data, setData] = useState(initialItems);
+  // set first data set
+  useEffect(() => {
+    // initialize table state
+    setData(initialItems);
+    replace(initialItems);
+  }, [initialItems]);
+
+  // ### USE FIELD ARRAY TO STORE ITEMS
+  const { fields, remove, replace } = useFieldArray({
+    control: methods.control,
+    name: fieldName,
+  });
+
+  // #################################
+  // FUNCTIONS
+  // #################################
+  // ### ADD ITEMS FROM TEXTAREA
+  const handleAddItems = (e) => {
+    // clear error on input field
+    methods.clearErrors('addItem');
+    // fetch values from textarea and split new lines & csv into array
+    const addItems = methods.getValues('addItem').split(/[\n,]/);
+    // return on empty input
+    if (addItems.length === 1 && addItems[0] === '') return;
+
+
+    // get valid entries
+    // trim spaces from each input
+    addItems.forEach((input, idx) => addItems[idx] = input.trim());
+    // filter array for valid entries using zod
+    const validInputs = addItems.filter((input, idx) => {
+      const isValid = validator(input);
+      return isValid.success ?? keyName;
+    });
+    // exit if no valid items found
+    if (validInputs.length === 0) return;
+    console.log('validInputs', validInputs);
+
+    // handle invalid inputs
+    // strip off valid entries from input to get remaining invalid inputs
+    const invalidInputs = addItems.filter(keyName => !validInputs.includes(keyName));
+    // set input value to remaining invalid entries
+    methods.setValue('addItem', invalidInputs.join('\n'));
+    if (invalidInputs.length > 0) {
+      // set error message on input
+      methods.setError('addItem', { message: 'invalid entries remaining' });
+    }
+    console.log('invalidInputs', invalidInputs);
+
+
+    // handle valid inputs
+    // get existing items from fieldarray
+    const extistingItems = methods.getValues(fieldName) || [];
+
+    // flatten fieldarray to array of strings only to merge with new inputs
+    let flatArray = extistingItems.map(field => field[keyName]);
+    // add valid inputs to existing items avoiding douplettes
+    flatArray = ([... new Set([...flatArray, ...validInputs])]);
+    // sort & convert flat array back into object
+    const wholeItemsObject = flatArray.sort().map(input => ({ [keyName]: input }));
+    // replace whole table data (invisible)
+    setData(wholeItemsObject);
+    // replace whole fieldarray, which will be submitted to backend
+    replace(wholeItemsObject);
+    // set focus back to input field, which is only used for display
+    methods.setFocus('addItem');
+  };
+
+
+  // ### REMOVE ITEM FROM ARRAY
+  const handleRemoveItem = (idx) => {
+    // delete from fieldarray, which will be submitted to backend
+    remove(idx);
+    // delete from tableData, which is only used for display
+    setData(methods.getValues(fieldName));
+  };
+
+  // #################################
+  // OUTPUT
+  // #################################
+
+  return (
+    <>
+      {/* add Owner */}
+      <Textarea name='addItem' title={fieldName} tooltip={tooltip}>
+        <Button type='button' variant="default" onClick={handleAddItems} className='px-4 h-16 self-center'>add</Button>
+      </Textarea>
+      {/* List */}
+      <CustomTable columns={columns} data={data} />
+
+      {fields && fields.map((item, idx) => {
+        return <div key={idx}>
+          <input className='w-full h-full' type='hidden' key={item.id} {...methods.register(`${fieldName}.${idx}.${keyName}`)} disabled={true} />
+        </div>;
+      })}
+
+      {/* confirmDialog */}
+      <ConfirmBox confirmDialog={confirmDialog} closeDialog={() => setConfirmDialog({ ...confirmDialog, open: false })} handleProceed={() => { handleRemoveItem(confirmDialog.idToDelete); }} />
+    </>
+  );
+}
+
+export default FlatListEdit;
\ No newline at end of file
diff --git a/src/components/form/Input.jsx b/src/components/form/Input.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..8d9f8c655a134e1b174605e30e8954d04be79dac
--- /dev/null
+++ b/src/components/form/Input.jsx
@@ -0,0 +1,62 @@
+import React, { useId } from 'react';
+import { useFormContext } from 'react-hook-form';
+import { twMerge } from 'tailwind-merge';
+import Tooltip from '../boxes/Tooltip';
+import RequiredBadge from './RequiredBadge';
+import { capitalizeFirstLetter } from '../../utils';
+
+function Input({ title, name, type, className, tooltip, ...props }) {
+  // #################################
+  // HOOKS
+  // #################################
+  const {
+    register,
+    formState: { errors }
+  } = useFormContext();
+
+  const id = useId();
+  // #################################
+  // FUNCTIONS
+  // #################################
+  // ### MERGE CLASSNAMES
+  // static class names every item should have
+  const staticClassName = 'focus:outline-none focus:border-UhhBlue block box-border h-8 px-4 border border-UhhGrey bg-UhhLightGrey w-full caret-UhhRed aria-[invalid=true]:border-UhhRed disabled:text-UhhGrey/70 disabled:cursor-not-allowed';
+  // dynamic class names depending on the type
+  const typeClassName = () => {
+    switch (type) {
+      case 'file': {
+        return 'file:text-xs file:py-2 file:border-0 file:bg-UhhBlue file:text-UhhWhite hover:cursor-pointer hover:file:cursor-pointer hover:file:text-UhhBlue hover:file:bg-UhhWhite pl-0';
+      }
+      default: {
+        return ' ';
+      }
+    }
+  };
+  // merge static and dynamic class names with className from props
+  const mergedClassName = twMerge(staticClassName, className);
+
+  // #################################
+  // OUTPUT
+  // #################################
+  return (
+    <>
+      <label htmlFor={id}>
+        {props.required && <RequiredBadge />}
+        {capitalizeFirstLetter(title)}
+        {tooltip && <Tooltip>{tooltip}</Tooltip>}
+
+        <input {...register(name)}
+          type={type}
+          id={id}
+          aria-invalid={errors[name] ? 'true' : 'false'}
+          className={mergedClassName}
+          autoComplete={type === 'password' ? 'off' : 'on'}
+          {...props}
+        />
+        <p className="validationNote mt-2 text-UhhRed text-xs overflow-auto">{errors[name]?.message}</p>
+      </label>
+    </>
+  );
+};
+
+export default React.memo(Input);
\ No newline at end of file
diff --git a/src/components/form/RequiredBadge.jsx b/src/components/form/RequiredBadge.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..721922a808e6f0df860b0ee44a9db423899454e3
--- /dev/null
+++ b/src/components/form/RequiredBadge.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+
+function RequiredBadge() {
+  // #################################
+  // HOOKS
+  // #################################
+
+  // #################################
+  // FUNCTIONS
+  // #################################
+
+  // #################################
+  // OUTPUT
+  // #################################
+  return (
+    <span className="text-UhhRed leading-4" title="This field is required">* </span>
+  );
+}
+
+export default React.memo(RequiredBadge);
\ No newline at end of file
diff --git a/src/components/form/Select.jsx b/src/components/form/Select.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..f2fc954bc01b6539c1c1f98a28082c3afa3982ed
--- /dev/null
+++ b/src/components/form/Select.jsx
@@ -0,0 +1,71 @@
+import React, { useId } from 'react';
+import { Controller, useFormContext, useFormState } from 'react-hook-form';
+import { twMerge } from 'tailwind-merge';
+import RequiredBadge from './RequiredBadge';
+import Tooltip from '../boxes/Tooltip';
+import { capitalizeFirstLetter } from '../../utils';
+
+
+function Select({ options, name, title, defaultValue, className, tooltip, type, ...props }) {
+  // #################################
+  // HOOKS
+  // #################################
+  const {
+    control,
+    formState: { errors }
+  } = useFormContext();
+
+
+
+
+  // GENERATE RANDOM UUID
+  const id = useId();
+
+  // #################################
+  // FUNCTIONS
+  // #################################
+  // ### MERGE CLASSNAMES
+  // static class names every item should have
+  const staticClassName = 'focus:outline-none focus:border-UhhBlue block box-border h-8 px-4 border border-UhhGrey bg-UhhLightGrey w-full aria-[invalid=true]:border-UhhRed';
+  // dynamic class names depending on the type
+  const typeClassName = () => {
+    switch (type) {
+      default: {
+        return '';
+      }
+    }
+  };
+  // merge static and dynamic class names with className from props
+  const mergedClassName = twMerge(staticClassName, typeClassName(), className);
+
+  // #################################
+  // OUTPUT
+  // #################################
+  return (
+    <label htmlFor={id}>
+      {props.required && <RequiredBadge />}
+      {capitalizeFirstLetter(title)}
+      {tooltip && <Tooltip>{tooltip}</Tooltip>}
+
+      <Controller
+        render={
+          ({ field }) => <select className={mergedClassName} {...field} {...props}>
+            <option key={`option-${id}`} value='' disabled>-- Select --</option>
+            {options.map(option => (
+              <option key={`option-${option._id}`} value={option._id}>
+                {option.title}
+              </option>
+            ))}
+          </select>
+        }
+        control={control}
+        name={name}
+        defaultValue={defaultValue}
+        id={id}
+      />
+      <p className="validationNote mt-2 text-UhhRed text-xs overflow-auto">{errors[name]?.message}</p>
+    </label>
+  );
+}
+
+export default React.memo(Select);
\ No newline at end of file
diff --git a/src/components/form/Submit.jsx b/src/components/form/Submit.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..f244f1bacdfe1de1c19e80f8aa8ddcf1e58ad752
--- /dev/null
+++ b/src/components/form/Submit.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { useFormContext } from 'react-hook-form';
+import { twMerge } from 'tailwind-merge';
+
+function Submit({ value, type, className, ...props }) {
+  // #################################
+  // HOOKS
+  // #################################
+  const {
+    formState: { isSubmitting, isValid }
+  } = useFormContext();
+
+  // #################################
+  // FUNCTIONS
+  // #################################
+  // ### MERGE CLASSNAMES
+  // static class names every item should have
+  let staticClassName = 'inline-block w-full h-16 mb-4 box-border border border-UhhBlue leading-[4rem] text-center align-middle bg-UhhBlue border-UhhBlue text-UhhWhite font-UhhBC cursor-pointer disabled:cursor-not-allowed disabled:opacity-60 lg:min-w-xs lg:w-[calc(1/2*100%-(1*1rem/2))] xl:w-[calc((1/4*100%)-(3*1rem/4))] hover:text-UhhBlue hover:bg-UhhWhite';
+  // dynamic class names depending on the type
+  const typeClassName = () => {
+    switch (type = 'submit') {
+      default: {
+        return '';
+      }
+    }
+  };
+  // merge static and dynamic class names with className from props
+  const mergedClassName = twMerge(staticClassName, typeClassName(), className);
+  // #################################
+  // OUTPUT
+  // #################################
+  return (
+    <>
+      {/* <input type="submit" className={mergedClassName} value={value} disabled={isSubmitting} /> */}
+      <input type="submit" className={mergedClassName} value={value} disabled={!isValid || isSubmitting} {...props} />
+    </>
+  );
+}
+
+export default React.memo(Submit);
\ No newline at end of file
diff --git a/src/components/form/Textarea.jsx b/src/components/form/Textarea.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..bce7ced7e573039be71f0a2877f0989c48ea805b
--- /dev/null
+++ b/src/components/form/Textarea.jsx
@@ -0,0 +1,63 @@
+import React, { useId } from 'react';
+import { useFormContext } from 'react-hook-form';
+import { twMerge } from 'tailwind-merge';
+import Tooltip from '../boxes/Tooltip';
+import RequiredBadge from './RequiredBadge';
+import { capitalizeFirstLetter } from '../../utils';
+
+
+function Textarea({ title, name, className, type, tooltip, children, ...props }) {
+  // #################################
+  // HOOKS
+  // #################################
+  const {
+    register,
+    formState: { errors }
+  } = useFormContext();
+
+  const id = useId();
+  // #################################
+  // FUNCTIONS
+  // #################################
+  // ### MERGE CLASSNAMES
+  // static class names every item should have
+  const staticClassName = 'h-24 focus:outline-none focus:border-UhhBlue block box-border h-16 py-2 px-4 border border-UhhGrey bg-UhhLightGrey w-full caret-UhhRed aria-[invalid=true]:border-UhhRed text-sm sm:text-base';
+  // dynamic class names depending on the type
+  const typeClassName = () => {
+    switch (type) {
+      default: {
+        return '';
+      }
+    }
+  };
+  // merge static and dynamic class names with className from props
+  const mergedClassName = twMerge(staticClassName, typeClassName(), className);
+
+  // #################################
+  // OUTPUT
+  // #################################
+  return (
+    <>
+      <label htmlFor={id}>
+        {props.required && <RequiredBadge />}
+        {capitalizeFirstLetter(title)}
+        {tooltip && <Tooltip>{tooltip}</Tooltip>}
+
+        <div className="inline-flex w-full">
+          <textarea {...register(name)}
+            id={id}
+            aria-invalid={errors[name] ? 'true' : 'false'}
+            className={mergedClassName}
+            autoComplete='off'
+            {...props}>
+          </textarea>
+
+          {children}
+        </div>
+        <p className="validationNote mt-2 text-UhhRed text-xs overflow-auto">{errors[name]?.message}</p>
+      </label>
+    </>
+  );
+}
+
+export default React.memo(Textarea);
\ No newline at end of file
diff --git a/src/contexts/Auth/AuthContext.jsx b/src/contexts/Auth/AuthContext.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..030c594a8d33277d34b59713f09865677dc0456f
--- /dev/null
+++ b/src/contexts/Auth/AuthContext.jsx
@@ -0,0 +1,5 @@
+import { createContext } from "react";
+
+const AuthContext = createContext();
+
+export default AuthContext;
\ No newline at end of file
diff --git a/src/contexts/Auth/AuthReducer.jsx b/src/contexts/Auth/AuthReducer.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..8c5f0af3fb15338b38b36356a02339c9e23a5313
--- /dev/null
+++ b/src/contexts/Auth/AuthReducer.jsx
@@ -0,0 +1,15 @@
+import { USER_ACTIONS } from "./AuthTypes";
+const authReducer = (state, action) => {
+  switch (action.type) {
+    case USER_ACTIONS.SET:
+      localStorage.setItem("user", JSON.stringify(action.payload));
+      return action.payload;
+    case USER_ACTIONS.DROP:
+      localStorage.removeItem("user");
+      localStorage.removeItem("accessToken");
+      return null;
+    default:
+      return state;
+  }
+};
+export default authReducer;
\ No newline at end of file
diff --git a/src/contexts/Auth/AuthState.jsx b/src/contexts/Auth/AuthState.jsx
new file mode 100755
index 0000000000000000000000000000000000000000..c7a904d229c0c1002125e433e998b8d90c74a7e0
--- /dev/null
+++ b/src/contexts/Auth/AuthState.jsx
@@ -0,0 +1,66 @@
+import React, { useContext, useReducer, useState } from 'react';
+import AuthContext from './AuthContext';
+import authReducer from './AuthReducer';
+import { USER_ACTIONS } from './AuthTypes';
+import api from '../../utils/AxiosConfig';
+
+// ### EXPORT useContext TO REDUCE NEEDED CODE IN CLIENT FILES
+export function useAuth() {
+  return useContext(AuthContext);
+}
+
+function AuthState({ children }) {
+  // #################################
+  // HOOKS
+  // #################################
+  // ### CURRENTUSER_ACTIONS
+  // set default user for first page load and hard reloads (<CTRL +> F5)
+  const initial_currenUser = JSON.parse(localStorage.getItem('user')) || null;
+  const [currentUser, dispatchCurrentUser] = useReducer(authReducer, initial_currenUser);
+  // ### ACCESSTOKEN
+  const [accessToken, setAccessToken] = useState();
+
+  // #################################
+  // FUNCTIONS
+  // #################################
+
+  // ### LOGIN
+  async function login(credentials) {
+    const result = await api.post(
+      '/users/login',
+      credentials,
+      { withCredentials: true }
+    );
+    // set current user to login and merge accessToken into currentUser
+    dispatchCurrentUser({ type: USER_ACTIONS.SET, payload: { ...result.data.document } });
+    setAccessToken(result.data.accessToken);
+    // TODO: don't store accessToken in localStorage, keep in memory only
+    localStorage.setItem("accessToken", JSON.stringify(result.data.accessToken));
+    return result;
+  }
+
+  // ### HANDLE LOGOUT
+  async function logout() {
+    dispatchCurrentUser({ type: USER_ACTIONS.DROP });
+    const result = await api.delete(
+      '/users/logout',
+      { withCredentials: true }
+    );
+    return result;
+  }
+
+  // ### RETURN
+  return (
+    <AuthContext.Provider
+      value={{
+        login,
+        logout,
+        USER_ACTIONS,
+        dispatchCurrentUser
+      }}>
+      {children}
+    </AuthContext.Provider>
+  );
+};
+
+export default React.memo(AuthState); 
\ No newline at end of file
diff --git a/src/contexts/Auth/AuthTypes.js b/src/contexts/Auth/AuthTypes.js
new file mode 100755
index 0000000000000000000000000000000000000000..3520a8d816b57c8dbbcde685ac15ea01469bc6ac
--- /dev/null
+++ b/src/contexts/Auth/AuthTypes.js
@@ -0,0 +1,4 @@
+export const USER_ACTIONS = {
+  SET: 'set',
+  DROP: 'drop'
+};
\ No newline at end of file
diff --git a/src/layouts/partials/Header.jsx b/src/layouts/partials/Header.jsx
index dcc89fbf132ada751e72c51c157d3ae788331a32..4e38ee0720f5fa807d5f2dd7f7f29489f94d60b8 100644
--- a/src/layouts/partials/Header.jsx
+++ b/src/layouts/partials/Header.jsx
@@ -6,8 +6,7 @@ import { RiFilter2Line } from 'react-icons/ri';
 import Hamburger from 'hamburger-react';
 
 
-
-function Header({ showMobileNav, toggleMobileNav }) {
+function Header({ layout, showMobileNav, toggleMobileNav }) {
   // #################################
   // HOOKS
   // #################################
@@ -19,45 +18,59 @@ function Header({ showMobileNav, toggleMobileNav }) {
   // #################################
   // OUTPUT
   // #################################
-  return (
-    <header role="banner" className='row-start-1 col-span-full sm:flex justify-center'>
-      {/* mobile */}
-      <div className="flex justify-between items-center bg-UhhBlue text-UhhWhite px-4 py-2 sm:hidden">
-        <a className="hidden h-16" href="https://www.uni-hamburg.de" target="_blank" title="UHH [external]">
-          <UhhImg />
-        </a>
-        <Link className="flex" to="/">
-          <div className="h-8 pr-2">
-            <PortalImg alt='Logo of ZBH Portal' />
-          </div>
-          <h2>ZBH-Portal</h2>
-        </Link>
-        <span className="h-12 flex items-center gap-x-4">
-          <div className="h-8 w-8">
-            <RiFilter2Line className='text-3xl' />
-          </div>
-          <div className="h-full w-8">
-            <Hamburger label='show navigation' toggled={showMobileNav} onToggle={toggleMobileNav} duration={0.3} />
-          </div>
-        </span>
-      </div>
-      {/* desktop */}
-      <div className="hidden text-UhhWhite px-4 py-2 sm:flex justify-between container">
-        <a className="inline-block h-20" href="https://www.uni-hamburg.de" target="_blank" title="UHH [external]">
+
+  // CLEAN LAYOUT
+  if (layout === 'clean') {
+    return (
+      <header role="banner" className="px-4 py-2">
+        <a className="block h-16" href="https://www.uni-hamburg.de" target="_blank" title="UHH [external]">
           <UhhImg />
         </a>
-        <Link className="flex items-center" to="/">
-          <div className="text-UhhBlue h-20 pr-2">
-            <PortalImg />
-          </div>
-          <h2 className="font-UhhBC">
-            <div className="text-UhhGrey">ZBH</div>
-            <div className="text-UhhBlue">Portal</div>
-          </h2>
-        </Link>
-      </div>
-    </header>
-  );
+      </header>
+    );
+  } else {
+
+    // MAIN LAYOUT
+    return (
+      <header role="banner" className='row-start-1 col-span-full sm:flex justify-center'>
+        {/* mobile */}
+        <div className="flex justify-between items-center bg-UhhBlue text-UhhWhite px-4 py-2 sm:hidden">
+          <a className="hidden h-16" href="https://www.uni-hamburg.de" target="_blank" title="UHH [external]">
+            <UhhImg />
+          </a>
+          <Link className="flex" to="/">
+            <div className="h-8 pr-2">
+              <PortalImg alt='Logo of ZBH Portal' />
+            </div>
+            <h2>ZBH-Portal</h2>
+          </Link>
+          <span className="h-12 flex items-center gap-x-4">
+            <div className="h-8 w-8">
+              <RiFilter2Line className='text-3xl' />
+            </div>
+            <div className="h-full w-8">
+              <Hamburger label='show navigation' toggled={showMobileNav} onToggle={toggleMobileNav} duration={0.3} />
+            </div>
+          </span>
+        </div>
+        {/* desktop */}
+        <div className="hidden text-UhhWhite px-4 py-2 sm:flex justify-between container">
+          <a className="inline-block h-20" href="https://www.uni-hamburg.de" target="_blank" title="UHH [external]">
+            <UhhImg />
+          </a>
+          <Link className="flex items-center" to="/">
+            <div className="text-UhhBlue h-20 pr-2">
+              <PortalImg />
+            </div>
+            <h2 className="font-UhhBC">
+              <div className="text-UhhGrey">ZBH</div>
+              <div className="text-UhhBlue">Portal</div>
+            </h2>
+          </Link>
+        </div>
+      </header>
+    );
+  }
 }
 
 export default React.memo(Header);
\ No newline at end of file
diff --git a/src/pages/User/Login.jsx b/src/pages/User/Login.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..d0adc6e8324c65c17772f61cc30b0f46a0c4a27e
--- /dev/null
+++ b/src/pages/User/Login.jsx
@@ -0,0 +1,89 @@
+import React, { useEffect } from 'react';
+import { Link, useLocation, useNavigate } from 'react-router-dom';
+import { useAuth } from '../../contexts/Auth/AuthState';
+import { FormProvider, useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from "zod";
+import { toast } from 'react-toastify';
+import { mergeBackendValidation, setFlashMsg } from '../../utils/ErrorHandling';
+import Input from '../../components/form/Input';
+import Submit from '../../components/form/Submit';
+import { Helmet } from 'react-helmet-async';
+import Heading from '../../components/font/Heading';
+
+function Login() {
+
+  // #################################
+  // VALIDATION SCHEMA
+  // #################################
+  const schema = z.object({
+    email: z.string().min(1),
+    password: z.string().min(1)
+  });
+
+  // #################################
+  // HOOKS
+  // #################################
+  // ### CONNECT AUTH CONTEXT
+  const { login } = useAuth();
+  // ### MAKE USE OF NAVIGATION
+  const redirect = useNavigate();
+
+  // ### MAKE USE OF location state to fetch former requested page
+  const { state } = useLocation();
+  // ### PREPARE FORM
+  const methods = useForm({
+    resolver: zodResolver(schema),
+    mode: 'onSubmit',
+    defaultValues: {
+      email: '',
+      password: ''
+    }
+  });
+
+  // #################################
+  // FUNCTIONS
+  // #################################
+  // ### HANDLE SUBMITTING LOGIN FORM
+  async function handleSendForm(record) {
+    try {
+      // send data to login function
+      const result = await login(record);
+      // if former page request was saved, redirect to this page
+      // otherwise redirect to default page
+      (state) ? redirect(`${state.redirectTo.pathname}${state.redirectTo.search}`) : redirect(import.meta.env.VITE_PAGE_AFTER_LOGIN);
+      // FIX: flash message not dislayed
+      setFlashMsg(result.data?.message);
+
+    } catch (err) {
+      console.log(err);
+      // merge front & backend validation errors
+      mergeBackendValidation(err.response.status, err.response.data, methods.setError);
+    }
+  }
+
+  // #################################
+  // OUTPUT
+  // #################################
+  return (
+    <>
+      {/* render page title */}
+      <Helmet><title>[{import.meta.env.VITE_APP_NAME}]</title></Helmet>
+
+      <Heading level="1">ZBH-Portal LogIn</Heading>
+      <FormProvider {...methods} >
+        <form onSubmit={methods.handleSubmit(handleSendForm)}>
+          <Input name='email' type='mail' title='E-Mail' className='h-16' autoFocus={true} />
+          <Input name='password' type='password' title='password' className='h-16' />
+          <Submit value='LogIn' />
+        </form>
+      </FormProvider>
+
+      <div className="mt-4">
+        <Link to="/forgot_password">Forgot your Password?</Link>
+      </div>
+    </>
+  );
+}
+
+export default Login;
\ No newline at end of file
diff --git a/src/routes/Sitemap.jsx b/src/routes/Sitemap.jsx
index 0635aed19632fa99fd80dbfc38a8118338c7ee0e..04299d2d3d544ed2607c3c185d3c5649ad0a7926 100644
--- a/src/routes/Sitemap.jsx
+++ b/src/routes/Sitemap.jsx
@@ -28,6 +28,8 @@ export const sitemap = [{
   ]
 }, {
   title: 'Others', element: <CleanLayout />, children: [
+    // USER
+    { path: '/login', element: loadComponent('User/Login') },
     // ERROR
     { path: '*', element: loadComponent('Err404') }
   ]
diff --git a/src/utils.js b/src/utils.js
new file mode 100755
index 0000000000000000000000000000000000000000..035e08650eaa3869c91db7cab99663ebfc7843b1
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,10 @@
+import { clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs) {
+  return twMerge(clsx(inputs));
+}
+
+export function capitalizeFirstLetter(string) {
+  return string.charAt(0).toUpperCase() + string.slice(1);
+}
\ No newline at end of file
diff --git a/src/utils/AxiosConfig.js b/src/utils/AxiosConfig.js
new file mode 100755
index 0000000000000000000000000000000000000000..98abfd7320fb09d0baac2b13ec007f009c05a268
--- /dev/null
+++ b/src/utils/AxiosConfig.js
@@ -0,0 +1,69 @@
+import axios from "axios";
+
+// ### CREATE BASE INSTANCE
+// defaults, used by all api requests
+const api = axios.create({
+  baseURL: `${import.meta.env.VITE_BACKEND_URL}:${import.meta.env.VITE_BACKEND_PORT}/${import.meta.env.VITE_BACKEND_PATH}`,
+});
+
+
+// ### REQUEST INTERCEPTOR 
+// injects accessToken if exists
+api.interceptors.request.use(
+  (config) => {
+    // fetch accessToken 
+    // TODO: don't store accessToken in localStorage, keep in memory only
+    const accessToken = JSON.parse(localStorage.getItem('accessToken')) || null;
+    // inject if exists
+    if (accessToken) {
+      config.headers["Authorization"] = 'Bearer ' + accessToken;
+    }
+    return config;
+  },
+  (error) => {
+    return Promise.reject(error);
+  }
+);
+
+
+// ### RESPONSE INTERCEPTOR 
+// refreshes accessToken if needed
+api.interceptors.response.use(
+  (res) => {
+    return res;
+  },
+  async (err) => {
+    // console.log(err);
+    // save original request config
+    const originalConfig = err.config;
+    // if access denied and not a retry already
+    // BUG: Infinit loop because _retry isn't set at runtime
+    // console.log(originalConfig);
+    // console.log(JSON.stringify(originalConfig));
+    if (originalConfig && err?.response?.status === 403 && originalConfig._retry !== true) {
+      // patch config to remember it's a retry
+      originalConfig._retry = true;
+      console.log('trying to refresh the accessToken and rerun the request');
+      // console.log('retry', err.code, originalConfig._retry);
+      // refresh access token
+      try {
+        const result = await api.post(
+          '/auth/token',
+          {},
+          { withCredentials: true }
+        );
+        // TODO: don't store accessToken in localStorage, keep in memory only
+        localStorage.setItem("accessToken", JSON.stringify(result.data.accessToken));
+        // run retry
+        return api(originalConfig);
+
+      } catch (error) {
+        return Promise.reject(error);
+      }
+    }
+    return Promise.reject(err);
+
+  }
+);
+
+export default api;
\ No newline at end of file
diff --git a/src/utils/ErrorHandling.js b/src/utils/ErrorHandling.js
new file mode 100644
index 0000000000000000000000000000000000000000..93eb81983d858eba25f685266d79772336986378
--- /dev/null
+++ b/src/utils/ErrorHandling.js
@@ -0,0 +1,29 @@
+import { toast } from 'react-toastify';
+
+// ### MERGE BACKEND ERRORS INTO FRONTEND
+export function mergeBackendValidation(status, errors, setError) {
+  // global error
+  if (errors.message) {
+    toast.error(errors.message, {
+      theme: "colored"
+    });
+    if (setError) {
+      setError('root', { type: status, message: errors.message });
+    }
+  }
+
+  // exit if no validation errors returned
+  if (!errors.validationErrors) return;
+  // set single field validation errors
+  const validationErrors = errors.validationErrors;
+  Object.keys(validationErrors).forEach(function (field) {
+    setError(field, { message: validationErrors[field] });
+  });
+
+  return;
+}
+
+// ### DISPLAY FLASH MESSAGES (mostly success from backend)
+export function setFlashMsg(message) {
+  toast.success(message);
+}
\ No newline at end of file
diff --git a/tailwind.config.js b/tailwind.config.js
index c9d44c71cf041856656d03b9a4180798010cb9a8..d8c39ef98878ad35fae52651d49ae6b2141989be 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,11 +1,38 @@
 /** @type {import('tailwindcss').Config} */
-export default {
+module.exports = {
+  darkMode: ["class"],
   content: [
     "./index.html",
-    "./src/**/*.{js,ts,jsx,tsx}",
+    "./src/**/*.{js,ts,jsx,tsx}"
   ],
   theme: {
+    container: {
+      center: true,
+      padding: "2rem",
+      screens: {
+        "2xl": "1400px",
+      },
+    },
     extend: {
+      borderRadius: {
+        lg: "var(--radius)",
+        md: "calc(var(--radius) - 2px)",
+        sm: "calc(var(--radius) - 4px)",
+      },
+      keyframes: {
+        "accordion-down": {
+          from: { height: 0 },
+          to: { height: "var(--radix-accordion-content-height)" },
+        },
+        "accordion-up": {
+          from: { height: "var(--radix-accordion-content-height)" },
+          to: { height: 0 },
+        },
+      },
+      animation: {
+        "accordion-down": "accordion-down 0.2s ease-out",
+        "accordion-up": "accordion-up 0.2s ease-out",
+      },
       colors: {
         UhhBlue: '#0271bb',
         UhhLightBlue: 'rgba(128, 184, 219, 0.5)',
@@ -61,6 +88,9 @@ export default {
         'UhhRC': ['TheSansUHHRegularCaps'],
         'UhhI': ['TheSansUHHItalic'],
       },
+      transitionProperty: {
+        'width': 'width'
+      },
       borderColor: {
         DEFAULT: '#3b515b'
       },
@@ -72,8 +102,7 @@ export default {
         'xl': '36rem',
         '2xl': '42rem',
       }
-    },
+    }
   },
   plugins: [],
-}
-
+};