This commit is contained in:
Anton Tranelis 2025-12-16 17:50:29 +01:00
parent 5d3a24fb64
commit 24f10e2b0a
8 changed files with 91 additions and 82 deletions

View File

@ -1,78 +1,93 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { useEffect, useRef, useState } from 'react'
import { TagView } from '#components/Templates/TagView'
import type { Tag } from '#types/Tag'
import type { ChangeEvent, KeyboardEvent } from 'react'
interface InputProps {
value: string
placeholder?: string
onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void
onKeyUp: () => void
onChange: (e: ChangeEvent<HTMLInputElement>) => void
className?: string
}
interface AutocompleteProps {
inputProps: InputProps
suggestions: Tag[]
onSelected: (suggestion: Tag) => void
pushFilteredSuggestions?: Tag[]
setFocus?: boolean
}
export const Autocomplete = ({
inputProps,
suggestions,
onSelected,
pushFilteredSuggestions,
setFocus,
}: {
inputProps: any
suggestions: any[]
onSelected: (suggestion) => void
pushFilteredSuggestions?: any[]
setFocus?: boolean
}) => {
const [filteredSuggestions, setFilteredSuggestions] = useState<any[]>([])
const [heighlightedSuggestion, setHeighlightedSuggestion] = useState<number>(0)
}: AutocompleteProps) => {
const [filteredSuggestions, setFilteredSuggestions] = useState<Tag[]>([])
const [highlightedSuggestion, setHighlightedSuggestion] = useState<number>(0)
useEffect(() => {
pushFilteredSuggestions && setFilteredSuggestions(pushFilteredSuggestions)
if (pushFilteredSuggestions) {
setFilteredSuggestions(pushFilteredSuggestions)
}
}, [pushFilteredSuggestions])
useEffect(() => {
setFocus && inputRef.current?.focus()
if (setFocus) {
inputRef.current?.focus()
}
}, [setFocus])
const inputRef = useRef<HTMLInputElement>()
const inputRef = useRef<HTMLInputElement>(null)
const getSuggestions = (value) => {
const getSuggestions = (value: string): Tag[] => {
const inputValue = value.trim().toLowerCase()
const inputLength = inputValue.length
return inputLength === 0
? []
: suggestions.filter((tag) => tag.name.toLowerCase().slice(0, inputLength) === inputValue)
: suggestions.filter((tag) => tag.name.toLowerCase().startsWith(inputValue))
}
const handleChange = (e) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setFilteredSuggestions(getSuggestions(e.target.value))
// Call the parent's onChange handler, if it exists
if (inputProps.onChange) {
inputProps.onChange(e)
}
// Call the parent's onChange handler
inputProps.onChange(e)
}
function handleSuggestionClick(suggestion) {
function handleSuggestionClick(suggestion: Tag) {
onSelected(suggestion)
}
const handleKeyDown = (event) => {
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
switch (event.key) {
case 'ArrowDown':
heighlightedSuggestion < filteredSuggestions.length - 1 &&
setHeighlightedSuggestion((current) => current + 1)
if (highlightedSuggestion < filteredSuggestions.length - 1) {
setHighlightedSuggestion((current) => current + 1)
}
break
case 'ArrowUp':
heighlightedSuggestion > 0 && setHeighlightedSuggestion((current) => current - 1)
if (highlightedSuggestion > 0) {
setHighlightedSuggestion((current) => current - 1)
}
break
case 'Enter':
event.preventDefault()
if (filteredSuggestions.length > 0) {
// eslint-disable-next-line security/detect-object-injection
onSelected(filteredSuggestions[heighlightedSuggestion])
setHeighlightedSuggestion(0)
onSelected(filteredSuggestions[highlightedSuggestion])
setHighlightedSuggestion(0)
}
if (filteredSuggestions.length === 0) {
inputProps.onKeyDown(event)
}
filteredSuggestions.length === 0 && inputProps.onKeyDown(event)
break
default:
inputProps.onKeyDown(event)
@ -87,16 +102,16 @@ export const Autocomplete = ({
{...inputProps}
type='text'
onChange={(e) => handleChange(e)}
tabIndex='-1'
tabIndex={-1}
onKeyDown={handleKeyDown}
className='tw:border-none tw:focus:outline-none tw:focus:ring-0 tw:mt-5 tw:w-full'
/>
<ul
className={`tw:absolute tw:z-4000 ${filteredSuggestions.length > 0 && 'tw:bg-base-100 tw:rounded-xl tw:p-2'}`}
className={`tw:absolute tw:z-4000 ${filteredSuggestions.length > 0 ? 'tw:bg-base-100 tw:rounded-xl tw:p-2' : ''}`}
>
{filteredSuggestions.map((suggestion, index) => (
<li key={index} onClick={() => handleSuggestionClick(suggestion)}>
<TagView heighlight={index === heighlightedSuggestion} tag={suggestion}></TagView>
<li key={suggestion.id} onClick={() => handleSuggestionClick(suggestion)}>
<TagView heighlight={index === highlightedSuggestion} tag={suggestion}></TagView>
</li>
))}
</ul>

View File

@ -21,8 +21,8 @@ import { useTags } from '#components/Map/hooks/useTags'
import { HeaderView } from '#components/Map/Subcomponents/ItemPopupComponents/HeaderView'
import { MapOverlayPage } from '#components/Templates'
import { handleDelete, linkItem, unlinkItem } from './itemFunctions'
import { AttestationsContext } from './hooks/useAttestations'
import { handleDelete, linkItem, unlinkItem } from './itemFunctions'
import { FlexView } from './Templates/FlexView'
import { OnepagerView } from './Templates/OnepagerView'
import { SimpleView } from './Templates/SimpleView'

View File

@ -53,13 +53,16 @@ export const AttestationsView = ({ item, heading = 'Trust', hideWhenEmpty = true
</td>
<td>
{getUserProfile(a.user_created.id) ? (
<Link to={'/item/' + getUserProfile(a.user_created.id)?.id}>
<Link to={'/item/' + (getUserProfile(a.user_created.id)?.id ?? '')}>
<div className='flex items-center gap-3'>
<div className='tw:avatar'>
<div className='tw:mask tw:rounded-full tw:h-8 tw:w-8 tw:mr-2'>
{getUserProfile(a.user_created.id)?.image && (
<img
src={appState.assetsApi.url + getUserProfile(a.user_created.id)?.image}
src={
appState.assetsApi.url +
(getUserProfile(a.user_created.id)?.image ?? '')
}
alt='Avatar'
/>
)}
@ -67,7 +70,7 @@ export const AttestationsView = ({ item, heading = 'Trust', hideWhenEmpty = true
</div>
<div>
<div className='font-bold'>
{getUserProfile(a.user_created.id)?.name ?? a.user_created.first_name}{' '}
{getUserProfile(a.user_created.id)?.name ?? a.user_created.first_name}
</div>
<div className='tw:text-xs opacity-50 tw:text-zinc-500'>
{timeAgo(a.date_created)}

View File

@ -12,26 +12,24 @@ interface Props {
placeholder?: string
}
export const ProfileTagsForm = ({
state,
setState,
dataField,
heading,
placeholder,
}: Props) => {
export const ProfileTagsForm = ({ state, setState, dataField, heading, placeholder }: Props) => {
const defaultHeading = dataField === 'offers' ? 'Offers' : 'Needs'
const defaultPlaceholder = dataField === 'offers' ? 'enter your offers' : 'enter your needs'
return (
<div className='tw:flex-1 tw:flex tw:flex-col tw:min-h-0'>
<h3 className='tw:text-base tw:font-semibold tw:mt-4 tw:mb-2 tw:flex-none'>{heading ?? defaultHeading}</h3>
<h3 className='tw:text-base tw:font-semibold tw:mt-4 tw:mb-2 tw:flex-none'>
{heading ?? defaultHeading}
</h3>
<TagsWidget
// eslint-disable-next-line security/detect-object-injection
defaultTags={state[dataField]}
onUpdate={(tags) =>
setState((prevState) => ({
...prevState,
[dataField]: tags,
}))
setState((prevState) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const updated = { ...prevState, [dataField]: tags }
return updated
})
}
placeholder={placeholder ?? defaultPlaceholder}
containerStyle='tw:bg-transparent tw:w-full tw:flex-1 tw:text-xs tw:pb-2 tw:overflow-auto'

View File

@ -12,16 +12,12 @@ interface Props {
hideWhenEmpty?: boolean
}
export const ProfileTagsView = ({
item,
dataField,
heading,
hideWhenEmpty = true,
}: Props) => {
export const ProfileTagsView = ({ item, dataField, heading, hideWhenEmpty = true }: Props) => {
const addFilterTag = useAddFilterTag()
const allTags = useTags()
// Get the tag IDs from the item based on dataField
// eslint-disable-next-line security/detect-object-injection
const tagRelations = item[dataField] ?? []
// Resolve tag IDs to full Tag objects

View File

@ -6,8 +6,8 @@ import { CrowdfundingForm } from '#components/Profile/Subcomponents/Crowdfunding
import { GalleryForm } from '#components/Profile/Subcomponents/GalleryForm'
import { GroupSubheaderForm } from '#components/Profile/Subcomponents/GroupSubheaderForm'
import { ProfileStartEndForm } from '#components/Profile/Subcomponents/ProfileStartEndForm'
import { ProfileTextForm } from '#components/Profile/Subcomponents/ProfileTextForm'
import { ProfileTagsForm } from '#components/Profile/Subcomponents/ProfileTagsForm'
import { ProfileTextForm } from '#components/Profile/Subcomponents/ProfileTextForm'
import type { FormState } from '#types/FormState'
import type { Item } from '#types/Item'
@ -34,7 +34,7 @@ interface TabItem {
title: string
icon?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items: Array<{ collection: string; id: Key | null | undefined; item: any }>
items: { collection: string; id: Key | null | undefined; item: any }[]
}
interface Props {
@ -42,24 +42,18 @@ interface Props {
state: FormState
setState: React.Dispatch<React.SetStateAction<FormState>>
tabs: TabItem[]
icon_as_labels?: boolean
iconAsLabels?: boolean
}
export const TabsContainerForm = ({
item,
state,
setState,
tabs,
icon_as_labels = false,
}: Props) => {
export const TabsContainerForm = ({ item, state, setState, tabs, iconAsLabels = false }: Props) => {
const location = useLocation()
const navigate = useNavigate()
const [activeTab, setActiveTab] = useState<number>(0)
const tabsLength = tabs?.length ?? 0
const tabsLength = tabs.length
useEffect(() => {
if (!tabs || tabs.length === 0) return
if (tabs.length === 0) return
const params = new URLSearchParams(location.search)
const urlTab = params.get('tab')
@ -82,7 +76,7 @@ export const TabsContainerForm = ({
[location.pathname, location.search, navigate],
)
if (!tabs || tabs.length === 0) {
if (tabs.length === 0) {
return null
}
@ -92,7 +86,7 @@ export const TabsContainerForm = ({
<div className='tw:flex tw:bg-base-200 tw:rounded-lg tw:p-1 tw:mb-4 tw:flex-none'>
{tabs.map((tab, index) => (
<button
type="button"
type='button'
key={tab.id}
className={`tw:flex-1 tw:flex tw:items-center tw:justify-center tw:gap-2 tw:py-2 tw:px-4 tw:rounded-md tw:transition-colors tw:cursor-pointer ${activeTab === index ? 'tw:bg-primary tw:text-primary-content' : 'hover:tw:bg-base-300'}`}
onClick={() => updateActiveTab(index)}
@ -104,15 +98,17 @@ export const TabsContainerForm = ({
}}
>
{tab.icon && <span>{tab.icon}</span>}
{!(icon_as_labels && activeTab !== index) && <span>{tab.title}</span>}
{!(iconAsLabels && activeTab !== index) && <span>{tab.title}</span>}
</button>
))}
</div>
{/* Tab Content */}
<div className='tw:flex-1 tw:flex tw:flex-col tw:min-h-0'>
{tabs[activeTab]?.items.map((templateItem) => {
{/* eslint-disable-next-line security/detect-object-injection */}
{tabs[activeTab].items.map((templateItem) => {
const TemplateComponent = componentMap[templateItem.collection]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return TemplateComponent ? (
<TemplateComponent
key={templateItem.id}

View File

@ -1,5 +1,4 @@
/* eslint-disable camelcase */
import React, { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { AttestationsView } from '#components/Profile/Subcomponents/AttestationsView'
@ -43,10 +42,10 @@ interface TabItem {
interface Props {
item: Item
tabs: TabItem[]
icon_as_labels?: boolean
iconAsLabels?: boolean
}
export const TabsContainerView = ({ item, tabs = [], icon_as_labels = false }: Props) => {
export const TabsContainerView = ({ item, tabs = [], iconAsLabels = false }: Props) => {
const location = useLocation()
const navigate = useNavigate()
const [activeTab, setActiveTab] = useState<number>(0)
@ -92,15 +91,17 @@ export const TabsContainerView = ({ item, tabs = [], icon_as_labels = false }: P
onClick={() => updateActiveTab(index)}
>
{tab.icon && <span>{tab.icon}</span>}
{!(icon_as_labels && activeTab !== index) && <span>{tab.title}</span>}
{!(iconAsLabels && activeTab !== index) && <span>{tab.title}</span>}
</button>
))}
</div>
{/* Tab Content */}
<div className='tw:overflow-y-auto fade tw:pb-4 tw:overflow-x-hidden'>
{tabs[activeTab]?.items.map((templateItem) => {
{/* eslint-disable-next-line security/detect-object-injection */}
{tabs[activeTab].items.map((templateItem) => {
const TemplateComponent = componentMap[templateItem.collection]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return TemplateComponent ? (
<TemplateComponent key={templateItem.id} item={item} {...templateItem.item} />
) : (

View File

@ -8,7 +8,7 @@ export interface Attestation {
shape: string
date_created: string
user_created: { id: string; first_name: string }
to: Array<{ directus_users_id: string }>
to: { directus_users_id: string }[]
}
export const AttestationsContext = createContext<Attestation[]>([])