diff --git a/DevSpaceOApi.json b/DevSpaceOApi.json
new file mode 100644
index 0000000..251138e
--- /dev/null
+++ b/DevSpaceOApi.json
@@ -0,0 +1,879 @@
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "DevSpace API",
+ "description": "API for DevSpace",
+ "version": "0.1.0"
+ },
+ "components": {
+ "securitySchemes": {
+ "bearerAuth": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT"
+ }
+ },
+ "schemas": {
+ "Post": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "description": "Post ID"
+ },
+ "title": {
+ "type": "string",
+ "description": "Post title"
+ },
+ "content": {
+ "type": "string",
+ "description": "Post content"
+ },
+ "createdAt": {
+ "type": "string",
+ "description": "Post creation date"
+ },
+ "createdBy": {
+ "$ref": "#/components/schemas/User"
+ },
+ "likedBy": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/User"
+ }
+ },
+ "comments": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Comment"
+ }
+ }
+ }
+ },
+ "Comment": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "description": "Comment ID"
+ },
+ "content": {
+ "type": "string",
+ "description": "Comment content"
+ },
+ "createdAt": {
+ "type": "string",
+ "description": "Comment creation date"
+ },
+ "createdBy": {
+ "$ref": "#/components/schemas/User"
+ },
+ "likedBy": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ },
+ "Notification": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "description": "Notification ID"
+ },
+ "message": {
+ "type": "string",
+ "description": "Notification message"
+ },
+ "timeStamp": {
+ "type": "string",
+ "description": "Notification creation date"
+ },
+ "seen": {
+ "type": "boolean",
+ "description": "Notification seen status"
+ }
+ }
+ },
+ "User": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "description": "User ID"
+ },
+ "firstName": {
+ "type": "string",
+ "description": "User first name"
+ },
+ "lastName": {
+ "type": "string",
+ "description": "User last name"
+ }
+ }
+ },
+ "UserWithRelations": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/User"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "posts": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Post"
+ }
+ },
+ "comments": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Comment"
+ }
+ },
+ "followed": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/User"
+ }
+ },
+ "followers": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "UserWithRelationsAndNotifications": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/UserWithRelations"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "notifications": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Notification"
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
+ "paths": {
+ "/auth/signup": {
+ "post": {
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Sign up a new user",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string",
+ "description": "The email of the user"
+ },
+ "password": {
+ "type": "string",
+ "description": "The password of the user"
+ },
+ "passwordValidation": {
+ "type": "string",
+ "description": "Password validation field"
+ },
+ "firstName": {
+ "type": "string",
+ "description": "The first name of the user"
+ },
+ "lastName": {
+ "type": "string",
+ "description": "The last name of the user"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successfully signed up the user",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid input or user already exists"
+ }
+ }
+ }
+ },
+ "/auth/login": {
+ "post": {
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Log in a user",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string",
+ "description": "The email of the user"
+ },
+ "password": {
+ "type": "string",
+ "description": "The password of the user"
+ },
+ "longExpiration": {
+ "type": "boolean",
+ "description": "Whether to keep the user logged in for a long time"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successfully logged in the user",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "example": "success"
+ },
+ "token": {
+ "type": "string",
+ "description": "The JWT token for the user"
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Missing email or password"
+ },
+ "401": {
+ "description": "Incorrect email or password"
+ }
+ }
+ }
+ },
+ "/auth/logout": {
+ "get": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Authentication"
+ ],
+ "summary": "Log out the user",
+ "responses": {
+ "200": {
+ "description": "Successfully logged out the user",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "example": "success"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/posts": {
+ "get": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Posts"
+ ],
+ "operationId": "getAllPosts",
+ "summary": "Get all posts",
+ "responses": {
+ "200": {
+ "description": "A list of all posts",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Post"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Posts"
+ ],
+ "operationId": "createPost",
+ "summary": "Create a post",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "Post title"
+ },
+ "content": {
+ "type": "string",
+ "description": "Post content"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "A post",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Post"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Title and content are required"
+ }
+ }
+ }
+ },
+ "/posts/{id}": {
+ "get": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Posts"
+ ],
+ "operationId": "getPost",
+ "summary": "Get a post by ID",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "description": "ID of the post",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A post",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Post"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid ID"
+ },
+ "404": {
+ "description": "No post found with that ID"
+ }
+ }
+ },
+ "patch": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Posts"
+ ],
+ "operationId": "updatePost",
+ "summary": "Update a post by ID",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "description": "ID of the post",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "Post title"
+ },
+ "content": {
+ "type": "string",
+ "description": "Post content"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "A post",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Post"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid ID or Title and content are required"
+ },
+ "404": {
+ "description": "No post found with that ID"
+ }
+ }
+ },
+ "delete": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Posts"
+ ],
+ "operationId": "deletePost",
+ "summary": "Delete a post by ID",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "description": "ID of the post",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No content"
+ },
+ "400": {
+ "description": "Invalid ID"
+ },
+ "404": {
+ "description": "No post found with that ID"
+ }
+ }
+ }
+ },
+ "/posts/followed": {
+ "get": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Posts"
+ ],
+ "operationId": "getFollowedPosts",
+ "summary": "Get posts of followed users",
+ "responses": {
+ "200": {
+ "description": "A list of posts",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Post"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/posts/{id}/like": {
+ "post": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Posts"
+ ],
+ "operationId": "likePost",
+ "summary": "Like a post by ID",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "description": "ID of the post",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A post",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Post"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid ID or You have already liked this post"
+ },
+ "404": {
+ "description": "No post found with that ID"
+ }
+ }
+ },
+ "delete": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Posts"
+ ],
+ "operationId": "unlikePost",
+ "summary": "Unlike a post by ID",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "description": "ID of the post",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A post",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Post"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid ID or You have not liked this post"
+ },
+ "404": {
+ "description": "No post found with that ID"
+ }
+ }
+ }
+ },
+ "/posts/{id}/comment": {
+ "post": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Posts"
+ ],
+ "operationId": "commentPost",
+ "summary": "Comment on a post by ID",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "description": "ID of the post",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "string",
+ "description": "Comment content"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "A comment",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Comment"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid Request"
+ },
+ "404": {
+ "description": "No post found with that ID"
+ }
+ }
+ }
+ },
+ "/users": {
+ "get": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Users"
+ ],
+ "summary": "Get all users",
+ "responses": {
+ "200": {
+ "description": "A list of all users",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/users/{id}": {
+ "get": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Users"
+ ],
+ "summary": "Get a single user",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "description": "ID of the user",
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "A single user",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserWithRelations"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/users/me": {
+ "get": {
+ "tags": [
+ "Users"
+ ],
+ "summary": "Get the currently logged in user",
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "The currently logged in user",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserWithRelationsAndNotifications"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/users/{id}/follow": {
+ "post": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Users"
+ ],
+ "summary": "Follow a user",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "description": "The ID of the user to follow"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successfully followed the user",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserWithRelations"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid ID"
+ },
+ "404": {
+ "description": "No user found with that ID"
+ }
+ }
+ },
+ "delete": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "tags": [
+ "Users"
+ ],
+ "summary": "Unfollow a user",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "description": "The ID of the user to unfollow"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successfully unfollowed the user",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserWithRelations"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid ID"
+ },
+ "404": {
+ "description": "No user found with that ID"
+ }
+ }
+ }
+ }
+ },
+ "tags": []
+}
\ No newline at end of file
diff --git a/client/openapitools.json b/client/openapitools.json
new file mode 100644
index 0000000..9841a49
--- /dev/null
+++ b/client/openapitools.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
+ "spaces": 2,
+ "generator-cli": {
+ "version": "7.3.0"
+ }
+}
diff --git a/client/package-lock.json b/client/package-lock.json
index c7ebb34..4e10a9b 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -8,18 +8,42 @@
"name": "client",
"version": "0.1.0",
"dependencies": {
+ "@emotion/react": "^11.11.3",
+ "@emotion/styled": "^11.11.0",
+ "@fontsource/roboto": "^5.0.8",
+ "@mui/icons-material": "^5.15.8",
+ "@mui/material": "^5.15.7",
+ "@reduxjs/toolkit": "^2.1.0",
+ "axios": "^1.6.7",
+ "match-sorter": "^6.3.3",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-redux": "^9.1.0",
+ "react-router-dom": "^6.22.0",
+ "react-scripts": "5.0.1",
+ "sort-by": "^1.2.0",
+ "typescript": "^4.9.5",
+ "web-vitals": "^2.1.4"
+ },
+ "devDependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "@types/axios": "^0.14.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.77",
"@types/react": "^18.2.48",
- "@types/react-dom": "^18.2.18",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "react-scripts": "5.0.1",
- "typescript": "^4.9.5",
- "web-vitals": "^2.1.4"
+ "@types/react-dom": "^18.2.18"
+ }
+ },
+ ".api/apis/devspace": {
+ "name": "@api/devspace",
+ "version": "0.1.0",
+ "extraneous": true,
+ "dependencies": {
+ "api": "^6.1.1",
+ "json-schema-to-ts": "^2.8.0-beta.0",
+ "oas": "^20.10.3"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -33,7 +57,8 @@
"node_modules/@adobe/css-tools": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz",
- "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ=="
+ "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==",
+ "dev": true
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
@@ -2288,6 +2313,163 @@
"postcss-selector-parser": "^6.0.10"
}
},
+ "node_modules/@emotion/babel-plugin": {
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
+ "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.1",
+ "@emotion/memoize": "^0.8.1",
+ "@emotion/serialize": "^1.1.2",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@emotion/cache": {
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
+ "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1",
+ "@emotion/sheet": "^1.2.2",
+ "@emotion/utils": "^1.2.1",
+ "@emotion/weak-memoize": "^0.3.1",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
+ "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
+ },
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz",
+ "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
+ },
+ "node_modules/@emotion/react": {
+ "version": "11.11.3",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz",
+ "integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.11.0",
+ "@emotion/cache": "^11.11.0",
+ "@emotion/serialize": "^1.1.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
+ "@emotion/utils": "^1.2.1",
+ "@emotion/weak-memoize": "^0.3.1",
+ "hoist-non-react-statics": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/serialize": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz",
+ "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==",
+ "dependencies": {
+ "@emotion/hash": "^0.9.1",
+ "@emotion/memoize": "^0.8.1",
+ "@emotion/unitless": "^0.8.1",
+ "@emotion/utils": "^1.2.1",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@emotion/sheet": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz",
+ "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA=="
+ },
+ "node_modules/@emotion/styled": {
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz",
+ "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.11.0",
+ "@emotion/is-prop-valid": "^1.2.1",
+ "@emotion/serialize": "^1.1.2",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
+ "@emotion/utils": "^1.2.1"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0-rc.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+ "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
+ },
+ "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
+ "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@emotion/utils": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz",
+ "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg=="
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
+ "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -2381,6 +2563,45 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz",
+ "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.1"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz",
+ "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==",
+ "dependencies": {
+ "@floating-ui/core": "^1.6.0",
+ "@floating-ui/utils": "^0.2.1"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz",
+ "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==",
+ "dependencies": {
+ "@floating-ui/dom": "^1.6.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
+ "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
+ },
+ "node_modules/@fontsource/roboto": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.8.tgz",
+ "integrity": "sha512-XxPltXs5R31D6UZeLIV1td3wTXU3jzd3f2DLsXI8tytMGBkIsGcc9sIyiupRtA8y73HAhuSCeweOoBqf6DbWCA=="
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -3220,6 +3441,261 @@
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
},
+ "node_modules/@mui/base": {
+ "version": "5.0.0-beta.34",
+ "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.34.tgz",
+ "integrity": "sha512-e2mbTGTtReD/y5RFwnhkl1Tgl3XwgJhY040IlfkTVaU9f5LWrVhEnpRsYXu3B1CtLrwiWs4cu7aMHV9yRd4jpw==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.9",
+ "@floating-ui/react-dom": "^2.0.8",
+ "@mui/types": "^7.2.13",
+ "@mui/utils": "^5.15.7",
+ "@popperjs/core": "^2.11.8",
+ "clsx": "^2.1.0",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/core-downloads-tracker": {
+ "version": "5.15.7",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.7.tgz",
+ "integrity": "sha512-AuF+Wo2Mp/edaO6vJnWjg+gj4tzEz5ChMZnAQpc22DXpSvM8ddgGcZvM7D7F99pIBoSv8ub+Iz0viL+yuGVmhg==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ }
+ },
+ "node_modules/@mui/icons-material": {
+ "version": "5.15.8",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.8.tgz",
+ "integrity": "sha512-3Ikivf+BOJ7jT1/71HrbKeicgF9ENM4qo+J1050HMJLtLiJEVXbicnsg2oWJZL+0AsrOMaKnTmx1URBpkctLWg==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.9"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@mui/material": "^5.0.0",
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/material": {
+ "version": "5.15.7",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.7.tgz",
+ "integrity": "sha512-l6+AiKZH3iOJmZCnlpel8ghYQe9Lq0BEuKP8fGj3g5xz4arO9GydqYAtLPMvuHKtArj8lJGNuT2yHYxmejincA==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.9",
+ "@mui/base": "5.0.0-beta.34",
+ "@mui/core-downloads-tracker": "^5.15.7",
+ "@mui/system": "^5.15.7",
+ "@mui/types": "^7.2.13",
+ "@mui/utils": "^5.15.7",
+ "@types/react-transition-group": "^4.4.10",
+ "clsx": "^2.1.0",
+ "csstype": "^3.1.2",
+ "prop-types": "^15.8.1",
+ "react-is": "^18.2.0",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/material/node_modules/react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ },
+ "node_modules/@mui/private-theming": {
+ "version": "5.15.7",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.7.tgz",
+ "integrity": "sha512-bcEeeXm7GyQCQvN9dwo8htGv8/6tP05p0i02Z7GXm5EoDPlBcqTNGugsjNLoGq6B0SsdyanjJGw0Jw00o1yAOA==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.9",
+ "@mui/utils": "^5.15.7",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/styled-engine": {
+ "version": "5.15.7",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.7.tgz",
+ "integrity": "sha512-ixSdslOjK1kzdGcxqj7O3d14By/LPQ7EWknsViQ8RaeT863EAQemS+zvUJDTcOpkfJh6q6gPnYMIb2TJCs9eWA==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.9",
+ "@emotion/cache": "^11.11.0",
+ "csstype": "^3.1.2",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.4.1",
+ "@emotion/styled": "^11.3.0",
+ "react": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/system": {
+ "version": "5.15.7",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.7.tgz",
+ "integrity": "sha512-9alZ4/dLxsTwUOdqakgzxiL5YW6ntqj0CfzWImgWnBMTZhgGcPsbYpBLniNkkk7/jptma4/bykWXHwju/ls/pg==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.9",
+ "@mui/private-theming": "^5.15.7",
+ "@mui/styled-engine": "^5.15.7",
+ "@mui/types": "^7.2.13",
+ "@mui/utils": "^5.15.7",
+ "clsx": "^2.1.0",
+ "csstype": "^3.1.2",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/types": {
+ "version": "7.2.13",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz",
+ "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==",
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/utils": {
+ "version": "5.15.7",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.7.tgz",
+ "integrity": "sha512-8qhsxQRNV6aEOjjSk6YQIYJxkF5klhj8oG1FEEU4z6HV78TjNqRxMP08QGcdsibEbez+nihAaz6vu83b4XqbAg==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.9",
+ "@types/prop-types": "^15.7.11",
+ "prop-types": "^15.8.1",
+ "react-is": "^18.2.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/utils/node_modules/react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
+ },
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -3338,6 +3814,55 @@
}
}
},
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.1.0.tgz",
+ "integrity": "sha512-nfJ/b4ZhzUevQ1ZPKjlDL6CMYxO4o7ZL7OSsvSOxzT/EN11LsBDgTqP7aedHtBrFSVoK7oTP1SbMWUwGb30NLg==",
+ "dependencies": {
+ "immer": "^10.0.3",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.0.1"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz",
+ "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.0.tgz",
+ "integrity": "sha512-HOil5aFtme37dVQTB6M34G95kPM3MMuqSmIRVCC52eKV+Y/tGSqw9P3rWhlAx6A+mz+MoX+XxsGsNJbaI5qCgQ==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -3660,6 +4185,7 @@
"version": "9.3.4",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
"integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==",
+ "dev": true,
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
@@ -3679,6 +4205,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
"peer": true,
"dependencies": {
"color-convert": "^2.0.1"
@@ -3694,6 +4221,7 @@
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
"integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==",
+ "dev": true,
"peer": true,
"dependencies": {
"deep-equal": "^2.0.5"
@@ -3703,6 +4231,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
"peer": true,
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -3719,6 +4248,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
"peer": true,
"dependencies": {
"color-name": "~1.1.4"
@@ -3731,12 +4261,14 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
"peer": true
},
"node_modules/@testing-library/dom/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
"peer": true,
"engines": {
"node": ">=8"
@@ -3746,6 +4278,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
"peer": true,
"dependencies": {
"has-flag": "^4.0.0"
@@ -3758,6 +4291,7 @@
"version": "5.17.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
"integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==",
+ "dev": true,
"dependencies": {
"@adobe/css-tools": "^4.0.1",
"@babel/runtime": "^7.9.2",
@@ -3779,6 +4313,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -3793,6 +4328,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -3805,6 +4341,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -3815,12 +4352,14 @@
"node_modules/@testing-library/jest-dom/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
},
"node_modules/@testing-library/jest-dom/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -3829,6 +4368,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -3840,6 +4380,7 @@
"version": "13.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz",
"integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==",
+ "dev": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@testing-library/dom": "^8.5.0",
@@ -3857,6 +4398,7 @@
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz",
"integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==",
+ "dev": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -3875,6 +4417,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -3889,6 +4432,7 @@
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
"integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==",
+ "dev": true,
"dependencies": {
"deep-equal": "^2.0.5"
}
@@ -3897,6 +4441,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -3912,6 +4457,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -3922,12 +4468,14 @@
"node_modules/@testing-library/react/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
},
"node_modules/@testing-library/react/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -3936,6 +4484,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -3947,6 +4496,7 @@
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz",
"integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==",
+ "dev": true,
"dependencies": {
"@babel/runtime": "^7.12.5"
},
@@ -3977,7 +4527,18 @@
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
- "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true
+ },
+ "node_modules/@types/axios": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
+ "integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==",
+ "deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!",
+ "dev": true,
+ "dependencies": {
+ "axios": "*"
+ }
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -4146,6 +4707,7 @@
"version": "27.5.2",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.2.tgz",
"integrity": "sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==",
+ "dev": true,
"dependencies": {
"jest-matcher-utils": "^27.0.0",
"pretty-format": "^27.0.0"
@@ -4223,6 +4785,15 @@
"version": "18.2.18",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz",
"integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-transition-group": {
+ "version": "4.4.10",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
+ "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
"dependencies": {
"@types/react": "*"
}
@@ -4294,6 +4865,7 @@
"version": "5.14.9",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz",
"integrity": "sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==",
+ "dev": true,
"dependencies": {
"@types/jest": "*"
}
@@ -4303,6 +4875,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
+ },
"node_modules/@types/ws": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
@@ -5169,6 +5746,29 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
+ "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
+ "dependencies": {
+ "follow-redirects": "^1.15.4",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/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==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -5866,6 +6466,14 @@
"wrap-ansi": "^7.0.0"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
+ "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -6368,7 +6976,8 @@
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
- "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true
},
"node_modules/cssdb": {
"version": "7.10.0",
@@ -6579,6 +7188,7 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
"integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
+ "dev": true,
"dependencies": {
"array-buffer-byte-length": "^1.0.0",
"call-bind": "^1.0.5",
@@ -6796,7 +7406,8 @@
"node_modules/dom-accessibility-api": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
- "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true
},
"node_modules/dom-converter": {
"version": "0.2.0",
@@ -6806,6 +7417,15 @@
"utila": "~0.4"
}
},
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@@ -7062,6 +7682,7 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
"integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
+ "dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.3",
@@ -8158,6 +8779,11 @@
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
}
},
+ "node_modules/find-root": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
+ },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -8817,6 +9443,19 @@
"he": "bin/he"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
"node_modules/hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -9170,6 +9809,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -9218,6 +9858,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+ "dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
@@ -12101,6 +12742,7 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -12143,6 +12785,15 @@
"tmpl": "1.0.5"
}
},
+ "node_modules/match-sorter": {
+ "version": "6.3.3",
+ "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.3.tgz",
+ "integrity": "sha512-sgiXxrRijEe0SzHKGX4HouCpfHRPnqteH42UdMEW7BlWy990ZkzcvonJGv4Uu9WE7Y1f8Yocm91+4qFPCbmNww==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.8",
+ "remove-accents": "0.5.0"
+ }
+ },
"node_modules/mdn-data": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
@@ -12247,6 +12898,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
"engines": {
"node": ">=4"
}
@@ -12537,6 +13189,7 @@
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
"integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
+ "dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3"
@@ -12556,6 +13209,14 @@
"node": ">= 0.4"
}
},
+ "node_modules/object-path": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.6.0.tgz",
+ "integrity": "sha512-fxrwsCFi3/p+LeLOAwo/wyRMODZxdGBtUlWRzsEpsUVrisZbEfZ21arxLGfaWfcnqb8oHPNihIb4XPE8CQPN5A==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/object.assign": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
@@ -14377,6 +15038,11 @@
"node": ">= 0.10"
}
},
+ "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=="
+ },
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -14665,6 +15331,32 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
+ "node_modules/react-redux": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.0.tgz",
+ "integrity": "sha512-6qoDzIO+gbrza8h3hjMA9aq4nwVFCKFtY2iLxCtVT38Swyy2C/dJCGBXHeHLtx6qlg/8qzc2MrhOeduf5K32wQ==",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.3",
+ "use-sync-external-store": "^1.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25",
+ "react": "^18.0",
+ "react-native": ">=0.69",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -14673,6 +15365,36 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz",
+ "integrity": "sha512-q2yemJeg6gw/YixRlRnVx6IRJWZD6fonnfZhN1JIOhV2iJCPeRNSH3V1ISwHf+JWcESzLC3BOLD1T07tmO5dmg==",
+ "dependencies": {
+ "@remix-run/router": "1.15.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.22.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.0.tgz",
+ "integrity": "sha512-z2w+M4tH5wlcLmH3BMMOMdrtrJ9T3oJJNsAlBJbwk+8Syxd5WFJ7J5dxMEW0/GEXD1BBis4uXRrNIz3mORr0ag==",
+ "dependencies": {
+ "@remix-run/router": "1.15.0",
+ "react-router": "6.22.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -14745,6 +15467,21 @@
}
}
},
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -14792,6 +15529,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
"dependencies": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
@@ -14800,6 +15538,19 @@
"node": ">=8"
}
},
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
@@ -14912,6 +15663,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/remove-accents": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
+ "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="
+ },
"node_modules/renderkid": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
@@ -14945,6 +15701,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
+ "node_modules/reselect": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz",
+ "integrity": "sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg=="
+ },
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -15592,6 +16353,14 @@
"websocket-driver": "^0.7.4"
}
},
+ "node_modules/sort-by": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/sort-by/-/sort-by-1.2.0.tgz",
+ "integrity": "sha512-aRyW65r3xMnf4nxJRluCg0H/woJpksU1dQxRtXYzau30sNBOmf5HACpDd9MZDhKh7ALQ5FgSOfMPwZEtUmMqcg==",
+ "dependencies": {
+ "object-path": "0.6.0"
+ }
+ },
"node_modules/source-list-map": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@@ -15824,6 +16593,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
"integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==",
+ "dev": true,
"dependencies": {
"internal-slot": "^1.0.4"
},
@@ -16018,6 +16788,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
"dependencies": {
"min-indent": "^1.0.0"
},
@@ -16066,6 +16837,11 @@
"postcss": "^8.2.15"
}
},
+ "node_modules/stylis": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
+ },
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -16872,6 +17648,14 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/client/package.json b/client/package.json
index 727bc71..486e14f 100644
--- a/client/package.json
+++ b/client/package.json
@@ -3,18 +3,32 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@emotion/react": "^11.11.3",
+ "@emotion/styled": "^11.11.0",
+ "@fontsource/roboto": "^5.0.8",
+ "@mui/icons-material": "^5.15.8",
+ "@mui/material": "^5.15.7",
+ "@reduxjs/toolkit": "^2.1.0",
+ "axios": "^1.6.7",
+ "match-sorter": "^6.3.3",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-redux": "^9.1.0",
+ "react-router-dom": "^6.22.0",
+ "react-scripts": "5.0.1",
+ "sort-by": "^1.2.0",
+ "typescript": "^4.9.5",
+ "web-vitals": "^2.1.4"
+ },
+ "devDependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "@types/axios": "^0.14.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.77",
"@types/react": "^18.2.48",
- "@types/react-dom": "^18.2.18",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "react-scripts": "5.0.1",
- "typescript": "^4.9.5",
- "web-vitals": "^2.1.4"
+ "@types/react-dom": "^18.2.18"
},
"scripts": {
"start": "react-scripts start",
diff --git a/client/src/App.test.tsx b/client/src/App.test.tsx
deleted file mode 100644
index 2a68616..0000000
--- a/client/src/App.test.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
diff --git a/client/src/App.tsx b/client/src/App.tsx
deleted file mode 100644
index a53698a..0000000
--- a/client/src/App.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-import logo from './logo.svg';
-import './App.css';
-
-function App() {
- return (
-
- );
-}
-
-export default App;
diff --git a/client/src/api/.gitignore b/client/src/api/.gitignore
new file mode 100644
index 0000000..149b576
--- /dev/null
+++ b/client/src/api/.gitignore
@@ -0,0 +1,4 @@
+wwwroot/*.js
+node_modules
+typings
+dist
diff --git a/client/src/api/.npmignore b/client/src/api/.npmignore
new file mode 100644
index 0000000..999d88d
--- /dev/null
+++ b/client/src/api/.npmignore
@@ -0,0 +1 @@
+# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm
\ No newline at end of file
diff --git a/client/src/api/.openapi-generator-ignore b/client/src/api/.openapi-generator-ignore
new file mode 100644
index 0000000..7484ee5
--- /dev/null
+++ b/client/src/api/.openapi-generator-ignore
@@ -0,0 +1,23 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
diff --git a/client/src/api/.openapi-generator/FILES b/client/src/api/.openapi-generator/FILES
new file mode 100644
index 0000000..16b445e
--- /dev/null
+++ b/client/src/api/.openapi-generator/FILES
@@ -0,0 +1,9 @@
+.gitignore
+.npmignore
+.openapi-generator-ignore
+api.ts
+base.ts
+common.ts
+configuration.ts
+git_push.sh
+index.ts
diff --git a/client/src/api/.openapi-generator/VERSION b/client/src/api/.openapi-generator/VERSION
new file mode 100644
index 0000000..8b23b8d
--- /dev/null
+++ b/client/src/api/.openapi-generator/VERSION
@@ -0,0 +1 @@
+7.3.0
\ No newline at end of file
diff --git a/client/src/api/api.ts b/client/src/api/api.ts
new file mode 100644
index 0000000..4c5df5b
--- /dev/null
+++ b/client/src/api/api.ts
@@ -0,0 +1,1737 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * DevSpace API
+ * API for DevSpace
+ *
+ * The version of the OpenAPI document: 0.1.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+
+import type { Configuration } from './configuration';
+import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
+import globalAxios from 'axios';
+// Some imports not used depending on template conditions
+// @ts-ignore
+import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common';
+import type { RequestArgs } from './base';
+// @ts-ignore
+import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base';
+
+/**
+ *
+ * @export
+ * @interface AuthLoginPost200Response
+ */
+export interface AuthLoginPost200Response {
+ /**
+ *
+ * @type {string}
+ * @memberof AuthLoginPost200Response
+ */
+ 'status'?: string;
+ /**
+ * The JWT token for the user
+ * @type {string}
+ * @memberof AuthLoginPost200Response
+ */
+ 'token'?: string;
+}
+/**
+ *
+ * @export
+ * @interface AuthLoginPostRequest
+ */
+export interface AuthLoginPostRequest {
+ /**
+ * The email of the user
+ * @type {string}
+ * @memberof AuthLoginPostRequest
+ */
+ 'email'?: string;
+ /**
+ * The password of the user
+ * @type {string}
+ * @memberof AuthLoginPostRequest
+ */
+ 'password'?: string;
+ /**
+ * Whether to keep the user logged in for a long time
+ * @type {boolean}
+ * @memberof AuthLoginPostRequest
+ */
+ 'longExpiration'?: boolean;
+}
+/**
+ *
+ * @export
+ * @interface AuthLogoutGet200Response
+ */
+export interface AuthLogoutGet200Response {
+ /**
+ *
+ * @type {string}
+ * @memberof AuthLogoutGet200Response
+ */
+ 'status'?: string;
+}
+/**
+ *
+ * @export
+ * @interface AuthSignupPostRequest
+ */
+export interface AuthSignupPostRequest {
+ /**
+ * The email of the user
+ * @type {string}
+ * @memberof AuthSignupPostRequest
+ */
+ 'email'?: string;
+ /**
+ * The password of the user
+ * @type {string}
+ * @memberof AuthSignupPostRequest
+ */
+ 'password'?: string;
+ /**
+ * Password validation field
+ * @type {string}
+ * @memberof AuthSignupPostRequest
+ */
+ 'passwordValidation'?: string;
+ /**
+ * The first name of the user
+ * @type {string}
+ * @memberof AuthSignupPostRequest
+ */
+ 'firstName'?: string;
+ /**
+ * The last name of the user
+ * @type {string}
+ * @memberof AuthSignupPostRequest
+ */
+ 'lastName'?: string;
+}
+/**
+ *
+ * @export
+ * @interface Comment
+ */
+export interface Comment {
+ /**
+ * Comment ID
+ * @type {number}
+ * @memberof Comment
+ */
+ 'id'?: number;
+ /**
+ * Comment content
+ * @type {string}
+ * @memberof Comment
+ */
+ 'content'?: string;
+ /**
+ * Comment creation date
+ * @type {string}
+ * @memberof Comment
+ */
+ 'createdAt'?: string;
+ /**
+ *
+ * @type {User}
+ * @memberof Comment
+ */
+ 'createdBy'?: User;
+ /**
+ *
+ * @type {Array}
+ * @memberof Comment
+ */
+ 'likedBy'?: Array;
+}
+/**
+ *
+ * @export
+ * @interface CommentPostRequest
+ */
+export interface CommentPostRequest {
+ /**
+ * Comment content
+ * @type {string}
+ * @memberof CommentPostRequest
+ */
+ 'content'?: string;
+}
+/**
+ *
+ * @export
+ * @interface CreatePostRequest
+ */
+export interface CreatePostRequest {
+ /**
+ * Post title
+ * @type {string}
+ * @memberof CreatePostRequest
+ */
+ 'title'?: string;
+ /**
+ * Post content
+ * @type {string}
+ * @memberof CreatePostRequest
+ */
+ 'content'?: string;
+}
+/**
+ *
+ * @export
+ * @interface Notification
+ */
+export interface Notification {
+ /**
+ * Notification ID
+ * @type {number}
+ * @memberof Notification
+ */
+ 'id'?: number;
+ /**
+ * Notification message
+ * @type {string}
+ * @memberof Notification
+ */
+ 'message'?: string;
+ /**
+ * Notification creation date
+ * @type {string}
+ * @memberof Notification
+ */
+ 'timeStamp'?: string;
+ /**
+ * Notification seen status
+ * @type {boolean}
+ * @memberof Notification
+ */
+ 'seen'?: boolean;
+}
+/**
+ *
+ * @export
+ * @interface Post
+ */
+export interface Post {
+ /**
+ * Post ID
+ * @type {number}
+ * @memberof Post
+ */
+ 'id'?: number;
+ /**
+ * Post title
+ * @type {string}
+ * @memberof Post
+ */
+ 'title'?: string;
+ /**
+ * Post content
+ * @type {string}
+ * @memberof Post
+ */
+ 'content'?: string;
+ /**
+ * Post creation date
+ * @type {string}
+ * @memberof Post
+ */
+ 'createdAt'?: string;
+ /**
+ *
+ * @type {User}
+ * @memberof Post
+ */
+ 'createdBy'?: User;
+ /**
+ *
+ * @type {Array}
+ * @memberof Post
+ */
+ 'likedBy'?: Array;
+ /**
+ *
+ * @type {Array}
+ * @memberof Post
+ */
+ 'comments'?: Array;
+}
+/**
+ *
+ * @export
+ * @interface User
+ */
+export interface User {
+ /**
+ * User ID
+ * @type {number}
+ * @memberof User
+ */
+ 'id'?: number;
+ /**
+ * User first name
+ * @type {string}
+ * @memberof User
+ */
+ 'firstName'?: string;
+ /**
+ * User last name
+ * @type {string}
+ * @memberof User
+ */
+ 'lastName'?: string;
+}
+/**
+ *
+ * @export
+ * @interface UserWithRelations
+ */
+export interface UserWithRelations {
+ /**
+ * User ID
+ * @type {number}
+ * @memberof UserWithRelations
+ */
+ 'id'?: number;
+ /**
+ * User first name
+ * @type {string}
+ * @memberof UserWithRelations
+ */
+ 'firstName'?: string;
+ /**
+ * User last name
+ * @type {string}
+ * @memberof UserWithRelations
+ */
+ 'lastName'?: string;
+ /**
+ *
+ * @type {Array}
+ * @memberof UserWithRelations
+ */
+ 'posts'?: Array;
+ /**
+ *
+ * @type {Array}
+ * @memberof UserWithRelations
+ */
+ 'comments'?: Array;
+ /**
+ *
+ * @type {Array}
+ * @memberof UserWithRelations
+ */
+ 'followed'?: Array;
+ /**
+ *
+ * @type {Array}
+ * @memberof UserWithRelations
+ */
+ 'followers'?: Array;
+}
+/**
+ *
+ * @export
+ * @interface UserWithRelationsAndNotifications
+ */
+export interface UserWithRelationsAndNotifications {
+ /**
+ * User ID
+ * @type {number}
+ * @memberof UserWithRelationsAndNotifications
+ */
+ 'id'?: number;
+ /**
+ * User first name
+ * @type {string}
+ * @memberof UserWithRelationsAndNotifications
+ */
+ 'firstName'?: string;
+ /**
+ * User last name
+ * @type {string}
+ * @memberof UserWithRelationsAndNotifications
+ */
+ 'lastName'?: string;
+ /**
+ *
+ * @type {Array}
+ * @memberof UserWithRelationsAndNotifications
+ */
+ 'posts'?: Array;
+ /**
+ *
+ * @type {Array}
+ * @memberof UserWithRelationsAndNotifications
+ */
+ 'comments'?: Array;
+ /**
+ *
+ * @type {Array}
+ * @memberof UserWithRelationsAndNotifications
+ */
+ 'followed'?: Array;
+ /**
+ *
+ * @type {Array}
+ * @memberof UserWithRelationsAndNotifications
+ */
+ 'followers'?: Array;
+ /**
+ *
+ * @type {Array}
+ * @memberof UserWithRelationsAndNotifications
+ */
+ 'notifications'?: Array;
+}
+
+/**
+ * AuthenticationApi - axios parameter creator
+ * @export
+ */
+export const AuthenticationApiAxiosParamCreator = function (configuration?: Configuration) {
+ return {
+ /**
+ *
+ * @summary Log in a user
+ * @param {AuthLoginPostRequest} authLoginPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ authLoginPost: async (authLoginPostRequest: AuthLoginPostRequest, options: RawAxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'authLoginPostRequest' is not null or undefined
+ assertParamExists('authLoginPost', 'authLoginPostRequest', authLoginPostRequest)
+ const localVarPath = `/auth/login`;
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+
+
+ localVarHeaderParameter['Content-Type'] = 'application/json';
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+ localVarRequestOptions.data = serializeDataIfNeeded(authLoginPostRequest, localVarRequestOptions, configuration)
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Log out the user
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ authLogoutGet: async (options: RawAxiosRequestConfig = {}): Promise => {
+ const localVarPath = `/auth/logout`;
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Sign up a new user
+ * @param {AuthSignupPostRequest} authSignupPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ authSignupPost: async (authSignupPostRequest: AuthSignupPostRequest, options: RawAxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'authSignupPostRequest' is not null or undefined
+ assertParamExists('authSignupPost', 'authSignupPostRequest', authSignupPostRequest)
+ const localVarPath = `/auth/signup`;
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+
+
+ localVarHeaderParameter['Content-Type'] = 'application/json';
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+ localVarRequestOptions.data = serializeDataIfNeeded(authSignupPostRequest, localVarRequestOptions, configuration)
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ }
+};
+
+/**
+ * AuthenticationApi - functional programming interface
+ * @export
+ */
+export const AuthenticationApiFp = function(configuration?: Configuration) {
+ const localVarAxiosParamCreator = AuthenticationApiAxiosParamCreator(configuration)
+ return {
+ /**
+ *
+ * @summary Log in a user
+ * @param {AuthLoginPostRequest} authLoginPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async authLoginPost(authLoginPostRequest: AuthLoginPostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.authLoginPost(authLoginPostRequest, options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['AuthenticationApi.authLoginPost']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Log out the user
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async authLogoutGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.authLogoutGet(options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['AuthenticationApi.authLogoutGet']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Sign up a new user
+ * @param {AuthSignupPostRequest} authSignupPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async authSignupPost(authSignupPostRequest: AuthSignupPostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.authSignupPost(authSignupPostRequest, options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['AuthenticationApi.authSignupPost']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ }
+};
+
+/**
+ * AuthenticationApi - factory interface
+ * @export
+ */
+export const AuthenticationApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
+ const localVarFp = AuthenticationApiFp(configuration)
+ return {
+ /**
+ *
+ * @summary Log in a user
+ * @param {AuthLoginPostRequest} authLoginPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ authLoginPost(authLoginPostRequest: AuthLoginPostRequest, options?: any): AxiosPromise {
+ return localVarFp.authLoginPost(authLoginPostRequest, options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Log out the user
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ authLogoutGet(options?: any): AxiosPromise {
+ return localVarFp.authLogoutGet(options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Sign up a new user
+ * @param {AuthSignupPostRequest} authSignupPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ authSignupPost(authSignupPostRequest: AuthSignupPostRequest, options?: any): AxiosPromise {
+ return localVarFp.authSignupPost(authSignupPostRequest, options).then((request) => request(axios, basePath));
+ },
+ };
+};
+
+/**
+ * AuthenticationApi - object-oriented interface
+ * @export
+ * @class AuthenticationApi
+ * @extends {BaseAPI}
+ */
+export class AuthenticationApi extends BaseAPI {
+ /**
+ *
+ * @summary Log in a user
+ * @param {AuthLoginPostRequest} authLoginPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof AuthenticationApi
+ */
+ public authLoginPost(authLoginPostRequest: AuthLoginPostRequest, options?: RawAxiosRequestConfig) {
+ return AuthenticationApiFp(this.configuration).authLoginPost(authLoginPostRequest, options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Log out the user
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof AuthenticationApi
+ */
+ public authLogoutGet(options?: RawAxiosRequestConfig) {
+ return AuthenticationApiFp(this.configuration).authLogoutGet(options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Sign up a new user
+ * @param {AuthSignupPostRequest} authSignupPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof AuthenticationApi
+ */
+ public authSignupPost(authSignupPostRequest: AuthSignupPostRequest, options?: RawAxiosRequestConfig) {
+ return AuthenticationApiFp(this.configuration).authSignupPost(authSignupPostRequest, options).then((request) => request(this.axios, this.basePath));
+ }
+}
+
+
+
+/**
+ * PostsApi - axios parameter creator
+ * @export
+ */
+export const PostsApiAxiosParamCreator = function (configuration?: Configuration) {
+ return {
+ /**
+ *
+ * @summary Comment on a post by ID
+ * @param {number} id ID of the post
+ * @param {CommentPostRequest} commentPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ commentPost: async (id: number, commentPostRequest: CommentPostRequest, options: RawAxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'id' is not null or undefined
+ assertParamExists('commentPost', 'id', id)
+ // verify required parameter 'commentPostRequest' is not null or undefined
+ assertParamExists('commentPost', 'commentPostRequest', commentPostRequest)
+ const localVarPath = `/posts/{id}/comment`
+ .replace(`{${"id"}}`, encodeURIComponent(String(id)));
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ localVarHeaderParameter['Content-Type'] = 'application/json';
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+ localVarRequestOptions.data = serializeDataIfNeeded(commentPostRequest, localVarRequestOptions, configuration)
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Create a post
+ * @param {CreatePostRequest} createPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ createPost: async (createPostRequest: CreatePostRequest, options: RawAxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'createPostRequest' is not null or undefined
+ assertParamExists('createPost', 'createPostRequest', createPostRequest)
+ const localVarPath = `/posts`;
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ localVarHeaderParameter['Content-Type'] = 'application/json';
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+ localVarRequestOptions.data = serializeDataIfNeeded(createPostRequest, localVarRequestOptions, configuration)
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Delete a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ deletePost: async (id: number, options: RawAxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'id' is not null or undefined
+ assertParamExists('deletePost', 'id', id)
+ const localVarPath = `/posts/{id}`
+ .replace(`{${"id"}}`, encodeURIComponent(String(id)));
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Get all posts
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ getAllPosts: async (options: RawAxiosRequestConfig = {}): Promise => {
+ const localVarPath = `/posts`;
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Get posts of followed users
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ getFollowedPosts: async (options: RawAxiosRequestConfig = {}): Promise => {
+ const localVarPath = `/posts/followed`;
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Get a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ getPost: async (id: number, options: RawAxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'id' is not null or undefined
+ assertParamExists('getPost', 'id', id)
+ const localVarPath = `/posts/{id}`
+ .replace(`{${"id"}}`, encodeURIComponent(String(id)));
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Like a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ likePost: async (id: number, options: RawAxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'id' is not null or undefined
+ assertParamExists('likePost', 'id', id)
+ const localVarPath = `/posts/{id}/like`
+ .replace(`{${"id"}}`, encodeURIComponent(String(id)));
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Unlike a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ unlikePost: async (id: number, options: RawAxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'id' is not null or undefined
+ assertParamExists('unlikePost', 'id', id)
+ const localVarPath = `/posts/{id}/like`
+ .replace(`{${"id"}}`, encodeURIComponent(String(id)));
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Update a post by ID
+ * @param {number} id ID of the post
+ * @param {CreatePostRequest} createPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ updatePost: async (id: number, createPostRequest: CreatePostRequest, options: RawAxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'id' is not null or undefined
+ assertParamExists('updatePost', 'id', id)
+ // verify required parameter 'createPostRequest' is not null or undefined
+ assertParamExists('updatePost', 'createPostRequest', createPostRequest)
+ const localVarPath = `/posts/{id}`
+ .replace(`{${"id"}}`, encodeURIComponent(String(id)));
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ localVarHeaderParameter['Content-Type'] = 'application/json';
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+ localVarRequestOptions.data = serializeDataIfNeeded(createPostRequest, localVarRequestOptions, configuration)
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ }
+};
+
+/**
+ * PostsApi - functional programming interface
+ * @export
+ */
+export const PostsApiFp = function(configuration?: Configuration) {
+ const localVarAxiosParamCreator = PostsApiAxiosParamCreator(configuration)
+ return {
+ /**
+ *
+ * @summary Comment on a post by ID
+ * @param {number} id ID of the post
+ * @param {CommentPostRequest} commentPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async commentPost(id: number, commentPostRequest: CommentPostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.commentPost(id, commentPostRequest, options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['PostsApi.commentPost']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Create a post
+ * @param {CreatePostRequest} createPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async createPost(createPostRequest: CreatePostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.createPost(createPostRequest, options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['PostsApi.createPost']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Delete a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async deletePost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.deletePost(id, options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['PostsApi.deletePost']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Get all posts
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async getAllPosts(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPosts(options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['PostsApi.getAllPosts']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Get posts of followed users
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async getFollowedPosts(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.getFollowedPosts(options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['PostsApi.getFollowedPosts']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Get a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async getPost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.getPost(id, options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['PostsApi.getPost']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Like a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async likePost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.likePost(id, options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['PostsApi.likePost']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Unlike a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async unlikePost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.unlikePost(id, options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['PostsApi.unlikePost']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Update a post by ID
+ * @param {number} id ID of the post
+ * @param {CreatePostRequest} createPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async updatePost(id: number, createPostRequest: CreatePostRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.updatePost(id, createPostRequest, options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['PostsApi.updatePost']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ }
+};
+
+/**
+ * PostsApi - factory interface
+ * @export
+ */
+export const PostsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
+ const localVarFp = PostsApiFp(configuration)
+ return {
+ /**
+ *
+ * @summary Comment on a post by ID
+ * @param {number} id ID of the post
+ * @param {CommentPostRequest} commentPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ commentPost(id: number, commentPostRequest: CommentPostRequest, options?: any): AxiosPromise {
+ return localVarFp.commentPost(id, commentPostRequest, options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Create a post
+ * @param {CreatePostRequest} createPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ createPost(createPostRequest: CreatePostRequest, options?: any): AxiosPromise {
+ return localVarFp.createPost(createPostRequest, options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Delete a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ deletePost(id: number, options?: any): AxiosPromise {
+ return localVarFp.deletePost(id, options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Get all posts
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ getAllPosts(options?: any): AxiosPromise> {
+ return localVarFp.getAllPosts(options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Get posts of followed users
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ getFollowedPosts(options?: any): AxiosPromise> {
+ return localVarFp.getFollowedPosts(options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Get a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ getPost(id: number, options?: any): AxiosPromise {
+ return localVarFp.getPost(id, options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Like a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ likePost(id: number, options?: any): AxiosPromise {
+ return localVarFp.likePost(id, options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Unlike a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ unlikePost(id: number, options?: any): AxiosPromise {
+ return localVarFp.unlikePost(id, options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Update a post by ID
+ * @param {number} id ID of the post
+ * @param {CreatePostRequest} createPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ updatePost(id: number, createPostRequest: CreatePostRequest, options?: any): AxiosPromise {
+ return localVarFp.updatePost(id, createPostRequest, options).then((request) => request(axios, basePath));
+ },
+ };
+};
+
+/**
+ * PostsApi - object-oriented interface
+ * @export
+ * @class PostsApi
+ * @extends {BaseAPI}
+ */
+export class PostsApi extends BaseAPI {
+ /**
+ *
+ * @summary Comment on a post by ID
+ * @param {number} id ID of the post
+ * @param {CommentPostRequest} commentPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof PostsApi
+ */
+ public commentPost(id: number, commentPostRequest: CommentPostRequest, options?: RawAxiosRequestConfig) {
+ return PostsApiFp(this.configuration).commentPost(id, commentPostRequest, options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Create a post
+ * @param {CreatePostRequest} createPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof PostsApi
+ */
+ public createPost(createPostRequest: CreatePostRequest, options?: RawAxiosRequestConfig) {
+ return PostsApiFp(this.configuration).createPost(createPostRequest, options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Delete a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof PostsApi
+ */
+ public deletePost(id: number, options?: RawAxiosRequestConfig) {
+ return PostsApiFp(this.configuration).deletePost(id, options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Get all posts
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof PostsApi
+ */
+ public getAllPosts(options?: RawAxiosRequestConfig) {
+ return PostsApiFp(this.configuration).getAllPosts(options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Get posts of followed users
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof PostsApi
+ */
+ public getFollowedPosts(options?: RawAxiosRequestConfig) {
+ return PostsApiFp(this.configuration).getFollowedPosts(options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Get a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof PostsApi
+ */
+ public getPost(id: number, options?: RawAxiosRequestConfig) {
+ return PostsApiFp(this.configuration).getPost(id, options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Like a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof PostsApi
+ */
+ public likePost(id: number, options?: RawAxiosRequestConfig) {
+ return PostsApiFp(this.configuration).likePost(id, options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Unlike a post by ID
+ * @param {number} id ID of the post
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof PostsApi
+ */
+ public unlikePost(id: number, options?: RawAxiosRequestConfig) {
+ return PostsApiFp(this.configuration).unlikePost(id, options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Update a post by ID
+ * @param {number} id ID of the post
+ * @param {CreatePostRequest} createPostRequest
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof PostsApi
+ */
+ public updatePost(id: number, createPostRequest: CreatePostRequest, options?: RawAxiosRequestConfig) {
+ return PostsApiFp(this.configuration).updatePost(id, createPostRequest, options).then((request) => request(this.axios, this.basePath));
+ }
+}
+
+
+
+/**
+ * UsersApi - axios parameter creator
+ * @export
+ */
+export const UsersApiAxiosParamCreator = function (configuration?: Configuration) {
+ return {
+ /**
+ *
+ * @summary Get all users
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ usersGet: async (options: RawAxiosRequestConfig = {}): Promise => {
+ const localVarPath = `/users`;
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Unfollow a user
+ * @param {number} id
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ usersIdFollowDelete: async (id: number, options: RawAxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'id' is not null or undefined
+ assertParamExists('usersIdFollowDelete', 'id', id)
+ const localVarPath = `/users/{id}/follow`
+ .replace(`{${"id"}}`, encodeURIComponent(String(id)));
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Follow a user
+ * @param {number} id
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ usersIdFollowPost: async (id: number, options: RawAxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'id' is not null or undefined
+ assertParamExists('usersIdFollowPost', 'id', id)
+ const localVarPath = `/users/{id}/follow`
+ .replace(`{${"id"}}`, encodeURIComponent(String(id)));
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Get a single user
+ * @param {number} id ID of the user
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ usersIdGet: async (id: number, options: RawAxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'id' is not null or undefined
+ assertParamExists('usersIdGet', 'id', id)
+ const localVarPath = `/users/{id}`
+ .replace(`{${"id"}}`, encodeURIComponent(String(id)));
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @summary Get the currently logged in user
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ usersMeGet: async (options: RawAxiosRequestConfig = {}): Promise => {
+ const localVarPath = `/users/me`;
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication bearerAuth required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ }
+};
+
+/**
+ * UsersApi - functional programming interface
+ * @export
+ */
+export const UsersApiFp = function(configuration?: Configuration) {
+ const localVarAxiosParamCreator = UsersApiAxiosParamCreator(configuration)
+ return {
+ /**
+ *
+ * @summary Get all users
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async usersGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.usersGet(options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['UsersApi.usersGet']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Unfollow a user
+ * @param {number} id
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async usersIdFollowDelete(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.usersIdFollowDelete(id, options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['UsersApi.usersIdFollowDelete']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Follow a user
+ * @param {number} id
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async usersIdFollowPost(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.usersIdFollowPost(id, options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['UsersApi.usersIdFollowPost']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Get a single user
+ * @param {number} id ID of the user
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async usersIdGet(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.usersIdGet(id, options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['UsersApi.usersIdGet']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ /**
+ *
+ * @summary Get the currently logged in user
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async usersMeGet(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.usersMeGet(options);
+ const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
+ const localVarOperationServerBasePath = operationServerMap['UsersApi.usersMeGet']?.[localVarOperationServerIndex]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
+ },
+ }
+};
+
+/**
+ * UsersApi - factory interface
+ * @export
+ */
+export const UsersApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
+ const localVarFp = UsersApiFp(configuration)
+ return {
+ /**
+ *
+ * @summary Get all users
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ usersGet(options?: any): AxiosPromise> {
+ return localVarFp.usersGet(options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Unfollow a user
+ * @param {number} id
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ usersIdFollowDelete(id: number, options?: any): AxiosPromise {
+ return localVarFp.usersIdFollowDelete(id, options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Follow a user
+ * @param {number} id
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ usersIdFollowPost(id: number, options?: any): AxiosPromise {
+ return localVarFp.usersIdFollowPost(id, options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Get a single user
+ * @param {number} id ID of the user
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ usersIdGet(id: number, options?: any): AxiosPromise {
+ return localVarFp.usersIdGet(id, options).then((request) => request(axios, basePath));
+ },
+ /**
+ *
+ * @summary Get the currently logged in user
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ usersMeGet(options?: any): AxiosPromise {
+ return localVarFp.usersMeGet(options).then((request) => request(axios, basePath));
+ },
+ };
+};
+
+/**
+ * UsersApi - object-oriented interface
+ * @export
+ * @class UsersApi
+ * @extends {BaseAPI}
+ */
+export class UsersApi extends BaseAPI {
+ /**
+ *
+ * @summary Get all users
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof UsersApi
+ */
+ public usersGet(options?: RawAxiosRequestConfig) {
+ return UsersApiFp(this.configuration).usersGet(options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Unfollow a user
+ * @param {number} id
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof UsersApi
+ */
+ public usersIdFollowDelete(id: number, options?: RawAxiosRequestConfig) {
+ return UsersApiFp(this.configuration).usersIdFollowDelete(id, options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Follow a user
+ * @param {number} id
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof UsersApi
+ */
+ public usersIdFollowPost(id: number, options?: RawAxiosRequestConfig) {
+ return UsersApiFp(this.configuration).usersIdFollowPost(id, options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Get a single user
+ * @param {number} id ID of the user
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof UsersApi
+ */
+ public usersIdGet(id: number, options?: RawAxiosRequestConfig) {
+ return UsersApiFp(this.configuration).usersIdGet(id, options).then((request) => request(this.axios, this.basePath));
+ }
+
+ /**
+ *
+ * @summary Get the currently logged in user
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof UsersApi
+ */
+ public usersMeGet(options?: RawAxiosRequestConfig) {
+ return UsersApiFp(this.configuration).usersMeGet(options).then((request) => request(this.axios, this.basePath));
+ }
+}
+
+
+
diff --git a/client/src/api/base.ts b/client/src/api/base.ts
new file mode 100644
index 0000000..c4c2bc4
--- /dev/null
+++ b/client/src/api/base.ts
@@ -0,0 +1,86 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * DevSpace API
+ * API for DevSpace
+ *
+ * The version of the OpenAPI document: 0.1.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+
+import type { Configuration } from './configuration';
+// Some imports not used depending on template conditions
+// @ts-ignore
+import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
+import globalAxios from 'axios';
+
+export const BASE_PATH = "http://localhost".replace(/\/+$/, "");
+
+/**
+ *
+ * @export
+ */
+export const COLLECTION_FORMATS = {
+ csv: ",",
+ ssv: " ",
+ tsv: "\t",
+ pipes: "|",
+};
+
+/**
+ *
+ * @export
+ * @interface RequestArgs
+ */
+export interface RequestArgs {
+ url: string;
+ options: RawAxiosRequestConfig;
+}
+
+/**
+ *
+ * @export
+ * @class BaseAPI
+ */
+export class BaseAPI {
+ protected configuration: Configuration | undefined;
+
+ constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {
+ if (configuration) {
+ this.configuration = configuration;
+ this.basePath = configuration.basePath ?? basePath;
+ }
+ }
+};
+
+/**
+ *
+ * @export
+ * @class RequiredError
+ * @extends {Error}
+ */
+export class RequiredError extends Error {
+ constructor(public field: string, msg?: string) {
+ super(msg);
+ this.name = "RequiredError"
+ }
+}
+
+interface ServerMap {
+ [key: string]: {
+ url: string,
+ description: string,
+ }[];
+}
+
+/**
+ *
+ * @export
+ */
+export const operationServerMap: ServerMap = {
+}
diff --git a/client/src/api/common.ts b/client/src/api/common.ts
new file mode 100644
index 0000000..0f54e29
--- /dev/null
+++ b/client/src/api/common.ts
@@ -0,0 +1,150 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * DevSpace API
+ * API for DevSpace
+ *
+ * The version of the OpenAPI document: 0.1.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+
+import type { Configuration } from "./configuration";
+import type { RequestArgs } from "./base";
+import type { AxiosInstance, AxiosResponse } from 'axios';
+import { RequiredError } from "./base";
+
+/**
+ *
+ * @export
+ */
+export const DUMMY_BASE_URL = 'https://example.com'
+
+/**
+ *
+ * @throws {RequiredError}
+ * @export
+ */
+export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) {
+ if (paramValue === null || paramValue === undefined) {
+ throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`);
+ }
+}
+
+/**
+ *
+ * @export
+ */
+export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {
+ if (configuration && configuration.apiKey) {
+ const localVarApiKeyValue = typeof configuration.apiKey === 'function'
+ ? await configuration.apiKey(keyParamName)
+ : await configuration.apiKey;
+ object[keyParamName] = localVarApiKeyValue;
+ }
+}
+
+/**
+ *
+ * @export
+ */
+export const setBasicAuthToObject = function (object: any, configuration?: Configuration) {
+ if (configuration && (configuration.username || configuration.password)) {
+ object["auth"] = { username: configuration.username, password: configuration.password };
+ }
+}
+
+/**
+ *
+ * @export
+ */
+export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {
+ if (configuration && configuration.accessToken) {
+ const accessToken = typeof configuration.accessToken === 'function'
+ ? await configuration.accessToken()
+ : await configuration.accessToken;
+ object["Authorization"] = "Bearer " + accessToken;
+ }
+}
+
+/**
+ *
+ * @export
+ */
+export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {
+ if (configuration && configuration.accessToken) {
+ const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
+ ? await configuration.accessToken(name, scopes)
+ : await configuration.accessToken;
+ object["Authorization"] = "Bearer " + localVarAccessTokenValue;
+ }
+}
+
+function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void {
+ if (parameter == null) return;
+ if (typeof parameter === "object") {
+ if (Array.isArray(parameter)) {
+ (parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key));
+ }
+ else {
+ Object.keys(parameter).forEach(currentKey =>
+ setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`)
+ );
+ }
+ }
+ else {
+ if (urlSearchParams.has(key)) {
+ urlSearchParams.append(key, parameter);
+ }
+ else {
+ urlSearchParams.set(key, parameter);
+ }
+ }
+}
+
+/**
+ *
+ * @export
+ */
+export const setSearchParams = function (url: URL, ...objects: any[]) {
+ const searchParams = new URLSearchParams(url.search);
+ setFlattenedQueryParams(searchParams, objects);
+ url.search = searchParams.toString();
+}
+
+/**
+ *
+ * @export
+ */
+export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) {
+ const nonString = typeof value !== 'string';
+ const needsSerialization = nonString && configuration && configuration.isJsonMime
+ ? configuration.isJsonMime(requestOptions.headers['Content-Type'])
+ : nonString;
+ return needsSerialization
+ ? JSON.stringify(value !== undefined ? value : {})
+ : (value || "");
+}
+
+/**
+ *
+ * @export
+ */
+export const toPathString = function (url: URL) {
+ return url.pathname + url.search + url.hash
+}
+
+/**
+ *
+ * @export
+ */
+export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
+ return >(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+ const axiosRequestArgs = {...axiosArgs.options, url: (axios.defaults.baseURL ? '' : configuration?.basePath ?? basePath) + axiosArgs.url};
+ return axios.request(axiosRequestArgs);
+ };
+}
diff --git a/client/src/api/configuration.ts b/client/src/api/configuration.ts
new file mode 100644
index 0000000..02064a6
--- /dev/null
+++ b/client/src/api/configuration.ts
@@ -0,0 +1,110 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * DevSpace API
+ * API for DevSpace
+ *
+ * The version of the OpenAPI document: 0.1.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+
+export interface ConfigurationParameters {
+ apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise);
+ username?: string;
+ password?: string;
+ accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise);
+ basePath?: string;
+ serverIndex?: number;
+ baseOptions?: any;
+ formDataCtor?: new () => any;
+}
+
+export class Configuration {
+ /**
+ * parameter for apiKey security
+ * @param name security name
+ * @memberof Configuration
+ */
+ apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise);
+ /**
+ * parameter for basic security
+ *
+ * @type {string}
+ * @memberof Configuration
+ */
+ username?: string;
+ /**
+ * parameter for basic security
+ *
+ * @type {string}
+ * @memberof Configuration
+ */
+ password?: string;
+ /**
+ * parameter for oauth2 security
+ * @param name security name
+ * @param scopes oauth2 scope
+ * @memberof Configuration
+ */
+ accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise);
+ /**
+ * override base path
+ *
+ * @type {string}
+ * @memberof Configuration
+ */
+ basePath?: string;
+ /**
+ * override server index
+ *
+ * @type {number}
+ * @memberof Configuration
+ */
+ serverIndex?: number;
+ /**
+ * base options for axios calls
+ *
+ * @type {any}
+ * @memberof Configuration
+ */
+ baseOptions?: any;
+ /**
+ * The FormData constructor that will be used to create multipart form data
+ * requests. You can inject this here so that execution environments that
+ * do not support the FormData class can still run the generated client.
+ *
+ * @type {new () => FormData}
+ */
+ formDataCtor?: new () => any;
+
+ constructor(param: ConfigurationParameters = {}) {
+ this.apiKey = param.apiKey;
+ this.username = param.username;
+ this.password = param.password;
+ this.accessToken = param.accessToken;
+ this.basePath = param.basePath;
+ this.serverIndex = param.serverIndex;
+ this.baseOptions = param.baseOptions;
+ this.formDataCtor = param.formDataCtor;
+ }
+
+ /**
+ * Check if the given MIME is a JSON MIME.
+ * JSON MIME examples:
+ * application/json
+ * application/json; charset=UTF8
+ * APPLICATION/JSON
+ * application/vnd.company+json
+ * @param mime - MIME (Multipurpose Internet Mail Extensions)
+ * @return True if the given MIME is JSON, false otherwise.
+ */
+ public isJsonMime(mime: string): boolean {
+ const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i');
+ return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');
+ }
+}
diff --git a/client/src/api/git_push.sh b/client/src/api/git_push.sh
new file mode 100644
index 0000000..f53a75d
--- /dev/null
+++ b/client/src/api/git_push.sh
@@ -0,0 +1,57 @@
+#!/bin/sh
+# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
+#
+# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
+
+git_user_id=$1
+git_repo_id=$2
+release_note=$3
+git_host=$4
+
+if [ "$git_host" = "" ]; then
+ git_host="github.com"
+ echo "[INFO] No command line input provided. Set \$git_host to $git_host"
+fi
+
+if [ "$git_user_id" = "" ]; then
+ git_user_id="GIT_USER_ID"
+ echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
+fi
+
+if [ "$git_repo_id" = "" ]; then
+ git_repo_id="GIT_REPO_ID"
+ echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
+fi
+
+if [ "$release_note" = "" ]; then
+ release_note="Minor update"
+ echo "[INFO] No command line input provided. Set \$release_note to $release_note"
+fi
+
+# Initialize the local directory as a Git repository
+git init
+
+# Adds the files in the local repository and stages them for commit.
+git add .
+
+# Commits the tracked changes and prepares them to be pushed to a remote repository.
+git commit -m "$release_note"
+
+# Sets the new remote
+git_remote=$(git remote)
+if [ "$git_remote" = "" ]; then # git remote not defined
+
+ if [ "$GIT_TOKEN" = "" ]; then
+ echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
+ git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
+ else
+ git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
+ fi
+
+fi
+
+git pull origin master
+
+# Pushes (Forces) the changes in the local repository up to the remote repository
+echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
+git push origin master 2>&1 | grep -v 'To https'
diff --git a/client/src/api/index.ts b/client/src/api/index.ts
new file mode 100644
index 0000000..e3a805a
--- /dev/null
+++ b/client/src/api/index.ts
@@ -0,0 +1,18 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * DevSpace API
+ * API for DevSpace
+ *
+ * The version of the OpenAPI document: 0.1.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+
+export * from "./api";
+export * from "./configuration";
+
diff --git a/client/src/app/hooks/useWindowDimensions.ts b/client/src/app/hooks/useWindowDimensions.ts
new file mode 100644
index 0000000..6f61af7
--- /dev/null
+++ b/client/src/app/hooks/useWindowDimensions.ts
@@ -0,0 +1,23 @@
+import { useEffect, useState } from "react";
+
+function getWindowDimensions() {
+ const { innerWidth: width, innerHeight: height } = window;
+ return { width, height };
+}
+
+export default function useWindowDimensions() {
+ const [windowDimensions, setWindowDimensions] = useState(
+ getWindowDimensions()
+ );
+
+ useEffect(() => {
+ function handleResize() {
+ setWindowDimensions(getWindowDimensions());
+ }
+
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ return windowDimensions;
+}
diff --git a/client/src/app/loginSlice.ts b/client/src/app/loginSlice.ts
new file mode 100644
index 0000000..0d2285e
--- /dev/null
+++ b/client/src/app/loginSlice.ts
@@ -0,0 +1,137 @@
+import { createSlice } from "@reduxjs/toolkit";
+import { Status } from "../util/types";
+import {
+ AuthLoginPostRequest,
+ AuthSignupPostRequest,
+ AuthenticationApi,
+ Configuration,
+ UsersApi,
+} from "../api";
+import { AppThunk } from "./store";
+
+interface loginState {
+ loggedIn: boolean;
+ status: Status;
+ error: string | null;
+ userInfo: {
+ firstName: string;
+ lastName: string;
+ jwt: string;
+ };
+}
+
+const initialState: loginState = {
+ loggedIn: false,
+ status: Status.idle,
+ error: null,
+ userInfo: {
+ firstName: "",
+ lastName: "",
+ jwt: "",
+ },
+};
+
+export const loginSlice = createSlice({
+ name: "login",
+ initialState,
+ reducers: {
+ login: (state, action) => {
+ state.loggedIn = true;
+ state.userInfo.jwt = action.payload.jwt;
+ state.userInfo.firstName = action.payload.firstName;
+ state.userInfo.lastName = action.payload.lastName;
+ },
+ logoff: (state) => {
+ state.loggedIn = false;
+ state.userInfo = initialState.userInfo;
+ },
+ setStatus: (state, action) => {
+ state.status = action.payload;
+ },
+ setError: (state, action) => {
+ state.error = action.payload;
+ },
+ },
+});
+
+const authApi = new AuthenticationApi(
+ new Configuration({
+ basePath: process.env.REACT_APP_BACKEND_URL,
+ })
+);
+
+export const postLogin =
+ (params: AuthLoginPostRequest): AppThunk =>
+ async (dispatch) => {
+ let response, userResponse;
+ try {
+ dispatch(setStatus(Status.loading));
+ response = await authApi.authLoginPost(params);
+
+ await addJWT(response.data.token || "");
+
+ // Get user info
+ const userApi = new UsersApi(
+ new Configuration({
+ basePath: process.env.REACT_APP_BACKEND_URL,
+ accessToken: response.data.token,
+ })
+ );
+
+ userResponse = await userApi.usersMeGet();
+
+ dispatch(
+ login({
+ jwt: response.data.token,
+ firstName: userResponse.data.firstName,
+ lastName: userResponse.data.lastName,
+ })
+ );
+ dispatch(setStatus(Status.succeeded));
+ } catch (error) {
+ dispatch(setStatus(Status.failed));
+ const errorMessage = "Invalid email or password";
+ dispatch(setError(errorMessage));
+ }
+ };
+
+export const postSignup =
+ (params: AuthSignupPostRequest): AppThunk =>
+ async (dispatch) => {
+ let response;
+ console.log(params);
+ try {
+ dispatch(setStatus(Status.loading));
+ response = await authApi.authSignupPost(params);
+
+ dispatch(postLogin({ email: params.email, password: params.password }));
+ } catch (error) {
+ dispatch(setStatus(Status.failed));
+ const errorMessage = "Change this pls";
+ dispatch(setError(errorMessage));
+ }
+ };
+
+export const postLogout = (): AppThunk => async (dispatch) => {
+ localStorage.removeItem("jwt");
+ sessionStorage.removeItem("jwt");
+ dispatch(logoff());
+ await authApi.authLogoutGet();
+};
+
+const addJWT = async (token: string) => {
+ localStorage.setItem("jwt", token);
+};
+
+export const { login, logoff, setStatus, setError } = loginSlice.actions;
+
+export default loginSlice.reducer;
+
+export const selectLoggedIn = (state: { login: loginState }) =>
+ state.login.loggedIn;
+
+export const selectUserInfo = (state: { login: loginState }) =>
+ state.login.userInfo;
+
+export const selectErrorMessage = (state: { login: loginState }) =>
+ state.login.error;
diff --git a/client/src/app/postSlice.ts b/client/src/app/postSlice.ts
new file mode 100644
index 0000000..30ff9c4
--- /dev/null
+++ b/client/src/app/postSlice.ts
@@ -0,0 +1,88 @@
+import { Store, createSlice } from "@reduxjs/toolkit";
+import { Configuration, Post, PostsApi } from "../api";
+import { Status } from "../util/types";
+import { AppThunk, store } from "./store";
+
+interface postSliceInterface {
+ status: Status;
+ followedPosts: Post[];
+ globalPosts: Post[];
+}
+
+const initialState: postSliceInterface = {
+ status: Status.idle,
+ followedPosts: [],
+ globalPosts: [],
+};
+
+export const postSlice = createSlice({
+ name: "post",
+ initialState,
+ reducers: {
+ setStatus: (state, action) => {
+ state.status = action.payload;
+ },
+ setFollowedPosts: (state, action) => {
+ state.followedPosts = action.payload;
+ },
+ setGlobalPosts: (state, action) => {
+ state.globalPosts = action.payload;
+ },
+ },
+});
+
+export const fetchFollowedPosts = (): AppThunk => async (dispatch: any) => {
+ const postApi = createApi(store);
+
+ dispatch(setStatus(Status.loading));
+ const response = await postApi.getFollowedPosts();
+ dispatch(setFollowedPosts(response.data));
+ dispatch(setStatus(Status.idle));
+};
+
+export const fetchGlobalPosts = (): AppThunk => async (dispatch: any) => {
+ const postApi = createApi(store);
+
+ dispatch(setStatus(Status.loading));
+ const response = await postApi.getAllPosts();
+ dispatch(setGlobalPosts(response.data));
+ dispatch(setStatus(Status.idle));
+};
+
+export const likePost =
+ (postId: number): AppThunk =>
+ async (dispatch: any) => {
+ const postApi = createApi(store);
+
+ dispatch(setStatus(Status.loading));
+ await postApi.likePost(postId);
+ dispatch(setStatus(Status.idle));
+ };
+
+export const unLikePost =
+ (postId: number): AppThunk =>
+ async (dispatch: any) => {
+ const postApi = createApi(store);
+
+ dispatch(setStatus(Status.loading));
+ await postApi.unlikePost(postId);
+ dispatch(setStatus(Status.idle));
+ };
+
+export const { setFollowedPosts, setGlobalPosts, setStatus } =
+ postSlice.actions;
+
+export default postSlice.reducer;
+
+export const selectFollowedPosts = (state: any) => state.post.followedPosts;
+export const selectAllPosts = (state: any) => state.post.globalPosts;
+export const selectStatus = (state: any) => state.post.status;
+
+function createApi(store: Store) {
+ return new PostsApi(
+ new Configuration({
+ basePath: process.env.REACT_APP_BACKEND_URL,
+ accessToken: store.getState().login.userInfo.jwt,
+ })
+ );
+}
diff --git a/client/src/app/store.ts b/client/src/app/store.ts
new file mode 100644
index 0000000..089d197
--- /dev/null
+++ b/client/src/app/store.ts
@@ -0,0 +1,22 @@
+import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
+import { useDispatch } from "react-redux";
+import loginReducer from "./loginSlice";
+import postReducer from "./postSlice";
+
+export const store = configureStore({
+ reducer: {
+ login: loginReducer,
+ post: postReducer,
+ },
+});
+
+export type AppDispatch = typeof store.dispatch;
+export type RootState = ReturnType;
+export type AppThunk = ThunkAction<
+ ReturnType,
+ RootState,
+ unknown,
+ Action
+>;
+
+export const useAppDispatch: () => AppDispatch = useDispatch;
diff --git a/client/src/components/Copyright.tsx b/client/src/components/Copyright.tsx
new file mode 100644
index 0000000..825d9f5
--- /dev/null
+++ b/client/src/components/Copyright.tsx
@@ -0,0 +1,18 @@
+import { Link, Typography } from "@mui/material";
+
+export function Copyright(props: any) {
+ return (
+
+ {"Copyright © "}
+
+ Tu Trastero Tu Otro Espacio S.L
+ {" "}
+ {new Date().getFullYear()}.
+
+ );
+}
diff --git a/client/src/components/Drawer.tsx b/client/src/components/Drawer.tsx
new file mode 100644
index 0000000..2ce0708
--- /dev/null
+++ b/client/src/components/Drawer.tsx
@@ -0,0 +1,45 @@
+import {Grid, Hidden, Typography, Drawer, Box} from "@mui/material";
+
+export default function ResponsiveDrawer(){
+ const drawer = (
+
+
+
+ Home
+
+
+ Search
+
+
+ Notifications
+
+
+
+ );
+
+ return (
+
+ {/* The implementation can be swapped with js to avoid SEO duplication of links. */}
+
+
+ {drawer}
+
+
+
+
+ {drawer}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/StyledComponents.tsx b/client/src/components/StyledComponents.tsx
new file mode 100644
index 0000000..74a67c2
--- /dev/null
+++ b/client/src/components/StyledComponents.tsx
@@ -0,0 +1,5 @@
+import { Divider, styled } from "@mui/material";
+
+export const StyledDivider = styled(Divider)({
+ marginBottom: "2%",
+});
diff --git a/client/src/components/bottomAppBar.tsx b/client/src/components/bottomAppBar.tsx
new file mode 100644
index 0000000..a94ff78
--- /dev/null
+++ b/client/src/components/bottomAppBar.tsx
@@ -0,0 +1,51 @@
+import { BottomNavigation, BottomNavigationAction, Paper } from "@mui/material";
+import { useState } from "react";
+import FeedIcon from "@mui/icons-material/Feed";
+import GlobalIcon from "@mui/icons-material/Public";
+import AddIcon from "@mui/icons-material/Add";
+import SearchIcon from "@mui/icons-material/Search";
+import { useNavigate } from "react-router-dom";
+
+export default function BottomAppBar() {
+ const [value, setValue] = useState(0);
+ const navigate = useNavigate();
+
+ const handleClick = (to: string) => () => {
+ navigate(to);
+ };
+ return (
+
+ {
+ setValue(newValue);
+ }}
+ >
+ }
+ onClick={handleClick("feed")}
+ />
+ }
+ onClick={handleClick("global")}
+ />
+ }
+ onClick={handleClick("newpost")}
+ />
+ }
+ onClick={handleClick("search")}
+ />
+
+
+ );
+}
diff --git a/client/src/components/notificationBell.tsx b/client/src/components/notificationBell.tsx
new file mode 100644
index 0000000..064cfb6
--- /dev/null
+++ b/client/src/components/notificationBell.tsx
@@ -0,0 +1,12 @@
+import NotificationsIcon from "@mui/icons-material/Notifications";
+import { Badge, Tooltip } from "@mui/material";
+
+export default function NotificationBell() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/postListItem.tsx b/client/src/components/postListItem.tsx
new file mode 100644
index 0000000..b825d79
--- /dev/null
+++ b/client/src/components/postListItem.tsx
@@ -0,0 +1,135 @@
+import {
+ Button,
+ Card,
+ CardActionArea,
+ CardContent,
+ Grid,
+ ListItem,
+ styled,
+ Typography,
+} from "@mui/material";
+import { Post } from "../api";
+import MessageIcon from "@mui/icons-material/Message";
+import ThumbUpOffAltIcon from "@mui/icons-material/ThumbUpOffAlt";
+import ThumbUpAltIcon from "@mui/icons-material/ThumbUpAlt";
+import { useNavigate } from "react-router-dom";
+import { likePost, unLikePost } from "../app/postSlice";
+import { useAppDispatch } from "../app/store";
+import { useEffect, useState } from "react";
+import { useSelector } from "react-redux";
+import { selectUserInfo } from "../app/loginSlice";
+
+interface PostListItemProps {
+ post: Post;
+ postType: "all" | "user";
+}
+
+export default function PostListItem(props: PostListItemProps) {
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+ const userInfo = useSelector(selectUserInfo);
+ const [liked, setLiked] = useState(false);
+ const [numberOfLikes, setNumberOfLikes] = useState(0);
+
+ // On component mount, set the number of likes
+ // and whether the user has liked the post
+ useEffect(() => {
+ setNumberOfLikes(props.post.likedBy?.length || 0);
+ setLiked(props.post.likedBy?.includes(userInfo) || false);
+ }, []);
+
+ const handleLike = () => {
+ if (!liked) {
+ // Instant feedback to the user
+ setLiked(true);
+ setNumberOfLikes(numberOfLikes + 1);
+ // Dispatch the call to the API
+ dispatch(likePost(props.post.id as number));
+ }
+ // If the user has already liked the post, unlike it
+ else {
+ setLiked(false);
+ setNumberOfLikes(numberOfLikes - 1);
+
+ dispatch(unLikePost(props.post.id as number));
+ }
+ };
+
+ const handlePostClick = () => {
+ navigate(`/post/${props.post.id}`);
+ };
+ const handleUserClick = () => {
+ navigate(`/user/${props.post.createdBy?.id}`);
+ };
+
+ return (
+
+
+
+
+
+
+ {props.post.title}
+
+
+ {props.post.content}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const StyledGrid = styled(Grid)({
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ padding: "auto",
+ marginBottom: "0.8rem",
+ marginRight: "0.8rem",
+});
+const StyledTypography = styled(Typography)({
+ padding: "0 0.5rem",
+});
diff --git a/client/src/components/sideAppBar.tsx b/client/src/components/sideAppBar.tsx
new file mode 100644
index 0000000..9b095cc
--- /dev/null
+++ b/client/src/components/sideAppBar.tsx
@@ -0,0 +1,93 @@
+import {
+ Divider,
+ Drawer,
+ List,
+ ListItem,
+ ListItemButton,
+ ListItemIcon,
+ ListItemText,
+ Toolbar,
+} from "@mui/material";
+
+import FeedIcon from "@mui/icons-material/Feed";
+import GlobalIcon from "@mui/icons-material/Public";
+import AddIcon from "@mui/icons-material/Add";
+import SearchIcon from "@mui/icons-material/Search";
+import { Link, useNavigate } from "react-router-dom";
+
+export interface SideAppBarProps {
+ drawerWidth: number;
+}
+
+export default function SideAppBar(props: SideAppBarProps) {
+ const { drawerWidth } = props;
+ const navigate = useNavigate();
+
+ const handleClick = (to: string) => () => {
+ navigate(to);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/topAppBar.tsx b/client/src/components/topAppBar.tsx
new file mode 100644
index 0000000..4d8ced4
--- /dev/null
+++ b/client/src/components/topAppBar.tsx
@@ -0,0 +1,112 @@
+import * as React from "react";
+import AppBar from "@mui/material/AppBar";
+import Box from "@mui/material/Box";
+import Toolbar from "@mui/material/Toolbar";
+import IconButton from "@mui/material/IconButton";
+import Typography from "@mui/material/Typography";
+import Menu from "@mui/material/Menu";
+import Avatar from "@mui/material/Avatar";
+import Tooltip from "@mui/material/Tooltip";
+import MenuItem from "@mui/material/MenuItem";
+import NotificationBell from "./notificationBell";
+import {useSelector} from "react-redux";
+import { postLogout, selectUserInfo } from "../app/loginSlice";
+import { useAppDispatch } from "../app/store";
+
+interface TopAppBarProps {
+ height: number;
+}
+
+function TopAppBar(props: TopAppBarProps) {
+ const dispatch = useAppDispatch();
+ const userInfo = useSelector(selectUserInfo);
+ const [anchorElUser, setAnchorElUser] = React.useState(
+ null
+ );
+
+
+ const handleOpenUserMenu = (event: React.MouseEvent) => {
+ setAnchorElUser(event.currentTarget);
+ };
+
+ const handleCloseUserMenu = () => {
+ setAnchorElUser(null);
+ };
+
+ const handleLogout = () => {
+ dispatch(postLogout());
+ setAnchorElUser(null);
+ };
+
+ return (
+
+
+
+ DevSpace
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+export default TopAppBar;
diff --git a/client/src/error-page.tsx b/client/src/error-page.tsx
new file mode 100644
index 0000000..cfa7103
--- /dev/null
+++ b/client/src/error-page.tsx
@@ -0,0 +1,29 @@
+import {useRouteError} from "react-router-dom";
+import {Box, Typography} from "@mui/material";
+
+export default function ErrorPage() {
+ const error = useRouteError() as any;
+ console.error(error);
+
+ return (
+
+
+
+ Whoops!
+
+
+ Something went wrong :(
+
+
+
+ {error.message || error.statusText}
+
+
+
+ )
+}
diff --git a/client/src/index.tsx b/client/src/index.tsx
index 032464f..1d23b5f 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -1,15 +1,94 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
-import './index.css';
-import App from './App';
-import reportWebVitals from './reportWebVitals';
+import "./index.css";
+import reportWebVitals from "./reportWebVitals";
+import "@fontsource/roboto";
+import { createBrowserRouter, RouterProvider } from "react-router-dom";
+import Root from "./routes/root";
+import ErrorPage from "./error-page";
+import { createTheme, CssBaseline, ThemeProvider } from "@mui/material";
+import Login from "./routes/Auth/login";
+import Register from "./routes/Auth/register";
+import AuthRoot from "./routes/Auth/authRoot";
+import { Provider } from "react-redux";
+import { store } from "./app/store";
+import PostList from "./routes/postList";
+import Profile from "./routes/profile";
+import Post from "./routes/post";
+import NewPost from "./routes/newPost";
+import Search from "./routes/search";
const root = ReactDOM.createRoot(
- document.getElementById('root') as HTMLElement
+ document.getElementById("root") as HTMLElement
);
+
+const defaultTheme = createTheme({
+ palette: {
+ mode: "dark",
+ },
+});
+
+const router = createBrowserRouter([
+ {
+ path: "/",
+ element: ,
+ errorElement: ,
+ children: [
+ {
+ path: "feed",
+ element: ,
+ },
+ {
+ path: "global",
+ element: ,
+ },
+ {
+ path: "me",
+ element: ,
+ },
+ {
+ path: "user/:id",
+ element: ,
+ },
+ {
+ path: "post/:id",
+ element: ,
+ },
+ {
+ path: "search",
+ element: ,
+ },
+ {
+ path: "newpost",
+ element: ,
+ },
+ ],
+ },
+ {
+ path: "/auth",
+ element: ,
+ errorElement: ,
+ children: [
+ {
+ path: "login",
+ element: ,
+ },
+ {
+ path: "register",
+ element: ,
+ },
+ ],
+ },
+]);
+
root.render(
-
+
+
+
+
+
+
);
diff --git a/client/src/routes/Auth/authRoot.tsx b/client/src/routes/Auth/authRoot.tsx
new file mode 100644
index 0000000..6a45d52
--- /dev/null
+++ b/client/src/routes/Auth/authRoot.tsx
@@ -0,0 +1,54 @@
+import { Copyright } from "@mui/icons-material";
+import { Grid, Typography } from "@mui/material";
+import { Outlet, useNavigate } from "react-router-dom";
+import { StyledDivider } from "../../components/StyledComponents";
+import { useSelector } from "react-redux";
+import { selectLoggedIn } from "../../app/loginSlice";
+import { useEffect } from "react";
+
+export default function AuthRoot() {
+ const loggedIn = useSelector(selectLoggedIn);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (loggedIn) {
+ return navigate("/feed");
+ }
+ });
+
+ return (
+
+ theme.palette.mode === "light"
+ ? theme.palette.grey[100]
+ : theme.palette.grey[900],
+ }}
+ direction="column"
+ spacing={5}
+ >
+
+
+ DevSpace
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/routes/Auth/login.tsx b/client/src/routes/Auth/login.tsx
new file mode 100644
index 0000000..a7ef01a
--- /dev/null
+++ b/client/src/routes/Auth/login.tsx
@@ -0,0 +1,117 @@
+import {
+ Box,
+ Button,
+ Checkbox,
+ Container,
+ FormControlLabel,
+ Grid,
+ Paper,
+ TextField,
+ Typography,
+ styled,
+} from "@mui/material";
+import { StyledDivider } from "../../components/StyledComponents";
+import { Link } from "@mui/material";
+import { useAppDispatch } from "../../app/store";
+import { postLogin, selectErrorMessage } from "../../app/loginSlice";
+import { AuthLoginPostRequest } from "../../api";
+import { useSelector } from "react-redux";
+
+export default function Login() {
+ const dispatch = useAppDispatch();
+ const errorMessage = useSelector(selectErrorMessage);
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ const data = new FormData(event.currentTarget);
+ const paramsObject = {
+ email: data.get("email"),
+ password: data.get("password"),
+ longExpiration: data.get("longExpiration"),
+ };
+
+ dispatch(postLogin(paramsObject as unknown as AuthLoginPostRequest));
+ };
+ return (
+ <>
+
+
+
+
+
+ Email:
+
+
+
+ Password:
+
+
+
+ }
+ label="Remember me"
+ />
+
+
+
+
+
+
+
+
+
+ Don't have an account yet?
+
+
+
+
+ Register now!
+
+
+
+
+ >
+ );
+}
+
+const StyledGrid = styled(Grid)({
+ width: "50%",
+});
+
+const StyledTypography = styled(Typography)({
+ marginBottom: "5%",
+});
diff --git a/client/src/routes/Auth/register.tsx b/client/src/routes/Auth/register.tsx
new file mode 100644
index 0000000..fb52abd
--- /dev/null
+++ b/client/src/routes/Auth/register.tsx
@@ -0,0 +1,123 @@
+import {
+ Button,
+ Container,
+ Grid,
+ Link,
+ Paper,
+ TextField,
+ Typography,
+ styled,
+} from "@mui/material";
+import { useAppDispatch } from "../../app/store";
+import { postSignup } from "../../app/loginSlice";
+import { AuthSignupPostRequest } from "../../api";
+
+export default function Register() {
+ const dispatch = useAppDispatch();
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ const data = new FormData(event.currentTarget);
+ const paramsObject = {
+ email: data.get("email"),
+ password: data.get("password"),
+ passwordValidation: data.get("passwordConfirmation"),
+ firstName: data.get("firstName"),
+ lastName: data.get("lastName"),
+ longExpiration: true,
+ };
+ dispatch(postSignup(paramsObject as unknown as AuthSignupPostRequest));
+ };
+
+ return (
+ <>
+
+
+
+
+ First Name:
+
+
+
+ Last Name:
+
+
+
+ Email:
+
+
+
+ Password:
+
+
+
+ Password Confirmation:
+
+
+
+
+
+
+
+
+ Already have an account?
+
+
+
+
+ Login
+
+
+
+
+ >
+ );
+}
+
+const StyledGrid = styled(Grid)({
+ width: "50%",
+});
+const StyledTypography = styled(Typography)({
+ marginBottom: "5%",
+});
diff --git a/client/src/routes/newPost.tsx b/client/src/routes/newPost.tsx
new file mode 100644
index 0000000..46ac5ce
--- /dev/null
+++ b/client/src/routes/newPost.tsx
@@ -0,0 +1,3 @@
+export default function NewPost() {
+ return <>New Post>;
+}
diff --git a/client/src/routes/notification.tsx b/client/src/routes/notification.tsx
new file mode 100644
index 0000000..9e98dfc
--- /dev/null
+++ b/client/src/routes/notification.tsx
@@ -0,0 +1,3 @@
+export default function Notification() {
+ return <>Notification>;
+}
diff --git a/client/src/routes/post.tsx b/client/src/routes/post.tsx
new file mode 100644
index 0000000..b0a457c
--- /dev/null
+++ b/client/src/routes/post.tsx
@@ -0,0 +1,3 @@
+export default function Post() {
+ return <>Post>;
+}
diff --git a/client/src/routes/postList.tsx b/client/src/routes/postList.tsx
new file mode 100644
index 0000000..40306ee
--- /dev/null
+++ b/client/src/routes/postList.tsx
@@ -0,0 +1,73 @@
+import { useSelector } from "react-redux";
+import {
+ fetchFollowedPosts,
+ fetchGlobalPosts,
+ selectAllPosts,
+ selectFollowedPosts,
+} from "../app/postSlice";
+import { useAppDispatch } from "../app/store";
+import { useEffect } from "react";
+import { Box, List, Typography } from "@mui/material";
+import { Post } from "../api";
+import PostListItem from "../components/postListItem";
+
+interface PostListProps {
+ type: "all" | "user";
+}
+
+export default function PostList(props: PostListProps) {
+ const dispatch = useAppDispatch();
+ const followedPosts = useSelector(selectFollowedPosts);
+ const globalPosts = useSelector(selectAllPosts);
+
+ useEffect(() => {
+ if (props.type === "all") dispatch(fetchGlobalPosts());
+ if (props.type === "user") dispatch(fetchFollowedPosts());
+ }, []);
+
+ return (
+
+ {props.type === "all" && (
+
+ {globalPosts.map((post: Post) => (
+
+ ))}
+
+ )}
+ {props.type === "user" && (
+
+ {followedPosts.map((post: Post) => (
+
+ ))}
+ {followedPosts.length === 0 && (
+
+
+ Whops!
+
+
+ There is no content here! Try following some users, or check the
+ global feed
+
+
+ )}
+
+ )}
+
+ );
+}
diff --git a/client/src/routes/profile.tsx b/client/src/routes/profile.tsx
new file mode 100644
index 0000000..ba4f76f
--- /dev/null
+++ b/client/src/routes/profile.tsx
@@ -0,0 +1,12 @@
+import { useParams } from "react-router-dom";
+
+export interface ProfileProps {
+ selfProfile?: boolean;
+}
+
+export default function Profile(props: ProfileProps) {
+ const { selfProfile } = props;
+ const userId = useParams<{ userId: string }>().userId;
+
+ return <>Profile>;
+}
diff --git a/client/src/routes/root.tsx b/client/src/routes/root.tsx
new file mode 100644
index 0000000..b007b75
--- /dev/null
+++ b/client/src/routes/root.tsx
@@ -0,0 +1,64 @@
+import { Outlet, useNavigate } from "react-router-dom";
+import { Box } from "@mui/material";
+import { selectLoggedIn } from "../app/loginSlice";
+import { useSelector } from "react-redux";
+import { useEffect } from "react";
+import useWindowDimensions from "../app/hooks/useWindowDimensions";
+import SideAppBar from "../components/sideAppBar";
+import BottomAppBar from "../components/bottomAppBar";
+import TopAppBar from "../components/topAppBar";
+
+const drawerWidth = 240;
+const topAppBarHeight = 64;
+
+export default function Root() {
+ const navigate = useNavigate();
+ const loggedIn = useSelector(selectLoggedIn);
+ const { width } = useWindowDimensions();
+
+ useEffect(() => {
+ if (!loggedIn) {
+ navigate("/auth/login");
+ }
+ }, [loggedIn, navigate]);
+
+
+ return (
+ <>
+
+
+
+
+ {width > 600 && (
+
+
+
+ )}
+
+ 600 ? drawerWidth : 0}px)` },
+ marginTop: { xs: `${topAppBarHeight}px`},
+ }}
+ >
+
+
+
+ {width <= 600 && (
+
+
+
+ )}
+
+ >
+ );
+}
diff --git a/client/src/routes/search.tsx b/client/src/routes/search.tsx
new file mode 100644
index 0000000..935588d
--- /dev/null
+++ b/client/src/routes/search.tsx
@@ -0,0 +1,3 @@
+export default function Search() {
+ return <>Search>;
+}
diff --git a/client/src/util/types.ts b/client/src/util/types.ts
new file mode 100644
index 0000000..571409c
--- /dev/null
+++ b/client/src/util/types.ts
@@ -0,0 +1,6 @@
+export enum Status {
+ idle = "idle",
+ loading = "loading",
+ succeeded = "succeeded",
+ failed = "failed",
+}
diff --git a/client/tsconfig.json b/client/tsconfig.json
index a273b0c..9d379a3 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
- "lib": [
- "dom",
- "dom.iterable",
- "esnext"
- ],
+ "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@@ -20,7 +16,5 @@
"noEmit": true,
"jsx": "react-jsx"
},
- "include": [
- "src"
- ]
+ "include": ["src"]
}
diff --git a/docker-compose.yml b/docker-compose.yml
index b4445b6..31794b9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,6 +6,7 @@ services:
build: ./client
environment:
NODE_ENV: development
+ REACT_APP_BACKEND_URL: http://localhost:3000
ports:
- "8080:3000"
server:
diff --git a/openapitools.json b/openapitools.json
new file mode 100644
index 0000000..e73b975
--- /dev/null
+++ b/openapitools.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
+ "spaces": 2,
+ "generator-cli": {
+ "version": "7.2.0"
+ }
+}
diff --git a/server/package-lock.json b/server/package-lock.json
index 38db4ec..0d9dd72 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -10,21 +10,69 @@
"dependencies": {
"bcrypt": "^5.1.1",
"body-parser": "^1.19.1",
+ "cors": "^2.8.5",
"express": "^4.17.2",
"jsonwebtoken": "^9.0.2",
+ "ms": "^2.1.3",
"mysql": "^2.14.1",
"reflect-metadata": "^0.1.13",
+ "swagger-jsdoc": "^6.2.8",
+ "swagger-ui-express": "^5.0.0",
"typeorm": "0.3.20"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
+ "@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.5",
+ "@types/ms": "^0.7.34",
"@types/node": "^16.11.10",
+ "@types/swagger-jsdoc": "^6.0.4",
+ "@types/swagger-ui-express": "^4.1.6",
"ts-node": "10.9.1",
"typescript": "4.5.2"
}
},
+ "node_modules/@apidevtools/json-schema-ref-parser": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
+ "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
+ "dependencies": {
+ "@jsdevtools/ono": "^7.1.3",
+ "@types/json-schema": "^7.0.6",
+ "call-me-maybe": "^1.0.1",
+ "js-yaml": "^4.1.0"
+ }
+ },
+ "node_modules/@apidevtools/openapi-schemas": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
+ "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@apidevtools/swagger-methods": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
+ "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg=="
+ },
+ "node_modules/@apidevtools/swagger-parser": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
+ "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
+ "dependencies": {
+ "@apidevtools/json-schema-ref-parser": "^9.0.6",
+ "@apidevtools/openapi-schemas": "^2.0.4",
+ "@apidevtools/swagger-methods": "^3.0.2",
+ "@jsdevtools/ono": "^7.1.3",
+ "call-me-maybe": "^1.0.1",
+ "z-schema": "^5.0.1"
+ },
+ "peerDependencies": {
+ "openapi-types": ">=7"
+ }
+ },
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -78,6 +126,11 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "node_modules/@jsdevtools/ono": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
+ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="
+ },
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
@@ -163,6 +216,15 @@
"@types/node": "*"
}
},
+ "node_modules/@types/cors": {
+ "version": "2.8.17",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
+ "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/express": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
@@ -193,6 +255,11 @@
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true
},
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
+ },
"node_modules/@types/jsonwebtoken": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz",
@@ -253,6 +320,22 @@
"@types/node": "*"
}
},
+ "node_modules/@types/swagger-jsdoc": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz",
+ "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==",
+ "dev": true
+ },
+ "node_modules/@types/swagger-ui-express": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.6.tgz",
+ "integrity": "sha512-UVSiGYXa5IzdJJG3hrc86e8KdZWLYxyEsVoUI4iPXc7CO4VZ3AfNP8d/8+hrDRIqz+HAaSMtZSqAsF3Nq2X/Dg==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*",
+ "@types/serve-static": "*"
+ }
+ },
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -397,6 +480,11 @@
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"devOptional": true
},
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ },
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -527,6 +615,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/call-me-maybe": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
+ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -748,6 +841,14 @@
"color-support": "bin.js"
}
},
+ "node_modules/commander": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
+ "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -795,6 +896,18 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
@@ -884,6 +997,17 @@
"node": ">=0.3.1"
}
},
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/dotenv": {
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz",
@@ -939,6 +1063,14 @@
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -1423,6 +1555,17 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
@@ -1463,6 +1606,11 @@
"safe-buffer": "^5.0.1"
}
},
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
+ },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -1473,6 +1621,11 @@
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
+ },
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
@@ -1493,6 +1646,11 @@
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
+ "node_modules/lodash.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="
+ },
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@@ -1770,6 +1928,12 @@
"wrappy": "1"
}
},
+ "node_modules/openapi-types": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
+ "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
+ "peer": true
+ },
"node_modules/parse5": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
@@ -2264,6 +2428,94 @@
"node": ">=8"
}
},
+ "node_modules/swagger-jsdoc": {
+ "version": "6.2.8",
+ "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
+ "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
+ "dependencies": {
+ "commander": "6.2.0",
+ "doctrine": "3.0.0",
+ "glob": "7.1.6",
+ "lodash.mergewith": "^4.6.2",
+ "swagger-parser": "^10.0.3",
+ "yaml": "2.0.0-1"
+ },
+ "bin": {
+ "swagger-jsdoc": "bin/swagger-jsdoc.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/swagger-jsdoc/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/swagger-jsdoc/node_modules/glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/swagger-jsdoc/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/swagger-parser": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
+ "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
+ "dependencies": {
+ "@apidevtools/swagger-parser": "10.0.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/swagger-ui-dist": {
+ "version": "5.11.2",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz",
+ "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A=="
+ },
+ "node_modules/swagger-ui-express": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz",
+ "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==",
+ "dependencies": {
+ "swagger-ui-dist": ">=5.0.0"
+ },
+ "engines": {
+ "node": ">= v0.10.32"
+ },
+ "peerDependencies": {
+ "express": ">=4.0.0 || >=5.0.0-beta"
+ }
+ },
"node_modules/tar": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
@@ -2574,6 +2826,14 @@
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"devOptional": true
},
+ "node_modules/validator": {
+ "version": "13.11.0",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
+ "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -2754,6 +3014,14 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
+ "node_modules/yaml": {
+ "version": "2.0.0-1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
+ "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -2824,6 +3092,34 @@
"engines": {
"node": ">=6"
}
+ },
+ "node_modules/z-schema": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
+ "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
+ "dependencies": {
+ "lodash.get": "^4.4.2",
+ "lodash.isequal": "^4.5.0",
+ "validator": "^13.7.0"
+ },
+ "bin": {
+ "z-schema": "bin/z-schema"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "commander": "^9.4.1"
+ }
+ },
+ "node_modules/z-schema/node_modules/commander": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+ "optional": true,
+ "engines": {
+ "node": "^12.20.0 || >=14"
+ }
}
}
}
diff --git a/server/package.json b/server/package.json
index f1b34d5..a8124b8 100644
--- a/server/package.json
+++ b/server/package.json
@@ -5,21 +5,27 @@
"type": "commonjs",
"devDependencies": {
"@types/bcrypt": "^5.0.2",
+ "@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.5",
"@types/ms": "^0.7.34",
"@types/node": "^16.11.10",
+ "@types/swagger-jsdoc": "^6.0.4",
+ "@types/swagger-ui-express": "^4.1.6",
"ts-node": "10.9.1",
"typescript": "4.5.2"
},
"dependencies": {
"bcrypt": "^5.1.1",
"body-parser": "^1.19.1",
+ "cors": "^2.8.5",
"express": "^4.17.2",
"jsonwebtoken": "^9.0.2",
"ms": "^2.1.3",
"mysql": "^2.14.1",
"reflect-metadata": "^0.1.13",
+ "swagger-jsdoc": "^6.2.8",
+ "swagger-ui-express": "^5.0.0",
"typeorm": "0.3.20"
},
"scripts": {
diff --git a/server/src/controller/UserController.ts b/server/src/controller/UserController.ts
index 339a75b..0445823 100644
--- a/server/src/controller/UserController.ts
+++ b/server/src/controller/UserController.ts
@@ -1,53 +1,281 @@
import { AppDataSource } from "../data-source"
import { NextFunction, Request, Response } from "express"
import { User } from "../entity/User"
+import {AppError} from "../util/AppError";
+import {AppRequest} from "../util/AppRequest";
+import {Notification} from "../entity/Notification";
+import {catchAsync} from "../util/catchAsync";
export class UserController {
+ private userRepository = AppDataSource.getRepository(User);
- private userRepository = AppDataSource.getRepository(User)
+ private notificationRepository = AppDataSource.getRepository(Notification);
+ /**
+ * @swagger
+ * /users:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Users
+ * summary: Get all users
+ * responses:
+ * 200:
+ * description: A list of all users
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: '#/components/schemas/User'
+ */
+ public getAllUsers = catchAsync(
+ async (req: Request, res: Response, next: NextFunction) => {
+ const users = await this.userRepository.find();
- async all(request: Request, response: Response, next: NextFunction) {
- return this.userRepository.find()
+ // remove sensitive fields
+ users.forEach((user) => {
+ user.deleteSensitiveFields();
+ });
+
+ res.status(200).send(users);
}
+ );
- async one(request: Request, response: Response, next: NextFunction) {
- const id = parseInt(request.params.id)
+ /**
+ * @swagger
+ * /users/{id}:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Users
+ * summary: Get a single user
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * description: ID of the user
+ * schema:
+ * type: integer
+ * responses:
+ * 200:
+ * description: A single user
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/UserWithRelations'
+ */
+ public getUser = catchAsync(
+ async (req: Request, res: Response, next: NextFunction) => {
+ const id = req.params.id;
+ const parsedId = parseInt(id);
+ // Check if ID is a number
+ if (isNaN(parsedId)) return next(new AppError("Invalid ID", 400));
+ const user = await this.userRepository.findOne({
+ where: { id: parsedId },
+ relations: {
+ followed: true,
+ followers: true,
+ posts: true,
+ comments: true,
+ },
+ });
- const user = await this.userRepository.findOne({
- where: { id }
- })
+ if (!user) return next(new AppError("No user found with that ID", 404));
- if (!user) {
- return "unregistered user"
- }
- return user
+ // remove sensitive fields
+ user.deleteSensitiveFields();
+ user.followed.forEach((followedUser) =>
+ followedUser.deleteSensitiveFields()
+ );
+ user.followers.forEach((follower) => follower.deleteSensitiveFields());
+
+ return res.send(user);
}
+ );
- async save(request: Request, response: Response, next: NextFunction) {
- const { firstName, lastName, age } = request.body;
+ /**
+ * @swagger
+ * /users/me:
+ * get:
+ * tags:
+ * - Users
+ * summary: Get the currently logged in user
+ * security:
+ * - bearerAuth: []
+ * responses:
+ * 200:
+ * description: The currently logged in user
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/UserWithRelationsAndNotifications'
+ */
+ public getMe = catchAsync(
+ async (req: AppRequest, res: Response, next: NextFunction) => {
+ const user = await this.userRepository.findOne({
+ where: { id: req.user.id },
+ relations: {
+ followed: true,
+ followers: true,
+ posts: true,
+ comments: true,
+ notifications: true,
+ },
+ });
- const user = Object.assign(new User(), {
- firstName,
- lastName,
- age
- })
+ user.followed.forEach((followedUser) =>
+ followedUser.deleteSensitiveFields()
+ );
+ user.followers.forEach((follower) => follower.deleteSensitiveFields());
- return this.userRepository.save(user)
+ return res.status(200).send(user);
}
+ );
- async remove(request: Request, response: Response, next: NextFunction) {
- const id = parseInt(request.params.id)
+ /**
+ * @swagger
+ * /users/{id}/follow:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Users
+ * summary: Follow a user
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: integer
+ * description: The ID of the user to follow
+ * responses:
+ * 200:
+ * description: Successfully followed the user
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/UserWithRelations'
+ * 400:
+ * description: Invalid ID
+ * 404:
+ * description: No user found with that ID
+ */
+ public followUser = catchAsync(
+ async (req: AppRequest, res: Response, next: NextFunction) => {
+ const userToFollowId = req.params.id;
+ const parsedId = parseInt(userToFollowId);
+ // Check if ID is a number
+ if (isNaN(parsedId)) return next(new AppError("Invalid ID", 400));
- let userToRemove = await this.userRepository.findOneBy({ id })
+ const user = req.user;
+ const userToFollow = await this.userRepository.findOne({
+ where: { id: parsedId },
+ relations: { followed: true, followers: true, notifications: true },
+ });
- if (!userToRemove) {
- return "this user not exist"
- }
+ if (!userToFollow)
+ return next(new AppError("No user found with that ID", 404));
- await this.userRepository.remove(userToRemove)
+ // Check if user is already following
+ if (
+ user.followed.some(
+ (followedUser) => followedUser.id === userToFollow.id
+ )
+ ) {
+ return next(new AppError("You are already following this user", 400));
+ }
+ // Follow the user
+ user.followed.push(userToFollow);
+ await this.userRepository.save(user);
+ // Add the requesting user to the followers of the user being followed
+ userToFollow.followers.push(user);
+ // Create a notification for the user being followed
+ const followNotification = Object.assign(new Notification(), {
+ seen: false,
+ message: `${user.firstName} is now following you`,
+ timeStamp: new Date(),
+ });
+ userToFollow.notifications.push(
+ await this.notificationRepository.save(followNotification)
+ );
+ await this.userRepository.save(userToFollow);
- return "user has been removed"
+ return res.status(200).send({
+ status: "success",
+ message: `You are now following ${userToFollow.firstName}`,
+ });
}
+ );
+ /**
+ * @swagger
+ * /users/{id}/follow:
+ * delete:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Users
+ * summary: Unfollow a user
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * schema:
+ * type: integer
+ * description: The ID of the user to unfollow
+ * responses:
+ * 200:
+ * description: Successfully unfollowed the user
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/UserWithRelations'
+ * 400:
+ * description: Invalid ID
+ * 404:
+ * description: No user found with that ID
+ */
+ public unfollowUser = catchAsync(
+ async (req: AppRequest, res: Response, next: NextFunction) => {
+ const userToUnfollowId = req.params.id;
+ const parsedId = parseInt(userToUnfollowId);
+ // Check if ID is a number
+ if (isNaN(parsedId)) return next(new AppError("Invalid ID", 400));
+
+ const user = req.user;
+ const userToUnfollow = await this.userRepository.findOne({
+ where: { id: parsedId },
+ relations: { followed: true, followers: true },
+ });
+
+ if (!userToUnfollow)
+ return next(new AppError("No user found with that ID", 404));
+ // Check if user is following
+ if (
+ !user.followed.some(
+ (followedUser) => followedUser.id === userToUnfollow.id
+ )
+ ) {
+ return next(new AppError("You are not following this user", 400));
+ }
+ // Unfollow the user
+ user.followed = user.followed.filter(
+ (followedUser) => followedUser.id !== userToUnfollow.id
+ );
+ await this.userRepository.save(user);
+
+ userToUnfollow.followers = userToUnfollow.followers.filter(
+ (follower) => follower.id !== user.id
+ );
+ await this.userRepository.save(userToUnfollow);
+
+ return res.status(200).send({
+ status: "success",
+ message: `You are no longer following ${userToUnfollow.firstName}`,
+ });
+ }
+ );
}
\ No newline at end of file
diff --git a/server/src/controller/authController.ts b/server/src/controller/authController.ts
index 50af6dd..f01ead1 100644
--- a/server/src/controller/authController.ts
+++ b/server/src/controller/authController.ts
@@ -38,9 +38,48 @@ export class AuthController {
}
- public handleSignUp = catchAsync(async (req, res, next) => {
- const {email, password, passwordValidation, firstName, lastName} = req.body
- // Body Validation
+ /**
+ * @swagger
+ * /auth/signup:
+ * post:
+ * tags:
+ * - Authentication
+ * summary: Sign up a new user
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * email:
+ * type: string
+ * description: The email of the user
+ * password:
+ * type: string
+ * description: The password of the user
+ * passwordValidation:
+ * type: string
+ * description: Password validation field
+ * firstName:
+ * type: string
+ * description: The first name of the user
+ * lastName:
+ * type: string
+ * description: The last name of the user
+ * responses:
+ * 200:
+ * description: Successfully signed up the user
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/User'
+ * 400:
+ * description: Invalid input or user already exists
+ */
+ public handleSignUp = catchAsync(async (req, res, next) => {
+ const {email, password, passwordValidation, firstName, lastName} = req.body
+ // Body Validation
if (password != passwordValidation) {
return next(new AppError('Passwords do not match', 400))
@@ -64,11 +103,53 @@ export class AuthController {
this.sendToken(await this.userRepository.save(newUser), res)
})
- public handleLogin = catchAsync(async (req, res, next) => {
- const {email, password, longExpiration} = req.body
- if (!email || !password) {
- return next(new AppError('Please provide email and password', 400))
- }
+ /**
+ * @swagger
+ * /auth/login:
+ * post:
+ * tags:
+ * - Authentication
+ * summary: Log in a user
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * email:
+ * type: string
+ * description: The email of the user
+ * password:
+ * type: string
+ * description: The password of the user
+ * longExpiration:
+ * type: boolean
+ * description: Whether to keep the user logged in for a long time
+ * responses:
+ * 200:
+ * description: Successfully logged in the user
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * status:
+ * type: string
+ * example: "success"
+ * token:
+ * type: string
+ * description: The JWT token for the user
+ * 400:
+ * description: Missing email or password
+ * 401:
+ * description: Incorrect email or password
+ */
+ public handleLogin = catchAsync(async (req, res, next) => {
+ const { email, password, longExpiration } = req.body;
+ if (!email || !password) {
+ return next(new AppError("Please provide email and password", 400));
+ }
const user = await this.userRepository.findOne({where: {email}})
if (!user || !(await user.comparePassword(password))) {
@@ -79,14 +160,35 @@ export class AuthController {
this.sendToken(user, res, {long: longExpiration})
})
- public handleLogout = catchAsync(async (req: AppRequest, res, next) => {
- // Set the jwt cookie to a dummy value and set the expiration to a date in the past
- res.cookie('jwt', 'loggedout', {
- expires: new Date(Date.now() - 10000),
- httpOnly: true
- })
- res.status(200).json({status: 'success'})
- })
+ /**
+ * @swagger
+ * /auth/logout:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Authentication
+ * summary: Log out the user
+ * responses:
+ * 200:
+ * description: Successfully logged out the user
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * status:
+ * type: string
+ * example: "success"
+ */
+ public handleLogout = catchAsync(async (req: AppRequest, res, next) => {
+ // Set the jwt cookie to a dummy value and set the expiration to a date in the past
+ res.cookie("jwt", "loggedout", {
+ expires: new Date(Date.now() - 10000),
+ httpOnly: true,
+ });
+ res.status(200).json({ status: "success" });
+ });
public protect = catchAsync(async (req: AppRequest, res, next) => {
let token: string | undefined;
@@ -106,7 +208,10 @@ export class AuthController {
// Verify the token
const decoded = jwt.verify(token, this.jwt_secret) as JwtPayload
// Check if the user still exists
- const candidateUser = await this.userRepository.findOne({where: {id: decoded.id}})
+ const candidateUser = await this.userRepository.findOne({
+ where: {id: decoded.id},
+ relations: {followed: true, followers: true, notifications: true}
+ })
if(!candidateUser){
return next(new AppError('The user belonging to this token no longer exists', 401))
}
diff --git a/server/src/controller/postController.ts b/server/src/controller/postController.ts
new file mode 100644
index 0000000..b5293ed
--- /dev/null
+++ b/server/src/controller/postController.ts
@@ -0,0 +1,515 @@
+import {AppDataSource} from "../data-source";
+import {User} from "../entity/User";
+import {Post} from "../entity/Post";
+import {Comment} from "../entity/Comment";
+import {Notification} from "../entity/Notification";
+import {catchAsync} from "../util/catchAsync";
+import {AppError} from "../util/AppError";
+import {AppRequest} from "../util/AppRequest";
+
+export class PostController {
+ private postRepository = AppDataSource.getRepository(Post)
+
+ private commentRepository = AppDataSource.getRepository(Comment)
+
+ private notificationRepository = AppDataSource.getRepository(Notification)
+
+ private userRepository = AppDataSource.getRepository(User)
+
+ /**
+ * @swagger
+ * /posts:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Posts
+ * operationId: getAllPosts
+ * summary: Get all posts
+ * responses:
+ * 200:
+ * description: A list of all posts
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: '#/components/schemas/Post'
+ */
+ public getAllPosts = catchAsync(async (_req, res, _next) => {
+ const posts = await this.postRepository.find({
+ relations: { createdBy: true, likedBy: true, comments: {createdBy: true} },
+ });
+
+ // Remove sensitive fields
+ posts.forEach(post => {
+ post.deleteSensitiveFields()
+ })
+
+ res.status(200).send(posts);
+ });
+
+ /**
+ * @swagger
+ * /posts/{id}:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Posts
+ * operationId: getPost
+ * summary: Get a post by ID
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * description: ID of the post
+ * schema:
+ * type: integer
+ * responses:
+ * 200:
+ * description: A post
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/Post'
+ * 400:
+ * description: Invalid ID
+ * 404:
+ * description: No post found with that ID
+ */
+ public getPost = catchAsync(async (req, res, next) => {
+ const { post, errorMessage } = await this.validateRequestAndGetEntities(
+ req
+ );
+
+ if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400))
+ if(errorMessage == 'No post found with that ID'){
+ return next(new AppError('No post found with that ID', 404))
+ }
+
+ // Remove sensitive fields
+ post.deleteSensitiveFields()
+
+ res.send(post);
+ });
+
+ /**
+ * @swagger
+ * /posts/followed:
+ * get:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Posts
+ * operationId: getFollowedPosts
+ * summary: Get posts of followed users
+ * responses:
+ * 200:
+ * description: A list of posts
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * $ref: '#/components/schemas/Post'
+ */
+
+ public getFollowedPosts = catchAsync(async (req : AppRequest, res, _next) => {
+ const user = await this.userRepository.findOne({
+ where: { id: req.user.id },
+ relations: {
+ followed: true,
+ posts: { likedBy: true, comments: true },
+ },
+ });
+
+ const followedPosts = user.followed.map(followedUser => followedUser.posts).flat()
+
+ // Remove sensitive fields
+ followedPosts.forEach(post => {
+ post.deleteSensitiveFields()
+ })
+
+ res.send(followedPosts);
+ });
+
+ /**
+ * @swagger
+ * /posts:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Posts
+ * operationId: createPost
+ * summary: Create a post
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * title:
+ * type: string
+ * description: Post title
+ * content:
+ * type: string
+ * description: Post content
+ * responses:
+ * 201:
+ * description: A post
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/Post'
+ * 400:
+ * description: Title and content are required
+ */
+
+ public createPost = catchAsync(async (req : AppRequest, res, next) => {
+ const user = await this.userRepository.findOne({where: {id: req.user.id}})
+ const {title, content} = req.body
+
+ if(!title || !content) return next(new AppError('Title and content are required', 400))
+
+
+ const newPost = Object.assign(new Post(), {
+ title,
+ content,
+ createdBy: user,
+ createdAt: new Date()})
+
+ const post = await this.postRepository.save(newPost)
+
+ // Remove sensitive fields
+ post.deleteSensitiveFields()
+
+ res.status(201).send(post);
+ });
+
+ /**
+ * @swagger
+ * /posts/{id}:
+ * patch:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Posts
+ * operationId: updatePost
+ * summary: Update a post by ID
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * description: ID of the post
+ * schema:
+ * type: integer
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * title:
+ * type: string
+ * description: Post title
+ * content:
+ * type: string
+ * description: Post content
+ * responses:
+ * 200:
+ * description: A post
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/Post'
+ * 400:
+ * description: Invalid ID or Title and content are required
+ * 404:
+ * description: No post found with that ID
+ */
+
+ public updatePost = catchAsync(async (req : AppRequest, res, next) => {
+ const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req)
+ const {title, content} = req.body
+
+ if(!title || !content) return next(new AppError('Title and content are required', 400))
+
+ if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400))
+ if(errorMessage == 'No post found with that ID'){
+ return next(new AppError('No post found with that ID', 404))
+ }
+
+ if(post.createdBy.id !== user.id){
+ return next(new AppError('You are not authorized to update this post', 403))
+ }
+
+ post.title = title
+ post.content = content
+ await this.postRepository.save(post)
+
+ // Remove sensitive fields
+ post.deleteSensitiveFields()
+
+ res.status(200).send(post);
+ });
+
+ /**
+ * @swagger
+ * /posts/{id}:
+ * delete:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Posts
+ * operationId: deletePost
+ * summary: Delete a post by ID
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * description: ID of the post
+ * schema:
+ * type: integer
+ * responses:
+ * 204:
+ * description: No content
+ * 400:
+ * description: Invalid ID
+ * 404:
+ * description: No post found with that ID
+ */
+
+ public deletePost = catchAsync(async (req : AppRequest, res, next) => {
+ const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req)
+
+ if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400))
+ if(errorMessage == 'No post found with that ID') return next(new AppError('No post found with that ID', 404))
+
+ if(post.createdBy.id !== user.id){
+ return next(new AppError('You are not authorized to delete this post', 403))
+ }
+
+ await this.postRepository.remove(post);
+ res.status(204).send();
+ });
+
+ /**
+ * @swagger
+ * /posts/{id}/like:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Posts
+ * operationId: likePost
+ * summary: Like a post by ID
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * description: ID of the post
+ * schema:
+ * type: integer
+ * responses:
+ * 200:
+ * description: A post
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/Post'
+ * 400:
+ * description: Invalid ID or You have already liked this post
+ * 404:
+ * description: No post found with that ID
+ */
+
+ public likePost = catchAsync(async (req : AppRequest, res, next) => {
+ const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req)
+
+ if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400))
+ if(errorMessage == 'No post found with that ID'){
+ return next(new AppError('No post found with that ID', 404))
+ }
+
+ // Check if user has already liked the post
+ if(post.likedBy.some(likedUser => likedUser.id === user.id)){
+ return next(new AppError('You have already liked this post', 400))
+ }
+
+ post.likedBy.push(user)
+ await this.postRepository.save(post)
+
+ // Send notification to post creator
+ const newNotification = Object.assign(new Notification(),{
+ message: `${user.firstName} ${user.lastName} liked your post`,
+ belongsTo: post.createdBy,
+ timeStamp: new Date()
+ })
+ const notification = await this.notificationRepository.save(newNotification)
+ post.createdBy.notifications.push(notification)
+ await this.userRepository.save(post.createdBy)
+
+ // Remove sensitive fields
+ post.deleteSensitiveFields()
+
+ res.status(200).send(post);
+ });
+
+ /**
+ * @swagger
+ * /posts/{id}/like:
+ * delete:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Posts
+ * operationId: unlikePost
+ * summary: Unlike a post by ID
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * description: ID of the post
+ * schema:
+ * type: integer
+ * responses:
+ * 200:
+ * description: A post
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/Post'
+ * 400:
+ * description: Invalid ID or You have not liked this post
+ * 404:
+ * description: No post found with that ID
+ */
+
+ public unlikePost = catchAsync(async (req : AppRequest, res, next) => {
+ const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req)
+
+ if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400))
+ if(errorMessage == 'No post found with that ID') return next(new AppError('No post found with that ID', 404))
+
+ // Check if user likes the post
+ if(!post.likedBy.some(likedUser => likedUser.id === user.id)){
+ return next(new AppError('You have not liked this post', 400))
+ }
+
+ post.likedBy = post.likedBy.filter(likedUser => likedUser.id !== user.id)
+ await this.postRepository.save(post)
+
+ // Remove sensitive fields
+ post.deleteSensitiveFields()
+
+ res.status(200).send(post);
+ });
+
+ /**
+ * @swagger
+ * /posts/{id}/comment:
+ * post:
+ * security:
+ * - bearerAuth: []
+ * tags:
+ * - Posts
+ * operationId: commentPost
+ * summary: Comment on a post by ID
+ * parameters:
+ * - in: path
+ * name: id
+ * required: true
+ * description: ID of the post
+ * schema:
+ * type: integer
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * content:
+ * type: string
+ * description: Comment content
+ * responses:
+ * 201:
+ * description: A comment
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/Comment'
+ * 400:
+ * description: Invalid Request
+ * 404:
+ * description: No post found with that ID
+ */
+
+ public commentPost = catchAsync(async (req : AppRequest, res, next) => {
+ const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req)
+
+ if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400))
+ if(errorMessage == 'No post found with that ID') {
+ return next(new AppError('No post found with that ID', 404))
+ }
+
+ const {content} = req.body
+ const newComment = Object.assign(new Comment(), {
+ content,
+ createdBy: user,
+ post,
+ createdAt: new Date()
+ })
+
+ const comment = await this.commentRepository.save(newComment)
+
+ // Send notification to post creator
+ const newNotification = Object.assign(new Notification(),{
+ message: `${user.firstName} ${user.lastName} commented on your post`,
+ belongsTo: post.createdBy,
+ timeStamp: new Date()
+ })
+ await this.notificationRepository.save(newNotification)
+
+ // Remove sensitive fields
+ comment.createdBy.deleteSensitiveFields()
+ post.deleteSensitiveFields()
+
+ res.status(201).send(comment)
+ })
+
+
+ private validateRequestAndGetEntities = async (req : AppRequest,) : Promise => {
+ const postId = req.params.id
+ const parsedId = parseInt(postId)
+ let errorMessage: 'Invalid ID' | 'No post found with that ID'
+
+ // Check if ID is a number
+ if(isNaN(parsedId)) errorMessage = 'Invalid ID'
+
+ const user = req.user
+ const post = await this.postRepository.findOne({
+ where: {id: parsedId},
+ relations: {
+ likedBy: true,
+ comments: { createdBy: true },
+ createdBy: {notifications: true}
+ }
+ })
+
+ // Check if post exists
+ if(!post) errorMessage = 'No post found with that ID'
+
+ return {user, post, errorMessage}
+
+ }
+}
+interface validatedEntities {
+ user: User
+ post: Post
+ errorMessage?: 'Invalid ID' | 'No post found with that ID'
+}
diff --git a/server/src/data-source.ts b/server/src/data-source.ts
index 879c428..6d37e96 100644
--- a/server/src/data-source.ts
+++ b/server/src/data-source.ts
@@ -3,6 +3,7 @@ import { DataSource } from "typeorm"
import { User } from "./entity/User"
import {Comment} from "./entity/Comment";
import {Post} from "./entity/Post";
+import {Notification} from "./entity/Notification";
export const AppDataSource = new DataSource({
type: "mysql",
@@ -13,7 +14,7 @@ export const AppDataSource = new DataSource({
database: process.env.MYSQL_DATABASE,
synchronize: true,
logging: false,
- entities: [User, Comment, Post],
+ entities: [User, Comment, Post, Notification],
migrations: [],
subscribers: [],
})
diff --git a/server/src/entity/Comment.ts b/server/src/entity/Comment.ts
index c7bd23f..2e6ccf0 100644
--- a/server/src/entity/Comment.ts
+++ b/server/src/entity/Comment.ts
@@ -1,5 +1,6 @@
import {Column, Entity, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn} from "typeorm";
import {User} from "./User";
+import {Post} from "./Post";
@Entity()
export class Comment {
@@ -12,9 +13,12 @@ export class Comment {
@Column()
createdAt: Date
- @ManyToOne(() => User, user => user.posts)
+ @ManyToOne(() => User, user => user.comments)
createdBy: User
+ @ManyToOne(() => Post, post => post.comments)
+ post: Post
+
@ManyToMany(()=> User)
@JoinTable()
likedBy: User[]
diff --git a/server/src/entity/Notification.ts b/server/src/entity/Notification.ts
new file mode 100644
index 0000000..8e06d47
--- /dev/null
+++ b/server/src/entity/Notification.ts
@@ -0,0 +1,21 @@
+import {Column, Entity, ManyToOne, PrimaryGeneratedColumn} from "typeorm";
+import {User} from "./User";
+
+@Entity()
+export class Notification {
+
+ @PrimaryGeneratedColumn()
+ id: number
+
+ @Column()
+ message: string
+
+ @Column()
+ timeStamp: Date
+
+ @Column({default: false})
+ seen: boolean
+
+ @ManyToOne(type => User, user => user.notifications)
+ belongsTo: User
+}
\ No newline at end of file
diff --git a/server/src/entity/Post.ts b/server/src/entity/Post.ts
index f9a9847..9b7eb14 100644
--- a/server/src/entity/Post.ts
+++ b/server/src/entity/Post.ts
@@ -1,5 +1,6 @@
-import {Column, Entity, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn} from "typeorm";
+import {Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn} from "typeorm";
import {User} from "./User";
+import {Comment} from "./Comment";
@Entity()
export class Post {
@@ -18,8 +19,22 @@ export class Post {
@ManyToOne(() => User, user => user.posts)
createdBy: User
+ @OneToMany(() => Comment, comment => comment.post)
+ comments: Comment[]
+
@ManyToMany(()=> User)
@JoinTable()
likedBy: User[]
+ public deleteSensitiveFields(){
+ this.createdBy.deleteSensitiveFields()
+ if(this.likedBy.length > 0){
+ this.likedBy.forEach(user => user.deleteSensitiveFields())
+ }
+ if(this.comments.length > 0){
+ this.comments.forEach(comment => comment.createdBy.deleteSensitiveFields())
+ }
+
+ }
+
}
\ No newline at end of file
diff --git a/server/src/entity/User.ts b/server/src/entity/User.ts
index 2927f71..d2b9705 100644
--- a/server/src/entity/User.ts
+++ b/server/src/entity/User.ts
@@ -2,6 +2,7 @@ import {Entity, PrimaryGeneratedColumn, Column, ManyToMany, OneToMany, JoinTable
import * as bcrypt from "bcrypt"
import {Post} from "./Post";
import {Comment} from "./Comment";
+import {Notification} from "./Notification";
@Entity()
export class User {
@@ -25,12 +26,19 @@ export class User {
@JoinTable()
followed: User[]
+ @ManyToMany(type => User)
+ @JoinTable()
+ followers: User[]
+
@OneToMany(type => Post, post=> post.createdBy)
posts: Post[]
@OneToMany(type => Comment, comment=> comment.createdBy)
comments: Comment[]
+ @OneToMany(type => Notification, notification => notification.belongsTo)
+ notifications: Notification[]
+
static async hashPassword(password: string){
return await bcrypt.hash(password, parseInt(process.env.SALT_ROUNDS))
@@ -39,4 +47,12 @@ export class User {
async comparePassword(password: string){
return await bcrypt.compare(password, this.password)
}
+
+ public deleteSensitiveFields(){
+ delete this.password
+ delete this.email
+ delete this.notifications
+ delete this.followed
+ delete this.followers
+ }
}
diff --git a/server/src/index.ts b/server/src/index.ts
index 71bccab..d7d355f 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -3,24 +3,40 @@ import * as bodyParser from "body-parser"
import { AppDataSource } from "./data-source"
import {errorHandler} from "./controller/errorController";
import {AuthRoutes} from "./routes/authRoutes";
+import {UserRoutes} from "./routes/userRoutes";
+import {AuthController} from "./controller/authController";
+import {PostRoutes} from "./routes/postRoutes";
+import {swaggerRouter} from "./routes/swaggerRoutes";
+import * as cors from "cors";
-AppDataSource.initialize().then(async () => {
-
+AppDataSource.initialize()
+ .then(async () => {
// create express app
- const app = express()
- app.use(bodyParser.json())
+ const app = express();
+ app.use(bodyParser.json());
+ app.use(cors());
// register express routes from defined application routes
// Auth Routes
- app.use('/auth', AuthRoutes)
+ app.use("/auth", AuthRoutes);
+
+ // Swagger Routes
+ app.use("/docs", swaggerRouter);
+
+ // All routes after this one require authentication
+ const authController = new AuthController();
+ app.use(authController.protect);
+
+ app.use("/users", UserRoutes);
+ app.use("/posts", PostRoutes);
// setup express app here
- app.use(errorHandler)
+ app.use(errorHandler);
// start express server
- app.listen(3000)
+ app.listen(3000);
-
-
- console.log("Express server has started on port 3000. Open http://localhost:3000/users to see results")
-
-}).catch(error => console.log(error))
+ console.log(
+ "Express server has started on port 3000. Open http://localhost:3000/users to see results"
+ );
+ })
+ .catch((error) => console.log(error));
diff --git a/server/src/routes/authRoutes.ts b/server/src/routes/authRoutes.ts
index 7762739..2731ad7 100644
--- a/server/src/routes/authRoutes.ts
+++ b/server/src/routes/authRoutes.ts
@@ -7,6 +7,7 @@ export const AuthRoutes = Router();
AuthRoutes.route("/signup").post(authController.handleSignUp)
AuthRoutes.route("/login").post(authController.handleLogin)
+AuthRoutes.route("/logout").get(authController.handleLogout)
diff --git a/server/src/routes/postRoutes.ts b/server/src/routes/postRoutes.ts
new file mode 100644
index 0000000..47ed5d0
--- /dev/null
+++ b/server/src/routes/postRoutes.ts
@@ -0,0 +1,19 @@
+import {PostController} from "../controller/postController";
+import {Router} from "express";
+
+const postController = new PostController()
+
+export const PostRoutes = Router();
+
+PostRoutes.route("/")
+ .get(postController.getAllPosts)
+ .post(postController.createPost)
+PostRoutes.route("/followed").get(postController.getFollowedPosts)
+PostRoutes.route("/:id")
+ .get(postController.getPost)
+ .patch(postController.updatePost)
+ .delete(postController.deletePost)
+PostRoutes.route("/:id/like")
+ .post(postController.likePost)
+ .delete(postController.unlikePost)
+PostRoutes.route("/:id/comment").post(postController.commentPost)
\ No newline at end of file
diff --git a/server/src/routes/swaggerRoutes.ts b/server/src/routes/swaggerRoutes.ts
new file mode 100644
index 0000000..152055e
--- /dev/null
+++ b/server/src/routes/swaggerRoutes.ts
@@ -0,0 +1,194 @@
+import * as swaggerJSdoc from "swagger-jsdoc";
+import {Router} from "express";
+import * as swaggerUi from "swagger-ui-express";
+import { catchAsync } from "../util/catchAsync";
+
+const swaggerOptions = {
+ swaggerDefinition: {
+ openapi: "3.0.0",
+ info: {
+ title: "DevSpace API",
+ description: "API for DevSpace",
+ version: "0.1.0",
+ },
+
+ components: {
+ securitySchemes: {
+ bearerAuth: {
+ type: "http",
+ scheme: "bearer",
+ bearerFormat: "JWT",
+ },
+ },
+ schemas: {
+ Post: {
+ type: "object",
+ properties: {
+ id: {
+ type: "integer",
+ description: "Post ID",
+ },
+ title: {
+ type: "string",
+ description: "Post title",
+ },
+ content: {
+ type: "string",
+ description: "Post content",
+ },
+ createdAt: {
+ type: "string",
+ description: "Post creation date",
+ },
+ createdBy: {
+ $ref: "#/components/schemas/User",
+ },
+ likedBy: {
+ type: "array",
+ items: {
+ $ref: "#/components/schemas/User",
+ },
+ },
+ comments: {
+ type: "array",
+ items: {
+ $ref: "#/components/schemas/Comment",
+ },
+ },
+ },
+ },
+ Comment: {
+ type: "object",
+ properties: {
+ id: {
+ type: "integer",
+ description: "Comment ID",
+ },
+ content: {
+ type: "string",
+ description: "Comment content",
+ },
+ createdAt: {
+ type: "string",
+ description: "Comment creation date",
+ },
+ createdBy: {
+ $ref: "#/components/schemas/User",
+ },
+ likedBy: {
+ type: "array",
+ items: {
+ $ref: "#/components/schemas/User",
+ },
+ },
+ },
+ },
+ Notification: {
+ type: "object",
+ properties: {
+ id: {
+ type: "integer",
+ description: "Notification ID",
+ },
+ message: {
+ type: "string",
+ description: "Notification message",
+ },
+ timeStamp: {
+ type: "string",
+ description: "Notification creation date",
+ },
+ seen: {
+ type: "boolean",
+ description: "Notification seen status",
+ },
+ },
+ },
+ User: {
+ type: "object",
+ properties: {
+ id: {
+ type: "integer",
+ description: "User ID",
+ },
+ firstName: {
+ type: "string",
+ description: "User first name",
+ },
+ lastName: {
+ type: "string",
+ description: "User last name",
+ },
+ },
+ },
+ UserWithRelations: {
+ allOf: [
+ {
+ $ref: "#/components/schemas/User",
+ },
+ {
+ type: "object",
+ properties: {
+ posts: {
+ type: "array",
+ items: {
+ $ref: "#/components/schemas/Post",
+ },
+ },
+ comments: {
+ type: "array",
+ items: {
+ $ref: "#/components/schemas/Comment", // Assuming you have a Comment schema
+ },
+ },
+ followed: {
+ type: "array",
+ items: {
+ $ref: "#/components/schemas/User",
+ },
+ },
+ followers: {
+ type: "array",
+ items: {
+ $ref: "#/components/schemas/User",
+ },
+ },
+ },
+ },
+ ],
+ },
+ UserWithRelationsAndNotifications: {
+ allOf: [
+ { $ref: "#/components/schemas/UserWithRelations" },
+ {
+ type: "object",
+ properties: {
+ notifications: {
+ type: "array",
+ items: {
+ $ref: "#/components/schemas/Notification",
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ apis: ["**/controller/*.ts"],
+};
+
+const swaggerDocs = swaggerJSdoc(swaggerOptions);
+
+
+console.log(swaggerDocs)
+export const swaggerRouter = Router()
+
+
+swaggerRouter.get(
+ "/json", catchAsync(async (req, res, next) => {
+ res.json(swaggerDocs);
+ })
+);
+swaggerRouter.use("/", swaggerUi.serve, swaggerUi.setup(swaggerDocs));
\ No newline at end of file
diff --git a/server/src/routes/userRoutes.ts b/server/src/routes/userRoutes.ts
new file mode 100644
index 0000000..c172de0
--- /dev/null
+++ b/server/src/routes/userRoutes.ts
@@ -0,0 +1,13 @@
+import {Router} from "express";
+import {UserController} from "../controller/UserController";
+
+export const UserRoutes = Router();
+
+const userController = new UserController()
+
+UserRoutes.route("/").get(userController.getAllUsers)
+UserRoutes.route("/me").get(userController.getMe)
+UserRoutes.route("/:id").get(userController.getUser)
+UserRoutes.route("/follow/:id")
+ .post(userController.followUser)
+ .delete(userController.unfollowUser)
\ No newline at end of file