Display images on flight logs

This commit is contained in:
april
2024-01-15 09:52:56 -06:00
parent a1b5332910
commit 4b80593aa3
8 changed files with 232 additions and 53 deletions

View File

@@ -1,5 +1,6 @@
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
import "@mantine/carousel/styles.css";
import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction } from "@remix-run/node";

View File

@@ -1,5 +1,6 @@
import CollapsibleFieldset from "@/ui/display/collapsible-fieldset";
import { VerticalLogItem } from "@/ui/display/log-item";
import SecureImage from "@/ui/display/secure-img";
import ErrorDisplay from "@/ui/error-display";
import { useApi } from "@/util/api";
import {
@@ -17,10 +18,12 @@ import {
Modal,
Button,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { Carousel } from "@mantine/carousel";
import { randomId, useDisclosure } from "@mantine/hooks";
import { useNavigate, useParams } from "@remix-run/react";
import { IconPencil, IconTrash } from "@tabler/icons-react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
export default function Flight() {
const params = useParams();
@@ -34,6 +37,14 @@ export default function Flight() {
await client.get(`/flights/${params.id}`).then((res) => res.data),
});
const [imageIds, setImageIds] = useState<string[]>([]);
useEffect(() => {
if (flight.data) {
setImageIds(flight.data.images ?? []);
}
}, [flight.data]);
const [deleteOpened, { open: openDelete, close: closeDelete }] =
useDisclosure(false);
@@ -117,6 +128,25 @@ export default function Flight() {
<ScrollArea h="calc(100vh - 95px - 110px)" m="0" p="0">
<Container h="100%">
<Grid justify="center">
{imageIds.length > 0 ? (
<CollapsibleFieldset legend="Images" mt="sm" w="100%">
<Carousel
withIndicators
slideGap="sm"
slideSize={{ base: "100%", sm: "80%" }}
>
{imageIds.map((img) => (
<Carousel.Slide key={randomId()}>
<SecureImage
key={randomId()}
id={img}
radius="lg"
/>
</Carousel.Slide>
))}
</Carousel>
</CollapsibleFieldset>
) : null}
<CollapsibleFieldset legend="About" mt="sm" w="100%">
<Group grow>
<VerticalLogItem label="Date" content={log.date} date />

View File

@@ -0,0 +1,91 @@
import { useApi } from "@/util/api";
import { Center, Image, Loader, Modal } from "@mantine/core";
import { UseQueryResult, useQuery } from "@tanstack/react-query";
import ErrorDisplay from "../error-display";
import { useDisclosure } from "@mantine/hooks";
function blobToBase64(blob: Blob): Promise<string | null> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsBinaryString(blob);
reader.onloadend = function () {
if (typeof reader.result === "string") {
const base64 = btoa(reader.result);
resolve(base64);
} else {
resolve(null);
}
};
});
}
function useFetchImageAsBase64(
img_id: string
): UseQueryResult<{ blob: string | null; type: string }> {
const client = useApi();
return useQuery({
queryKey: ["image", img_id],
queryFn: async (): Promise<{
blob: string;
type: string;
}> => {
const response = await client.get(`/img/${img_id}`, {
responseType: "arraybuffer",
});
const blob = (await blobToBase64(new Blob([response.data]))) as string;
const type = (response.headers["content-type"] as string) ?? "image/jpeg";
return { blob, type };
},
});
}
export default function SecureImage({
id,
radius = "sm",
clickable = true,
}: {
id: string;
radius?: string;
clickable?: boolean;
}) {
const { isLoading, error, data } = useFetchImageAsBase64(id);
const [opened, { open, close }] = useDisclosure(false);
if (isLoading)
return (
<Center h="100%">
<Loader />
</Center>
);
if (error) return <ErrorDisplay error="Failed to load image" />;
return (
<>
{clickable ? (
<Modal
title="Image"
opened={opened}
onClose={close}
centered
size="auto"
>
<Image src={`data:${data?.type};base64,${data?.blob}`} />
</Modal>
) : null}
<Image
src={`data:${data?.type};base64,${data?.blob}`}
radius={radius}
onClick={() => {
if (clickable) {
open();
}
}}
/>
</>
);
}

View File

@@ -337,12 +337,32 @@ export default function FlightForm({
<Fieldset legend="Start/Stop" mt="md">
<Group justify="center" grow>
<TimeInput form={form} field="time_start" label="Start Time" />
<TimeInput form={form} field="time_off" label="Time Off" />
<TimeInput
form={form}
field="time_start"
label="Start Time"
allowLeadingZeros
/>
<TimeInput
form={form}
field="time_off"
label="Time Off"
allowLeadingZeros
/>
</Group>
<Group justify="center" grow mt="md">
<TimeInput form={form} field="time_down" label="Time Down" />
<TimeInput form={form} field="time_stop" label="Stop Time" />
<TimeInput
form={form}
field="time_down"
label="Time Down"
allowLeadingZeros
/>
<TimeInput
form={form}
field="time_stop"
label="Stop Time"
allowLeadingZeros
/>
</Group>
</Fieldset>

View File

@@ -8,10 +8,12 @@ export default function TimeInput({
form,
label,
field,
allowLeadingZeros = false,
}: {
form: UseFormReturnType<FlightFormSchema>;
field: string;
label: string;
allowLeadingZeros?: boolean;
}) {
const field_key = field as keyof typeof form.getTransformedValues;
@@ -21,6 +23,7 @@ export default function TimeInput({
allowDecimal={false}
min={0}
max={2359}
allowLeadingZeros={allowLeadingZeros}
leftSection={
<CloseButton
aria-label="Clear input"

View File

@@ -70,6 +70,8 @@ type FlightDisplaySchema = FlightBaseSchema & {
time_off: number | null;
time_down: number | null;
time_stop: number | null;
images: string[] | null;
};
type FlightConciseSchema = {