Implement basic API interaction
This commit is contained in:
@@ -1,41 +1,5 @@
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { Outlet } from "@remix-run/react";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ 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>
|
||||
);
|
||||
export default function Tailfin() {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user