diff --git a/task_3_react/.gitignore b/task_3_react/.gitignore
new file mode 100644
index 0000000..513496c
--- /dev/null
+++ b/task_3_react/.gitignore
@@ -0,0 +1,25 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+public
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/task_3_react/.gitlab-ci.yml b/task_3_react/.gitlab-ci.yml
new file mode 100644
index 0000000..db9fb6b
--- /dev/null
+++ b/task_3_react/.gitlab-ci.yml
@@ -0,0 +1,73 @@
+stages:
+ - lint
+ - build
+ - deploy
+
+workflow:
+ rules:
+ - if: $CI_COMMIT_BRANCH =~ /^(master|main)$/
+ - if: $FORCE_DEPLOY
+ when: always
+ - when: never
+
+variables:
+ IMAGE_NAME: $CI_REGISTRY/$CI_PROJECT_PATH
+ BASE_URI: "a3.webdev-25.ivia.isginf.ch"
+ FORCE_DEPLOY:
+ value: "false"
+ description: "Force deploy the application"
+ options:
+ - "true"
+ - "false"
+
+default:
+ before_script:
+ - IMAGE_NAME=$(echo $IMAGE_NAME | tr '[:upper:]' '[:lower:]')
+ - URI_NAME=$(echo $CI_PROJECT_NAME | tr '[:upper:]' '[:lower:]' | tr _ -)
+
+lint_helm:
+ stage: lint
+ image: matthiasgabathuler/my-runner:ubuntu-20.04
+ script:
+ - >-
+ helm lint ${CI_PROJECT_DIR}/helm
+ --set image.name=${IMAGE_NAME}
+ --set image.tag=${CI_COMMIT_REF_NAME}
+ --set build.job_id=${CI_JOB_ID}
+ --set build.commit=${CI_COMMIT_SHA}
+
+build_webapp:
+ stage: build
+ rules:
+ image:
+ name: gcr.io/kaniko-project/executor:debug
+ entrypoint: [""]
+ script:
+ - echo "${DOCKER_REGISTRY_AUTH}" > /kaniko/.docker/config.json
+ - /kaniko/executor
+ --context "${CI_PROJECT_DIR}"
+ --dockerfile "${CI_PROJECT_DIR}/Dockerfile"
+ --destination "${IMAGE_NAME}-webapp:latest"
+
+deploy_app:
+ stage: deploy
+ rules:
+ image:
+ name: alpine/helm:3.11.1
+ entrypoint: [""]
+ script:
+ - >-
+ helm --namespace $K8S_NAMESPACE
+ --kube-context $K8S_CONTEXT
+ upgrade a3-$(echo ${CI_PROJECT_NAME} | tr _ -) ${CI_PROJECT_DIR}/helm
+ --install
+ --history-max 5
+ --set image.host=${CI_REGISTRY}
+ --set image.name=${IMAGE_NAME}
+ --set image.tag=latest
+ --set url.hostname.uri=${URI_NAME}
+ --set url.hostname.base_uri=${BASE_URI}
+ --set build.job_id=${CI_JOB_ID}
+ --set build.commit=${CI_COMMIT_SHA}
+ - >-
+ echo "webapp URL: http://${URI_NAME}.${BASE_URI}"
\ No newline at end of file
diff --git a/task_3_react/Dockerfile b/task_3_react/Dockerfile
new file mode 100644
index 0000000..711677a
--- /dev/null
+++ b/task_3_react/Dockerfile
@@ -0,0 +1,8 @@
+FROM node
+WORKDIR /app
+COPY package.json .
+RUN npm i
+COPY . .
+RUN npm run build
+EXPOSE 5173
+CMD ["npm", "run", "start"]
diff --git a/task_3_react/README.md b/task_3_react/README.md
new file mode 100644
index 0000000..d590cad
--- /dev/null
+++ b/task_3_react/README.md
@@ -0,0 +1,29 @@
+# Local Development
+
+Only change files inside the `src` directory.
+
+## Client side
+
+All client side files are located in the `src/client` directory.
+
+## Server side
+
+All server side files are located in the `src/server` directory.
+
+# Local Testing
+
+## run container for local testing
+
+```bash
+docker build -t my-webapp .
+
+docker run -it --rm -p 5173:5173 my-webapp
+```
+Open a browser and connect to http://localhost:5173
+
+## run bash in interactive container
+```bash
+docker build -t my-webapp src/.
+
+docker run -it --rm -p 5173:5173 my-webapp bash
+```
\ No newline at end of file
diff --git a/task_3_react/helm/.helmignore b/task_3_react/helm/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/task_3_react/helm/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/task_3_react/helm/Chart.yaml b/task_3_react/helm/Chart.yaml
new file mode 100644
index 0000000..5af4328
--- /dev/null
+++ b/task_3_react/helm/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: a3-group-g3-rbacher-jahutz #name of the app, this should be the project name, spaces should be substituted with -
+description: My Example App #description for the app
+type: application
+version: 0.0.1 #for versioning of the helm chart, increase it when new release is built/deployed
+appVersion: "0.0.1"
\ No newline at end of file
diff --git a/task_3_react/helm/charts/.gitkeep b/task_3_react/helm/charts/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/task_3_react/helm/files/.gitkeep b/task_3_react/helm/files/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/task_3_react/helm/templates/.gitkeep b/task_3_react/helm/templates/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/task_3_react/helm/templates/deployment.yaml b/task_3_react/helm/templates/deployment.yaml
new file mode 100644
index 0000000..06838e0
--- /dev/null
+++ b/task_3_react/helm/templates/deployment.yaml
@@ -0,0 +1,56 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ .Release.Name }}-webapp
+spec:
+ selector:
+ matchLabels:
+ app: {{ .Release.Name }}-webapp
+ replicas: 1
+ template:
+ metadata:
+ labels:
+ app: {{ .Release.Name }}-webapp
+ annotations:
+ rollme: {{ randAlphaNum 5 | quote }}
+ spec:
+ imagePullSecrets:
+ - name: regcred
+ containers:
+ - name: {{ .Release.Name }}-webapp
+ image: {{ .Values.image.name }}-webapp:{{ .Values.image.tag }}
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 5173
+ resources:
+ limits:
+ memory: "250M"
+ requests:
+ memory: "100M"
+ commnand: ["npm", "run", "dev"]
+ volumeMounts:
+ - name: config-volume
+ mountPath: /app/vite.config.js
+ subPath: vite.config.js
+ volumes:
+ - name: config-volume
+ configMap:
+ name: {{ .Release.Name }}-webapp-config
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ .Release.Name }}-webapp-config
+data:
+ vite.config.js: |
+ export default {
+ server: {
+ host: '0.0.0.0',
+ port: 5173,
+ allowedHosts: true,
+ watch: {
+ usePolling: true,
+ interval: 1000,
+ },
+ },
+ }
\ No newline at end of file
diff --git a/task_3_react/helm/templates/ingress.yaml b/task_3_react/helm/templates/ingress.yaml
new file mode 100644
index 0000000..c5e2480
--- /dev/null
+++ b/task_3_react/helm/templates/ingress.yaml
@@ -0,0 +1,24 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: {{ .Release.Name }}-ingress
+ annotations:
+ nginx.ingress.kubernetes.io/enable-cors: "true"
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+spec:
+ ingressClassName: nginx
+ tls:
+ - hosts:
+ - {{ .Values.url.hostname.uri }}.{{ .Values.url.hostname.base_uri }}
+ secretName: {{ .Release.Name }}-tls
+ rules:
+ - host: {{ .Values.url.hostname.uri }}.{{ .Values.url.hostname.base_uri }}
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: {{ .Release.Name }}-webapp
+ port:
+ number: 80
\ No newline at end of file
diff --git a/task_3_react/helm/templates/service.yaml b/task_3_react/helm/templates/service.yaml
new file mode 100644
index 0000000..10e8164
--- /dev/null
+++ b/task_3_react/helm/templates/service.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ .Release.Name }}-webapp
+spec:
+ ports:
+ - port: 80
+ targetPort: 5173
+ selector:
+ app: {{ .Release.Name }}-webapp
diff --git a/task_3_react/helm/values.yaml b/task_3_react/helm/values.yaml
new file mode 100644
index 0000000..d752e2b
--- /dev/null
+++ b/task_3_react/helm/values.yaml
@@ -0,0 +1,10 @@
+image:
+ name: ""
+ tag: ""
+build:
+ job_id: ""
+ commit: ""
+url:
+ hostname:
+ base_uri: "webdev-25.ivia.isginf.ch"
+ uri: ""
\ No newline at end of file
diff --git a/task_3_react/index.html b/task_3_react/index.html
new file mode 100644
index 0000000..fe60a05
--- /dev/null
+++ b/task_3_react/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Open data explorer
+
+
+
+
+
+
diff --git a/task_3_react/package.json b/task_3_react/package.json
new file mode 100644
index 0000000..952e573
--- /dev/null
+++ b/task_3_react/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "vite-react-typescript-starter",
+ "private": true,
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "nodemon src/server/main.ts -w src/server",
+ "build": "vite build",
+ "start": "NODE_ENV=production ts-node --transpile-only src/server/main.ts",
+ "format": "prettier . --write",
+ "tsc": "tsc"
+ },
+ "dependencies": {
+ "@fortawesome/fontawesome-free": "^6.6.0",
+ "@picocss/pico": "^2.0.6",
+ "express": "^4.21.1",
+ "json-2-csv": "^5.5.6",
+ "multer": "^1.4.5-lts.1",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "ts-node": "^10.9.2",
+ "typescript": "^5.6.3",
+ "vite-express": "*"
+ },
+ "devDependencies": {
+ "@types/express": "^5.0.0",
+ "@types/multer": "^1.4.12",
+ "@types/node": "^22.7.6",
+ "@types/react": "^18.3.11",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.2",
+ "nodemon": "^3.1.7",
+ "prettier": "^3.3.3",
+ "vite": "^5.4.9"
+ }
+}
diff --git a/task_3_react/src/client/App.css b/task_3_react/src/client/App.css
new file mode 100644
index 0000000..09d81a6
--- /dev/null
+++ b/task_3_react/src/client/App.css
@@ -0,0 +1,138 @@
+/*some style for app component*/
+:root {
+ --spacing: 0.25rem;
+ --border-color: #a0a0a0;
+}
+
+/* Style for definition list */
+dl {
+ margin-top: 0;
+ margin-bottom: 20px;
+}
+
+dt,
+dd {
+ line-height: 1.42857143;
+}
+
+dt {
+ font-weight: 700;
+}
+
+dd {
+ margin-left: 0;
+}
+
+body h1,
+body h2 {
+ margin-bottom: 0;
+}
+
+nav {
+ border-bottom: 1px solid var(--border-color);
+}
+
+article {
+ width: 400px;
+}
+
+body>main {
+ max-height: calc(100vh - 100px);
+ overflow-y: auto;
+
+ padding: var(--spacing) !important;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: start;
+ gap: var(--spacing);
+
+ & article {
+ margin: 0;
+ padding: var(--spacing);
+
+ &>header {
+ margin: calc(var(--spacing) * -1) calc(var(--spacing) * -1) var(--spacing);
+ padding: var(--spacing);
+ }
+ }
+}
+
+.info {
+ h4 {
+ margin-bottom: 2px;
+ margin-top: 10px;
+ }
+
+ p {
+ margin: 0;
+ }
+}
+
+.table-container {
+ width: 100%;
+}
+
+.table-scroll-wrapper {
+ width: 100%;
+ max-height: 70vh;
+ overflow: scroll;
+}
+
+/* Set a fixed scrollable wrapper */
+#table-content {
+ max-height: 400px;
+ overflow: auto;
+
+ /* Set header to stick to the top of the container. */
+ & thead tr th {
+ position: sticky;
+ top: 0;
+ }
+
+ /* If we use border, we must use table-collapse to avoid a slight movement of the header row */
+ & table {
+ border-collapse: collapse;
+ border-top: 0;
+ }
+
+ /* Because we must set sticky on th, we have to apply background styles here rather than on thead */
+ & th {
+ cursor: pointer;
+ border-left: 1px dotted rgba(200, 209, 224, 0.6);
+ border-bottom: 1px solid #ddd;
+
+ background: #eee;
+ text-align: left;
+ box-shadow: 0px 0px 0 2px #e8e8e8;
+
+ &.active {
+ background: #ddd;
+ }
+
+ &.sortable::after {
+ font-family: FontAwesome;
+ content: "\f0dc";
+ position: absolute;
+ right: 8px;
+ color: #999;
+ }
+
+ &.active::after {
+ position: absolute;
+ right: 8px;
+ color: #999;
+ }
+
+ &.active.sorting.asc::after {
+ font-family: FontAwesome;
+ content: "\f0d8";
+ }
+
+ &.active.sorting.desc::after {
+ font-family: FontAwesome;
+ content: "\f0d7";
+ }
+ }
+}
\ No newline at end of file
diff --git a/task_3_react/src/client/App.tsx b/task_3_react/src/client/App.tsx
new file mode 100644
index 0000000..07b5854
--- /dev/null
+++ b/task_3_react/src/client/App.tsx
@@ -0,0 +1,47 @@
+import "./App.css";
+import '@fortawesome/fontawesome-free/css/all.css';
+import { readCSV } from './csv';
+import { CSV_Data, fileInfo } from './types';
+
+import React, { useState } from "react";
+import Layout from "./Layout";
+import CSVCard from "./components/CSVCard";
+import InfoCard from "./components/InfoCard";
+import DataTable from "./components/DataTable";
+
+function App() {
+ const [data, setData] = useState([] as CSV_Data);
+ const [info, setInfo] = useState({
+ filename: "None",
+ filetype: "None",
+ filesize: "None",
+ rowcount: 0
+ });
+
+ // This is triggered in CSVCard
+ const handleFileChange = async (e: React.ChangeEvent): Promise => {
+ const file = e.target.files?.[0];
+ if (!file) throw new Error("No file received");
+
+ const data = await readCSV(e);
+
+ const newFileInfo: fileInfo = {
+ filename: file.name,
+ filetype: file.type,
+ filesize: String(file.size) + "B",
+ rowcount: data.length
+ }
+ setInfo(newFileInfo);
+ setData(data);
+ }
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/task_3_react/src/client/Layout.css b/task_3_react/src/client/Layout.css
new file mode 100644
index 0000000..43f0010
--- /dev/null
+++ b/task_3_react/src/client/Layout.css
@@ -0,0 +1,29 @@
+body h1,
+body h2 {
+ margin-bottom: 0;
+}
+
+nav {
+ border-bottom: 1px solid var(--border-color);
+}
+
+body main {
+ max-height: calc(100vh - 100px);
+ overflow-y: auto;
+
+ padding: var(--spacing) !important;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: start;
+ gap: var(--spacing);
+
+ & article {
+ margin: 0;
+ padding: var(--spacing);
+ & > header {
+ margin: calc(var(--spacing) * -1) calc(var(--spacing) * -1) var(--spacing);
+ padding: var(--spacing);
+ }
+ }
+}
diff --git a/task_3_react/src/client/Layout.tsx b/task_3_react/src/client/Layout.tsx
new file mode 100644
index 0000000..55dfead
--- /dev/null
+++ b/task_3_react/src/client/Layout.tsx
@@ -0,0 +1,21 @@
+import React from "react";
+import "./Layout.css";
+
+const Layout = (props: { children: React.ReactNode }) => {
+ return (
+ <>
+
+
+ {props.children}
+
+ >
+ );
+};
+
+export default Layout;
diff --git a/task_3_react/src/client/components/CSVCard.tsx b/task_3_react/src/client/components/CSVCard.tsx
new file mode 100644
index 0000000..53d0788
--- /dev/null
+++ b/task_3_react/src/client/components/CSVCard.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+import "../Layout.css";
+
+const CSVCard = (props: {
+ handleChange: (e: React.ChangeEvent) => void
+}) => {
+ return (
+
+
+
+
+ );
+}
+
+export default CSVCard;
\ No newline at end of file
diff --git a/task_3_react/src/client/components/DataTable.tsx b/task_3_react/src/client/components/DataTable.tsx
new file mode 100644
index 0000000..70f09c0
--- /dev/null
+++ b/task_3_react/src/client/components/DataTable.tsx
@@ -0,0 +1,84 @@
+import { Key, SetStateAction, useState } from "react";
+import { CSV_Data } from "../types";
+
+const DataTable = (props: {data: CSV_Data}) => {
+ if (props.data.length == 0) return <>>;
+
+ const header = Object.keys(props.data[0]!);
+ const [sortCol, setSortCol] = useState("None");
+ const [sortType, setSortType] = useState("asc");
+
+ const sortingHandler = (col: String) => {
+ if (sortCol !== col) {
+ setSortCol(col as SetStateAction);
+ setSortType("asc");
+ } else if (sortType === "asc") {
+ setSortType("desc");
+ } else {
+ setSortCol("None");
+ setSortType("None");
+ }
+ }
+
+ if (sortCol !== "None" && sortType === "asc") {
+ props.data.sort( (a, b) => {
+ if (a[sortCol]! < b[sortCol]!) return -1;
+ if (a[sortCol]! > b[sortCol]!) return 1;
+ return 0;
+ });
+ } else if (sortCol !== "None" && sortType === "desc") {
+ props.data.sort( (a, b) => {
+ if (a[sortCol]! > b[sortCol]!) return -1;
+ if (a[sortCol]! < b[sortCol]!) return 1;
+ return 0;
+ });
+ }
+
+ return (
+
+
+
+ {
+ header.map( (col) => (
+
+ ))
+ }
+
+
+
+ {
+ props.data.map( (row, i) => (
+
+ {
+ header.map( (col) => (
+
+ ))
+ }
+
+ ))
+ }
+
+
+ )
+}
+
+const ColHeader = (props: {col: String, sortingHandle: (s: String) => void, isSelected: boolean, sortType: String}) => {
+ return (
+ {props.sortingHandle!(props.col)}}
+ key={props.col as Key}>
+ {props.col}
+ |
+ );
+}
+
+const Row = (props: {col: String, content: String}) => {
+ return {props.content} | ;
+}
+
+export default DataTable;
\ No newline at end of file
diff --git a/task_3_react/src/client/components/InfoCard.tsx b/task_3_react/src/client/components/InfoCard.tsx
new file mode 100644
index 0000000..684086f
--- /dev/null
+++ b/task_3_react/src/client/components/InfoCard.tsx
@@ -0,0 +1,35 @@
+import "../Layout.css";
+import { fileInfo } from "../types";
+
+const InfoCard = (props: {
+ info: fileInfo
+}) => {
+
+ let noFileMessage =
+ if (props.info.filename === "None")
+ noFileMessage = No file selected
;
+
+ return (
+
+
+
+
Filename
+
{props.info.filename}
+
+
File type
+
{props.info.filetype}
+
+
File size
+
{props.info.filesize}
+
+
Number of rows
+
{props.info.rowcount}
+
+ {noFileMessage}
+
+ );
+}
+
+export default InfoCard;
\ No newline at end of file
diff --git a/task_3_react/src/client/csv.ts b/task_3_react/src/client/csv.ts
new file mode 100644
index 0000000..bc29697
--- /dev/null
+++ b/task_3_react/src/client/csv.ts
@@ -0,0 +1,30 @@
+import { CSV_Data } from './types';
+import { csv2json } from 'json-2-csv';
+
+const convertCSVtoJSON = async ( csvText: string ) => {
+ // Type cast OK, as the typing of the external library is not perfect -> Actually it is.
+ // NOTE: On transpilation to JS, it will be (more or less) disregarded anyway.
+ // If you claim it isn't good typing, it's the same as expecting it to guess the typing,
+ // which is literally impossible in TS. But sure...
+ return ( await csv2json( csvText ) ) as CSV_Data;
+};
+
+/**
+ * Reads a CSV file and returns the data as JSON.
+ * @param event The change event of the file input.
+ */
+export const readCSV = async ( event: React.ChangeEvent ): Promise => {
+ if ( !( event.target instanceof HTMLInputElement ) ) {
+ throw new Error( 'Not an HTMLInputElement' );
+ }
+
+ const file = event.target.files?.[0];
+
+ if ( file == null ) {
+ throw new Error( 'No file selected' );
+ }
+
+ const result = await file.text();
+
+ return await convertCSVtoJSON( result );
+};
diff --git a/task_3_react/src/client/index.css b/task_3_react/src/client/index.css
new file mode 100644
index 0000000..1818ca7
--- /dev/null
+++ b/task_3_react/src/client/index.css
@@ -0,0 +1,20 @@
+:root {
+ --spacing: 1rem;
+ --border-color: #a0a0a0;
+}
+
+/* Style for definition list */
+dl {
+ margin-top: 0;
+ margin-bottom: 20px;
+}
+dt,
+dd {
+ line-height: 1.42857143;
+}
+dt {
+ font-weight: 700;
+}
+dd {
+ margin-left: 0;
+}
diff --git a/task_3_react/src/client/main.tsx b/task_3_react/src/client/main.tsx
new file mode 100644
index 0000000..4a35729
--- /dev/null
+++ b/task_3_react/src/client/main.tsx
@@ -0,0 +1,13 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import "@fortawesome/fontawesome-free/css/all.css";
+import App from "./App";
+import "@picocss/pico/css/pico.min.css";
+
+import "./index.css";
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/task_3_react/src/client/types.d.ts b/task_3_react/src/client/types.d.ts
new file mode 100644
index 0000000..e949b5d
--- /dev/null
+++ b/task_3_react/src/client/types.d.ts
@@ -0,0 +1,11 @@
+export type CSVRecord = Record;
+
+export type CSV_Data = CSVRecord[];
+
+// InfoCard receives this via props
+export type fileInfo = {
+ filename: string;
+ filetype: string;
+ filesize: string;
+ rowcount: number;
+}
diff --git a/task_3_react/src/client/vite-env.d.ts b/task_3_react/src/client/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/task_3_react/src/client/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/task_3_react/src/server/main.ts b/task_3_react/src/server/main.ts
new file mode 100644
index 0000000..05a5ded
--- /dev/null
+++ b/task_3_react/src/server/main.ts
@@ -0,0 +1,17 @@
+import express from "express";
+import ViteExpress from "vite-express";
+
+// creates the expres app do not change
+const app = express();
+
+// add your routes here
+
+// example route which returns a message
+app.get("/hello", async function (_req, res) {
+ res.status(200).json({ message: "Hello World!" });
+});
+
+// Do not change below this line
+ViteExpress.listen(app, 5173, () =>
+ console.log("Server is listening on http://localhost:5173"),
+);
diff --git a/task_3_react/tsconfig.json b/task_3_react/tsconfig.json
new file mode 100644
index 0000000..fc3018c
--- /dev/null
+++ b/task_3_react/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "CommonJS",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ "strict": true,
+ "allowUnusedLabels": false,
+ "allowUnreachableCode": false,
+ "exactOptionalPropertyTypes": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitOverride": true,
+ "noImplicitReturns": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noUncheckedIndexedAccess": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "allowJs": false,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src"],
+ "exclude": ["tests/**/*", "public/**/*", "build/**/*"]
+}
diff --git a/task_3_react/vite.config.ts b/task_3_react/vite.config.ts
new file mode 100644
index 0000000..9cc50ea
--- /dev/null
+++ b/task_3_react/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+});