refactor API calls and routers

This commit is contained in:
2026-04-04 18:27:45 +02:00
parent 229edd42e9
commit c14864aa29
7 changed files with 141 additions and 109 deletions
+5
View File
@@ -3,6 +3,11 @@ import { render } from '@testing-library/react';
import { vi } from 'vitest'; import { vi } from 'vitest';
import App from './App'; import App from './App';
vi.mock('./shared/api/http', () => ({
getText: vi.fn(async () => ''),
getJson: vi.fn(),
}));
vi.mock('react-router-dom', async () => { vi.mock('react-router-dom', async () => {
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom'); const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
+100 -72
View File
@@ -1,9 +1,9 @@
import * as React from "react"; import * as React from "react";
import "./App.css"; import "./App.css";
import {Faro} from "@grafana/faro-web-sdk"; import {Faro} from "@grafana/faro-web-sdk";
import {createBrowserRouter, Outlet, redirect, RouterProvider} from "react-router-dom"; import {createBrowserRouter, LoaderFunctionArgs, Navigate, Outlet, redirect, RouterProvider} from "react-router-dom";
import CreateCharacter from "./first-edition/pages/CreateCharacter"; import CreateCharacter from "./first-edition/pages/CreateCharacter";
import CharacterList from "./shared/pages/CharacterList"; import CharacterList, {CharacterListItemDto} from "./shared/pages/CharacterList";
import ErrorBoundary from "./shared/ErrorBoundary"; import ErrorBoundary from "./shared/ErrorBoundary";
import Header from "./shared/Header"; import Header from "./shared/Header";
import Footer from "./shared/Footer"; import Footer from "./shared/Footer";
@@ -13,6 +13,11 @@ import { Provider } from "react-redux"
import { useSelector, useDispatch } from 'react-redux' import { useSelector, useDispatch } from 'react-redux'
import {load, setUser, unsetUser, userSelector} from './shared/domain-models/userSlice' import {load, setUser, unsetUser, userSelector} from './shared/domain-models/userSlice'
import {setCharacter} from "./second-edition/domain-models/characterSlice"; import {setCharacter} from "./second-edition/domain-models/characterSlice";
import {getJson, getText} from "./shared/api/http";
import {KarakterInputs} from "./first-edition/domain-models/karakter";
import {Karakter2E} from "./second-edition/domain-models/karakter2E";
const FaroContext = React.createContext<Faro | undefined>(undefined)
function RootLayout() { function RootLayout() {
return ( return (
@@ -26,7 +31,27 @@ function RootLayout() {
) )
} }
function Router(props: {faro?: Faro}) { function IndexRedirect() {
const fetchedUser = useSelector.withTypes<RootState>()(userSelector)
if (fetchedUser.state !== "finished") {
return null;
}
return <Navigate replace to={fetchedUser.email == null ? "/2e/karakter" : "/karaktereim"} />
}
function CreateCharacter1ERoute() {
const faro = React.useContext(FaroContext)
return <CreateCharacter faro={faro} />
}
function CreateCharacter2ERoute() {
const faro = React.useContext(FaroContext)
return <CreateCharacter2E faro={faro} />
}
function UserBootstrap() {
const dispatch = useDispatch.withTypes<AppDispatch>()() const dispatch = useDispatch.withTypes<AppDispatch>()()
const fetchedUser = useSelector.withTypes<RootState>()(userSelector) const fetchedUser = useSelector.withTypes<RootState>()(userSelector)
@@ -37,13 +62,7 @@ function Router(props: {faro?: Faro}) {
let isCancelled = false; let isCancelled = false;
dispatch(load()) dispatch(load())
fetch(`${window.location.origin}/api/User/me`) getText("/api/User/me")
.then(response => {
if (response.ok) {
return response.text();
}
console.log(response)
})
.then(userNameResponse => { .then(userNameResponse => {
if (isCancelled) { if (isCancelled) {
return; return;
@@ -68,74 +87,83 @@ function Router(props: {faro?: Faro}) {
} }
}, [dispatch, fetchedUser.state]) }, [dispatch, fetchedUser.state])
const router = React.useMemo(() => createBrowserRouter([ return null;
{
path: "/",
element: <RootLayout />,
children: [
{
index: true,
loader: () => fetchedUser.email == null ? redirect("/2e/karakter") : redirect("/karaktereim")
},
{
path: "karaktereim",
element: <CharacterList />,
loader: () => fetch(`${window.location.origin}/api/Character`),
ErrorBoundary: ErrorBoundary,
},
{
path: "1e",
loader: () => redirect("/1e/karakter")
},
{
path: "1e/karakter",
element: <CreateCharacter faro={props.faro} />,
ErrorBoundary: ErrorBoundary,
},
{
path: "1e/karakter/:id",
element: <CreateCharacter faro={props.faro}/>,
loader: args => fetch(`${window.location.origin}/api/Character1E/${args.params.id}`),
ErrorBoundary: ErrorBoundary,
},
{
path: "2e",
loader: () => redirect("/2e/karakter")
},
{
path: "2e/karakter",
element: <CreateCharacter2E faro={props.faro}/>,
ErrorBoundary: ErrorBoundary,
},
{
path: "2e/karakter/:id",
element: <CreateCharacter2E faro={props.faro}/>,
loader: async args => {
const response = await fetch(`${window.location.origin}/api/Character2E/${args.params.id}`)
if (response.ok) {
const loaded2Echaracter = await response.json()
dispatch(setCharacter(loaded2Echaracter))
return loaded2Echaracter;
}
return {
isPublic: false,
error: response.statusText,
};
},
ErrorBoundary: ErrorBoundary,
},
],
},
]), [dispatch, fetchedUser.email, props.faro]);
return <RouterProvider router={router}/>
} }
const loadCharacterList = () => getJson<CharacterListItemDto[]>("/api/Character")
const loadCharacter1E = ({params}: LoaderFunctionArgs) => getJson<KarakterInputs & { isPublic: boolean }>(`/api/Character1E/${params.id}`)
const loadCharacter2E = async ({params}: LoaderFunctionArgs) => {
try {
const loaded2Echaracter = await getJson<Karakter2E & { isPublic: boolean }>(`/api/Character2E/${params.id}`)
store.dispatch(setCharacter(loaded2Echaracter))
return loaded2Echaracter;
} catch (error) {
return {
isPublic: false,
error: error instanceof Error ? error.message : "Failed to load character",
};
}
}
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: [
{
index: true,
element: <IndexRedirect />,
},
{
path: "karaktereim",
element: <CharacterList />,
loader: loadCharacterList,
ErrorBoundary: ErrorBoundary,
},
{
path: "1e",
loader: () => redirect("/1e/karakter")
},
{
path: "1e/karakter",
element: <CreateCharacter1ERoute />,
ErrorBoundary: ErrorBoundary,
},
{
path: "1e/karakter/:id",
element: <CreateCharacter1ERoute />,
loader: loadCharacter1E,
ErrorBoundary: ErrorBoundary,
},
{
path: "2e",
loader: () => redirect("/2e/karakter")
},
{
path: "2e/karakter",
element: <CreateCharacter2ERoute />,
ErrorBoundary: ErrorBoundary,
},
{
path: "2e/karakter/:id",
element: <CreateCharacter2ERoute />,
loader: loadCharacter2E,
ErrorBoundary: ErrorBoundary,
},
],
},
]);
function App(props: { faro?: Faro }) { function App(props: { faro?: Faro }) {
return ( return (
<Provider store={store}> <Provider store={store}>
<Router faro={props.faro} /> <FaroContext.Provider value={props.faro}>
<UserBootstrap />
<RouterProvider router={router}/>
</FaroContext.Provider>
</Provider> </Provider>
); );
} }
@@ -1,32 +1,18 @@
import {KarakterInputs} from "../domain-models/karakter"; import {KarakterInputs} from "../domain-models/karakter";
import axios from "axios"; import {postJson} from "../../shared/api/http";
export async function StoreNewCharacter(karakter: KarakterInputs, isPublic: boolean = false) { export async function StoreNewCharacter(karakter: KarakterInputs, isPublic: boolean = false) {
let response = await axios.post(`${window.location.origin}/api/Character1E/`, karakter, { return postJson<KarakterInputs, string>("/api/Character1E/", karakter, {
withCredentials: true,
params: { params: {
isPublic, isPublic,
} }
}) })
if (response.status < 300){
return response.data as string
}
else {
throw Error(response.statusText)
}
} }
export async function UpdateCharacter(id: string, karakter: KarakterInputs, isPublic: boolean = false) { export async function UpdateCharacter(id: string, karakter: KarakterInputs, isPublic: boolean = false) {
let response = await axios.post(`${window.location.origin}/api/Character1E/${id}`, karakter, { return postJson<KarakterInputs, string>(`/api/Character1E/${id}`, karakter, {
withCredentials: true,
params: { params: {
isPublic, isPublic,
} }
}) })
if (response.status < 300){
return response.data as string
}
else {
throw Error(response.statusText)
}
} }
@@ -1,32 +1,18 @@
import axios from "axios"; import {postJson} from "../../shared/api/http";
import {Karakter2E} from "../domain-models/karakter2E"; import {Karakter2E} from "../domain-models/karakter2E";
export async function StoreNewCharacter2E(karakter: Karakter2E, isPublic: boolean = false) { export async function StoreNewCharacter2E(karakter: Karakter2E, isPublic: boolean = false) {
let response = await axios.post(`${window.location.origin}/api/Character2E/`, karakter, { return postJson<Karakter2E, string>("/api/Character2E/", karakter, {
withCredentials: true,
params: { params: {
isPublic, isPublic,
} }
}) })
if (response.status < 300){
return response.data as string
}
else {
throw Error(response.statusText)
}
} }
export async function UpdateCharacter2E(id: string, karakter: Karakter2E, isPublic: boolean = false) { export async function UpdateCharacter2E(id: string, karakter: Karakter2E, isPublic: boolean = false) {
let response = await axios.post(`${window.location.origin}/api/Character2E/${id}`, karakter, { return postJson<Karakter2E, string>(`/api/Character2E/${id}`, karakter, {
withCredentials: true,
params: { params: {
isPublic, isPublic,
} }
}) })
if (response.status < 300){
return response.data as string
}
else {
throw Error(response.statusText)
}
} }
+2 -2
View File
@@ -1,10 +1,10 @@
import * as React from "react"; import * as React from "react";
import axios from "axios";
import {Container, Nav, Navbar} from "react-bootstrap"; import {Container, Nav, Navbar} from "react-bootstrap";
import {unsetUser, userSelector} from "./domain-models/userSlice"; import {unsetUser, userSelector} from "./domain-models/userSlice";
import {AppDispatch, RootState} from "../store"; import {AppDispatch, RootState} from "../store";
import {useDispatch, useSelector} from "react-redux"; import {useDispatch, useSelector} from "react-redux";
import {Link, useNavigate} from "react-router-dom"; import {Link, useNavigate} from "react-router-dom";
import {postVoid} from "./api/http";
function Header() { function Header() {
@@ -13,7 +13,7 @@ function Header() {
const fetchedUser = useSelector.withTypes<RootState>()(userSelector); const fetchedUser = useSelector.withTypes<RootState>()(userSelector);
const logout = async () => { const logout = async () => {
await axios.post("/Identity/Account/Logout") await postVoid("/Identity/Account/Logout")
dispatch(unsetUser()) dispatch(unsetUser())
navigate("/") navigate("/")
} }
+27
View File
@@ -0,0 +1,27 @@
import axios, {AxiosRequestConfig} from "axios";
export const http = axios.create({
withCredentials: true,
});
export async function getJson<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await http.get<T>(url, config)
return response.data
}
export async function getText(url: string, config?: AxiosRequestConfig): Promise<string> {
const response = await http.get<string>(url, {
...config,
responseType: "text",
})
return response.data
}
export async function postJson<TRequest, TResponse>(url: string, body: TRequest, config?: AxiosRequestConfig): Promise<TResponse> {
const response = await http.post<TResponse>(url, body, config)
return response.data
}
export async function postVoid<TRequest = undefined>(url: string, body?: TRequest, config?: AxiosRequestConfig): Promise<void> {
await http.post(url, body, config)
}
+1 -1
View File
@@ -33,7 +33,7 @@ interface CharacterListItemDto2E {
edition: "2e" edition: "2e"
} }
type CharacterListItemDto = CharacterListItemDto1E | CharacterListItemDto2E; export type CharacterListItemDto = CharacterListItemDto1E | CharacterListItemDto2E;
function mapDtoToViewModel(dto: CharacterListItemDto): CharacterListItemViewModel { function mapDtoToViewModel(dto: CharacterListItemDto): CharacterListItemViewModel {
if (dto.edition === "1e") { if (dto.edition === "1e") {