optimized search, filter and items index pages

This commit is contained in:
Anton Tranelis 2024-06-16 11:45:18 +02:00
parent f1c272126c
commit 22ab0e3acc
9 changed files with 120 additions and 90 deletions

View File

@ -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>
)
}

View File

@ -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>

View File

@ -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>

View File

@ -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]);

View File

@ -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>

View File

@ -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({

View File

@ -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 &&

View File

@ -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>

View File

@ -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;
}
}