mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
Filter Control and more styling
This commit is contained in:
parent
bbe755a034
commit
3a9c3de127
@ -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>
|
||||
|
||||
|
||||
@ -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}}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
28
src/Components/Map/Subcomponents/FilterControl.tsx
Normal file
28
src/Components/Map/Subcomponents/FilterControl.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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 () {
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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'>
|
||||
|
||||
@ -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='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://tile.osmand.net/hd/{z}/{x}/{y}.png" />
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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" })
|
||||
}, []);
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user