This commit is contained in:
Kilian Hofmann 2024-07-29 00:40:35 +02:00
parent 7723dd0722
commit 0b661c7ccc
33 changed files with 620 additions and 372 deletions

1
exam/dist/assets/index-BliQ0crd.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,10 +5,10 @@
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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/mui-C4H8cxTH.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-C0csOcmc.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-53GMXZgr.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="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css">
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/index-D83Ey19k.css">

View File

@ -14,7 +14,7 @@
"FailedUpdate_Duplicate:username:userUpdate": "Ein Benutzer mit diesem Benutzernamen 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",
"NotAllowed_postUpdate": "Keine Berechtigung",

View File

@ -1,23 +1,23 @@
{
"Unauthorized": "NotAllowed",
"Unauthorized": "Not allowed",
"NotAllowed_login": "Invalid email or password",
"NotFound_user:login": "User does not exist",
"MissingField_email:login": "E-Mail required",
"MissingField_password:login": "Password required",
"NotAllowed_deletPost": "NotAllowed",
"NotAllowed_deletPost": "Not allowed",
"NotFound_post:deletePost": "Post not found",
"NotAllowed_userUpdate": "NotAllowed",
"NotAllowed_userUpdate": "Not allowed",
"NotFound_user:userUpdate": "User not found",
"FailedUpdate_Duplicate:userUpdate": "A user with this username 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",
"NotAllowed_postUpdate": "NotAllowed",
"NotAllowed_postUpdate": "Not allowed",
"NotFound_post:postUpdate": "Post not found",
"Duplicate_user:register": "A user with this username or email already exists",

File diff suppressed because one or more lines are too long

View File

@ -12,5 +12,7 @@ module.exports = {
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
},
};

View File

@ -24,6 +24,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.0.0",
"use-local-storage-state": "^19.3.1",
"zustand": "^4.5.4"
},
"devDependencies": {

View File

@ -32,9 +32,6 @@ importers:
'@tanstack/react-router':
specifier: ^1.45.8
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:
specifier: ^23.12.2
version: 23.12.2
@ -53,6 +50,9 @@ importers:
react-i18next:
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)
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:
specifier: ^4.5.4
version: 4.5.4(@types/react@18.3.3)(react@18.3.1)
@ -72,6 +72,9 @@ importers:
'@tanstack/router-plugin':
specifier: ^1.45.8
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':
specifier: ^18.3.3
version: 18.3.3
@ -1842,6 +1845,13 @@ packages:
uri-js@4.4.1:
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:
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies:
@ -3721,6 +3731,11 @@ snapshots:
dependencies:
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):
dependencies:
react: 18.3.1

View File

@ -14,7 +14,7 @@
"FailedUpdate_Duplicate:username:userUpdate": "Ein Benutzer mit diesem Benutzernamen 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",
"NotAllowed_postUpdate": "Keine Berechtigung",

View File

@ -1,23 +1,23 @@
{
"Unauthorized": "NotAllowed",
"Unauthorized": "Not allowed",
"NotAllowed_login": "Invalid email or password",
"NotFound_user:login": "User does not exist",
"MissingField_email:login": "E-Mail required",
"MissingField_password:login": "Password required",
"NotAllowed_deletPost": "NotAllowed",
"NotAllowed_deletPost": "Not allowed",
"NotFound_post:deletePost": "Post not found",
"NotAllowed_userUpdate": "NotAllowed",
"NotAllowed_userUpdate": "Not allowed",
"NotFound_user:userUpdate": "User not found",
"FailedUpdate_Duplicate:userUpdate": "A user with this username 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",
"NotAllowed_postUpdate": "NotAllowed",
"NotAllowed_postUpdate": "Not allowed",
"NotFound_post:postUpdate": "Post not found",
"Duplicate_user:register": "A user with this username or email already exists",

View File

@ -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;

326
exam/react/src/api/Api.tsx Normal file
View File

@ -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;

View File

@ -14,7 +14,7 @@ import { useForm } from '@tanstack/react-form';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { t } from 'i18next';
import { FC, FormEvent, useState } from 'react';
import Api from '../../../api/Api';
import { useApi } from '../../../api/Api';
import { PostAuth, PostUpdate } from '../../../types/Post';
import ErrorComponent from '../../Error/ErrorComponent';
@ -29,6 +29,11 @@ const PostEditDialog: FC<Props> = ({ post, open, onClose }) => {
const [error, setError] = useState<any>();
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({
mutationFn: ({ data, id }: { data: PostUpdate; id: number }) => {
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
} catch (error: any) {
setError(error);
} catch (_error: any) {
setError(_error);
}
},
});
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
const queryClient = useQueryClient();
const handleClose = () => {
form.reset();
setError(undefined);

View File

@ -16,7 +16,7 @@ import { useForm } from '@tanstack/react-form';
import { useMutation } from '@tanstack/react-query';
import { FC, FormEvent, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Api from '../../../api/Api';
import { useApi } from '../../../api/Api';
import { UserCreate } from '../../../types/User';
import ErrorComponent from '../../Error/ErrorComponent';
@ -29,16 +29,17 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
//eslint-disable-next-line @typescript-eslint/no-explicit-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({
mutationFn: ({ data }: { data: UserCreate }) => {
return Api.createUser(data);
},
});
const { t } = useTranslation();
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
const form = useForm<UserCreate>({
defaultValues: {
username: '',
@ -55,8 +56,8 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
}
);
//eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
setError(error);
} catch (_error: any) {
setError(_error);
}
},
});

