Filter Control and more styling

This commit is contained in:
Anton 2023-08-31 16:27:31 +02:00
parent bbe755a034
commit 3a9c3de127
11 changed files with 152 additions and 67 deletions

View File

@ -133,7 +133,7 @@ export default function NavBar({ appName, useAuth }: { appName: string, useAuth:
</label> </label>
<ul tabIndex={1} className="tw-menu tw-menu-compact tw-dropdown-content tw-mt-3 tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-w-52 !tw-z-[1500]"> <ul tabIndex={1} className="tw-menu tw-menu-compact tw-dropdown-content tw-mt-3 tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-w-52 !tw-z-[1500]">
<li><a onClick={() => setLoginOpen(true)}>Login</a></li> <li><a onClick={() => setLoginOpen(true)}>Login</a></li>
<li><a onClick={() => setSignupOpen(true)}>Sign In</a></li> <li><a onClick={() => setSignupOpen(true)}>Sign Up</a></li>
</ul> </ul>
</div> </div>

View File

@ -6,7 +6,7 @@ import { ItemViewPopup } from './Subcomponents/ItemViewPopup'
import { useItems, useResetItems, useSetItemsApi, useSetItemsData } from './hooks/useItems' import { useItems, useResetItems, useSetItemsApi, useSetItemsData } from './hooks/useItems'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { ItemFormPopupProps, ItemFormPopup } from './Subcomponents/ItemFormPopup' import { ItemFormPopupProps, ItemFormPopup } from './Subcomponents/ItemFormPopup'
import { useAddFilterTag, useFilterTags } from './hooks/useFilter' import { useAddFilterTag, useFilterTags, useSearchPhrase } from './hooks/useFilter'
export const Layer = (props: LayerProps) => { export const Layer = (props: LayerProps) => {
@ -15,9 +15,9 @@ export const Layer = (props: LayerProps) => {
const filterTags = useFilterTags(); const filterTags = useFilterTags();
const setFilterTag = useAddFilterTag(); const setFilterTag = useAddFilterTag();
// setFilterTag({id: "healing", color: "#000"})
// setFilterTag({id: "healing", color: "#000"})
const items = useItems(); const items = useItems();
@ -26,6 +26,8 @@ export const Layer = (props: LayerProps) => {
const resetItems = useResetItems(); const resetItems = useResetItems();
const searchPhrase = useSearchPhrase();
useEffect(() => { useEffect(() => {
resetItems(props); resetItems(props);
@ -36,36 +38,45 @@ export const Layer = (props: LayerProps) => {
return ( return (
<> <>
{items && {items &&
items.filter(item => item.layer?.name === props.name)?.filter(item => items.
filterTags.length == 0 ? item : item.tags?.some(tag => filterTags.some(filterTag => filterTag.id === tag.id)))?.map((place: Item) => { filter(item => item.layer?.name === props.name)?.
const tags = place.tags; filter(item =>
// filterTags.length == 0 ? item : item.tags?.some(tag => filterTags.some(filterTag => filterTag.id === tag.id)))?.
let color1 = "#666"; filterTags.length == 0 ? item : filterTags.every(tag => item.tags?.some(filterTag => filterTag.id === tag.id)))?.
let color2 = "RGBA(35, 31, 32, 0.2)"; filter(item => {
if (tags && tags[0]) { return searchPhrase === ''
color1 = tags[0].color; ? item :
} item.name.toLowerCase().includes(searchPhrase.toLowerCase())
if (tags && tags[1]) { }).
color2 = tags[1].color; map((place: Item) => {
} const tags = place.tags;
return (
<Marker icon={MarkerIconFactory(props.markerShape, color1, color2, props.markerIcon)} key={place.id} position={[place.position.coordinates[1], place.position.coordinates[0]]}>
{
(props.children && React.Children.toArray(props.children).some(child => React.isValidElement(child) && child.props.__TYPE === "ItemView") ?
React.Children.toArray(props.children).map((child) =>
React.isValidElement(child) && child.props.__TYPE === "ItemView" ?
<ItemViewPopup key={place.id} item={place} setItemFormPopup={props.setItemFormPopup} >{child}</ItemViewPopup>
: ""
)
:
<>
<ItemViewPopup item={place} setItemFormPopup={props.setItemFormPopup} />
</>)
}
</Marker> let color1 = "#666";
); let color2 = "RGBA(35, 31, 32, 0.2)";
}) if (tags && tags[0]) {
color1 = tags[0].color;
}
if (tags && tags[1]) {
color2 = tags[1].color;
}
return (
<Marker icon={MarkerIconFactory(props.markerShape, color1, color2, props.markerIcon)} key={place.id} position={[place.position.coordinates[1], place.position.coordinates[0]]}>
{
(props.children && React.Children.toArray(props.children).some(child => React.isValidElement(child) && child.props.__TYPE === "ItemView") ?
React.Children.toArray(props.children).map((child) =>
React.isValidElement(child) && child.props.__TYPE === "ItemView" ?
<ItemViewPopup key={place.id} item={place} setItemFormPopup={props.setItemFormPopup} >{child}</ItemViewPopup>
: ""
)
:
<>
<ItemViewPopup item={place} setItemFormPopup={props.setItemFormPopup} />
</>)
}
</Marker>
);
})
} }
{//{props.children}} {//{props.children}}
} }

View File

@ -9,7 +9,7 @@ export default function AddButton({setSelectMode} : {setSelectMode: React.Dispat
return ( return (
<div className="tw-dropdown tw-dropdown-top tw-dropdown-end tw-dropdown-hover tw-z-500 tw-absolute tw-right-5 tw-bottom-5" > <div className="tw-dropdown tw-dropdown-top tw-dropdown-end tw-dropdown-hover tw-z-500 tw-absolute tw-right-5 tw-bottom-5" >
<button tabIndex={0} className="tw-z-500 tw-border-0 tw-m-0 tw-mt-2 tw-p-0 tw-w-14 tw-h-14 tw-cursor-pointer tw-bg-white tw-rounded-full hover:tw-bg-gray-100 tw-mouse tw-drop-shadow-md tw-transition tw-ease-in tw-duration-200 focus:tw-outline-none"> <button tabIndex={0} className="tw-z-500 tw-border-0 tw-m-0 tw-mt-2 tw-p-0 tw-w-14 tw-h-14 tw-cursor-pointer tw-bg-white tw-rounded-full hover:tw-bg-gray-100 tw-mouse tw-drop-shadow-md tw-transition tw-ease-in tw-duration-200 focus:tw-outline-none tw-shadow-xl">
<svg viewBox="0 0 20 20" enableBackground="new 0 0 20 20" className="tw-w-6 tw-h-6 tw-inline-block"> <svg viewBox="0 0 20 20" enableBackground="new 0 0 20 20" className="tw-w-6 tw-h-6 tw-inline-block">
<path fill="#2e8555" d="M16,10c0,0.553-0.048,1-0.601,1H11v4.399C11,15.951,10.553,16,10,16c-0.553,0-1-0.049-1-0.601V11H4.601 <path fill="#2e8555" d="M16,10c0,0.553-0.048,1-0.601,1H11v4.399C11,15.951,10.553,16,10,16c-0.553,0-1-0.049-1-0.601V11H4.601
C4.049,11,4,10.553,4,10c0-0.553,0.049-1,0.601-1H9V4.601C9,4.048,9.447,4,10,4c0.553,0,1,0.048,1,0.601V9h4.399 C4.049,11,4,10.553,4,10c0-0.553,0.049-1,0.601-1H9V4.601C9,4.048,9.447,4,10,4c0.553,0,1,0.048,1,0.601V9h4.399

View File

@ -0,0 +1,28 @@
import * as React from 'react'
import { useFilterTags, useRemoveFilterTag, useSetSearchPhrase } from '../hooks/useFilter'
export const FilterControl = () => {
const filterTags = useFilterTags();
const removeFilterTag = useRemoveFilterTag();
const setSearchPhrase = useSetSearchPhrase();
return (
<div className='tw-flex tw-flex-col tw-absolute tw-top-4 tw-left-4 tw-z-1000 tw-right-4'>
<input type="text" placeholder="search ..." className="tw-input tw-input-bordered tw-w-full tw-max-w-sm tw-shadow-xl tw-rounded-2xl" onChange={(e) => setSearchPhrase(e.target.value)} />
<div className='tw-flex tw-flex-wrap tw-mt-4'>
{
filterTags.map(tag =>
<div key={tag.id} className='tw-rounded-2xl tw-text-white tw-p-2 tw-px-4 tw-shadow-xl tw-card tw-mr-2 tw-mb-2' style={{ backgroundColor: tag.color }}>
<div className="tw-card-actions tw-justify-end">
<label className="tw-btn tw-btn-xs tw-btn-circle tw-absolute tw--right-2 tw--top-2 tw-bg-white tw-text-gray-600" onClick={() => (removeFilterTag(tag.id))}></label>
</div><b>#{tag.id}</b>
</div>
)
}
</div>
</div>
)
}

View File

@ -71,7 +71,7 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
}, [props.position]) }, [props.position])
return ( return (
<LeafletPopup minWidth={275} maxWidth={275} autoPanPadding={[20, 5]} <LeafletPopup minWidth={275} maxWidth={275} autoPanPadding={[20, 80]}
eventHandlers={{ eventHandlers={{
remove: () => { remove: () => {
setTimeout(function () { setTimeout(function () {

View File

@ -1,25 +1,44 @@
import * as React from 'react' import * as React from 'react'
import { Item } from '../../../../types' import { Item } from '../../../../types'
import { useTags } from '../../hooks/useTags'; import { useAddTag, useTags } from '../../hooks/useTags';
import reactStringReplace from 'react-string-replace'; import reactStringReplace from 'react-string-replace';
import { useAddFilterTag, useResetFilterTags } from '../../hooks/useFilter'; import { useAddFilterTag, useResetFilterTags } from '../../hooks/useFilter';
import { hashTagRegex } from '../../../../Utils/HashTagRegex'; import { hashTagRegex } from '../../../../Utils/HashTagRegex';
import { fixUrls, mailRegex, urlRegex } from '../../../../Utils/ReplaceURLs'; import { fixUrls, mailRegex, urlRegex } from '../../../../Utils/ReplaceURLs';
import { useMap } from 'react-leaflet';
import { randomColor } from '../../../../Utils/RandomColor';
import { useEffect, useRef } from 'react';
export const TextView = ({ item }: { item?: Item }) => { export const TextView = ({ item }: { item?: Item }) => {
const tags = useTags(); const tags = useTags();
const addTag = useAddTag();
const addFilterTag = useAddFilterTag(); const addFilterTag = useAddFilterTag();
const resetFilterTags = useResetFilterTags(); const resetFilterTags = useResetFilterTags();
const map = useMap();
let replacedText; let replacedText;
// use init-Ref to prevent react18 from calling useEffect twice
const init = useRef(false)
useEffect(() => {
if (!init.current) {
item?.text.toLocaleLowerCase().match(hashTagRegex)?.map(tag=> {
if (!tags.find((t) => t.id === tag.slice(1))) {
console.log(tag);
addTag({id: tag.slice(1), color: randomColor()})
}
});
init.current = true;
}
}, [])
if (item && item.text) replacedText = fixUrls(item.text); if (item && item.text) replacedText = fixUrls(item.text);
replacedText = reactStringReplace(replacedText, /(https?:\/\/\S+)/g, (url, i) => { replacedText = reactStringReplace(replacedText, /(https?:\/\/\S+)/g, (url, i) => {
let shortUrl = url; let shortUrl = url;
if (url.match('^https:\/\/')) { if (url.match('^https:\/\/')) {
@ -39,19 +58,15 @@ export const TextView = ({ item }: { item?: Item }) => {
) )
}); });
//ts-ignore
replacedText = reactStringReplace(replacedText, hashTagRegex, (match, i) => { replacedText = reactStringReplace(replacedText, hashTagRegex, (match, i) => {
const tag = tags.find(t => t.id.toLowerCase() == match.slice(1).toLowerCase()) const tag = tags.find(t => t.id.toLowerCase() == match.slice(1).toLowerCase())
return ( return (
<a style={{ color: tag ? tag.color : '#aaa' , fontWeight: 'bold', cursor: 'pointer' }} key={tag ? tag.id+item!.id+i : i} onClick={() => { <a style={{ color: tag ? tag.color : '#aaa' , fontWeight: 'bold', cursor: 'pointer' }} key={tag ? tag.id+item!.id+i : i} onClick={() => {
resetFilterTags();
addFilterTag(tag!); addFilterTag(tag!);
map.closePopup();
}}>{match}</a> }}>{match}</a>
) )
}) })
return ( return (

View File

@ -3,7 +3,6 @@ import { Popup as LeafletPopup, useMap } from 'react-leaflet'
import { Item } from '../../../types' import { Item } from '../../../types'
import { ItemFormPopupProps } from './ItemFormPopup' import { ItemFormPopupProps } from './ItemFormPopup'
import { HeaderView } from './ItemPopupComponents/HeaderView' import { HeaderView } from './ItemPopupComponents/HeaderView'
import { StartEndView } from './ItemPopupComponents/StartEndView'
import { TextView } from './ItemPopupComponents/TextView' import { TextView } from './ItemPopupComponents/TextView'
export interface ItemViewPopupProps { export interface ItemViewPopupProps {
@ -12,13 +11,11 @@ export interface ItemViewPopupProps {
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>> setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
} }
export const ItemViewPopup = (props: ItemViewPopupProps) => { export const ItemViewPopup = (props: ItemViewPopupProps) => {
const item: Item = props.item; const item: Item = props.item;
return ( return (
<LeafletPopup maxHeight={377} minWidth={275} maxWidth={275} autoPanPadding={[20, 5]}> <LeafletPopup maxHeight={377} minWidth={275} maxWidth={275} autoPanPadding={[20, 80]}>
<div> <div>
<HeaderView item={props.item} setItemFormPopup={props.setItemFormPopup} /> <HeaderView item={props.item} setItemFormPopup={props.setItemFormPopup} />
<div className='tw-overflow-y-auto tw-max-h-72'> <div className='tw-overflow-y-auto tw-max-h-72'>

View File

@ -12,6 +12,7 @@ import { ItemsProvider } from "./hooks/useItems";
import { TagsProvider } from "./hooks/useTags"; import { TagsProvider } from "./hooks/useTags";
import { LayersProvider } from "./hooks/useLayers"; import { LayersProvider } from "./hooks/useLayers";
import { FilterProvider } from "./hooks/useFilter"; import { FilterProvider } from "./hooks/useFilter";
import { FilterControl } from "./Subcomponents/FilterControl";
export interface MapEventListenerProps { export interface MapEventListenerProps {
@ -59,7 +60,8 @@ function UtopiaMap({
<FilterProvider initialTags={[]}> <FilterProvider initialTags={[]}>
<ItemsProvider initialItems={[]}> <ItemsProvider initialItems={[]}>
<div className={(selectMode != null ? "crosshair-cursor-enabled" : undefined)}> <div className={(selectMode != null ? "crosshair-cursor-enabled" : undefined)}>
<MapContainer ref={mapDivRef} style={{ height: height, width: width }} center={center} zoom={zoom}> <MapContainer ref={mapDivRef} style={{ height: height, width: width }} center={center} zoom={zoom} zoomControl={false}>
<FilterControl></FilterControl>
<TileLayer <TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://tile.osmand.net/hd/{z}/{x}/{y}.png" /> url="https://tile.osmand.net/hd/{z}/{x}/{y}.png" />

View File

@ -3,28 +3,32 @@ import * as React from "react";
import {Tag} from "../../../types"; import {Tag} from "../../../types";
type ActionType = type ActionType =
| { type: "ADD"; tag: Tag } | { type: "ADD_TAG"; tag: Tag }
| { type: "REMOVE"; id: string } | { type: "REMOVE_TAG"; id: string }
| { type: "RESET"}; | { type: "RESET_TAGS"};
type UseFilterManagerResult = ReturnType<typeof useFilterManager>; type UseFilterManagerResult = ReturnType<typeof useFilterManager>;
const FilterContext = createContext<UseFilterManagerResult>({ const FilterContext = createContext<UseFilterManagerResult>({
filterTags: [], filterTags: [],
searchPhrase: "",
addFilterTag: () => { }, addFilterTag: () => { },
removeFilterTag: () => { }, removeFilterTag: () => { },
resetFilterTags: () => { }, resetFilterTags: () => { },
setSearchPhrase: () => { },
}); });
function useFilterManager(initialTags: Tag[]): { function useFilterManager(initialTags: Tag[]): {
filterTags: Tag[]; filterTags: Tag[];
searchPhrase: string;
addFilterTag: (tag: Tag) => void; addFilterTag: (tag: Tag) => void;
removeFilterTag: (id: string) => void; removeFilterTag: (id: string) => void;
resetFilterTags: () => void; resetFilterTags: () => void;
setSearchPhrase: (phrase: string) => void;
} { } {
const [filterTags, dispatch] = useReducer((state: Tag[], action: ActionType) => { const [filterTags, dispatchTags] = useReducer((state: Tag[], action: ActionType) => {
switch (action.type) { switch (action.type) {
case "ADD": case "ADD_TAG":
const exist = state.find((tag) => const exist = state.find((tag) =>
tag.id === action.tag.id ? true : false tag.id === action.tag.id ? true : false
); );
@ -33,40 +37,43 @@ function useFilterManager(initialTags: Tag[]): {
action.tag, action.tag,
]; ];
else return state; else return state;
case "REMOVE": case "REMOVE_TAG":
return state.filter(({ id }) => id !== action.id); return state.filter(({ id }) => id !== action.id);
case "RESET": case "RESET_TAGS":
return initialTags; return initialTags;
default: default:
throw new Error(); throw new Error();
} }
}, initialTags); }, initialTags);
const [searchPhrase, searchPhraseSet] = React.useState<string>("");
const addFilterTag = (tag: Tag) => { const addFilterTag = (tag: Tag) => {
dispatch({ dispatchTags({
type: "ADD", type: "ADD_TAG",
tag, tag,
}); });
}; };
const removeFilterTag = useCallback((id: string) => { const removeFilterTag = useCallback((id: string) => {
dispatch({ dispatchTags({
type: "REMOVE", type: "REMOVE_TAG",
id, id,
}); });
}, []); }, []);
const resetFilterTags = useCallback(() => { const resetFilterTags = useCallback(() => {
dispatch({ dispatchTags({
type: "RESET", type: "RESET_TAGS",
}); });
}, []); }, []);
const setSearchPhrase = useCallback((phrase:string) => {
searchPhraseSet(phrase)
}, []);
return { filterTags, addFilterTag, removeFilterTag, resetFilterTags }; return { filterTags, addFilterTag, removeFilterTag, resetFilterTags, setSearchPhrase, searchPhrase };
} }
export const FilterProvider: React.FunctionComponent<{ export const FilterProvider: React.FunctionComponent<{
@ -96,3 +103,13 @@ export const useResetFilterTags = (): UseFilterManagerResult["resetFilterTags"]
const { resetFilterTags } = useContext(FilterContext); const { resetFilterTags } = useContext(FilterContext);
return resetFilterTags; return resetFilterTags;
}; };
export const useSearchPhrase = (): UseFilterManagerResult["searchPhrase"] => {
const { searchPhrase } = useContext(FilterContext);
return searchPhrase;
};
export const useSetSearchPhrase = (): UseFilterManagerResult["setSearchPhrase"] => {
const { setSearchPhrase } = useContext(FilterContext);
return setSearchPhrase;
};

View File

@ -3,8 +3,9 @@ import * as React from "react";
import { Item, ItemsApi, LayerProps, Tag } from "../../../types"; import { Item, ItemsApi, LayerProps, Tag } from "../../../types";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useAddLayer } from "./useLayers"; import { useAddLayer } from "./useLayers";
import { useTags } from "./useTags"; import { useAddTag, useTags } from "./useTags";
import { hashTagRegex } from "../../../Utils/HashTagRegex"; import { hashTagRegex } from "../../../Utils/HashTagRegex";
import { randomColor } from "../../../Utils/RandomColor";
type ActionType = type ActionType =
@ -39,6 +40,7 @@ function useItemsManager(initialItems: Item[]): {
const addLayer = useAddLayer(); const addLayer = useAddLayer();
const tags = useTags(); const tags = useTags();
const addTag = useAddTag();
const [items, dispatch] = useReducer((state: Item[], action: ActionType) => { const [items, dispatch] = useReducer((state: Item[], action: ActionType) => {
switch (action.type) { switch (action.type) {
@ -67,7 +69,9 @@ function useItemsManager(initialItems: Item[]): {
const itemTagStrings = item.text.toLocaleLowerCase().match(hashTagRegex); const itemTagStrings = item.text.toLocaleLowerCase().match(hashTagRegex);
const itemTags: Tag[] = []; const itemTags: Tag[] = [];
itemTagStrings?.map(tag => { itemTagStrings?.map(tag => {
if (tags.find(t => t.id === tag.slice(1))) { itemTags.push(tags.find(t => t.id === tag.slice(1))!) } if (tags.find(t => t.id === tag.slice(1))) {
itemTags.push(tags.find(t => t.id === tag.slice(1))!)
}
}) })
return { ...item, tags: itemTags } return { ...item, tags: itemTags }
}) })
@ -97,7 +101,7 @@ function useItemsManager(initialItems: Item[]): {
const setItemsData = useCallback((layer: LayerProps) => { const setItemsData = useCallback((layer: LayerProps) => {
layer.data?.map(item => { layer.data?.map(item => {
dispatch({ type: "ADD", item: { ...item, layer: layer } }) dispatch({ type: "ADD", item: { ...item, layer: layer } });
}) })
dispatch({ type: "ADD_TAGS" }) dispatch({ type: "ADD_TAGS" })
}, []); }, []);

View File

@ -16,4 +16,15 @@
.tw-modal-box { .tw-modal-box {
max-height: calc(100vh - 2em); max-height: calc(100vh - 2em);
}
.Toastify__toast {
border-radius: 1rem;
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
margin-left: 1rem;
margin-right: 1rem;
margin-bottom: 1rem;
} }