diff --git a/src/Components/Input/Autocomplete.tsx b/src/Components/Input/Autocomplete.tsx index 7f2843b4..8d10acc3 100644 --- a/src/Components/Input/Autocomplete.tsx +++ b/src/Components/Input/Autocomplete.tsx @@ -1,5 +1,7 @@ import * as React from 'react' import { useEffect } from 'react'; +import { decodeTag } from '../../Utils/FormatTags'; +import { TagView } from '../Templates/TagView'; export const Autocomplete = ({ inputProps, suggestions, onSelected, pushFilteredSuggestions, setFocus }: { inputProps: any, suggestions: Array, onSelected: (suggestion) => void, pushFilteredSuggestions?: Array, setFocus?: boolean }) => { @@ -20,14 +22,6 @@ export const Autocomplete = ({ inputProps, suggestions, onSelected, pushFiltered const getSuggestionValue = suggestion => suggestion.name; - // Use your imagination to render suggestions. - const renderSuggestion = suggestion => ( -
-
-
#{suggestion.name} -
- ); - const getSuggestions = value => { const inputValue = value.trim().toLowerCase(); const inputLength = inputValue.length; @@ -51,9 +45,7 @@ export const Autocomplete = ({ inputProps, suggestions, onSelected, pushFiltered onSelected(suggestion) } - const handleKeyDown = (event) => { - console.log(filteredSuggestions); - + const handleKeyDown = (event) => { switch (event.key) { case 'ArrowDown': heighlightedSuggestion < filteredSuggestions.length-1 && setHeighlightedSuggestion(current => current +1) @@ -72,7 +64,6 @@ export const Autocomplete = ({ inputProps, suggestions, onSelected, pushFiltered inputProps.onKeyDown(event); break; } - } return ( @@ -80,7 +71,7 @@ export const Autocomplete = ({ inputProps, suggestions, onSelected, pushFiltered handleChange(e)} onKeyDown={handleKeyDown}/> diff --git a/src/Components/Map/Layer.tsx b/src/Components/Map/Layer.tsx index 5e424b4b..bdb20bf6 100644 --- a/src/Components/Map/Layer.tsx +++ b/src/Components/Map/Layer.tsx @@ -33,7 +33,11 @@ export const Layer = ({ itemOwnerField, itemLatitudeField = 'position.coordinates.1', itemLongitudeField = 'position.coordinates.0', + itemTagsField, + itemOffersField, + itemNeedsField, onlyOnePerOwner = false, + customEditLink, setItemFormPopup, itemFormPopup, clusterRef @@ -66,8 +70,8 @@ export const Layer = ({ useEffect(() => { - data && setItemsData({ data, children, name, menuIcon, menuText, menuColor, markerIcon, markerShape, markerDefaultColor, api, itemNameField, itemTextField, itemAvatarField, itemColorField, itemOwnerField, onlyOnePerOwner, setItemFormPopup, itemFormPopup, clusterRef }); - api && setItemsApi({ data, children, name, menuIcon, menuText, menuColor, markerIcon, markerShape, markerDefaultColor, api, itemNameField, itemTextField, itemAvatarField, itemColorField, itemOwnerField, onlyOnePerOwner, setItemFormPopup, itemFormPopup, clusterRef }); + data && setItemsData({ data, children, name, menuIcon, menuText, menuColor, markerIcon, markerShape, markerDefaultColor, api, itemNameField, itemTextField, itemAvatarField, itemColorField, itemOwnerField, itemTagsField, itemOffersField, itemNeedsField, onlyOnePerOwner, customEditLink, setItemFormPopup, itemFormPopup, clusterRef }); + api && setItemsApi({ data, children, name, menuIcon, menuText, menuColor, markerIcon, markerShape, markerDefaultColor, api, itemNameField, itemTextField, itemAvatarField, itemColorField, itemOwnerField, itemTagsField, itemOffersField, itemNeedsField, onlyOnePerOwner, customEditLink, setItemFormPopup, itemFormPopup, clusterRef }); }, [data, api]) useMapEvents({ @@ -139,9 +143,9 @@ export const Layer = ({ map((item: Item) => { if (getValue(item, itemLongitudeField) && getValue(item, itemLatitudeField)) { - if (getValue(item, itemTextField)) item[itemTextField] = getValue(item, itemTextField); else item[itemTextField] = ""; + if (item?.tags) { item[itemTextField] = item[itemTextField] + '\n\n'; item.tags.map(tag => { @@ -154,9 +158,9 @@ export const Layer = ({ if (allTagsLoaded && allItemsLoaded) { - item[itemTextField].toLocaleLowerCase().match(hashTagRegex)?.map(tag => { + item[itemTextField].match(hashTagRegex)?.map(tag => { if ((!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) && !newTagsToAdd.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) { - const newTag = { id: crypto.randomUUID(), name: tag.slice(1).toLocaleLowerCase(), color: randomColor() }; + const newTag = { id: crypto.randomUUID(), name: tag.slice(1), color: randomColor() }; setNewTagsToAdd(current => [...current, newTag]); } }); diff --git a/src/Components/Map/Subcomponents/Controls/TagsControl.tsx b/src/Components/Map/Subcomponents/Controls/TagsControl.tsx index 2f9adeb5..df4c53b7 100644 --- a/src/Components/Map/Subcomponents/Controls/TagsControl.tsx +++ b/src/Components/Map/Subcomponents/Controls/TagsControl.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { useFilterTags, useRemoveFilterTag } from '../../hooks/useFilter'; +import { decodeTag } from '../../../../Utils/FormatTags'; export const TagsControl = () => { @@ -13,7 +14,7 @@ export const TagsControl = () => {
-
#{formatTag(tag.name)} +
#{decodeTag(tag.name)} ) } @@ -21,7 +22,3 @@ export const TagsControl = () => { } -function formatTag(string : string) { - let formatedTag = string.replace(/_/g, " "); - return formatedTag = formatedTag.charAt(0).toUpperCase() + formatedTag.slice(1); -} \ No newline at end of file diff --git a/src/Components/Map/Subcomponents/ItemFormPopup.tsx b/src/Components/Map/Subcomponents/ItemFormPopup.tsx index 78abc2c6..16416f95 100644 --- a/src/Components/Map/Subcomponents/ItemFormPopup.tsx +++ b/src/Components/Map/Subcomponents/ItemFormPopup.tsx @@ -58,7 +58,7 @@ export function ItemFormPopup(props: ItemFormPopupProps) { formItem.text && formItem.text.toLocaleLowerCase().match(hashTagRegex)?.map(tag=> { if (!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) { - addTag({id: crypto.randomUUID(), name: tag.slice(1).toLocaleLowerCase(), color: randomColor()}) + addTag({id: crypto.randomUUID(), name: tag.slice(1), color: randomColor()}) } }); diff --git a/src/Components/Map/Subcomponents/ItemPopupComponents/TextView.tsx b/src/Components/Map/Subcomponents/ItemPopupComponents/TextView.tsx index 3ef49c64..be9445d6 100644 --- a/src/Components/Map/Subcomponents/ItemPopupComponents/TextView.tsx +++ b/src/Components/Map/Subcomponents/ItemPopupComponents/TextView.tsx @@ -7,6 +7,7 @@ import { fixUrls, mailRegex } from '../../../../Utils/ReplaceURLs'; import Markdown from 'react-markdown' import { getValue } from '../../../../Utils/GetValue'; import remarkBreaks from 'remark-breaks' +import { decodeTag } from '../../../../Utils/FormatTags'; export const TextView = ({ item, truncate = false}: { item?: Item, truncate?: boolean }) => { const tags = useTags(); @@ -95,7 +96,7 @@ export const TextView = ({ item, truncate = false}: { item?: Item, truncate?: bo addFilterTag(tag!); // map.fitBounds(items) // map.closePopup(); - }}>{children} + }}>{decodeTag(children)} )}; diff --git a/src/Components/Map/UtopiaMap.tsx b/src/Components/Map/UtopiaMap.tsx index e7b8c36d..2b4bdd7b 100644 --- a/src/Components/Map/UtopiaMap.tsx +++ b/src/Components/Map/UtopiaMap.tsx @@ -9,9 +9,9 @@ import AddButton from "./Subcomponents/AddButton"; import { useEffect, useState } from "react"; import { ItemFormPopupProps } from "./Subcomponents/ItemFormPopup"; import { ItemsProvider } from "./hooks/useItems"; -import { TagsProvider, useAllTagsLoaded, useTags } from "./hooks/useTags"; +import { TagsProvider } from "./hooks/useTags"; import { LayersProvider } from "./hooks/useLayers"; -import { FilterProvider, useAddFilterTag } from "./hooks/useFilter"; +import { FilterProvider } from "./hooks/useFilter"; import { SearchControl } from "./Subcomponents/Controls/SearchControl"; import { PermissionsProvider } from "./hooks/usePermissions"; import { LeafletRefsProvider } from "./hooks/useLeafletRefs"; diff --git a/src/Components/Map/hooks/useTags.tsx b/src/Components/Map/hooks/useTags.tsx index f542a368..db9d1cce 100644 --- a/src/Components/Map/hooks/useTags.tsx +++ b/src/Components/Map/hooks/useTags.tsx @@ -61,7 +61,7 @@ function useTagsManager(initialTags: Tag[]): { if(tagCount == 0) setallTagsLoaded(true); if (result) { result.map(tag => { - tag.name = tag.name.toLocaleLowerCase(); + //tag.name = tag.name.toLocaleLowerCase(); dispatch({ type: "ADD", tag }); }) } @@ -71,7 +71,7 @@ function useTagsManager(initialTags: Tag[]): { const setTagData = useCallback((data: Tag[]) => { data.map(tag => { - tag.name = tag.name.toLocaleLowerCase(); + //tag.name = tag.name.toLocaleLowerCase(); dispatch({ type: "ADD", tag }) }) }, []); @@ -90,11 +90,11 @@ function useTagsManager(initialTags: Tag[]): { const getItemTags = useCallback((item: Item) => { const text = item?.layer?.itemTextField && item ? getValue(item, item.layer?.itemTextField) : undefined; - const itemTagStrings = text?.toLocaleLowerCase().match(hashTagRegex); + const itemTagStrings = text?.match(hashTagRegex); const itemTags: Tag[] = []; itemTagStrings?.map(tag => { if (tags.find(t => t.name === tag.slice(1))) { - itemTags.push(tags.find(t => t.name === tag.slice(1))!) + itemTags.push(tags.find(t => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())!) } }) return itemTags diff --git a/src/Components/Profile/OverlayProfile.tsx b/src/Components/Profile/OverlayProfile.tsx index cb044899..8ac29b76 100644 --- a/src/Components/Profile/OverlayProfile.tsx +++ b/src/Components/Profile/OverlayProfile.tsx @@ -9,6 +9,8 @@ import { useMap } from 'react-leaflet'; import { LatLng } from 'leaflet'; import { TextView } from '../Map'; import useWindowDimensions from '../Map/hooks/useWindowDimension'; +import { TagView } from '../Templates/TagView'; +import { useTags } from '../Map/hooks/useTags'; export function OverlayProfile() { @@ -18,6 +20,11 @@ export function OverlayProfile() { const map = useMap(); const windowDimension = useWindowDimensions(); + const tags = useTags(); + + console.log(item); + + React.useEffect(() => { const itemId = location.pathname.split("/")[2]; @@ -32,18 +39,46 @@ export function OverlayProfile() { return ( - + {item && <>

{item.layer?.itemAvatarField && getValue(item, item.layer.itemAvatarField) && } {item.layer?.itemNameField && getValue(item, item.layer.itemNameField)}

-
+
+ { + item.layer?.itemOffersField && getValue(item, item.layer.itemOffersField).length > 0 ? + <> +

Offers

+ < div className='tw-flex tw-flex-wrap tw-mb-2'> + { + item.layer?.itemOffersField && getValue(item, item.layer.itemOffersField).map(o => { + const tag = tags.find(t => t.id === o.tags_id); + return (tag ? : "") + }) + } +
+ : "" + } + { + item.layer?.itemNeedsField && getValue(item, item.layer.itemNeedsField).length > 0 ? + <> +

Needs

+ < div className='tw-flex tw-flex-wrap tw-mb-4'> + { + item.layer?.itemNeedsField && getValue(item, item.layer.itemNeedsField).map(o => { + const tag = tags.find(t => t.id === o.tags_id); + return (tag ? : "") + }) + } +
+ : "" + } } -
+
) } diff --git a/src/Components/Profile/OverlayProfileSettings.tsx b/src/Components/Profile/OverlayProfileSettings.tsx index 672e50e7..f7b3ce95 100644 --- a/src/Components/Profile/OverlayProfileSettings.tsx +++ b/src/Components/Profile/OverlayProfileSettings.tsx @@ -13,9 +13,10 @@ import { hashTagRegex } from '../../Utils/HashTagRegex'; import { useAddTag, useTags } from '../Map/hooks/useTags'; import { randomColor } from '../../Utils/RandomColor'; import { useNavigate } from 'react-router-dom'; -import { UserItem } from '../../types'; +import { Tag, UserItem } from '../../types'; import { MapOverlayPage } from '../Templates'; import { TagsWidget } from './TagsWidget'; +import { decodeTag, encodeTag } from '../../Utils/FormatTags'; export function OverlayProfileSettings() { @@ -27,6 +28,8 @@ export function OverlayProfileSettings() { const [text, setText] = useState(""); const [avatar, setAvatar] = useState(""); const [color, setColor] = useState(""); + const [offers, setOffers] = useState>([]); + const [needs, setNeeds] = useState>([]); const [activeTab, setActiveTab] = useState(1); @@ -48,8 +51,18 @@ export function OverlayProfileSettings() { setId(user?.id ? user.id : ""); setName(user?.first_name ? user.first_name : ""); setText(user?.description ? user.description : ""); - setAvatar(user?.avatar ? user?.avatar : ""), - setColor(user?.color ? user.color : "#aabbcc") + setAvatar(user?.avatar ? user?.avatar : ""); + setColor(user?.color ? user.color : "#aabbcc"); + setOffers([]); + setNeeds([]); + user?.offers.map(o=> { + const offer = tags.find(t => t.id === o.tags_id); + offer && setOffers(current => [...current,offer]) + }) + user?.needs.map(o=> { + const need = tags.find(t => t.id === o.tags_id); + need && setNeeds(current => [...current,need]) + }) }, [user]) const imgRef = React.useRef(null) @@ -145,17 +158,49 @@ export function OverlayProfileSettings() { setAvatar(asset.id) } - - const onUpdateUser = () => { + const onUpdateUser = async () => { let changedUser = {} as UserItem; - changedUser = { id: id, first_name: name, description: text, color: color, ...avatar.length > 10 && { avatar: avatar } }; - const item = items.find(i => i.layer?.itemOwnerField && getValue(i, i.layer?.itemOwnerField).id === id); - if (item && item.layer && item.layer.itemOwnerField) item[item.layer.itemOwnerField] = changedUser; + let offer_updates : Array = []; + //check for new offers + offers.map(o => { + const existingOffer = user?.offers.find(t => t.tags_id === o.id) + existingOffer && offer_updates.push(existingOffer.id) + if(!existingOffer && !tags.some(t => t.id === o.id)) addTag({...o,offer_or_need: true}) + !existingOffer && offer_updates.push({directus_user_id: user?.id, tags_id: o.id}) + }); + let needs_updates : Array = []; + + needs.map(n => { + const existingNeed = user?.needs.find(t => t.tags_id === n.id) + existingNeed && needs_updates.push(existingNeed.id) + !existingNeed && needs_updates.push({directus_user_id: user?.id, tags_id: n.id}) + !existingNeed && !tags.some(t => t.id === n.id) && addTag({...n,offer_or_need: true}) + }); + + + changedUser = { id: id, first_name: name, description: text, color: color, ...avatar.length > 10 && { avatar: avatar }, ... offers.length > 0 && {offers: offer_updates}, ... needs.length > 0 && {needs: needs_updates} }; + // update profile item in current state + const item = items.find(i => i.layer?.itemOwnerField && getValue(i, i.layer?.itemOwnerField).id === id); + + let offer_state : Array = []; + let needs_state : Array = []; + + offers.map(o => { + offer_state.push({directus_user_id: user?.id, tags_id: o.id}) + }); + + needs.map(n => { + needs_state.push({directus_user_id: user?.id, tags_id: n.id}) + }); + + + if (item && item.layer && item.layer.itemOwnerField) item[item.layer.itemOwnerField] = {... changedUser, offers: offer_state, needs: needs_state}; + // add new hashtags from profile text text.toLocaleLowerCase().match(hashTagRegex)?.map(tag => { if (!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) { - addTag({ id: crypto.randomUUID(), name: tag.slice(1).toLocaleLowerCase(), color: randomColor() }) + addTag({ id: crypto.randomUUID(), name: encodeTag(tag.slice(1).toLocaleLowerCase()), color: randomColor()}) } }); @@ -178,7 +223,7 @@ export function OverlayProfileSettings() { return ( <> - +
{!cropping ? @@ -216,17 +261,17 @@ export function OverlayProfileSettings() {
setActiveTab(1)} />
- setText(v)} containerStyle='tw-h-full' inputStyle='tw-h-full tw-border-t-0' /> + setText(v)} containerStyle='tw-h-full' inputStyle='tw-h-full tw-border-t-0 tw-rounded-tl-none' />
setActiveTab(2)} />
- + setOffers(v)} placeholder="enter your offers" containerStyle='tw-bg-transparent tw-w-full tw-h-full tw-mt-3 tw-text-xs tw-h-[calc(100%-1rem)] tw-min-h-[5em] tw-pb-2 tw-overflow-auto'/>
- + setNeeds(v)} placeholder="enter your needs" containerStyle='tw-bg-transparent tw-w-full tw-h-full tw-mt-3 tw-text-xs tw-h-[calc(100%-1rem)] tw-min-h-[5em] tw-pb-2 tw-overflow-auto'/>
diff --git a/src/Components/Profile/TagsWidget.tsx b/src/Components/Profile/TagsWidget.tsx index d07ae530..d3a359a6 100644 --- a/src/Components/Profile/TagsWidget.tsx +++ b/src/Components/Profile/TagsWidget.tsx @@ -4,11 +4,11 @@ import { useTags } from '../Map/hooks/useTags'; import { Tag } from '../../types'; import { Autocomplete } from '../Input/Autocomplete'; import { randomColor } from '../../Utils/RandomColor'; +import { decodeTag, encodeTag } from '../../Utils/FormatTags'; -export const TagsWidget = ({placeholder, containerStyle}) => { +export const TagsWidget = ({placeholder, containerStyle, defaultTags, onUpdate}) => { const [input, setInput] = useState(''); - const [localTags, setLocalTags] = useState>([]); const [isKeyReleased, setIsKeyReleased] = useState(false); const tags = useTags(); const [pushFilteredSuggestions, setPushFilteredSuggestions] = useState>([]); @@ -25,20 +25,20 @@ export const TagsWidget = ({placeholder, containerStyle}) => { const { key } = e; const trimmedInput = input.trim(); - if ((key === 'Enter' || key === ',' ) && trimmedInput.length && !localTags.some(tag => tag.name.toLocaleLowerCase() === trimmedInput.toLocaleLowerCase())) { + if ((key === 'Enter' || key === ',' ) && trimmedInput.length && !defaultTags.some(tag => tag.name.toLocaleLowerCase() === trimmedInput.toLocaleLowerCase())) { e.preventDefault(); const newTag = tags.find(t => t.name === trimmedInput.toLocaleLowerCase()) - newTag && setLocalTags(prevState => [...prevState, newTag]); - !newTag && setLocalTags(prevState => [...prevState, { id: crypto.randomUUID(), name: trimmedInput.toLocaleLowerCase(), color: randomColor() }]); + newTag && onUpdate(prevState => [...prevState, newTag]); + !newTag && onUpdate(prevState => [...prevState, { id: crypto.randomUUID(), name: encodeTag(trimmedInput), color: randomColor() }]); setInput(''); setPushFilteredSuggestions([]); } - if (key === "Backspace" && !input.length && localTags.length && isKeyReleased) { - const localTagsCopy = [...localTags]; - const poppedTag = localTagsCopy.pop(); + if (key === "Backspace" && !input.length && defaultTags.length && isKeyReleased) { + const defaultTagsCopy = [...defaultTags]; + const poppedTag = defaultTagsCopy.pop(); e.preventDefault(); - setLocalTags(localTagsCopy); + onUpdate(defaultTagsCopy); poppedTag && setInput(poppedTag.name); } @@ -50,15 +50,15 @@ export const TagsWidget = ({placeholder, containerStyle}) => { } const deleteTag = (tag) => { - setLocalTags(prevState => prevState.filter((t) => t !== tag)) + onUpdate(prevState => prevState.filter((t) => t !== tag)) } const onSelected = (tag) => { - if(!localTags.some(t => t.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase())) { - const newTag = tags.find(t => t.name === tag.name.toLocaleLowerCase()) - newTag && setLocalTags(prevState => [...prevState, newTag]); - !newTag && setLocalTags(prevState => [...prevState, { id: crypto.randomUUID(), name: tag.name.toLocaleLowerCase(), color: randomColor() }]); + if(!defaultTags.some(t => t.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase())) { + const newTag = tags.find(t => t.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase()) + newTag && onUpdate(prevState => [...prevState, newTag]); + !newTag && onUpdate(prevState => [...prevState, { id: crypto.randomUUID(), name: tag.name.toLocaleLowerCase(), color: randomColor() }]); setInput(''); setPushFilteredSuggestions([]); } @@ -81,11 +81,11 @@ export const TagsWidget = ({placeholder, containerStyle}) => { }, 200) }} className={`tw-input tw-input-bordered tw-cursor-text ${containerStyle}`}>
- {localTags.map((tag) => ( + {defaultTags.map((tag) => (
-
#{tag.name} +
#{decodeTag(tag.name)}
))} diff --git a/src/Components/Templates/TagView.tsx b/src/Components/Templates/TagView.tsx new file mode 100644 index 00000000..b694b67a --- /dev/null +++ b/src/Components/Templates/TagView.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' +import { decodeTag } from '../../Utils/FormatTags' +import { Tag } from '../../types' + +export const TagView = ({tag} : {tag: Tag}) => { + return ( + // Use your imagination to render suggestions. + +
+
+
#{decodeTag(tag.name)} +
+ + ) +} diff --git a/src/Utils/FormatTags.ts b/src/Utils/FormatTags.ts new file mode 100644 index 00000000..85e85324 --- /dev/null +++ b/src/Utils/FormatTags.ts @@ -0,0 +1,8 @@ +export function decodeTag(string : string) { + let formatedTag = string.replace(/_/g, "\u00A0"); + return formatedTag = formatedTag.charAt(0).toUpperCase() + formatedTag.slice(1); +} + +export function encodeTag(string : string) { + return string.replace(/\s+/, "_"); +} \ No newline at end of file diff --git a/src/Utils/GetValue.ts b/src/Utils/GetValue.ts index 5e68d834..6e78c8ff 100644 --- a/src/Utils/GetValue.ts +++ b/src/Utils/GetValue.ts @@ -1,8 +1,10 @@ export function getValue(obj, path) { - if (obj) { - for (var i = 0, path = path.split('.'), len = path.length; i < len; i++) { - if(obj) obj = obj[path[i]]; - }; - return obj; + if (!obj) return undefined; // Return early if obj is falsy + + var pathArray = path.split('.'); // Use a different variable for the split path + for (var i = 0, len = pathArray.length; i < len; i++) { + if (!obj) return undefined; // Check if obj is falsy at each step + obj = obj[pathArray[i]]; // Dive one level deeper } -}; \ No newline at end of file + return obj; // Return the final value +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 7447d1a3..1ee9d13b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,10 +26,13 @@ export interface LayerProps { itemAvatarField?: string, itemColorField?: string, itemOwnerField?: string, - itemTagField?: string, + itemTagsField?: string, itemLatitudeField?: any, itemLongitudeField?: any, + itemOffersField?: string, + itemNeedsField?: string, onlyOnePerOwner?: boolean, + customEditLink?: string, setItemFormPopup?: React.Dispatch>, itemFormPopup?: ItemFormPopupProps | null, clusterRef?: React.MutableRefObject @@ -71,6 +74,7 @@ export interface Tag { color: string; id: string; name: string; + offer_or_need?: boolean } export interface ItemsApi {