Implement basic API interaction
This commit is contained in:
parent
a456f8155b
commit
73b11482ff
@ -1,18 +1,81 @@
|
|||||||
|
import "@mantine/core/styles.css";
|
||||||
|
|
||||||
import { cssBundleHref } from "@remix-run/css-bundle";
|
import { cssBundleHref } from "@remix-run/css-bundle";
|
||||||
import type { LinksFunction } from "@remix-run/node";
|
import type { LinksFunction } from "@remix-run/node";
|
||||||
import {
|
import {
|
||||||
|
Link,
|
||||||
Links,
|
Links,
|
||||||
LiveReload,
|
LiveReload,
|
||||||
Meta,
|
Meta,
|
||||||
Outlet,
|
Outlet,
|
||||||
Scripts,
|
Scripts,
|
||||||
ScrollRestoration,
|
ScrollRestoration,
|
||||||
|
isRouteErrorResponse,
|
||||||
|
useRouteError,
|
||||||
} from "@remix-run/react";
|
} from "@remix-run/react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ColorSchemeScript,
|
||||||
|
Container,
|
||||||
|
Group,
|
||||||
|
MantineProvider,
|
||||||
|
Stack,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { TailfinAppShell } from "./ui/nav/app-shell";
|
||||||
|
import { IconRocket } from "@tabler/icons-react";
|
||||||
|
import Providers from "./providers";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
|
|
||||||
export const links: LinksFunction = () => [
|
export const links: LinksFunction = () => [
|
||||||
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
|
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function ErrorBoundary() {
|
||||||
|
const error = useRouteError();
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Oops!</title>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<Meta />
|
||||||
|
<Links />
|
||||||
|
<ColorSchemeScript />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<MantineProvider>
|
||||||
|
<Container>
|
||||||
|
<Stack>
|
||||||
|
<Title order={2} pt="xl" style={{ textAlign: "center" }}>
|
||||||
|
{isRouteErrorResponse(error)
|
||||||
|
? `Error ${error.status} - ${error.statusText}`
|
||||||
|
: error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Unknown Error"}
|
||||||
|
</Title>
|
||||||
|
<Group justify="center">
|
||||||
|
<Button
|
||||||
|
leftSection={<IconRocket />}
|
||||||
|
component={Link}
|
||||||
|
to="/"
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
Get me out of here!
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
</MantineProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@ -21,12 +84,18 @@ export default function App() {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
|
<ColorSchemeScript />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<MantineProvider theme={{ primaryColor: "violet" }}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
<LiveReload />
|
<LiveReload />
|
||||||
|
</MantineProvider>
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
</QueryClientProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
@ -1,41 +1,5 @@
|
|||||||
import type { MetaFunction } from "@remix-run/node";
|
import { Outlet } from "@remix-run/react";
|
||||||
|
|
||||||
export const meta: MetaFunction = () => {
|
export default function Tailfin() {
|
||||||
return [
|
return <Outlet />;
|
||||||
{ title: "New Remix App" },
|
|
||||||
{ name: "description", content: "Welcome to Remix!" },
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
return (
|
|
||||||
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
|
|
||||||
<h1>Welcome to Remix</h1>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href="https://remix.run/tutorials/blog"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
15m Quickstart Blog Tutorial
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href="https://remix.run/tutorials/jokes"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Deep Dive Jokes App Tutorial
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a target="_blank" href="https://remix.run/docs" rel="noreferrer">
|
|
||||||
Remix Docs
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
40
web/app/routes/logbook.flights.$id/route.tsx
Normal file
40
web/app/routes/logbook.flights.$id/route.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { client } from "@/util/api";
|
||||||
|
import { List, Stack, Text } from "@mantine/core";
|
||||||
|
import { useParams } from "@remix-run/react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export default function Flight() {
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const flight = useQuery({
|
||||||
|
queryKey: [params.id],
|
||||||
|
queryFn: async () =>
|
||||||
|
await client.get(`/flights/${params.id}`).then((res) => res.data),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack h="calc(100vh - 95px)" m="0" p="0">
|
||||||
|
{flight.isError ? (
|
||||||
|
<Text c="red">Error fetching flight</Text>
|
||||||
|
) : flight.isPending ? (
|
||||||
|
<Text>Loading...</Text>
|
||||||
|
) : (
|
||||||
|
<List>
|
||||||
|
{Object.entries(flight.data).map(([key, value]) =>
|
||||||
|
value && value.length !== 0 ? (
|
||||||
|
<List.Item key={key}>
|
||||||
|
<Text span>
|
||||||
|
<Text span fw={700}>
|
||||||
|
{key}
|
||||||
|
</Text>
|
||||||
|
: <Text span>{value}</Text>
|
||||||
|
</Text>
|
||||||
|
</List.Item>
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
19
web/app/routes/logbook.flights._index/route.tsx
Normal file
19
web/app/routes/logbook.flights._index/route.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Center, Container, Stack } from "@mantine/core";
|
||||||
|
import { MobileFlightsList } from "@/routes/logbook.flights/flights-list";
|
||||||
|
import { IconFeather } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export default function Flights() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container visibleFrom="md" h="calc(100vh - 95px)">
|
||||||
|
<Stack align="center" justify="center" h="100%">
|
||||||
|
<IconFeather size="3rem" />
|
||||||
|
<Center>Select a flight</Center>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
<Container hiddenFrom="md">
|
||||||
|
<MobileFlightsList />
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
85
web/app/routes/logbook.flights/flights-list.tsx
Normal file
85
web/app/routes/logbook.flights/flights-list.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { client } from "@/util/api";
|
||||||
|
import {
|
||||||
|
Divider,
|
||||||
|
NavLink,
|
||||||
|
Text,
|
||||||
|
Container,
|
||||||
|
Button,
|
||||||
|
ScrollArea,
|
||||||
|
Stack,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { Link, useLocation } from "@remix-run/react";
|
||||||
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export function FlightsList() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const flights = useQuery({
|
||||||
|
queryKey: ["flights-list"],
|
||||||
|
queryFn: () => client.get(`/flights`).then((res) => res.data),
|
||||||
|
});
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const page = location.pathname.split("/")[3];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack p="0" m="0" gap="0">
|
||||||
|
<Button variant="outline" leftSection={<IconPlus />} mb="md">
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
<ScrollArea>
|
||||||
|
{flights.data ? (
|
||||||
|
flights.data.map((flight, index) => (
|
||||||
|
<NavLink
|
||||||
|
key={index}
|
||||||
|
component={Link}
|
||||||
|
to={`/logbook/flights/${flight.id}`}
|
||||||
|
label={`${flight.date}`}
|
||||||
|
active={page === flight.id}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text p="sm">No Flights</Text>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MobileFlightsList() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const flights = useQuery({
|
||||||
|
queryKey: ["flights-list"],
|
||||||
|
queryFn: () => client.get(`/flights`).then((res) => res.data),
|
||||||
|
});
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const page = location.pathname.split("/")[3];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack p="0" m="0" justify="space-between" h="calc(100vh - 95px)">
|
||||||
|
<ScrollArea h="calc(100vh - 95px - 50px">
|
||||||
|
{flights.data ? (
|
||||||
|
flights.data.map((flight, index) => (
|
||||||
|
<NavLink
|
||||||
|
key={index}
|
||||||
|
component={Link}
|
||||||
|
to={`/logbook/flights/${flight.id}`}
|
||||||
|
label={`${flight.date}`}
|
||||||
|
active={page === flight.id}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text p="sm">No Flights</Text>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
<Button variant="outline" leftSection={<IconPlus />} mt="md">
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { FlightsList, MobileFlightsList };
|
22
web/app/routes/logbook.flights/route.tsx
Normal file
22
web/app/routes/logbook.flights/route.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Divider, Grid, Container } from "@mantine/core";
|
||||||
|
import { Outlet } from "@remix-run/react";
|
||||||
|
import { FlightsList } from "./flights-list";
|
||||||
|
|
||||||
|
export default function FlightsLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid h="100%" visibleFrom="md">
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<FlightsList />
|
||||||
|
</Grid.Col>
|
||||||
|
<Divider orientation="vertical" m="sm" />
|
||||||
|
<Grid.Col span="auto">
|
||||||
|
<Outlet />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
<Container hiddenFrom="md">
|
||||||
|
<Outlet />
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
12
web/app/routes/logbook.me/route.tsx
Normal file
12
web/app/routes/logbook.me/route.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useMe } from "@/util/hooks";
|
||||||
|
import { Container, Title } from "@mantine/core";
|
||||||
|
|
||||||
|
export default function Me() {
|
||||||
|
const me = useMe();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Title order={2}>{me.data?.username}</Title>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
20
web/app/routes/logbook/route.tsx
Normal file
20
web/app/routes/logbook/route.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { TailfinAppShell } from "@/ui/nav/app-shell";
|
||||||
|
import type { MetaFunction } from "@remix-run/node";
|
||||||
|
import { Outlet } from "@remix-run/react";
|
||||||
|
|
||||||
|
export const meta: MetaFunction = () => {
|
||||||
|
return [
|
||||||
|
{ title: "Tailfin" },
|
||||||
|
{ name: "description", content: "Self-hosted flight logbook" },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
return (
|
||||||
|
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
|
||||||
|
<TailfinAppShell>
|
||||||
|
<Outlet />
|
||||||
|
</TailfinAppShell>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
52
web/app/routes/login/route.tsx
Normal file
52
web/app/routes/login/route.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { client } from "@/util/api";
|
||||||
|
import { useLogin } from "@/util/hooks";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
PasswordInput,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const signInMutation = useLogin();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md" h="100%" justify="center" align="stretch">
|
||||||
|
<Title order={2} style={{ textAlign: "center" }}>
|
||||||
|
Login
|
||||||
|
</Title>
|
||||||
|
<Box maw={340} mx="auto">
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit((values) => {
|
||||||
|
signInMutation(values);
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
label="Username"
|
||||||
|
{...form.getInputProps("username")}
|
||||||
|
mt="md"
|
||||||
|
/>
|
||||||
|
<PasswordInput
|
||||||
|
label="Password"
|
||||||
|
{...form.getInputProps("password")}
|
||||||
|
mt="md"
|
||||||
|
/>
|
||||||
|
<Group justify="center" mt="xl">
|
||||||
|
<Button type="submit">Log In</Button>
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
53
web/app/ui/nav/app-shell.tsx
Normal file
53
web/app/ui/nav/app-shell.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
AppShell,
|
||||||
|
Burger,
|
||||||
|
Group,
|
||||||
|
Title,
|
||||||
|
UnstyledButton,
|
||||||
|
Image,
|
||||||
|
Avatar,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
|
import ThemeToggle from "../theme-toggle";
|
||||||
|
import { Link, useNavigate } from "@remix-run/react";
|
||||||
|
import Navbar from "./navbar";
|
||||||
|
|
||||||
|
export function TailfinAppShell({ children }: { children: React.ReactNode }) {
|
||||||
|
const [opened, { toggle }] = useDisclosure();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
navbar={{ width: 300, breakpoint: "sm", collapsed: { mobile: !opened } }}
|
||||||
|
padding="md"
|
||||||
|
>
|
||||||
|
<AppShell.Header>
|
||||||
|
<Group h="100%" justify="space-between" px="md">
|
||||||
|
<Group h="100%" px="md">
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggle}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Avatar src="/logo.png" component={Link} to="/logbook" />
|
||||||
|
<UnstyledButton onClick={() => navigate("/logbook")}>
|
||||||
|
<Title order={2} fw="normal">
|
||||||
|
Tailfin
|
||||||
|
</Title>
|
||||||
|
</UnstyledButton>
|
||||||
|
</Group>
|
||||||
|
<ThemeToggle />
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
<AppShell.Navbar>
|
||||||
|
<Navbar />
|
||||||
|
</AppShell.Navbar>
|
||||||
|
<AppShell.Main>{children}</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
68
web/app/ui/nav/navbar.tsx
Normal file
68
web/app/ui/nav/navbar.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { useMe, useSignOut } from "@/util/hooks";
|
||||||
|
import { Stack, NavLink, ActionIcon } from "@mantine/core";
|
||||||
|
import { Link, useLocation } from "@remix-run/react";
|
||||||
|
import {
|
||||||
|
IconBook2,
|
||||||
|
IconLogout,
|
||||||
|
IconMapRoute,
|
||||||
|
IconPlaneDeparture,
|
||||||
|
IconUser,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export default function Navbar() {
|
||||||
|
const location = useLocation();
|
||||||
|
const page = location.pathname.split("/")[2];
|
||||||
|
|
||||||
|
const me = useMe();
|
||||||
|
const signOut = useSignOut();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack justify="space-between" h="100%">
|
||||||
|
<Stack gap="0">
|
||||||
|
<NavLink
|
||||||
|
p="md"
|
||||||
|
component={Link}
|
||||||
|
to="/logbook"
|
||||||
|
label="Dashboard"
|
||||||
|
leftSection={<IconPlaneDeparture />}
|
||||||
|
active={page == null}
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
p="md"
|
||||||
|
component={Link}
|
||||||
|
to="/logbook/flights"
|
||||||
|
label="Flights"
|
||||||
|
leftSection={<IconBook2 />}
|
||||||
|
active={page === "flights"}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack gap="0">
|
||||||
|
<NavLink
|
||||||
|
p="md"
|
||||||
|
label={
|
||||||
|
me.isError
|
||||||
|
? me.error.message
|
||||||
|
: me.isFetched
|
||||||
|
? me.data?.username
|
||||||
|
: "Not Logged In"
|
||||||
|
}
|
||||||
|
leftSection={<IconUser />}
|
||||||
|
>
|
||||||
|
<NavLink
|
||||||
|
p="md"
|
||||||
|
component={Link}
|
||||||
|
to="/logbook/me"
|
||||||
|
label="Account"
|
||||||
|
leftSection={<IconUser />}
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
p="md"
|
||||||
|
onClick={() => signOut()}
|
||||||
|
label="Sign Out"
|
||||||
|
leftSection={<IconLogout />}
|
||||||
|
/>
|
||||||
|
</NavLink>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
30
web/app/ui/theme-toggle.tsx
Normal file
30
web/app/ui/theme-toggle.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Tooltip,
|
||||||
|
useComputedColorScheme,
|
||||||
|
useMantineColorScheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconMoonStars, IconSun } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
const ThemeToggle = () => {
|
||||||
|
const { colorScheme, setColorScheme } = useMantineColorScheme();
|
||||||
|
const comoputedColorScheme = useComputedColorScheme("dark");
|
||||||
|
const toggleColorScheme = () => {
|
||||||
|
setColorScheme(comoputedColorScheme === "dark" ? "light" : "dark");
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Tooltip label={(colorScheme === "dark" ? "Light" : "Dark") + " Theme"}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="default"
|
||||||
|
radius="xl"
|
||||||
|
size="lg"
|
||||||
|
aria-label="Toggle Dark Theme"
|
||||||
|
onClick={toggleColorScheme}
|
||||||
|
>
|
||||||
|
{colorScheme === "dark" ? <IconSun /> : <IconMoonStars />}
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeToggle;
|
45
web/app/util/api.ts
Normal file
45
web/app/util/api.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const client = axios.create({
|
||||||
|
baseURL: "http://localhost:8081",
|
||||||
|
headers: { "Access-Control-Allow-Origin": "*" },
|
||||||
|
});
|
||||||
|
|
||||||
|
client.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
client.interceptors.request.use(
|
||||||
|
(response) => response,
|
||||||
|
async (error) => {
|
||||||
|
const originalRequest = error.config;
|
||||||
|
if (error.response.status == 401 && !originalRequest._retry) {
|
||||||
|
originalRequest._retry = true;
|
||||||
|
const refreshToken = localStorage.getItem("refresh-token");
|
||||||
|
if (refreshToken) {
|
||||||
|
try {
|
||||||
|
const { data } = await client.post("/auth/refresh", {
|
||||||
|
refresh: refreshToken,
|
||||||
|
});
|
||||||
|
localStorage.setItem("token", data.refreshToken);
|
||||||
|
client.defaults.headers.common[
|
||||||
|
"Authorization"
|
||||||
|
] = `Bearer ${data.refreshToken}`;
|
||||||
|
return client(originalRequest);
|
||||||
|
} catch (_error) {
|
||||||
|
return Promise.reject(_error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
52
web/app/util/hooks.ts
Normal file
52
web/app/util/hooks.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { client } from "./api";
|
||||||
|
import { useNavigate } from "@remix-run/react";
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
username: string;
|
||||||
|
level: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useMe() {
|
||||||
|
return useQuery<User, Error>({
|
||||||
|
queryKey: ["me"],
|
||||||
|
queryFn: () => client.get(`/users/me`).then((res) => res.data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSignOut() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onSignOut = async () => {
|
||||||
|
queryClient.setQueryData(["user"], null);
|
||||||
|
const res = await client.post("/auth/logout");
|
||||||
|
if (res.status == 200) {
|
||||||
|
navigate("/login");
|
||||||
|
} else {
|
||||||
|
console.error("Failed to log out");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return onSignOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLogin() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { mutate: signInMutation } = useMutation({
|
||||||
|
mutationFn: async (values) => {
|
||||||
|
return await client.postForm("/auth/login", values);
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
localStorage.setItem("token", data.data.access_token);
|
||||||
|
localStorage.setItem("refresh-token", data.data.refresh_token);
|
||||||
|
navigate("/logbook");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return signInMutation;
|
||||||
|
}
|
2402
web/package-lock.json
generated
2402
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,25 +11,41 @@
|
|||||||
"typecheck": "tsc"
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mantine/core": "^7.3.2",
|
||||||
|
"@mantine/dates": "^7.3.2",
|
||||||
|
"@mantine/dropzone": "^7.3.2",
|
||||||
|
"@mantine/form": "^7.3.2",
|
||||||
|
"@mantine/hooks": "^7.3.2",
|
||||||
|
"@mantine/notifications": "^7.3.2",
|
||||||
"@remix-run/css-bundle": "^2.4.1",
|
"@remix-run/css-bundle": "^2.4.1",
|
||||||
"@remix-run/node": "^2.4.1",
|
"@remix-run/node": "^2.4.1",
|
||||||
"@remix-run/react": "^2.4.1",
|
"@remix-run/react": "^2.4.1",
|
||||||
"@remix-run/serve": "^2.4.1",
|
"@remix-run/serve": "^2.4.1",
|
||||||
|
"@tabler/icons-react": "^2.44.0",
|
||||||
|
"@tanstack/react-query": "^5.17.0",
|
||||||
|
"@tanstack/react-query-devtools": "^5.17.0",
|
||||||
|
"axios": "^1.6.3",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
"isbot": "^3.6.8",
|
"isbot": "^3.6.8",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@remix-run/dev": "^2.4.1",
|
"@remix-run/dev": "^2.4.1",
|
||||||
|
"@tanstack/eslint-plugin-query": "^5.14.6",
|
||||||
"@types/react": "^18.2.20",
|
"@types/react": "^18.2.20",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||||
"eslint": "^8.38.0",
|
"eslint": "^8.38.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
"eslint-plugin-import": "^2.28.1",
|
"eslint-plugin-import": "^2.28.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"postcss-preset-mantine": "^1.12.2",
|
||||||
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
14
web/postcss.config.cjs
Normal file
14
web/postcss.config.cjs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
"postcss-preset-mantine": {},
|
||||||
|
"postcss-simple-vars": {
|
||||||
|
variables: {
|
||||||
|
"mantine-breakpoint-xs": "36em",
|
||||||
|
"mantine-breakpoint-sm": "48em",
|
||||||
|
"mantine-breakpoint-md": "62em",
|
||||||
|
"mantine-breakpoint-lg": "75em",
|
||||||
|
"mantine-breakpoint-xl": "88em",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
BIN
web/public/logo.png
Normal file
BIN
web/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 243 KiB |
@ -1,5 +1,6 @@
|
|||||||
/** @type {import('@remix-run/dev').AppConfig} */
|
/** @type {import('@remix-run/dev').AppConfig} */
|
||||||
export default {
|
export default {
|
||||||
|
postcss: true,
|
||||||
ignoredRouteFiles: ["**/.*"],
|
ignoredRouteFiles: ["**/.*"],
|
||||||
// appDirectory: "app",
|
// appDirectory: "app",
|
||||||
// assetsBuildDirectory: "public/build",
|
// assetsBuildDirectory: "public/build",
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./app/*"]
|
"@/*": ["./app/*"]
|
||||||
},
|
},
|
||||||
|
|
||||||
// Remix takes care of building everything in `remix build`.
|
// Remix takes care of building everything in `remix build`.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user