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>
<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={() => setSignupOpen(true)}>Sign In</a></li>
<li><a onClick={() => setSignupOpen(true)}>Sign Up</a></li>
</ul>
</div>

View File

@ -6,7 +6,7 @@ import { ItemViewPopup } from './Subcomponents/ItemViewPopup'
import { useItems, useResetItems, useSetItemsApi, useSetItemsData } from './hooks/useItems'
import { useEffect, useState } from 'react'
import { ItemFormPopupProps, ItemFormPopup } from './Subcomponents/ItemFormPopup'
import { useAddFilterTag, useFilterTags } from './hooks/useFilter'
import { useAddFilterTag, useFilterTags, useSearchPhrase } from './hooks/useFilter'
export const Layer = (props: LayerProps) => {
@ -15,9 +15,9 @@ export const Layer = (props: LayerProps) => {
const filterTags = useFilterTags();
const setFilterTag = useAddFilterTag();
// setFilterTag({id: "healing", color: "#000"})
// setFilterTag({id: "healing", color: "#000"})
const items = useItems();
@ -26,6 +26,8 @@ export const Layer = (props: LayerProps) => {
const resetItems = useResetItems();
const searchPhrase = useSearchPhrase();
useEffect(() => {
resetItems(props);
@ -36,36 +38,45 @@ export const Layer = (props: LayerProps) => {
return (
<>
{items &&
items.filter(item => item.layer?.name === props.name)?.filter(item =>
filterTags.length == 0 ? item : item.tags?.some(tag => filterTags.some(filterTag => filterTag.id === tag.id)))?.map((place: Item) => {
const tags = place.tags;
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} />
</>)
}
items.
filter(item => item.layer?.name === props.name)?.
filter(item =>
// filterTags.length == 0 ? item : item.tags?.some(tag => filterTags.some(filterTag => filterTag.id === tag.id)))?.
filterTags.length == 0 ? item : filterTags.every(tag => item.tags?.some(filterTag => filterTag.id === tag.id)))?.
filter(item => {
return searchPhrase === ''
? item :
item.name.toLowerCase().includes(searchPhrase.toLowerCase())
}).
map((place: Item) => {
const tags = place.tags;
</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}}
}

View File

@ -9,7 +9,7 @@ export default function AddButton({setSelectMode} : {setSelectMode: React.Dispat
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" >
<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">
<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

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])
return (
<LeafletPopup minWidth={275} maxWidth={275} autoPanPadding={[20, 5]}
<LeafletPopup minWidth={275} maxWidth={275} autoPanPadding={[20, 80]}
eventHandlers={{
remove: () => {
setTimeout(function () {

View File

@ -1,25 +1,44 @@
import * as React from 'react'
import { Item } from '../../../../types'
import { useTags } from '../../hooks/useTags';
import { useAddTag, useTags } from '../../hooks/useTags';
import reactStringReplace from 'react-string-replace';
import { useAddFilterTag, useResetFilterTags } from '../../hooks/useFilter';
import { hashTagRegex } from '../../../../Utils/HashTagRegex';
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 }) => {
const tags = useTags();
const addTag = useAddTag();
const addFilterTag = useAddFilterTag();
const resetFilterTags = useResetFilterTags();
const map = useMap();
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);
replacedText = reactStringReplace(replacedText, /(https?:\/\/\S+)/g, (url, i) => {
let shortUrl = url;
if (url.match('^https:\/\/')) {
@ -39,19 +58,15 @@ export const TextView = ({ item }: { item?: Item }) => {
)
});
//ts-ignore
replacedText = reactStringReplace(replacedText, hashTagRegex, (match, i) => {
const tag = tags.find(t => t.id.toLowerCase() == match.slice(1).toLowerCase())
return (
<a style={{ color: tag ? tag.color : '#aaa' , fontWeight: 'bold', cursor: 'pointer' }} key={tag ? tag.id+item!.id+i : i} onClick={() => {
resetFilterTags();
addFilterTag(tag!);
map.closePopup();
}}>{match}</a>
)
})
return (

View File

@ -3,7 +3,6 @@ import { Popup as LeafletPopup, useMap } from 'react-leaflet'
import { Item } from '../../../types'
import { ItemFormPopupProps } from './ItemFormPopup'
import { HeaderView } from './ItemPopupComponents/HeaderView'
import { StartEndView } from './ItemPopupComponents/StartEndView'
import { TextView } from './ItemPopupComponents/TextView'
export interface ItemViewPopupProps {
@ -12,13 +11,11 @@ export interface ItemViewPopupProps {
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
}
export const ItemViewPopup = (props: ItemViewPopupProps) => {
const item: Item = props.item;
return (
<LeafletPopup maxHeight={377} minWidth={275} maxWidth={275} autoPanPadding={[20, 5]}>
<LeafletPopup maxHeight={377} minWidth={275} maxWidth={275} autoPanPadding={[20, 80]}>
<div>
<HeaderView item={props.item} setItemFormPopup={props.setItemFormPopup} />
<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 { LayersProvider } from "./hooks/useLayers";
import { FilterProvider } from "./hooks/useFilter";
import { FilterControl } from "./Subcomponents/FilterControl";
export interface MapEventListenerProps {
@ -59,7 +60,8 @@ function UtopiaMap({
<FilterProvider initialTags={[]}>
<ItemsProvider initialItems={[]}>
<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
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
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";
type ActionType =
| { type: "ADD"; tag: Tag }
| { type: "REMOVE"; id: string }
| { type: "RESET"};
| { type: "ADD_TAG"; tag: Tag }
| { type: "REMOVE_TAG"; id: string }
| { type: "RESET_TAGS"};
type UseFilterManagerResult = ReturnType<typeof useFilterManager>;
const FilterContext = createContext<UseFilterManagerResult>({
filterTags: [],
searchPhrase: "",
addFilterTag: () => { },
removeFilterTag: () => { },
resetFilterTags: () => { },
setSearchPhrase: () => { },
});
function useFilterManager(initialTags: Tag[]): {
filterTags: Tag[];
searchPhrase: string;
addFilterTag: (tag: Tag) => void;
removeFilterTag: (id: string) => 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) {
case "ADD":
case "ADD_TAG":
const exist = state.find((tag) =>
tag.id === action.tag.id ? true : false
);
@ -33,40 +37,43 @@ function useFilterManager(initialTags: Tag[]): {
action.tag,
];
else return state;
case "REMOVE":
case "REMOVE_TAG":
return state.filter(({ id }) => id !== action.id);
case "RESET":
case "RESET_TAGS":
return initialTags;
default:
throw new Error();
}
}, initialTags);
const [searchPhrase, searchPhraseSet] = React.useState<string>("");
const addFilterTag = (tag: Tag) => {
dispatch({
type: "ADD",
dispatchTags({
type: "ADD_TAG",
tag,
});
};
const removeFilterTag = useCallback((id: string) => {
dispatch({
type: "REMOVE",
dispatchTags({
type: "REMOVE_TAG",
id,
});
}, []);
const resetFilterTags = useCallback(() => {
dispatch({
type: "RESET",
dispatchTags({
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<{
@ -96,3 +103,13 @@ export const useResetFilterTags = (): UseFilterManagerResult["resetFilterTags"]
const { resetFilterTags } = useContext(FilterContext);
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 { toast } from "react-toastify";
import { useAddLayer } from "./useLayers";
import { useTags } from "./useTags";
import { useAddTag, useTags } from "./useTags";
import { hashTagRegex } from "../../../Utils/HashTagRegex";
import { randomColor } from "../../../Utils/RandomColor";
type ActionType =
@ -39,6 +40,7 @@ function useItemsManager(initialItems: Item[]): {
const addLayer = useAddLayer();
const tags = useTags();
const addTag = useAddTag();
const [items, dispatch] = useReducer((state: Item[], action: ActionType) => {
switch (action.type) {
@ -67,7 +69,9 @@ function useItemsManager(initialItems: Item[]): {
const itemTagStrings = item.text.toLocaleLowerCase().match(hashTagRegex);
const itemTags: 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 }
})
@ -97,7 +101,7 @@ function useItemsManager(initialItems: Item[]): {
const setItemsData = useCallback((layer: LayerProps) => {
layer.data?.map(item => {
dispatch({ type: "ADD", item: { ...item, layer: layer } })
dispatch({ type: "ADD", item: { ...item, layer: layer } });
})
dispatch({ type: "ADD_TAGS" })
}, []);

View File

@ -16,4 +16,15 @@
.tw-modal-box {
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;
}