diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7203229..770fa2e 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -3,6 +3,11 @@ import { render } from '@testing-library/react'; import { vi } from 'vitest'; import App from './App'; +vi.mock('./shared/api/http', () => ({ + getText: vi.fn(async () => ''), + getJson: vi.fn(), +})); + vi.mock('react-router-dom', async () => { const actual = await vi.importActual('react-router-dom'); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 572bd6a..a6ccaad 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,9 +1,9 @@ import * as React from "react"; import "./App.css"; 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 CharacterList from "./shared/pages/CharacterList"; +import CharacterList, {CharacterListItemDto} from "./shared/pages/CharacterList"; import ErrorBoundary from "./shared/ErrorBoundary"; import Header from "./shared/Header"; import Footer from "./shared/Footer"; @@ -13,6 +13,11 @@ import { Provider } from "react-redux" import { useSelector, useDispatch } from 'react-redux' import {load, setUser, unsetUser, userSelector} from './shared/domain-models/userSlice' 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(undefined) function RootLayout() { return ( @@ -26,7 +31,27 @@ function RootLayout() { ) } -function Router(props: {faro?: Faro}) { +function IndexRedirect() { + const fetchedUser = useSelector.withTypes()(userSelector) + + if (fetchedUser.state !== "finished") { + return null; + } + + return +} + +function CreateCharacter1ERoute() { + const faro = React.useContext(FaroContext) + return +} + +function CreateCharacter2ERoute() { + const faro = React.useContext(FaroContext) + return +} + +function UserBootstrap() { const dispatch = useDispatch.withTypes()() const fetchedUser = useSelector.withTypes()(userSelector) @@ -37,13 +62,7 @@ function Router(props: {faro?: Faro}) { let isCancelled = false; dispatch(load()) - fetch(`${window.location.origin}/api/User/me`) - .then(response => { - if (response.ok) { - return response.text(); - } - console.log(response) - }) + getText("/api/User/me") .then(userNameResponse => { if (isCancelled) { return; @@ -68,74 +87,83 @@ function Router(props: {faro?: Faro}) { } }, [dispatch, fetchedUser.state]) - const router = React.useMemo(() => createBrowserRouter([ - { - path: "/", - element: , - children: [ - { - index: true, - loader: () => fetchedUser.email == null ? redirect("/2e/karakter") : redirect("/karaktereim") - }, - { - path: "karaktereim", - element: , - loader: () => fetch(`${window.location.origin}/api/Character`), - ErrorBoundary: ErrorBoundary, - }, - { - path: "1e", - loader: () => redirect("/1e/karakter") - }, - { - path: "1e/karakter", - element: , - ErrorBoundary: ErrorBoundary, - }, - { - path: "1e/karakter/:id", - element: , - loader: args => fetch(`${window.location.origin}/api/Character1E/${args.params.id}`), - ErrorBoundary: ErrorBoundary, - }, - { - path: "2e", - loader: () => redirect("/2e/karakter") - }, - { - path: "2e/karakter", - element: , - ErrorBoundary: ErrorBoundary, - }, - { - path: "2e/karakter/:id", - element: , - 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 + return null; } +const loadCharacterList = () => getJson("/api/Character") + +const loadCharacter1E = ({params}: LoaderFunctionArgs) => getJson(`/api/Character1E/${params.id}`) + +const loadCharacter2E = async ({params}: LoaderFunctionArgs) => { + try { + const loaded2Echaracter = await getJson(`/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: , + children: [ + { + index: true, + element: , + }, + { + path: "karaktereim", + element: , + loader: loadCharacterList, + ErrorBoundary: ErrorBoundary, + }, + { + path: "1e", + loader: () => redirect("/1e/karakter") + }, + { + path: "1e/karakter", + element: , + ErrorBoundary: ErrorBoundary, + }, + { + path: "1e/karakter/:id", + element: , + loader: loadCharacter1E, + ErrorBoundary: ErrorBoundary, + }, + { + path: "2e", + loader: () => redirect("/2e/karakter") + }, + { + path: "2e/karakter", + element: , + ErrorBoundary: ErrorBoundary, + }, + { + path: "2e/karakter/:id", + element: , + loader: loadCharacter2E, + ErrorBoundary: ErrorBoundary, + }, + ], + }, +]); + function App(props: { faro?: Faro }) { return ( - + + + + ); } diff --git a/frontend/src/first-edition/api/character.api.ts b/frontend/src/first-edition/api/character.api.ts index 9b1f0c3..2790522 100644 --- a/frontend/src/first-edition/api/character.api.ts +++ b/frontend/src/first-edition/api/character.api.ts @@ -1,32 +1,18 @@ 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) { - let response = await axios.post(`${window.location.origin}/api/Character1E/`, karakter, { - withCredentials: true, + return postJson("/api/Character1E/", karakter, { params: { 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) { - let response = await axios.post(`${window.location.origin}/api/Character1E/${id}`, karakter, { - withCredentials: true, + return postJson(`/api/Character1E/${id}`, karakter, { params: { isPublic, } }) - if (response.status < 300){ - return response.data as string - } - else { - throw Error(response.statusText) - } } diff --git a/frontend/src/second-edition/api/character.api.ts b/frontend/src/second-edition/api/character.api.ts index bddff3e..e45a18a 100644 --- a/frontend/src/second-edition/api/character.api.ts +++ b/frontend/src/second-edition/api/character.api.ts @@ -1,32 +1,18 @@ -import axios from "axios"; +import {postJson} from "../../shared/api/http"; import {Karakter2E} from "../domain-models/karakter2E"; export async function StoreNewCharacter2E(karakter: Karakter2E, isPublic: boolean = false) { - let response = await axios.post(`${window.location.origin}/api/Character2E/`, karakter, { - withCredentials: true, + return postJson("/api/Character2E/", karakter, { params: { 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) { - let response = await axios.post(`${window.location.origin}/api/Character2E/${id}`, karakter, { - withCredentials: true, + return postJson(`/api/Character2E/${id}`, karakter, { params: { isPublic, } }) - if (response.status < 300){ - return response.data as string - } - else { - throw Error(response.statusText) - } } diff --git a/frontend/src/shared/Header.tsx b/frontend/src/shared/Header.tsx index 11185a4..8cd0727 100644 --- a/frontend/src/shared/Header.tsx +++ b/frontend/src/shared/Header.tsx @@ -1,10 +1,10 @@ import * as React from "react"; -import axios from "axios"; import {Container, Nav, Navbar} from "react-bootstrap"; import {unsetUser, userSelector} from "./domain-models/userSlice"; import {AppDispatch, RootState} from "../store"; import {useDispatch, useSelector} from "react-redux"; import {Link, useNavigate} from "react-router-dom"; +import {postVoid} from "./api/http"; function Header() { @@ -13,7 +13,7 @@ function Header() { const fetchedUser = useSelector.withTypes()(userSelector); const logout = async () => { - await axios.post("/Identity/Account/Logout") + await postVoid("/Identity/Account/Logout") dispatch(unsetUser()) navigate("/") } diff --git a/frontend/src/shared/api/http.ts b/frontend/src/shared/api/http.ts new file mode 100644 index 0000000..c2d2648 --- /dev/null +++ b/frontend/src/shared/api/http.ts @@ -0,0 +1,27 @@ +import axios, {AxiosRequestConfig} from "axios"; + +export const http = axios.create({ + withCredentials: true, +}); + +export async function getJson(url: string, config?: AxiosRequestConfig): Promise { + const response = await http.get(url, config) + return response.data +} + +export async function getText(url: string, config?: AxiosRequestConfig): Promise { + const response = await http.get(url, { + ...config, + responseType: "text", + }) + return response.data +} + +export async function postJson(url: string, body: TRequest, config?: AxiosRequestConfig): Promise { + const response = await http.post(url, body, config) + return response.data +} + +export async function postVoid(url: string, body?: TRequest, config?: AxiosRequestConfig): Promise { + await http.post(url, body, config) +} \ No newline at end of file diff --git a/frontend/src/shared/pages/CharacterList.tsx b/frontend/src/shared/pages/CharacterList.tsx index c37ccb7..2dbe463 100644 --- a/frontend/src/shared/pages/CharacterList.tsx +++ b/frontend/src/shared/pages/CharacterList.tsx @@ -33,7 +33,7 @@ interface CharacterListItemDto2E { edition: "2e" } -type CharacterListItemDto = CharacterListItemDto1E | CharacterListItemDto2E; +export type CharacterListItemDto = CharacterListItemDto1E | CharacterListItemDto2E; function mapDtoToViewModel(dto: CharacterListItemDto): CharacterListItemViewModel { if (dto.edition === "1e") {