Automatic deployment of PR to test server #4
@ -1,57 +1,21 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import { useBackend } from "./hooks/backend.ts";
|
|
||||||
import { paths } from "./lib/api";
|
|
||||||
import suLogoLandscape from "./assets/SU_logo_optimized.svg";
|
import suLogoLandscape from "./assets/SU_logo_optimized.svg";
|
||||||
import { ProfileContext } from "./hooks/profile.ts";
|
import { ProfileContext } from "./hooks/profile.ts";
|
||||||
import Studentportalen from "./Studentportalen.tsx";
|
import Studentportalen from "./Studentportalen.tsx";
|
||||||
|
import { useViewTransitioningFetch } from "./hooks/fetch";
|
||||||
type Profile =
|
|
||||||
paths["/profile"]["get"]["responses"]["200"]["content"]["application/json"];
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { client } = useBackend();
|
const { data: profile, error } = useViewTransitioningFetch("/profile");
|
||||||
const [state, setState] = useState<
|
|
||||||
"initializing" | "authenticating" | "error" | Profile
|
|
||||||
>("initializing");
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (!profile) {
|
||||||
const controller = new AbortController();
|
|
||||||
withViewTransition(() => {
|
|
||||||
return client
|
|
||||||
.GET("/profile", { signal: controller.signal })
|
|
||||||
.then(({ data, response }) => {
|
|
||||||
if (data) {
|
|
||||||
setState(data);
|
|
||||||
}
|
|
||||||
if (response.status === 401) {
|
|
||||||
setState("authenticating");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (error.name !== "AbortError") {
|
|
||||||
setState("error");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
controller.abort();
|
|
||||||
};
|
|
||||||
}, [client]);
|
|
||||||
|
|
||||||
if (state === "initializing") {
|
|
||||||
return splashScreen("Loading...");
|
return splashScreen("Loading...");
|
||||||
}
|
}
|
||||||
if (state === "authenticating") {
|
if (error) {
|
||||||
return splashScreen("Logging in...");
|
|
||||||
}
|
|
||||||
if (state === "error") {
|
|
||||||
return splashScreen("Application failed to start");
|
return splashScreen("Application failed to start");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProfileContext value={state}>
|
<ProfileContext value={profile}>
|
||||||
<Studentportalen />
|
<Studentportalen />
|
||||||
</ProfileContext>
|
</ProfileContext>
|
||||||
);
|
);
|
||||||
@ -70,9 +34,4 @@ function splashScreen(extraContent: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function withViewTransition(f: () => Promise<unknown>): void {
|
|
||||||
if (document.startViewTransition) void document.startViewTransition(f);
|
|
||||||
else void f();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
39
frontend/src/hooks/fetch.d.ts
vendored
Normal file
39
frontend/src/hooks/fetch.d.ts
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Using a .js & .d.ts file to make the types work well at use sites
|
||||||
|
// and make TypeScript, IDE, and eslint happy.
|
||||||
|
//
|
||||||
|
// Important to use `useMemo` on any options object passed in
|
||||||
|
// since it is used as a dependency to re-fetch
|
||||||
|
|
||||||
|
import {
|
||||||
|
PathsWithMethod,
|
||||||
|
type RequiredKeysOf,
|
||||||
|
} from "openapi-typescript-helpers";
|
||||||
|
import { paths } from "../lib/api";
|
||||||
|
import { FetchResponse, MaybeOptionalInit } from "openapi-fetch";
|
||||||
|
|
||||||
|
type InitParam<Init> =
|
||||||
|
RequiredKeysOf<Init> extends never
|
||||||
|
? [(Init & { [key: string]: unknown })?]
|
||||||
|
: [Init & { [key: string]: unknown }];
|
||||||
|
|
||||||
|
export function useViewTransitioningFetch<
|
||||||
|
Path extends PathsWithMethod<paths, "get">,
|
||||||
|
Init extends MaybeOptionalInit<paths[Path], "get">,
|
||||||
|
>(
|
||||||
|
path: Path,
|
||||||
|
...init: InitParam<Init>
|
||||||
|
): Omit<
|
||||||
|
FetchResponse<paths[Path]["get"], Init, "application/json">,
|
||||||
|
"response"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function useFetch<
|
||||||
|
Path extends PathsWithMethod<paths, "get">,
|
||||||
|
Init extends MaybeOptionalInit<paths[Path], "get">,
|
||||||
|
>(
|
||||||
|
path: Path,
|
||||||
|
...init: InitParam<Init>
|
||||||
|
): Omit<
|
||||||
|
FetchResponse<paths[Path]["get"], Init, "application/json">,
|
||||||
|
"response"
|
||||||
|
>;
|
||||||
65
frontend/src/hooks/fetch.js
Normal file
65
frontend/src/hooks/fetch.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// Using a .js & .d.ts file to make the types work well at use sites
|
||||||
|
// and make TypeScript, IDE, and eslint happy.
|
||||||
|
//
|
||||||
|
// Important to use `useMemo` on any options object passed in
|
||||||
|
// since it is used as a dependency to re-fetch
|
||||||
|
|
||||||
|
import { useBackend } from "./backend.ts";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useFetch(path, options) {
|
||||||
|
const { client } = useBackend();
|
||||||
|
const [response, setResponse] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
void doFetch(client, path, options, abortController, setResponse);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
abortController.abort();
|
||||||
|
};
|
||||||
|
}, [client, path, options]);
|
||||||
|
|
||||||
|
return { data: response.data, error: response.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useViewTransitioningFetch(path, options) {
|
||||||
|
const { client } = useBackend();
|
||||||
|
const [response, setResponse] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
|
withViewTransition(function () {
|
||||||
|
return doFetch(client, path, options, abortController, setResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
abortController.abort();
|
||||||
|
};
|
||||||
|
}, [client, path, options]);
|
||||||
|
|
||||||
|
return { data: response.data, error: response.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doFetch(client, path, options, abortController, setResponse) {
|
||||||
|
try {
|
||||||
|
const response = await client.GET(path, {
|
||||||
|
...options,
|
||||||
|
signal: abortController.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
setResponse(response);
|
||||||
|
} catch (error) {
|
||||||
|
if (abortController.signal.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setResponse({ error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function withViewTransition(f) {
|
||||||
|
if (document.startViewTransition) void document.startViewTransition(f);
|
||||||
|
else void f();
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user