From 9eb5465bc38a0b5cff3c24e84fc9ce59d10e68a6 Mon Sep 17 00:00:00 2001 From: april Date: Mon, 15 Jan 2024 16:59:02 -0600 Subject: [PATCH] Add inline image add/remove --- web/app/routes/logbook.flights.$id/route.tsx | 25 +-- .../display/editable/img-log-item.module.css} | 0 web/app/ui/display/editable/img-log-item.tsx | 146 ++++++++++++++++++ web/app/ui/form/flight-form.tsx | 2 - web/app/ui/input/image-upload.tsx | 52 +++++++ web/app/ui/input/list-input.tsx | 14 +- 6 files changed, 213 insertions(+), 26 deletions(-) rename web/app/{routes/logbook.flights.$id/route.module.css => ui/display/editable/img-log-item.module.css} (100%) create mode 100644 web/app/ui/display/editable/img-log-item.tsx create mode 100644 web/app/ui/input/image-upload.tsx diff --git a/web/app/routes/logbook.flights.$id/route.tsx b/web/app/routes/logbook.flights.$id/route.tsx index 22c9b2c..23b39a4 100644 --- a/web/app/routes/logbook.flights.$id/route.tsx +++ b/web/app/routes/logbook.flights.$id/route.tsx @@ -25,7 +25,6 @@ import { IconPencil, IconTrash } from "@tabler/icons-react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { useEffect, useState } from "react"; -import classes from "./route.module.css"; import { AircraftLogItem } from "@/ui/display/editable/aircraft-log-item"; import { DateLogItem } from "@/ui/display/editable/date-log-item"; import { HourLogItem } from "@/ui/display/editable/hour-log-item"; @@ -33,6 +32,7 @@ import { IntLogItem } from "@/ui/display/editable/int-log-item"; import { ListLogItem } from "@/ui/display/editable/list-log-item"; import { TimeLogItem } from "@/ui/display/editable/time-log-item"; import { TextLogItem } from "@/ui/display/editable/text-log-item"; +import ImageLogItem from "@/ui/display/editable/img-log-item"; export default function Flight() { const params = useParams(); @@ -140,24 +140,11 @@ export default function Flight() { {imageIds.length > 0 ? ( - - {imageIds.map((img) => ( - - - - ))} - + ) : null} diff --git a/web/app/routes/logbook.flights.$id/route.module.css b/web/app/ui/display/editable/img-log-item.module.css similarity index 100% rename from web/app/routes/logbook.flights.$id/route.module.css rename to web/app/ui/display/editable/img-log-item.module.css diff --git a/web/app/ui/display/editable/img-log-item.tsx b/web/app/ui/display/editable/img-log-item.tsx new file mode 100644 index 0000000..a8af330 --- /dev/null +++ b/web/app/ui/display/editable/img-log-item.tsx @@ -0,0 +1,146 @@ +import { Carousel } from "@mantine/carousel"; +import classes from "./img-log-item.module.css"; +import { randomId, useDisclosure } from "@mantine/hooks"; +import SecureImage from "../secure-img"; +import { + ActionIcon, + Button, + Group, + Loader, + Modal, + Stack, + Tooltip, + Text, +} from "@mantine/core"; +import { IconPencil } from "@tabler/icons-react"; +import ListInput from "@/ui/input/list-input"; +import ImageUpload from "@/ui/input/image-upload"; +import { useState } from "react"; +import { useApi } from "@/util/api"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +export default function ImageLogItem({ + imageIds, + id, + mah = "", +}: { + imageIds: string[]; + id: string; + mah?: string; +}) { + const [editOpened, { open: openEdit, close: closeEdit }] = + useDisclosure(false); + + const [existingImages, setExistingImages] = useState(imageIds); + const [newImages, setNewImages] = useState([]); + + const client = useApi(); + const queryClient = useQueryClient(); + + const updateValue = useMutation({ + mutationFn: async () => { + const missing = imageIds.filter( + (item: string) => existingImages?.indexOf(item) < 0 + ); + + for (const img of missing) { + await client.delete(`/img/${img}`); + } + + await client + .patch(`/flights/${id}`, { images: existingImages }) + .then((res) => res.data); + + // Upload images + if (newImages.length > 0) { + const imageForm = new FormData(); + + for (const img of newImages ?? []) { + imageForm.append("images", img); + } + + console.log(imageForm); + + const img_id = await client.post( + `/flights/${id}/add_images`, + imageForm, + { headers: { "Content-Type": "multipart/form-data" } } + ); + + if (!img_id) { + await queryClient.invalidateQueries({ queryKey: ["flights-list"] }); + throw new Error("Image upload failed"); + } + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [id] }); + queryClient.invalidateQueries({ queryKey: ["flights-list"] }); + closeEdit(); + }, + }); + + return ( + <> + + + + + + + {updateValue.isPending ? : null} + {updateValue.isError ? ( + {updateValue.error?.message} + ) : null} + + + + + + + + + + + + + + {imageIds.map((img) => ( + + + + ))} + + + + ); +} diff --git a/web/app/ui/form/flight-form.tsx b/web/app/ui/form/flight-form.tsx index 494e80f..cd7191c 100644 --- a/web/app/ui/form/flight-form.tsx +++ b/web/app/ui/form/flight-form.tsx @@ -1,7 +1,6 @@ import { AircraftFormSchema, AircraftSchema, - FlightDisplaySchema, FlightFormSchema, } from "@/util/types"; import { @@ -29,7 +28,6 @@ import TimeInput from "./time-input"; import { ZeroIntInput } from "./int-input"; import ListInput from "./list-input"; import { IconPencil, IconPlaneTilt, IconPlus } from "@tabler/icons-react"; -import { AxiosError } from "axios"; import { useDisclosure } from "@mantine/hooks"; import AircraftForm from "./aircraft-form"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; diff --git a/web/app/ui/input/image-upload.tsx b/web/app/ui/input/image-upload.tsx new file mode 100644 index 0000000..bdcc34b --- /dev/null +++ b/web/app/ui/input/image-upload.tsx @@ -0,0 +1,52 @@ +import { FlightFormSchema } from "@/util/types"; +import { FileInput, FileInputProps, Pill } from "@mantine/core"; +import { UseFormReturnType } from "@mantine/form"; +import { randomId } from "@mantine/hooks"; +import { IconPhoto } from "@tabler/icons-react"; +import { Dispatch, SetStateAction } from "react"; + +export default function ImageUpload({ + value, + setValue, + label = "", + placeholder = "", + mt = "", +}: { + value: File[]; + setValue: Dispatch>; + label?: string; + placeholder?: string; + mt?: string; +}) { + const ValueComponent: FileInputProps["valueComponent"] = ({ value }) => { + if (value === null) { + return null; + } + + if (Array.isArray(value)) { + return ( + + {value.map((file) => ( + {file.name} + ))} + + ); + } + + return {value.name}; + }; + + return ( + } + onChange={setValue} + /> + ); +} diff --git a/web/app/ui/input/list-input.tsx b/web/app/ui/input/list-input.tsx index 16c3bb0..9138d93 100644 --- a/web/app/ui/input/list-input.tsx +++ b/web/app/ui/input/list-input.tsx @@ -5,10 +5,12 @@ export default function ListInput({ label, value, setValue, + canAdd = true, }: { label: string; value: string[]; setValue: Dispatch>; + canAdd?: boolean; }) { const [inputValue, setInputValue] = useState(""); @@ -43,11 +45,13 @@ export default function ListInput({ {item} ))} - setInputValue(event.currentTarget.value)} - onKeyDown={handleKeyDown} - /> + {canAdd ? ( + setInputValue(event.currentTarget.value)} + onKeyDown={handleKeyDown} + /> + ) : null} );