View File

@ -13,7 +13,7 @@ import { useForm } from '@tanstack/react-form';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { t } from 'i18next';
import { FC, FormEvent, useState } from 'react';
import Api from '../../../api/Api';
import { useApi } from '../../../api/Api';
import { User, UserUpdate } from '../../../types/User';
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
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({
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => {
return Api.updateUser(data, id);
@ -41,12 +46,12 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
onSubmit: async ({ value }) => {
try {
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: () => {
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 });
},
onError: setError,
@ -54,16 +59,12 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
);
//eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
setError(error);
} catch (_error: any) {
setError(_error);
}
},
});
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
const queryClient = useQueryClient();
const handleClose = () => {
form.reset();
setError(undefined);

View File

@ -24,7 +24,7 @@ import { useForm } from '@tanstack/react-form';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { t } from 'i18next';
import { FC, FormEvent, useState } from 'react';
import Api from '../../../api/Api';
import { useApi } from '../../../api/Api';
import { User, UserImageUpdate } from '../../../types/User';
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
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({
mutationFn: ({ data, id }: { data: UserImageUpdate; id?: number }) => {
return Api.updateUserImage(data, id);
@ -48,27 +53,24 @@ const UserImageDialog: FC<Props> = ({ user, open, onClose }) => {
onSubmit: async ({ value }) => {
try {
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: () => {
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 });
},
onError: setError,
}
);
//eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
setError(error);
} catch (_error: any) {
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 handleClose = () => {

View File

@ -20,6 +20,8 @@ const ErrorComponent: FC<Props> = ({ error, context, color = 'error.main' }) =>
case ERRORS.NOT_FOUND:
return <Typography color={color}>{t(error.code, { context: `${error.entity}:${context}` })}</Typography>;
case ERRORS.NOT_ALLOWED:
case ERRORS.UNAUTHORIZED:
console.log(error, context);
return <Typography color={color}>{t(error.code, { context })}</Typography>;
case ERRORS.FAILED_UPDATE:
return error.fields.map((field: string, index: number) => (

View File

@ -3,7 +3,7 @@ import { useForm } from '@tanstack/react-form';
import { useRouter } from '@tanstack/react-router';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Api from '../../../api/Api';
import { useApi } from '../../../api/Api';
import { Login } from '../../../types/User';
import ErrorComponent from '../../Error/ErrorComponent';
@ -16,6 +16,7 @@ const LoginForm: FC<Props> = ({ handleClose }) => {
const { t } = useTranslation();
const router = useRouter();
const Api = useApi();
const form = useForm<Login>({
defaultValues: {
@ -28,8 +29,8 @@ const LoginForm: FC<Props> = ({ handleClose }) => {
router.invalidate();
handleClose();
//eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
setError(error);
} catch (_error: any) {
setError(_error);
}
},
});

View File

@ -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 { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from '@tanstack/react-router';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Api from '../../../api/Api';
import { useApi } from '../../../api/Api';
import { PostCreate } from '../../../types/Post';
import ErrorComponent from '../../Error/ErrorComponent';
@ -16,6 +16,7 @@ const PostForm: FC = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const navigate = useNavigate();
const Api = useApi();
const newMutation = useMutation({
mutationFn: ({ data }: { data: PostCreate }) => {
@ -42,8 +43,8 @@ const PostForm: FC = () => {
);
//eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
setError(error);
} catch (_error: any) {
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>
</form>
);

View File

@ -10,10 +10,9 @@ import {
useScrollTrigger,
} from '@mui/material';
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 Api from '../../api/Api';
import { User } from '../../types/User';
import { useApi } from '../../api/Api';
import LanguageMenu from '../Menus/Language/LanguageMenu';
import UserMenu from '../Menus/User/UserMenu';
@ -31,12 +30,10 @@ const ElevationScroll = ({ children }: { children: ReactElement }) => {
const Header: FC = () => {
const [anchorUserMenu, setAnchorUserMenu] = useState<null | HTMLElement>(null);
const [anchorLanguageMenu, setAnchorLanguageMenu] = useState<null | HTMLElement>(null);
const [user, setUser] = useState<User>();
const { t } = useTranslation();
const isLoading = useRouterState({ select: (s) => s.status === 'pending' });
useEffect(() => Api.subscribeToAuthenticatedUser((user) => setUser(user)), []);
const Api = useApi();
const handleClose = () => {
setAnchorLanguageMenu(null);
@ -59,9 +56,9 @@ const Header: FC = () => {
<IconButton size="large" onClick={(event) => setAnchorLanguageMenu(event.currentTarget)}>
<Translate sx={{ color: 'white' }} />
</IconButton>
{user ? (
{Api.authenticatedUser ? (
<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 />
</Avatar>
</IconButton>

View File

@ -3,7 +3,7 @@ import { useMatch, useNavigate, useRouter } from '@tanstack/react-router';
import { t } from 'i18next';
import { FC, useState } from 'react';
import { Trans } from 'react-i18next/TransWithoutContext';
import Api from '../../../api/Api';
import { useApi } from '../../../api/Api';
import { ROUTES } from '../../../types/Routes';
import RegisterDialog from '../../Dialogs/Register/RegisterDialog';
import LoginForm from '../../Forms/Login/LoginForm';
@ -20,7 +20,7 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
const router = useRouter();
const match = useMatch({ from: '/profile/', strict: true, shouldThrow: false });
const user = Api.getAuthenticatedUser();
const Api = useApi();
const _handleClose = () => {
setRegister(false);
@ -47,7 +47,7 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
},
}}
>
{user ? (
{Api.authenticatedUser ? (
[
<MenuItem
selected={!!match}

View File

@ -20,7 +20,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Link, useNavigate } from '@tanstack/react-router';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Api from '../../api/Api';
import { useApi } from '../../api/Api';
import { PostAuth, PostNonAuth } from '../../types/Post';
import convertDate from '../../utils/date';
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
const [error, setError] = useState<any>();
const { t } = useTranslation();
const queryClient = useQueryClient();
const navigate = useNavigate();
const Api = useApi();
const deleteMutation = useMutation({
mutationFn: (id: number) => {
return Api.deletePost(id);
},
});
const { t } = useTranslation();
const queryClient = useQueryClient();
const navigate = useNavigate();
return (
<Card>
<CardHeader
avatar={
!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 }}>
<Avatar alt={post.user.username} src={`${post.user.image}`}>
<Person />
@ -73,7 +74,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
}
title={
!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 }}>
{post.user.username}
</MUILink>
@ -96,7 +97,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
<CardActions>
{!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)}>
{t('Edit')}
@ -104,7 +105,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
<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)}>
{t('Delete')}
@ -152,7 +153,7 @@ const Post: FC<Props> = ({ post, disableActions }) => {
}}
>
<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>
</Snackbar>
<Snackbar open={deleteMutation.isPending} message={t('Deleting')} />

View File

@ -1,13 +1,9 @@
import { QueryClient, QueryClientProvider, useQueryClient, useQueryErrorResetBoundary } 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 { QueryClient, QueryClientProvider } from '@tanstack/react-query';
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';
@ -21,61 +17,6 @@ import '@fontsource/roboto/700.css';
// Query Client
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
const rootElement = document.getElementById('root')!;
if (!rootElement.innerHTML) {
@ -83,7 +24,9 @@ if (!rootElement.innerHTML) {
root.render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<ApiProvider>
<Router />
</ApiProvider>
{process.env.NODE_ENV === 'development' && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider>
</StrictMode>

View File

@ -1,8 +1,8 @@
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({
queryKey: ['posts', { page: page ?? 0, hasAuth: Api.hasAuth() }],
queryFn: () => Api.posts(page),
queryKey: ['posts', { page: page ?? 0, hasAuth: Api.hasAuth }],
queryFn: async () => await Api.posts(page),
});

View File

@ -1,19 +1,20 @@
import { queryOptions } from '@tanstack/react-query';
import Api from '../api/Api';
import { useApi } from '../api/Api';
export const profileSelfQueryOptions = queryOptions({
queryKey: ['profile'],
queryFn: () => Api.user(),
});
export const profileSelfQueryOptions = (Api: ReturnType<typeof useApi>) =>
queryOptions({
queryKey: ['profile'],
queryFn: async () => await Api.user(),
});
export const profileQueryOptions = (id?: number) =>
export const profileQueryOptions = (Api: ReturnType<typeof useApi>, id?: number) =>
queryOptions({
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({
queryKey: ['profilePosts', { id }],
queryFn: () => Api.userPosts(id),
queryFn: async () => await Api.userPosts(id),
});

70
exam/react/src/router.tsx Normal file
View File

@ -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;

View File

@ -1,7 +1,9 @@
import { Box } from '@mui/material';
import { QueryClient } from '@tanstack/react-query';
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
import { QueryClient, useQueryErrorResetBoundary } from '@tanstack/react-query';
import { createRootRouteWithContext, ErrorRouteComponent, Outlet, useRouter } from '@tanstack/react-router';
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
import { useEffect } from 'react';
import { useApi } from '../api/Api';
import Header from '../components/Header/Header';
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,
errorComponent: ErrorComponent,
});

View File

@ -12,15 +12,16 @@ import { useSuspenseQuery } from '@tanstack/react-query';
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Api from '../api/Api';
import { useApi } from '../api/Api';
import PostForm from '../components/Forms/Post/PostForm';
import Post from '../components/Post/Post';
import { postsQueryOptions } from '../queries/postsQuery';
import { ROUTES } from '../types/Routes';
const Home = () => {
const Api = useApi();
const { page } = Route.useSearch();
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(page));
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(Api, page));
const { t } = useTranslation();
const navigate = useNavigate();
const theme = useTheme();
@ -62,7 +63,7 @@ const Home = () => {
)}
/>
</Grid>
{Api.hasAuth() ? (
{Api.hasAuth ? (
<Grid item xs={12}>
<PostForm />
</Grid>
@ -79,7 +80,8 @@ const Home = () => {
export const Route = createFileRoute(ROUTES.INDEX)({
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 } => {
return {
page: search?.page !== undefined ? Number(search?.page ?? 0) : undefined,

View File

@ -2,21 +2,35 @@ import { Snackbar } from '@mui/material';
import { useSuspenseQuery } from '@tanstack/react-query';
import { createFileRoute, redirect } from '@tanstack/react-router';
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 { profilePostsQueryOptions, profileQueryOptions } from '../../queries/profileQuery';
import { PostAuth } from '../../types/Post';
import { ROUTES } from '../../types/Routes';
const ProfilePage = () => {
const Api = useApi();
const { id } = Route.useParams();
const { data: profileQuery, isFetching: isFetchingProfile } = useSuspenseQuery(profileQueryOptions(id));
const { data: profilePostsQuery, isFetching: isFetchingPosts } = useSuspenseQuery(profilePostsQueryOptions(id));
const {
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 (
<>
<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) }),
stringify: ({ id }) => ({ id: id.toString() }),
},
loader: ({ context: { queryClient }, params: { id } }) => {
queryClient.ensureQueryData(profileQueryOptions(id));
queryClient.ensureQueryData(profilePostsQueryOptions(id));
loader: ({ context: { queryClient, Api }, params: { id } }) => {
console.log('LOAD PROFILE');
queryClient.ensureQueryData(profileQueryOptions(Api, id));
queryClient.ensureQueryData(profilePostsQueryOptions(Api, id));
},
beforeLoad: ({ params: { id } }) => {
if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX });
if (id === Api.getAuthenticatedUser()?.id) throw redirect({ to: ROUTES.PROFILE });
beforeLoad: ({ params: { id }, context: { Api } }) => {
console.log('BEFORE PROFILE');
if (!Api.hasAuth) throw redirect({ to: ROUTES.INDEX });
if (id === Api.authenticatedUser?.id) throw redirect({ to: ROUTES.PROFILE });
},
component: ProfilePage,
});

View File

@ -2,18 +2,28 @@ import { Snackbar } from '@mui/material';
import { useSuspenseQuery } from '@tanstack/react-query';
import { createFileRoute, redirect } from '@tanstack/react-router';
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 { profilePostsQueryOptions, profileSelfQueryOptions } from '../../queries/profileQuery';
import { PostAuth } from '../../types/Post';
import { ROUTES } from '../../types/Routes';
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(
profilePostsQueryOptions(Api.getAuthenticatedUser()?.id ?? 0)
profilePostsQueryOptions(Api, Api.authenticatedUser?.id ?? 0)
);
if (failureReason && 'code' in failureReason && failureReason.code === ERRORS.UNAUTHORIZED) {
throw failureReason;
}
return (
<>
<Snackbar open={isFetchingProfile || isFetchingPosts} message={t('Updating')} />
@ -23,12 +33,12 @@ const ProfilePage = () => {
};
export const Route = createFileRoute(`${ROUTES.PROFILE}/`)({
loader: ({ context: { queryClient } }) => {
queryClient.ensureQueryData(profileSelfQueryOptions);
queryClient.ensureQueryData(profilePostsQueryOptions(Api.getAuthenticatedUser()?.id ?? 0));
loader: ({ context: { queryClient, Api } }) => {
queryClient.ensureQueryData(profileSelfQueryOptions(Api));
queryClient.ensureQueryData(profilePostsQueryOptions(Api, Api.authenticatedUser?.id ?? 0));
},
beforeLoad: () => {
if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX });
beforeLoad: ({ context: { Api } }) => {
if (!Api.hasAuth) throw redirect({ to: ROUTES.INDEX });
},
component: ProfilePage,
});