From 4b80593aa3aa776369b3968656746f6c867509af Mon Sep 17 00:00:00 2001 From: april Date: Mon, 15 Jan 2024 09:52:56 -0600 Subject: [PATCH] Display images on flight logs --- web/app/root.tsx | 1 + web/app/routes/logbook.flights.$id/route.tsx | 32 +++++- web/app/ui/display/secure-img.tsx | 91 +++++++++++++++ web/app/ui/form/flight-form.tsx | 28 ++++- web/app/ui/form/time-input.tsx | 3 + web/app/util/types.ts | 2 + web/package-lock.json | 112 ++++++++++++------- web/package.json | 16 +-- 8 files changed, 232 insertions(+), 53 deletions(-) create mode 100644 web/app/ui/display/secure-img.tsx diff --git a/web/app/root.tsx b/web/app/root.tsx index ef174ce..6a475e7 100644 --- a/web/app/root.tsx +++ b/web/app/root.tsx @@ -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"; diff --git a/web/app/routes/logbook.flights.$id/route.tsx b/web/app/routes/logbook.flights.$id/route.tsx index c589e79..9f3bf79 100644 --- a/web/app/routes/logbook.flights.$id/route.tsx +++ b/web/app/routes/logbook.flights.$id/route.tsx @@ -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([]); + + 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() { + {imageIds.length > 0 ? ( + + + {imageIds.map((img) => ( + + + + ))} + + + ) : null} diff --git a/web/app/ui/display/secure-img.tsx b/web/app/ui/display/secure-img.tsx new file mode 100644 index 0000000..30e4b1b --- /dev/null +++ b/web/app/ui/display/secure-img.tsx @@ -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 { + 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 ( +
+ +
+ ); + + if (error) return ; + + return ( + <> + {clickable ? ( + + + + ) : null} + { + if (clickable) { + open(); + } + }} + /> + + ); +} diff --git a/web/app/ui/form/flight-form.tsx b/web/app/ui/form/flight-form.tsx index 731abb0..ae7856e 100644 --- a/web/app/ui/form/flight-form.tsx +++ b/web/app/ui/form/flight-form.tsx @@ -337,12 +337,32 @@ export default function FlightForm({
- - + + - - + +
diff --git a/web/app/ui/form/time-input.tsx b/web/app/ui/form/time-input.tsx index b133ab8..66b6c5d 100644 --- a/web/app/ui/form/time-input.tsx +++ b/web/app/ui/form/time-input.tsx @@ -8,10 +8,12 @@ export default function TimeInput({ form, label, field, + allowLeadingZeros = false, }: { form: UseFormReturnType; 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={ =7.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, "node_modules/@mantine/core": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.4.0.tgz", - "integrity": "sha512-wnQOz1aSpqVlCpdyY4XyJKRqlW87mexMADQrbCTwg/5BbxKp8XU6sTcnk1piwyR0mM6SI1uo0Yik2qYNGFlyWw==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.4.1.tgz", + "integrity": "sha512-crz9BemmwR8V/h6db9FgznCp0Ssp6rCUYkBBO4JprpH8NDSEblHyWcZZo43IuA1vZptp8eyrhRNJ4nfe8CAYFQ==", "dependencies": { "@floating-ui/react": "^0.24.8", "clsx": "2.0.0", @@ -1442,44 +1456,44 @@ "type-fest": "^3.13.1" }, "peerDependencies": { - "@mantine/hooks": "7.4.0", + "@mantine/hooks": "7.4.1", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/dates": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-7.4.0.tgz", - "integrity": "sha512-KNRVMSUW4sIk8U5SM2+4PRLqndWNaMtTabENPZUVA/Zl99yk6tDsNsu/CuinE1K5LQo9H6RJho0FRGzmzEsTwA==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-7.4.1.tgz", + "integrity": "sha512-a7DNeJmLCgnFbd9NAEQ/LP998zLFPu8IWVjtJY/YJ4OUIDLEPi56OzuopA3epVzGkMcEvL4Ak78Z23KfZPJepg==", "dependencies": { "clsx": "2.0.0" }, "peerDependencies": { - "@mantine/core": "7.4.0", - "@mantine/hooks": "7.4.0", + "@mantine/core": "7.4.1", + "@mantine/hooks": "7.4.1", "dayjs": ">=1.0.0", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/dropzone": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.4.0.tgz", - "integrity": "sha512-vMX9vrYBl9A/0frIcvgHjCLAdZ0hSI79VMQfMoWQ9GpOG15auQGtleT04JEgmB83I0mChSdS1I+8rV9erTBlhQ==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.4.1.tgz", + "integrity": "sha512-VeexJtIDrqf22udZcnxhQSR0TXT1/n2EzoTTw5OCgo118UVDfjMkH0o+go8koN+9S8BthXisy5e+W4CYccqaoQ==", "dependencies": { "react-dropzone-esm": "15.0.1" }, "peerDependencies": { - "@mantine/core": "7.4.0", - "@mantine/hooks": "7.4.0", + "@mantine/core": "7.4.1", + "@mantine/hooks": "7.4.1", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/form": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.4.0.tgz", - "integrity": "sha512-JI/o2nECWct/Kvn3GF6VplHyJeaLy0q/jGNEB/F4yt12mAYBsux6vPfAhpWrKKZ8Jt31RI+ikn6R4UcY1HGIAw==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.4.1.tgz", + "integrity": "sha512-8oWD21ioJN0RYA+7D8WnJw+jyB9GufuvjuWIKGpG2sLpSVmHyIVLRQi0kqpttsx87K+JNPEJU1KmZbdtA8NuKg==", "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" @@ -1489,43 +1503,43 @@ } }, "node_modules/@mantine/hooks": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.4.0.tgz", - "integrity": "sha512-Swv23D8XmZqE2hohPBcff+ITwv5l8UlwiiEGMhL+ceUvJLnPzdwlW21qnLBtRtZWyQQ59TAav4M0GFGd93JS8Q==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.4.1.tgz", + "integrity": "sha512-7gV9YR+xZ1L69MGVaSNwV0gaxIz4bCZuGxXTtnuaamDcO/4YiNDtmvdD7/jC/RTa1iJMnZ6YiYrcPXLOn+8saQ==", "peerDependencies": { "react": "^18.2.0" } }, "node_modules/@mantine/modals": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.4.0.tgz", - "integrity": "sha512-uXZuN5vCx0Wdu0gOmoDaGD8/GVpx7qCeyAAFCH94WPHl/aK3fzKSk4K63deWY5Ml9a5ktic/i5pYil3MUBEj5w==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-7.4.1.tgz", + "integrity": "sha512-LNE7tge2FJfmaZpW/Ai+5wMW8rbZ0CN2O0BtCxXF4Ztpzdsb+d/RJ3Me92Cext3ykWljfCFaNDbsEirWxS2mZQ==", "peerDependencies": { - "@mantine/core": "7.4.0", - "@mantine/hooks": "7.4.0", + "@mantine/core": "7.4.1", + "@mantine/hooks": "7.4.1", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/notifications": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.4.0.tgz", - "integrity": "sha512-nRXYIcJpqqKxwYs2r17IBZ8uQZK57x6K2hkzOQ+ZFviO5rejxl4ip+fC+LUhIi3P7D1YSxyoZwumT73gSPz9Xw==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.4.1.tgz", + "integrity": "sha512-tDp2le/CsX7l8X4Kgx7eUQ2tdC8Z8cscPjRv2RPlkXI6giFYp0agOvc3BhwxED8xFDNZC4BkC+uoOonB9XTmdg==", "dependencies": { - "@mantine/store": "7.4.0", + "@mantine/store": "7.4.1", "react-transition-group": "4.4.5" }, "peerDependencies": { - "@mantine/core": "7.4.0", - "@mantine/hooks": "7.4.0", + "@mantine/core": "7.4.1", + "@mantine/hooks": "7.4.1", "react": "^18.2.0", "react-dom": "^18.2.0" } }, "node_modules/@mantine/store": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.4.0.tgz", - "integrity": "sha512-sSaBj6qVU0e5ml70/8e3A9pwAMBL5yKWNdnhw20b+74j85+FUDhDy8bEGZfyS0BtYPGVoxj5yF8/uZhxnDXpbg==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.4.1.tgz", + "integrity": "sha512-BWU2b+t8Rnlc+GsPMEhGzU0hzZIuf58miZvxDXDnXr8NcBubSPhoR97bSFQ81UAvDAczI4xqOQi8vVUcoKa+ng==", "peerDependencies": { "react": "^18.2.0" } @@ -4474,6 +4488,22 @@ "integrity": "sha512-1n7zWYh8eS0L9Uy+GskE0lkBUNK83cXTVJI0pU3mGprFsbfSdAc15VTFbo+A+Bq4pwstmL30AVcEU3Fo463lNg==", "dev": true }, + "node_modules/embla-carousel": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-7.1.0.tgz", + "integrity": "sha512-Bh8Pa8NWzgugLkf8sAGexQlBCNDFaej5BXiKgQdRJ1mUC9NWBrw9Z23YVPVGkguWoz5LMjZXXFVGCobl3UPt/Q==" + }, + "node_modules/embla-carousel-react": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-7.1.0.tgz", + "integrity": "sha512-tbYRPRZSDNd2QLNqYDcArAakGIxtUbhS7tkP0dGXktXHGgcX+3ji3VrOUTOftBiujZrMV8kRxtrRUe/1soloIQ==", + "dependencies": { + "embla-carousel": "7.1.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", diff --git a/web/package.json b/web/package.json index f65094c..a5e1c66 100644 --- a/web/package.json +++ b/web/package.json @@ -11,13 +11,14 @@ "typecheck": "tsc" }, "dependencies": { - "@mantine/core": "^7.4.0", - "@mantine/dates": "^7.4.0", - "@mantine/dropzone": "^7.4.0", - "@mantine/form": "^7.4.0", - "@mantine/hooks": "^7.4.0", - "@mantine/modals": "^7.4.0", - "@mantine/notifications": "^7.4.0", + "@mantine/carousel": "^7.4.1", + "@mantine/core": "^7.4.1", + "@mantine/dates": "^7.4.1", + "@mantine/dropzone": "^7.4.1", + "@mantine/form": "^7.4.1", + "@mantine/hooks": "^7.4.1", + "@mantine/modals": "^7.4.1", + "@mantine/notifications": "^7.4.1", "@remix-run/css-bundle": "^2.4.1", "@remix-run/node": "^2.4.1", "@remix-run/react": "^2.4.1", @@ -28,6 +29,7 @@ "axios": "^1.6.3", "dayjs": "^1.11.10", "dayjs-plugin-utc": "^0.1.2", + "embla-carousel-react": "^7.1.0", "isbot": "^3.6.8", "react": "^18.2.0", "react-dom": "^18.2.0",