Add inline image add/remove
This commit is contained in:
parent
924252b38f
commit
9eb5465bc3
@ -25,7 +25,6 @@ import { IconPencil, IconTrash } from "@tabler/icons-react";
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import classes from "./route.module.css";
|
|
||||||
import { AircraftLogItem } from "@/ui/display/editable/aircraft-log-item";
|
import { AircraftLogItem } from "@/ui/display/editable/aircraft-log-item";
|
||||||
import { DateLogItem } from "@/ui/display/editable/date-log-item";
|
import { DateLogItem } from "@/ui/display/editable/date-log-item";
|
||||||
import { HourLogItem } from "@/ui/display/editable/hour-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 { ListLogItem } from "@/ui/display/editable/list-log-item";
|
||||||
import { TimeLogItem } from "@/ui/display/editable/time-log-item";
|
import { TimeLogItem } from "@/ui/display/editable/time-log-item";
|
||||||
import { TextLogItem } from "@/ui/display/editable/text-log-item";
|
import { TextLogItem } from "@/ui/display/editable/text-log-item";
|
||||||
|
import ImageLogItem from "@/ui/display/editable/img-log-item";
|
||||||
|
|
||||||
export default function Flight() {
|
export default function Flight() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@ -140,24 +140,11 @@ export default function Flight() {
|
|||||||
<Grid justify="center">
|
<Grid justify="center">
|
||||||
{imageIds.length > 0 ? (
|
{imageIds.length > 0 ? (
|
||||||
<CollapsibleFieldset legend="Images" mt="sm" w="100%">
|
<CollapsibleFieldset legend="Images" mt="sm" w="100%">
|
||||||
<Carousel
|
<ImageLogItem
|
||||||
style={{ maxHeight: "700px" }}
|
imageIds={imageIds}
|
||||||
withIndicators
|
id={params.id}
|
||||||
slideGap="sm"
|
mah="700px"
|
||||||
slideSize={{ base: "100%", sm: "80%" }}
|
/>
|
||||||
classNames={classes}
|
|
||||||
>
|
|
||||||
{imageIds.map((img) => (
|
|
||||||
<Carousel.Slide key={randomId()}>
|
|
||||||
<SecureImage
|
|
||||||
key={randomId()}
|
|
||||||
id={img}
|
|
||||||
h="700px"
|
|
||||||
radius="lg"
|
|
||||||
/>
|
|
||||||
</Carousel.Slide>
|
|
||||||
))}
|
|
||||||
</Carousel>
|
|
||||||
</CollapsibleFieldset>
|
</CollapsibleFieldset>
|
||||||
) : null}
|
) : null}
|
||||||
<CollapsibleFieldset legend="About" mt="sm" w="100%">
|
<CollapsibleFieldset legend="About" mt="sm" w="100%">
|
||||||
|
146
web/app/ui/display/editable/img-log-item.tsx
Normal file
146
web/app/ui/display/editable/img-log-item.tsx
Normal file
@ -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<string[]>(imageIds);
|
||||||
|
const [newImages, setNewImages] = useState<File[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
opened={editOpened}
|
||||||
|
onClose={closeEdit}
|
||||||
|
title={`Edit Images`}
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<ListInput
|
||||||
|
label="Existing Images"
|
||||||
|
value={existingImages}
|
||||||
|
setValue={setExistingImages}
|
||||||
|
canAdd={false}
|
||||||
|
/>
|
||||||
|
<ImageUpload
|
||||||
|
value={newImages}
|
||||||
|
setValue={setNewImages}
|
||||||
|
label="Add Images"
|
||||||
|
mt="md"
|
||||||
|
placeholder="Images"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="flex-end">
|
||||||
|
{updateValue.isPending ? <Loader /> : null}
|
||||||
|
{updateValue.isError ? (
|
||||||
|
<Text c="red">{updateValue.error?.message}</Text>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
updateValue.mutate();
|
||||||
|
}}
|
||||||
|
leftSection={<IconPencil />}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
<Stack>
|
||||||
|
<Group justify="flex-end" py="0" my="0">
|
||||||
|
<Tooltip label="Edit Images">
|
||||||
|
<ActionIcon variant="transparent" onClick={openEdit}>
|
||||||
|
<IconPencil />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<Carousel
|
||||||
|
style={{ maxHeight: mah }}
|
||||||
|
withIndicators
|
||||||
|
slideGap="sm"
|
||||||
|
slideSize={{ base: "100%", sm: "80%" }}
|
||||||
|
classNames={classes}
|
||||||
|
>
|
||||||
|
{imageIds.map((img) => (
|
||||||
|
<Carousel.Slide key={randomId()}>
|
||||||
|
<SecureImage key={randomId()} id={img} h="700px" radius="lg" />
|
||||||
|
</Carousel.Slide>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AircraftFormSchema,
|
AircraftFormSchema,
|
||||||
AircraftSchema,
|
AircraftSchema,
|
||||||
FlightDisplaySchema,
|
|
||||||
FlightFormSchema,
|
FlightFormSchema,
|
||||||
} from "@/util/types";
|
} from "@/util/types";
|
||||||
import {
|
import {
|
||||||
@ -29,7 +28,6 @@ import TimeInput from "./time-input";
|
|||||||
import { ZeroIntInput } from "./int-input";
|
import { ZeroIntInput } from "./int-input";
|
||||||
import ListInput from "./list-input";
|
import ListInput from "./list-input";
|
||||||
import { IconPencil, IconPlaneTilt, IconPlus } from "@tabler/icons-react";
|
import { IconPencil, IconPlaneTilt, IconPlus } from "@tabler/icons-react";
|
||||||
import { AxiosError } from "axios";
|
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import AircraftForm from "./aircraft-form";
|
import AircraftForm from "./aircraft-form";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
52
web/app/ui/input/image-upload.tsx
Normal file
52
web/app/ui/input/image-upload.tsx
Normal file
@ -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<SetStateAction<File[]>>;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
mt?: string;
|
||||||
|
}) {
|
||||||
|
const ValueComponent: FileInputProps["valueComponent"] = ({ value }) => {
|
||||||
|
if (value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return (
|
||||||
|
<Pill.Group>
|
||||||
|
{value.map((file) => (
|
||||||
|
<Pill key={randomId()}>{file.name}</Pill>
|
||||||
|
))}
|
||||||
|
</Pill.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Pill>{value.name}</Pill>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileInput
|
||||||
|
label={label}
|
||||||
|
placeholder={placeholder}
|
||||||
|
multiple
|
||||||
|
mt={mt}
|
||||||
|
accept="image/*"
|
||||||
|
valueComponent={ValueComponent}
|
||||||
|
rightSectionPointerEvents="none"
|
||||||
|
rightSection={<IconPhoto />}
|
||||||
|
onChange={setValue}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -5,10 +5,12 @@ export default function ListInput({
|
|||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
setValue,
|
setValue,
|
||||||
|
canAdd = true,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string[];
|
value: string[];
|
||||||
setValue: Dispatch<SetStateAction<string[]>>;
|
setValue: Dispatch<SetStateAction<string[]>>;
|
||||||
|
canAdd?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [inputValue, setInputValue] = useState<string>("");
|
const [inputValue, setInputValue] = useState<string>("");
|
||||||
|
|
||||||
@ -43,11 +45,13 @@ export default function ListInput({
|
|||||||
{item}
|
{item}
|
||||||
</Pill>
|
</Pill>
|
||||||
))}
|
))}
|
||||||
<PillsInput.Field
|
{canAdd ? (
|
||||||
value={inputValue}
|
<PillsInput.Field
|
||||||
onChange={(event) => setInputValue(event.currentTarget.value)}
|
value={inputValue}
|
||||||
onKeyDown={handleKeyDown}
|
onChange={(event) => setInputValue(event.currentTarget.value)}
|
||||||
/>
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</Pill.Group>
|
</Pill.Group>
|
||||||
</PillsInput>
|
</PillsInput>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user