mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
optimized search, filter and items index pages
This commit is contained in:
parent
f1c272126c
commit
22ab0e3acc
@ -23,15 +23,15 @@ export function AppShell({ appName, children, assetsApi }: { appName: string, ch
|
||||
|
||||
|
||||
return (
|
||||
<PermissionsProvider initialPermissions={[]}>
|
||||
<TagsProvider initialTags={[]}>
|
||||
<LayersProvider initialLayers={[]}>
|
||||
<FilterProvider initialTags={[]}>
|
||||
<ItemsProvider initialItems={[]}>
|
||||
<SelectPositionProvider>
|
||||
<LeafletRefsProvider initialLeafletRefs={{}}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<BrowserRouter>
|
||||
<PermissionsProvider initialPermissions={[]}>
|
||||
<TagsProvider initialTags={[]}>
|
||||
<LayersProvider initialLayers={[]}>
|
||||
<FilterProvider initialTags={[]}>
|
||||
<ItemsProvider initialItems={[]}>
|
||||
<SelectPositionProvider>
|
||||
<LeafletRefsProvider initialLeafletRefs={{}}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AssetsProvider>
|
||||
<ClusterRefProvider>
|
||||
<SetAssetsApi assetsApi={assetsApi}></SetAssetsApi>
|
||||
@ -55,15 +55,15 @@ export function AppShell({ appName, children, assetsApi }: { appName: string, ch
|
||||
</QuestsProvider>
|
||||
</ClusterRefProvider>
|
||||
</AssetsProvider>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</LeafletRefsProvider>
|
||||
</SelectPositionProvider>
|
||||
</ItemsProvider>
|
||||
</FilterProvider>
|
||||
</LayersProvider>
|
||||
</TagsProvider>
|
||||
</PermissionsProvider>
|
||||
</QueryClientProvider>
|
||||
</LeafletRefsProvider>
|
||||
</SelectPositionProvider>
|
||||
</ItemsProvider>
|
||||
</FilterProvider>
|
||||
</LayersProvider>
|
||||
</TagsProvider>
|
||||
</PermissionsProvider>
|
||||
</BrowserRouter>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import * as React from 'react'
|
||||
|
||||
|
||||
|
||||
export const Control = ({ position, children, zIndex }: { position: "topLeft" | "topRight" | "bottomLeft" | "bottomRight", children: React.ReactNode, zIndex: string }) => {
|
||||
export const Control = ({ position, children, zIndex, absolute }: { position: "topLeft" | "topRight" | "bottomLeft" | "bottomRight", children: React.ReactNode, zIndex: string, absolute: boolean }) => {
|
||||
|
||||
|
||||
const controlContainerRef = React.createRef<HTMLDivElement>()
|
||||
@ -17,7 +17,7 @@ export const Control = ({ position, children, zIndex }: { position: "topLeft" |
|
||||
}, [controlContainerRef])
|
||||
|
||||
return (
|
||||
<div ref={controlContainerRef} style={{zIndex: zIndex}} className={`tw-absolute tw-z-[999] tw-flex-col ${position === 'topLeft' && "tw-top-4 tw-left-4"} ${position === 'bottomLeft' && "tw-bottom-4 tw-left-4"} ${position === 'topRight' && "tw-bottom-4 tw-right-4"} ${position === 'bottomRight' && "tw-bottom-4 tw-right-4"}`}>
|
||||
<div ref={controlContainerRef} style={{zIndex: zIndex}} className={`${absolute && 'tw-absolute'} tw-z-[999] tw-flex-col ${position === 'topLeft' && "tw-top-4 tw-left-4"} ${position === 'bottomLeft' && "tw-bottom-4 tw-left-4"} ${position === 'topRight' && "tw-bottom-4 tw-right-4"} ${position === 'bottomRight' && "tw-bottom-4 tw-right-4"}`}>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -88,7 +88,7 @@ export const SearchControl = () => {
|
||||
useEffect(() => {
|
||||
let params = new URLSearchParams(location.search);
|
||||
let embedded = params.get("embedded");
|
||||
embedded != "true" && setEmbedded(false)
|
||||
embedded != "true" && setEmbedded(false)
|
||||
}, [location]);
|
||||
|
||||
|
||||
@ -97,6 +97,7 @@ export const SearchControl = () => {
|
||||
<div className='tw-w-[calc(100vw-2rem)] tw-max-w-[22rem] '>
|
||||
<div className='flex tw-flex-row'>
|
||||
{embedded && <SidebarControl />}
|
||||
<div className='tw-relative'>
|
||||
<input type="text" placeholder="search ..." autoComplete="off" value={value} className="tw-input tw-input-bordered tw-grow tw-shadow-xl tw-rounded-lg"
|
||||
ref={searchInput}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
@ -105,9 +106,10 @@ export const SearchControl = () => {
|
||||
if (windowDimensions.width < 500) map.closePopup();
|
||||
}}
|
||||
onBlur={() => hide()} />
|
||||
{value.length > 0 && <button className="tw-btn tw-btn-sm tw-btn-circle tw-absolute tw-right-2 tw-top-2" onClick={() => setValue("")}>✕</button>}
|
||||
</div>
|
||||
<LocateControl />
|
||||
</div>
|
||||
{value.length > 0 && <button className="tw-btn tw-btn-sm tw-btn-circle tw-absolute tw-right-16 tw-top-2" onClick={() => setValue("")}>✕</button>}
|
||||
{hideSuggestions || Array.from(geoResults).length == 0 && itemsResults.length == 0 && tagsResults.length == 0 && !isGeoCoordinate(value) || value.length == 0 ? "" :
|
||||
<div className='tw-card tw-card-body tw-bg-base-100 tw-p-4 tw-mt-2 tw-shadow-xl tw-overflow-y-auto tw-max-h-[calc(100dvh-152px)]'>
|
||||
{tagsResults.length > 0 &&
|
||||
@ -115,8 +117,6 @@ export const SearchControl = () => {
|
||||
{tagsResults.slice(0, 3).map(tag => (
|
||||
<div key={tag.name} className='tw-rounded-2xl tw-text-white tw-p-1 tw-px-4 tw-shadow-md tw-card tw-mr-2 tw-mb-2 tw-cursor-pointer' style={{ backgroundColor: tag.color }} onClick={() => {
|
||||
addFilterTag(tag)
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
window.history.pushState({}, "", "/" + `${params ? `?${params}` : ""}`);
|
||||
}}>
|
||||
<b>#{decodeTag(tag.name)}</b>
|
||||
</div>
|
||||
|
||||
@ -3,7 +3,7 @@ import { useEffect } from 'react';
|
||||
import { ItemsApi, Tag } from '../../types';
|
||||
import { useSetTagData, useSetTagApi, useTags } from './hooks/useTags'
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAddFilterTag, useResetFilterTags } from './hooks/useFilter';
|
||||
import { useAddFilterTag, useFilterTags, useResetFilterTags } from './hooks/useFilter';
|
||||
|
||||
export function Tags({data, api} : {data?: Tag[], api?: ItemsApi<Tag>}) {
|
||||
const setTagData = useSetTagData();
|
||||
@ -19,16 +19,18 @@ const location = useLocation();
|
||||
const addFilterTag = useAddFilterTag();
|
||||
const resetFilterTags = useResetFilterTags();
|
||||
const tags = useTags();
|
||||
const filterTags = useFilterTags()
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let params = new URLSearchParams(location.search);
|
||||
let urlTags = params.get("tags");
|
||||
resetFilterTags()
|
||||
urlTags?.split(",").map(urlTag => {
|
||||
let urlTags = params.get("tags")?.split(",");
|
||||
if(urlTags?.some(ut => !filterTags.find(ft => ut.toLocaleLowerCase() === ft.name.toLocaleLowerCase()))||filterTags?.some(ft => !urlTags?.find(ut => ut.toLocaleLowerCase() === ft.name.toLocaleLowerCase())))
|
||||
{resetFilterTags()
|
||||
urlTags?.map(urlTag => {
|
||||
const tag = tags.find(t => t.name.toLocaleLowerCase() === urlTag.toLocaleLowerCase())
|
||||
tag && addFilterTag(tag)
|
||||
});
|
||||
});}
|
||||
|
||||
}, [location, tags]);
|
||||
|
||||
|
||||
@ -78,11 +78,11 @@ function UtopiaMap({
|
||||
<div className={`tw-h-full ${(selectNewItemPosition != null ? "crosshair-cursor-enabled" : undefined)}`}>
|
||||
<MapContainer ref={mapDivRef} style={{ height: height, width: width }} center={new LatLng(center[0], center[1])} zoom={zoom} zoomControl={false} maxZoom={19}>
|
||||
<Outlet></Outlet>
|
||||
<Control position='topLeft' zIndex="1000">
|
||||
<Control position='topLeft' zIndex="1000" absolute>
|
||||
<SearchControl />
|
||||
<TagsControl />
|
||||
</Control>
|
||||
<Control position='bottomLeft' zIndex="999">
|
||||
<Control position='bottomLeft' zIndex="999" absolute>
|
||||
<QuestControl></QuestControl>
|
||||
<LayerControl></LayerControl>
|
||||
</Control>
|
||||
|
||||
@ -2,7 +2,7 @@ import { useCallback, useReducer, createContext, useContext } from "react";
|
||||
import * as React from "react";
|
||||
import { LayerProps, Tag } from "../../../types";
|
||||
import { useLayers } from "./useLayers";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
type ActionType =
|
||||
| { type: "ADD_TAG"; tag: Tag }
|
||||
@ -63,6 +63,7 @@ function useFilterManager(initialTags: Tag[]): {
|
||||
}, initialTags);
|
||||
|
||||
const initialLayers = useLayers()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [visibleLayers, dispatchLayers] = useReducer((state: LayerProps[], action: ActionType) => {
|
||||
switch (action.type) {
|
||||
@ -95,7 +96,8 @@ function useFilterManager(initialTags: Tag[]): {
|
||||
let urlTags = params.get("tags");
|
||||
if(!urlTags?.includes(tag.name))
|
||||
params.set("tags", `${urlTags ? urlTags : ""}${urlTags? ',' : ''}${tag.name}`)
|
||||
window.history.pushState('','', "?" +params.toString());
|
||||
navigate(location.pathname + `${params ? `?${params}` : ""}`);
|
||||
|
||||
|
||||
dispatchTags({
|
||||
type: "ADD_TAG",
|
||||
@ -117,11 +119,11 @@ function useFilterManager(initialTags: Tag[]): {
|
||||
});
|
||||
if(newUrlTags !== "") {
|
||||
params.set("tags", `${newUrlTags}`)
|
||||
window.history.pushState('','', "?" +params.toString());
|
||||
navigate(location.pathname + `${params ? `?${params}` : ""}`);
|
||||
}
|
||||
else {
|
||||
|
||||
window.history.pushState('','', window.location.pathname);
|
||||
params.delete("tags");
|
||||
navigate(location.pathname + `${params ? `?${params}` : ""}`);
|
||||
}
|
||||
|
||||
dispatchTags({
|
||||
|
||||
@ -9,7 +9,10 @@ export const ItemCard = ({i,loading, url, parameterField, deleteCallback}:{i:Ite
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<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-4 tw-mb-4 tw-h-fit' onClick={() => navigate(url + getValue(i, parameterField))}>
|
||||
<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-4 tw-mb-4 tw-h-fit' onClick={() => {
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
navigate(url + getValue(i, parameterField) + `${params ? `?${params}` : ""}`)
|
||||
}}>
|
||||
<HeaderView loading={loading} item={i} api={i.layer?.api} itemAvatarField={i.layer?.itemAvatarField} itemNameField={i.layer?.itemNameField} itemSubnameField={i.layer?.itemSubnameField} editCallback={() => navigate("/edit-item/" + i.id)} deleteCallback={() => deleteCallback(i)}></HeaderView>
|
||||
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
|
||||
{i.layer?.itemType.show_start_end &&
|
||||
|
||||
@ -5,7 +5,7 @@ import { getValue } from '../../Utils/GetValue';
|
||||
import { PopupStartEndInput, StartEndView, TextView } from '../Map';
|
||||
import { PlusButton } from '../Profile/PlusButton';
|
||||
import { TextInput, TextAreaInput } from '../Input';
|
||||
import { useAddTag, useTags } from '../Map/hooks/useTags';
|
||||
import { useAddTag, useGetItemTags, useTags } from '../Map/hooks/useTags';
|
||||
import { toast } from 'react-toastify';
|
||||
import { hashTagRegex } from '../../Utils/HashTagRegex';
|
||||
import { randomColor } from '../../Utils/RandomColor';
|
||||
@ -16,6 +16,10 @@ import { MapOverlayPage } from './MapOverlayPage';
|
||||
import { useAddItem, useItems, useRemoveItem } from '../Map/hooks/useItems';
|
||||
import { DateUserInfo } from './DateUserInfo';
|
||||
import { ItemCard } from './ItemCard';
|
||||
import { Control } from '../Map/Subcomponents/Controls/Control';
|
||||
import { SearchControl } from '../Map/Subcomponents/Controls/SearchControl';
|
||||
import { TagsControl } from '../Map/Subcomponents/Controls/TagsControl';
|
||||
import { useFilterTags } from '../Map/hooks/useFilter';
|
||||
|
||||
|
||||
type breadcrumb = {
|
||||
@ -24,7 +28,7 @@ type breadcrumb = {
|
||||
}
|
||||
|
||||
|
||||
export const OverlayItemsIndexPage = ({ url, layerName, parameterField, breadcrumbs, plusButton = true, children }: { layerName: string, url: string, parameterField: string, breadcrumbs: Array<breadcrumb>, plusButton?: boolean, children?: ReactNode }) => {
|
||||
export const OverlayItemsIndexPage = ({ url, layerName, parameterField, plusButton = true }: { layerName: string, url: string, parameterField: string, plusButton?: boolean }) => {
|
||||
|
||||
|
||||
|
||||
@ -41,8 +45,6 @@ export const OverlayItemsIndexPage = ({ url, layerName, parameterField, breadcru
|
||||
scroll();
|
||||
}, [addItemPopupType])
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const tags = useTags();
|
||||
const addTag = useAddTag();
|
||||
const { user } = useAuth();
|
||||
@ -51,15 +53,11 @@ export const OverlayItemsIndexPage = ({ url, layerName, parameterField, breadcru
|
||||
const removeItem = useRemoveItem();
|
||||
const layers = useLayers();
|
||||
|
||||
useEffect(() => {
|
||||
console.log(items);
|
||||
|
||||
|
||||
}, [items])
|
||||
const filterTags = useFilterTags();
|
||||
const getItemTags = useGetItemTags();
|
||||
|
||||
const layer = layers.find(l => l.name == layerName);
|
||||
|
||||
|
||||
const submitNewItem = async (evt: any) => {
|
||||
evt.preventDefault();
|
||||
const formItem: Item = {} as Item;
|
||||
@ -107,58 +105,59 @@ export const OverlayItemsIndexPage = ({ url, layerName, parameterField, breadcru
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
|
||||
<MapOverlayPage className='tw-rounded-none tw-overflow-y-auto tw-bg-base-200'>
|
||||
<div className='tw-h-fit'>
|
||||
{breadcrumbs &&
|
||||
<div className="tw-text-sm tw-breadcrumbs">
|
||||
<ul>
|
||||
{breadcrumbs.map((b, i) => <li key={i}><Link to={b.path} >{b.name}</Link></li>)}
|
||||
</ul>
|
||||
</div>}
|
||||
</div>
|
||||
<div className="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 lg:tw-grid-cols-3 tw-gap-6 tw-pt-4">
|
||||
{
|
||||
items?.filter(i => i.layer?.name === layerName)
|
||||
.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) => {
|
||||
return (
|
||||
<ItemCard key={k} i={i} loading={loading} url={url} parameterField={parameterField} deleteCallback={()=> deleteItem(i)} ></ItemCard>
|
||||
)
|
||||
})
|
||||
}
|
||||
{addItemPopupType == "place" ?
|
||||
<MapOverlayPage className='tw-rounded-none tw-overflow-y-auto tw-bg-base-200 !tw-p-4'>
|
||||
<div className='tw-flex tw-flex-col tw-h-full'>
|
||||
<div className='tw-flex-none'>
|
||||
<Control position='topLeft' zIndex="1000" absolute={false}>
|
||||
<SearchControl />
|
||||
<TagsControl />
|
||||
</Control>
|
||||
</div>
|
||||
<div className="tw-overflow-scroll fade tw-flex-1 tw-grid tw-grid-cols-1 md:tw-grid-cols-2 lg:tw-grid-cols-3 2xl:tw-grid-cols-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) => {
|
||||
return (
|
||||
<ItemCard key={k} i={i} loading={loading} url={url} parameterField={parameterField} deleteCallback={() => deleteItem(i)} ></ItemCard>
|
||||
)
|
||||
})
|
||||
}
|
||||
{addItemPopupType == "place" ?
|
||||
|
||||
<form ref={tabRef} autoComplete='off' onSubmit={e => submitNewItem(e)} >
|
||||
<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></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 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></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>
|
||||
{children}
|
||||
|
||||
|
||||
</MapOverlayPage>
|
||||
|
||||
|
||||
@ -74,4 +74,28 @@ input[type="file"] {
|
||||
|
||||
.tw-tab-content .container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.masonry {
|
||||
column-count: 1;
|
||||
column-gap: 1.5rem;
|
||||
}
|
||||
.masonry-item {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.masonry {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.masonry {
|
||||
column-count: 3;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1536px) {
|
||||
.masonry {
|
||||
column-count: 4;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user