mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
merged
This commit is contained in:
commit
e0eb300304
12
package-lock.json
generated
12
package-lock.json
generated
@ -1,20 +1,12 @@
|
||||
{
|
||||
"name": "utopia-ui",
|
||||
<<<<<<< HEAD
|
||||
"version": "3.0.0-alpha.204",
|
||||
=======
|
||||
"version": "3.0.0-alpha.215",
|
||||
>>>>>>> 8dc8779fe58040fb5c2a763d6519e57ddffc7ab7
|
||||
"version": "3.0.0-alpha.248",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "utopia-ui",
|
||||
<<<<<<< HEAD
|
||||
"version": "3.0.0-alpha.204",
|
||||
=======
|
||||
"version": "3.0.0-alpha.215",
|
||||
>>>>>>> 8dc8779fe58040fb5c2a763d6519e57ddffc7ab7
|
||||
"version": "3.0.0-alpha.248",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.0.17",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "utopia-ui",
|
||||
"version": "3.0.0-alpha.215",
|
||||
"version": "3.0.0-alpha.248",
|
||||
"description": "Reuseable React Components to build mapping apps for real life communities and networks",
|
||||
"repository": "https://github.com/utopia-os/utopia-ui",
|
||||
"homepage:": "https://utopia-os.org/",
|
||||
|
||||
@ -71,7 +71,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: route[], bottomRoute
|
||||
data-te-sidenav-content="#app-content"
|
||||
data-te-sidenav-slim-collapsed="true"
|
||||
data-te-sidenav-slim-width="56"
|
||||
data-te-sidenav-width="160">
|
||||
data-te-sidenav-width="180">
|
||||
<div className={`tw-flex tw-flex-col ${embedded ? "tw-h-full" :"tw-h-[calc(100dvh-64px)]"}`}>
|
||||
<ul className="tw-menu tw-w-full tw-bg-base-100 tw-text-base-content tw-p-0" data-te-sidenav-menu-ref>
|
||||
{
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
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<any>, onSelected: (suggestion) => void, pushFilteredSuggestions?: Array<any>, setFocus?: boolean }) => {
|
||||
@ -46,6 +45,7 @@ export const Autocomplete = ({ inputProps, suggestions, onSelected, pushFiltered
|
||||
}
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
event.preventDefault();
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
heighlightedSuggestion < filteredSuggestions.length-1 && setHeighlightedSuggestion(current => current +1)
|
||||
@ -68,7 +68,7 @@ export const Autocomplete = ({ inputProps, suggestions, onSelected, pushFiltered
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input ref={inputRef} {...inputProps} type="text" onChange={(e) => handleChange(e)} onKeyDown={handleKeyDown}/>
|
||||
<input ref={inputRef} {...inputProps} type="text" onChange={(e) => handleChange(e)} tabindex="-1" onKeyDown={handleKeyDown}/>
|
||||
<ul 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>
|
||||
|
||||
@ -15,17 +15,38 @@ type InputTextProps = {
|
||||
}
|
||||
|
||||
|
||||
export function TextInput({labelTitle, labelStyle, type, dataField, containerStyle, inputStyle, defaultValue, placeholder, autocomplete, updateFormValue} : InputTextProps){
|
||||
export function TextInput({ labelTitle, labelStyle, type, dataField, containerStyle, inputStyle, defaultValue, placeholder, autocomplete, updateFormValue }: InputTextProps) {
|
||||
const [inputValue, setInputValue] = useState<string>(defaultValue || "");
|
||||
|
||||
return(
|
||||
useEffect(() => {
|
||||
setInputValue(defaultValue || "");
|
||||
}, [defaultValue]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setInputValue(newValue);
|
||||
if (updateFormValue) {
|
||||
updateFormValue(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`tw-form-control ${containerStyle}`}>
|
||||
{labelTitle ? <label className="tw-label">
|
||||
<span className={"tw-label-text tw-text-base-content " + labelStyle}>{labelTitle}</span>
|
||||
</label>
|
||||
: " "}
|
||||
<input required type={type || "text"} name={dataField} defaultValue={defaultValue} placeholder={placeholder || ""} autoComplete={autocomplete} onChange={(e) => updateFormValue&& updateFormValue(e.target.value)}className={`tw-input tw-input-bordered tw-w-full ${inputStyle ? inputStyle : ""}`} />
|
||||
{labelTitle ? (
|
||||
<label className="tw-label">
|
||||
<span className={`tw-label-text tw-text-base-content ${labelStyle}`}>{labelTitle}</span>
|
||||
</label>
|
||||
) : null}
|
||||
<input
|
||||
required
|
||||
type={type || "text"}
|
||||
name={dataField}
|
||||
value={inputValue}
|
||||
placeholder={placeholder || ""}
|
||||
autoComplete={autocomplete}
|
||||
onChange={handleChange}
|
||||
className={`tw-input tw-input-bordered tw-w-full ${inputStyle || ""}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
@ -149,7 +149,7 @@ export const Layer = ({
|
||||
items.
|
||||
filter(item => item.layer?.name === name)?.
|
||||
filter(item =>
|
||||
filterTags.length == 0 ? item : filterTags.every(tag => getItemTags(item).some(filterTag => filterTag.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase())))?.
|
||||
filterTags.length == 0 ? item : filterTags.some(tag => getItemTags(item).some(filterTag => filterTag.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase())))?.
|
||||
filter(item => item.layer && isLayerVisible(item.layer)).
|
||||
filter(item => item.group_type && isGroupTypeVisible(item.group_type)|| visibleGroupTypes.length == 0).
|
||||
map((item: Item) => {
|
||||
|
||||
@ -26,7 +26,7 @@ export function FilterControl() {
|
||||
<div className="tw-card tw-bg-base-100 tw-shadow-xl tw-mt-2 tw-w-fit">
|
||||
{
|
||||
open ?
|
||||
<div className="tw-card-body tw-p-2 tw-w-fit tw-transition-all tw-duration-300">
|
||||
<div className="tw-card-body tw-pr-4 tw-min-w-[8rem] tw-p-2 tw-w-fit tw-transition-all tw-duration-300">
|
||||
<label className="tw-btn tw-btn-sm tw-rounded-2xl tw-btn-circle tw-btn-ghost hover:tw-bg-transparent tw-absolute tw-right-0 tw-top-0 tw-text-gray-600" onClick={() => {
|
||||
setOpen(false)
|
||||
}}>
|
||||
|
||||
@ -20,7 +20,7 @@ export function LayerControl() {
|
||||
<div className="tw-card tw-bg-base-100 tw-shadow-xl tw-mt-2 tw-w-fit">
|
||||
{
|
||||
open ?
|
||||
<div className="tw-card-body tw-p-2 tw-w-36 tw-transition-all tw-duration-300">
|
||||
<div className="tw-card-body tw-pr-4 tw-min-w-[8rem] tw-p-2 tw-transition-all tw-w-fit tw-duration-300">
|
||||
<label className="tw-btn tw-btn-sm tw-rounded-2xl tw-btn-circle tw-btn-ghost hover:tw-bg-transparent tw-absolute tw-right-0 tw-top-0 tw-text-gray-600" onClick={() => {
|
||||
setOpen(false)
|
||||
}}>
|
||||
|
||||
@ -129,7 +129,7 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
|
||||
<form ref={formRef} onReset={resetPopup} autoComplete='off' onSubmit={e => handleSubmit(e)}>
|
||||
{props.item ? <div className='tw-h-3'></div>
|
||||
:
|
||||
<div className='tw-flex tw-justify-center'><b className="tw-text-xl tw-font-bold">{ popupTitle? popupTitle : `New ${props.layer.name}`}</b></div>
|
||||
<div className='tw-flex tw-justify-center'><b className="tw-text-xl tw-text-center tw-font-bold">{ props.layer.menuText}</b></div>
|
||||
}
|
||||
|
||||
{props.children ?
|
||||
|
||||
@ -2,11 +2,18 @@ import * as React from 'react'
|
||||
import { TextInput } from '../../../Input'
|
||||
import { Item } from '../../../../types'
|
||||
|
||||
export const PopupStartEndInput = ({item}:{item?:Item}) => {
|
||||
type StartEndInputProps = {
|
||||
item?:Item,
|
||||
showLabels?: boolean
|
||||
updateStartValue?: (value: string ) => void;
|
||||
updateEndValue?: (value: string ) => void;
|
||||
}
|
||||
|
||||
export const PopupStartEndInput = ({item, showLabels = true, updateStartValue, updateEndValue}:StartEndInputProps) => {
|
||||
return (
|
||||
<div className='tw-grid tw-grid-cols-2 tw-gap-2 tw-mb-5'>
|
||||
<TextInput type='date' placeholder='start' dataField='start' inputStyle='tw-text-sm tw-px-2' labelTitle='start' defaultValue={item && item.start? item.start.substring(0, 10) : ""} autocomplete='one-time-code'></TextInput>
|
||||
<TextInput type='date' placeholder='end' dataField='end' inputStyle='tw-text-sm tw-px-2' labelTitle='end' defaultValue={item && item.end ? item.end.substring(0, 10) : ""} autocomplete='one-time-code'></TextInput>
|
||||
<TextInput type='date' placeholder='start' dataField='start' inputStyle='tw-text-sm tw-px-2' labelTitle={showLabels ? "start" :""} defaultValue={item && item.start? item.start.substring(0, 10) : ""} autocomplete='one-time-code' updateFormValue={updateStartValue}></TextInput>
|
||||
<TextInput type='date' placeholder='end' dataField='end' inputStyle='tw-text-sm tw-px-2' labelTitle={showLabels ? "end" :""} defaultValue={item && item.end ? item.end.substring(0, 10) : ""} autocomplete='one-time-code' updateFormValue={updateEndValue}></TextInput>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -80,7 +80,11 @@ function usePermissionsManager(initialPermissions: Permission[]): {
|
||||
};
|
||||
|
||||
const evaluatePermissions = (permissionConditions: any) => {
|
||||
return permissionConditions._and?.every((andCondition: any) =>
|
||||
if (!permissionConditions || !permissionConditions._and) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return permissionConditions._and.every((andCondition: any) =>
|
||||
andCondition._or
|
||||
? andCondition._or.some((orCondition: any) => evaluateCondition(orCondition))
|
||||
: evaluateCondition(andCondition)
|
||||
@ -96,12 +100,12 @@ function usePermissionsManager(initialPermissions: Permission[]): {
|
||||
(
|
||||
(p.role === user?.role &&
|
||||
(
|
||||
!item || !p.permissions || evaluatePermissions(p.permissions)
|
||||
!item || evaluatePermissions(p.permissions)
|
||||
)) ||
|
||||
(p.role == null &&
|
||||
(
|
||||
(layer?.public_edit_items || item?.layer?.public_edit_items) &&
|
||||
(!item || !p.permissions || evaluatePermissions(p.permissions))
|
||||
(!item || evaluatePermissions(p.permissions))
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
@ -31,7 +31,9 @@ export function ProfileForm({ userType }: { userType: string }) {
|
||||
markerIcon: "",
|
||||
offers: [] as Tag[],
|
||||
needs: [] as Tag[],
|
||||
relations: [] as Item[]
|
||||
relations: [] as Item[],
|
||||
start: "",
|
||||
end: ""
|
||||
});
|
||||
|
||||
const [updatePermission, setUpdatePermission] = useState<boolean>(false);
|
||||
@ -49,6 +51,9 @@ export function ProfileForm({ userType }: { userType: string }) {
|
||||
const getItemTags = useGetItemTags();
|
||||
const items = useItems();
|
||||
|
||||
const [urlParams, setUrlParams] = useState(new URLSearchParams(location.search));
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
item && hasUserPermission("items", "update", item) && setUpdatePermission(true);
|
||||
}, [item])
|
||||
@ -63,7 +68,7 @@ export function ProfileForm({ userType }: { userType: string }) {
|
||||
|
||||
!item && setItem({ id: crypto.randomUUID(), name: user ? user.first_name : "", text: "", layer: layer, new: true })
|
||||
|
||||
}, [items])
|
||||
}, [items])
|
||||
|
||||
useEffect(() => {
|
||||
const newColor = item.layer?.itemColorField && getValue(item, item.layer?.itemColorField)
|
||||
@ -105,7 +110,9 @@ export function ProfileForm({ userType }: { userType: string }) {
|
||||
markerIcon: item?.marker_icon ?? "",
|
||||
offers: offers,
|
||||
needs: needs,
|
||||
relations: relations
|
||||
relations: relations,
|
||||
start: item?.start ?? "",
|
||||
end: item?.end ?? ""
|
||||
});
|
||||
}, [item, tags, items]);
|
||||
|
||||
@ -115,8 +122,6 @@ export function ProfileForm({ userType }: { userType: string }) {
|
||||
setTemplate(item.layer?.itemType.template || userType);
|
||||
}, [userType, item])
|
||||
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MapOverlayPage backdrop className='tw-mx-4 tw-mt-4 tw-mb-4 tw-overflow-x-hidden tw-w-[calc(100%-32px)] md:tw-w-[calc(50%-32px)] tw-max-w-3xl !tw-left-auto tw-top-0 tw-bottom-0'>
|
||||
@ -130,15 +135,15 @@ export function ProfileForm({ userType }: { userType: string }) {
|
||||
)}
|
||||
|
||||
{template == "simple" &&
|
||||
<SimpleForm item={item} setState={setState}></SimpleForm>
|
||||
<SimpleForm state={state} setState={setState}></SimpleForm>
|
||||
}
|
||||
|
||||
{template == "tabs" &&
|
||||
<TabsForm loading={loading} item={item} state={state} setState={setState} updatePermission={updatePermission} linkItem={(id) => linkItem(id, item, updateItem)} unlinkItem={(id) => unlinkItem(id, item, updateItem)}></TabsForm>
|
||||
<TabsForm loading={loading} item={item} state={state} setState={setState} updatePermission={updatePermission} linkItem={(id) => linkItem(id, item, updateItem)} unlinkItem={(id) => unlinkItem(id, item, updateItem)} setUrlParams={setUrlParams}></TabsForm>
|
||||
}
|
||||
|
||||
<div className="tw-mt-4 tw-mb-4">
|
||||
<button className={loading ? " tw-loading tw-btn tw-float-right" : "tw-btn tw-float-right"} onClick={() => onUpdateItem(state, item, tags, addTag, setLoading, navigate, updateItem, addItem, user, params)} style={true ? { backgroundColor: `${item.layer?.itemColorField && getValue(item, item.layer?.itemColorField) ? getValue(item, item.layer?.itemColorField) : (getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item?.layer?.markerDefaultColor)}`, color: "#fff" } : { color: "#fff" }}>Update</button>
|
||||
<button className={loading ? " tw-loading tw-btn tw-float-right" : "tw-btn tw-float-right"} onClick={() => onUpdateItem(state, item, tags, addTag, setLoading, navigate, updateItem, addItem, user, urlParams)} style={true ? { backgroundColor: `${item.layer?.itemColorField && getValue(item, item.layer?.itemColorField) ? getValue(item, item.layer?.itemColorField) : (getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item?.layer?.markerDefaultColor)}`, color: "#fff" } : { color: "#fff" }}>Update</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -119,6 +119,9 @@ export function ProfileView({ userType }: { userType: string }) {
|
||||
setTemplate(item.layer?.itemType.template || userType);
|
||||
}, [userType, item])
|
||||
|
||||
const [urlParams, setUrlParams] = useState(new URLSearchParams(location.search));
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{item &&
|
||||
@ -137,7 +140,7 @@ export function ProfileView({ userType }: { userType: string }) {
|
||||
}
|
||||
|
||||
{template == "tabs" &&
|
||||
<TabsView item={item} loading={loading} offers={offers} needs={needs} relations={relations} updatePermission={updatePermission} linkItem={(id) => linkItem(id, item, updateItem)} unlinkItem={(id) => unlinkItem(id, item, updateItem)}/>
|
||||
<TabsView setUrlParams={setUrlParams} item={item} loading={loading} offers={offers} needs={needs} relations={relations} updatePermission={updatePermission} linkItem={(id) => linkItem(id, item, updateItem)} unlinkItem={(id) => unlinkItem(id, item, updateItem)}/>
|
||||
}
|
||||
</>
|
||||
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import { TextAreaInput } from "../../Input"
|
||||
import { TextAreaInput } from "../../Input";
|
||||
|
||||
export const SimpleForm = (item, setState) => {
|
||||
export const SimpleForm = ({ state, setState }) => {
|
||||
return (
|
||||
<TextAreaInput placeholder="About me ..." defaultValue={item?.text ? item.text : ""} updateFormValue={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
text: v
|
||||
}))} containerStyle='tw-mt-8 tw-h-full' inputStyle='tw-h-full' />
|
||||
)
|
||||
}
|
||||
<TextAreaInput
|
||||
placeholder="About me ..."
|
||||
defaultValue={state?.text || ""}
|
||||
updateFormValue={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
text: v
|
||||
}))}
|
||||
containerStyle='tw-mt-8 tw-h-full'
|
||||
inputStyle='tw-h-full'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,43 +1,71 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { TextAreaInput } from "../../Input"
|
||||
import { TextView } from "../../Map"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { TextAreaInput, TextInput } from "../../Input"
|
||||
import { PopupStartEndInput, TextView } from "../../Map"
|
||||
import { ActionButton } from "../Subcomponents/ActionsButton"
|
||||
import { LinkedItemsHeaderView } from "../Subcomponents/LinkedItemsHeaderView"
|
||||
import { TagsWidget } from "../Subcomponents/TagsWidget"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { useUpdateItem } from "../../Map/hooks/useItems"
|
||||
|
||||
export const TabsForm = ({ item, state, setState, updatePermission, linkItem, unlinkItem, loading }) => {
|
||||
export const TabsForm = ({ item, state, setState, updatePermission, linkItem, unlinkItem, loading, setUrlParams }) => {
|
||||
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
const navigate = useNavigate();
|
||||
const updateItem = useUpdateItem();
|
||||
|
||||
const updateActiveTab = (id: number) => {
|
||||
const updateActiveTab = useCallback((id: number) => {
|
||||
setActiveTab(id);
|
||||
|
||||
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
let urlTab = params.get("tab");
|
||||
if (!urlTab?.includes(id.toString()))
|
||||
params.set("tab", `${id ? id : ""}`)
|
||||
navigate(location.pathname+ "?" + params.toString());
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
params.set("tab", `${id}`);
|
||||
const newUrl = location.pathname + "?" + params.toString();
|
||||
window.history.pushState({}, '', newUrl);
|
||||
setUrlParams(params);
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
let params = new URLSearchParams(location.search);
|
||||
let urlTab = params.get("tab");
|
||||
urlTab ? setActiveTab(Number(urlTab)) : setActiveTab(1);
|
||||
}, [location])
|
||||
setActiveTab(urlTab ? Number(urlTab) : 1);
|
||||
}, [location.search]);
|
||||
|
||||
return (
|
||||
<div role="tablist" className="tw-tabs tw-tabs-lifted tw-mt-4">
|
||||
<div role="tablist" className="tw-tabs tw-tabs-lifted tw-mt-3">
|
||||
<input type="radio" name="my_tabs_2" role="tab" className={`tw-tab [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]`} aria-label="Info" checked={activeTab == 1 && true} onChange={() => updateActiveTab(1)} />
|
||||
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-border-[var(--fallback-bc,oklch(var(--bc)/0.2))] tw-rounded-box tw-h-[calc(100dvh-332px)] tw-min-h-56 tw-border-none">
|
||||
<TextAreaInput placeholder="About me ..." defaultValue={item?.text ? item.text : ""} updateFormValue={(v) => setState(prevState => ({
|
||||
<div className={`tw-flex tw-flex-col tw-h-full ${item.layer.itemType.show_start_end_input && "tw-pt-4"}`}>
|
||||
{item.layer.itemType.show_start_end_input &&
|
||||
<PopupStartEndInput
|
||||
item={item}
|
||||
showLabels={false}
|
||||
updateEndValue={(e) => setState(prevState => ({
|
||||
...prevState,
|
||||
text: v
|
||||
}))} containerStyle='tw-h-full' inputStyle='tw-h-full tw-border-t-0 tw-rounded-tl-none' />
|
||||
end: e
|
||||
}))}
|
||||
updateStartValue={(s) => setState(prevState => ({
|
||||
...prevState,
|
||||
start: s
|
||||
}))}></PopupStartEndInput>
|
||||
}
|
||||
|
||||
<TextAreaInput placeholder="about ..." defaultValue={item?.text ? item.text : ""} updateFormValue={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
text: v
|
||||
}))} containerStyle='tw-grow' inputStyle={`tw-h-full ${!item.layer.itemType.show_start_end_input && "tw-border-t-0 tw-rounded-tl-none"}`} />
|
||||
<div>
|
||||
<TextAreaInput
|
||||
placeholder="contact info ..."
|
||||
defaultValue={state.contact || ""}
|
||||
updateFormValue={(c) => setState(prevState => ({
|
||||
...prevState,
|
||||
contact: c
|
||||
}))}
|
||||
inputStyle="tw-h-24"
|
||||
containerStyle="tw-pt-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{item.layer?.itemType.offers_and_needs &&
|
||||
<>
|
||||
@ -50,7 +78,7 @@ export const TabsForm = ({ item, state, setState, updatePermission, linkItem, un
|
||||
offers: 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' />
|
||||
</div>
|
||||
<div className='tw-w-full tw-h-[calc(50%-0.75em)] '>
|
||||
<div className='tw-w-full tw-h-[calc(50%-1.5em)]'>
|
||||
<TagsWidget defaultTags={state.needs} onUpdate={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
needs: v
|
||||
@ -63,7 +91,7 @@ export const TabsForm = ({ item, state, setState, updatePermission, linkItem, un
|
||||
{item.layer?.itemType.relations &&
|
||||
<>
|
||||
<input type="radio" name="my_tabs_2" role="tab" className="tw-tab [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]" aria-label="Relations" checked={activeTab == 7 && true} onChange={() => updateActiveTab(7)} />
|
||||
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-rounded-box tw-h-[calc(100dvh-340px)] tw-overflow-y-auto tw-pt-4 tw-pb-1 -tw-mx-4 tw-overflow-x-hidden fade">
|
||||
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-rounded-box tw-h-[calc(100dvh-332px)] tw-overflow-y-auto tw-pt-4 tw-pb-1 -tw-mx-4 tw-overflow-x-hidden fade">
|
||||
<div className='tw-h-full'>
|
||||
<div className='tw-grid tw-grid-cols-1 sm:tw-grid-cols-2 md:tw-grid-cols-1 lg:tw-grid-cols-1 xl:tw-grid-cols-1 2xl:tw-grid-cols-2 tw-mb-4'>
|
||||
{state.relations && state.relations.map(i =>
|
||||
|
||||
@ -2,15 +2,15 @@ import { StartEndView, TextView } from '../../Map'
|
||||
import { TagView } from '../../Templates/TagView'
|
||||
import { LinkedItemsHeaderView } from '../Subcomponents/LinkedItemsHeaderView'
|
||||
import { ActionButton } from '../Subcomponents/ActionsButton'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useAddFilterTag } from '../../Map/hooks/useFilter'
|
||||
import { Item, Tag } from 'utopia-ui/dist/types'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
export const TabsView = ({ item, offers, needs, relations, updatePermission, loading, linkItem, unlinkItem }: { item: Item, offers: Array<Tag>, needs: Array<Tag>, relations: Array<Item>, updatePermission: boolean, loading: boolean, linkItem: (id: string) => Promise<void>, unlinkItem: (id: string) => Promise<void> }) => {
|
||||
export const TabsView = ({ item, offers, needs, relations, updatePermission, loading, linkItem, unlinkItem, setUrlParams }: { item: Item, offers: Array<Tag>, needs: Array<Tag>, relations: Array<Item>, updatePermission: boolean, loading: boolean, linkItem: (id: string) => Promise<void>, unlinkItem: (id: string) => Promise<void> , setUrlParams: any}) => {
|
||||
|
||||
const addFilterTag = useAddFilterTag();
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
const [activeTab, setActiveTab] = useState<number>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [addItemPopupType, setAddItemPopupType] = useState<string>("");
|
||||
@ -25,21 +25,22 @@ export const TabsView = ({ item, offers, needs, relations, updatePermission, loa
|
||||
|
||||
const tabRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
const updateActiveTab = (id: number) => {
|
||||
const updateActiveTab = useCallback((id: number) => {
|
||||
setActiveTab(id);
|
||||
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
let urlTab = params.get("tab");
|
||||
if (!urlTab?.includes(id.toString()))
|
||||
params.set("tab", `${id ? id : ""}`)
|
||||
navigate(location.pathname+ "?" + params.toString());
|
||||
}
|
||||
params.set("tab", `${id}`);
|
||||
const newUrl = location.pathname + "?" + params.toString();
|
||||
window.history.pushState({}, '', newUrl);
|
||||
setUrlParams(params);
|
||||
}, [location.pathname]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let params = new URLSearchParams(location.search);
|
||||
let urlTab = params.get("tab");
|
||||
urlTab ? setActiveTab(Number(urlTab)) : setActiveTab(1);
|
||||
}, [location])
|
||||
setActiveTab(urlTab ? Number(urlTab) : 1);
|
||||
}, [location.search]);
|
||||
|
||||
const attestations = [{
|
||||
from: "Timo",
|
||||
@ -104,6 +105,8 @@ export const TabsView = ({ item, offers, needs, relations, updatePermission, loa
|
||||
<div className='tw-max-w-xs'><StartEndView item={item}></StartEndView></div>
|
||||
}
|
||||
<TextView item={item} />
|
||||
<div className='tw-h-4'></div>
|
||||
<TextView item={item} itemTextField='contact'/>
|
||||
</div>
|
||||
{item.layer?.itemType.questlog &&
|
||||
<>
|
||||
|
||||
@ -4,6 +4,8 @@ import { hashTagRegex } from '../../Utils/HashTagRegex';
|
||||
import { randomColor } from '../../Utils/RandomColor';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export const submitNewItem = async (evt: any, type: string, item, user, setLoading, tags, addTag, addItem, linkItem, resetFilterTags, layers, addItemPopupType, setAddItemPopupType) => {
|
||||
evt.preventDefault();
|
||||
const formItem: Item = {} as Item;
|
||||
@ -103,7 +105,7 @@ export const onUpdateItem = async (state, item, tags, addTag, setLoading, naviga
|
||||
|
||||
let offer_updates: Array<any> = [];
|
||||
//check for new offers
|
||||
state.offers?.map(o => {
|
||||
await state.offers?.map(o => {
|
||||
const existingOffer = item?.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 })
|
||||
@ -112,28 +114,29 @@ export const onUpdateItem = async (state, item, tags, addTag, setLoading, naviga
|
||||
|
||||
let needs_updates: Array<any> = [];
|
||||
|
||||
state.needs?.map(n => {
|
||||
await state.needs?.map(n => {
|
||||
const existingNeed = item?.needs?.find(t => t.tags_id === n.id)
|
||||
existingNeed && needs_updates.push(existingNeed.id)
|
||||
!existingNeed && needs_updates.push({ items_id: item?.id, tags_id: n.id })
|
||||
!existingNeed && !tags.some(t => t.id === n.id) && addTag({ ...n, offer_or_need: true })
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// update profile item in current state
|
||||
changedItem = {
|
||||
id: state.id,
|
||||
group_type: state.groupType,
|
||||
status: state.status,
|
||||
name: state.name,
|
||||
subname: state.subname,
|
||||
text: state.text,
|
||||
color: state.color,
|
||||
...state.subname && {subname: state.subname},
|
||||
...state.text && {text: state.text},
|
||||
...state.color && {color: state.color},
|
||||
position: item.position,
|
||||
contact: state.contact,
|
||||
telephone: state.telephone,
|
||||
...state.groupType && {group_type: state.groupType},
|
||||
...state.status && {status: state.status},
|
||||
...state.contact && {contact: state.contact},
|
||||
...state.telephone && {telephone: state.telephone},
|
||||
...state.end && {end: state.end},
|
||||
...state.start && {start: state.start},
|
||||
...state.markerIcon && { markerIcon: state.markerIcon },
|
||||
next_appointment: state.nextAppointment,
|
||||
...state.nextAppointment && {next_appointment: state.nextAppointment},
|
||||
...state.image.length > 10 && { image: state.image },
|
||||
...state.offers.length > 0 && { offers: offer_updates },
|
||||
...state.needs.length > 0 && { needs: needs_updates }
|
||||
@ -142,24 +145,26 @@ export const onUpdateItem = async (state, item, tags, addTag, setLoading, naviga
|
||||
let offers_state: Array<any> = [];
|
||||
let needs_state: Array<any> = [];
|
||||
|
||||
await state.offers.map(o => {
|
||||
state.offers.map(o => {
|
||||
offers_state.push({ items_id: item?.id, tags_id: o.id })
|
||||
});
|
||||
|
||||
await state.needs.map(n => {
|
||||
state.needs.map(n => {
|
||||
needs_state.push({ items_id: item?.id, tags_id: n.id })
|
||||
});
|
||||
|
||||
changedItem = { ...changedItem, offers: offers_state, needs: needs_state };
|
||||
|
||||
setLoading(true);
|
||||
|
||||
state.text.toLocaleLowerCase().match(hashTagRegex)?.map(tag => {
|
||||
await state.text.toLocaleLowerCase().match(hashTagRegex)?.map(tag => {
|
||||
if (!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) {
|
||||
addTag({ id: crypto.randomUUID(), name: encodeTag(tag.slice(1).toLocaleLowerCase()), color: randomColor() })
|
||||
}
|
||||
});
|
||||
|
||||
setLoading(true);
|
||||
//take care that addTag request comes before item request
|
||||
await sleep(200);
|
||||
|
||||
if (!item.new) {
|
||||
item?.layer?.api?.updateItem && toast.promise(
|
||||
@ -181,6 +186,7 @@ export const onUpdateItem = async (state, item, tags, addTag, setLoading, naviga
|
||||
|
||||
}
|
||||
else {
|
||||
item.new = false;
|
||||
item.layer?.api?.createItem && toast.promise(
|
||||
item.layer?.api?.createItem(changedItem),
|
||||
{
|
||||
|
||||
@ -22,12 +22,6 @@ import { TagsControl } from '../Map/Subcomponents/Controls/TagsControl';
|
||||
import { useFilterTags } from '../Map/hooks/useFilter';
|
||||
|
||||
|
||||
type breadcrumb = {
|
||||
name: string,
|
||||
path: string
|
||||
}
|
||||
|
||||
|
||||
export const OverlayItemsIndexPage = ({ url, layerName, parameterField, plusButton = true }: { layerName: string, url: string, parameterField: string, plusButton?: boolean }) => {
|
||||
|
||||
|
||||
@ -45,6 +39,11 @@ export const OverlayItemsIndexPage = ({ url, layerName, parameterField, plusButt
|
||||
scroll();
|
||||
}, [addItemPopupType])
|
||||
|
||||
useEffect(() => {
|
||||
setAddItemPopupType("");
|
||||
}, [layerName])
|
||||
|
||||
|
||||
const tags = useTags();
|
||||
const addTag = useAddTag();
|
||||
const { user } = useAuth();
|
||||
@ -120,42 +119,44 @@ export const OverlayItemsIndexPage = ({ url, layerName, parameterField, plusButt
|
||||
<div className='tw-columns-1 md:tw-columns-2 lg:tw-columns-3 2xl:tw-columns-4 tw-gap-6 tw-pt-4'>
|
||||
{
|
||||
items?.filter(i => i.layer?.name === layerName).
|
||||
filter(item =>
|
||||
filterTags.length == 0 ? item : filterTags.every(tag => getItemTags(item).some(filterTag => filterTag.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase())))?.
|
||||
sort((a, b) => {
|
||||
// Convert date_created to milliseconds, handle undefined by converting to lowest possible date (0 milliseconds)
|
||||
const dateA = a.date_updated ? new Date(a.date_updated).getTime() : a.date_created ? new Date(a.date_created).getTime() : 0;
|
||||
const dateB = b.date_updated ? new Date(b.date_updated).getTime() : b.date_created ? new Date(b.date_created).getTime() : 0;
|
||||
return dateB - dateA; // Subtracts milliseconds which are numbers
|
||||
})?.
|
||||
map((i, k) => (
|
||||
<div key={k} className="tw-break-inside-avoid tw-mb-6">
|
||||
<ItemCard i={i} loading={loading} url={url} parameterField={parameterField} deleteCallback={() => deleteItem(i)} />
|
||||
</div>
|
||||
))
|
||||
filter(item =>
|
||||
filterTags.length == 0 ? item : filterTags.some(tag => getItemTags(item).some(filterTag => filterTag.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase())))?.
|
||||
sort((a, b) => {
|
||||
// Convert date_created to milliseconds, handle undefined by converting to lowest possible date (0 milliseconds)
|
||||
const dateA = a.date_updated ? new Date(a.date_updated).getTime() : a.date_created ? new Date(a.date_created).getTime() : 0;
|
||||
const dateB = b.date_updated ? new Date(b.date_updated).getTime() : b.date_created ? new Date(b.date_created).getTime() : 0;
|
||||
return dateB - dateA; // Subtracts milliseconds which are numbers
|
||||
})?.
|
||||
map((i, k) => (
|
||||
<div key={k} className="tw-break-inside-avoid tw-mb-6">
|
||||
<ItemCard i={i} loading={loading} url={url} parameterField={parameterField} deleteCallback={() => deleteItem(i)} />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{addItemPopupType == "place" && (
|
||||
<form ref={tabRef} autoComplete='off' onSubmit={e => submitNewItem(e)}>
|
||||
<div className='tw-cursor-pointer tw-card tw-border-[1px] tw-border-base-300 tw-card-body tw-shadow-xl tw-bg-base-100 tw-text-base-content tw-p-6 tw-mb-10'>
|
||||
<label className="tw-btn tw-btn-sm tw-rounded-2xl tw-btn-circle tw-btn-ghost hover:tw-bg-transparent tw-absolute tw-right-0 tw-top-0 tw-text-gray-600" onClick={() => setAddItemPopupType("")}>
|
||||
<p className='tw-text-center'>✕</p>
|
||||
</label>
|
||||
<TextInput type="text" placeholder="Name" dataField="name" defaultValue={""} inputStyle='' />
|
||||
{layer?.itemType.show_start_end_input && <PopupStartEndInput />}
|
||||
<TextAreaInput placeholder="Text" dataField="text" defaultValue={""} inputStyle='tw-h-40 tw-mt-5' />
|
||||
<div className='tw-flex tw-justify-center'>
|
||||
<button className={loading ? 'tw-btn tw-btn-disabled tw-mt-5 tw-place-self-center' : 'tw-btn tw-mt-5 tw-place-self-center'} type='submit'>
|
||||
{loading ? <span className="tw-loading tw-loading-spinner"></span> : 'Save'}
|
||||
</button>
|
||||
{addItemPopupType == "place" && (
|
||||
<form ref={tabRef} autoComplete='off' onSubmit={e => submitNewItem(e)}>
|
||||
<div className='tw-cursor-pointer tw-break-inside-avoid tw-card tw-border-[1px] tw-border-base-300 tw-card-body tw-shadow-xl tw-bg-base-100 tw-text-base-content tw-p-6 tw-mb-10'>
|
||||
<label className="tw-btn tw-btn-sm tw-rounded-2xl tw-btn-circle tw-btn-ghost hover:tw-bg-transparent tw-absolute tw-right-0 tw-top-0 tw-text-gray-600" onClick={() => setAddItemPopupType("")}>
|
||||
<p className='tw-text-center'>✕</p>
|
||||
</label>
|
||||
<TextInput type="text" placeholder="Name" dataField="name" defaultValue={""} inputStyle='' />
|
||||
{layer?.itemType.show_start_end_input && <PopupStartEndInput />}
|
||||
<TextAreaInput placeholder="Text" dataField="text" defaultValue={""} inputStyle='tw-h-40 tw-mt-5' />
|
||||
<div className='tw-flex tw-justify-center'>
|
||||
<button className={loading ? 'tw-btn tw-btn-disabled tw-mt-5 tw-place-self-center' : 'tw-btn tw-mt-5 tw-place-self-center'} type='submit'>
|
||||
{loading ? <span className="tw-loading tw-loading-spinner"></span> : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</MapOverlayPage>
|
||||
|
||||
{plusButton && <PlusButton layer={layer} triggerAction={() => { setAddItemPopupType("place"); scroll(); }} color={'#777'} collection='items' />}
|
||||
</>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ export const TagView = ({tag, heighlight, onClick} : {tag: Tag, heighlight?: boo
|
||||
return (
|
||||
// Use your imagination to render suggestions.
|
||||
|
||||
<div key={tag.name} onClick={onClick} className={`tw-rounded-2xl tw-text-white tw-p-2 tw-px-4 tw-shadow-xl tw-card tw-mt-3 tw-mr-4 tw-cursor-pointer tw-w-fit ${heighlight && 'tw-border-primary tw-shadow-te-primary'}`} style={{ backgroundColor: tag.color ? tag.color : "#666" }}>
|
||||
<div key={tag.name} onClick={onClick} className={`tw-rounded-2xl tw-text-white tw-p-2 tw-px-4 tw-shadow-xl tw-card tw-mt-3 tw-mr-4 tw-cursor-pointer tw-w-fit ${heighlight && 'tw-border-4 tw-border-base-200 tw-shadow-lg'}`} style={{ backgroundColor: tag.color ? tag.color : "#666" }}>
|
||||
<div className="tw-card-actions tw-justify-end">
|
||||
</div><b>{decodeTag(tag.name)}</b>
|
||||
</div>
|
||||
|
||||
@ -36,6 +36,8 @@ const addIcon = (icon: string) => {
|
||||
return '<svg class="liebevoll-jetzt-icon" fill="#fff" height="1.5em" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 42 42"><path fill="#e52220" d="M32.554 33.551c0-.2.2-.5 0-.7-.2-.2-.1 0-.3-.1-.5-.2-1.2-.1-1.7.2-.7.3-1.7 1.5-1.3 2.4.2.3.5.6.8.8.5.3.8-.1 1.2-.4.4-.3.7-.5.9-1 .1-.1.8-1.2.4-1.2z"/> <path fill="#fff" d="M14.426 41.57c-1.512-.808-2.234-2.322-2.403-5.038-.104-1.671.136-3.65.844-6.962.497-2.327.545-2.56.81-3.95.236-1.234 3.381-10.767 4.237-12.842.688-1.667 3.022-6.084 3.379-6.394.158-.137.708-.948 1.22-1.802C24.243 1.704 25.935 0 27.065 0c1.134 0 2.303 1.066 2.763 2.521.274.864.133 2.528-.34 4.032-.551 1.754-2.928 6.42-3.994 7.84a37.025 37.025 0 0 0-1.753 2.622c-1.173 1.957-2.233 3.525-4.002 5.92-.831 1.126-1.628 2.306-1.77 2.621-.422.932-1.792 6.558-1.836 7.536-.023.496-.095 1.73-.16 2.742-.175 2.714.265 3.687 1.54 3.407.737-.162 1.569-.967 3.197-3.092 2.302-3.005 5.204-7.528 6.524-10.17.622-1.243 1.228-2.06 1.528-2.06.394 0 .46 1.003.131 1.975-.413 1.219-4.57 8.126-6.186 10.277-1.727 2.3-4.191 4.785-5.233 5.277-1.194.564-2.15.602-3.048.123zm6.76-23.631c2.714-4.414 5.248-9.115 5.42-10.052.027-.148.172-.553.322-.9.397-.915.872-2.532.997-3.386.077-.532.04-.833-.132-1.065-.232-.315-.259-.305-.918.33-1.692 1.632-4.116 7.08-6.488 14.581-.484 1.532-.88 2.855-.881 2.94-.002.255.192-.026 1.68-2.449z"/></svg>'
|
||||
case "group":
|
||||
return '<svg class="group-icon" stroke="#fff" fill="#fff" stroke-width="0" viewBox="0 0 20 20" aria-hidden="true" height="1.6em" width="1.6em" xmlns="http://www.w3.org/2000/svg"><path d="M10 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM6 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM1.49 15.326a.78.78 0 0 1-.358-.442 3 3 0 0 1 4.308-3.516 6.484 6.484 0 0 0-1.905 3.959c-.023.222-.014.442.025.654a4.97 4.97 0 0 1-2.07-.655ZM16.44 15.98a4.97 4.97 0 0 0 2.07-.654.78.78 0 0 0 .357-.442 3 3 0 0 0-4.308-3.517 6.484 6.484 0 0 1 1.907 3.96 2.32 2.32 0 0 1-.026.654ZM18 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM5.304 16.19a.844.844 0 0 1-.277-.71 5 5 0 0 1 9.947 0 .843.843 0 0 1-.277.71A6.975 6.975 0 0 1 10 18a6.974 6.974 0 0 1-4.696-1.81Z"></path></svg>'
|
||||
case "puzzle":
|
||||
return '<svg class="group-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#fff" width="1.6em" height="1.6em"><path d="M11.25 5.337c0-.355-.186-.676-.401-.959a1.647 1.647 0 0 1-.349-1.003c0-1.036 1.007-1.875 2.25-1.875S15 2.34 15 3.375c0 .369-.128.713-.349 1.003-.215.283-.401.604-.401.959 0 .332.278.598.61.578 1.91-.114 3.79-.342 5.632-.676a.75.75 0 0 1 .878.645 49.17 49.17 0 0 1 .376 5.452.657.657 0 0 1-.66.664c-.354 0-.675-.186-.958-.401a1.647 1.647 0 0 0-1.003-.349c-1.035 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401.31 0 .557.262.534.571a48.774 48.774 0 0 1-.595 4.845.75.75 0 0 1-.61.61c-1.82.317-3.673.533-5.555.642a.58.58 0 0 1-.611-.581c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.035-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959a.641.641 0 0 1-.658.643 49.118 49.118 0 0 1-4.708-.36.75.75 0 0 1-.645-.878c.293-1.614.504-3.257.629-4.924A.53.53 0 0 0 5.337 15c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.036 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.369 0 .713.128 1.003.349.283.215.604.401.959.401a.656.656 0 0 0 .659-.663 47.703 47.703 0 0 0-.31-4.82.75.75 0 0 1 .83-.832c1.343.155 2.703.254 4.077.294a.64.64 0 0 0 .657-.642Z" /></svg>'
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user