mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
merged
This commit is contained in:
commit
4c6cfa6587
5
package-lock.json
generated
5
package-lock.json
generated
@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "utopia-ui",
|
||||
"version": "3.0.0-alpha.186",
|
||||
|
||||
"version": "3.0.0-alpha.194",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "utopia-ui",
|
||||
"version": "3.0.0-alpha.186",
|
||||
"version": "3.0.0-alpha.194",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.0.17",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "utopia-ui",
|
||||
"version": "3.0.0-alpha.186",
|
||||
"version": "3.0.0-alpha.194",
|
||||
"description": "Reuseable React Components to build mapping apps for real life communities and networks",
|
||||
"repository": "https://github.com/utopia-os/utopia-ui",
|
||||
"homepage:": "https://utopia-os.org/",
|
||||
|
||||
@ -16,7 +16,7 @@ import { LeafletRefsProvider } from '../Map/hooks/useLeafletRefs'
|
||||
import { SelectPositionProvider } from '../Map/hooks/useSelectPosition'
|
||||
import { ClusterRefProvider } from '../Map/hooks/useClusterRef'
|
||||
|
||||
export function AppShell({ appName, children, assetsApi }: { appName: string, children: React.ReactNode, assetsApi: AssetsApi }) {
|
||||
export function AppShell({ appName, children, assetsApi, userType }: { appName: string, children: React.ReactNode, assetsApi: AssetsApi, userType: string }) {
|
||||
|
||||
// Create a client
|
||||
const queryClient = new QueryClient()
|
||||
@ -47,7 +47,7 @@ export function AppShell({ appName, children, assetsApi }: { appName: string, ch
|
||||
pauseOnHover
|
||||
theme="light" />
|
||||
<div className='tw-flex tw-flex-col tw-h-full'>
|
||||
<NavBar appName={appName}></NavBar>
|
||||
<NavBar userType={userType} appName={appName}></NavBar>
|
||||
<div id="app-content" className="tw-flex-grow">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@ -7,7 +7,7 @@ import { useItems } from "../Map/hooks/useItems";
|
||||
import { Item } from "../../types";
|
||||
|
||||
|
||||
export default function NavBar({ appName}: { appName: string }) {
|
||||
export default function NavBar({ appName, userType}: { appName: string, userType: string }) {
|
||||
|
||||
|
||||
const { isAuthenticated, user, logout } = useAuth();
|
||||
@ -16,7 +16,7 @@ export default function NavBar({ appName}: { appName: string }) {
|
||||
const items = useItems();
|
||||
|
||||
useEffect(() => {
|
||||
const profile = user && items.find(i => (i.user_created?.id === user.id) && i.layer?.itemType.name === "user");
|
||||
const profile = user && items.find(i => (i.user_created?.id === user.id) && i.layer?.itemType.name === userType);
|
||||
profile ? setUserProfile(profile) : setUserProfile({id: crypto.randomUUID(), name: user?.first_name, text: ""});
|
||||
}, [user, items])
|
||||
|
||||
@ -86,12 +86,14 @@ export default function NavBar({ appName}: { appName: string }) {
|
||||
|
||||
{isAuthenticated ?
|
||||
<div className="tw-flex-none">
|
||||
{ userProfile?.image? <div className="tw-avatar">
|
||||
<Link to={`${userProfile.id && "/item/"+userProfile.id}`} className="tw-flex tw-items-center">
|
||||
{ userProfile?.image && <div className="tw-avatar">
|
||||
<div className="tw-w-10 tw-rounded-full">
|
||||
<img src={"https://api.utopia-lab.org/assets/" + userProfile.image} />
|
||||
</div>
|
||||
</div> : <></>}
|
||||
</div>}
|
||||
<div className='tw-ml-2 tw-mr-2'>{userProfile.name||user?.first_name}</div>
|
||||
</Link>
|
||||
<div className="tw-dropdown tw-dropdown-end">
|
||||
<label tabIndex={0} className="tw-btn tw-btn-ghost tw-btn-square">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="tw-h-5 tw-w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
|
||||
@ -63,7 +63,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: route[], bottomRoute
|
||||
return (
|
||||
<nav
|
||||
id="sidenav"
|
||||
className={`group tw-fixed tw-left-0 ${embedded ? 'tw-mt-0' : 'tw-mt-16' } tw-top-0 tw-z-[10035] tw-h-[calc(100dvh-64px)] tw--translate-x-full tw-overflow-hidden tw-shadow-xl data-[te-sidenav-slim='true']:tw-hidden data-[te-sidenav-slim-collapsed='true']:tw-w-[56px] data-[te-sidenav-slim='true']:tw-w-[56px] data-[te-sidenav-hidden='false']:tw-translate-x-0 dark:tw-bg-zinc-800 [&[data-te-sidenav-slim-collapsed='true'][data-te-sidenav-slim='false']]:tw-hidden [&[data-te-sidenav-slim-collapsed='true'][data-te-sidenav-slim='true']]:[display:unset]`}
|
||||
className={`group tw-fixed tw-left-0 ${embedded ? 'tw-mt-0 tw-h-[100dvh]' : 'tw-mt-16 tw-h-[calc(100dvh-64px)]' } tw-top-0 tw-z-[10035] tw--translate-x-full tw-overflow-hidden tw-shadow-xl data-[te-sidenav-slim='true']:tw-hidden data-[te-sidenav-slim-collapsed='true']:tw-w-[56px] data-[te-sidenav-slim='true']:tw-w-[56px] data-[te-sidenav-hidden='false']:tw-translate-x-0 dark:tw-bg-zinc-800 [&[data-te-sidenav-slim-collapsed='true'][data-te-sidenav-slim='false']]:tw-hidden [&[data-te-sidenav-slim-collapsed='true'][data-te-sidenav-slim='true']]:[display:unset]`}
|
||||
data-te-sidenav-init
|
||||
data-te-sidenav-hidden="true"
|
||||
data-te-sidenav-mode="side"
|
||||
@ -72,7 +72,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: route[], bottomRoute
|
||||
data-te-sidenav-slim-collapsed="true"
|
||||
data-te-sidenav-slim-width="56"
|
||||
data-te-sidenav-width="160">
|
||||
<div className='tw-flex tw-flex-col tw-h-[calc(100dvh-64px)]'>
|
||||
<div className={`tw-flex tw-flex-col ${embedded ? "tw-h-full" :"tw-h-[calc(100dvh-64px)]"}`}>
|
||||
<ul className="tw-menu tw-w-full tw-bg-base-100 tw-text-base-content tw-p-0" data-te-sidenav-menu-ref>
|
||||
{
|
||||
routes.map((route, k) => {
|
||||
|
||||
34
src/Components/Input/ComboBoxInput.tsx
Normal file
34
src/Components/Input/ComboBoxInput.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { useState } from "react"
|
||||
import * as React from "react"
|
||||
|
||||
interface ComboBoxProps {
|
||||
id?: string;
|
||||
options: { value: string, label: string }[];
|
||||
value: string;
|
||||
onValueChange: (newValue: string) => void;
|
||||
}
|
||||
|
||||
const ComboBoxInput = ({ id, options, value, onValueChange }: ComboBoxProps) => {
|
||||
|
||||
const [selectedValue, setSelectedValue] = useState(value);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
setSelectedValue(value);
|
||||
onValueChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<select
|
||||
id={id}
|
||||
className="tw-form-select tw-block tw-w-full tw-py-2 tw-px-4 tw-border tw-border-gray-300 rounded-md tw-shadow-sm tw-text-sm focus:tw-outline-none focus:tw-ring-indigo-500 focus:tw-border-indigo-500 sm:tw-text-sm"
|
||||
onChange={handleChange}
|
||||
>
|
||||
{options.map((o) =>
|
||||
<option value={o.value} key={o.value} selected={o.value == value}>{o.label}</option>
|
||||
)}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
export default ComboBoxInput;
|
||||
@ -6,7 +6,7 @@ import { ItemViewPopup } from './Subcomponents/ItemViewPopup'
|
||||
import { useAllItemsLoaded, useItems, useSetItemsApi, useSetItemsData } from './hooks/useItems'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ItemFormPopup } from './Subcomponents/ItemFormPopup'
|
||||
import { useFilterTags, useIsLayerVisible } from './hooks/useFilter'
|
||||
import { useFilterTags, useIsGroupTypeVisible, useIsLayerVisible } from './hooks/useFilter'
|
||||
import { useAddTag, useAllTagsLoaded, useGetItemTags, useTags } from './hooks/useTags'
|
||||
import { useAddMarker, useAddPopup, useLeafletRefs } from './hooks/useLeafletRefs'
|
||||
import { Popup } from 'leaflet'
|
||||
@ -27,6 +27,7 @@ export const Layer = ({
|
||||
markerIcon = 'circle-solid',
|
||||
markerShape = 'circle',
|
||||
markerDefaultColor = '#777',
|
||||
markerDefaultColor2,
|
||||
api,
|
||||
itemType,
|
||||
itemNameField = 'name',
|
||||
@ -44,6 +45,7 @@ export const Layer = ({
|
||||
customEditLink,
|
||||
customEditParameter,
|
||||
public_edit_items,
|
||||
listed = true,
|
||||
setItemFormPopup,
|
||||
itemFormPopup,
|
||||
clusterRef
|
||||
@ -77,10 +79,13 @@ export const Layer = ({
|
||||
|
||||
const isLayerVisible = useIsLayerVisible();
|
||||
|
||||
const isGroupTypeVisible = useIsGroupTypeVisible();
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
data && setItemsData({ data, children, name, menuIcon, menuText, menuColor, markerIcon, markerShape, markerDefaultColor, api, itemType, itemNameField, itemSubnameField, itemTextField, itemAvatarField, itemColorField, itemOwnerField, itemTagsField, itemOffersField, itemNeedsField, onlyOnePerOwner, customEditLink, customEditParameter, public_edit_items, setItemFormPopup, itemFormPopup, clusterRef });
|
||||
api && setItemsApi({ data, children, name, menuIcon, menuText, menuColor, markerIcon, markerShape, markerDefaultColor, api, itemType, itemNameField, itemSubnameField, itemTextField, itemAvatarField, itemColorField, itemOwnerField, itemTagsField, itemOffersField, itemNeedsField, onlyOnePerOwner, customEditLink, customEditParameter, public_edit_items, setItemFormPopup, itemFormPopup, clusterRef });
|
||||
data && setItemsData({ data, children, name, menuIcon, menuText, menuColor, markerIcon, markerShape, markerDefaultColor, markerDefaultColor2, api, itemType, itemNameField, itemSubnameField, itemTextField, itemAvatarField, itemColorField, itemOwnerField, itemTagsField, itemOffersField, itemNeedsField, onlyOnePerOwner, customEditLink, customEditParameter, public_edit_items, listed, setItemFormPopup, itemFormPopup, clusterRef });
|
||||
api && setItemsApi({ data, children, name, menuIcon, menuText, menuColor, markerIcon, markerShape, markerDefaultColor, markerDefaultColor2, api, itemType, itemNameField, itemSubnameField, itemTextField, itemAvatarField, itemColorField, itemOwnerField, itemTagsField, itemOffersField, itemNeedsField, onlyOnePerOwner, customEditLink, customEditParameter, public_edit_items, listed, setItemFormPopup, itemFormPopup, clusterRef });
|
||||
}, [data, api])
|
||||
|
||||
useMapEvents({
|
||||
@ -144,6 +149,7 @@ export const Layer = ({
|
||||
filter(item =>
|
||||
filterTags.length == 0 ? item : filterTags.every(tag => getItemTags(item).some(filterTag => filterTag.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase())))?.
|
||||
filter(item => item.layer && isLayerVisible(item.layer)).
|
||||
filter(item => item.group_type && isGroupTypeVisible(item.group_type)).
|
||||
map((item: Item) => {
|
||||
if (getValue(item, itemLongitudeField) && getValue(item, itemLatitudeField)) {
|
||||
|
||||
@ -178,7 +184,7 @@ export const Layer = ({
|
||||
const longitude = itemLongitudeField && item ? getValue(item, itemLongitudeField) : undefined;
|
||||
|
||||
let color1 = markerDefaultColor;
|
||||
let color2 = "RGBA(35, 31, 32, 0.2)";
|
||||
let color2 = markerDefaultColor2;
|
||||
if (itemColorField && getValue(item, itemColorField) != null) color1 = getValue(item, itemColorField);
|
||||
else if (itemTags && itemTags[0]) {
|
||||
color1 = itemTags[0].color;
|
||||
@ -197,7 +203,7 @@ export const Layer = ({
|
||||
selectPosition && setMarkerClicked(item)
|
||||
},
|
||||
}}
|
||||
icon={MarkerIconFactory(markerShape, color1, color2, markerIcon)} key={item.id} position={[latitude, longitude]}>
|
||||
icon={MarkerIconFactory(markerShape, color1, color2, item.markerIcon ? item.markerIcon : markerIcon)} key={item.id} position={[latitude, longitude]}>
|
||||
{
|
||||
(children && React.Children.toArray(children).some(child => React.isValidElement(child) && child.props.__TYPE === "ItemView") ?
|
||||
React.Children.toArray(children).map((child) =>
|
||||
|
||||
@ -11,7 +11,7 @@ export default function AddButton({ triggerAction }: { triggerAction: React.Disp
|
||||
const canAddItems = () => {
|
||||
let canAdd = false;
|
||||
layers.map(layer => {
|
||||
if (layer.api?.createItem && hasUserPermission(layer.api.collectionName!, "create", undefined, layer)) canAdd = true;
|
||||
if (layer.api?.createItem && hasUserPermission(layer.api.collectionName!, "create", undefined, layer) && layer.listed) canAdd = true;
|
||||
})
|
||||
return canAdd;
|
||||
}
|
||||
@ -28,7 +28,7 @@ export default function AddButton({ triggerAction }: { triggerAction: React.Disp
|
||||
</label>
|
||||
<ul tabIndex={0} className="tw-dropdown-content tw-pr-1 tw-list-none">
|
||||
{layers.map((layer) => (
|
||||
layer.api?.createItem && hasUserPermission(layer.api.collectionName!, "create", undefined, layer) && (
|
||||
layer.api?.createItem && hasUserPermission(layer.api.collectionName!, "create", undefined, layer) && layer.listed &&(
|
||||
<li key={layer.name} >
|
||||
<a>
|
||||
<div className="tw-tooltip tw-tooltip-left" data-tip={layer.menuText}>
|
||||
|
||||
55
src/Components/Map/Subcomponents/Controls/FilterControl.tsx
Normal file
55
src/Components/Map/Subcomponents/Controls/FilterControl.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import * as React from 'react'
|
||||
import * as L from 'leaflet'
|
||||
import { useLayers } from '../../hooks/useLayers';
|
||||
import { useAddVisibleGroupType, useIsGroupTypeVisible, useToggleVisibleGroupType } from '../../hooks/useFilter';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function FilterControl() {
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const groupTypes = [{ text: "Regional Gruppe", value: "wuerdekompass" }, { text: "Themen Gruppe", value: "themenkompass" }, { text: "liebevoll.jetzt", value: "liebevoll.jetzt" }]
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
groupTypes.map(layer =>
|
||||
addVisibleGroupType(layer.value)
|
||||
)
|
||||
}, [])
|
||||
|
||||
|
||||
const isGroupTypeVisible = useIsGroupTypeVisible();
|
||||
const toggleVisibleGroupType = useToggleVisibleGroupType();
|
||||
const addVisibleGroupType = useAddVisibleGroupType();
|
||||
|
||||
return (
|
||||
<div className="tw-card tw-bg-base-100 tw-shadow-xl tw-mt-2 tw-w-fit">
|
||||
{
|
||||
open ?
|
||||
<div className="tw-card-body tw-p-2 tw-w-fit tw-transition-all tw-duration-300">
|
||||
<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={() => {
|
||||
setOpen(false)
|
||||
}}>
|
||||
<p className='tw-text-center '>✕</p></label>
|
||||
<ul className='tw-flex-row'>
|
||||
{
|
||||
groupTypes.map(groupType =>
|
||||
<li key={groupType.value}><label htmlFor={groupType.value} className="tw-label tw-justify-normal tw-pt-1 tw-pb-1"><input id={groupType.value} onChange={() => toggleVisibleGroupType(groupType.value)} type="checkbox" className="tw-checkbox tw-checkbox-xs tw-checkbox-success" checked={isGroupTypeVisible(groupType.value)} /><span className='tw-text-sm tw-label-text tw-mx-2 tw-cursor-pointer'>{groupType.text}</span></label></li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
:
|
||||
<div className="tw-card-body hover:tw-bg-slate-300 tw-card tw-p-2 tw-h-10 tw-w-10 tw-transition-all tw-duration-300 hover:tw-cursor-pointer" onClick={() => {
|
||||
setOpen(true)
|
||||
}}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2.3} stroke="currentColor" className="size-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -10,18 +10,14 @@ export function LayerControl() {
|
||||
|
||||
const layers = useLayers();
|
||||
|
||||
useEffect(() => {
|
||||
layers.map(layer =>
|
||||
addVisibleLayer(layer)
|
||||
)
|
||||
}, [layers])
|
||||
|
||||
|
||||
const isLayerVisible = useIsLayerVisible();
|
||||
const toggleVisibleLayer = useToggleVisibleLayer();
|
||||
const addVisibleLayer = useAddVisibleLayer();
|
||||
|
||||
return (
|
||||
<div className="tw-card tw-bg-base-100 tw-shadow-xl ">
|
||||
<div className="tw-card tw-bg-base-100 tw-shadow-xl tw-mt-2 tw-w-fit">
|
||||
{
|
||||
open ?
|
||||
<div className="tw-card-body tw-p-2 tw-w-36 tw-transition-all tw-duration-300">
|
||||
@ -32,7 +28,7 @@ export function LayerControl() {
|
||||
<ul className='tw-flex-row'>
|
||||
{
|
||||
layers.map(layer =>
|
||||
<li key={layer.name}><label htmlFor={layer.name} className="tw-label tw-justify-normal tw-pt-1 tw-pb-1"><input id={layer.name} onChange={() => toggleVisibleLayer(layer)} type="checkbox" className="tw-checkbox tw-checkbox-xs tw-checkbox-success" checked={isLayerVisible(layer)} /><span className='tw-text-sm tw-label-text tw-mx-2 tw-cursor-pointer'>{layer.name}</span></label></li>
|
||||
(layer.listed && <li key={layer.name}><label htmlFor={layer.name} className="tw-label tw-justify-normal tw-pt-1 tw-pb-1"><input id={layer.name} onChange={() => toggleVisibleLayer(layer)} type="checkbox" className="tw-checkbox tw-checkbox-xs tw-checkbox-success" checked={isLayerVisible(layer)} /><span className='tw-text-sm tw-label-text tw-mx-2 tw-cursor-pointer'>{layer.name}</span></label></li>)
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
@ -45,6 +41,8 @@ export function LayerControl() {
|
||||
<path id="svg_1" fill="currentColor" d="m2.75565,11.90727l-1.03852,0.28372c-0.77718,0.38859 -0.77718,1.0138 0,1.4023l7.0156,3.5078c0.77718,0.38859 2.0275,0.38859 2.8047,0l7.0156,-3.5078c0.77718,-0.38859 0.77718,-1.0138 0,-1.4023l-0.63311,-0.48643l-4.67718,2.23624c-1.5452,0.77262 -3.31877,1.58343 -4.86407,0.81081l-5.62302,-2.84434z" />
|
||||
<path id="svg_2" strokeWidth="2" stroke="currentColor" fill="none" d="m11.247,4.30851l6.2349,3.0877c0.69083,0.34211 0.69083,0.89295 0,1.2351l-6.2349,3.0877c-0.69083,0.34211 -1.8031,0.34212 -2.494,0l-6.2349,-3.0877c-0.69083,-0.34211 -0.69083,-0.89295 0,-1.2351l6.2349,-3.0877c0.69083,-0.34211 1.8031,-0.34211 2.494,0z" />
|
||||
</svg>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ export const SearchControl = () => {
|
||||
setItemsResults(items.filter(item => {
|
||||
if (item.layer?.itemNameField) item.name = getValue(item, item.layer.itemNameField)
|
||||
if (item.layer?.itemTextField) item.text = getValue(item, item.layer.itemTextField)
|
||||
return value.length > 2 && (item.name?.toLowerCase().includes(value.toLowerCase()) || item.text?.toLowerCase().includes(value.toLowerCase()))
|
||||
return value.length > 2 && ((item.layer?.listed && item.name?.toLowerCase().includes(value.toLowerCase()) || item.text?.toLowerCase().includes(value.toLowerCase())))
|
||||
}))
|
||||
let phrase = value;
|
||||
if (value.startsWith("#")) phrase = value.substring(1);
|
||||
@ -149,7 +149,7 @@ export const SearchControl = () => {
|
||||
{Array.from(geoResults).map((geo) => (
|
||||
<div className='tw-flex tw-flex-row hover:tw-font-bold tw-cursor-pointer' key={Math.random()} onClick={() => {
|
||||
searchInput.current?.blur();
|
||||
L.marker(new LatLng(geo.geometry.coordinates[1], geo.geometry.coordinates[0]), { icon: MarkerIconFactory("circle", "#777", "RGBA(35, 31, 32, 0.2)", "circle-solid") }).addTo(map).bindPopup(`<h3 class="tw-text-base tw-font-bold">${geo?.properties.name ? geo?.properties.name : value}<h3>${capitalizeFirstLetter(geo?.properties?.osm_value)}`).openPopup().addEventListener("popupclose", (e) => { console.log(e.target.remove()) });
|
||||
L.marker(new LatLng(geo.geometry.coordinates[1], geo.geometry.coordinates[0]), { icon: MarkerIconFactory("circle", "#777", "RGBA(35, 31, 32, 0.2)", "point") }).addTo(map).bindPopup(`<h3 class="tw-text-base tw-font-bold">${geo?.properties.name ? geo?.properties.name : value}<h3>${capitalizeFirstLetter(geo?.properties?.osm_value)}`).openPopup().addEventListener("popupclose", (e) => { console.log(e.target.remove()) });
|
||||
if (geo.properties.extent) map.fitBounds(new LatLngBounds(new LatLng(geo.properties.extent[1], geo.properties.extent[0]), new LatLng(geo.properties.extent[3], geo.properties.extent[2])));
|
||||
else map.setView(new LatLng(geo.geometry.coordinates[1], geo.geometry.coordinates[0]), 15, { duration: 1 });
|
||||
hide();
|
||||
|
||||
@ -6,11 +6,12 @@ import { useAssetApi } from '../../../AppShell/hooks/useAssets'
|
||||
import DialogModal from "../../../Templates/DialogModal";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useMap } from "react-leaflet";
|
||||
import { useEffect } from "react";
|
||||
|
||||
|
||||
|
||||
|
||||
export function HeaderView({ item, api, editCallback, deleteCallback, setPositionCallback, itemNameField, itemSubnameField, itemAvatarField, loading, hideMenu = false, big = false, truncateSubname = true, hideSubname = false }: {
|
||||
export function HeaderView({ item, api, editCallback, deleteCallback, setPositionCallback, itemNameField, itemSubnameField, itemAvatarField, loading, hideMenu = false, big = false, truncateSubname = true, hideSubname = false, showAddress = false }: {
|
||||
item: Item,
|
||||
api?: ItemsApi<any>,
|
||||
editCallback?: any,
|
||||
@ -23,7 +24,8 @@ export function HeaderView({ item, api, editCallback, deleteCallback, setPositio
|
||||
hideMenu?: boolean,
|
||||
big?: boolean,
|
||||
hideSubname?: boolean,
|
||||
truncateSubname?:boolean
|
||||
truncateSubname?:boolean,
|
||||
showAddress?: boolean
|
||||
}) {
|
||||
|
||||
|
||||
@ -37,6 +39,8 @@ export function HeaderView({ item, api, editCallback, deleteCallback, setPositio
|
||||
const title = itemNameField ? getValue(item, itemNameField) : item.layer?.itemNameField && item && getValue(item, item.layer?.itemNameField);
|
||||
const subtitle = itemSubnameField ? getValue(item, itemSubnameField) : item.layer?.itemSubnameField && item && getValue(item, item.layer?.itemSubnameField);
|
||||
|
||||
const [address, setAdress] = React.useState<string>("");
|
||||
|
||||
|
||||
|
||||
const openDeleteModal = async (event: React.MouseEvent<HTMLElement>) => {
|
||||
@ -62,6 +66,9 @@ export function HeaderView({ item, api, editCallback, deleteCallback, setPositio
|
||||
<div className={`${big ? "xl:tw-text-3xl tw-text-2xl" : "tw-text-xl"} tw-font-semibold tw-truncate`}>
|
||||
{title}
|
||||
</div>
|
||||
{showAddress && address && !hideSubname && <div className={`tw-text-xs tw-text-gray-500 ${truncateSubname && "tw-truncate"}`}>
|
||||
{address}
|
||||
</div>}
|
||||
{subtitle && !hideSubname && <div className={`tw-text-xs tw-text-gray-500 ${truncateSubname && "tw-truncate"}`}>
|
||||
{subtitle}
|
||||
</div>}
|
||||
|
||||
@ -11,6 +11,6 @@ export const PopupTextInput = ({ dataField, placeholder, style, item }:
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<TextInput defaultValue={item?.name ? item.name : ""} dataField={dataField} placeholder={placeholder} inputStyle={style} type='text'></TextInput>
|
||||
<TextInput defaultValue={item?.name ? item.name : ""} dataField={dataField} placeholder={placeholder} inputStyle={style} type='text' containerStyle={'tw-mt-4 tw-mb-4'}></TextInput>
|
||||
)
|
||||
}
|
||||
|
||||
@ -9,18 +9,22 @@ import { getValue } from '../../../../Utils/GetValue';
|
||||
import remarkBreaks from 'remark-breaks';
|
||||
import { decodeTag } from '../../../../Utils/FormatTags';
|
||||
|
||||
export const TextView = ({ item, truncate = false, itemTextField }: { item?: Item, truncate?: boolean, itemTextField?: string }) => {
|
||||
export const TextView = ({ item, truncate = false, itemTextField, rawText }: { item?: Item, truncate?: boolean, itemTextField?: string, rawText?: string }) => {
|
||||
const tags = useTags();
|
||||
const addFilterTag = useAddFilterTag();
|
||||
|
||||
let text = "";
|
||||
let replacedText = "";
|
||||
|
||||
if (itemTextField && item) text = getValue(item, itemTextField);
|
||||
else text = item?.layer?.itemTextField && item ? getValue(item, item.layer?.itemTextField) : "";
|
||||
if (rawText)
|
||||
text = replacedText = rawText;
|
||||
else if (itemTextField && item)
|
||||
text = getValue(item, itemTextField);
|
||||
else
|
||||
text = item?.layer?.itemTextField && item ? getValue(item, item.layer?.itemTextField) : "";
|
||||
|
||||
if (item && text && truncate) text = truncateText(removeMarkdownKeepLinksAndParagraphs(text), 100);
|
||||
|
||||
let replacedText;
|
||||
|
||||
item && text ? replacedText = fixUrls(text) : "";
|
||||
|
||||
@ -100,7 +104,7 @@ export const TextView = ({ item, truncate = false, itemTextField }: { item?: Ite
|
||||
};
|
||||
|
||||
return (
|
||||
<Markdown className={`tw-text-map tw-leading-map `} remarkPlugins={[remarkBreaks]} components={{
|
||||
<Markdown className={`tw-text-map tw-leading-map tw-text-sm`} remarkPlugins={[remarkBreaks]} components={{
|
||||
p: CustomParagraph,
|
||||
a: ({ href, children }) => {
|
||||
const isYouTubeVideo = href?.startsWith('https://www.youtube.com/watch?v=');
|
||||
|
||||
@ -9,14 +9,17 @@ import AddButton from "./Subcomponents/AddButton";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ItemFormPopupProps } from "./Subcomponents/ItemFormPopup";
|
||||
import { SearchControl } from "./Subcomponents/Controls/SearchControl";
|
||||
import { LayerControl } from "./Subcomponents/Controls/LayerControl";
|
||||
import { QuestControl } from "./Subcomponents/Controls/QuestControl";
|
||||
// import { QuestControl } from "./Subcomponents/Controls/QuestControl";
|
||||
import { Control } from "./Subcomponents/Controls/Control";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { TagsControl } from "./Subcomponents/Controls/TagsControl";
|
||||
import { useSelectPosition, useSetMapClicked,useSetSelectPosition } from "./hooks/useSelectPosition";
|
||||
import { useClusterRef, useSetClusterRef } from "./hooks/useClusterRef";
|
||||
import { Feature, Geometry as GeoJSONGeometry } from 'geojson';
|
||||
import {FilterControl} from "./Subcomponents/Controls/FilterControl";
|
||||
import {LayerControl} from "./Subcomponents/Controls/LayerControl";
|
||||
import { useLayers } from "./hooks/useLayers";
|
||||
import { useAddVisibleLayer } from "./hooks/useFilter";
|
||||
|
||||
// for refreshing map on resize (needs to be implemented)
|
||||
const mapDivRef = React.createRef();
|
||||
@ -27,7 +30,10 @@ function UtopiaMap({
|
||||
center = [50.6, 9.5],
|
||||
zoom = 10,
|
||||
children,
|
||||
geo }
|
||||
geo,
|
||||
showFilterControl=false,
|
||||
showLayerControl = true
|
||||
}
|
||||
: UtopiaMapProps) {
|
||||
|
||||
function MapEventListener() {
|
||||
@ -62,11 +68,27 @@ function UtopiaMap({
|
||||
|
||||
const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null);
|
||||
|
||||
const [embedded, setEmbedded] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
let params = new URLSearchParams(location.search);
|
||||
let urlPosition = params.get("position");
|
||||
let embedded = params.get("embedded");
|
||||
embedded != "true" && setEmbedded(false)
|
||||
}, [location]);
|
||||
|
||||
|
||||
const layers = useLayers();
|
||||
const addVisibleLayer = useAddVisibleLayer();
|
||||
|
||||
useEffect(() => {
|
||||
layers.map(l => addVisibleLayer(l))
|
||||
|
||||
}, [layers])
|
||||
|
||||
|
||||
|
||||
|
||||
const onEachFeature = (feature: Feature<GeoJSONGeometry, any>, layer: L.Layer) => {
|
||||
if (feature.properties) {
|
||||
layer.bindPopup(feature.properties.name);
|
||||
@ -83,8 +105,12 @@ function UtopiaMap({
|
||||
<TagsControl />
|
||||
</Control>
|
||||
<Control position='bottomLeft' zIndex="999" absolute>
|
||||
<QuestControl></QuestControl>
|
||||
<LayerControl></LayerControl>
|
||||
{/*{!embedded && (*/}
|
||||
{/* <QuestControl></QuestControl>*/}
|
||||
{/*)}*/}
|
||||
{showFilterControl && <FilterControl/>}
|
||||
{/*todo: needed layer handling is located LayerControl*/}
|
||||
{showLayerControl && <LayerControl></LayerControl>}
|
||||
</Control>
|
||||
<TileLayer
|
||||
maxZoom={19}
|
||||
@ -106,7 +132,7 @@ function UtopiaMap({
|
||||
}} />}
|
||||
<MapEventListener />
|
||||
</MapContainer>
|
||||
<AddButton triggerAction={setSelectNewItemPosition}></AddButton>
|
||||
<AddButton triggerAction={setSelectNewItemPosition}></AddButton>
|
||||
{selectNewItemPosition != null &&
|
||||
<div className="tw-button tw-z-1000 tw-absolute tw-right-5 tw-top-4 tw-drop-shadow-md">
|
||||
<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={() => {
|
||||
|
||||
@ -12,6 +12,9 @@ type ActionType =
|
||||
| { type: "TOGGLE_LAYER"; layer: LayerProps }
|
||||
| { type: "ADD_LAYER"; layer: LayerProps }
|
||||
| { type: "RESET_LAYERS" }
|
||||
| { type: "TOGGLE_GROUP_TYPE"; groupType: string }
|
||||
| { type: "ADD_GROUP_TYPE"; groupType: string }
|
||||
| { type: "RESET_GROUP_TYPE" }
|
||||
;
|
||||
|
||||
type UseFilterManagerResult = ReturnType<typeof useFilterManager>;
|
||||
@ -27,7 +30,11 @@ const FilterContext = createContext<UseFilterManagerResult>({
|
||||
addVisibleLayer: () => { },
|
||||
toggleVisibleLayer: () => { },
|
||||
resetVisibleLayers: () => { },
|
||||
isLayerVisible: () => true
|
||||
isLayerVisible: () => true,
|
||||
|
||||
addVisibleGroupType: () => { },
|
||||
toggleVisibleGroupType: () => { },
|
||||
isGroupTypeVisible: () => true
|
||||
});
|
||||
|
||||
function useFilterManager(initialTags: Tag[]): {
|
||||
@ -42,6 +49,9 @@ function useFilterManager(initialTags: Tag[]): {
|
||||
toggleVisibleLayer: (layer: LayerProps) => void;
|
||||
resetVisibleLayers: () => void;
|
||||
isLayerVisible: (layer: LayerProps) => boolean;
|
||||
addVisibleGroupType: (groupType: string) => void;
|
||||
toggleVisibleGroupType: (groupType: string) => void;
|
||||
isGroupTypeVisible: (groupType: string) => boolean;
|
||||
} {
|
||||
const [filterTags, dispatchTags] = useReducer((state: Tag[], action: ActionType) => {
|
||||
switch (action.type) {
|
||||
@ -90,6 +100,29 @@ function useFilterManager(initialTags: Tag[]): {
|
||||
}
|
||||
}, initialLayers);
|
||||
|
||||
const [visibleGroupTypes, dispatchGroupTypes] = useReducer((state: string[], action: ActionType) => {
|
||||
switch (action.type) {
|
||||
case "ADD_GROUP_TYPE":
|
||||
const exist1 = state.find((groupType) =>
|
||||
groupType === action.groupType ? true : false
|
||||
);
|
||||
if (!exist1) return [
|
||||
...state,
|
||||
action.groupType,
|
||||
];
|
||||
else return state;
|
||||
case "TOGGLE_GROUP_TYPE":
|
||||
const exist2 = state.some((groupType) =>
|
||||
groupType === action.groupType);
|
||||
if(exist2) return state.filter((groupType) => groupType != action.groupType);
|
||||
else return [... state, action.groupType];
|
||||
case "RESET_GROUP_TYPE":
|
||||
return initialLayers;
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}, initialLayers);
|
||||
|
||||
const [searchPhrase, searchPhraseSet] = React.useState<string>("");
|
||||
|
||||
const addFilterTag = (tag: Tag) => {
|
||||
@ -170,11 +203,31 @@ function useFilterManager(initialTags: Tag[]): {
|
||||
return visibleLayers.some(l => l.name === layer.name)
|
||||
}, [visibleLayers]);
|
||||
|
||||
const addVisibleGroupType = (groupType: string) => {
|
||||
dispatchGroupTypes({
|
||||
type: "ADD_GROUP_TYPE",
|
||||
groupType,
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
const toggleVisibleGroupType = (groupType: string) => {
|
||||
dispatchGroupTypes({
|
||||
type: "TOGGLE_GROUP_TYPE",
|
||||
groupType,
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
const isGroupTypeVisible = useCallback((groupType: string) => {
|
||||
return visibleGroupTypes.some(gt => gt === groupType)
|
||||
}, [visibleGroupTypes]);
|
||||
|
||||
const setSearchPhrase = useCallback((phrase: string) => {
|
||||
searchPhraseSet(phrase)
|
||||
}, []);
|
||||
|
||||
return { filterTags, addFilterTag, removeFilterTag, resetFilterTags, setSearchPhrase, searchPhrase, visibleLayers, toggleVisibleLayer, resetVisibleLayers, isLayerVisible, addVisibleLayer };
|
||||
return { filterTags, addFilterTag, removeFilterTag, resetFilterTags, setSearchPhrase, searchPhrase, visibleLayers, toggleVisibleLayer, resetVisibleLayers, isLayerVisible, addVisibleLayer, addVisibleGroupType, toggleVisibleGroupType, isGroupTypeVisible };
|
||||
}
|
||||
|
||||
export const FilterProvider: React.FunctionComponent<{
|
||||
@ -239,4 +292,21 @@ export const useResetVisibleLayers = (): UseFilterManagerResult["resetVisibleLay
|
||||
export const useIsLayerVisible = (): UseFilterManagerResult["isLayerVisible"] => {
|
||||
const { isLayerVisible } = useContext(FilterContext);
|
||||
return isLayerVisible;
|
||||
};
|
||||
|
||||
export const useAddVisibleGroupType = (): UseFilterManagerResult["addVisibleGroupType"] => {
|
||||
const { addVisibleGroupType } = useContext(FilterContext);
|
||||
return addVisibleGroupType;
|
||||
};
|
||||
|
||||
|
||||
export const useToggleVisibleGroupType = (): UseFilterManagerResult["toggleVisibleGroupType"] => {
|
||||
const { toggleVisibleGroupType } = useContext(FilterContext);
|
||||
return toggleVisibleGroupType;
|
||||
};
|
||||
|
||||
|
||||
export const useIsGroupTypeVisible = (): UseFilterManagerResult["isGroupTypeVisible"] => {
|
||||
const { isGroupTypeVisible } = useContext(FilterContext);
|
||||
return isGroupTypeVisible
|
||||
};
|
||||
71
src/Components/Profile/ContactInfo.tsx
Normal file
71
src/Components/Profile/ContactInfo.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAssetApi } from "../AppShell/hooks/useAssets";
|
||||
|
||||
const ContactInfo = ({ email, telephone, name, avatar, link }: { email: string, telephone: string, name: string, avatar: string, link?: string }) => {
|
||||
const assetsApi = useAssetApi();
|
||||
|
||||
return (
|
||||
<div className="tw-bg-gray-100 tw-my-10 tw-p-6">
|
||||
<h2 className="tw-text-lg tw-font-semibold">Du hast Fragen?</h2>
|
||||
<div className="tw-mt-4 tw-flex tw-items-center">
|
||||
{avatar && (
|
||||
<ConditionalLink url={link}>
|
||||
<div className="tw-mr-5 tw-flex tw-items-center tw-justify-center">
|
||||
<div className="tw-avatar">
|
||||
<div className="tw-w-20 tw-h-20 tw-bg-gray-200 rounded-full tw-flex tw-items-center tw-justify-center overflow-hidden">
|
||||
<img src={assetsApi.url + avatar} alt={name}
|
||||
className="tw-w-full tw-h-full tw-object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ConditionalLink>
|
||||
)}
|
||||
<div className="tw-text-sm tw-flex-grow">
|
||||
<p className="tw-font-semibold">{name}</p>
|
||||
{email && (
|
||||
<p>
|
||||
<a href={`mailto:${email}`}
|
||||
className="tw-mt-2 tw-text-green-500 tw-inline-flex tw-items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
||||
className="tw-w-4 tw-h-4 tw-mr-1">
|
||||
<path
|
||||
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
||||
<polyline points="22,6 12,13 2,6"></polyline>
|
||||
</svg>
|
||||
{email}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{telephone && (
|
||||
<p>
|
||||
<a href={`tel:${telephone}`}
|
||||
className="tw-mt-2 tw-text-green-500 tw-inline-flex tw-items-center tw-whitespace-nowrap">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
||||
className="tw-w-4 tw-h-4 tw-mr-1">
|
||||
<path
|
||||
d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 16.92z" />
|
||||
</svg>
|
||||
{telephone}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContactInfo;
|
||||
|
||||
const ConditionalLink = ({ url, children }) => {
|
||||
if (url) {
|
||||
return (
|
||||
<Link to={url}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return children;
|
||||
};
|
||||
@ -7,7 +7,7 @@ import { useMap } from 'react-leaflet';
|
||||
import { LatLng } from 'leaflet';
|
||||
import { StartEndView, TextView } from '../Map';
|
||||
import { useAddTag, useTags } from '../Map/hooks/useTags';
|
||||
import { useAddFilterTag, useResetFilterTags } from '../Map/hooks/useFilter';
|
||||
import { useAddFilterTag, useResetFilterTags } from '../Map/hooks/useFilter';
|
||||
import { useHasUserPermission } from '../Map/hooks/usePermissions';
|
||||
import { hashTagRegex } from '../../Utils/HashTagRegex';
|
||||
import { randomColor } from '../../Utils/RandomColor';
|
||||
@ -22,8 +22,11 @@ import { useClusterRef } from '../Map/hooks/useClusterRef';
|
||||
import { useLeafletRefs } from '../Map/hooks/useLeafletRefs';
|
||||
import { getValue } from '../../Utils/GetValue';
|
||||
import { TagView } from '../Templates/TagView';
|
||||
import RelationCard from "./RelationCard";
|
||||
import ContactInfo from "./ContactInfo";
|
||||
import ProfileSubHeader from "./ProfileSubHeader";
|
||||
|
||||
export function OverlayItemProfile() {
|
||||
export function OverlayItemProfile({ userType }: { userType: string }) {
|
||||
|
||||
const [updatePermission, setUpdatePermission] = useState<boolean>(false);
|
||||
const [relations, setRelations] = useState<Array<Item>>([]);
|
||||
@ -65,6 +68,21 @@ export function OverlayItemProfile() {
|
||||
scroll();
|
||||
}, [addItemPopupType])
|
||||
|
||||
const [profile, setProfile] = useState<Item>();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
console.log(userType);
|
||||
|
||||
setProfile(items.find(i => (i.user_created?.id === item.user_created?.id) && i.layer?.itemType.name === userType));
|
||||
}, [item, items])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(profile);
|
||||
|
||||
}, [profile])
|
||||
|
||||
|
||||
|
||||
const updateActiveTab = (id: number) => {
|
||||
setActiveTab(id);
|
||||
@ -160,7 +178,7 @@ export function OverlayItemProfile() {
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
item && hasUserPermission("items", "update", item) && setUpdatePermission(true);
|
||||
item && hasUserPermission("items", "update", item) && setUpdatePermission(true);
|
||||
}, [item])
|
||||
|
||||
|
||||
@ -270,111 +288,163 @@ export function OverlayItemProfile() {
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
const typeMapping = {
|
||||
'default': 'Würdekompass',
|
||||
'themenkompass': 'Themenkompass-Gruppe',
|
||||
'liebevoll.jetzt': 'liebevoll.jetzt',
|
||||
};
|
||||
|
||||
let groupType = item.group_type ? item.group_type : 'default';
|
||||
let groupTypeText = typeMapping[groupType];
|
||||
|
||||
const [template, setTemplate] = useState<string>("")
|
||||
|
||||
useEffect(() => {
|
||||
setTemplate(item.layer?.itemType.template || userType);
|
||||
}, [userType, item])
|
||||
|
||||
return (
|
||||
<>
|
||||
{item &&
|
||||
<MapOverlayPage key={item.id} className={`tw-mx-4 tw-mt-4 tw-max-h-[calc(100dvh-96px)] tw-h-[calc(100dvh-96px)] md:tw-w-[calc(50%-32px)] tw-w-[calc(100%-32px)] tw-min-w-80 tw-max-w-3xl !tw-left-0 sm:!tw-left-auto tw-top-0 tw-bottom-0 tw-transition-opacity tw-duration-500 ${!selectPosition ? 'tw-opacity-100 tw-pointer-events-auto' : 'tw-opacity-0 tw-pointer-events-none'}`}>
|
||||
<MapOverlayPage key={item.id}
|
||||
className={`${template == "onepager" && '!tw-p-0'} tw-mx-4 tw-mt-4 tw-max-h-[calc(100dvh-96px)] tw-h-[calc(100dvh-96px)] md:tw-w-[calc(50%-32px)] tw-w-[calc(100%-32px)] tw-min-w-80 tw-max-w-3xl !tw-left-0 sm:!tw-left-auto tw-top-0 tw-bottom-0 tw-transition-opacity tw-duration-500 ${!selectPosition ? 'tw-opacity-100 tw-pointer-events-auto' : 'tw-opacity-0 tw-pointer-events-none'}`}>
|
||||
|
||||
<>
|
||||
<HeaderView api={item.layer?.api} item={item} deleteCallback={handleDelete} editCallback={() => navigate("/edit-item/" + item.id)} setPositionCallback={() => { map.closePopup(); setSelectPosition(item); navigate("/") }} big truncateSubname={false} />
|
||||
|
||||
<div className='tw-h-full'>
|
||||
|
||||
{item.layer?.itemType.onepager &&
|
||||
<>
|
||||
<TextView item={item} />
|
||||
<div className='tw-grid tw-grid-cols-1 sm:tw-grid-cols-2 md:tw-grid-cols-1 lg:tw-grid-cols-1 xl:tw-grid-cols-1 2xl:tw-grid-cols-2'>
|
||||
{relations && relations.map(i =>
|
||||
|
||||
|
||||
<div key={i.id} className='tw-cursor-pointer tw-card tw-bg-base-200 tw-border-[1px] tw-border-base-300 tw-card-body tw-shadow-xl tw-text-base-content tw-mx-4 tw-p-6 tw-mb-4' onClick={() => navigate('/item/' + i.id)}>
|
||||
<LinkedItemsHeaderView unlinkPermission={updatePermission} item={i} unlinkCallback={unlinkItem} loading={loading} />
|
||||
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
|
||||
<TextView truncate item={i} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{updatePermission && <ActionButton collection="items" item={item} existingRelations={relations} triggerItemSelected={linkItem} colorField={item.layer.itemColorField}></ActionButton>}
|
||||
|
||||
<div className={`${template=="onepager" && "tw-px-6 tw-pt-6"}`}>
|
||||
<HeaderView api={item.layer?.api} item={item} deleteCallback={handleDelete} editCallback={() => navigate("/edit-item/" + item.id)} setPositionCallback={() => { map.closePopup(); setSelectPosition(item); navigate("/") }} big truncateSubname={false} />
|
||||
{template=="onepager" && <ProfileSubHeader
|
||||
type={groupTypeText}
|
||||
status={item.status}
|
||||
url={window.location.href}
|
||||
title={item.name}
|
||||
/>}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
{!item.layer?.itemType.onepager &&
|
||||
<div role="tablist" className="tw-tabs tw-tabs-lifted tw-mt-2 tw-mb-2">
|
||||
<input type="radio" name="my_tabs_2" role="tab" className={`tw-tab [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]`} aria-label="Info" checked={activeTab == 1 && true} onChange={() => updateActiveTab(1)} />
|
||||
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-rounded-box tw-h-[calc(100dvh-280px)] tw-overflow-y-auto fade tw-pt-2 tw-pb-4 tw-mb-4 tw-overflow-x-hidden">
|
||||
{item.layer?.itemType.show_start_end &&
|
||||
<div className='tw-max-w-xs'><StartEndView item={item}></StartEndView></div>
|
||||
}
|
||||
<div className='tw-h-full tw-overflow-y-auto fade'>
|
||||
|
||||
{template == "onepager" &&
|
||||
<>
|
||||
{item.user_created.first_name && (
|
||||
<ContactInfo link={`/item/${profile?.id}`} name={profile?.name ? profile.name : item.user_created.first_name} avatar={profile?.image ? profile.image : item.user_created.avatar} email={item.contact} telephone={item.telephone} />
|
||||
)}
|
||||
|
||||
{/* Description Section */}
|
||||
<div className="tw-my-10 tw-mt-2 tw-px-6 tw-text-sm tw-text-gray-600">
|
||||
<TextView rawText={item.text || 'Keine Beschreibung vorhanden'} />
|
||||
</div>
|
||||
|
||||
{/* Next Appointment Section */}
|
||||
{item.next_appointment && (
|
||||
<div className="tw-my-10 tw-px-6">
|
||||
<h2 className="tw-text-lg tw-font-semibold">Nächste Termine</h2>
|
||||
<div className="tw-mt-2 tw-text-sm tw-text-gray-600">
|
||||
<TextView rawText={item.next_appointment} />
|
||||
</div>
|
||||
</div>
|
||||
)};
|
||||
|
||||
{/* Relations Section */}
|
||||
{/*{d.relations && (*/}
|
||||
{/* <div className="tw-my-10 tw-px-6">*/}
|
||||
{/* <h2 className="tw-text-lg tw-font-semibold tw-mb-4">Projekte</h2>*/}
|
||||
{/* {d.relations.map((project, index) => (*/}
|
||||
{/* <RelationCard*/}
|
||||
{/* key={index}*/}
|
||||
{/* title={project.title}*/}
|
||||
{/* description={project.description}*/}
|
||||
{/* imageSrc={project.imageSrc}*/}
|
||||
{/* />*/}
|
||||
{/* ))}*/}
|
||||
{/* </div>*/}
|
||||
{/*)}*/}
|
||||
</>
|
||||
}
|
||||
|
||||
{template == "simple" &&
|
||||
<div className='tw-mt-8'>
|
||||
<TextView item={item} />
|
||||
</div>
|
||||
}
|
||||
|
||||
{item.layer?.itemType.offers_and_needs &&
|
||||
{template == "tabs" &&
|
||||
<div role="tablist" className="tw-tabs tw-tabs-lifted tw-mt-2 tw-mb-2">
|
||||
<input type="radio" name="my_tabs_2" role="tab"
|
||||
className={`tw-tab [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]`}
|
||||
aria-label="Info" checked={activeTab == 1 && true}
|
||||
onChange={() => updateActiveTab(1)} />
|
||||
<div role="tabpanel"
|
||||
className="tw-tab-content tw-bg-base-100 tw-rounded-box tw-h-[calc(100dvh-280px)] tw-overflow-y-auto fade tw-pt-2 tw-pb-4 tw-mb-4 tw-overflow-x-hidden">
|
||||
{item.layer?.itemType.show_start_end &&
|
||||
<div className='tw-max-w-xs'><StartEndView item={item}></StartEndView></div>
|
||||
}
|
||||
<TextView item={item} />
|
||||
</div>
|
||||
|
||||
<>
|
||||
{item.layer?.itemType.offers_and_needs &&
|
||||
|
||||
<input type="radio" name="my_tabs_2" role="tab" className="tw-tab tw-min-w-[10em] [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]" aria-label="Offers & Needs" checked={activeTab == 3 && true} onChange={() => updateActiveTab(3)} />
|
||||
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-rounded-box tw-h-[calc(100dvh-268px)] tw-overflow-y-auto fade tw-pt-4 tw-pb-1" >
|
||||
<div className='tw-h-full'>
|
||||
<div className='tw-grid tw-grid-cols-1'>
|
||||
{
|
||||
offers.length > 0 ?
|
||||
<div className='tw-col-span-1'>
|
||||
<h3 className='-tw-mb-2'>Offers</h3>
|
||||
< div className='tw-flex tw-flex-wrap tw-mb-4'>
|
||||
{
|
||||
offers.map(o => <TagView key={o?.id} tag={o} onClick={() => {
|
||||
console.log(o);
|
||||
addFilterTag(o)
|
||||
}} />)
|
||||
}
|
||||
</div>
|
||||
</div> : ""
|
||||
}
|
||||
{
|
||||
needs.length > 0 ?
|
||||
<div className='tw-col-span-1'>
|
||||
<h3 className='-tw-mb-2 tw-col-span-1'>Needs</h3>
|
||||
< div className='tw-flex tw-flex-wrap tw-mb-4'>
|
||||
{
|
||||
needs.map(n => <TagView key={n?.id} tag={n} onClick={() => addFilterTag(n)} />)
|
||||
}
|
||||
</div>
|
||||
</div> : ""
|
||||
}
|
||||
<>
|
||||
|
||||
<input type="radio" name="my_tabs_2" role="tab" className="tw-tab tw-min-w-[10em] [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]" aria-label="Offers & Needs" checked={activeTab == 3 && true} onChange={() => updateActiveTab(3)} />
|
||||
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-rounded-box tw-h-[calc(100dvh-268px)] tw-overflow-y-auto fade tw-pt-4 tw-pb-1" >
|
||||
<div className='tw-h-full'>
|
||||
<div className='tw-grid tw-grid-cols-1'>
|
||||
{
|
||||
offers.length > 0 ?
|
||||
<div className='tw-col-span-1'>
|
||||
<h3 className='-tw-mb-2'>Offers</h3>
|
||||
< div className='tw-flex tw-flex-wrap tw-mb-4'>
|
||||
{
|
||||
offers.map(o => <TagView key={o?.id} tag={o} onClick={() => {
|
||||
console.log(o);
|
||||
addFilterTag(o)
|
||||
}} />)
|
||||
}
|
||||
</div>
|
||||
</div> : ""
|
||||
}
|
||||
{
|
||||
needs.length > 0 ?
|
||||
<div className='tw-col-span-1'>
|
||||
<h3 className='-tw-mb-2 tw-col-span-1'>Needs</h3>
|
||||
< div className='tw-flex tw-flex-wrap tw-mb-4'>
|
||||
{
|
||||
needs.map(n => <TagView key={n?.id} tag={n} onClick={() => addFilterTag(n)} />)
|
||||
}
|
||||
</div>
|
||||
</div> : ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
{item.layer?.itemType.relations &&
|
||||
<>
|
||||
<input type="radio" name="my_tabs_2" role="tab" className="tw-tab [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]" aria-label="Relations" checked={activeTab == 7 && true} onChange={() => updateActiveTab(7)} />
|
||||
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-rounded-box tw-h-[calc(100dvh-280px)] tw-overflow-y-auto tw-pt-4 tw-pb-1 -tw-mx-4 tw-overflow-x-hidden">
|
||||
<div className='tw-h-full'>
|
||||
<div className='tw-grid tw-grid-cols-1 sm:tw-grid-cols-2 md:tw-grid-cols-1 lg:tw-grid-cols-1 xl:tw-grid-cols-1 2xl:tw-grid-cols-2'>
|
||||
{relations && relations.map(i =>
|
||||
{item.layer?.itemType.relations &&
|
||||
<>
|
||||
<input type="radio" name="my_tabs_2" role="tab" className="tw-tab [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]" aria-label="Relations" checked={activeTab == 7 && true} onChange={() => updateActiveTab(7)} />
|
||||
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-rounded-box tw-h-[calc(100dvh-280px)] tw-overflow-y-auto tw-pt-4 tw-pb-1 -tw-mx-4 tw-overflow-x-hidden">
|
||||
<div className='tw-h-full'>
|
||||
<div className='tw-grid tw-grid-cols-1 sm:tw-grid-cols-2 md:tw-grid-cols-1 lg:tw-grid-cols-1 xl:tw-grid-cols-1 2xl:tw-grid-cols-2'>
|
||||
{relations && relations.map(i =>
|
||||
|
||||
|
||||
<div key={i.id} className='tw-cursor-pointer tw-card tw-bg-base-200 tw-border-[1px] tw-border-base-300 tw-card-body tw-shadow-xl tw-text-base-content tw-mx-4 tw-p-6 tw-mb-4' onClick={() => navigate('/item/' + i.id)}>
|
||||
<LinkedItemsHeaderView unlinkPermission={updatePermission} item={i} unlinkCallback={unlinkItem} loading={loading} />
|
||||
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
|
||||
<TextView truncate item={i} />
|
||||
<div key={i.id} className='tw-cursor-pointer tw-card tw-bg-base-200 tw-border-[1px] tw-border-base-300 tw-card-body tw-shadow-xl tw-text-base-content tw-mx-4 tw-p-6 tw-mb-4' onClick={() => navigate('/item/' + i.id)}>
|
||||
<LinkedItemsHeaderView unlinkPermission={updatePermission} item={i} unlinkCallback={unlinkItem} loading={loading} />
|
||||
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
|
||||
<TextView truncate item={i} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{updatePermission && <ActionButton collection="items" item={item} existingRelations={relations} triggerItemSelected={linkItem} colorField={item.layer.itemColorField}></ActionButton>}
|
||||
)}
|
||||
{updatePermission && <ActionButton collection="items" item={item} existingRelations={relations} triggerItemSelected={linkItem} colorField={item.layer.itemColorField}></ActionButton>}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -4,6 +4,7 @@ import { getValue } from '../../Utils/GetValue';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useAuth } from '../Auth';
|
||||
import { TextInput, TextAreaInput } from '../Input';
|
||||
import ComboBoxInput from '../Input/ComboBoxInput';
|
||||
import { ColorPicker } from './ColorPicker';
|
||||
import { hashTagRegex } from '../../Utils/HashTagRegex';
|
||||
import { useAddTag, useGetItemTags, useTags } from '../Map/hooks/useTags';
|
||||
@ -22,12 +23,29 @@ import { useHasUserPermission } from '../Map/hooks/usePermissions';
|
||||
|
||||
|
||||
|
||||
export function OverlayItemProfileSettings() {
|
||||
export function OverlayItemProfileSettings({ userType }: { userType: string }) {
|
||||
|
||||
const typeMapping = [
|
||||
{ value: 'wuerdekompass', label: 'Regional-Gruppe' },
|
||||
{ value: 'themenkompass', label: 'Themen-Gruppe' },
|
||||
{ value: 'liebevoll.jetzt', label: 'liebevoll.jetzt' }
|
||||
];
|
||||
const statusMapping = [
|
||||
{ value: 'active', label: 'aktiv' },
|
||||
{ value: 'in_planning', label: 'in Planung' },
|
||||
{ value: 'paused', label: 'pausiert' }
|
||||
];
|
||||
|
||||
const [id, setId] = useState<string>("");
|
||||
const [groupType, setGroupType] = useState<string>("");
|
||||
const [status, setStatus] = useState<string>("");
|
||||
const [name, setName] = useState<string>("");
|
||||
const [subname, setSubname] = useState<string>("");
|
||||
const [text, setText] = useState<string>("");
|
||||
const [contact, setContact] = useState<string>("");
|
||||
const [telephone, setTelephone] = useState<string>("");
|
||||
const [nextAppointment, setNextAppointment] = useState<string>("");
|
||||
const [markerIcon, setMarkerIcon] = useState<string>("");
|
||||
const [image, setImage] = useState<string>("");
|
||||
const [color, setColor] = useState<string>("");
|
||||
const [offers, setOffers] = useState<Array<Tag>>([]);
|
||||
@ -53,6 +71,32 @@ export function OverlayItemProfileSettings() {
|
||||
const hasUserPermission = useHasUserPermission();
|
||||
const getItemTags = useGetItemTags();
|
||||
|
||||
useEffect(() => {
|
||||
switch (groupType) {
|
||||
case "wuerdekompass":
|
||||
setColor(item?.layer?.menuColor || "#1A5FB4");
|
||||
setMarkerIcon("group");
|
||||
setImage("88930921-6076-4bdf-a5b2-241d6e7bc875")
|
||||
|
||||
break;
|
||||
case "themenkompass":
|
||||
setColor("#26A269");
|
||||
setMarkerIcon("group");
|
||||
setImage("88930921-6076-4bdf-a5b2-241d6e7bc875")
|
||||
|
||||
break;
|
||||
case "liebevoll.jetzt":
|
||||
setColor("#E8B620");
|
||||
setMarkerIcon("liebevoll.jetzt");
|
||||
setImage("e735b96c-507b-471c-8317-386ece0ca51d")
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [groupType])
|
||||
|
||||
|
||||
|
||||
|
||||
const items = useItems();
|
||||
@ -68,7 +112,7 @@ export function OverlayItemProfileSettings() {
|
||||
const item = items.find(i => i.id === itemId);
|
||||
item && setItem(item);
|
||||
|
||||
const layer = layers.find(l => l.itemType.name == "user")
|
||||
const layer = layers.find(l => l.itemType.name == userType)
|
||||
|
||||
!item && setItem({ id: crypto.randomUUID(), name: user ? user.first_name : "", text: "", layer: layer, new: true })
|
||||
|
||||
@ -96,10 +140,16 @@ export function OverlayItemProfileSettings() {
|
||||
setColor(item.layer?.itemColorField && getValue(item, item.layer?.itemColorField) ? getValue(item, item.layer?.itemColorField) : (getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item?.layer?.markerDefaultColor))
|
||||
|
||||
setId(item?.id ? item.id : "");
|
||||
setGroupType(item?.group_type || "wuerdekompass");
|
||||
setStatus(item?.status || "active");
|
||||
setName(item?.name ? item.name : "");
|
||||
setSubname(item?.subname ? item.subname : "");
|
||||
setText(item?.text ? item.text : "");
|
||||
setContact(item?.contact || "");
|
||||
setTelephone(item?.telephone || "");
|
||||
setNextAppointment(item?.next_appointment || "");
|
||||
setImage(item?.image ? item?.image : "");
|
||||
setMarkerIcon(item?.marker_icon ? item.marker_icon : "");
|
||||
setOffers([]);
|
||||
setNeeds([]);
|
||||
setRelations([]);
|
||||
@ -141,13 +191,24 @@ export function OverlayItemProfileSettings() {
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
changedItem = { id: id, name: name, subname: subname, text: text, color: color, position: item.position, ...image.length > 10 && { image: image }, ...offers.length > 0 && { offers: offer_updates }, ...needs.length > 0 && { needs: needs_updates } };
|
||||
// update profile item in current state
|
||||
changedItem = {
|
||||
id: id,
|
||||
group_type: groupType,
|
||||
status: status,
|
||||
name: name,
|
||||
subname: subname,
|
||||
text: text,
|
||||
color: color,
|
||||
position: item.position,
|
||||
contact: contact,
|
||||
telephone: telephone,
|
||||
...markerIcon && { markerIcon: markerIcon },
|
||||
next_appointment: nextAppointment,
|
||||
...image.length > 10 && { image: image },
|
||||
...offers.length > 0 && { offers: offer_updates },
|
||||
...needs.length > 0 && { needs: needs_updates }
|
||||
};
|
||||
|
||||
let offers_state: Array<any> = [];
|
||||
let needs_state: Array<any> = [];
|
||||
@ -253,11 +314,12 @@ export function OverlayItemProfileSettings() {
|
||||
|
||||
}
|
||||
|
||||
const [template, setTemplate] = useState<string>("")
|
||||
|
||||
useEffect(() => {
|
||||
console.log(item);
|
||||
|
||||
}, [item])
|
||||
|
||||
setTemplate(item.layer?.itemType.template || userType);
|
||||
}, [userType, item])
|
||||
|
||||
|
||||
|
||||
|
||||
@ -274,20 +336,93 @@ export function OverlayItemProfileSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{item.layer?.itemType.onepager &&
|
||||
{template == "onepager" && (
|
||||
<div className="tw-space-y-6 tw-mt-6">
|
||||
<div className="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 tw-gap-6">
|
||||
<div>
|
||||
<label htmlFor="groupType" className="tw-block tw-text-sm tw-font-medium tw-text-gray-700 tw-mb-1">
|
||||
Gruppenart:
|
||||
</label>
|
||||
<ComboBoxInput
|
||||
id="groupType"
|
||||
options={typeMapping}
|
||||
value={groupType}
|
||||
onValueChange={(v) => setGroupType(v)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="status" className="tw-block tw-text-sm tw-font-medium tw-text-gray-700 tw-mb-1">
|
||||
Gruppenstatus:
|
||||
</label>
|
||||
<ComboBoxInput
|
||||
id="status"
|
||||
options={statusMapping}
|
||||
value={status}
|
||||
onValueChange={(v) => setStatus(v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TextAreaInput placeholder="My Visino..." defaultValue={item?.text ? item.text : ""} updateFormValue={(v) => { console.log(v); setText(v) }} containerStyle='tw-h-full' inputStyle='tw-h-full tw-border-t-0 tw-rounded-tl-none' />
|
||||
<div>
|
||||
<label htmlFor="email" className="tw-block tw-text-sm tw-font-medium tw-text-gray-700 tw-mb-1">
|
||||
Email-Adresse (Kontakt):
|
||||
</label>
|
||||
<TextInput
|
||||
placeholder="Email"
|
||||
defaultValue={contact}
|
||||
updateFormValue={(v) => setContact(v)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="telephone" className="tw-block tw-text-sm tw-font-medium tw-text-gray-700 tw-mb-1">
|
||||
Telefonnummer (Kontakt):
|
||||
</label>
|
||||
<TextInput
|
||||
placeholder="Telefonnummer"
|
||||
defaultValue={telephone}
|
||||
updateFormValue={(v) => setTelephone(v)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="nextAppointment" className="tw-block tw-text-sm tw-font-medium tw-text-gray-700 tw-mb-1">
|
||||
Nächste Termine:
|
||||
</label>
|
||||
<TextAreaInput
|
||||
placeholder="Nächste Termine"
|
||||
defaultValue={nextAppointment}
|
||||
updateFormValue={(v) => setNextAppointment(v)}
|
||||
inputStyle="tw-h-24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="description" className="tw-block tw-text-sm tw-font-medium tw-text-gray-700 tw-mb-1">
|
||||
Gruppenbeschreibung:
|
||||
</label>
|
||||
<TextAreaInput
|
||||
placeholder="Beschreibung"
|
||||
defaultValue={item?.text ?? ""}
|
||||
updateFormValue={(v) => setText(v)}
|
||||
inputStyle="tw-h-48"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{template == "simple" &&
|
||||
<TextAreaInput placeholder="About me ..." defaultValue={item?.text ? item.text : ""} updateFormValue={(v) => { console.log(v); setText(v) }} containerStyle='tw-mt-8 tw-h-full' inputStyle='tw-h-full' />
|
||||
|
||||
}
|
||||
|
||||
{!item.layer?.itemType.onepager &&
|
||||
{template == "tabs" &&
|
||||
|
||||
|
||||
<div role="tablist" className="tw-tabs tw-tabs-lifted tw-mt-4">
|
||||
<input type="radio" name="my_tabs_2" role="tab" className={`tw-tab [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]`} aria-label="Info" checked={activeTab == 1 && true} onChange={() => updateActiveTab(1)} />
|
||||
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-border-[var(--fallback-bc,oklch(var(--bc)/0.2))] tw-rounded-box tw-h-[calc(100dvh-332px)] tw-min-h-56 tw-border-none">
|
||||
<TextAreaInput placeholder="My Visino..." defaultValue={item?.text ? item.text : ""} updateFormValue={(v) => { console.log(v); setText(v) }} containerStyle='tw-h-full' inputStyle='tw-h-full tw-border-t-0 tw-rounded-tl-none' />
|
||||
<TextAreaInput placeholder="About me ..." defaultValue={item?.text ? item.text : ""} updateFormValue={(v) => { console.log(v); setText(v) }} containerStyle='tw-h-full' inputStyle='tw-h-full tw-border-t-0 tw-rounded-tl-none' />
|
||||
</div>
|
||||
{item.layer?.itemType.offers_and_needs &&
|
||||
<>
|
||||
|
||||
37
src/Components/Profile/ProfileSubHeader.tsx
Normal file
37
src/Components/Profile/ProfileSubHeader.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import SocialShareBar from './SocialShareBar';
|
||||
|
||||
|
||||
const flags = {
|
||||
de: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 3" className="tw-w-5 tw-h-3">
|
||||
<rect width="5" height="3" fill="#FFCE00"/>
|
||||
<rect width="5" height="2" fill="#DD0000"/>
|
||||
<rect width="5" height="1" fill="#000000"/>
|
||||
</svg>
|
||||
),
|
||||
at: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 3" className="tw-w-5 tw-h-3">
|
||||
<rect width="5" height="3" fill="#ED2939"/>
|
||||
<rect width="5" height="2" fill="#FFFFFF"/>
|
||||
<rect width="5" height="1" fill="#ED2939"/>
|
||||
</svg>
|
||||
)
|
||||
};
|
||||
|
||||
const statusMapping = {
|
||||
'in_planning': 'in Planung',
|
||||
'paused': 'pausiert',
|
||||
};
|
||||
|
||||
const SubHeader = ({ type, status, url, title }) => (
|
||||
<div>
|
||||
<div className="tw-flex tw-items-center tw-mt-6">
|
||||
<span className="tw-text-sm tw-text-gray-600">{type}{(status && status !== 'active') ? ` (${statusMapping[status]})` : ''}</span>
|
||||
</div>
|
||||
<div className="tw-mt-4">
|
||||
<SocialShareBar url={url} title={title} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SubHeader;
|
||||
16
src/Components/Profile/RelationCard.tsx
Normal file
16
src/Components/Profile/RelationCard.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
const RelationCard = ({ title, description, imageSrc }) => (
|
||||
<div className={`tw-mb-6 ${imageSrc ? 'md:tw-flex md:tw-space-x-4' : ''}`}>
|
||||
{imageSrc && (
|
||||
<div className="md:tw-w-1/2 tw-mb-4 md:tw-mb-0">
|
||||
<img src={imageSrc} alt={title} className="tw-w-full tw-h-32 tw-object-cover" />
|
||||
</div>
|
||||
)}
|
||||
<div className={imageSrc ? 'md:tw-w-1/2' : 'tw-w-full'}>
|
||||
<h3 className="tw-text-lg tw-font-semibold">{title}</h3>
|
||||
<p className="tw-mt-2 tw-text-sm tw-text-gray-600">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default RelationCard;
|
||||
18
src/Components/Profile/SocialShareBar.tsx
Normal file
18
src/Components/Profile/SocialShareBar.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import SocialShareButton from './SocialShareButton';
|
||||
|
||||
const SocialShareBar = ({url, title, platforms = ['facebook', 'twitter', 'linkedin', 'xing', 'email']}) => {
|
||||
return (
|
||||
<div className="tw-flex tw-items-center tw-justify-end tw-space-x-2">
|
||||
{platforms.map((platform) => (
|
||||
<SocialShareButton
|
||||
key={platform}
|
||||
platform={platform}
|
||||
url={url}
|
||||
title={title}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SocialShareBar;
|
||||
80
src/Components/Profile/SocialShareButton.tsx
Normal file
80
src/Components/Profile/SocialShareButton.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import * as React from 'react';
|
||||
|
||||
const platformConfigs = {
|
||||
facebook: {
|
||||
shareUrl: 'https://www.facebook.com/sharer/sharer.php?u={url}',
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white">
|
||||
<path d="M18 2h-3a5 5 0 00-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 011-1h3z" />
|
||||
</svg>
|
||||
),
|
||||
bgColor: '#3b5998'
|
||||
},
|
||||
twitter: {
|
||||
shareUrl: 'https://twitter.com/intent/tweet?text={title}:%20{url}',
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white">
|
||||
<path d="M23 3a10.9 10.9 0 01-3.14 1.53 4.48 4.48 0 00-7.86 3v1A10.66 10.66 0 013 4s-4 9 5 13a11.64 11.64 0 01-7 2c9 5 20 0 20-11.5a4.5 4.5 0 00-.08-.83A7.72 7.72 0 0023 3z" />
|
||||
</svg>
|
||||
),
|
||||
bgColor: '#55acee'
|
||||
},
|
||||
linkedin: {
|
||||
shareUrl: 'http://www.linkedin.com/shareArticle?mini=true&url={url}&title={title}',
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white">
|
||||
<path d="M16 8a6 6 0 016 6v7h-4v-7a2 2 0 00-2-2 2 2 0 00-2 2v7h-4v-7a6 6 0 016-6zM2 9h4v12H2z" />
|
||||
<circle cx="4" cy="4" r="2" />
|
||||
</svg>
|
||||
),
|
||||
bgColor: '#4875b4'
|
||||
},
|
||||
xing: {
|
||||
shareUrl: 'https://www.xing-share.com/app/user?op=share;sc_p=xing-share;url={url}',
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white">
|
||||
<path d="M18.188 0c-.517 0-.741.325-.927.66 0 0-7.455 13.224-7.702 13.657.015.024 4.919 9.023 4.919 9.023.17.308.436.66.967.66h3.454c.211 0 .375-.078.463-.22.089-.151.089-.346-.009-.536l-4.879-8.916c-.004-.006-.004-.016 0-.022L22.139.756c.095-.191.097-.387.006-.535C22.056.078 21.894 0 21.686 0h-3.498zM3.648 4.74c-.211 0-.385.074-.473.216-.09.149-.078.339.02.531l2.34 4.05c.004.01.004.016 0 .021L1.86 16.051c-.099.188-.093.381 0 .529.085.142.239.234.45.234h3.461c.518 0 .766-.348.945-.667l3.734-6.609-2.378-4.155c-.172-.315-.434-.659-.962-.659H3.648v.016z" />
|
||||
</svg>
|
||||
),
|
||||
bgColor: '#026466'
|
||||
},
|
||||
email: {
|
||||
shareUrl: 'mailto:?subject={title}&body={url}',
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white">
|
||||
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z" />
|
||||
</svg>
|
||||
),
|
||||
bgColor: '#444444'
|
||||
}
|
||||
};
|
||||
|
||||
const SocialShareButton = ({ platform, url, title }) => {
|
||||
const config = platformConfigs[platform];
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { shareUrl, icon, bgColor } = config;
|
||||
const finalShareUrl = shareUrl
|
||||
.replace('{url}', encodeURIComponent(url))
|
||||
.replace('{title}', encodeURIComponent(title));
|
||||
|
||||
return (
|
||||
<a
|
||||
href={finalShareUrl}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='tw-w-8 tw-h-8 tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-white'
|
||||
style={{
|
||||
color: 'white',
|
||||
backgroundColor: bgColor
|
||||
}}
|
||||
>
|
||||
{React.cloneElement(icon, { className: 'tw-w-4 tw-h-4 tw-fill-current' })}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default SocialShareButton;
|
||||
9
src/Components/README.md
Normal file
9
src/Components/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
**AppShell** provides componentes to structure the overall layout of a singlepage application including Navbar and Sidebar
|
||||
|
||||
**Auth** provides the UI components for Login, Signup, Password Reset and the useAuth hook, which handls all the authentification logic and provides the user context
|
||||
|
||||
**Gaming** provides components for gamification
|
||||
|
||||
**Input**
|
||||
|
||||
**Map**
|
||||
@ -39,5 +39,4 @@ export function MapOverlayPage({ children, className, backdrop, card = true }: {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@ -108,8 +108,6 @@ export const OverlayItemsIndexPage = ({ url, layerName, parameterField, plusButt
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
|
||||
<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'>
|
||||
@ -119,55 +117,45 @@ export const OverlayItemsIndexPage = ({ url, layerName, parameterField, plusButt
|
||||
</Control>
|
||||
</div>
|
||||
<div className="tw-overflow-scroll fade tw-flex-1">
|
||||
<div className='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'>
|
||||
<div className='tw-columns-1 md:tw-columns-2 lg:tw-columns-3 2xl:tw-columns-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>
|
||||
)
|
||||
})
|
||||
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) => (
|
||||
<div key={k} className="tw-break-inside-avoid tw-mb-6">
|
||||
<ItemCard i={i} loading={loading} url={url} parameterField={parameterField} deleteCallback={() => deleteItem(i)} />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{addItemPopupType == "place" ?
|
||||
|
||||
<form ref={tabRef} autoComplete='off' onSubmit={e => submitNewItem(e)} >
|
||||
|
||||
{addItemPopupType == "place" && (
|
||||
<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>
|
||||
<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>
|
||||
}
|
||||
{layer?.itemType.show_start_end_input && <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>
|
||||
<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>
|
||||
</form> : <></>
|
||||
}
|
||||
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</MapOverlayPage>
|
||||
|
||||
|
||||
{plusButton && <PlusButton layer={layer} triggerAction={() => { setAddItemPopupType("place"); scroll(); }} color={'#777'} collection='items' />}
|
||||
|
||||
</>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
)}
|
||||
|
||||
41
src/Utils/ReverseGeocoder.ts
Normal file
41
src/Utils/ReverseGeocoder.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import axios from 'axios';
|
||||
|
||||
interface ReverseGeocodeResponse {
|
||||
place_id: number;
|
||||
licence: string;
|
||||
osm_type: string;
|
||||
osm_id: number;
|
||||
lat: string;
|
||||
lon: string;
|
||||
display_name: string;
|
||||
address: {
|
||||
road?: string;
|
||||
house_number?: string;
|
||||
city?: string;
|
||||
town?: string;
|
||||
village?: string;
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
boundingbox: string[];
|
||||
}
|
||||
|
||||
export async function reverseGeocode(lat: number, lon: number): Promise<string> {
|
||||
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}&addressdetails=1`;
|
||||
|
||||
try {
|
||||
const response = await axios.get<ReverseGeocodeResponse>(url);
|
||||
const address = response.data.address;
|
||||
|
||||
// Extrahiere Straße, Hausnummer und Ort
|
||||
const street = address.road || '';
|
||||
const houseNumber = address.house_number || '';
|
||||
const city = address.city || address.town || address.village || '';
|
||||
|
||||
// Formatiere die Adresse
|
||||
const formattedAddress = `${street} ${houseNumber}, ${city}`.trim();
|
||||
return formattedAddress || '';
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,9 @@ export interface UtopiaMapProps {
|
||||
zoom?: number,
|
||||
tags?: Tag[],
|
||||
children?: React.ReactNode,
|
||||
geo?: any
|
||||
geo?: any,
|
||||
showFilterControl?: boolean,
|
||||
showLayerControl?: boolean
|
||||
}
|
||||
|
||||
export interface LayerProps {
|
||||
@ -38,7 +40,9 @@ export interface LayerProps {
|
||||
onlyOnePerOwner?: boolean,
|
||||
customEditLink?: string,
|
||||
customEditParameter?: string,
|
||||
public_edit_items?: boolean
|
||||
public_edit_items?: boolean,
|
||||
listed?: boolean,
|
||||
item_presets?: Record<string, unknown>,
|
||||
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>,
|
||||
itemFormPopup?: ItemFormPopupProps | null,
|
||||
clusterRef?: any
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user