Reauth
This commit is contained in:
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+3
-3
@@ -5,10 +5,10 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>Vite + React + TS</title>
|
||||||
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-CfSUaRjT.js"></script>
|
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-BliQ0crd.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C_FdcE2X.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C_FdcE2X.js">
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-C4H8cxTH.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-53GMXZgr.js">
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-C0csOcmc.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-ZGp-Rrdw.js">
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-DyW0LrNj.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-DyW0LrNj.js">
|
||||||
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css">
|
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css">
|
||||||
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/index-D83Ey19k.css">
|
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/index-D83Ey19k.css">
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
"FailedUpdate_Duplicate:username:userUpdate": "Ein Benutzer mit diesem Benutzernamen existiert schon",
|
"FailedUpdate_Duplicate:username:userUpdate": "Ein Benutzer mit diesem Benutzernamen existiert schon",
|
||||||
"FailedUpdate_Duplicate:email:userUpdate": "Ein Benutzer mit dieser E-Mail existiert schon",
|
"FailedUpdate_Duplicate:email:userUpdate": "Ein Benutzer mit dieser E-Mail existiert schon",
|
||||||
|
|
||||||
"NotAllowed_newPost": "Keine Berechtigung",
|
"Unauthorized_newPost": "Keine Berechtigung",
|
||||||
"MissingField_content:newPost": "Beitrag darf nicht leer sein",
|
"MissingField_content:newPost": "Beitrag darf nicht leer sein",
|
||||||
|
|
||||||
"NotAllowed_postUpdate": "Keine Berechtigung",
|
"NotAllowed_postUpdate": "Keine Berechtigung",
|
||||||
|
|||||||
+5
-5
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"Unauthorized": "NotAllowed",
|
"Unauthorized": "Not allowed",
|
||||||
|
|
||||||
"NotAllowed_login": "Invalid email or password",
|
"NotAllowed_login": "Invalid email or password",
|
||||||
"NotFound_user:login": "User does not exist",
|
"NotFound_user:login": "User does not exist",
|
||||||
"MissingField_email:login": "E-Mail required",
|
"MissingField_email:login": "E-Mail required",
|
||||||
"MissingField_password:login": "Password required",
|
"MissingField_password:login": "Password required",
|
||||||
|
|
||||||
"NotAllowed_deletPost": "NotAllowed",
|
"NotAllowed_deletPost": "Not allowed",
|
||||||
"NotFound_post:deletePost": "Post not found",
|
"NotFound_post:deletePost": "Post not found",
|
||||||
|
|
||||||
"NotAllowed_userUpdate": "NotAllowed",
|
"NotAllowed_userUpdate": "Not allowed",
|
||||||
"NotFound_user:userUpdate": "User not found",
|
"NotFound_user:userUpdate": "User not found",
|
||||||
"FailedUpdate_Duplicate:userUpdate": "A user with this username already exists",
|
"FailedUpdate_Duplicate:userUpdate": "A user with this username already exists",
|
||||||
"FailedUpdate_Duplicate:email:userUpdate": "A user with this email already exists",
|
"FailedUpdate_Duplicate:email:userUpdate": "A user with this email already exists",
|
||||||
|
|
||||||
"NotAllowed_newPost": "NotAllowed",
|
"Unauthorized_newPost": "Not allowed",
|
||||||
"MissingField_content:newPost": "Content required",
|
"MissingField_content:newPost": "Content required",
|
||||||
|
|
||||||
"NotAllowed_postUpdate": "NotAllowed",
|
"NotAllowed_postUpdate": "Not allowed",
|
||||||
"NotFound_post:postUpdate": "Post not found",
|
"NotFound_post:postUpdate": "Post not found",
|
||||||
|
|
||||||
"Duplicate_user:register": "A user with this username or email already exists",
|
"Duplicate_user:register": "A user with this username or email already exists",
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -12,5 +12,7 @@ module.exports = {
|
|||||||
plugins: ['react-refresh'],
|
plugins: ['react-refresh'],
|
||||||
rules: {
|
rules: {
|
||||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||||
|
'no-shadow': 'off',
|
||||||
|
'@typescript-eslint/no-shadow': 'error',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-i18next": "^15.0.0",
|
"react-i18next": "^15.0.0",
|
||||||
|
"use-local-storage-state": "^19.3.1",
|
||||||
"zustand": "^4.5.4"
|
"zustand": "^4.5.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Generated
+18
-3
@@ -32,9 +32,6 @@ importers:
|
|||||||
'@tanstack/react-router':
|
'@tanstack/react-router':
|
||||||
specifier: ^1.45.8
|
specifier: ^1.45.8
|
||||||
version: 1.45.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 1.45.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
'@types/node':
|
|
||||||
specifier: ^20.14.12
|
|
||||||
version: 20.14.12
|
|
||||||
i18next:
|
i18next:
|
||||||
specifier: ^23.12.2
|
specifier: ^23.12.2
|
||||||
version: 23.12.2
|
version: 23.12.2
|
||||||
@@ -53,6 +50,9 @@ importers:
|
|||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: ^15.0.0
|
specifier: ^15.0.0
|
||||||
version: 15.0.0(i18next@23.12.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 15.0.0(i18next@23.12.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
use-local-storage-state:
|
||||||
|
specifier: ^19.3.1
|
||||||
|
version: 19.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
zustand:
|
zustand:
|
||||||
specifier: ^4.5.4
|
specifier: ^4.5.4
|
||||||
version: 4.5.4(@types/react@18.3.3)(react@18.3.1)
|
version: 4.5.4(@types/react@18.3.3)(react@18.3.1)
|
||||||
@@ -72,6 +72,9 @@ importers:
|
|||||||
'@tanstack/router-plugin':
|
'@tanstack/router-plugin':
|
||||||
specifier: ^1.45.8
|
specifier: ^1.45.8
|
||||||
version: 1.45.8(vite@5.3.4(@types/node@20.14.12))
|
version: 1.45.8(vite@5.3.4(@types/node@20.14.12))
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^20.14.12
|
||||||
|
version: 20.14.12
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^18.3.3
|
specifier: ^18.3.3
|
||||||
version: 18.3.3
|
version: 18.3.3
|
||||||
@@ -1842,6 +1845,13 @@ packages:
|
|||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
use-local-storage-state@19.3.1:
|
||||||
|
resolution: {integrity: sha512-y3Z1dODXvZXZB4qtLDNN8iuXbsYD6TAxz61K58GWB9/yKwrNG9ynI0GzCTHi/Je1rMiyOwMimz0oyFsZn+Kj7Q==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=18'
|
||||||
|
react-dom: '>=18'
|
||||||
|
|
||||||
use-sync-external-store@1.2.0:
|
use-sync-external-store@1.2.0:
|
||||||
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3721,6 +3731,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
use-local-storage-state@19.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
use-sync-external-store@1.2.0(react@18.3.1):
|
use-sync-external-store@1.2.0(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"FailedUpdate_Duplicate:username:userUpdate": "Ein Benutzer mit diesem Benutzernamen existiert schon",
|
"FailedUpdate_Duplicate:username:userUpdate": "Ein Benutzer mit diesem Benutzernamen existiert schon",
|
||||||
"FailedUpdate_Duplicate:email:userUpdate": "Ein Benutzer mit dieser E-Mail existiert schon",
|
"FailedUpdate_Duplicate:email:userUpdate": "Ein Benutzer mit dieser E-Mail existiert schon",
|
||||||
|
|
||||||
"NotAllowed_newPost": "Keine Berechtigung",
|
"Unauthorized_newPost": "Keine Berechtigung",
|
||||||
"MissingField_content:newPost": "Beitrag darf nicht leer sein",
|
"MissingField_content:newPost": "Beitrag darf nicht leer sein",
|
||||||
|
|
||||||
"NotAllowed_postUpdate": "Keine Berechtigung",
|
"NotAllowed_postUpdate": "Keine Berechtigung",
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"Unauthorized": "NotAllowed",
|
"Unauthorized": "Not allowed",
|
||||||
|
|
||||||
"NotAllowed_login": "Invalid email or password",
|
"NotAllowed_login": "Invalid email or password",
|
||||||
"NotFound_user:login": "User does not exist",
|
"NotFound_user:login": "User does not exist",
|
||||||
"MissingField_email:login": "E-Mail required",
|
"MissingField_email:login": "E-Mail required",
|
||||||
"MissingField_password:login": "Password required",
|
"MissingField_password:login": "Password required",
|
||||||
|
|
||||||
"NotAllowed_deletPost": "NotAllowed",
|
"NotAllowed_deletPost": "Not allowed",
|
||||||
"NotFound_post:deletePost": "Post not found",
|
"NotFound_post:deletePost": "Post not found",
|
||||||
|
|
||||||
"NotAllowed_userUpdate": "NotAllowed",
|
"NotAllowed_userUpdate": "Not allowed",
|
||||||
"NotFound_user:userUpdate": "User not found",
|
"NotFound_user:userUpdate": "User not found",
|
||||||
"FailedUpdate_Duplicate:userUpdate": "A user with this username already exists",
|
"FailedUpdate_Duplicate:userUpdate": "A user with this username already exists",
|
||||||
"FailedUpdate_Duplicate:email:userUpdate": "A user with this email already exists",
|
"FailedUpdate_Duplicate:email:userUpdate": "A user with this email already exists",
|
||||||
|
|
||||||
"NotAllowed_newPost": "NotAllowed",
|
"Unauthorized_newPost": "Not allowed",
|
||||||
"MissingField_content:newPost": "Content required",
|
"MissingField_content:newPost": "Content required",
|
||||||
|
|
||||||
"NotAllowed_postUpdate": "NotAllowed",
|
"NotAllowed_postUpdate": "Not allowed",
|
||||||
"NotFound_post:postUpdate": "Post not found",
|
"NotFound_post:postUpdate": "Post not found",
|
||||||
|
|
||||||
"Duplicate_user:register": "A user with this username or email already exists",
|
"Duplicate_user:register": "A user with this username or email already exists",
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
import { POST_LIMIT, PROFILE_POST_LIMIT } from '../constanst';
|
|
||||||
import { PostAuth, PostCreate, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
|
|
||||||
import { User, UserCreate, UserImageUpdate, UserUpdate } from '../types/User';
|
|
||||||
|
|
||||||
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
|
||||||
|
|
||||||
let instance: ApiImpl;
|
|
||||||
|
|
||||||
class ApiImpl {
|
|
||||||
private token?: string;
|
|
||||||
private refreshToken?: string;
|
|
||||||
private self?: User;
|
|
||||||
private userListeners: ((user?: User) => void)[] = [];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
if (instance) {
|
|
||||||
throw new Error('New instance cannot be created!!');
|
|
||||||
}
|
|
||||||
|
|
||||||
//eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
||||||
instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasAuth = () => this.token !== undefined;
|
|
||||||
//FIXME: TESTING
|
|
||||||
public isAdmin = () => this.hasAuth() && this.self?.isAdmin;
|
|
||||||
public getAuthenticatedUser = () => this.self;
|
|
||||||
public getCurrentSession = () => [this.token, this.refreshToken];
|
|
||||||
|
|
||||||
public subscribeToAuthenticatedUser = (callback: (user?: User) => void) => {
|
|
||||||
this.userListeners.push(callback);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
this.userListeners = this.userListeners.filter((item) => item !== callback);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
public logIn = async (email: string, password: string): Promise<void> => {
|
|
||||||
const { user, token } = await (await this.post('login', { email, password })).json();
|
|
||||||
this.self = user;
|
|
||||||
this.token = token;
|
|
||||||
this.userListeners.forEach((listener) => listener(user));
|
|
||||||
};
|
|
||||||
|
|
||||||
public logOut = async (): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
if (this.token) return await (await this.postAuth('logout')).json();
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
this.self = undefined;
|
|
||||||
this.token = undefined;
|
|
||||||
this.userListeners.forEach((listener) => listener());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public posts = async (page?: number): Promise<PostListNonAuth | PostListAuth> => {
|
|
||||||
const url = `posts?p=${page ?? 0}&l=${POST_LIMIT}`;
|
|
||||||
|
|
||||||
if (this.token) return await (await this.getAuth(url)).json();
|
|
||||||
|
|
||||||
return await (await this.get(url)).json();
|
|
||||||
};
|
|
||||||
|
|
||||||
public deletePost = async (id: number): Promise<PostDelete> => {
|
|
||||||
return await (await this.delete(`posts/${id}?l=${POST_LIMIT}`)).json();
|
|
||||||
};
|
|
||||||
|
|
||||||
public user = async (id?: number): Promise<User> => {
|
|
||||||
return await (await this.getAuth(`users/${id ?? this.self?.id}`)).json();
|
|
||||||
};
|
|
||||||
|
|
||||||
public updateUser = async (data: UserUpdate, id?: number): Promise<User> => {
|
|
||||||
const user = await (await this.patch(`users/${id ?? 'self'}`, data as Record<string, unknown>)).json();
|
|
||||||
this.self = user;
|
|
||||||
this.userListeners.forEach((listener) => listener(user));
|
|
||||||
return user;
|
|
||||||
};
|
|
||||||
|
|
||||||
public updateUserImage = async (data: UserImageUpdate, id?: number): Promise<User> => {
|
|
||||||
const formData = new FormData();
|
|
||||||
if (data.image) formData.append('image', data.image);
|
|
||||||
if (!data.image && data.predefined) formData.append('predefined', data.predefined);
|
|
||||||
|
|
||||||
const user = await (await this.postAuthRaw(`users/${id ?? 'self'}/image`, formData)).json();
|
|
||||||
this.self = user;
|
|
||||||
this.userListeners.forEach((listener) => listener(user));
|
|
||||||
return user;
|
|
||||||
};
|
|
||||||
|
|
||||||
public newPost = async (data: PostCreate): Promise<PostNew> => {
|
|
||||||
return await (await this.postAuth(`posts?l=${POST_LIMIT}`, data as unknown as Record<string, unknown>)).json();
|
|
||||||
};
|
|
||||||
|
|
||||||
public updatePost = async (data: PostUpdate, id: number): Promise<PostAuth> => {
|
|
||||||
return await (await this.patch(`posts/${id}`, data as Record<string, unknown>)).json();
|
|
||||||
};
|
|
||||||
|
|
||||||
public userPosts = async (id?: number): Promise<PostListAuth> => {
|
|
||||||
return await (await this.getAuth(`users/${id}/posts?l=${PROFILE_POST_LIMIT}&s=desc`)).json();
|
|
||||||
};
|
|
||||||
|
|
||||||
public createUser = async (data: UserCreate): Promise<User> => {
|
|
||||||
return await (await this.post(`register`, data as unknown as Record<string, unknown>)).json();
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Internal */
|
|
||||||
|
|
||||||
private post = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
|
||||||
const response = await fetch(`${BASE}${endpoint}`, {
|
|
||||||
mode: 'cors',
|
|
||||||
method: 'post',
|
|
||||||
headers,
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (response.ok) return response;
|
|
||||||
throw await response.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
private postAuth = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
|
||||||
const response = await fetch(`${BASE}${endpoint}`, {
|
|
||||||
mode: 'cors',
|
|
||||||
method: 'post',
|
|
||||||
headers: { token: this.token ?? '', ...headers },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (response.ok) return response;
|
|
||||||
throw await response.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
private postAuthRaw = async (endpoint: string, body?: FormData, headers?: HeadersInit) => {
|
|
||||||
const response = await fetch(`${BASE}${endpoint}`, {
|
|
||||||
mode: 'cors',
|
|
||||||
method: 'post',
|
|
||||||
headers: { token: this.token ?? '', ...headers },
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
if (response.ok) return response;
|
|
||||||
throw await response.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
private get = async (endpoint: string, headers?: HeadersInit) => {
|
|
||||||
const response = await fetch(`${BASE}${endpoint}`, {
|
|
||||||
mode: 'cors',
|
|
||||||
method: 'get',
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
if (response.ok) return response;
|
|
||||||
throw await response.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
private getAuth = async (endpoint: string, headers?: HeadersInit) => {
|
|
||||||
const response = await fetch(`${BASE}${endpoint}`, {
|
|
||||||
mode: 'cors',
|
|
||||||
method: 'get',
|
|
||||||
headers: { token: this.token ?? '', ...headers },
|
|
||||||
});
|
|
||||||
if (response.ok) return response;
|
|
||||||
throw await response.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
private delete = async (endpoint: string, headers?: HeadersInit) => {
|
|
||||||
const response = await fetch(`${BASE}${endpoint}`, {
|
|
||||||
mode: 'cors',
|
|
||||||
method: 'delete',
|
|
||||||
headers: { token: this.token ?? '', ...headers },
|
|
||||||
});
|
|
||||||
if (response.ok) return response;
|
|
||||||
throw await response.json();
|
|
||||||
};
|
|
||||||
|
|
||||||
private patch = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
|
||||||
const response = await fetch(`${BASE}${endpoint}`, {
|
|
||||||
mode: 'cors',
|
|
||||||
method: 'patch',
|
|
||||||
headers: { token: this.token ?? '', ...headers },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (response.ok) return response;
|
|
||||||
throw await response.json();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const Api = new ApiImpl();
|
|
||||||
export default Api;
|
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
import { createContext, FC, PropsWithChildren, useContext, useEffect, useRef, useState } from 'react';
|
||||||
|
import useLocalStorageState from 'use-local-storage-state';
|
||||||
|
import { POST_LIMIT, PROFILE_POST_LIMIT } from '../constanst';
|
||||||
|
import { PostAuth, PostCreate, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
|
||||||
|
import { User, UserCreate, UserImageUpdate, UserUpdate } from '../types/User';
|
||||||
|
|
||||||
|
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
||||||
|
|
||||||
|
interface ApiContext {
|
||||||
|
hasAuth: boolean;
|
||||||
|
authenticatedUser?: User;
|
||||||
|
currentSession: [string?, string?];
|
||||||
|
|
||||||
|
logIn?: (email: string, password: string) => Promise<void>;
|
||||||
|
logOut?: () => Promise<boolean>;
|
||||||
|
|
||||||
|
posts?: (page?: number) => Promise<PostListNonAuth | PostListAuth>;
|
||||||
|
newPost?: (data: PostCreate) => Promise<PostNew>;
|
||||||
|
updatePost?: (data: PostUpdate, id: number) => Promise<PostAuth>;
|
||||||
|
deletePost?: (id: number) => Promise<PostDelete>;
|
||||||
|
|
||||||
|
user?: (id?: number) => Promise<User>;
|
||||||
|
createUser?: (data: UserCreate) => Promise<User>;
|
||||||
|
updateUser?: (data: UserUpdate, id?: number) => Promise<User>;
|
||||||
|
updateUserImage?: (data: UserImageUpdate, id?: number) => Promise<User>;
|
||||||
|
|
||||||
|
userPosts?: (id?: number) => Promise<PostListAuth>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApiContext = createContext<ApiContext>({
|
||||||
|
hasAuth: false,
|
||||||
|
currentSession: [undefined, undefined],
|
||||||
|
});
|
||||||
|
|
||||||
|
//eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
export const useApi = () => {
|
||||||
|
const {
|
||||||
|
hasAuth,
|
||||||
|
authenticatedUser,
|
||||||
|
currentSession,
|
||||||
|
|
||||||
|
logIn,
|
||||||
|
logOut,
|
||||||
|
posts,
|
||||||
|
newPost,
|
||||||
|
updatePost,
|
||||||
|
deletePost,
|
||||||
|
|
||||||
|
user,
|
||||||
|
createUser,
|
||||||
|
updateUser,
|
||||||
|
updateUserImage,
|
||||||
|
|
||||||
|
userPosts,
|
||||||
|
} = useContext(ApiContext);
|
||||||
|
|
||||||
|
if (
|
||||||
|
logIn &&
|
||||||
|
logOut &&
|
||||||
|
posts &&
|
||||||
|
newPost &&
|
||||||
|
updatePost &&
|
||||||
|
deletePost &&
|
||||||
|
user &&
|
||||||
|
createUser &&
|
||||||
|
updateUser &&
|
||||||
|
updateUserImage &&
|
||||||
|
userPosts
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
hasAuth,
|
||||||
|
authenticatedUser,
|
||||||
|
currentSession,
|
||||||
|
|
||||||
|
logIn,
|
||||||
|
logOut,
|
||||||
|
posts,
|
||||||
|
newPost,
|
||||||
|
updatePost,
|
||||||
|
deletePost,
|
||||||
|
|
||||||
|
user,
|
||||||
|
createUser,
|
||||||
|
updateUser,
|
||||||
|
updateUserImage,
|
||||||
|
|
||||||
|
userPosts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Couldn't find context. Is your component inside an ApiProvider?");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ApiProvider: FC<PropsWithChildren<Record<string, unknown>>> = ({ children }) => {
|
||||||
|
const [hasAuth, setHasAuth] = useState(false);
|
||||||
|
const [authenticatedUser, setAuthenticatedUser] = useState<User>();
|
||||||
|
const [currentSession, setCurrentSession] = useLocalStorageState<[string | undefined, string | undefined]>(
|
||||||
|
'egb_session',
|
||||||
|
{ defaultValue: [undefined, undefined] }
|
||||||
|
);
|
||||||
|
|
||||||
|
const token = useRef<string | undefined>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentSession[0] && !token.current) {
|
||||||
|
token.current = currentSession[0];
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}, [currentSession]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const logIn = async (email: string, password: string): Promise<void> => {
|
||||||
|
const { user, token: _token, refreshToken } = await (await post('login', { email, password })).json();
|
||||||
|
setAuthenticatedUser(user);
|
||||||
|
setCurrentSession([_token, refreshToken]);
|
||||||
|
setHasAuth(true);
|
||||||
|
token.current = _token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const logOut = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
if (token.current) return await (await reAuth(() => postAuth('logout'))).json();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setAuthenticatedUser(undefined);
|
||||||
|
setCurrentSession([undefined, undefined]);
|
||||||
|
setHasAuth(false);
|
||||||
|
token.current = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const posts = async (page?: number): Promise<PostListNonAuth | PostListAuth> => {
|
||||||
|
const url = `posts?p=${page ?? 0}&l=${POST_LIMIT}`;
|
||||||
|
|
||||||
|
if (hasAuth) return await (await reAuth(() => getAuth(url))).json();
|
||||||
|
|
||||||
|
return await (await get(url)).json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const newPost = async (data: PostCreate): Promise<PostNew> => {
|
||||||
|
return await (
|
||||||
|
await reAuth(() => postAuth(`posts?l=${POST_LIMIT}`, data as unknown as Record<string, unknown>))
|
||||||
|
).json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePost = async (data: PostUpdate, id: number): Promise<PostAuth> => {
|
||||||
|
return await (await reAuth(() => patch(`posts/${id}`, data as Record<string, unknown>))).json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePost = async (id: number): Promise<PostDelete> => {
|
||||||
|
return await (await reAuth(() => _delete(`posts/${id}?l=${POST_LIMIT}`))).json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const user = async (id?: number): Promise<User> => {
|
||||||
|
return await (await reAuth(() => getAuth(`users/${id ?? authenticatedUser?.id}`))).json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUser = async (data: UserCreate): Promise<User> => {
|
||||||
|
return await (await post(`register`, data as unknown as Record<string, unknown>)).json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUser = async (data: UserUpdate, id?: number): Promise<User> => {
|
||||||
|
const _user = await (await reAuth(() => patch(`users/${id ?? 'self'}`, data as Record<string, unknown>))).json();
|
||||||
|
setAuthenticatedUser(_user);
|
||||||
|
return _user;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUserImage = async (data: UserImageUpdate, id?: number): Promise<User> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
if (data.image) formData.append('image', data.image);
|
||||||
|
if (!data.image && data.predefined) formData.append('predefined', data.predefined);
|
||||||
|
|
||||||
|
const _user = await (await reAuth(() => postAuthRaw(`users/${id ?? 'self'}/image`, formData))).json();
|
||||||
|
setAuthenticatedUser(_user);
|
||||||
|
return _user;
|
||||||
|
};
|
||||||
|
|
||||||
|
const userPosts = async (id?: number): Promise<PostListAuth> => {
|
||||||
|
return await (await reAuth(() => getAuth(`users/${id}/posts?l=${PROFILE_POST_LIMIT}&s=desc`))).json();
|
||||||
|
};
|
||||||
|
|
||||||
|
/* IMPL */
|
||||||
|
|
||||||
|
const post = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||||
|
const response = await fetch(`${BASE}${endpoint}`, {
|
||||||
|
mode: 'cors',
|
||||||
|
method: 'post',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
if (response.ok) return response;
|
||||||
|
throw await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const postAuth = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||||
|
const response = await fetch(`${BASE}${endpoint}`, {
|
||||||
|
mode: 'cors',
|
||||||
|
method: 'post',
|
||||||
|
headers: { token: token.current ?? '', ...headers },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
if (response.ok) return response;
|
||||||
|
throw await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const postAuthRaw = async (endpoint: string, body?: FormData, headers?: HeadersInit) => {
|
||||||
|
const response = await fetch(`${BASE}${endpoint}`, {
|
||||||
|
mode: 'cors',
|
||||||
|
method: 'post',
|
||||||
|
headers: { token: token.current ?? '', ...headers },
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
if (response.ok) return response;
|
||||||
|
throw await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const get = async (endpoint: string, headers?: HeadersInit) => {
|
||||||
|
const response = await fetch(`${BASE}${endpoint}`, {
|
||||||
|
mode: 'cors',
|
||||||
|
method: 'get',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
if (response.ok) return response;
|
||||||
|
throw await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAuth = async (endpoint: string, headers?: HeadersInit) => {
|
||||||
|
const response = await fetch(`${BASE}${endpoint}`, {
|
||||||
|
mode: 'cors',
|
||||||
|
method: 'get',
|
||||||
|
headers: { token: token.current ?? '', ...headers },
|
||||||
|
});
|
||||||
|
if (response.ok) return response;
|
||||||
|
throw await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const _delete = async (endpoint: string, headers?: HeadersInit) => {
|
||||||
|
const response = await fetch(`${BASE}${endpoint}`, {
|
||||||
|
mode: 'cors',
|
||||||
|
method: 'delete',
|
||||||
|
headers: { token: token.current ?? '', ...headers },
|
||||||
|
});
|
||||||
|
if (response.ok) return response;
|
||||||
|
throw await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const patch = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||||
|
const response = await fetch(`${BASE}${endpoint}`, {
|
||||||
|
mode: 'cors',
|
||||||
|
method: 'patch',
|
||||||
|
headers: { token: token.current ?? '', ...headers },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
if (response.ok) return response;
|
||||||
|
throw await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const reAuth = async <T,>(callback: () => Promise<T>): Promise<T> => {
|
||||||
|
try {
|
||||||
|
console.log('[REAUTH] fetching');
|
||||||
|
const ret = await callback();
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
console.log('[REAUTH] fail, refreshing');
|
||||||
|
// REAUTH
|
||||||
|
await refresh();
|
||||||
|
// DO AGAIN
|
||||||
|
console.log('[REAUTH] refreshed, fetching again');
|
||||||
|
const ret = await callback();
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
} catch (error) {
|
||||||
|
console.log('[REAUTH] terminating session', error);
|
||||||
|
setAuthenticatedUser(undefined);
|
||||||
|
setHasAuth(false);
|
||||||
|
setCurrentSession([undefined, undefined]);
|
||||||
|
token.current = undefined;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
const {
|
||||||
|
user: _user,
|
||||||
|
token: _token,
|
||||||
|
refreshToken,
|
||||||
|
} = await (await postAuth('refresh', { refreshToken: currentSession[1] ?? 'INVALID DEFAULT' })).json();
|
||||||
|
setAuthenticatedUser(_user);
|
||||||
|
setCurrentSession([_token, refreshToken]);
|
||||||
|
setHasAuth(true);
|
||||||
|
token.current = _token;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ApiContext.Provider
|
||||||
|
value={{
|
||||||
|
hasAuth,
|
||||||
|
authenticatedUser,
|
||||||
|
currentSession,
|
||||||
|
|
||||||
|
logIn,
|
||||||
|
logOut,
|
||||||
|
|
||||||
|
posts,
|
||||||
|
newPost,
|
||||||
|
updatePost,
|
||||||
|
deletePost,
|
||||||
|
|
||||||
|
user,
|
||||||
|
createUser,
|
||||||
|
updateUser,
|
||||||
|
updateUserImage,
|
||||||
|
|
||||||
|
userPosts,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ApiContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApiProvider;
|
||||||
@@ -14,7 +14,7 @@ import { useForm } from '@tanstack/react-form';
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { FC, FormEvent, useState } from 'react';
|
import { FC, FormEvent, useState } from 'react';
|
||||||
import Api from '../../../api/Api';
|
import { useApi } from '../../../api/Api';
|
||||||
import { PostAuth, PostUpdate } from '../../../types/Post';
|
import { PostAuth, PostUpdate } from '../../../types/Post';
|
||||||
import ErrorComponent from '../../Error/ErrorComponent';
|
import ErrorComponent from '../../Error/ErrorComponent';
|
||||||
|
|
||||||
@@ -29,6 +29,11 @@ const PostEditDialog: FC<Props> = ({ post, open, onClose }) => {
|
|||||||
const [error, setError] = useState<any>();
|
const [error, setError] = useState<any>();
|
||||||
const [characterCount, setCharacterCount] = useState(post.content.length);
|
const [characterCount, setCharacterCount] = useState(post.content.length);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const Api = useApi();
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ data, id }: { data: PostUpdate; id: number }) => {
|
mutationFn: ({ data, id }: { data: PostUpdate; id: number }) => {
|
||||||
return Api.updatePost(data, id);
|
return Api.updatePost(data, id);
|
||||||
@@ -54,16 +59,12 @@ const PostEditDialog: FC<Props> = ({ post, open, onClose }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (_error: any) {
|
||||||
setError(error);
|
setError(_error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
form.reset();
|
form.reset();
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { useForm } from '@tanstack/react-form';
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { FC, FormEvent, useState } from 'react';
|
import { FC, FormEvent, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Api from '../../../api/Api';
|
import { useApi } from '../../../api/Api';
|
||||||
import { UserCreate } from '../../../types/User';
|
import { UserCreate } from '../../../types/User';
|
||||||
import ErrorComponent from '../../Error/ErrorComponent';
|
import ErrorComponent from '../../Error/ErrorComponent';
|
||||||
|
|
||||||
@@ -29,16 +29,17 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
|
|||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const [error, setError] = useState<any>();
|
const [error, setError] = useState<any>();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||||
|
const Api = useApi();
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: ({ data }: { data: UserCreate }) => {
|
mutationFn: ({ data }: { data: UserCreate }) => {
|
||||||
return Api.createUser(data);
|
return Api.createUser(data);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const theme = useTheme();
|
|
||||||
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
|
||||||
|
|
||||||
const form = useForm<UserCreate>({
|
const form = useForm<UserCreate>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: '',
|
username: '',
|
||||||
@@ -55,8 +56,8 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (_error: any) {
|
||||||
setError(error);
|
setError(_error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useForm } from '@tanstack/react-form';
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { FC, FormEvent, useState } from 'react';
|
import { FC, FormEvent, useState } from 'react';
|
||||||
import Api from '../../../api/Api';
|
import { useApi } from '../../../api/Api';
|
||||||
import { User, UserUpdate } from '../../../types/User';
|
import { User, UserUpdate } from '../../../types/User';
|
||||||
import ErrorComponent from '../../Error/ErrorComponent';
|
import ErrorComponent from '../../Error/ErrorComponent';
|
||||||
|
|
||||||
@@ -27,6 +27,11 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
|
|||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const [error, setError] = useState<any>();
|
const [error, setError] = useState<any>();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const Api = useApi();
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => {
|
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => {
|
||||||
return Api.updateUser(data, id);
|
return Api.updateUser(data, id);
|
||||||
@@ -41,12 +46,12 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
|
|||||||
onSubmit: async ({ value }) => {
|
onSubmit: async ({ value }) => {
|
||||||
try {
|
try {
|
||||||
updateMutation.mutate(
|
updateMutation.mutate(
|
||||||
{ data: value, id: Api.getAuthenticatedUser()?.id === user.id ? undefined : user.id },
|
{ data: value, id: Api.authenticatedUser?.id === user.id ? undefined : user.id },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
handleClose();
|
handleClose();
|
||||||
|
|
||||||
const queryKey = Api.getAuthenticatedUser()?.id === user.id ? ['profile'] : ['profile', { id: user.id }];
|
const queryKey = Api.authenticatedUser?.id === user.id ? ['profile'] : ['profile', { id: user.id }];
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
},
|
},
|
||||||
onError: setError,
|
onError: setError,
|
||||||
@@ -54,16 +59,12 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (_error: any) {
|
||||||
setError(error);
|
setError(_error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
form.reset();
|
form.reset();
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { useForm } from '@tanstack/react-form';
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { FC, FormEvent, useState } from 'react';
|
import { FC, FormEvent, useState } from 'react';
|
||||||
import Api from '../../../api/Api';
|
import { useApi } from '../../../api/Api';
|
||||||
import { User, UserImageUpdate } from '../../../types/User';
|
import { User, UserImageUpdate } from '../../../types/User';
|
||||||
import ErrorComponent from '../../Error/ErrorComponent';
|
import ErrorComponent from '../../Error/ErrorComponent';
|
||||||
|
|
||||||
@@ -38,6 +38,11 @@ const UserImageDialog: FC<Props> = ({ user, open, onClose }) => {
|
|||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const [error, setError] = useState<any>();
|
const [error, setError] = useState<any>();
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const Api = useApi();
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ data, id }: { data: UserImageUpdate; id?: number }) => {
|
mutationFn: ({ data, id }: { data: UserImageUpdate; id?: number }) => {
|
||||||
return Api.updateUserImage(data, id);
|
return Api.updateUserImage(data, id);
|
||||||
@@ -48,27 +53,24 @@ const UserImageDialog: FC<Props> = ({ user, open, onClose }) => {
|
|||||||
onSubmit: async ({ value }) => {
|
onSubmit: async ({ value }) => {
|
||||||
try {
|
try {
|
||||||
updateMutation.mutate(
|
updateMutation.mutate(
|
||||||
{ data: value, id: Api.getAuthenticatedUser()?.id === user.id ? undefined : user.id },
|
{ data: value, id: Api.authenticatedUser?.id === user.id ? undefined : user.id },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
handleClose();
|
handleClose();
|
||||||
|
|
||||||
const queryKey = Api.getAuthenticatedUser()?.id === user.id ? ['profile'] : ['profile', { id: user.id }];
|
const queryKey = Api.authenticatedUser?.id === user.id ? ['profile'] : ['profile', { id: user.id }];
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
},
|
},
|
||||||
onError: setError,
|
onError: setError,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (_error: any) {
|
||||||
setError(error);
|
setError(_error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const theme = useTheme();
|
|
||||||
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const formState = form.useStore((state) => ({ image: state.values.image, predefined: state.values.predefined }));
|
const formState = form.useStore((state) => ({ image: state.values.image, predefined: state.values.predefined }));
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const ErrorComponent: FC<Props> = ({ error, context, color = 'error.main' }) =>
|
|||||||
case ERRORS.NOT_FOUND:
|
case ERRORS.NOT_FOUND:
|
||||||
return <Typography color={color}>{t(error.code, { context: `${error.entity}:${context}` })}</Typography>;
|
return <Typography color={color}>{t(error.code, { context: `${error.entity}:${context}` })}</Typography>;
|
||||||
case ERRORS.NOT_ALLOWED:
|
case ERRORS.NOT_ALLOWED:
|
||||||
|
case ERRORS.UNAUTHORIZED:
|
||||||
|
console.log(error, context);
|
||||||
return <Typography color={color}>{t(error.code, { context })}</Typography>;
|
return <Typography color={color}>{t(error.code, { context })}</Typography>;
|
||||||
case ERRORS.FAILED_UPDATE:
|
case ERRORS.FAILED_UPDATE:
|
||||||
return error.fields.map((field: string, index: number) => (
|
return error.fields.map((field: string, index: number) => (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useForm } from '@tanstack/react-form';
|
|||||||
import { useRouter } from '@tanstack/react-router';
|
import { useRouter } from '@tanstack/react-router';
|
||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Api from '../../../api/Api';
|
import { useApi } from '../../../api/Api';
|
||||||
import { Login } from '../../../types/User';
|
import { Login } from '../../../types/User';
|
||||||
import ErrorComponent from '../../Error/ErrorComponent';
|
import ErrorComponent from '../../Error/ErrorComponent';
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ const LoginForm: FC<Props> = ({ handleClose }) => {
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const Api = useApi();
|
||||||
|
|
||||||
const form = useForm<Login>({
|
const form = useForm<Login>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -28,8 +29,8 @@ const LoginForm: FC<Props> = ({ handleClose }) => {
|
|||||||
router.invalidate();
|
router.invalidate();
|
||||||
handleClose();
|
handleClose();
|
||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (_error: any) {
|
||||||
setError(error);
|
setError(_error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Box, Button, CircularProgress, LinearProgress, TextField } from '@mui/material';
|
import { Alert, Box, Button, CircularProgress, LinearProgress, Snackbar, TextField } from '@mui/material';
|
||||||
import { useForm } from '@tanstack/react-form';
|
import { useForm } from '@tanstack/react-form';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Api from '../../../api/Api';
|
import { useApi } from '../../../api/Api';
|
||||||
import { PostCreate } from '../../../types/Post';
|
import { PostCreate } from '../../../types/Post';
|
||||||
import ErrorComponent from '../../Error/ErrorComponent';
|
import ErrorComponent from '../../Error/ErrorComponent';
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ const PostForm: FC = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const Api = useApi();
|
||||||
|
|
||||||
const newMutation = useMutation({
|
const newMutation = useMutation({
|
||||||
mutationFn: ({ data }: { data: PostCreate }) => {
|
mutationFn: ({ data }: { data: PostCreate }) => {
|
||||||
@@ -42,8 +43,8 @@ const PostForm: FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (_error: any) {
|
||||||
setError(error);
|
setError(_error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -120,7 +121,21 @@ const PostForm: FC = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{error && <ErrorComponent error={error} context="newPost" />}
|
|
||||||
|
<Snackbar
|
||||||
|
open={newMutation.isError}
|
||||||
|
autoHideDuration={2000}
|
||||||
|
onClose={() => {
|
||||||
|
newMutation.reset();
|
||||||
|
}}
|
||||||
|
TransitionProps={{
|
||||||
|
onExited: () => setError(undefined),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Alert severity="error" variant="filled" sx={{ width: '100%' }}>
|
||||||
|
{error && <ErrorComponent error={error} context="newPost" color="white" />}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
</Box>
|
</Box>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ import {
|
|||||||
useScrollTrigger,
|
useScrollTrigger,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Link, useRouterState } from '@tanstack/react-router';
|
import { Link, useRouterState } from '@tanstack/react-router';
|
||||||
import { cloneElement, FC, ReactElement, useEffect, useState } from 'react';
|
import { cloneElement, FC, ReactElement, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Api from '../../api/Api';
|
import { useApi } from '../../api/Api';
|
||||||
import { User } from '../../types/User';
|
|
||||||
import LanguageMenu from '../Menus/Language/LanguageMenu';
|
import LanguageMenu from '../Menus/Language/LanguageMenu';
|
||||||
import UserMenu from '../Menus/User/UserMenu';
|
import UserMenu from '../Menus/User/UserMenu';
|
||||||
|
|
||||||
@@ -31,12 +30,10 @@ const ElevationScroll = ({ children }: { children: ReactElement }) => {
|
|||||||
const Header: FC = () => {
|
const Header: FC = () => {
|
||||||
const [anchorUserMenu, setAnchorUserMenu] = useState<null | HTMLElement>(null);
|
const [anchorUserMenu, setAnchorUserMenu] = useState<null | HTMLElement>(null);
|
||||||
const [anchorLanguageMenu, setAnchorLanguageMenu] = useState<null | HTMLElement>(null);
|
const [anchorLanguageMenu, setAnchorLanguageMenu] = useState<null | HTMLElement>(null);
|
||||||
const [user, setUser] = useState<User>();
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isLoading = useRouterState({ select: (s) => s.status === 'pending' });
|
const isLoading = useRouterState({ select: (s) => s.status === 'pending' });
|
||||||
|
const Api = useApi();
|
||||||
useEffect(() => Api.subscribeToAuthenticatedUser((user) => setUser(user)), []);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setAnchorLanguageMenu(null);
|
setAnchorLanguageMenu(null);
|
||||||
@@ -59,9 +56,9 @@ const Header: FC = () => {
|
|||||||
<IconButton size="large" onClick={(event) => setAnchorLanguageMenu(event.currentTarget)}>
|
<IconButton size="large" onClick={(event) => setAnchorLanguageMenu(event.currentTarget)}>
|
||||||
<Translate sx={{ color: 'white' }} />
|
<Translate sx={{ color: 'white' }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{user ? (
|
{Api.authenticatedUser ? (
|
||||||
<IconButton onClick={(event) => setAnchorUserMenu(event.currentTarget)} sx={{ p: 0 }}>
|
<IconButton onClick={(event) => setAnchorUserMenu(event.currentTarget)} sx={{ p: 0 }}>
|
||||||
<Avatar alt={user.username} src={`${user.image}`}>
|
<Avatar alt={Api.authenticatedUser.username} src={`${Api.authenticatedUser.image}`}>
|
||||||
<Person />
|
<Person />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useMatch, useNavigate, useRouter } from '@tanstack/react-router';
|
|||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { Trans } from 'react-i18next/TransWithoutContext';
|
import { Trans } from 'react-i18next/TransWithoutContext';
|
||||||
import Api from '../../../api/Api';
|
import { useApi } from '../../../api/Api';
|
||||||
import { ROUTES } from '../../../types/Routes';
|
import { ROUTES } from '../../../types/Routes';
|
||||||
import RegisterDialog from '../../Dialogs/Register/RegisterDialog';
|
import RegisterDialog from '../../Dialogs/Register/RegisterDialog';
|
||||||
import LoginForm from '../../Forms/Login/LoginForm';
|
import LoginForm from '../../Forms/Login/LoginForm';
|
||||||
@@ -20,7 +20,7 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const match = useMatch({ from: '/profile/', strict: true, shouldThrow: false });
|
const match = useMatch({ from: '/profile/', strict: true, shouldThrow: false });
|
||||||
|
|
||||||
const user = Api.getAuthenticatedUser();
|
const Api = useApi();
|
||||||
|
|
||||||
const _handleClose = () => {
|
const _handleClose = () => {
|
||||||
setRegister(false);
|
setRegister(false);
|
||||||
@@ -47,7 +47,7 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{user ? (
|
{Api.authenticatedUser ? (
|
||||||
[
|
[
|
||||||
<MenuItem
|
<MenuItem
|
||||||
selected={!!match}
|
selected={!!match}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { Link, useNavigate } from '@tanstack/react-router';
|
import { Link, useNavigate } from '@tanstack/react-router';
|
||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Api from '../../api/Api';
|
import { useApi } from '../../api/Api';
|
||||||
import { PostAuth, PostNonAuth } from '../../types/Post';
|
import { PostAuth, PostNonAuth } from '../../types/Post';
|
||||||
import convertDate from '../../utils/date';
|
import convertDate from '../../utils/date';
|
||||||
import PostEditDialog from '../Dialogs/PostEdit/PostEditDialog';
|
import PostEditDialog from '../Dialogs/PostEdit/PostEditDialog';
|
||||||
@@ -37,22 +37,23 @@ const Post: FC<Props> = ({ post, disableActions }) => {
|
|||||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const [error, setError] = useState<any>();
|
const [error, setError] = useState<any>();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const Api = useApi();
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: (id: number) => {
|
mutationFn: (id: number) => {
|
||||||
return Api.deletePost(id);
|
return Api.deletePost(id);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader
|
||||||
avatar={
|
avatar={
|
||||||
!disableActions && 'id' in post.user ? (
|
!disableActions && 'id' in post.user ? (
|
||||||
post.user.id !== Api.getAuthenticatedUser()?.id ? (
|
post.user.id !== Api.authenticatedUser?.id ? (
|
||||||
<MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}>
|
<MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}>
|
||||||
<Avatar alt={post.user.username} src={`${post.user.image}`}>
|
<Avatar alt={post.user.username} src={`${post.user.image}`}>
|
||||||
<Person />
|
<Person />
|
||||||
@@ -73,7 +74,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
|
|||||||
}
|
}
|
||||||
title={
|
title={
|
||||||
!disableActions && 'id' in post.user ? (
|
!disableActions && 'id' in post.user ? (
|
||||||
post.user.id !== Api.getAuthenticatedUser()?.id ? (
|
post.user.id !== Api.authenticatedUser?.id ? (
|
||||||
<MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}>
|
<MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}>
|
||||||
{post.user.username}
|
{post.user.username}
|
||||||
</MUILink>
|
</MUILink>
|
||||||
@@ -96,7 +97,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
|
|||||||
|
|
||||||
<CardActions>
|
<CardActions>
|
||||||
{!disableActions &&
|
{!disableActions &&
|
||||||
(Api.isAdmin() || ('id' in post.user && post.user.id === Api.getAuthenticatedUser()?.id)) && (
|
(Api.authenticatedUser?.isAdmin || ('id' in post.user && post.user.id === Api.authenticatedUser?.id)) && (
|
||||||
<>
|
<>
|
||||||
<Button size="small" onClick={() => setEditOpen(true)}>
|
<Button size="small" onClick={() => setEditOpen(true)}>
|
||||||
{t('Edit')}
|
{t('Edit')}
|
||||||
@@ -104,7 +105,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
|
|||||||
<PostEditDialog post={post as PostAuth} open={editOpen} onClose={() => setEditOpen(false)} />
|
<PostEditDialog post={post as PostAuth} open={editOpen} onClose={() => setEditOpen(false)} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!disableActions && Api.isAdmin() && (
|
{!disableActions && Api.authenticatedUser?.isAdmin && (
|
||||||
<>
|
<>
|
||||||
<Button size="small" color="error" onClick={() => setDeleteOpen(true)}>
|
<Button size="small" color="error" onClick={() => setDeleteOpen(true)}>
|
||||||
{t('Delete')}
|
{t('Delete')}
|
||||||
@@ -152,7 +153,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Alert severity="error" variant="filled" sx={{ width: '100%' }}>
|
<Alert severity="error" variant="filled" sx={{ width: '100%' }}>
|
||||||
{error && <ErrorComponent error={error} context="delete" color="white" />}
|
{error && <ErrorComponent error={error} context="deletePost" color="white" />}
|
||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
<Snackbar open={deleteMutation.isPending} message={t('Deleting')} />
|
<Snackbar open={deleteMutation.isPending} message={t('Deleting')} />
|
||||||
|
|||||||
+8
-65
@@ -1,13 +1,9 @@
|
|||||||
import { QueryClient, QueryClientProvider, useQueryClient, useQueryErrorResetBoundary } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ErrorRouteComponent, RouterProvider, createRouter, useRouter } from '@tanstack/react-router';
|
|
||||||
import { StrictMode, useEffect } from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import Api from './api/Api';
|
|
||||||
import { ERRORS } from './components/Error/Errors';
|
|
||||||
|
|
||||||
// Import the generated route tree
|
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import { routeTree } from './routeTree.gen';
|
import { StrictMode } from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import ApiProvider from './api/Api';
|
||||||
|
import Router from './router';
|
||||||
|
|
||||||
// Import i18n
|
// Import i18n
|
||||||
import './i18n';
|
import './i18n';
|
||||||
@@ -21,61 +17,6 @@ import '@fontsource/roboto/700.css';
|
|||||||
// Query Client
|
// Query Client
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
//TODO: REAUTH HERE
|
|
||||||
|
|
||||||
//TODO: Make nice
|
|
||||||
export const Error: ErrorRouteComponent = ({ error }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const queryErrorResetBoundary = useQueryErrorResetBoundary();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Reset the query error boundary
|
|
||||||
queryErrorResetBoundary.reset();
|
|
||||||
}, [queryErrorResetBoundary]);
|
|
||||||
|
|
||||||
//TODO: Split display, show something went wrong with retry on all, show session expired and back to main on auth
|
|
||||||
if ('code' in error && error.code === ERRORS.NOT_ALLOWED) {
|
|
||||||
Api.logOut().finally(() => {
|
|
||||||
queryClient.clear();
|
|
||||||
router.invalidate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{error.message}
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
router.invalidate();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
retry
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a new router instance
|
|
||||||
const router = createRouter({
|
|
||||||
routeTree,
|
|
||||||
context: {
|
|
||||||
queryClient,
|
|
||||||
},
|
|
||||||
defaultPreload: 'intent',
|
|
||||||
// Since we're using React Query, we don't want loader calls to ever be stale
|
|
||||||
// This will ensure that the loader is always called when the route is preloaded or visited
|
|
||||||
defaultPreloadStaleTime: 0,
|
|
||||||
basepath: process.env.NODE_ENV === 'development' ? 'phpCourse/exam/dist' : '/phpCourse/exam',
|
|
||||||
defaultErrorComponent: Error,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register the router instance for type safety
|
|
||||||
declare module '@tanstack/react-router' {
|
|
||||||
interface Register {
|
|
||||||
router: typeof router;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render the app
|
// Render the app
|
||||||
const rootElement = document.getElementById('root')!;
|
const rootElement = document.getElementById('root')!;
|
||||||
if (!rootElement.innerHTML) {
|
if (!rootElement.innerHTML) {
|
||||||
@@ -83,7 +24,9 @@ if (!rootElement.innerHTML) {
|
|||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<RouterProvider router={router} />
|
<ApiProvider>
|
||||||
|
<Router />
|
||||||
|
</ApiProvider>
|
||||||
{process.env.NODE_ENV === 'development' && <ReactQueryDevtools initialIsOpen={false} />}
|
{process.env.NODE_ENV === 'development' && <ReactQueryDevtools initialIsOpen={false} />}
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { queryOptions } from '@tanstack/react-query';
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
import Api from '../api/Api';
|
import { useApi } from '../api/Api';
|
||||||
|
|
||||||
export const postsQueryOptions = (page?: number) =>
|
export const postsQueryOptions = (Api: ReturnType<typeof useApi>, page?: number) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ['posts', { page: page ?? 0, hasAuth: Api.hasAuth() }],
|
queryKey: ['posts', { page: page ?? 0, hasAuth: Api.hasAuth }],
|
||||||
queryFn: () => Api.posts(page),
|
queryFn: async () => await Api.posts(page),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import { queryOptions } from '@tanstack/react-query';
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
import Api from '../api/Api';
|
import { useApi } from '../api/Api';
|
||||||
|
|
||||||
export const profileSelfQueryOptions = queryOptions({
|
export const profileSelfQueryOptions = (Api: ReturnType<typeof useApi>) =>
|
||||||
|
queryOptions({
|
||||||
queryKey: ['profile'],
|
queryKey: ['profile'],
|
||||||
queryFn: () => Api.user(),
|
queryFn: async () => await Api.user(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const profileQueryOptions = (id?: number) =>
|
export const profileQueryOptions = (Api: ReturnType<typeof useApi>, id?: number) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ['profile', { id }],
|
queryKey: ['profile', { id }],
|
||||||
queryFn: () => Api.user(id),
|
queryFn: async () => await Api.user(id),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const profilePostsQueryOptions = (id: number) =>
|
export const profilePostsQueryOptions = (Api: ReturnType<typeof useApi>, id: number) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ['profilePosts', { id }],
|
queryKey: ['profilePosts', { id }],
|
||||||
queryFn: () => Api.userPosts(id),
|
queryFn: async () => await Api.userPosts(id),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useQueryClient, useQueryErrorResetBoundary } from '@tanstack/react-query';
|
||||||
|
import { createRouter, ErrorRouteComponent, RouterProvider, useRouter } from '@tanstack/react-router';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useApi } from './api/Api';
|
||||||
|
|
||||||
|
import { routeTree } from './routeTree.gen';
|
||||||
|
|
||||||
|
//TODO: REAUTH HERE
|
||||||
|
|
||||||
|
//TODO: Make nice
|
||||||
|
export const Error: ErrorRouteComponent = ({ error }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const queryErrorResetBoundary = useQueryErrorResetBoundary();
|
||||||
|
const _queryClient = useQueryClient();
|
||||||
|
const Api = useApi();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset the query error boundary
|
||||||
|
queryErrorResetBoundary.reset();
|
||||||
|
}, [queryErrorResetBoundary]);
|
||||||
|
|
||||||
|
//TODO: Split display, show something went wrong with retry on all, show session expired and back to main on auth
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{error.message}
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
_queryClient.clear();
|
||||||
|
if (Api.hasAuth) await Api.logOut();
|
||||||
|
router.invalidate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a new router instance
|
||||||
|
const router = createRouter({
|
||||||
|
routeTree,
|
||||||
|
context: {
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
queryClient: undefined as any,
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
Api: undefined as any,
|
||||||
|
},
|
||||||
|
defaultPreload: 'intent',
|
||||||
|
// Since we're using React Query, we don't want loader calls to ever be stale
|
||||||
|
// This will ensure that the loader is always called when the route is preloaded or visited
|
||||||
|
defaultPreloadStaleTime: 0,
|
||||||
|
basepath: process.env.NODE_ENV === 'development' ? 'phpCourse/exam/dist' : '/phpCourse/exam',
|
||||||
|
//defaultErrorComponent: Error,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register the router instance for type safety
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface Register {
|
||||||
|
router: typeof router;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Router = () => {
|
||||||
|
const Api = useApi();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return <RouterProvider router={router} context={{ queryClient, Api }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Router;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { QueryClient } from '@tanstack/react-query';
|
import { QueryClient, useQueryErrorResetBoundary } from '@tanstack/react-query';
|
||||||
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
import { createRootRouteWithContext, ErrorRouteComponent, Outlet, useRouter } from '@tanstack/react-router';
|
||||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useApi } from '../api/Api';
|
||||||
import Header from '../components/Header/Header';
|
import Header from '../components/Header/Header';
|
||||||
|
|
||||||
const Root = () => {
|
const Root = () => {
|
||||||
@@ -18,6 +20,31 @@ const Root = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
|
const ErrorComponent: ErrorRouteComponent = ({ error }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const queryErrorResetBoundary = useQueryErrorResetBoundary();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Reset the query error boundary
|
||||||
|
queryErrorResetBoundary.reset();
|
||||||
|
}, [queryErrorResetBoundary]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{error.message}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// Invalidate the route to reload the loader, and reset any router error boundaries
|
||||||
|
router.invalidate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Route = createRootRouteWithContext<{ queryClient: QueryClient; Api: ReturnType<typeof useApi> }>()({
|
||||||
component: Root,
|
component: Root,
|
||||||
|
errorComponent: ErrorComponent,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,15 +12,16 @@ import { useSuspenseQuery } from '@tanstack/react-query';
|
|||||||
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Api from '../api/Api';
|
import { useApi } from '../api/Api';
|
||||||
import PostForm from '../components/Forms/Post/PostForm';
|
import PostForm from '../components/Forms/Post/PostForm';
|
||||||
import Post from '../components/Post/Post';
|
import Post from '../components/Post/Post';
|
||||||
import { postsQueryOptions } from '../queries/postsQuery';
|
import { postsQueryOptions } from '../queries/postsQuery';
|
||||||
import { ROUTES } from '../types/Routes';
|
import { ROUTES } from '../types/Routes';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
|
const Api = useApi();
|
||||||
const { page } = Route.useSearch();
|
const { page } = Route.useSearch();
|
||||||
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(page));
|
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(Api, page));
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -62,7 +63,7 @@ const Home = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
{Api.hasAuth() ? (
|
{Api.hasAuth ? (
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<PostForm />
|
<PostForm />
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -79,7 +80,8 @@ const Home = () => {
|
|||||||
|
|
||||||
export const Route = createFileRoute(ROUTES.INDEX)({
|
export const Route = createFileRoute(ROUTES.INDEX)({
|
||||||
loaderDeps: ({ search: { page } }) => ({ page }),
|
loaderDeps: ({ search: { page } }) => ({ page }),
|
||||||
loader: ({ context: { queryClient }, deps: { page } }) => queryClient.ensureQueryData(postsQueryOptions(page)),
|
loader: ({ context: { queryClient, Api }, deps: { page } }) =>
|
||||||
|
queryClient.ensureQueryData(postsQueryOptions(Api, page)),
|
||||||
validateSearch: (search: Record<string, unknown>): { page?: number } => {
|
validateSearch: (search: Record<string, unknown>): { page?: number } => {
|
||||||
return {
|
return {
|
||||||
page: search?.page !== undefined ? Number(search?.page ?? 0) : undefined,
|
page: search?.page !== undefined ? Number(search?.page ?? 0) : undefined,
|
||||||
|
|||||||
@@ -2,21 +2,35 @@ import { Snackbar } from '@mui/material';
|
|||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import Api from '../../api/Api';
|
import { useApi } from '../../api/Api';
|
||||||
|
import { ERRORS } from '../../components/Error/Errors';
|
||||||
import Profile from '../../components/Profile/Profile';
|
import Profile from '../../components/Profile/Profile';
|
||||||
import { profilePostsQueryOptions, profileQueryOptions } from '../../queries/profileQuery';
|
import { profilePostsQueryOptions, profileQueryOptions } from '../../queries/profileQuery';
|
||||||
import { PostAuth } from '../../types/Post';
|
import { PostAuth } from '../../types/Post';
|
||||||
import { ROUTES } from '../../types/Routes';
|
import { ROUTES } from '../../types/Routes';
|
||||||
|
|
||||||
const ProfilePage = () => {
|
const ProfilePage = () => {
|
||||||
|
const Api = useApi();
|
||||||
const { id } = Route.useParams();
|
const { id } = Route.useParams();
|
||||||
const { data: profileQuery, isFetching: isFetchingProfile } = useSuspenseQuery(profileQueryOptions(id));
|
const {
|
||||||
const { data: profilePostsQuery, isFetching: isFetchingPosts } = useSuspenseQuery(profilePostsQueryOptions(id));
|
data: profileQuery,
|
||||||
|
isFetching: isFetchingProfile,
|
||||||
|
failureReason,
|
||||||
|
} = useSuspenseQuery(profileQueryOptions(Api, id));
|
||||||
|
const { data: profilePostsQuery, isFetching: isFetchingPosts } = useSuspenseQuery(profilePostsQueryOptions(Api, id));
|
||||||
|
|
||||||
|
if (failureReason && 'code' in failureReason && failureReason.code === ERRORS.UNAUTHORIZED) {
|
||||||
|
throw failureReason;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Snackbar open={isFetchingProfile || isFetchingPosts} message={t('Updating')} />
|
<Snackbar open={isFetchingProfile || isFetchingPosts} message={t('Updating')} />
|
||||||
<Profile user={profileQuery} posts={profilePostsQuery.data as PostAuth[]} canEdit={Api.isAdmin()} />
|
<Profile
|
||||||
|
user={profileQuery}
|
||||||
|
posts={profilePostsQuery.data as PostAuth[]}
|
||||||
|
canEdit={Api.authenticatedUser?.isAdmin}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -26,13 +40,15 @@ export const Route = createFileRoute(`${ROUTES.PROFILE}/$id`)({
|
|||||||
parse: ({ id }) => ({ id: parseInt(id) }),
|
parse: ({ id }) => ({ id: parseInt(id) }),
|
||||||
stringify: ({ id }) => ({ id: id.toString() }),
|
stringify: ({ id }) => ({ id: id.toString() }),
|
||||||
},
|
},
|
||||||
loader: ({ context: { queryClient }, params: { id } }) => {
|
loader: ({ context: { queryClient, Api }, params: { id } }) => {
|
||||||
queryClient.ensureQueryData(profileQueryOptions(id));
|
console.log('LOAD PROFILE');
|
||||||
queryClient.ensureQueryData(profilePostsQueryOptions(id));
|
queryClient.ensureQueryData(profileQueryOptions(Api, id));
|
||||||
|
queryClient.ensureQueryData(profilePostsQueryOptions(Api, id));
|
||||||
},
|
},
|
||||||
beforeLoad: ({ params: { id } }) => {
|
beforeLoad: ({ params: { id }, context: { Api } }) => {
|
||||||
if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX });
|
console.log('BEFORE PROFILE');
|
||||||
if (id === Api.getAuthenticatedUser()?.id) throw redirect({ to: ROUTES.PROFILE });
|
if (!Api.hasAuth) throw redirect({ to: ROUTES.INDEX });
|
||||||
|
if (id === Api.authenticatedUser?.id) throw redirect({ to: ROUTES.PROFILE });
|
||||||
},
|
},
|
||||||
component: ProfilePage,
|
component: ProfilePage,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,18 +2,28 @@ import { Snackbar } from '@mui/material';
|
|||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import Api from '../../api/Api';
|
import { useApi } from '../../api/Api';
|
||||||
|
import { ERRORS } from '../../components/Error/Errors';
|
||||||
import Profile from '../../components/Profile/Profile';
|
import Profile from '../../components/Profile/Profile';
|
||||||
import { profilePostsQueryOptions, profileSelfQueryOptions } from '../../queries/profileQuery';
|
import { profilePostsQueryOptions, profileSelfQueryOptions } from '../../queries/profileQuery';
|
||||||
import { PostAuth } from '../../types/Post';
|
import { PostAuth } from '../../types/Post';
|
||||||
import { ROUTES } from '../../types/Routes';
|
import { ROUTES } from '../../types/Routes';
|
||||||
|
|
||||||
const ProfilePage = () => {
|
const ProfilePage = () => {
|
||||||
const { data: profileQuery, isFetching: isFetchingProfile } = useSuspenseQuery(profileSelfQueryOptions);
|
const Api = useApi();
|
||||||
|
const {
|
||||||
|
data: profileQuery,
|
||||||
|
isFetching: isFetchingProfile,
|
||||||
|
failureReason,
|
||||||
|
} = useSuspenseQuery(profileSelfQueryOptions(Api));
|
||||||
const { data: profilePostsQuery, isFetching: isFetchingPosts } = useSuspenseQuery(
|
const { data: profilePostsQuery, isFetching: isFetchingPosts } = useSuspenseQuery(
|
||||||
profilePostsQueryOptions(Api.getAuthenticatedUser()?.id ?? 0)
|
profilePostsQueryOptions(Api, Api.authenticatedUser?.id ?? 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (failureReason && 'code' in failureReason && failureReason.code === ERRORS.UNAUTHORIZED) {
|
||||||
|
throw failureReason;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Snackbar open={isFetchingProfile || isFetchingPosts} message={t('Updating')} />
|
<Snackbar open={isFetchingProfile || isFetchingPosts} message={t('Updating')} />
|
||||||
@@ -23,12 +33,12 @@ const ProfilePage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Route = createFileRoute(`${ROUTES.PROFILE}/`)({
|
export const Route = createFileRoute(`${ROUTES.PROFILE}/`)({
|
||||||
loader: ({ context: { queryClient } }) => {
|
loader: ({ context: { queryClient, Api } }) => {
|
||||||
queryClient.ensureQueryData(profileSelfQueryOptions);
|
queryClient.ensureQueryData(profileSelfQueryOptions(Api));
|
||||||
queryClient.ensureQueryData(profilePostsQueryOptions(Api.getAuthenticatedUser()?.id ?? 0));
|
queryClient.ensureQueryData(profilePostsQueryOptions(Api, Api.authenticatedUser?.id ?? 0));
|
||||||
},
|
},
|
||||||
beforeLoad: () => {
|
beforeLoad: ({ context: { Api } }) => {
|
||||||
if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX });
|
if (!Api.hasAuth) throw redirect({ to: ROUTES.INDEX });
|
||||||
},
|
},
|
||||||
component: ProfilePage,
|
component: ProfilePage,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user