307 lines
8.6 KiB
TypeScript

/* eslint-disable camelcase */ // Directus database fields use snake_case
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable no-catch-all/no-catch-all */
import { useCallback, useContext, useEffect, useRef, useState } from 'react'
import { Popup as LeafletPopup, useMap } from 'react-leaflet'
import { toast } from 'react-toastify'
import { useAuth } from '#components/Auth/useAuth'
import { TextAreaInput } from '#components/Input/TextAreaInput'
import { TextInput } from '#components/Input/TextInput'
import TemplateItemContext from '#components/Item/TemplateItemContext'
import { useResetFilterTags } from '#components/Map/hooks/useFilter'
import { useAddItem, useItems, useUpdateItem } from '#components/Map/hooks/useItems'
import { usePopupForm } from '#components/Map/hooks/usePopupForm'
import { useAddTag, useTags } from '#components/Map/hooks/useTags'
import LayerContext from '#components/Map/LayerContext'
import { hashTagRegex } from '#utils/HashTagRegex'
import { randomColor } from '#utils/RandomColor'
import type { Item } from '#types/Item'
interface Props {
children?: React.ReactNode
}
export function ItemFormPopup(props: Props) {
const layerContext = useContext(LayerContext)
const { menuText, name: activeLayerName } = layerContext
const { popupForm, setPopupForm } = usePopupForm()
const [spinner, setSpinner] = useState(false)
const formRef = useRef<HTMLFormElement>(null)
const map = useMap()
const addItem = useAddItem()
const updateItem = useUpdateItem()
const items = useItems()
const tags = useTags()
const addTag = useAddTag()
const resetFilterTags = useResetFilterTags()
const { user } = useAuth()
// Extract form data into Item object
const parseFormData = useCallback(
(evt: React.FormEvent<HTMLFormElement>): Item => {
if (!popupForm) {
throw new Error('Popup form is not defined')
}
const formItem: Item = {} as Item
const formData = new FormData(evt.currentTarget)
for (const [key, value] of formData.entries()) {
if (key && typeof value === 'string') {
// eslint-disable-next-line security/detect-object-injection
;(formItem as unknown as Record<string, unknown>)[key] = value
}
}
formItem.position = {
type: 'Point',
coordinates: [popupForm.position.lng, popupForm.position.lat],
}
return formItem
},
[popupForm],
)
// Process hashtags in text and create new tags if needed
const processHashtags = useCallback(
(text: string) => {
if (!text) return
text
.toLocaleLowerCase()
.match(hashTagRegex)
?.forEach((tag) => {
const tagName = tag.slice(1).toLocaleLowerCase()
if (!tags.find((t) => t.name.toLocaleLowerCase() === tagName)) {
addTag({ id: crypto.randomUUID(), name: tag.slice(1), color: randomColor() })
}
})
},
[tags, addTag],
)
// Handle API operations with consistent error handling and return response data
const handleApiOperation = useCallback(
async (
operation: () => Promise<Item>,
successMessage: string,
): Promise<{ success: boolean; data?: Item }> => {
try {
const data = await operation()
toast.success(successMessage)
return { success: true, data }
} catch (error) {
toast.error(error instanceof Error ? error.message : String(error))
return { success: false }
}
},
[],
)
// Update existing item
const handleUpdateItem = useCallback(
async (formItem: Item) => {
if (!popupForm?.item) return false
const result = await handleApiOperation(
() =>
popupForm.layer.api?.updateItem!({ ...formItem, id: popupForm.item!.id }) ??
Promise.resolve({} as Item),
'Item updated',
)
if (result.success && result.data) {
// Ensure the item has the layer object attached
const itemWithLayer = {
...result.data,
layer: popupForm.layer,
user_created: formItem.user_created,
}
updateItem(itemWithLayer)
}
return result.success
},
[popupForm, handleApiOperation, updateItem],
)
// Create new item or update existing user profile
const handleCreateItem = useCallback(
async (formItem: Item) => {
if (!popupForm) return false
const existingUserItem = items.find(
(i) => i.user_created?.id === user?.id && i.layer === popupForm.layer,
)
const itemName = formItem.name ?? user?.first_name
if (!itemName) {
toast.error('Name must be defined')
return false
}
const isUserProfileUpdate = popupForm.layer.userProfileLayer && existingUserItem
const operation = isUserProfileUpdate
? () =>
popupForm.layer.api?.updateItem!({ ...formItem, id: existingUserItem.id }) ??
Promise.resolve({} as Item)
: () =>
popupForm.layer.api?.createItem!({
...formItem,
name: itemName,
id: crypto.randomUUID(),
}) ?? Promise.resolve({} as Item)
const result = await handleApiOperation(
operation,
isUserProfileUpdate ? 'Profile updated' : 'New item created',
)
if (result.success && result.data) {
// Ensure the item has the layer object attached
const itemWithLayer = {
...result.data,
layer: popupForm.layer,
user_created: user ?? undefined,
}
if (isUserProfileUpdate) {
updateItem(itemWithLayer)
} else {
addItem(itemWithLayer)
}
resetFilterTags()
}
return result.success
},
[popupForm, items, user, handleApiOperation, updateItem, addItem, resetFilterTags],
)
const handleSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
evt.preventDefault()
if (!popupForm) {
throw new Error('Popup form is not defined')
}
setSpinner(true)
try {
const formItem = parseFormData(evt)
// Process hashtags if text exists
if (formItem.text) {
processHashtags(formItem.text)
}
let success: boolean
if (popupForm.item) {
success = await handleUpdateItem(formItem)
} else {
success = await handleCreateItem(formItem)
}
if (success) {
map.closePopup()
setPopupForm(null)
}
} finally {
setSpinner(false)
}
}
const resetPopup = () => {
if (formRef.current) {
formRef.current.reset()
}
}
useEffect(() => {
resetPopup()
}, [popupForm?.position])
return (
popupForm?.layer.name === activeLayerName && (
<LeafletPopup
minWidth={275}
maxWidth={275}
autoPanPadding={[20, 80]}
eventHandlers={{
remove: () => {
setTimeout(function () {
resetPopup()
}, 100)
},
}}
position={popupForm.position}
>
<form
ref={formRef}
onReset={resetPopup}
autoComplete='off'
onSubmit={(e) => handleSubmit(e)}
>
{popupForm.item ? (
<div className=''></div>
) : (
<div className='tw:flex tw:justify-center'>
<b className='tw:text-xl tw:text-center tw:font-bold'>{menuText}</b>
</div>
)}
{props.children ? (
<TemplateItemContext.Provider value={popupForm.item}>
{props.children}
</TemplateItemContext.Provider>
) : (
<>
<TextInput
type='text'
placeholder='Name'
dataField='name'
defaultValue={popupForm.item ? popupForm.item.name : ''}
inputStyle=''
/>
<TextAreaInput
key={popupForm.position.toString()}
placeholder='Text'
dataField='text'
defaultValue={popupForm.item?.text ?? ''}
inputStyle='tw:h-40 tw:mt-5'
/>
</>
)}
<div className='tw:flex tw:justify-center'>
<button
className={
spinner
? 'tw:btn tw:btn-disabled tw:mt-5 tw:place-self-center'
: 'tw:btn tw:mt-5 tw:place-self-center'
}
type='submit'
>
{spinner ? <span className='tw:loading tw:loading-spinner'></span> : 'Save'}
</button>
</div>
</form>
</LeafletPopup>
)
)
}