import { useState } from "react";
import type { z } from "zod";
import type { EventPlaceForm } from "../model/types";
import { zodResolver } from "@hookform/resolvers/zod";
import { type Control, useForm } from "react-hook-form";
import { Button } from "@/shared/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import { type EventPlace, placesQueries } from "@/entities/place";
import { handleNumericInput } from "@/shared/lib/handleNumericInput";
import { formatUsdt } from "@/shared/lib/money/usdt";
import { Switch } from "@/shared/ui/switch";
import { useCheckSubmit } from "../hooks/useCheckSubmit";
import { usePlacePersist } from "../hooks/usePlacePersist";
import { formSchema } from "../model/formSchema";
import type { LatLng } from "@/entities/geodata";
import { EbaliMap } from "@/features/map";
import { Circle } from "@/features/map";
import { useQuery } from "@tanstack/react-query";
import { AdvancedMarker, Pin } from "@vis.gl/react-google-maps";
import { twMerge } from "tailwind-merge";
type PlaceFormProps = {
existingPlace?: EventPlace;
onSubmit: (values: EventPlaceForm) => void;
disableFields: boolean;
};
export const PlaceForm: React.FC<PlaceFormProps> = ({
existingPlace,
onSubmit: submitHandler,
disableFields,
}) => {
const { data: places } = useQuery(placesQueries.list());
const { WithSubmitCheck, props } = useCheckSubmit();
const hasActiveEvents = existingPlace?.isUsed ?? false;
const isApproved = existingPlace?.status === "approved";
// Deposit can only be edited if:
// 1. Place is not yet approved (still submitted), AND
// 2. There are no active events
const canEditDeposit = !isApproved && !hasActiveEvents;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: existingPlace
? {
title: existingPlace.title,
geometry: existingPlace.geometry.coordinates[0] as LatLng,
maxTickets: existingPlace.maxTickets,
minTickets: existingPlace.minTickets,
minPrice: formatUsdt(BigInt(existingPlace.minPrice)),
minDays: existingPlace.minDays,
daysBeforeCancel: existingPlace.daysBeforeCancel,
eventDepositSize: formatUsdt(BigInt(existingPlace.eventDepositSize)),
available: existingPlace.available ?? true,
}
: {
title: "",
geometry: null,
minTickets: 1,
maxTickets: 100,
daysBeforeCancel: 1,
minDays: 1,
minPrice: "1",
eventDepositSize: "1",
available: true,
},
});
!existingPlace && usePlacePersist(form);
function onSubmit(values: z.infer<typeof formSchema>) {
submitHandler(values);
}
const [selectedLocation, setSelectedLocation] = useState<LatLng | null>(null);
const formLocation = form.watch("geometry");
const setFormLocation = (coords: LatLng | null) =>
form.setValue("geometry", coords);
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-7"
>
<FormField
control={form.control}
disabled={disableFields}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="Place's name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="geometry"
render={() => {
return (
<FormItem>
<FormLabel>Place location</FormLabel>
<div className="relative">
<p
className={twMerge(
"absolute pointer-events-none top-0 left-1 z-1 transition-all duration-500",
(selectedLocation || formLocation) && "opacity-0",
)}
>
Long press to place marker
</p>
<EbaliMap
initialCenter={formLocation ?? undefined}
className={twMerge(
"h-[55dvh] transition-all duration-300",
selectedLocation && "h-[40dvh]",
formLocation && "h-[30dvh]",
)}
longPressHandler={
formLocation
? () => {}
: (latLng) => {
form.clearErrors("geometry");
setSelectedLocation(latLng);
}
}
>
{places?.map((place) => {
const isExisting = place.id === existingPlace?.id;
return (
<Circle
key={place.id}
center={place.geometry.coordinates[0]}
radius={60}
strokeWeight={2}
strokeColor={isExisting ? "#3e8801" : "#02d6f2"}
fillColor={isExisting ? "#3e8801" : "#02d6f2"}
/>
);
})}
{selectedLocation && (
<AdvancedMarker position={selectedLocation}>
<Pin
background={"#ffc107"}
borderColor={"#ff9800"}
glyphColor={"#ffe082"}
/>
</AdvancedMarker>
)}
{formLocation && (
<AdvancedMarker position={formLocation}>
<Pin
background={"#76ff05"}
borderColor={"#006400"}
glyphColor={"#b2ff59"}
/>
</AdvancedMarker>
)}
</EbaliMap>
<FormMessage />
{selectedLocation && (
<div className="py-1">
<p className="text-lg py-2">Place set correctly?</p>
<div className="flex flex-row justify-between items-center">
<Button
variant="secondary"
className="w-1/4"
onClick={() => {
setFormLocation(selectedLocation);
form.clearErrors("geometry");
setSelectedLocation(null);
}}
>
Yes
</Button>
<Button
variant="destructive"
className="w-1/4"
onClick={() => setSelectedLocation(null)}
>
No
</Button>
</div>
</div>
)}
{formLocation && (
<Button
className="w-full mt-4"
onClick={() => {
setSelectedLocation(formLocation);
setFormLocation(null);
}}
>
Change
</Button>
)}
</div>
</FormItem>
);
}}
/>
<CustomFormComponent
control={form.control}
fieldDisabled={disableFields}
fieldName="minTickets"
title="Minimum tickets amount"
/>
<CustomFormComponent
control={form.control}
fieldDisabled={disableFields}
fieldName="maxTickets"
title="Maximum tickets amount"
/>
<CustomFormComponent
control={form.control}
fieldDisabled={disableFields}
fieldName="minDays"
title="Minimum days"
/>
<CustomFormComponent
control={form.control}
fieldDisabled={disableFields}
fieldName="minPrice"
title="Minimum price"
/>
<CustomFormComponent
control={form.control}
fieldDisabled={disableFields}
fieldName="daysBeforeCancel"
title="Days before cancel"
/>
<FormField
control={form.control}
name="eventDepositSize"
disabled={disableFields || !canEditDeposit}
render={({ field }) => (
<FormItem>
<FormLabel>Event deposit (USDT)</FormLabel>
<FormControl>
<Input
placeholder="Deposit required for events"
inputMode="decimal"
{...field}
value={field.value || ""}
onChange={(e) => field.onChange(e.target.value)}
disabled={disableFields || !canEditDeposit}
/>
</FormControl>
<FormMessage />
{!canEditDeposit && existingPlace && (
<p className="text-xs text-muted-foreground">
{isApproved
? "Deposit is set during approval and cannot be changed after."
: "Deposit cannot be changed while there are active events at this place."}
</p>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name={"available"}
disabled={disableFields}
render={({ field }) => (
<FormItem className="flex justify-between items-center">
<FormLabel>Available</FormLabel>
<FormControl>
<Switch
className="h-12"
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="sticky bottom-5 w-full mt-5">
<WithSubmitCheck {...props}>
<Button className="block mx-auto" type="submit" variant="default">
Submit
</Button>
</WithSubmitCheck>
</div>
</form>
</Form>
);
};
type CustomFormComponentProps = {
control: Control<EventPlaceForm, unknown, EventPlaceForm>;
fieldName:
| "maxTickets"
| "minTickets"
| "minPrice"
| "minDays"
| "daysBeforeCancel";
title: string;
fieldDisabled: boolean;
};
const CustomFormComponent = ({
control,
fieldName,
title,
fieldDisabled,
}: CustomFormComponentProps) => {
const isMoney = fieldName === "minPrice";
return (
<FormField
control={control}
name={fieldName}
disabled={fieldDisabled}
render={({ field }) => (
<FormItem>
<FormLabel>{title}</FormLabel>
<FormControl>
<Input
placeholder=""
inputMode={isMoney ? "decimal" : "numeric"}
{...field}
value={
isMoney
? ((field.value as string | undefined) ?? "")
: field.value
}
onChange={(e) =>
field.onChange(
isMoney ? e.target.value : handleNumericInput(e.target.value),
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
};