refactoring hooks and api

This commit is contained in:
Anton 2023-08-29 13:11:39 +02:00
parent b0e611c9c5
commit c92b2be9fc
11 changed files with 580 additions and 434 deletions

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -18,5 +18,5 @@ export default {
}),
typescript()
],
external: ['react', 'react-dom', 'leaflet', 'react-leaflet', 'react-toastify' , 'react-toastify/dist/ReactToastify.css', 'tw-elements' ,'react-router-dom', 'react-leaflet-cluster', '@tanstack/react-query', 'leaflet/dist/leaflet.css', '@heroicons/react/20/solid']
external: ['react', 'react-dom', 'leaflet', 'react-leaflet', 'react-toastify' , 'react-toastify/dist/ReactToastify.css', 'tw-elements' ,'react-router-dom', 'react-leaflet-cluster', '@tanstack/react-query', 'tributejs', 'prop-types', 'leaflet/dist/leaflet.css', '@heroicons/react/20/solid']
}

View File

@ -1,84 +1,47 @@
import * as React from 'react'
import { Marker } from 'react-leaflet'
import { Item, Tag, LayerProps } from '../../types'
import { Item, LayerProps } from '../../types'
import MarkerIconFactory from '../../Utils/MarkerIconFactory'
import { ItemViewPopup } from './Subcomponents/ItemViewPopup'
import { useTags } from './hooks/useTags'
import { useAddItem, useItems, useResetItems } from './hooks/useItems'
import { useItems, useResetItems, useSetItemsApi, useSetItemsData } from './hooks/useItems'
import { useEffect, useState } from 'react'
import { useAddLayer } from './hooks/useLayers'
import { ItemFormPopupProps, ItemFormPopup } from './Subcomponents/ItemFormPopup'
import { toast } from 'react-toastify'
import { hashTagRegex } from '../../Utils/HeighlightTags'
import { useFilterTags } from './hooks/useFilter'
export const Layer = (props: LayerProps) => {
const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null);
const tags = useTags();
const filterTags = useFilterTags();
const items = useItems();
const addItem = useAddItem()
const addLayer = useAddLayer();
const setItemsApi = useSetItemsApi();
const setItemsData = useSetItemsData();
const resetItems = useResetItems();
const getItemTags = (item: Item) : Tag[] => {
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))!) }
})
return itemTags;
};
const loadItems = async () => {
props.data?.map(item => {
if (item.position) {
item.layer = props;
addItem(item);
}
})
if (props.api) {
addLayer(props);
}
if(props.api) {
const result = await toast.promise(
props.api!.getItems(),
{
pending: `loading ${props.name} ...`,
success: `${props.name} loaded` ,
error: `error while loading ${props.name}`
}
);
if (result) {
result.map(item => {
if (item.position) {
addItem(({ layer: props, api: props.api, ...item }));
}
});
}
}
}
useEffect(() => {
resetItems(props);
loadItems();
props.data && setItemsData(props);
props.api && setItemsApi(props);
}, [props.data, props.api])
return (
<>
{items &&
items.filter(item => item.layer?.name === props.name)?.map((place: Item) => {
const tags = getItemTags(place);
items.filter(item => item.layer?.name === props.name)?.filter(item => item)?.map((place: Item) => {
const tags = place.tags;
if(place.name === "docutopia")
console.log(tags);
let color1 = "#666";
let color2 = "RGBA(35, 31, 32, 0.2)";
if (tags[0]) {
if (tags && tags[0]) {
color1 = tags[0].color;
}
if (tags[1]) {
if (tags && tags[1]) {
color2 = tags[1].color;
}
return (
@ -106,12 +69,12 @@ export const Layer = (props: LayerProps) => {
(props.children && React.Children.toArray(props.children).some(child => React.isValidElement(child) && child.props.__TYPE === "ItemForm") ?
React.Children.toArray(props.children).map((child) =>
React.isValidElement(child) && child.props.__TYPE === "ItemForm" ?
<ItemFormPopup key={props.setItemFormPopup?.name} position={props.itemFormPopup!.position} layer={props.itemFormPopup!.layer} setItemFormPopup={setItemFormPopup} item={props.itemFormPopup!.item} api={props.api} >{child}</ItemFormPopup>
<ItemFormPopup key={props.setItemFormPopup?.name} position={props.itemFormPopup!.position} layer={props.itemFormPopup!.layer} setItemFormPopup={setItemFormPopup} item={props.itemFormPopup!.item} >{child}</ItemFormPopup>
: ""
)
:
<>
<ItemFormPopup position={props.itemFormPopup!.position} layer={props.itemFormPopup!.layer} setItemFormPopup={setItemFormPopup} item={props.itemFormPopup!.item} api={props.api} />
<ItemFormPopup position={props.itemFormPopup!.position} layer={props.itemFormPopup!.layer} setItemFormPopup={setItemFormPopup} item={props.itemFormPopup!.item} />
</>)
}
</>

View File

@ -14,7 +14,6 @@ export interface ItemFormPopupProps {
position: LatLng,
layer: LayerProps,
item?: Item,
api?: ItemsApi<any>,
children?: React.ReactNode,
setItemFormPopup: React.Dispatch<React.SetStateAction<any>>
}
@ -47,22 +46,13 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
addTag({id: tag.slice(1), color: randomColor()})
});
if (props.item) {
formItem['id'] = props.item.id;
await props.api?.updateItem!(formItem);
formItem['api'] = props.api;
formItem['layer'] = props.layer;
await updateItem(formItem);
await updateItem({...props.item, ...formItem});
setSpinner(false);
map.closePopup();
}
else {
formItem['id'] = crypto.randomUUID();
await props.api?.createItem!(formItem);
formItem['api'] = props.api;
formItem['layer'] = props.layer;
await addItem(formItem);
await addItem({...formItem, id: crypto.randomUUID(), layer: props.layer});
setSpinner(false);
map.closePopup();
}

View File

@ -19,7 +19,7 @@ export function HeaderView({ item, setItemFormPopup }: {
const removeItemFromMap = (event: React.MouseEvent<HTMLElement>) => {
setLoading(true);
item.api?.deleteItem!(item.id)
item.layer.api?.deleteItem!(item.id)
.then(() => removeItem(item))
.then(() => map.closePopup())
.then(()=>setLoading(false))
@ -41,7 +41,7 @@ export function HeaderView({ item, setItemFormPopup }: {
<b className="tw-text-xl tw-font-bold">{item.name}</b>
</div>
<div className='tw-col-span-1'>
{item.api &&
{item.layer.api &&
<div className="tw-dropdown tw-dropdown-bottom">
<label tabIndex={0} className="tw-btn tw-m-1 tw-bg-white hover:tw-bg-white tw-text-gray-500 hover:tw-text-gray-700 tw-leading-3 tw-border-none tw-min-h-0 tw-h-4">
<svg xmlns="http://www.w3.org/2000/svg" className="tw-h-5 tw-w-5" viewBox="0 0 20 20" fill="currentColor">
@ -49,7 +49,7 @@ export function HeaderView({ item, setItemFormPopup }: {
</svg>
</label>
<ul tabIndex={0} className="tw-dropdown-content tw-menu tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box">
{item.api.updateItem && <li>
{item.layer.api.updateItem && <li>
<a className='tw-bg-white hover:tw-bg-gray-300 !tw-text-blue-800 hover:tw-text-gray-700' onClick={openEditPopup}>
<svg xmlns="http://www.w3.org/2000/svg" className="tw-h-5 tw-w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
@ -57,7 +57,7 @@ export function HeaderView({ item, setItemFormPopup }: {
</a>
</li>}
{item.api.deleteItem && <li>
{item.layer.api.deleteItem && <li>
<a className='tw-bg-white hover:tw-bg-gray-300 !tw-text-red-800 hover:tw-text-red-950' onClick={removeItemFromMap}>
{loading ? <span className="tw-loading tw-loading-spinner tw-loading-sm"></span>
:

View File

@ -0,0 +1,84 @@
import { useCallback, useReducer, createContext, useContext } from "react";
import * as React from "react";
import {Tag} from "../../../types";
type ActionType =
| { type: "ADD"; tag: Tag }
| { type: "REMOVE"; id: string };
type UseTagManagerResult = ReturnType<typeof useTagsManager>;
const TagContext = createContext<UseTagManagerResult>({
filterTags: [],
addFilterTag: () => { },
removeFilterTag: () => { },
});
function useTagsManager(initialTags: Tag[]): {
filterTags: Tag[];
addFilterTag: (tag: Tag) => void;
removeFilterTag: (id: string) => void;
} {
const [filterTags, dispatch] = useReducer((state: Tag[], action: ActionType) => {
switch (action.type) {
case "ADD":
const exist = state.find((tag) =>
tag.id === action.tag.id ? true : false
);
if (!exist) return [
...state,
action.tag,
];
else return state;
case "REMOVE":
return state.filter(({ id }) => id !== action.id);
default:
throw new Error();
}
}, initialTags);
const addFilterTag = (tag: Tag) => {
dispatch({
type: "ADD",
tag,
});
};
const removeFilterTag = useCallback((id: string) => {
dispatch({
type: "REMOVE",
id,
});
}, []);
return { filterTags, addFilterTag, removeFilterTag };
}
export const TagsProvider: React.FunctionComponent<{
initialTags: Tag[], children?: React.ReactNode
}> = ({ initialTags, children }) => (
<TagContext.Provider value={useTagsManager(initialTags)}>
{children}
</TagContext.Provider>
);
export const useFilterTags = (): Tag[] => {
const { filterTags } = useContext(TagContext);
return filterTags;
};
export const useAddFilterTag = (): UseTagManagerResult["addFilterTag"] => {
const { addFilterTag } = useContext(TagContext);
return addFilterTag;
};
export const useRemoveFilterTag = (): UseTagManagerResult["removeFilterTag"] => {
const { removeFilterTag } = useContext(TagContext);
return removeFilterTag;
};

View File

@ -1,69 +1,124 @@
import { useCallback, useReducer, createContext, useContext } from "react";
import { useCallback, useReducer, createContext, useContext, useEffect } from "react";
import * as React from "react";
import { Item, LayerProps } from "../../../types";
import { Item, ItemsApi, LayerProps, Tag } from "../../../types";
import { toast } from "react-toastify";
import { useAddLayer } from "./useLayers";
import { useTags } from "./useTags";
import { hashTagRegex } from "../../../Utils/HeighlightTags";
type ActionType =
| { type: "ADD"; item: Item }
| { type: "UPDATE"; item: Item }
| { type: "REMOVE"; item: Item }
| { type: "RESET"; layer: LayerProps };
| { type: "ADD"; item: Item }
| { type: "UPDATE"; item: Item }
| { type: "REMOVE"; item: Item }
| { type: "RESET"; layer: LayerProps }
| { type: "ADD_TAGS" };
type UseItemManagerResult = ReturnType<typeof useItemsManager>;
const ItemContext = createContext<UseItemManagerResult>({
items: [],
addItem: () => {},
updateItem: () => {},
removeItem: () => {},
resetItems: () => {}
items: [],
addItem: () => { },
updateItem: () => { },
removeItem: () => { },
resetItems: () => { },
setItemsApi: () => { },
setItemsData: () => { },
});
function useItemsManager (initialItems: Item[]): {
function useItemsManager(initialItems: Item[]): {
items: Item[];
addItem: (item: Item) => void;
updateItem: (item: Item) => void;
removeItem: (item: Item) => void;
resetItems: (layer: LayerProps) => void;
setItemsApi: (layer: LayerProps) => void;
setItemsData: (layer: LayerProps) => void;
} {
const addLayer = useAddLayer();
const tags = useTags();
const [items, dispatch] = useReducer((state: Item[], action: ActionType) => {
switch (action.type) {
case "ADD":
case "ADD":
const exist = state.find((item) =>
item.id === action.item.id ? true : false
);
if (!exist) return [
...state,
action.item,
];
else return state;
case "UPDATE":
return state.map((item) => {
if (item.id === action.item.id) {
return action.item
}
return item
});
case "REMOVE":
return state.filter(item => item !== action.item);
case "RESET":
return state.filter(item => item.layer.name !== action.layer.name);
default:
throw new Error();
}
item.id === action.item.id ? true : false
);
if (!exist) return [
...state,
action.item,
];
else return state;
case "UPDATE":
return state.map((item) => {
if (item.id === action.item.id) {
return action.item
}
return item
});
case "REMOVE":
return state.filter(item => item !== action.item);
case "RESET":
return state.filter(item => item.layer.name !== action.layer.name);
case "ADD_TAGS":
return state.map(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))!) }
})
return { ...item, tags: itemTags }
})
default:
throw new Error();
}
}, initialItems);
const addItem = useCallback((item: Item) => {
const setItemsApi = useCallback(async (layer: LayerProps) => {
layer.api?.createItem && addLayer(layer);
const result = await toast.promise(
layer.api!.getItems(),
{
pending: `loading ${layer.name} ...`,
success: `${layer.name} loaded`,
error: `error while loading ${layer.name}`
}
);
if (result) {
result.map(item => {
dispatch({ type: "ADD", item: { ...item, layer: layer } });
})
}
dispatch({ type: "ADD_TAGS" })
}, [])
const setItemsData = useCallback((layer: LayerProps) => {
layer.data?.map(item => {
dispatch({ type: "ADD", item: { ...item, layer: layer } })
})
dispatch({ type: "ADD_TAGS" })
}, []);
const addItem = useCallback(async (item: Item) => {
await item.layer.api?.createItem!(item);
dispatch({
type: "ADD",
item,
});
dispatch({ type: "ADD_TAGS" })
}, []);
const updateItem = useCallback((item: Item) => {
const updateItem = useCallback(async (item: Item) => {
await item.layer.api?.updateItem!(item);
dispatch({
type: "UPDATE",
item,
});
dispatch({ type: "ADD_TAGS" })
}, []);
const removeItem = useCallback((item: Item) => {
@ -80,11 +135,16 @@ function useItemsManager (initialItems: Item[]): {
});
}, []);
return { items, updateItem, addItem, removeItem, resetItems };
useEffect(() => {
dispatch({ type: "ADD_TAGS" })
}, [tags])
return { items, updateItem, addItem, removeItem, resetItems, setItemsApi, setItemsData };
}
export const ItemsProvider: React.FunctionComponent<{
initialItems: Item[], children?: React.ReactNode
initialItems: Item[], children?: React.ReactNode
}> = ({ initialItems, children }) => (
<ItemContext.Provider value={useItemsManager(initialItems)}>
{children}
@ -109,9 +169,19 @@ export const useUpdateItem = (): UseItemManagerResult["updateItem"] => {
export const useRemoveItem = (): UseItemManagerResult["removeItem"] => {
const { removeItem } = useContext(ItemContext);
return removeItem;
};
};
export const useResetItems = (): UseItemManagerResult["resetItems"] => {
const { resetItems } = useContext(ItemContext);
return resetItems;
};
export const useSetItemsApi = (): UseItemManagerResult["setItemsApi"] => {
const { setItemsApi } = useContext(ItemContext);
return setItemsApi;
};
export const useSetItemsData = (): UseItemManagerResult["setItemsData"] => {
const { setItemsData } = useContext(ItemContext);
return setItemsData;
};

View File

@ -1,10 +1,9 @@
import { useCallback, useReducer, createContext, useContext } from "react";
import * as React from "react";
import { Item, LayerProps } from "../../../types";
import { LayerProps } from "../../../types";
type ActionType =
| { type: "ADD LAYER"; layer: LayerProps }
| { type: "ADD ITEM"; item: Item; layer: LayerProps };
type UseItemManagerResult = ReturnType<typeof useLayerManager>;

View File

@ -1,7 +1,6 @@
import { useCallback, useReducer, createContext, useContext } from "react";
import * as React from "react";
import { ItemsApi, Tag } from "../../../types";
import { toast } from "react-toastify";
type ActionType =
| { type: "ADD"; tag: Tag }
@ -43,7 +42,7 @@ function useTagsManager(initialTags: Tag[]): {
}
}, initialTags);
const [api, setApi] = React.useState<ItemsApi<Tag> | undefined>(undefined)
const [api, setApi] = React.useState<ItemsApi<Tag>>({} as ItemsApi<Tag>)
const setTagApi = useCallback(async (api: ItemsApi<Tag>) => {
setApi(api);

View File

@ -1,30 +1,18 @@
/**
export const randomColor = (brightness) => {
function randomChannel(brightness){
var r = 255-brightness;
var n = 0|((Math.random() * r) + brightness);
var s = n.toString(16);
return (s.length==1) ? '0'+s : s;
}
return '#' + randomChannel(brightness) + randomChannel(brightness) + randomChannel(brightness);
}
*/
export const randomColor = () => {
return hsvToRgb((Math.random()+golden_ratio_conjugate)%1,0.8, 0.7)
return hsvToHex((Math.random() + golden_ratio_conjugate) % 1, 0.8, 0.7)
}
const golden_ratio_conjugate = 0.618033988749895;
function hsvToRgb(h, s, v){
function hsvToHex(h, s, v) {
var r, g, b;
var i = (Math.floor(h * 6));
var i = (Math.floor(h * 6));
var f = h * 6 - i;
var p = v * (1 - s);
var q = v * (1 - f * s);
var t = v * (1 - (1 - f) * s);
switch(i % 6){
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
@ -33,11 +21,11 @@ const golden_ratio_conjugate = 0.618033988749895;
case 5: r = v, g = p, b = q; break;
}
return rgbToHex(Math.round(r*255), Math.round(g*255), Math.round(b*255))
return rgbToHex(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255))
}
const rgbToHex = (r, g, b) => '#' + [r, g, b].map(x => {
const hex = x.toString(16)
return hex.length === 1 ? '0' + hex : hex
}).join('')
}).join('')

View File

@ -20,7 +20,6 @@ export interface LayerProps {
markerIcon: string,
markerShape: string,
markerDefaultColor: string,
tags?: Tag[],
api?: ItemsApi<any>,
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>,
itemFormPopup?: ItemFormPopupProps | null
@ -35,8 +34,8 @@ export class Item {
date_updated?: string | null;
start?: string;
end?: string;
tags?: number[];
api?: ItemsApi<any>;
tags?: Tag[];
[key: string]: any;
constructor(id:string|number,name:string,text:string,position:Geometry, layer?: LayerProps, api?: ItemsApi<any>){
this.id = id;