Implement flight log creation
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import "@mantine/core/styles.css";
|
import "@mantine/core/styles.css";
|
||||||
|
import "@mantine/dates/styles.css";
|
||||||
|
|
||||||
import { cssBundleHref } from "@remix-run/css-bundle";
|
import { cssBundleHref } from "@remix-run/css-bundle";
|
||||||
import type { LinksFunction } from "@remix-run/node";
|
import type { LinksFunction } from "@remix-run/node";
|
||||||
|
344
web/app/routes/logbook.flights.new/route.tsx
Normal file
344
web/app/routes/logbook.flights.new/route.tsx
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
CloseButton,
|
||||||
|
Container,
|
||||||
|
Fieldset,
|
||||||
|
Group,
|
||||||
|
NumberInput,
|
||||||
|
ScrollArea,
|
||||||
|
ScrollAreaAutosize,
|
||||||
|
Stack,
|
||||||
|
TextInput,
|
||||||
|
Textarea,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useForm } from "@mantine/form";
|
||||||
|
import { DatePickerInput } from "@mantine/dates";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { IconPencil } from "@tabler/icons-react";
|
||||||
|
import TimeInput from "@/ui/form/time-input";
|
||||||
|
import { FlightFormSchema, flightCreateHelper } from "@/util/types";
|
||||||
|
import { HourInput, ZeroHourInput } from "@/ui/form/hour-input";
|
||||||
|
import { ZeroIntInput } from "@/ui/form/int-input";
|
||||||
|
import ListInput from "@/ui/form/list-input";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { client } from "@/util/api";
|
||||||
|
import { useNavigate } from "@remix-run/react";
|
||||||
|
|
||||||
|
export default function NewFlight() {
|
||||||
|
const form = useForm<FlightFormSchema>({
|
||||||
|
initialValues: {
|
||||||
|
date: dayjs(),
|
||||||
|
aircraft: "",
|
||||||
|
waypoint_from: "",
|
||||||
|
waypoint_to: "",
|
||||||
|
route: "",
|
||||||
|
|
||||||
|
hobbs_start: null,
|
||||||
|
hobbs_end: null,
|
||||||
|
tach_start: null,
|
||||||
|
tach_end: null,
|
||||||
|
|
||||||
|
time_start: null,
|
||||||
|
time_off: null,
|
||||||
|
time_down: null,
|
||||||
|
time_stop: null,
|
||||||
|
|
||||||
|
time_total: 0.0,
|
||||||
|
time_pic: 0.0,
|
||||||
|
time_sic: 0.0,
|
||||||
|
time_night: 0.0,
|
||||||
|
time_solo: 0.0,
|
||||||
|
|
||||||
|
time_xc: 0.0,
|
||||||
|
dist_xc: 0.0,
|
||||||
|
|
||||||
|
takeoffs_day: 0,
|
||||||
|
landings_day: 0,
|
||||||
|
takeoffs_night: 0,
|
||||||
|
landings_night: 0,
|
||||||
|
|
||||||
|
time_instrument: 0.0,
|
||||||
|
time_sim_instrument: 0.0,
|
||||||
|
holds_instrument: 0,
|
||||||
|
|
||||||
|
dual_given: 0.0,
|
||||||
|
dual_recvd: 0.0,
|
||||||
|
time_sim: 0.0,
|
||||||
|
time_ground: 0.0,
|
||||||
|
|
||||||
|
tags: [],
|
||||||
|
|
||||||
|
pax: [],
|
||||||
|
crew: [],
|
||||||
|
|
||||||
|
comments: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const createFlight = useMutation({
|
||||||
|
mutationFn: async (values: FlightFormSchema) => {
|
||||||
|
const newFlight = flightCreateHelper(values);
|
||||||
|
const res = await client.post("/flights", newFlight);
|
||||||
|
navigate(`/logbook/flights/${res.data.id}`);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.log(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Stack>
|
||||||
|
<Title order={2}>New Flight</Title>
|
||||||
|
|
||||||
|
<form onSubmit={form.onSubmit((values) => createFlight.mutate(values))}>
|
||||||
|
<ScrollAreaAutosize mah="calc(100vh - 95px - 110px">
|
||||||
|
<Container>
|
||||||
|
{/* Date and Aircraft */}
|
||||||
|
|
||||||
|
<Fieldset>
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<DatePickerInput
|
||||||
|
label="Date"
|
||||||
|
{...form.getInputProps("date")}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Aircraft"
|
||||||
|
{...form.getInputProps("aircraft")}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
|
{/* Route */}
|
||||||
|
|
||||||
|
<Fieldset legend="Route" mt="lg">
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<TextInput
|
||||||
|
label="Waypoint From"
|
||||||
|
{...form.getInputProps("waypoint_from")}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Waypoint To"
|
||||||
|
{...form.getInputProps("waypoint_to")}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<TextInput
|
||||||
|
label="Route"
|
||||||
|
{...form.getInputProps("route")}
|
||||||
|
mt="md"
|
||||||
|
/>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
|
{/* Times */}
|
||||||
|
|
||||||
|
<Fieldset legend="Times" mt="md">
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<HourInput
|
||||||
|
form={form}
|
||||||
|
field="hobbs_start"
|
||||||
|
label="Hobbs Start"
|
||||||
|
/>
|
||||||
|
<HourInput form={form} field="hobbs_end" label="Hobbs End" />
|
||||||
|
</Group>
|
||||||
|
<Group justify="center" grow mt="md">
|
||||||
|
<HourInput
|
||||||
|
form={form}
|
||||||
|
field="tach_start"
|
||||||
|
label="Tach Start"
|
||||||
|
/>
|
||||||
|
<HourInput form={form} field="tach_end" label="Tach End" />
|
||||||
|
</Group>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
|
{/* Start/Stop */}
|
||||||
|
|
||||||
|
<Fieldset legend="Start/Stop" mt="md">
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<TimeInput
|
||||||
|
form={form}
|
||||||
|
field="time_start"
|
||||||
|
label="Start Time"
|
||||||
|
/>
|
||||||
|
<TimeInput form={form} field="time_stop" label="Stop Time" />
|
||||||
|
</Group>
|
||||||
|
<Group justify="center" grow mt="md">
|
||||||
|
<TimeInput form={form} field="time_off" label="Time Off" />
|
||||||
|
<TimeInput form={form} field="time_down" label="Time Down" />
|
||||||
|
</Group>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
|
{/* Hours */}
|
||||||
|
|
||||||
|
<Fieldset legend="Hours" mt="md">
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<ZeroHourInput
|
||||||
|
form={form}
|
||||||
|
field="time_total"
|
||||||
|
label="Time Total"
|
||||||
|
/>
|
||||||
|
<ZeroHourInput
|
||||||
|
form={form}
|
||||||
|
field="time_pic"
|
||||||
|
label="Time PIC"
|
||||||
|
/>
|
||||||
|
<ZeroHourInput
|
||||||
|
form={form}
|
||||||
|
field="time_sic"
|
||||||
|
label="Time SIC"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group justify="center" grow mt="md">
|
||||||
|
<ZeroHourInput
|
||||||
|
form={form}
|
||||||
|
field="time_night"
|
||||||
|
label="Time Night"
|
||||||
|
/>
|
||||||
|
<ZeroHourInput
|
||||||
|
form={form}
|
||||||
|
field="time_solo"
|
||||||
|
label="Time Solo"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
|
{/* Cross-Country */}
|
||||||
|
|
||||||
|
<Fieldset legend="Cross-Country" mt="md">
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<ZeroHourInput
|
||||||
|
form={form}
|
||||||
|
field="time_xc"
|
||||||
|
label="Time Cross-Country"
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label="Distance Cross-Country"
|
||||||
|
decimalScale={2}
|
||||||
|
min={0}
|
||||||
|
fixedDecimalScale
|
||||||
|
leftSection={
|
||||||
|
<CloseButton
|
||||||
|
aria-label="Clear input"
|
||||||
|
onClick={() => form.setFieldValue("dist_xc", 0)}
|
||||||
|
style={{
|
||||||
|
display:
|
||||||
|
form.getTransformedValues().dist_xc == 0
|
||||||
|
? "none"
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{...form.getInputProps("dist_xc")}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
|
{/* Takeoffs and Landings */}
|
||||||
|
|
||||||
|
<Fieldset legend="Takeoffs and Landings" mt="md">
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<ZeroIntInput
|
||||||
|
form={form}
|
||||||
|
field="takeoffs_day"
|
||||||
|
label="Day Takeoffs"
|
||||||
|
/>
|
||||||
|
<ZeroIntInput
|
||||||
|
form={form}
|
||||||
|
field="landings_day"
|
||||||
|
label="Day Landings"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group justify="center" grow mt="md">
|
||||||
|
<ZeroIntInput
|
||||||
|
form={form}
|
||||||
|
field="takeoffs_night"
|
||||||
|
label="Night Takeoffs"
|
||||||
|
/>
|
||||||
|
<ZeroIntInput
|
||||||
|
form={form}
|
||||||
|
field="landings_night"
|
||||||
|
label="Night Landings"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
|
{/* Instrument */}
|
||||||
|
|
||||||
|
<Fieldset legend="Instrument" mt="md">
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<ZeroHourInput
|
||||||
|
form={form}
|
||||||
|
field="time_instrument"
|
||||||
|
label="Time Instrument"
|
||||||
|
/>
|
||||||
|
<ZeroHourInput
|
||||||
|
form={form}
|
||||||
|
field="time_sim_instrument"
|
||||||
|
label="Time Sim Instrument"
|
||||||
|
/>
|
||||||
|
<ZeroIntInput
|
||||||
|
form={form}
|
||||||
|
field="holds_instrument"
|
||||||
|
label="Instrument Holds"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
|
{/* Instruction */}
|
||||||
|
|
||||||
|
<Fieldset legend="Instruction" mt="md">
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<ZeroHourInput
|
||||||
|
form={form}
|
||||||
|
field="dual_given"
|
||||||
|
label="Dual Given"
|
||||||
|
/>
|
||||||
|
<ZeroHourInput
|
||||||
|
form={form}
|
||||||
|
field="dual_recvd"
|
||||||
|
label="Dual Received"
|
||||||
|
/>
|
||||||
|
<ZeroHourInput
|
||||||
|
form={form}
|
||||||
|
field="time_sim"
|
||||||
|
label="Sim Time"
|
||||||
|
/>
|
||||||
|
<ZeroHourInput
|
||||||
|
form={form}
|
||||||
|
field="time_ground"
|
||||||
|
label="Ground Time"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
|
{/* About the Flight */}
|
||||||
|
|
||||||
|
<Fieldset legend="About" mt="md">
|
||||||
|
<ListInput form={form} field="tags" label="Tags" />
|
||||||
|
<Group justify="center" grow mt="md">
|
||||||
|
<ListInput form={form} field="pax" label="Pax" />
|
||||||
|
<ListInput form={form} field="crew" label="Crew" />
|
||||||
|
</Group>
|
||||||
|
<Textarea
|
||||||
|
label="Comments"
|
||||||
|
mt="md"
|
||||||
|
autosize
|
||||||
|
minRows={4}
|
||||||
|
{...form.getInputProps("comments")}
|
||||||
|
/>
|
||||||
|
</Fieldset>
|
||||||
|
</Container>
|
||||||
|
</ScrollAreaAutosize>
|
||||||
|
|
||||||
|
<Group justify="flex-end" mt="md">
|
||||||
|
<Button type="submit" leftSection={<IconPencil />}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</form>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
import { client } from "@/util/api";
|
import { client } from "@/util/api";
|
||||||
import { Flight } from "@/util/types";
|
import { FlightConciseSchema } from "@/util/types";
|
||||||
import { NavLink, Text, Button, ScrollArea, Stack } from "@mantine/core";
|
import { NavLink, Text, Button, ScrollArea, Stack } from "@mantine/core";
|
||||||
import { Link, useLocation } from "@remix-run/react";
|
import { Link, useLocation, useNavigate } from "@remix-run/react";
|
||||||
import { IconPlus } from "@tabler/icons-react";
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
@@ -14,14 +14,21 @@ export function FlightsList() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const page = location.pathname.split("/")[3];
|
const page = location.pathname.split("/")[3];
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="0" m="0" gap="0">
|
<Stack p="0" m="0" gap="0">
|
||||||
<Button variant="outline" leftSection={<IconPlus />} mb="md">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
leftSection={<IconPlus />}
|
||||||
|
mb="md"
|
||||||
|
onClick={() => navigate("/logbook/flights/new")}
|
||||||
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
{flights.data ? (
|
{flights.data ? (
|
||||||
flights.data.map((flight: Flight, index: number) => (
|
flights.data.map((flight: FlightConciseSchema, index: number) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={index}
|
key={index}
|
||||||
component={Link}
|
component={Link}
|
||||||
@@ -47,11 +54,13 @@ export function MobileFlightsList() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const page = location.pathname.split("/")[3];
|
const page = location.pathname.split("/")[3];
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="0" m="0" justify="space-between" h="calc(100vh - 95px)">
|
<Stack p="0" m="0" justify="space-between" h="calc(100vh - 95px)">
|
||||||
<ScrollArea h="calc(100vh - 95px - 50px">
|
<ScrollArea h="calc(100vh - 95px - 50px">
|
||||||
{flights.data ? (
|
{flights.data ? (
|
||||||
flights.data.map((flight: Flight, index: number) => (
|
flights.data.map((flight: FlightConciseSchema, index: number) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={index}
|
key={index}
|
||||||
component={Link}
|
component={Link}
|
||||||
@@ -64,7 +73,12 @@ export function MobileFlightsList() {
|
|||||||
<Text p="sm">No Flights</Text>
|
<Text p="sm">No Flights</Text>
|
||||||
)}
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<Button variant="outline" leftSection={<IconPlus />} mt="md">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
leftSection={<IconPlus />}
|
||||||
|
mt="md"
|
||||||
|
onClick={() => navigate("/logbook/flights/new")}
|
||||||
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Divider, Grid, Container } from "@mantine/core";
|
import { Divider, Grid, Container, ScrollAreaAutosize } from "@mantine/core";
|
||||||
import { Outlet } from "@remix-run/react";
|
import { Outlet } from "@remix-run/react";
|
||||||
import { FlightsList } from "./flights-list";
|
import { FlightsList } from "./flights-list";
|
||||||
|
|
||||||
@@ -11,7 +11,9 @@ export default function FlightsLayout() {
|
|||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Divider orientation="vertical" m="sm" />
|
<Divider orientation="vertical" m="sm" />
|
||||||
<Grid.Col span="auto">
|
<Grid.Col span="auto">
|
||||||
<Outlet />
|
<ScrollAreaAutosize mah="calc(100vh - 95px)">
|
||||||
|
<Outlet />
|
||||||
|
</ScrollAreaAutosize>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Container hiddenFrom="md">
|
<Container hiddenFrom="md">
|
||||||
|
77
web/app/ui/form/hour-input.tsx
Normal file
77
web/app/ui/form/hour-input.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { FlightFormSchema } from "@/util/types";
|
||||||
|
import { CloseButton, NumberInput } from "@mantine/core";
|
||||||
|
import { UseFormReturnType } from "@mantine/form";
|
||||||
|
|
||||||
|
function HourInput({
|
||||||
|
form,
|
||||||
|
field,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
form: UseFormReturnType<
|
||||||
|
FlightFormSchema,
|
||||||
|
(values: FlightFormSchema) => FlightFormSchema
|
||||||
|
>;
|
||||||
|
field: string;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
const field_key = field as keyof typeof form.getTransformedValues;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NumberInput
|
||||||
|
label={label}
|
||||||
|
decimalScale={1}
|
||||||
|
min={0}
|
||||||
|
fixedDecimalScale
|
||||||
|
leftSection={
|
||||||
|
<CloseButton
|
||||||
|
aria-label="Clear input"
|
||||||
|
onClick={() => form.setFieldValue(field, "")}
|
||||||
|
style={{
|
||||||
|
display:
|
||||||
|
["", null].indexOf(form.getTransformedValues()[field_key]) > -1
|
||||||
|
? "none"
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{...form.getInputProps(field)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ZeroHourInput({
|
||||||
|
form,
|
||||||
|
field,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
form: UseFormReturnType<
|
||||||
|
FlightFormSchema,
|
||||||
|
(values: FlightFormSchema) => FlightFormSchema
|
||||||
|
>;
|
||||||
|
field: string;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
const field_key = field as keyof typeof form.getTransformedValues;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NumberInput
|
||||||
|
label={label}
|
||||||
|
decimalScale={1}
|
||||||
|
min={0}
|
||||||
|
fixedDecimalScale
|
||||||
|
leftSection={
|
||||||
|
<CloseButton
|
||||||
|
aria-label="Clear input"
|
||||||
|
onClick={() => form.setFieldValue(field, 0)}
|
||||||
|
style={{
|
||||||
|
display:
|
||||||
|
form.getTransformedValues()[field_key] === 0 ? "none" : undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{...form.getInputProps(field)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { HourInput, ZeroHourInput };
|
75
web/app/ui/form/int-input.tsx
Normal file
75
web/app/ui/form/int-input.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { FlightFormSchema } from "@/util/types";
|
||||||
|
import { CloseButton, NumberInput } from "@mantine/core";
|
||||||
|
import { UseFormReturnType } from "@mantine/form";
|
||||||
|
|
||||||
|
function IntInput({
|
||||||
|
form,
|
||||||
|
field,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
form: UseFormReturnType<
|
||||||
|
FlightFormSchema,
|
||||||
|
(values: FlightFormSchema) => FlightFormSchema
|
||||||
|
>;
|
||||||
|
field: string;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
const field_key = field as keyof typeof form.getTransformedValues;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NumberInput
|
||||||
|
label={label}
|
||||||
|
min={0}
|
||||||
|
allowDecimal={false}
|
||||||
|
leftSection={
|
||||||
|
<CloseButton
|
||||||
|
aria-label="Clear input"
|
||||||
|
onClick={() => form.setFieldValue(field, "")}
|
||||||
|
style={{
|
||||||
|
display:
|
||||||
|
["", null].indexOf(form.getTransformedValues()[field_key]) > -1
|
||||||
|
? "none"
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{...form.getInputProps(field)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ZeroIntInput({
|
||||||
|
form,
|
||||||
|
field,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
form: UseFormReturnType<
|
||||||
|
FlightFormSchema,
|
||||||
|
(values: FlightFormSchema) => FlightFormSchema
|
||||||
|
>;
|
||||||
|
field: string;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
const field_key = field as keyof typeof form.getTransformedValues;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NumberInput
|
||||||
|
label={label}
|
||||||
|
min={0}
|
||||||
|
allowDecimal={false}
|
||||||
|
leftSection={
|
||||||
|
<CloseButton
|
||||||
|
aria-label="Clear input"
|
||||||
|
onClick={() => form.setFieldValue(field, 0)}
|
||||||
|
style={{
|
||||||
|
display:
|
||||||
|
form.getTransformedValues()[field_key] > 0 ? "none" : undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{...form.getInputProps(field)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { IntInput, ZeroIntInput };
|
69
web/app/ui/form/list-input.tsx
Normal file
69
web/app/ui/form/list-input.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { FlightFormSchema } from "@/util/types";
|
||||||
|
import { Pill, PillsInput } from "@mantine/core";
|
||||||
|
import { UseFormReturnType } from "@mantine/form";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function ListInput({
|
||||||
|
form,
|
||||||
|
field,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
form: UseFormReturnType<
|
||||||
|
FlightFormSchema,
|
||||||
|
(values: FlightFormSchema) => FlightFormSchema
|
||||||
|
>;
|
||||||
|
field: string;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
const field_key = field as keyof typeof form.getTransformedValues;
|
||||||
|
const [inputValue, setInputValue] = useState<string>("");
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||||
|
if (event.key === "Enter" || event.key === ",") {
|
||||||
|
event.preventDefault();
|
||||||
|
const values = form.getTransformedValues()[field_key] as string[];
|
||||||
|
const newItem = inputValue.trim();
|
||||||
|
if (newItem && values.indexOf(newItem) == -1) {
|
||||||
|
form.setFieldValue(field, [...values, newItem]);
|
||||||
|
setInputValue("");
|
||||||
|
}
|
||||||
|
} else if (event.key === "Backspace") {
|
||||||
|
const values = form.getTransformedValues()[field_key] as string[];
|
||||||
|
const newItem = inputValue.trim();
|
||||||
|
if (newItem === "") {
|
||||||
|
form.setFieldValue(field, values.slice(0, -1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PillsInput label={label}>
|
||||||
|
<Pill.Group>
|
||||||
|
{(form.getTransformedValues()[field_key] as string[]).map(
|
||||||
|
(item: string) => (
|
||||||
|
<Pill
|
||||||
|
radius="sm"
|
||||||
|
key={item}
|
||||||
|
withRemoveButton
|
||||||
|
onRemove={() =>
|
||||||
|
form.setFieldValue(
|
||||||
|
field,
|
||||||
|
(form.getTransformedValues()[field_key] as string[]).filter(
|
||||||
|
(value: string) => value !== item
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</Pill>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<PillsInput.Field
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(event) => setInputValue(event.currentTarget.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
</Pill.Group>
|
||||||
|
</PillsInput>
|
||||||
|
);
|
||||||
|
}
|
52
web/app/ui/form/time-input.tsx
Normal file
52
web/app/ui/form/time-input.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { FlightFormSchema } from "@/util/types";
|
||||||
|
import { ActionIcon, CloseButton, NumberInput, Tooltip } from "@mantine/core";
|
||||||
|
import { UseFormReturnType } from "@mantine/form";
|
||||||
|
import { IconClock } from "@tabler/icons-react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export default function TimeInput({
|
||||||
|
form,
|
||||||
|
label,
|
||||||
|
field,
|
||||||
|
}: {
|
||||||
|
form: UseFormReturnType<FlightFormSchema>;
|
||||||
|
field: string;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
const field_key = field as keyof typeof form.getTransformedValues;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NumberInput
|
||||||
|
label={label}
|
||||||
|
allowDecimal={false}
|
||||||
|
min={0}
|
||||||
|
max={2359}
|
||||||
|
leftSection={
|
||||||
|
<CloseButton
|
||||||
|
aria-label="Clear input"
|
||||||
|
onClick={() => form.setFieldValue(field, "")}
|
||||||
|
style={{
|
||||||
|
display:
|
||||||
|
["", null].indexOf(form.getTransformedValues()[field_key]) > -1
|
||||||
|
? "none"
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
rightSection={
|
||||||
|
<Tooltip label="Now">
|
||||||
|
<ActionIcon
|
||||||
|
variant="transparent"
|
||||||
|
mr="sm"
|
||||||
|
onClick={() => {
|
||||||
|
form.setFieldValue(field, dayjs().format("HHmm"));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconClock style={{ width: "70%", height: "70%" }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
{...form.getInputProps(field)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -91,6 +91,7 @@ function useProvideAuth() {
|
|||||||
.get("/users/me")
|
.get("/users/me")
|
||||||
.then((response) => handleUser(response.data.username))
|
.then((response) => handleUser(response.data.username))
|
||||||
.catch(() => handleUser(null));
|
.catch(() => handleUser(null));
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
type Flight = {
|
import dayjs from "dayjs";
|
||||||
id: string;
|
import utc from "dayjs/plugin/utc.js";
|
||||||
user: string;
|
|
||||||
|
|
||||||
date: string;
|
dayjs.extend(utc);
|
||||||
|
|
||||||
|
type FlightBaseSchema = {
|
||||||
aircraft: string | null;
|
aircraft: string | null;
|
||||||
waypoint_from: string | null;
|
waypoint_from: string | null;
|
||||||
waypoint_to: string | null;
|
waypoint_to: string | null;
|
||||||
@@ -49,4 +50,49 @@ type Flight = {
|
|||||||
comments: string;
|
comments: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { type Flight };
|
type FlightFormSchema = FlightBaseSchema & {
|
||||||
|
date: dayjs.Dayjs;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FlightCreateSchema = FlightBaseSchema & {
|
||||||
|
date: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FlightDisplaySchema = FlightBaseSchema & {
|
||||||
|
id: string;
|
||||||
|
user: string;
|
||||||
|
date: dayjs.Dayjs;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FlightConciseSchema = {
|
||||||
|
user: string;
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
date: dayjs.Dayjs;
|
||||||
|
aircraft: string;
|
||||||
|
waypoint_from: string;
|
||||||
|
waypoint_to: string;
|
||||||
|
|
||||||
|
time_total: number;
|
||||||
|
|
||||||
|
comments: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const flightCreateHelper = (values: FlightFormSchema): FlightCreateSchema => {
|
||||||
|
return {
|
||||||
|
...values,
|
||||||
|
date: values.date.utc().startOf("day").toISOString(),
|
||||||
|
hobbs_start: Number(values.hobbs_start),
|
||||||
|
hobbs_end: Number(values.hobbs_end),
|
||||||
|
tach_start: Number(values.tach_start),
|
||||||
|
tach_end: Number(values.tach_end),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
flightCreateHelper,
|
||||||
|
type FlightFormSchema,
|
||||||
|
type FlightCreateSchema,
|
||||||
|
type FlightDisplaySchema,
|
||||||
|
type FlightConciseSchema,
|
||||||
|
};
|
||||||
|
76
web/package-lock.json
generated
76
web/package-lock.json
generated
@@ -5,12 +5,12 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^7.3.2",
|
"@mantine/core": "^7.4.0",
|
||||||
"@mantine/dates": "^7.3.2",
|
"@mantine/dates": "^7.4.0",
|
||||||
"@mantine/dropzone": "^7.3.2",
|
"@mantine/dropzone": "^7.4.0",
|
||||||
"@mantine/form": "^7.3.2",
|
"@mantine/form": "^7.4.0",
|
||||||
"@mantine/hooks": "^7.3.2",
|
"@mantine/hooks": "^7.4.0",
|
||||||
"@mantine/notifications": "^7.3.2",
|
"@mantine/notifications": "^7.4.0",
|
||||||
"@remix-run/css-bundle": "^2.4.1",
|
"@remix-run/css-bundle": "^2.4.1",
|
||||||
"@remix-run/node": "^2.4.1",
|
"@remix-run/node": "^2.4.1",
|
||||||
"@remix-run/react": "^2.4.1",
|
"@remix-run/react": "^2.4.1",
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"@tanstack/react-query-devtools": "^5.17.0",
|
"@tanstack/react-query-devtools": "^5.17.0",
|
||||||
"axios": "^1.6.3",
|
"axios": "^1.6.3",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
"dayjs-plugin-utc": "^0.1.2",
|
||||||
"isbot": "^3.6.8",
|
"isbot": "^3.6.8",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
@@ -1426,9 +1427,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@mantine/core": {
|
"node_modules/@mantine/core": {
|
||||||
"version": "7.3.2",
|
"version": "7.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.4.0.tgz",
|
||||||
"integrity": "sha512-CwAuQogVLcLR7O9e1eOgi3gtk4XX6cnaqevAxzJJpIOIyCnHiQ3cEGINVXyUUjUUipBlvK3sqz3NPGJ2ekLFDQ==",
|
"integrity": "sha512-wnQOz1aSpqVlCpdyY4XyJKRqlW87mexMADQrbCTwg/5BbxKp8XU6sTcnk1piwyR0mM6SI1uo0Yik2qYNGFlyWw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/react": "^0.24.8",
|
"@floating-ui/react": "^0.24.8",
|
||||||
"clsx": "2.0.0",
|
"clsx": "2.0.0",
|
||||||
@@ -1438,44 +1439,44 @@
|
|||||||
"type-fest": "^3.13.1"
|
"type-fest": "^3.13.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@mantine/hooks": "7.3.2",
|
"@mantine/hooks": "7.4.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mantine/dates": {
|
"node_modules/@mantine/dates": {
|
||||||
"version": "7.3.2",
|
"version": "7.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-7.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-7.4.0.tgz",
|
||||||
"integrity": "sha512-mmP0PYpM9GYTXRhEL4Ulb+170vgW8fpGwRdVNuCCE/cyzIKHPJzaI2pEGR/i+sboH+RC1InCy/JaiDkPY6Fzlg==",
|
"integrity": "sha512-KNRVMSUW4sIk8U5SM2+4PRLqndWNaMtTabENPZUVA/Zl99yk6tDsNsu/CuinE1K5LQo9H6RJho0FRGzmzEsTwA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"clsx": "2.0.0"
|
"clsx": "2.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@mantine/core": "7.3.2",
|
"@mantine/core": "7.4.0",
|
||||||
"@mantine/hooks": "7.3.2",
|
"@mantine/hooks": "7.4.0",
|
||||||
"dayjs": ">=1.0.0",
|
"dayjs": ">=1.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mantine/dropzone": {
|
"node_modules/@mantine/dropzone": {
|
||||||
"version": "7.3.2",
|
"version": "7.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-7.4.0.tgz",
|
||||||
"integrity": "sha512-SqeBARkfS/x8UkxTc8qup7oWF1NDXP2WsbzllsLpjnlH7A/MpNgEUHs/y6oUF0Sxnc3g5Z0cvzHlZpFiWDGCmg==",
|
"integrity": "sha512-vMX9vrYBl9A/0frIcvgHjCLAdZ0hSI79VMQfMoWQ9GpOG15auQGtleT04JEgmB83I0mChSdS1I+8rV9erTBlhQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-dropzone-esm": "15.0.1"
|
"react-dropzone-esm": "15.0.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@mantine/core": "7.3.2",
|
"@mantine/core": "7.4.0",
|
||||||
"@mantine/hooks": "7.3.2",
|
"@mantine/hooks": "7.4.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mantine/form": {
|
"node_modules/@mantine/form": {
|
||||||
"version": "7.3.2",
|
"version": "7.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.4.0.tgz",
|
||||||
"integrity": "sha512-/qa1KQKVC46XWgIU190r3XM3Xld8Lsvz4L/an//TO67RnAGEdC5OCvr2JCb+fprZZi3YdxaKOkVNvP20W23qkg==",
|
"integrity": "sha512-JI/o2nECWct/Kvn3GF6VplHyJeaLy0q/jGNEB/F4yt12mAYBsux6vPfAhpWrKKZ8Jt31RI+ikn6R4UcY1HGIAw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"klona": "^2.0.6"
|
"klona": "^2.0.6"
|
||||||
@@ -1485,32 +1486,32 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mantine/hooks": {
|
"node_modules/@mantine/hooks": {
|
||||||
"version": "7.3.2",
|
"version": "7.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.4.0.tgz",
|
||||||
"integrity": "sha512-xgumuuI3PBWXff5N02HCI7PEy25mDEdyXDQklUYK93J6FKwpcosyZnGVitoUrV1gLtYYa9ZudeAWdhHuh/CpOg==",
|
"integrity": "sha512-Swv23D8XmZqE2hohPBcff+ITwv5l8UlwiiEGMhL+ceUvJLnPzdwlW21qnLBtRtZWyQQ59TAav4M0GFGd93JS8Q==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mantine/notifications": {
|
"node_modules/@mantine/notifications": {
|
||||||
"version": "7.3.2",
|
"version": "7.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.4.0.tgz",
|
||||||
"integrity": "sha512-XOzgm4pm4XszavVN0QUjN+IP0xiG2IochxJSz/FduTI0r3u1WxdpvDYlOvEJpHhtWvyqI8W8rx6cPJaD2HdAwQ==",
|
"integrity": "sha512-nRXYIcJpqqKxwYs2r17IBZ8uQZK57x6K2hkzOQ+ZFviO5rejxl4ip+fC+LUhIi3P7D1YSxyoZwumT73gSPz9Xw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/store": "7.3.2",
|
"@mantine/store": "7.4.0",
|
||||||
"react-transition-group": "4.4.5"
|
"react-transition-group": "4.4.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@mantine/core": "7.3.2",
|
"@mantine/core": "7.4.0",
|
||||||
"@mantine/hooks": "7.3.2",
|
"@mantine/hooks": "7.4.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mantine/store": {
|
"node_modules/@mantine/store": {
|
||||||
"version": "7.3.2",
|
"version": "7.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.4.0.tgz",
|
||||||
"integrity": "sha512-M1eWHzTRCeCFvrpFhXKIM9zblrlIT5/XrMue/fP2HrkA43dpkgq+ArnZkN3LhG9lWR/EKbRwQWDhDIvdLtfD7w==",
|
"integrity": "sha512-sSaBj6qVU0e5ml70/8e3A9pwAMBL5yKWNdnhw20b+74j85+FUDhDy8bEGZfyS0BtYPGVoxj5yF8/uZhxnDXpbg==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
}
|
}
|
||||||
@@ -4210,6 +4211,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
|
||||||
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
|
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs-plugin-utc": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs-plugin-utc/-/dayjs-plugin-utc-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-ExERH5o3oo6jFOdkvMP3gytTCQ9Ksi5PtylclJWghr7k7m3o2U5QrwtdiJkOxLOH4ghr0EKhpqGefzGz1VvVJg=="
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
|
@@ -11,12 +11,12 @@
|
|||||||
"typecheck": "tsc"
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mantine/core": "^7.3.2",
|
"@mantine/core": "^7.4.0",
|
||||||
"@mantine/dates": "^7.3.2",
|
"@mantine/dates": "^7.4.0",
|
||||||
"@mantine/dropzone": "^7.3.2",
|
"@mantine/dropzone": "^7.4.0",
|
||||||
"@mantine/form": "^7.3.2",
|
"@mantine/form": "^7.4.0",
|
||||||
"@mantine/hooks": "^7.3.2",
|
"@mantine/hooks": "^7.4.0",
|
||||||
"@mantine/notifications": "^7.3.2",
|
"@mantine/notifications": "^7.4.0",
|
||||||
"@remix-run/css-bundle": "^2.4.1",
|
"@remix-run/css-bundle": "^2.4.1",
|
||||||
"@remix-run/node": "^2.4.1",
|
"@remix-run/node": "^2.4.1",
|
||||||
"@remix-run/react": "^2.4.1",
|
"@remix-run/react": "^2.4.1",
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"@tanstack/react-query-devtools": "^5.17.0",
|
"@tanstack/react-query-devtools": "^5.17.0",
|
||||||
"axios": "^1.6.3",
|
"axios": "^1.6.3",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
"dayjs-plugin-utc": "^0.1.2",
|
||||||
"isbot": "^3.6.8",
|
"isbot": "^3.6.8",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
|
Reference in New Issue
Block a user