This commit is contained in:
2024-07-29 00:40:35 +02:00
parent 7723dd0722
commit 0b661c7ccc
33 changed files with 620 additions and 372 deletions
+2
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',
},
};
+1
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": {
+18 -3
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
@@ -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",
@@ -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",
-186
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
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;
@@ -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);
@@ -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);
}
},
});
@@ -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);
@@ -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 = () => {
@@ -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) => (
@@ -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);
}
},
});
@@ -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>
);
+5 -8
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>
@@ -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}
+11 -10
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')} />
+8 -65
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>
+4 -4
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),
});
+10 -9
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
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;
+30 -3
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,
});
+6 -4
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,
+26 -10
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,
});
+18 -8
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,
});