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
5e38215038
15
CONTRIBUTING.md
Normal file
15
CONTRIBUTING.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Contribution Guide
|
||||
|
||||
## Open ToDos
|
||||
|
||||
[Kanban Board](https://github.com/orgs/utopia-os/projects/2/)
|
||||
|
||||
## Code
|
||||
|
||||
* use named exports
|
||||
|
||||
## Layout
|
||||
|
||||
* use [heroicons](https://heroicons.com/) or alternatively [React Icons](https://react-icons.github.io/react-icons/)
|
||||
* use [Daisy UI](https://daisyui.com/) with [tailwindcss](https://tailwindcss.com/)
|
||||
* make use of the Daisy UI [theme colors](https://daisyui.com/docs/colors/)
|
||||
22
package-lock.json
generated
22
package-lock.json
generated
@ -1,12 +1,20 @@
|
||||
{
|
||||
"name": "utopia-ui",
|
||||
<<<<<<< HEAD
|
||||
"version": "3.0.0-alpha.204",
|
||||
=======
|
||||
"version": "3.0.0-alpha.215",
|
||||
>>>>>>> 8dc8779fe58040fb5c2a763d6519e57ddffc7ab7
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "utopia-ui",
|
||||
<<<<<<< HEAD
|
||||
"version": "3.0.0-alpha.204",
|
||||
=======
|
||||
"version": "3.0.0-alpha.215",
|
||||
>>>>>>> 8dc8779fe58040fb5c2a763d6519e57ddffc7ab7
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.0.17",
|
||||
@ -825,11 +833,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -2078,9 +2086,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
{
|
||||
"name": "utopia-ui",
|
||||
<<<<<<< HEAD
|
||||
"version": "3.0.0-alpha.204",
|
||||
=======
|
||||
"version": "3.0.0-alpha.215",
|
||||
>>>>>>> 8dc8779fe58040fb5c2a763d6519e57ddffc7ab7
|
||||
"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/",
|
||||
|
||||
@ -15,6 +15,8 @@ import { LayersProvider } from '../Map/hooks/useLayers'
|
||||
import { LeafletRefsProvider } from '../Map/hooks/useLeafletRefs'
|
||||
import { SelectPositionProvider } from '../Map/hooks/useSelectPosition'
|
||||
import { ClusterRefProvider } from '../Map/hooks/useClusterRef'
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
|
||||
export function AppShell({ appName, children, assetsApi, userType }: { appName: string, children: React.ReactNode, assetsApi: AssetsApi, userType: string }) {
|
||||
|
||||
|
||||
36
src/Components/AppShell/Sitemap.tsx
Normal file
36
src/Components/AppShell/Sitemap.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useItems } from '../Map/hooks/useItems';
|
||||
|
||||
export const Sitemap = ({url}:{url:string}) => {
|
||||
const [sitemap, setSitemap] = useState('');
|
||||
|
||||
const items = useItems();
|
||||
|
||||
useEffect(() => {
|
||||
if (items.length) {
|
||||
const generateSitemap = () => {
|
||||
let sitemapXML = `<?xml version="1.0" encoding="UTF-8"?>\n`;
|
||||
sitemapXML += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n`;
|
||||
|
||||
items.forEach(item => {
|
||||
sitemapXML += ` <url>\n`;
|
||||
sitemapXML += ` <loc>${url}/${item.slug}</loc>\n`;
|
||||
sitemapXML += ` </url>\n`;
|
||||
});
|
||||
|
||||
sitemapXML += `</urlset>`;
|
||||
return sitemapXML;
|
||||
};
|
||||
|
||||
setSitemap(generateSitemap());
|
||||
}
|
||||
}, [items]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Sitemap</h1>
|
||||
<textarea value={sitemap} readOnly rows={items.length + 4} cols={80} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export {AppShell} from "./AppShell"
|
||||
export {SideBar} from "./SideBar"
|
||||
export {Content} from "./Content"
|
||||
export {Content} from "./Content"
|
||||
export {Sitemap} from "./Sitemap"
|
||||
@ -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, useIsGroupTypeVisible, useIsLayerVisible } from './hooks/useFilter'
|
||||
import { useFilterTags, useIsGroupTypeVisible, useIsLayerVisible, useVisibleGroupType } from './hooks/useFilter'
|
||||
import { useAddTag, useAllTagsLoaded, useGetItemTags, useTags } from './hooks/useTags'
|
||||
import { useAddMarker, useAddPopup, useLeafletRefs } from './hooks/useLeafletRefs'
|
||||
import { Popup } from 'leaflet'
|
||||
@ -81,6 +81,8 @@ export const Layer = ({
|
||||
|
||||
const isGroupTypeVisible = useIsGroupTypeVisible();
|
||||
|
||||
const visibleGroupTypes = useVisibleGroupType();
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@ -149,7 +151,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)||item.group_type==null).
|
||||
filter(item => item.group_type && isGroupTypeVisible(item.group_type)|| visibleGroupTypes.length == 0).
|
||||
map((item: Item) => {
|
||||
if (getValue(item, itemLongitudeField) && getValue(item, itemLatitudeField)) {
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import * as React from 'react'
|
||||
import * as L from 'leaflet'
|
||||
import { useLayers } from '../../hooks/useLayers';
|
||||
import { useAddVisibleGroupType, useIsGroupTypeVisible, useToggleVisibleGroupType } from '../../hooks/useFilter';
|
||||
import { useAddVisibleGroupType, useIsGroupTypeVisible, useToggleVisibleGroupType, useVisibleGroupType } from '../../hooks/useFilter';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function FilterControl() {
|
||||
@ -21,6 +20,7 @@ export function FilterControl() {
|
||||
const isGroupTypeVisible = useIsGroupTypeVisible();
|
||||
const toggleVisibleGroupType = useToggleVisibleGroupType();
|
||||
const addVisibleGroupType = useAddVisibleGroupType();
|
||||
const visibleGroupTypes = useVisibleGroupType();
|
||||
|
||||
return (
|
||||
<div className="tw-card tw-bg-base-100 tw-shadow-xl tw-mt-2 tw-w-fit">
|
||||
@ -40,16 +40,19 @@ export function FilterControl() {
|
||||
</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 className="tw-indicator">
|
||||
{visibleGroupTypes.length < groupTypes.length && <span className="tw-indicator-item tw-badge tw-badge-success tw-h-4 tw-p-2 tw-translate-x-1/3 -tw-translate-y-1/3 tw-border-0"></span>}
|
||||
<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>
|
||||
|
||||
}
|
||||
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
@ -83,7 +83,6 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
|
||||
}
|
||||
else {
|
||||
const item = items.find(i => i.user_created?.id === user?.id && i.layer?.itemType.name === props.layer.itemType.name);
|
||||
console.log(item);
|
||||
|
||||
const uuid = crypto.randomUUID();
|
||||
let success = false;
|
||||
|
||||
@ -41,6 +41,7 @@ export function HeaderView({ item, api, editCallback, deleteCallback, setPositio
|
||||
|
||||
const [address, setAdress] = React.useState<string>("");
|
||||
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
|
||||
|
||||
const openDeleteModal = async (event: React.MouseEvent<HTMLElement>) => {
|
||||
@ -90,8 +91,8 @@ export function HeaderView({ item, api, editCallback, deleteCallback, setPositio
|
||||
</svg>
|
||||
</label>
|
||||
<ul tabIndex={0} className="tw-dropdown-content tw-menu tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-z-1000">
|
||||
{((api?.updateItem && hasUserPermission(api.collectionName!, "update", item)) || item.layer?.customEditLink) && editCallback && <li>
|
||||
<a className="!tw-text-base-content tw-cursor-pointer" onClick={(e) => item.layer?.customEditLink ? navigate(`${item.layer.customEditLink}${item.layer.customEditParameter ? "/" + getValue(item, item.layer.customEditParameter) : ""} `) : editCallback(e)}>
|
||||
{((api?.updateItem && hasUserPermission(api.collectionName!, "update", item))) && editCallback && <li>
|
||||
<a className="!tw-text-base-content tw-cursor-pointer" onClick={(e) => item.layer?.customEditLink ? navigate(`${item.layer.customEditLink}${item.layer.customEditParameter ? `/${getValue(item, item.layer.customEditParameter)}${params && "?"+params}` : ""} `) : editCallback(e)}>
|
||||
<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" />
|
||||
</svg>
|
||||
|
||||
@ -79,7 +79,7 @@ export const TextView = ({ item, truncate = false, itemTextField, rawText }: { i
|
||||
);
|
||||
const CustomImage = ({ alt, src, title }) => (
|
||||
<img
|
||||
className="max-w-full rounded-lg shadow-md"
|
||||
className="tw-max-w-full tw-rounded tw-shadow"
|
||||
src={src}
|
||||
alt={alt}
|
||||
title={title}
|
||||
|
||||
@ -23,6 +23,7 @@ const FilterContext = createContext<UseFilterManagerResult>({
|
||||
filterTags: [],
|
||||
searchPhrase: "",
|
||||
visibleLayers: [],
|
||||
visibleGroupTypes: [],
|
||||
addFilterTag: () => { },
|
||||
removeFilterTag: () => { },
|
||||
resetFilterTags: () => { },
|
||||
@ -41,6 +42,7 @@ function useFilterManager(initialTags: Tag[]): {
|
||||
filterTags: Tag[];
|
||||
searchPhrase: string;
|
||||
visibleLayers: LayerProps[];
|
||||
visibleGroupTypes: string[];
|
||||
addFilterTag: (tag: Tag) => void;
|
||||
removeFilterTag: (name: string) => void;
|
||||
resetFilterTags: () => void;
|
||||
@ -117,11 +119,11 @@ function useFilterManager(initialTags: Tag[]): {
|
||||
if(exist2) return state.filter((groupType) => groupType != action.groupType);
|
||||
else return [... state, action.groupType];
|
||||
case "RESET_GROUP_TYPE":
|
||||
return initialLayers;
|
||||
return [];
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}, initialLayers);
|
||||
},[]);
|
||||
|
||||
const [searchPhrase, searchPhraseSet] = React.useState<string>("");
|
||||
|
||||
@ -227,7 +229,7 @@ function useFilterManager(initialTags: Tag[]): {
|
||||
searchPhraseSet(phrase)
|
||||
}, []);
|
||||
|
||||
return { filterTags, addFilterTag, removeFilterTag, resetFilterTags, setSearchPhrase, searchPhrase, visibleLayers, toggleVisibleLayer, resetVisibleLayers, isLayerVisible, addVisibleLayer, addVisibleGroupType, toggleVisibleGroupType, isGroupTypeVisible };
|
||||
return { filterTags, addFilterTag, removeFilterTag, resetFilterTags, setSearchPhrase, searchPhrase, visibleLayers, toggleVisibleLayer, resetVisibleLayers, isLayerVisible, addVisibleLayer, visibleGroupTypes, addVisibleGroupType, toggleVisibleGroupType, isGroupTypeVisible };
|
||||
}
|
||||
|
||||
export const FilterProvider: React.FunctionComponent<{
|
||||
@ -309,4 +311,9 @@ export const useToggleVisibleGroupType = (): UseFilterManagerResult["toggleVisib
|
||||
export const useIsGroupTypeVisible = (): UseFilterManagerResult["isGroupTypeVisible"] => {
|
||||
const { isGroupTypeVisible } = useContext(FilterContext);
|
||||
return isGroupTypeVisible
|
||||
};
|
||||
|
||||
export const useVisibleGroupType = (): UseFilterManagerResult["visibleGroupTypes"] => {
|
||||
const { visibleGroupTypes } = useContext(FilterContext);
|
||||
return visibleGroupTypes;
|
||||
};
|
||||
@ -63,33 +63,46 @@ function usePermissionsManager(initialPermissions: Permission[]): {
|
||||
}, []);
|
||||
|
||||
const hasUserPermission = useCallback(
|
||||
(collectionName: string, action: PermissionAction, item?: Item, layer?: LayerProps) => {
|
||||
(
|
||||
collectionName: string,
|
||||
action: PermissionAction,
|
||||
item?: Item,
|
||||
layer?: LayerProps
|
||||
) => {
|
||||
const evaluateCondition = (condition: any) => {
|
||||
if (condition.user_created?._eq === "$CURRENT_USER") {
|
||||
return item?.user_created?.id === user?.id;
|
||||
}
|
||||
if (condition.public_edit?._eq === true) {
|
||||
return item?.public_edit === true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const evaluatePermissions = (permissionConditions: any) => {
|
||||
return permissionConditions._and?.every((andCondition: any) =>
|
||||
andCondition._or
|
||||
? andCondition._or.some((orCondition: any) => evaluateCondition(orCondition))
|
||||
: evaluateCondition(andCondition)
|
||||
);
|
||||
};
|
||||
|
||||
if (permissions.length === 0) return true;
|
||||
else if (user && user.role === adminRole) return true;
|
||||
else {
|
||||
return permissions.some(p =>
|
||||
p.action === action &&
|
||||
p.collection === collectionName &&
|
||||
p.role === user?.role &&
|
||||
(
|
||||
// Wenn 'item' nicht gesetzt ist, ignorieren wir die Überprüfung von 'user_created'
|
||||
!item || !p.permissions || !p.permissions._and ||
|
||||
p.permissions._and.some(condition =>
|
||||
condition.user_created &&
|
||||
condition.user_created._eq === "$CURRENT_USER" &&
|
||||
item.user_created?.id === user?.id
|
||||
)
|
||||
)
|
||||
|| ( !user && p.role == null ) &&
|
||||
(layer?.public_edit_items || item?.layer?.public_edit_items) &&
|
||||
(
|
||||
// Wenn 'item' nicht gesetzt ist, ignorieren wir die Überprüfung von 'public_edit'
|
||||
!item ||
|
||||
p.permissions?._and?.some(condition =>
|
||||
condition.public_edit &&
|
||||
condition.public_edit._eq == true &&
|
||||
item.public_edit == true
|
||||
)
|
||||
(p.role === user?.role &&
|
||||
(
|
||||
!item || !p.permissions || evaluatePermissions(p.permissions)
|
||||
)) ||
|
||||
(p.role == null &&
|
||||
(
|
||||
(layer?.public_edit_items || item?.layer?.public_edit_items) &&
|
||||
(!item || !p.permissions || evaluatePermissions(p.permissions))
|
||||
))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
export function TextEditor({ value, updateFormValue }: { value: string, updateFormValue: (string) => void }) {
|
||||
|
||||
console.log(value);
|
||||
console.log(updateFormValue);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Item } from "../../types";
|
||||
import { useItems } from "../Map/hooks/useItems";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
|
||||
export const OverlayItemProfileSettings = () => {
|
||||
|
||||
const items = useItems();
|
||||
const [item, setItem] = useState<Item>({} as Item)
|
||||
const location = useLocation();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const itemId = location.pathname.split("/")[2];
|
||||
const item = items.find(i => i.id === itemId);
|
||||
item && setItem(item);
|
||||
|
||||
}, [location, items])
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,635 +0,0 @@
|
||||
import { MapOverlayPage } from '../Templates'
|
||||
import { useAddItem, useItems, useRemoveItem, useUpdateItem } from '../Map/hooks/useItems'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Item, Tag } from '../../types';
|
||||
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 { useHasUserPermission } from '../Map/hooks/usePermissions';
|
||||
import { hashTagRegex } from '../../Utils/HashTagRegex';
|
||||
import { randomColor } from '../../Utils/RandomColor';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useAuth } from '../Auth';
|
||||
import { useLayers } from '../Map/hooks/useLayers';
|
||||
import { ActionButton } from './ActionsButton';
|
||||
import { LinkedItemsHeaderView } from './LinkedItemsHeaderView';
|
||||
import { HeaderView } from '../Map/Subcomponents/ItemPopupComponents/HeaderView';
|
||||
import { useSelectPosition, useSetSelectPosition } from '../Map/hooks/useSelectPosition';
|
||||
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";
|
||||
import { symbol } from 'prop-types';
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
|
||||
import { add, eachDayOfInterval, endOfMonth, endOfWeek, format, getDay, isSameMonth, isToday, parse, startOfToday, startOfWeek } from 'date-fns';
|
||||
|
||||
export function OverlayItemProfile({ userType }: { userType: string }) {
|
||||
|
||||
const [updatePermission, setUpdatePermission] = useState<boolean>(false);
|
||||
const [relations, setRelations] = useState<Array<Item>>([]);
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
const [addItemPopupType, setAddItemPopupType] = useState<string>("");
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [offers, setOffers] = useState<Array<Tag>>([]);
|
||||
const [needs, setNeeds] = useState<Array<Tag>>([]);
|
||||
|
||||
const location = useLocation();
|
||||
const items = useItems();
|
||||
const updateItem = useUpdateItem();
|
||||
const [item, setItem] = useState<Item>({} as Item)
|
||||
const map = useMap();
|
||||
const layers = useLayers();
|
||||
const selectPosition = useSelectPosition();
|
||||
const removeItem = useRemoveItem();
|
||||
const tags = useTags();
|
||||
const navigate = useNavigate();
|
||||
const addTag = useAddTag();
|
||||
const resetFilterTags = useResetFilterTags();
|
||||
const addItem = useAddItem();
|
||||
const { user } = useAuth();
|
||||
const hasUserPermission = useHasUserPermission();
|
||||
const setSelectPosition = useSetSelectPosition();
|
||||
const clusterRef = useClusterRef();
|
||||
const leafletRefs = useLeafletRefs();
|
||||
const addFilterTag = useAddFilterTag();
|
||||
|
||||
|
||||
|
||||
const tabRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
function scroll() {
|
||||
tabRef.current?.scrollIntoView();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
scroll();
|
||||
}, [addItemPopupType])
|
||||
|
||||
const [profile, setProfile] = useState<Item>();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setProfile(items.find(i => (i.user_created?.id === item.user_created?.id) && i.layer?.itemType.name === userType));
|
||||
}, [item, items])
|
||||
|
||||
|
||||
|
||||
|
||||
const updateActiveTab = (id: number) => {
|
||||
setActiveTab(id);
|
||||
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
let urlTab = params.get("tab");
|
||||
if (!urlTab?.includes(id.toString()))
|
||||
params.set("tab", `${id ? id : ""}`)
|
||||
window.history.pushState('', '', "?" + params.toString());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const itemId = location.pathname.split("/")[2];
|
||||
const item = items.find(i => i.id === itemId);
|
||||
item && setItem(item);
|
||||
}, [items, location])
|
||||
|
||||
useEffect(() => {
|
||||
setOffers([]);
|
||||
setNeeds([]);
|
||||
setRelations([]);
|
||||
|
||||
item.layer?.itemOffersField && getValue(item, item.layer.itemOffersField)?.map(o => {
|
||||
const tag = tags.find(t => t.id === o.tags_id);
|
||||
tag && setOffers(current => [...current, tag])
|
||||
})
|
||||
item.layer?.itemNeedsField && getValue(item, item.layer.itemNeedsField)?.map(n => {
|
||||
const tag = tags.find(t => t.id === n.tags_id);
|
||||
tag && setNeeds(current => [...current, tag])
|
||||
})
|
||||
item.relations?.map(r => {
|
||||
const item = items.find(i => i.id == r.related_items_id)
|
||||
item && setRelations(current => [...current, item])
|
||||
})
|
||||
|
||||
}, [item, items])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const setMap = async (marker, x) => {
|
||||
await map.setView(new LatLng(item?.position?.coordinates[1]!, item?.position?.coordinates[0]! + x / 4), undefined);
|
||||
setTimeout(() => {
|
||||
marker.openPopup();
|
||||
}, 500);
|
||||
}
|
||||
if (item) {
|
||||
if (item.position) {
|
||||
const marker = Object.entries(leafletRefs).find(r => r[1].item == item)?.[1].marker;
|
||||
marker && clusterRef.hasLayer(marker) && clusterRef?.zoomToShowLayer(marker, () => {
|
||||
const bounds = map.getBounds();
|
||||
const x = bounds.getEast() - bounds.getWest();
|
||||
setMap(marker, x);
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
const parent = getFirstAncestor(item);
|
||||
const marker = Object.entries(leafletRefs).find(r => r[1].item == parent)?.[1].marker;
|
||||
marker && clusterRef.hasLayer(marker) && clusterRef?.zoomToShowLayer(marker, () => {
|
||||
const bounds = map.getBounds();
|
||||
const x = bounds.getEast() - bounds.getWest();
|
||||
setMap(marker, x);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [item])
|
||||
|
||||
|
||||
const getFirstAncestor = (item: Item): Item | undefined => {
|
||||
const parent = items.find(i => i.id === item.parent);
|
||||
if (parent?.parent) {
|
||||
return getFirstAncestor(parent);
|
||||
} else {
|
||||
return parent;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let params = new URLSearchParams(location.search);
|
||||
let urlTab = params.get("tab");
|
||||
urlTab ? setActiveTab(Number(urlTab)) : setActiveTab(1);
|
||||
}, [location])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
item && hasUserPermission("items", "update", item) && setUpdatePermission(true);
|
||||
}, [item])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
selectPosition && map.closePopup();
|
||||
}, [selectPosition])
|
||||
|
||||
|
||||
|
||||
const submitNewItem = async (evt: any, type: string) => {
|
||||
evt.preventDefault();
|
||||
const formItem: Item = {} as Item;
|
||||
Array.from(evt.target).forEach((input: HTMLInputElement) => {
|
||||
if (input.name) {
|
||||
formItem[input.name] = input.value;
|
||||
}
|
||||
});
|
||||
setLoading(true);
|
||||
formItem.text && formItem.text.toLocaleLowerCase().match(hashTagRegex)?.map(tag => {
|
||||
if (!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) {
|
||||
addTag({ id: crypto.randomUUID(), name: tag.slice(1), color: randomColor() })
|
||||
}
|
||||
});
|
||||
const uuid = crypto.randomUUID();
|
||||
|
||||
const layer = layers.find(l => l.name.toLocaleLowerCase().replace("s", "") == addItemPopupType.toLocaleLowerCase())
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
await layer?.api?.createItem!({ ...formItem, id: uuid, type: type, parent: item.id });
|
||||
await linkItem(uuid);
|
||||
success = true;
|
||||
} catch (error) {
|
||||
toast.error(error.toString());
|
||||
}
|
||||
if (success) {
|
||||
addItem({ ...formItem, id: uuid, type: type, layer: layer, user_created: user, parent: item.id });
|
||||
toast.success("New item created");
|
||||
resetFilterTags();
|
||||
}
|
||||
setLoading(false);
|
||||
setAddItemPopupType("");
|
||||
}
|
||||
|
||||
const linkItem = async (id: string) => {
|
||||
let new_relations = item.relations || [];
|
||||
new_relations?.push({ items_id: item.id, related_items_id: id })
|
||||
const updatedItem = { id: item.id, relations: new_relations }
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
await item?.layer?.api?.updateItem!(updatedItem)
|
||||
success = true;
|
||||
} catch (error) {
|
||||
toast.error(error.toString());
|
||||
}
|
||||
if (success) {
|
||||
updateItem({ ...item, relations: new_relations })
|
||||
toast.success("Item linked");
|
||||
}
|
||||
}
|
||||
|
||||
const unlinkItem = async (id: string) => {
|
||||
console.log(id);
|
||||
|
||||
let new_relations = item.relations?.filter(r => r.related_items_id !== id)
|
||||
console.log(new_relations);
|
||||
|
||||
const updatedItem = { id: item.id, relations: new_relations }
|
||||
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
await item?.layer?.api?.updateItem!(updatedItem)
|
||||
success = true;
|
||||
} catch (error) {
|
||||
toast.error(error.toString());
|
||||
}
|
||||
if (success) {
|
||||
updateItem({ ...item, relations: new_relations })
|
||||
toast.success("Item unlinked");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const handleDelete = async (event: React.MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
setLoading(true);
|
||||
let success = false;
|
||||
try {
|
||||
await item.layer?.api?.deleteItem!(item.id)
|
||||
success = true;
|
||||
} catch (error) {
|
||||
toast.error(error.toString());
|
||||
}
|
||||
if (success) {
|
||||
removeItem(item);
|
||||
toast.success("Item deleted");
|
||||
}
|
||||
setLoading(false);
|
||||
map.closePopup();
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
window.history.pushState({}, "", "/" + `${params ? `?${params}` : ""}`);
|
||||
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])
|
||||
|
||||
const attestations = [{
|
||||
from: "Timo",
|
||||
avatar: "https://api.utopia-lab.org/assets/262117f8-feb6-444f-9bd2-e84087285760?width=80&heigth=80",
|
||||
symbol: "🥇",
|
||||
text: "1. Platz im Bogenschießen",
|
||||
date: "21.06.2024",
|
||||
},
|
||||
{
|
||||
from: "Sebastian",
|
||||
avatar: "https://api.utopia-lab.org/assets/7510a082-882b-41c3-aa7d-5a19f9502f25?width=80&heigth=80",
|
||||
symbol: "🌱",
|
||||
text: "danke fürs Rasen mähen",
|
||||
date: "29.06.2024",
|
||||
},
|
||||
{
|
||||
from: "Yurij",
|
||||
avatar: "https://api.utopia-lab.org/assets/abe62291-35ad-45de-b978-e5906d8a3eb6?width=80&heigth=80",
|
||||
symbol: "🏆",
|
||||
text: "bester Coder ever",
|
||||
date: "04.07.2024",
|
||||
},
|
||||
{
|
||||
from: "Luca",
|
||||
avatar: "https://api.utopia-lab.org/assets/e285e653-36e8-4211-a69d-00053c1f610e?width=80&heigth=80",
|
||||
symbol: "🙏",
|
||||
text: "Vielen Dank für deine Hilfe!!!",
|
||||
date: "04.07.2024",
|
||||
},
|
||||
{
|
||||
from: "Lisa",
|
||||
avatar: "https://i.pinimg.com/originals/c0/ed/08/c0ed088cd6532d4fd27396aefddac57c.jpg",
|
||||
symbol: "❤️",
|
||||
text: "Vielen Dank für deine Hilfe!!!",
|
||||
date: "04.07.2024",
|
||||
},
|
||||
{
|
||||
from: "Timo",
|
||||
avatar: "https://api.utopia-lab.org/assets/262117f8-feb6-444f-9bd2-e84087285760?width=80&heigth=80",
|
||||
symbol: "🥈",
|
||||
text: "2. Platz im Bogenschießen",
|
||||
date: "21.06.2024",
|
||||
},
|
||||
{
|
||||
from: "Anton",
|
||||
avatar: "https://api.utopia-lab.org/assets/007dc678-6073-4ad1-9b47-f2cfe1dca582?width=80&heigth=80",
|
||||
symbol: "🌱",
|
||||
text: "danke fürs Rasen mähen",
|
||||
date: "29.06.2024"
|
||||
},
|
||||
]
|
||||
|
||||
const today = startOfToday();
|
||||
const days = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
||||
const colStartClasses = [
|
||||
"",
|
||||
"col-start-2",
|
||||
"col-start-3",
|
||||
"col-start-4",
|
||||
"col-start-5",
|
||||
"col-start-6",
|
||||
"col-start-7",
|
||||
];
|
||||
|
||||
const [currMonth, setCurrMonth] = useState(() => format(today, "MMM-yyyy"));
|
||||
let firstDayOfMonth = parse(currMonth, "MMM-yyyy", new Date());
|
||||
|
||||
const daysInMonth = eachDayOfInterval({
|
||||
start: startOfWeek(firstDayOfMonth),
|
||||
end: endOfWeek(endOfMonth(firstDayOfMonth)),
|
||||
});
|
||||
|
||||
const getPrevMonth = (event: React.MouseEvent<SVGSVGElement>) => {
|
||||
event.preventDefault();
|
||||
const firstDayOfPrevMonth = add(firstDayOfMonth, { months: -1 });
|
||||
setCurrMonth(format(firstDayOfPrevMonth, "MMM-yyyy"));
|
||||
};
|
||||
|
||||
const getNextMonth = (event: React.MouseEvent<SVGSVGElement>) => {
|
||||
event.preventDefault();
|
||||
const firstDayOfNextMonth = add(firstDayOfMonth, { months: 1 });
|
||||
setCurrMonth(format(firstDayOfNextMonth, "MMM-yyyy"));
|
||||
};
|
||||
|
||||
const capitalizeFirstLetter = (string: string) => {
|
||||
return string
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{item &&
|
||||
<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'}`}>
|
||||
|
||||
<>
|
||||
<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>
|
||||
|
||||
|
||||
{template == "onepager" &&
|
||||
<div className='tw-h-full tw-overflow-y-auto fade'>
|
||||
|
||||
{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>*/}
|
||||
{/*)}*/}
|
||||
</div>
|
||||
}
|
||||
|
||||
{template == "simple" &&
|
||||
<div className='tw-mt-8 tw-h-full tw-overflow-y-auto fade'>
|
||||
|
||||
|
||||
<TextView item={item} />
|
||||
</div>
|
||||
}
|
||||
|
||||
{template == "tabs" &&
|
||||
<div role="tablist" className="tw-tabs tw-tabs-lifted tw-mt-2 tw-mb-2">
|
||||
{item.layer?.itemType.text &&
|
||||
<>
|
||||
<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="👤" 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.questlog &&
|
||||
<>
|
||||
<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="❤️" checked={activeTab == 2 && true}
|
||||
onChange={() => updateActiveTab(2)} />
|
||||
|
||||
<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">
|
||||
<table className="sm:tw-table-sm md:tw-table-md">
|
||||
<tbody>
|
||||
{attestations.map((a, i) => <tr key={i}>
|
||||
<td>
|
||||
<div className='tw-mask tw-mask-circle tw-text-xl md:tw-text-2xl tw-bg-slate-200 tw-rounded-full tw-p-2 tw-my-1 tw-mr-2'>{a.symbol}</div>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<div className='tw-mr-2' ><i>{a.text}</i></div>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="tw-avatar">
|
||||
<div className="tw-mask tw-rounded-full h-8 w-8 tw-mr-2">
|
||||
<img
|
||||
src={a.avatar}
|
||||
alt="Avatar Tailwind CSS Component" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold">{a.from}</div>
|
||||
<div className="tw-text-xs opacity-50 tw-text-zinc-500">{a.date}</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{item.layer?.itemType.text &&
|
||||
<>
|
||||
<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="🗓" checked={activeTab == 4 && true}
|
||||
onChange={() => updateActiveTab(4)} />
|
||||
<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 tw-mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-semibold text-xl">
|
||||
{format(firstDayOfMonth, "MMMM yyyy")}
|
||||
</p>
|
||||
<div className="flex items-center justify-evenly gap-6 sm:gap-12">
|
||||
<ChevronLeftIcon
|
||||
className="w-6 h-6 cursor-pointer"
|
||||
onClick={getPrevMonth}
|
||||
/>
|
||||
<ChevronRightIcon
|
||||
className="w-6 h-6 cursor-pointer"
|
||||
onClick={getNextMonth}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="my-6" />
|
||||
<div className="grid grid-cols-7 gap-6 sm:gap-12 place-items-center">
|
||||
{days.map((day, idx) => {
|
||||
return (
|
||||
<div key={idx} className="font-semibold">
|
||||
{capitalizeFirstLetter(day)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-4 sm:gap-12 mt-8 place-items-center">
|
||||
{daysInMonth.map((day, idx) => {
|
||||
return (
|
||||
<div key={idx} className={colStartClasses[getDay(day)]}>
|
||||
<p
|
||||
className={`cursor-pointer flex items-center justify-center font-semibold h-8 w-8 rounded-full hover:text-white ${isSameMonth(day, today) ? "text-current" : "text-gray-500"
|
||||
} ${!isToday(day) && "hover:bg-primary-content"} ${isToday(day) && "bg-primary !text-white"
|
||||
}`}
|
||||
>
|
||||
{format(day, "d")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
{item.layer?.itemType.offers_and_needs &&
|
||||
|
||||
<>
|
||||
|
||||
<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="🧩" 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={() => {
|
||||
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>
|
||||
</>
|
||||
|
||||
|
||||
}
|
||||
|
||||
{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="🔗" 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-mr-4 -tw-mb-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 tw-pb-4'>
|
||||
{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-p-6 tw-mr-4 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>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
|
||||
</MapOverlayPage >
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,481 +0,0 @@
|
||||
import { useItems, useUpdateItem, useAddItem } from '../Map/hooks/useItems'
|
||||
import { useEffect, useState } from 'react';
|
||||
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';
|
||||
import { randomColor } from '../../Utils/RandomColor';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Item, Tag } from '../../types';
|
||||
import { MapOverlayPage } from '../Templates';
|
||||
import { AvatarWidget } from './AvatarWidget';
|
||||
import { encodeTag } from '../../Utils/FormatTags';
|
||||
import { useLayers } from '../Map/hooks/useLayers';
|
||||
import { TagsWidget } from './TagsWidget';
|
||||
import { LinkedItemsHeaderView } from './LinkedItemsHeaderView';
|
||||
import { TextView } from '../Map';
|
||||
import { ActionButton } from './ActionsButton';
|
||||
import { useHasUserPermission } from '../Map/hooks/usePermissions';
|
||||
|
||||
|
||||
|
||||
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>>([]);
|
||||
const [needs, setNeeds] = useState<Array<Tag>>([]);
|
||||
const [relations, setRelations] = useState<Array<Item>>([]);
|
||||
|
||||
const [updatePermission, setUpdatePermission] = useState<boolean>(false);
|
||||
|
||||
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const { user } = useAuth();
|
||||
|
||||
const updateItem = useUpdateItem();
|
||||
const addItem = useAddItem();
|
||||
const layers = useLayers();
|
||||
const location = useLocation();
|
||||
|
||||
|
||||
const tags = useTags();
|
||||
const addTag = useAddTag();
|
||||
const navigate = useNavigate();
|
||||
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();
|
||||
const [item, setItem] = useState<Item>({} as Item)
|
||||
|
||||
useEffect(() => {
|
||||
item && hasUserPermission("items", "update", item) && setUpdatePermission(true);
|
||||
}, [item])
|
||||
|
||||
useEffect(() => {
|
||||
const itemId = location.pathname.split("/")[2];
|
||||
|
||||
const item = items.find(i => i.id === itemId);
|
||||
item && setItem(item);
|
||||
|
||||
const layer = layers.find(l => l.itemType.name == userType)
|
||||
|
||||
!item && setItem({ id: crypto.randomUUID(), name: user ? user.first_name : "", text: "", layer: layer, new: true })
|
||||
|
||||
}, [items])
|
||||
|
||||
const updateActiveTab = (id: number) => {
|
||||
setActiveTab(id);
|
||||
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
let urlTab = params.get("tab");
|
||||
if (!urlTab?.includes(id.toString()))
|
||||
params.set("tab", `${id ? id : ""}`)
|
||||
window.history.pushState('', '', "?" + params.toString());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let params = new URLSearchParams(location.search);
|
||||
let urlTab = params.get("tab");
|
||||
urlTab ? setActiveTab(Number(urlTab)) : setActiveTab(1);
|
||||
}, [location])
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
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([]);
|
||||
item?.offers?.map(o => {
|
||||
const offer = tags?.find(t => t.id === o.tags_id);
|
||||
offer && setOffers(current => [...current, offer])
|
||||
})
|
||||
item?.needs?.map(o => {
|
||||
const need = tags?.find(t => t.id === o.tags_id);
|
||||
need && setNeeds(current => [...current, need])
|
||||
})
|
||||
item.relations?.map(r => {
|
||||
const item = items.find(i => i.id == r.related_items_id)
|
||||
item && setRelations(current => [...current, item])
|
||||
})
|
||||
|
||||
}, [item])
|
||||
|
||||
|
||||
const onUpdateItem = async () => {
|
||||
let changedItem = {} as Item;
|
||||
|
||||
let offer_updates: Array<any> = [];
|
||||
//check for new offers
|
||||
offers?.map(o => {
|
||||
const existingOffer = item?.offers?.find(t => t.tags_id === o.id)
|
||||
existingOffer && offer_updates.push(existingOffer.id)
|
||||
if (!existingOffer && !tags.some(t => t.id === o.id)) addTag({ ...o, offer_or_need: true })
|
||||
!existingOffer && offer_updates.push({ items_id: item?.id, tags_id: o.id })
|
||||
});
|
||||
|
||||
let needs_updates: Array<any> = [];
|
||||
|
||||
needs?.map(n => {
|
||||
const existingNeed = item?.needs?.find(t => t.tags_id === n.id)
|
||||
existingNeed && needs_updates.push(existingNeed.id)
|
||||
!existingNeed && needs_updates.push({ items_id: item?.id, tags_id: n.id })
|
||||
!existingNeed && !tags.some(t => t.id === n.id) && addTag({ ...n, offer_or_need: true })
|
||||
});
|
||||
|
||||
|
||||
// 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> = [];
|
||||
|
||||
await offers.map(o => {
|
||||
offers_state.push({ items_id: item?.id, tags_id: o.id })
|
||||
});
|
||||
|
||||
await needs.map(n => {
|
||||
needs_state.push({ items_id: item?.id, tags_id: n.id })
|
||||
});
|
||||
|
||||
changedItem = { ...changedItem, offers: offers_state, needs: needs_state };
|
||||
|
||||
|
||||
text.toLocaleLowerCase().match(hashTagRegex)?.map(tag => {
|
||||
if (!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) {
|
||||
addTag({ id: crypto.randomUUID(), name: encodeTag(tag.slice(1).toLocaleLowerCase()), color: randomColor() })
|
||||
}
|
||||
});
|
||||
|
||||
setLoading(true);
|
||||
console.log(item.layer);
|
||||
|
||||
|
||||
if (!item.new) {
|
||||
item?.layer?.api?.updateItem && toast.promise(
|
||||
item?.layer?.api?.updateItem(changedItem),
|
||||
{
|
||||
pending: 'updating Item ...',
|
||||
success: 'Item updated',
|
||||
error: {
|
||||
render({ data }) {
|
||||
return `${data}`
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => item && updateItem({ ...item, ...changedItem }))
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
navigate("/item/" + item.id)
|
||||
});
|
||||
|
||||
}
|
||||
else {
|
||||
item.layer?.api?.createItem && toast.promise(
|
||||
item.layer?.api?.createItem(changedItem),
|
||||
{
|
||||
pending: 'updating Item ...',
|
||||
success: 'Item updated',
|
||||
error: {
|
||||
render({ data }) {
|
||||
return `${data}`
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => item && addItem({ ...item, ...changedItem, layer: item.layer, user_created: user, type: item.layer?.itemType }))
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
navigate("/")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const linkItem = async (id: string) => {
|
||||
let new_relations = item.relations || [];
|
||||
new_relations?.push({ items_id: item.id, related_items_id: id })
|
||||
const updatedItem = { id: item.id, relations: new_relations }
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
await item?.layer?.api?.updateItem!(updatedItem)
|
||||
success = true;
|
||||
} catch (error) {
|
||||
toast.error(error.toString());
|
||||
}
|
||||
if (success) {
|
||||
updateItem({ ...item, relations: new_relations })
|
||||
toast.success("Item linked");
|
||||
}
|
||||
}
|
||||
|
||||
const unlinkItem = async (id: string) => {
|
||||
console.log(id);
|
||||
|
||||
let new_relations = item.relations?.filter(r => r.related_items_id !== id)
|
||||
console.log(new_relations);
|
||||
|
||||
const updatedItem = { id: item.id, relations: new_relations }
|
||||
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
await item?.layer?.api?.updateItem!(updatedItem)
|
||||
success = true;
|
||||
} catch (error) {
|
||||
toast.error(error.toString());
|
||||
}
|
||||
if (success) {
|
||||
updateItem({ ...item, relations: new_relations })
|
||||
toast.success("Item unlinked");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const [template, setTemplate] = useState<string>("")
|
||||
|
||||
useEffect(() => {
|
||||
setTemplate(item.layer?.itemType.template || userType);
|
||||
}, [userType, item])
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<MapOverlayPage backdrop className='tw-mx-4 tw-mt-4 tw-mb-12 tw-overflow-x-hidden tw-max-h-[calc(100dvh-96px)] !tw-h-[calc(100dvh-96px)] tw-w-[calc(100%-32px)] md:tw-w-[calc(50%-32px)] tw-max-w-3xl !tw-left-auto tw-top-0 tw-bottom-0'>
|
||||
<div className='tw-flex tw-flex-col tw-h-full'>
|
||||
<div className="tw-flex">
|
||||
<AvatarWidget avatar={image} setAvatar={setImage} />
|
||||
<ColorPicker color={color} onChange={setColor} className={"-tw-left-6 tw-top-14 -tw-mr-6"} />
|
||||
<div className='tw-grow tw-mr-4'>
|
||||
<TextInput placeholder="Name" defaultValue={item?.name ? item.name : ""} updateFormValue={(v) => setName(v)} containerStyle='tw-grow tw-input-md' />
|
||||
<TextInput placeholder="Subtitle" defaultValue={item?.subname ? item.subname : ""} updateFormValue={(v) => setSubname(v)} containerStyle='tw-grow tw-input-sm tw-px-4 tw-mt-1' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
|
||||
<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' />
|
||||
|
||||
}
|
||||
|
||||
{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="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 &&
|
||||
<>
|
||||
<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-border-[var(--fallback-bc,oklch(var(--bc)/0.2))] tw-rounded-box tw-h-[calc(100dvh-332px)] tw-min-h-56 tw-border-none">
|
||||
<div className='tw-h-full'>
|
||||
<div className='tw-w-full tw-h-[calc(50%-0.75em)] tw-mb-4'>
|
||||
<TagsWidget defaultTags={offers} onUpdate={(v) => setOffers(v)} placeholder="enter your offers" containerStyle='tw-bg-transparent tw-w-full tw-h-full tw-mt-3 tw-text-xs tw-h-[calc(100%-1rem)] tw-min-h-[5em] tw-pb-2 tw-overflow-auto' />
|
||||
</div>
|
||||
<div className='tw-w-full tw-h-[calc(50%-0.75em)] '>
|
||||
<TagsWidget defaultTags={needs} onUpdate={(v) => setNeeds(v)} placeholder="enter your needs" containerStyle='tw-bg-transparent tw-w-full tw-h-full tw-mt-3 tw-text-xs tw-h-[calc(100%-1rem)] tw-min-h-[5em] tw-pb-2 tw-overflow-auto' />
|
||||
</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-332px)] 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>
|
||||
</div>
|
||||
)}
|
||||
{updatePermission && <ActionButton customStyle="!tw-bottom-20" collection="items" item={item} existingRelations={relations} triggerItemSelected={linkItem} colorField={item.layer.itemColorField}></ActionButton>}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
<div className="tw-mt-4 tw-mb-4"><button className={loading ? " tw-loading tw-btn tw-float-right" : "tw-btn tw-float-right"} onClick={() => onUpdateItem()} style={true ? { backgroundColor: `${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)}`, color: "#fff" } : { color: "#fff" }}>Update</button></div>
|
||||
|
||||
</div>
|
||||
|
||||
</MapOverlayPage>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,147 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { MapOverlayPage } from '../Templates'
|
||||
import { useItems } from '../Map/hooks/useItems'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Item, Tag, UserItem } from '../../types';
|
||||
import { getValue } from '../../Utils/GetValue';
|
||||
import { useMap } from 'react-leaflet';
|
||||
import { LatLng } from 'leaflet';
|
||||
import { TextView } from '../Map';
|
||||
import useWindowDimensions from '../Map/hooks/useWindowDimension';
|
||||
import { TagView } from '../Templates/TagView';
|
||||
import { useTags } from '../Map/hooks/useTags';
|
||||
import { useAuth } from '../Auth';
|
||||
import { useAddFilterTag } from '../Map/hooks/useFilter';
|
||||
|
||||
export function OverlayProfile() {
|
||||
|
||||
const location = useLocation();
|
||||
const items = useItems();
|
||||
const [item, setItem] = useState<Item>({} as Item)
|
||||
const map = useMap();
|
||||
const windowDimension = useWindowDimensions();
|
||||
|
||||
const tags = useTags();
|
||||
const { user } = useAuth();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [owner, setOwner] = useState<UserItem>();
|
||||
const [offers, setOffers] = useState<Array<Tag>>([]);
|
||||
const [needs, setNeeds] = useState<Array<Tag>>([]);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
|
||||
|
||||
const addFilterTag = useAddFilterTag();
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const itemId = location.pathname.split("/")[2];
|
||||
const item = items.find(i => i.id === itemId);
|
||||
item && setItem(item);
|
||||
|
||||
const bounds = map.getBounds();
|
||||
const x = bounds.getEast() - bounds.getWest()
|
||||
if (windowDimension.width > 768)
|
||||
if (item?.position?.coordinates[0])
|
||||
map.setView(new LatLng(item?.position.coordinates[1]!, item?.position.coordinates[0]! + x / 4))
|
||||
}, [location, items, activeTab])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setOffers([]);
|
||||
setNeeds([]);
|
||||
setOwner(undefined);
|
||||
item?.layer?.itemOwnerField && setOwner(getValue(item, item.layer?.itemOwnerField));
|
||||
item.layer?.itemOffersField && getValue(item, item.layer.itemOffersField).map(o => {
|
||||
const tag = tags.find(t => t.id === o.tags_id);
|
||||
tag && setOffers(current => [...current, tag])
|
||||
})
|
||||
item.layer?.itemNeedsField && getValue(item, item.layer.itemNeedsField).map(n => {
|
||||
const tag = tags.find(t => t.id === n.tags_id);
|
||||
tag && setNeeds(current => [...current, tag])
|
||||
})
|
||||
}, [item])
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<MapOverlayPage 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-max-w-3xl !tw-left-auto tw-top-0 tw-bottom-0'>
|
||||
{item &&
|
||||
<>
|
||||
<div className='tw-flex tw-flex-row'>
|
||||
<div className="tw-grow">
|
||||
<p className="tw-text-3xl tw-font-semibold">{item.layer?.itemAvatarField && getValue(item, item.layer.itemAvatarField) && <img className='tw-h-20 tw-rounded-full tw-inline' src={`https://api.utopia-lab.org/assets/${getValue(item, item.layer.itemAvatarField)}?width=160&heigth=160`}></img>} {item.layer?.itemNameField && getValue(item, item.layer.itemNameField)}</p>
|
||||
</div>
|
||||
{owner?.id === user?.id && owner?.id ?
|
||||
<a className='tw-self-center tw-btn tw-btn-sm tw-mr-4 tw-cursor-pointer' onClick={() => navigate("/profile-settings")}>
|
||||
<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" />
|
||||
</svg>
|
||||
</a> : ""
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div className='tw-h-full'>
|
||||
|
||||
<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="Vision" checked={activeTab == 1 && true} onChange={() => setActiveTab(1)} />
|
||||
<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-2 tw-pb-1">
|
||||
<TextView item={item} />
|
||||
</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 == 2 && true} onChange={() => setActiveTab(2)} />
|
||||
<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>
|
||||
|
||||
<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="Contact" checked={activeTab == 3 && true} onChange={() => setActiveTab(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-2 tw-pb-1">
|
||||
<TextView item={item} itemTextField='user_created.contact' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</MapOverlayPage >
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,170 +0,0 @@
|
||||
import { useItems, useUpdateItem } from '../Map/hooks/useItems'
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getValue } from '../../Utils/GetValue';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useAuth } from '../Auth';
|
||||
import { TextInput, TextAreaInput } from '../Input';
|
||||
import { ColorPicker } from './ColorPicker';
|
||||
import { hashTagRegex } from '../../Utils/HashTagRegex';
|
||||
import { useAddTag, useTags } from '../Map/hooks/useTags';
|
||||
import { randomColor } from '../../Utils/RandomColor';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Tag, UserItem } from '../../types';
|
||||
import { MapOverlayPage } from '../Templates';
|
||||
import { TagsWidget } from './TagsWidget';
|
||||
import { encodeTag } from '../../Utils/FormatTags';
|
||||
import { AvatarWidget } from './AvatarWidget';
|
||||
|
||||
|
||||
export function OverlayProfileSettings() {
|
||||
|
||||
const { user, updateUser, loading } = useAuth();
|
||||
|
||||
const [id, setId] = useState<string>("");
|
||||
const [name, setName] = useState<string>("");
|
||||
const [text, setText] = useState<string>("");
|
||||
const [avatar, setAvatar] = useState<string>("");
|
||||
const [color, setColor] = useState<string>("");
|
||||
const [offers, setOffers] = useState<Array<Tag>>([]);
|
||||
const [needs, setNeeds] = useState<Array<Tag>>([]);
|
||||
const [contact, setContact] = useState<string>("");
|
||||
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
|
||||
|
||||
|
||||
const items = useItems();
|
||||
const updateItem = useUpdateItem();
|
||||
|
||||
const tags = useTags();
|
||||
const addTag = useAddTag();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
setId(user?.id ? user.id : "");
|
||||
setName(user?.first_name ? user.first_name : "");
|
||||
setText(user?.description ? user.description : "");
|
||||
setAvatar(user?.avatar ? user?.avatar : "");
|
||||
setColor(user?.color ? user.color : "#3D3846");
|
||||
setOffers([]);
|
||||
setNeeds([]);
|
||||
user?.offers.map(o=> {
|
||||
const offer = tags.find(t => t.id === o.tags_id);
|
||||
offer && setOffers(current => [...current,offer])
|
||||
})
|
||||
user?.needs.map(o=> {
|
||||
const need = tags.find(t => t.id === o.tags_id);
|
||||
need && setNeeds(current => [...current,need])
|
||||
})
|
||||
setContact(user?.contact ? user.contact : "");
|
||||
}, [user])
|
||||
|
||||
|
||||
const onUpdateUser = async () => {
|
||||
let changedUser = {} as UserItem;
|
||||
|
||||
let offer_updates : Array<any> = [];
|
||||
//check for new offers
|
||||
offers.map(o => {
|
||||
const existingOffer = user?.offers.find(t => t.tags_id === o.id)
|
||||
existingOffer && offer_updates.push(existingOffer.id)
|
||||
if(!existingOffer && !tags.some(t => t.id === o.id)) addTag({...o,offer_or_need: true})
|
||||
!existingOffer && offer_updates.push({directus_user_id: user?.id, tags_id: o.id})
|
||||
});
|
||||
|
||||
let needs_updates : Array<any> = [];
|
||||
|
||||
needs.map(n => {
|
||||
const existingNeed = user?.needs.find(t => t.tags_id === n.id)
|
||||
existingNeed && needs_updates.push(existingNeed.id)
|
||||
!existingNeed && needs_updates.push({directus_user_id: user?.id, tags_id: n.id})
|
||||
!existingNeed && !tags.some(t => t.id === n.id) && addTag({...n,offer_or_need: true})
|
||||
});
|
||||
|
||||
|
||||
changedUser = { id: id, first_name: name, description: text, contact: contact, color: color, ...avatar.length > 10 && { avatar: avatar }, ... offers.length > 0 && {offers: offer_updates}, ... needs.length > 0 && {needs: needs_updates} };
|
||||
// update profile item in current state
|
||||
const item = items.find(i => i.layer?.itemOwnerField && getValue(i, i.layer?.itemOwnerField).id === id);
|
||||
|
||||
let offer_state : Array<any> = [];
|
||||
let needs_state : Array<any> = [];
|
||||
|
||||
await offers.map(o => {
|
||||
offer_state.push({directus_user_id: user?.id, tags_id: o.id})
|
||||
});
|
||||
|
||||
await needs.map(n => {
|
||||
needs_state.push({directus_user_id: user?.id, tags_id: n.id})
|
||||
});
|
||||
|
||||
|
||||
if (item && item.layer && item.layer.itemOwnerField) item[item.layer.itemOwnerField] = {... changedUser, offers: offer_state, needs: needs_state};
|
||||
// add new hashtags from profile text
|
||||
text.toLocaleLowerCase().match(hashTagRegex)?.map(tag => {
|
||||
if (!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) {
|
||||
addTag({ id: crypto.randomUUID(), name: encodeTag(tag.slice(1).toLocaleLowerCase()), color: randomColor()})
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
toast.promise(
|
||||
updateUser(changedUser),
|
||||
{
|
||||
pending: 'updating Profile ...',
|
||||
success: 'Profile updated',
|
||||
error: {
|
||||
render({ data }) {
|
||||
return `${data}`
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => item && updateItem(item))
|
||||
.then(() => navigate("/"));
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<MapOverlayPage backdrop className='tw-mx-4 tw-mt-4 tw-mb-12 tw-overflow-x-hidden tw-max-h-[calc(100dvh-96px)] !tw-h-[calc(100dvh-96px)] tw-w-[calc(100%-32px)] md:tw-w-[calc(50%-32px)] tw-max-w-3xl !tw-left-auto tw-top-0 tw-bottom-0'>
|
||||
<div className='tw-flex tw-flex-col tw-h-full'>
|
||||
<div className="tw-flex">
|
||||
<AvatarWidget avatar={avatar} setAvatar={setAvatar}/>
|
||||
<ColorPicker color={color} onChange={setColor} className={"-tw-left-6 tw-top-14 -tw-mr-6"} />
|
||||
<TextInput placeholder="Name" defaultValue={user?.first_name ? user.first_name : ""} updateFormValue={(v) => setName(v)} containerStyle='tw-grow tw-ml-6 tw-my-auto ' />
|
||||
</div>
|
||||
|
||||
|
||||
<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="Vision" checked={activeTab == 1 && true} onChange={() => setActiveTab(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">
|
||||
<TextAreaInput placeholder="My Vision..." defaultValue={user?.description ? user.description : ""} updateFormValue={(v) => setText(v)} containerStyle='tw-h-full' inputStyle='tw-h-full tw-border-t-0 tw-rounded-tl-none' />
|
||||
</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 == 2 && true} onChange={() => setActiveTab(2)} />
|
||||
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-rounded-box tw-pt-4 tw-h-[calc(100dvh-332px)] tw-min-h-56">
|
||||
<div className='tw-h-full'>
|
||||
<div className='tw-w-full tw-h-[calc(50%-0.75em)] tw-mb-4'>
|
||||
<TagsWidget defaultTags={offers} onUpdate={(v)=>setOffers(v)} placeholder="enter your offers" containerStyle='tw-bg-transparent tw-w-full tw-h-full tw-mt-3 tw-text-xs tw-h-[calc(100%-1rem)] tw-min-h-[5em] tw-pb-2 tw-overflow-auto'/>
|
||||
</div>
|
||||
<div className='tw-w-full tw-h-[calc(50%-0.75em)] '>
|
||||
<TagsWidget defaultTags={needs} onUpdate={(v)=>setNeeds(v)} placeholder="enter your needs" containerStyle='tw-bg-transparent tw-w-full tw-h-full tw-mt-3 tw-text-xs tw-h-[calc(100%-1rem)] tw-min-h-[5em] tw-pb-2 tw-overflow-auto'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="Contact" checked={activeTab == 3 && true} onChange={() => setActiveTab(3)} />
|
||||
<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">
|
||||
<TextAreaInput placeholder="Contact ..." defaultValue={user?.contact ? user.contact : ""} updateFormValue={(v) => setContact(v)} containerStyle='tw-h-full' inputStyle='tw-h-full tw-border-t-0 ' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tw-mt-4 tw-mb-4"><button className={loading ? " tw-loading tw-btn-disabled tw-btn tw-btn-primary tw-float-right" : "tw-btn tw-btn-primary tw-float-right"} onClick={() => onUpdateUser()}>Update</button></div>
|
||||
|
||||
</div>
|
||||
|
||||
</MapOverlayPage>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,83 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { CardPage, MapOverlayPage } from '../Templates'
|
||||
import { useItems } from '../Map/hooks/useItems'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useState } from 'react';
|
||||
import { Item, UserItem } from '../../types';
|
||||
import { getValue } from '../../Utils/GetValue';
|
||||
import { useMap } from 'react-leaflet';
|
||||
import { LatLng } from 'leaflet';
|
||||
import { TextView } from '../Map';
|
||||
import useWindowDimensions from '../Map/hooks/useWindowDimension';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useAuth } from '../Auth';
|
||||
import { TextInput } from '../Input';
|
||||
|
||||
export function OverlayUserSettings() {
|
||||
const { user, updateUser, loading, token } = useAuth();
|
||||
|
||||
const [id, setId] = useState<string>("");
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
|
||||
|
||||
|
||||
const [passwordChanged, setPasswordChanged] = useState<boolean>(false);
|
||||
|
||||
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
React.useEffect(() => {
|
||||
setId(user?.id ? user.id : "");
|
||||
setEmail(user?.email ? user.email : "");
|
||||
setPassword(user?.password ? user.password : "");
|
||||
}, [user])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const onUpdateUser = () => {
|
||||
let changedUser = {} as UserItem;
|
||||
|
||||
changedUser = { id: id, email: email, ...passwordChanged && { password: password } };
|
||||
|
||||
|
||||
toast.promise(
|
||||
|
||||
updateUser(changedUser),
|
||||
{
|
||||
pending: 'updating Profile ...',
|
||||
success: 'Profile updated',
|
||||
error: {
|
||||
render({ data }) {
|
||||
return `${data}`
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => navigate("/"));
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<MapOverlayPage backdrop className='tw-mx-4 tw-mt-4 tw-max-h-[calc(100dvh-96px)] tw-h-fit md:tw-w-[calc(50%-32px)] tw-w-[calc(100%-32px)] tw-max-w-xl !tw-left-auto tw-top-0 tw-bottom-0'>
|
||||
<div className={`tw-text-xl tw-font-semibold`}>Settings</div>
|
||||
<div className="tw-divider tw-mt-2"></div>
|
||||
<div className="tw-grid tw-grid-cols-1 tw-gap-6">
|
||||
<TextInput type='email' placeholder="new E-Mail" defaultValue={user?.email ? user.email : ""} updateFormValue={(v) => setEmail(v)} />
|
||||
<TextInput type='password' placeholder="new Password" defaultValue={user?.password ? user.password : ""} updateFormValue={(v) => {
|
||||
setPassword(v);
|
||||
setPasswordChanged(true);
|
||||
}} />
|
||||
{/* <ToogleInput updateType="syncData" labelTitle="Sync Data" defaultValue={true} updateFormValue={updateFormValue}/> */}
|
||||
</div>
|
||||
|
||||
<div className="tw-mt-8"><button className={loading ? " tw-loading tw-btn-disabled tw-btn tw-btn-primary tw-float-right" : "tw-btn tw-btn-primary tw-float-right"} onClick={() => onUpdateUser()}>Update</button></div>
|
||||
|
||||
</MapOverlayPage>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
149
src/Components/Profile/ProfileForm.tsx
Normal file
149
src/Components/Profile/ProfileForm.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import { useItems, useUpdateItem, useAddItem } from '../Map/hooks/useItems'
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getValue } from '../../Utils/GetValue';
|
||||
import { useAuth } from '../Auth';
|
||||
import { useAddTag, useGetItemTags, useTags } from '../Map/hooks/useTags';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Item, Tag } from '../../types';
|
||||
import { MapOverlayPage } from '../Templates';
|
||||
import { useLayers } from '../Map/hooks/useLayers';
|
||||
import { useHasUserPermission } from '../Map/hooks/usePermissions';
|
||||
import { OnepagerForm } from './Templates/OnepagerForm';
|
||||
import { linkItem, onUpdateItem, unlinkItem } from './itemFunctions';
|
||||
import { SimpleForm } from './Templates/SimpleForm';
|
||||
import { TabsForm } from './Templates/TabsForm';
|
||||
import { FormHeader } from './Subcomponents/FormHeader';
|
||||
|
||||
export function ProfileForm({ userType }: { userType: string }) {
|
||||
|
||||
const [state, setState] = useState({
|
||||
color: "",
|
||||
id: "",
|
||||
groupType: "wuerdekompass",
|
||||
status: "active",
|
||||
name: "",
|
||||
subname: "",
|
||||
text: "",
|
||||
contact: "",
|
||||
telephone: "",
|
||||
nextAppointment: "",
|
||||
image: "",
|
||||
markerIcon: "",
|
||||
offers: [] as Tag[],
|
||||
needs: [] as Tag[],
|
||||
relations: [] as Item[]
|
||||
});
|
||||
|
||||
const [updatePermission, setUpdatePermission] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [item, setItem] = useState<Item>({} as Item)
|
||||
const { user } = useAuth();
|
||||
const updateItem = useUpdateItem();
|
||||
const addItem = useAddItem();
|
||||
const layers = useLayers();
|
||||
const location = useLocation();
|
||||
const tags = useTags();
|
||||
const addTag = useAddTag();
|
||||
const navigate = useNavigate();
|
||||
const hasUserPermission = useHasUserPermission();
|
||||
const getItemTags = useGetItemTags();
|
||||
const items = useItems();
|
||||
|
||||
useEffect(() => {
|
||||
item && hasUserPermission("items", "update", item) && setUpdatePermission(true);
|
||||
}, [item])
|
||||
|
||||
useEffect(() => {
|
||||
const itemId = location.pathname.split("/")[2];
|
||||
|
||||
const item = items.find(i => i.id === itemId);
|
||||
item && setItem(item);
|
||||
|
||||
const layer = layers.find(l => l.itemType.name == userType)
|
||||
|
||||
!item && setItem({ id: crypto.randomUUID(), name: user ? user.first_name : "", text: "", layer: layer, new: true })
|
||||
|
||||
}, [items])
|
||||
|
||||
useEffect(() => {
|
||||
const newColor = item.layer?.itemColorField && getValue(item, item.layer?.itemColorField)
|
||||
? getValue(item, item.layer?.itemColorField)
|
||||
: (getItemTags(item) && getItemTags(item)[0]?.color)
|
||||
? getItemTags(item)[0].color
|
||||
: item?.layer?.markerDefaultColor;
|
||||
|
||||
const offers = (item?.offers ?? []).reduce((acc: Tag[], o) => {
|
||||
const offer = tags.find(t => t.id === o.tags_id);
|
||||
if (offer) acc.push(offer);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const needs = (item?.needs ?? []).reduce((acc: Tag[], o) => {
|
||||
const need = tags.find(t => t.id === o.tags_id);
|
||||
if (need) acc.push(need);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const relations = (item?.relations ?? []).reduce((acc: Item[], r) => {
|
||||
const relatedItem = items.find(i => i.id === r.related_items_id);
|
||||
if (relatedItem) acc.push(relatedItem);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
setState({
|
||||
color: newColor,
|
||||
id: item?.id ?? "",
|
||||
groupType: item?.group_type ?? "wuerdekompass",
|
||||
status: item?.status ?? "active",
|
||||
name: item?.name ?? "",
|
||||
subname: item?.subname ?? "",
|
||||
text: item?.text ?? "",
|
||||
contact: item?.contact ?? "",
|
||||
telephone: item?.telephone ?? "",
|
||||
nextAppointment: item?.next_appointment ?? "",
|
||||
image: item?.image ?? "",
|
||||
markerIcon: item?.marker_icon ?? "",
|
||||
offers: offers,
|
||||
needs: needs,
|
||||
relations: relations
|
||||
});
|
||||
}, [item, tags, items]);
|
||||
|
||||
const [template, setTemplate] = useState<string>("")
|
||||
|
||||
useEffect(() => {
|
||||
setTemplate(item.layer?.itemType.template || userType);
|
||||
}, [userType, item])
|
||||
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MapOverlayPage backdrop className='tw-mx-4 tw-mt-4 tw-mb-4 tw-overflow-x-hidden tw-w-[calc(100%-32px)] md:tw-w-[calc(50%-32px)] tw-max-w-3xl !tw-left-auto tw-top-0 tw-bottom-0'>
|
||||
|
||||
<div className='tw-flex tw-flex-col tw-h-full'>
|
||||
|
||||
<FormHeader item={item} state={state} setState={setState} />
|
||||
|
||||
{template == "onepager" && (
|
||||
<OnepagerForm item={item} state={state} setState={setState}></OnepagerForm>
|
||||
)}
|
||||
|
||||
{template == "simple" &&
|
||||
<SimpleForm item={item} setState={setState}></SimpleForm>
|
||||
}
|
||||
|
||||
{template == "tabs" &&
|
||||
<TabsForm loading={loading} item={item} state={state} setState={setState} updatePermission={updatePermission} linkItem={(id) => linkItem(id, item, updateItem)} unlinkItem={(id) => unlinkItem(id, item, updateItem)}></TabsForm>
|
||||
}
|
||||
|
||||
<div className="tw-mt-4 tw-mb-4">
|
||||
<button className={loading ? " tw-loading tw-btn tw-float-right" : "tw-btn tw-float-right"} onClick={() => onUpdateItem(state, item, tags, addTag, setLoading, navigate, updateItem, addItem, user, params)} style={true ? { backgroundColor: `${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)}`, color: "#fff" } : { color: "#fff" }}>Update</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</MapOverlayPage>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,223 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { TitleCard } from '../Templates/TitleCard'
|
||||
import { TextInput } from '../Input/TextInput'
|
||||
import { TextAreaInput } from '../Input/TextAreaInput'
|
||||
import { toast } from 'react-toastify';
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../Auth';
|
||||
import * as React from 'react'
|
||||
import ReactCrop, { Crop, centerCrop, makeAspectCrop } from 'react-image-crop'
|
||||
import 'react-image-crop/dist/ReactCrop.css'
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { UserItem } from '../../types';
|
||||
import DialogModal from '../Templates/DialogModal';
|
||||
import { useAssetApi } from '../AppShell/hooks/useAssets';
|
||||
import { ColorPicker } from './ColorPicker';
|
||||
|
||||
export function ProfileSettings() {
|
||||
const { user, updateUser, loading, token } = useAuth();
|
||||
|
||||
const [id, setId] = useState<string>("");
|
||||
const [name, setName] = useState<string>("");
|
||||
const [text, setText] = useState<string>("");
|
||||
const [avatar, setAvatar] = useState<string>("");
|
||||
const [color, setColor] = useState<string>("");
|
||||
|
||||
|
||||
const [crop, setCrop] = useState<Crop>();
|
||||
const [image, setImage] = useState<string>("");
|
||||
const [cropModalOpen, setCropModalOpen] = useState<boolean>(false);
|
||||
const [cropping, setCropping] = useState<boolean>(false);
|
||||
|
||||
const assetsApi = useAssetApi();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
setId(user?.id ? user.id : "");
|
||||
setName(user?.first_name ? user.first_name : "");
|
||||
setText(user?.description ? user.description : "");
|
||||
setAvatar(user?.avatar ? user?.avatar : ""),
|
||||
setColor(user?.color? user.color : "#aabbcc")
|
||||
}, [user])
|
||||
|
||||
const imgRef = useRef<HTMLImageElement>(null)
|
||||
|
||||
const onImageChange = (event) => {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
setImage(URL.createObjectURL(event.target.files[0]));
|
||||
}
|
||||
setCropModalOpen(true);
|
||||
}
|
||||
|
||||
function onImageLoad(e: React.SyntheticEvent<HTMLImageElement>) {
|
||||
const { width, height } = e.currentTarget
|
||||
|
||||
setCrop(centerAspectCrop(width, height, 1))
|
||||
}
|
||||
|
||||
|
||||
// This is to demonstate how to make and center a % aspect crop
|
||||
// which is a bit trickier so we use some helper functions.
|
||||
function centerAspectCrop(
|
||||
mediaWidth: number,
|
||||
mediaHeight: number,
|
||||
aspect: number,
|
||||
) {
|
||||
return centerCrop(
|
||||
makeAspectCrop(
|
||||
{
|
||||
unit: 'px',
|
||||
width: mediaWidth / 2,
|
||||
},
|
||||
aspect,
|
||||
mediaWidth,
|
||||
mediaHeight,
|
||||
),
|
||||
mediaWidth,
|
||||
mediaHeight,
|
||||
)
|
||||
}
|
||||
|
||||
async function renderCrop() {
|
||||
// get the image element
|
||||
const image = imgRef.current;
|
||||
if (crop && image) {
|
||||
|
||||
const scaleX = image.naturalWidth / image.width
|
||||
const scaleY = image.naturalHeight / image.height
|
||||
|
||||
// create a canvas element to draw the cropped image
|
||||
const canvas = new OffscreenCanvas(
|
||||
crop.width * scaleX,
|
||||
crop.height * scaleY,
|
||||
)
|
||||
const ctx = canvas.getContext("2d");
|
||||
const pixelRatio = window.devicePixelRatio;
|
||||
canvas.width = crop.width * pixelRatio * scaleX;
|
||||
canvas.height = crop.height * pixelRatio * scaleY;
|
||||
|
||||
if (ctx) {
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
|
||||
ctx.drawImage(
|
||||
image,
|
||||
crop.x * scaleX,
|
||||
crop.y * scaleY,
|
||||
crop.width * scaleX,
|
||||
crop.height * scaleY,
|
||||
0,
|
||||
0,
|
||||
crop.width * scaleX,
|
||||
crop.height * scaleY
|
||||
);
|
||||
}
|
||||
const blob = await canvas.convertToBlob();
|
||||
await resizeBlob(blob);
|
||||
setCropping(false);
|
||||
setImage("");
|
||||
}
|
||||
}
|
||||
|
||||
async function resizeBlob(blob) {
|
||||
var img = new Image();
|
||||
img.src = URL.createObjectURL(blob);
|
||||
await img.decode();
|
||||
const canvas = new OffscreenCanvas(
|
||||
400,
|
||||
400
|
||||
)
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx?.drawImage(img, 0, 0, 400, 400);
|
||||
const resizedBlob = await canvas.convertToBlob()
|
||||
const asset = await assetsApi.upload(resizedBlob, "test");
|
||||
setAvatar(asset.id)
|
||||
}
|
||||
|
||||
|
||||
const onUpdateUser = () => {
|
||||
let changedUser = {} as UserItem;
|
||||
|
||||
changedUser = { id: id, first_name: name, description: text, color: color, ...avatar.length > 10 && { avatar: avatar } };
|
||||
|
||||
|
||||
toast.promise(
|
||||
|
||||
updateUser(changedUser),
|
||||
{
|
||||
pending: 'updating Profile ...',
|
||||
success: 'Profile updated',
|
||||
error: {
|
||||
render({ data }) {
|
||||
return `${data}`
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => navigate("/"));
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className='tw-backdrop-contrast-50 tw-h-full tw-w-full'>
|
||||
<main className="tw-flex-1 tw-overflow-y-auto tw-overflow-x-hidden tw-pt-8 tw-px-6 tw-min-w-80 tw-flex tw-justify-center" >
|
||||
<div className='tw-w-full xl:tw-max-w-6xl'>
|
||||
<TitleCard title="Profile" topMargin="tw-mt-2" className='tw-mb-6'>
|
||||
<div className="tw-flex">
|
||||
{!cropping ?
|
||||
<label className="custom-file-upload">
|
||||
<input type="file" accept="image/*" className="tw-file-input tw-w-full tw-max-w-xs" onChange={onImageChange} />
|
||||
<div className='button tw-btn tw-btn-lg tw-btn-circle tw-animate-none'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="tw-w-6 tw-h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||
</svg>
|
||||
</div>
|
||||
{avatar ?
|
||||
<div className='tw-h-20 tw-w-20'>
|
||||
<img src={assetsApi.url + avatar + "?access_token=" + token} className=' tw-rounded-full' />
|
||||
</div>
|
||||
:
|
||||
<div className='tw-h-20 tw-w-20'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 150 150" className='tw-w-20 tw-h-20 tw-rounded-full' style={{ backgroundColor: "#eee" }}>
|
||||
<path fill="#ccc" d="M 104.68731,56.689353 C 102.19435,80.640493 93.104981,97.26875 74.372196,97.26875 55.639402,97.26875 46.988823,82.308034 44.057005,57.289941 41.623314,34.938838 55.639402,15.800152 74.372196,15.800152 c 18.732785,0 32.451944,18.493971 30.315114,40.889201 z" />
|
||||
<path fill="#ccc" d="M 92.5675 89.6048 C 90.79484 93.47893 89.39893 102.4504 94.86478 106.9039 C 103.9375 114.2963 106.7064 116.4723 118.3117 118.9462 C 144.0432 124.4314 141.6492 138.1543 146.5244 149.2206 L 4.268444 149.1023 C 8.472223 138.6518 6.505799 124.7812 32.40051 118.387 C 41.80992 116.0635 45.66513 113.8823 53.58659 107.0158 C 58.52744 102.7329 57.52583 93.99267 56.43084 89.26926 C 52.49275 88.83011 94.1739 88.14054 92.5675 89.6048 z" />
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
</label>
|
||||
|
||||
: <div className='tw-w-20 tw-flex tw-items-center tw-justify-center'>
|
||||
<span className="tw-loading tw-loading-spinner"></span>
|
||||
</div>
|
||||
|
||||
}
|
||||
<ColorPicker color={color} onChange={setColor} className={"-tw-left-6 tw-top-14 -tw-mr-6"} />
|
||||
<TextInput placeholder="Name" defaultValue={user?.first_name ? user.first_name : ""} updateFormValue={(v) => setName(v)} containerStyle='tw-grow tw-ml-6 tw-my-auto ' />
|
||||
</div>
|
||||
|
||||
<div className="tw-grid tw-grid-cols-1 tw-md:grid-cols-1 tw-gap-6 tw-pt-6 tw-pb-6">
|
||||
<TextAreaInput placeholder="About me, Contact, #Tags, ..." defaultValue={user?.description ? user.description : ""} updateFormValue={(v) => setText(v)} inputStyle='tw-h-64' />
|
||||
</div>
|
||||
|
||||
<div className="tw-mt-8"><button className={loading ? " tw-loading tw-btn-disabled tw-btn tw-btn-primary tw-float-right" : "tw-btn tw-btn-primary tw-float-right"} onClick={() => onUpdateUser()}>Update</button></div>
|
||||
|
||||
</TitleCard>
|
||||
</div>
|
||||
</main>
|
||||
<DialogModal
|
||||
title=""
|
||||
isOpened={cropModalOpen}
|
||||
onClose={() => {
|
||||
setCropModalOpen(false);
|
||||
setImage("");
|
||||
}}>
|
||||
<ReactCrop crop={crop} onChange={(c) => setCrop(c)} aspect={1} >
|
||||
<img src={image} ref={imgRef} onLoad={onImageLoad} />
|
||||
</ReactCrop>
|
||||
<button className={`tw-btn tw-btn-primary`} onClick={() => {
|
||||
setCropping(true);
|
||||
setCropModalOpen(false);
|
||||
renderCrop();
|
||||
}}>Select</button>
|
||||
</DialogModal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
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;
|
||||
148
src/Components/Profile/ProfileView.tsx
Normal file
148
src/Components/Profile/ProfileView.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import { MapOverlayPage } from '../Templates'
|
||||
import { useItems, useRemoveItem, useUpdateItem } from '../Map/hooks/useItems'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Item, Tag } from '../../types';
|
||||
import { useMap } from 'react-leaflet';
|
||||
import { LatLng } from 'leaflet';
|
||||
import { useHasUserPermission } from '../Map/hooks/usePermissions';
|
||||
import { HeaderView } from '../Map/Subcomponents/ItemPopupComponents/HeaderView';
|
||||
import { useSelectPosition, useSetSelectPosition } from '../Map/hooks/useSelectPosition';
|
||||
import { useClusterRef } from '../Map/hooks/useClusterRef';
|
||||
import { useLeafletRefs } from '../Map/hooks/useLeafletRefs';
|
||||
import { getValue } from '../../Utils/GetValue';
|
||||
import { TabsView } from './Templates/TabsView';
|
||||
import { OnepagerView } from './Templates/OnepagerView';
|
||||
import { SimpleView } from './Templates/SimpleView';
|
||||
import { handleDelete, linkItem, unlinkItem } from './itemFunctions';
|
||||
import { useTags } from '../Map/hooks/useTags';
|
||||
|
||||
export function ProfileView({ userType }: { userType: string }) {
|
||||
|
||||
const [item, setItem] = useState<Item>({} as Item)
|
||||
const [updatePermission, setUpdatePermission] = useState<boolean>(false);
|
||||
const [relations, setRelations] = useState<Array<Item>>([]);
|
||||
const [offers, setOffers] = useState<Array<Tag>>([]);
|
||||
const [needs, setNeeds] = useState<Array<Tag>>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [template, setTemplate] = useState<string>("");
|
||||
|
||||
const location = useLocation();
|
||||
const items = useItems();
|
||||
const updateItem = useUpdateItem();
|
||||
const map = useMap();
|
||||
const selectPosition = useSelectPosition();
|
||||
const removeItem = useRemoveItem();
|
||||
const tags = useTags();
|
||||
const navigate = useNavigate();
|
||||
const hasUserPermission = useHasUserPermission();
|
||||
const setSelectPosition = useSetSelectPosition();
|
||||
const clusterRef = useClusterRef();
|
||||
const leafletRefs = useLeafletRefs();
|
||||
|
||||
useEffect(() => {
|
||||
const itemId = location.pathname.split("/")[2];
|
||||
const item = items.find(i => i.id === itemId);
|
||||
item && setItem(item);
|
||||
}, [items, location])
|
||||
|
||||
useEffect(() => {
|
||||
setOffers([]);
|
||||
setNeeds([]);
|
||||
setRelations([]);
|
||||
|
||||
item.layer?.itemOffersField && getValue(item, item.layer.itemOffersField)?.map(o => {
|
||||
const tag = tags.find(t => t.id === o.tags_id);
|
||||
tag && setOffers(current => [...current, tag])
|
||||
})
|
||||
item.layer?.itemNeedsField && getValue(item, item.layer.itemNeedsField)?.map(n => {
|
||||
const tag = tags.find(t => t.id === n.tags_id);
|
||||
tag && setNeeds(current => [...current, tag])
|
||||
})
|
||||
item.relations?.map(r => {
|
||||
const item = items.find(i => i.id == r.related_items_id)
|
||||
item && setRelations(current => [...current, item])
|
||||
})
|
||||
|
||||
}, [item, items])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const setMap = async (marker, x) => {
|
||||
await map.setView(new LatLng(item?.position?.coordinates[1]!, item?.position?.coordinates[0]! + x / 4), undefined);
|
||||
setTimeout(() => {
|
||||
marker.openPopup();
|
||||
}, 500);
|
||||
}
|
||||
if (item) {
|
||||
if (item.position) {
|
||||
const marker = Object.entries(leafletRefs).find(r => r[1].item == item)?.[1].marker;
|
||||
marker && clusterRef.hasLayer(marker) && clusterRef?.zoomToShowLayer(marker, () => {
|
||||
const bounds = map.getBounds();
|
||||
const x = bounds.getEast() - bounds.getWest();
|
||||
setMap(marker, x);
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
const parent = getFirstAncestor(item);
|
||||
const marker = Object.entries(leafletRefs).find(r => r[1].item == parent)?.[1].marker;
|
||||
marker && clusterRef.hasLayer(marker) && clusterRef?.zoomToShowLayer(marker, () => {
|
||||
const bounds = map.getBounds();
|
||||
const x = bounds.getEast() - bounds.getWest();
|
||||
setMap(marker, x);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [item])
|
||||
|
||||
const getFirstAncestor = (item: Item): Item | undefined => {
|
||||
const parent = items.find(i => i.id === item.parent);
|
||||
if (parent?.parent) {
|
||||
return getFirstAncestor(parent);
|
||||
} else {
|
||||
return parent;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
item && hasUserPermission("items", "update", item) && setUpdatePermission(true);
|
||||
}, [item])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
selectPosition && map.closePopup();
|
||||
}, [selectPosition])
|
||||
|
||||
useEffect(() => {
|
||||
setTemplate(item.layer?.itemType.template || userType);
|
||||
}, [userType, item])
|
||||
|
||||
return (
|
||||
<>
|
||||
{item &&
|
||||
<MapOverlayPage key={item.id} className={`!tw-p-0 tw-mx-4 tw-mt-4 tw-mb-4 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'}`}>
|
||||
<>
|
||||
<div className={`tw-px-6 tw-pt-6`}>
|
||||
<HeaderView api={item.layer?.api} item={item} deleteCallback={(e) => handleDelete(e, item, setLoading, removeItem, map, navigate)} editCallback={() => navigate("/edit-item/" + item.id)} setPositionCallback={() => { map.closePopup(); setSelectPosition(item); navigate("/") }} big truncateSubname={false} />
|
||||
</div>
|
||||
|
||||
{template == "onepager" &&
|
||||
<OnepagerView item={item} userType={userType}/>
|
||||
}
|
||||
|
||||
{template == "simple" &&
|
||||
<SimpleView item={item}/>
|
||||
}
|
||||
|
||||
{template == "tabs" &&
|
||||
<TabsView item={item} loading={loading} offers={offers} needs={needs} relations={relations} updatePermission={updatePermission} linkItem={(id) => linkItem(id, item, updateItem)} unlinkItem={(id) => unlinkItem(id, item, updateItem)}/>
|
||||
}
|
||||
</>
|
||||
|
||||
</MapOverlayPage >
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,12 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { useHasUserPermission, usePermissions } from "../Map/hooks/usePermissions";
|
||||
import DialogModal from "../Templates/DialogModal";
|
||||
import { useItems } from "../Map/hooks/useItems";
|
||||
import { HeaderView } from "../Map/Subcomponents/ItemPopupComponents/HeaderView";
|
||||
import { Item } from "../../types";
|
||||
import { TextInput } from "../Input";
|
||||
import { getValue } from "../../Utils/GetValue";
|
||||
import { useGetItemTags } from "../Map/hooks/useTags";
|
||||
import { useHasUserPermission, usePermissions } from "../../Map/hooks/usePermissions";
|
||||
import DialogModal from "../../Templates/DialogModal";
|
||||
import { useItems } from "../../Map/hooks/useItems";
|
||||
import { HeaderView } from "../../Map/Subcomponents/ItemPopupComponents/HeaderView";
|
||||
import { Item } from "../../../types";
|
||||
import { TextInput } from "../../Input";
|
||||
import { getValue } from "../../../Utils/GetValue";
|
||||
import { useGetItemTags } from "../../Map/hooks/useTags";
|
||||
|
||||
export function ActionButton({ item, triggerAddButton, triggerItemSelected, existingRelations, itemType, colorField, collection = "items", customStyle }: {
|
||||
triggerAddButton?: any,
|
||||
@ -1,11 +1,12 @@
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import ReactCrop, { Crop, centerCrop, makeAspectCrop } from 'react-image-crop';
|
||||
import { useAssetApi } from '../AppShell/hooks/useAssets';
|
||||
import DialogModal from "../Templates/DialogModal";
|
||||
import { useAssetApi } from '../../AppShell/hooks/useAssets';
|
||||
import DialogModal from "../../Templates/DialogModal";
|
||||
import 'react-image-crop/dist/ReactCrop.css'
|
||||
|
||||
|
||||
export const AvatarWidget = ({avatar, setAvatar}:{avatar:string, setAvatar : React.Dispatch<React.SetStateAction<string>>}) => {
|
||||
export const AvatarWidget = ({avatar, setAvatar}:{avatar:string, setAvatar : React.Dispatch<React.SetStateAction<any>>}) => {
|
||||
|
||||
|
||||
const [crop, setCrop] = useState<Crop>();
|
||||
@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import * as React from "react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import "./ColorPicker.css"
|
||||
import useClickOutside from "./useClickOutside";
|
||||
import useClickOutside from "../hooks/useClickOutside";
|
||||
|
||||
export const ColorPicker = ({ color, onChange, className }) => {
|
||||
const popover = useRef<HTMLDivElement>(null);
|
||||
@ -1,11 +1,11 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAssetApi } from "../AppShell/hooks/useAssets";
|
||||
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">
|
||||
<div className="tw-bg-base-200 tw-mb-6 tw-mt-6 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 && (
|
||||
28
src/Components/Profile/Subcomponents/FormHeader.tsx
Normal file
28
src/Components/Profile/Subcomponents/FormHeader.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { TextInput } from "../../Input"
|
||||
import { AvatarWidget } from "./AvatarWidget"
|
||||
import { ColorPicker } from "./ColorPicker"
|
||||
|
||||
export const FormHeader = ({item, state, setState}) => {
|
||||
return (
|
||||
<div className="tw-flex">
|
||||
<AvatarWidget avatar={state.image} setAvatar={(i) => setState(prevState => ({
|
||||
...prevState,
|
||||
image: i
|
||||
}))} />
|
||||
<ColorPicker color={state.color} onChange={(c) => setState(prevState => ({
|
||||
...prevState,
|
||||
color: c
|
||||
}))} className={"-tw-left-6 tw-top-14 -tw-mr-6"} />
|
||||
<div className='tw-grow tw-mr-4'>
|
||||
<TextInput placeholder="Name" defaultValue={item?.name ? item.name : ""} updateFormValue={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
name: v
|
||||
}))} containerStyle='tw-grow tw-input-md' />
|
||||
<TextInput placeholder="Subtitle" defaultValue={item?.subname ? item.subname : ""} updateFormValue={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
subname: v
|
||||
}))} containerStyle='tw-grow tw-input-sm tw-px-4 tw-mt-1' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { getValue } from "../../Utils/GetValue";
|
||||
import { Item } from "../../types";
|
||||
import { useAssetApi } from "../AppShell/hooks/useAssets";
|
||||
import { getValue } from "../../../Utils/GetValue";
|
||||
import { Item } from "../../../types";
|
||||
import { useAssetApi } from "../../AppShell/hooks/useAssets";
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { LayerProps } from "../../types";
|
||||
import { useHasUserPermission } from "../Map/hooks/usePermissions";
|
||||
import { LayerProps } from "../../../types";
|
||||
import { useHasUserPermission } from "../../Map/hooks/usePermissions";
|
||||
|
||||
export function PlusButton({ layer, triggerAction, color, collection="items" }: { layer?: LayerProps ,triggerAction: any, color: string, collection?:string }) {
|
||||
const hasUserPermission = useHasUserPermission();
|
||||
44
src/Components/Profile/Subcomponents/ProfileSubHeader.tsx
Normal file
44
src/Components/Profile/Subcomponents/ProfileSubHeader.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
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',
|
||||
'active': 'aktiv'
|
||||
};
|
||||
|
||||
const SubHeader = ({ type, status, url, title }) => (
|
||||
<div>
|
||||
<div className='tw-float-left tw-mt-2 tw-mb-4 tw-flex tw-items-center'>
|
||||
|
||||
{status && <div className="tw-mt-1.5">
|
||||
<span className="tw-text-sm tw-text-current tw-bg-base-300 tw-rounded tw-py-0.5 tw-px-2 tw-inline-flex tw-items-center tw-mr-2"><span className={`tw-w-2 tw-h-2 ${ status=="in_planning" && "tw-bg-blue-700"} ${ status=="paused" && "tw-bg-orange-400"} ${ status=="active" && "tw-bg-green-500"} tw-rounded-full tw-mr-1.5`}></span>{statusMapping[status]}</span>
|
||||
</div>}
|
||||
{type && <div className="tw-mt-1.5">
|
||||
<span className="tw-text-sm tw-text-current tw-bg-base-300 tw-rounded tw-py-1 tw-px-2">{type}</span>
|
||||
</div>}
|
||||
</div>
|
||||
<div>
|
||||
<SocialShareBar url={url} title={title} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SubHeader;
|
||||
@ -2,7 +2,7 @@ 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">
|
||||
<div className="tw-flex tw-place-content-end tw-justify-end tw-space-x-2 tw-grow tw-min-w-fit tw-pl-2">
|
||||
{platforms.map((platform) => (
|
||||
<SocialShareButton
|
||||
key={platform}
|
||||
@ -66,7 +66,7 @@ const SocialShareButton = ({ platform, url, title }) => {
|
||||
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'
|
||||
className='tw-w-8 tw-h-8 tw-mt-2 tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-white'
|
||||
style={{
|
||||
color: 'white',
|
||||
backgroundColor: bgColor
|
||||
@ -1,10 +1,10 @@
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react';
|
||||
import { useTags } from '../Map/hooks/useTags';
|
||||
import { Tag } from '../../types';
|
||||
import { Autocomplete } from '../Input/Autocomplete';
|
||||
import { randomColor } from '../../Utils/RandomColor';
|
||||
import { decodeTag, encodeTag } from '../../Utils/FormatTags';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTags } from '../../Map/hooks/useTags';
|
||||
import { Tag } from '../../../types';
|
||||
import { Autocomplete } from '../../Input/Autocomplete';
|
||||
import { randomColor } from '../../../Utils/RandomColor';
|
||||
import { decodeTag, encodeTag } from '../../../Utils/FormatTags';
|
||||
|
||||
export const TagsWidget = ({placeholder, containerStyle, defaultTags, onUpdate}) => {
|
||||
|
||||
@ -14,6 +14,12 @@ export const TagsWidget = ({placeholder, containerStyle, defaultTags, onUpdate})
|
||||
const [pushFilteredSuggestions, setPushFilteredSuggestions] = useState<Array<any>>([]);
|
||||
|
||||
const [focusInput, setFocusInput] = useState<boolean>(false);
|
||||
const [currentTags, setCurrentTags] = useState<Array<Tag>>(defaultTags);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentTags(defaultTags)
|
||||
}, [defaultTags])
|
||||
|
||||
|
||||
|
||||
const onChange = (e) => {
|
||||
@ -28,8 +34,8 @@ export const TagsWidget = ({placeholder, containerStyle, defaultTags, onUpdate})
|
||||
if ((key === 'Enter' || key === ',' ) && trimmedInput.length && !defaultTags.some(tag => tag.name.toLocaleLowerCase() === trimmedInput.toLocaleLowerCase())) {
|
||||
e.preventDefault();
|
||||
const newTag = tags.find(t => t.name === trimmedInput.toLocaleLowerCase())
|
||||
newTag && onUpdate(prevState => [...prevState, newTag]);
|
||||
!newTag && onUpdate(prevState => [...prevState, { id: crypto.randomUUID(), name: encodeTag(trimmedInput), color: randomColor() }]);
|
||||
newTag && onUpdate([...currentTags, newTag]);
|
||||
!newTag && onUpdate([...currentTags, { id: crypto.randomUUID(), name: encodeTag(trimmedInput), color: randomColor() }]);
|
||||
setInput('');
|
||||
setPushFilteredSuggestions([]);
|
||||
}
|
||||
@ -50,15 +56,15 @@ export const TagsWidget = ({placeholder, containerStyle, defaultTags, onUpdate})
|
||||
}
|
||||
|
||||
const deleteTag = (tag) => {
|
||||
onUpdate(prevState => prevState.filter((t) => t !== tag))
|
||||
onUpdate(currentTags.filter((t) => t !== tag))
|
||||
}
|
||||
|
||||
|
||||
const onSelected = (tag) => {
|
||||
if(!defaultTags.some(t => t.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase())) {
|
||||
const newTag = tags.find(t => t.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase())
|
||||
newTag && onUpdate(prevState => [...prevState, newTag]);
|
||||
!newTag && onUpdate(prevState => [...prevState, { id: crypto.randomUUID(), name: tag.name.toLocaleLowerCase(), color: randomColor() }]);
|
||||
newTag && onUpdate([...currentTags, newTag]);
|
||||
!newTag && onUpdate([...currentTags, { id: crypto.randomUUID(), name: tag.name.toLocaleLowerCase(), color: randomColor() }]);
|
||||
setInput('');
|
||||
setPushFilteredSuggestions([]);
|
||||
}
|
||||
164
src/Components/Profile/Templates/OnepagerForm.tsx
Normal file
164
src/Components/Profile/Templates/OnepagerForm.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import { useEffect } from "react";
|
||||
import { Item, Tag } from "../../../types"
|
||||
import { TextAreaInput, TextInput } from "../../Input"
|
||||
import ComboBoxInput from "../../Input/ComboBoxInput"
|
||||
|
||||
export const OnepagerForm = ({ item, state, setState }: {
|
||||
state: {
|
||||
color: string;
|
||||
id: string;
|
||||
groupType: string;
|
||||
status: string;
|
||||
name: string;
|
||||
subname: string;
|
||||
text: string;
|
||||
contact: string;
|
||||
telephone: string;
|
||||
nextAppointment: string;
|
||||
image: string;
|
||||
markerIcon: string;
|
||||
offers: Tag[];
|
||||
needs: Tag[];
|
||||
relations: Item[];
|
||||
},
|
||||
setState: React.Dispatch<React.SetStateAction<any>>,
|
||||
item: Item
|
||||
}) => {
|
||||
|
||||
useEffect(() => {
|
||||
switch (state.groupType) {
|
||||
case "wuerdekompass":
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
color: item?.layer?.menuColor || "#1A5FB4",
|
||||
markerIcon: "group",
|
||||
image: "59e6a346-d1ee-4767-9e42-fc720fb535c9"
|
||||
}));
|
||||
break;
|
||||
case "themenkompass":
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
color: "#26A269",
|
||||
markerIcon: "group",
|
||||
image: "59e6a346-d1ee-4767-9e42-fc720fb535c9"
|
||||
}));
|
||||
break;
|
||||
case "liebevoll.jetzt":
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
color: "#E8B620",
|
||||
markerIcon: "liebevoll.jetzt",
|
||||
image: "e735b96c-507b-471c-8317-386ece0ca51d"
|
||||
}));
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [state.groupType])
|
||||
|
||||
|
||||
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' }
|
||||
];
|
||||
|
||||
return (
|
||||
<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-500 tw-mb-1">
|
||||
Gruppenart:
|
||||
</label>
|
||||
<ComboBoxInput
|
||||
id="groupType"
|
||||
options={typeMapping}
|
||||
value={state.groupType}
|
||||
onValueChange={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
groupType: v
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="status" className="tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1">
|
||||
Gruppenstatus:
|
||||
</label>
|
||||
<ComboBoxInput
|
||||
id="status"
|
||||
options={statusMapping}
|
||||
value={state.status}
|
||||
onValueChange={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
status: v
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1">
|
||||
Email-Adresse (Kontakt):
|
||||
</label>
|
||||
<TextInput
|
||||
placeholder="Email"
|
||||
defaultValue={state.contact}
|
||||
updateFormValue={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
contact: v
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="telephone" className="tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1">
|
||||
Telefonnummer (Kontakt):
|
||||
</label>
|
||||
<TextInput
|
||||
placeholder="Telefonnummer"
|
||||
defaultValue={state.telephone}
|
||||
updateFormValue={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
telephone: v
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="nextAppointment" className="tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1">
|
||||
Nächste Termine:
|
||||
</label>
|
||||
<TextAreaInput
|
||||
placeholder="Nächste Termine"
|
||||
defaultValue={state.nextAppointment}
|
||||
updateFormValue={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
nextAppointment: v
|
||||
}))}
|
||||
inputStyle="tw-h-24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="description" className="tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1">
|
||||
Gruppenbeschreibung:
|
||||
</label>
|
||||
<TextAreaInput
|
||||
placeholder="Beschreibung"
|
||||
defaultValue={state.text || ""}
|
||||
updateFormValue={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
text: v
|
||||
}))}
|
||||
inputStyle="tw-h-48"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
73
src/Components/Profile/Templates/OnepagerView.tsx
Normal file
73
src/Components/Profile/Templates/OnepagerView.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { Item } from "utopia-ui/dist/types"
|
||||
import { TextView } from "../../Map"
|
||||
import ContactInfo from "../Subcomponents/ContactInfo"
|
||||
import ProfileSubHeader from "../Subcomponents/ProfileSubHeader"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useItems } from "../../Map/hooks/useItems"
|
||||
|
||||
export const OnepagerView = ({item, userType}:{item: Item, userType: string}) => {
|
||||
|
||||
const [profile_owner, setProfileOwner] = useState<Item>();
|
||||
const items = useItems();
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setProfileOwner(items.find(i => (i.user_created?.id === item.user_created?.id) && i.layer?.itemType.name === userType));
|
||||
}, [item, items])
|
||||
|
||||
const typeMapping = {
|
||||
'wuerdekompass': 'Regional-Gruppe',
|
||||
'themenkompass': 'Themenkompass-Gruppe',
|
||||
'liebevoll.jetzt': 'liebevoll.jetzt',
|
||||
};
|
||||
|
||||
let groupType = item.group_type ? item.group_type : 'default';
|
||||
let groupTypeText = typeMapping[groupType];
|
||||
|
||||
return (
|
||||
<div className='tw-h-full tw-overflow-y-auto fade'>
|
||||
<div className="tw-px-6">
|
||||
<ProfileSubHeader
|
||||
type={groupTypeText}
|
||||
status={item.status}
|
||||
url={`https://www.wuerdekompass.org/aktivitaeten/gruppensuche/#/gruppe/${item.slug}`}
|
||||
title={item.name}
|
||||
/>
|
||||
</div>
|
||||
{item.user_created.first_name && (
|
||||
<ContactInfo link={`/item/${profile_owner?.id}`} name={profile_owner?.name ? profile_owner.name : item.user_created.first_name} avatar={profile_owner?.image ? profile_owner.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 ">
|
||||
<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">
|
||||
<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>*/}
|
||||
{/*)}*/}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
10
src/Components/Profile/Templates/SimpleForm.tsx
Normal file
10
src/Components/Profile/Templates/SimpleForm.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { TextAreaInput } from "../../Input"
|
||||
|
||||
export const SimpleForm = (item, setState) => {
|
||||
return (
|
||||
<TextAreaInput placeholder="About me ..." defaultValue={item?.text ? item.text : ""} updateFormValue={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
text: v
|
||||
}))} containerStyle='tw-mt-8 tw-h-full' inputStyle='tw-h-full' />
|
||||
)
|
||||
}
|
||||
11
src/Components/Profile/Templates/SimpleView.tsx
Normal file
11
src/Components/Profile/Templates/SimpleView.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import * as React from 'react'
|
||||
import { TextView } from '../../Map'
|
||||
import { Item } from '../../../types'
|
||||
|
||||
export const SimpleView = ({item}:{item: Item}) => {
|
||||
return (
|
||||
<div className='tw-mt-8 tw-h-full tw-overflow-y-auto fade tw-px-6'>
|
||||
<TextView item={item} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
185
src/Components/Profile/Templates/TabsForm.tsx
Normal file
185
src/Components/Profile/Templates/TabsForm.tsx
Normal file
@ -0,0 +1,185 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { TextAreaInput } from "../../Input"
|
||||
import { TextView } from "../../Map"
|
||||
import { ActionButton } from "../Subcomponents/ActionsButton"
|
||||
import { LinkedItemsHeaderView } from "../Subcomponents/LinkedItemsHeaderView"
|
||||
import { TagsWidget } from "../Subcomponents/TagsWidget"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { useUpdateItem } from "../../Map/hooks/useItems"
|
||||
|
||||
export const TabsForm = ({ item, state, setState, updatePermission, linkItem, unlinkItem, loading }) => {
|
||||
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
const navigate = useNavigate();
|
||||
const updateItem = useUpdateItem();
|
||||
|
||||
const updateActiveTab = (id: number) => {
|
||||
setActiveTab(id);
|
||||
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
let urlTab = params.get("tab");
|
||||
if (!urlTab?.includes(id.toString()))
|
||||
params.set("tab", `${id ? id : ""}`)
|
||||
navigate(location.pathname+ "?" + params.toString());
|
||||
}
|
||||
|
||||
const attestations = [{
|
||||
from: "Timo",
|
||||
avatar: "https://api.utopia-lab.org/assets/262117f8-feb6-444f-9bd2-e84087285760?width=80&heigth=80",
|
||||
symbol: "🥇",
|
||||
text: "1. Platz im Bogenschießen",
|
||||
date: "21.06.2024",
|
||||
},
|
||||
{
|
||||
from: "Sebastian",
|
||||
avatar: "https://api.utopia-lab.org/assets/7510a082-882b-41c3-aa7d-5a19f9502f25?width=80&heigth=80",
|
||||
symbol: "🌱",
|
||||
text: "danke fürs Rasen mähen",
|
||||
date: "29.06.2024",
|
||||
},
|
||||
{
|
||||
from: "Yurij",
|
||||
avatar: "https://api.utopia-lab.org/assets/abe62291-35ad-45de-b978-e5906d8a3eb6?width=80&heigth=80",
|
||||
symbol: "🏆",
|
||||
text: "bester Coder ever",
|
||||
date: "04.07.2024",
|
||||
},
|
||||
{
|
||||
from: "Luca",
|
||||
avatar: "https://api.utopia-lab.org/assets/e285e653-36e8-4211-a69d-00053c1f610e?width=80&heigth=80",
|
||||
symbol: "🙏",
|
||||
text: "Vielen Dank für deine Hilfe!!!",
|
||||
date: "04.07.2024",
|
||||
},
|
||||
{
|
||||
from: "Lisa",
|
||||
avatar: "https://i.pinimg.com/originals/c0/ed/08/c0ed088cd6532d4fd27396aefddac57c.jpg",
|
||||
symbol: "❤️",
|
||||
text: "Vielen Dank für deine Hilfe!!!",
|
||||
date: "04.07.2024",
|
||||
},
|
||||
{
|
||||
from: "Timo",
|
||||
avatar: "https://api.utopia-lab.org/assets/262117f8-feb6-444f-9bd2-e84087285760?width=80&heigth=80",
|
||||
symbol: "🥈",
|
||||
text: "2. Platz im Bogenschießen",
|
||||
date: "21.06.2024",
|
||||
},
|
||||
{
|
||||
from: "Anton",
|
||||
avatar: "https://api.utopia-lab.org/assets/007dc678-6073-4ad1-9b47-f2cfe1dca582?width=80&heigth=80",
|
||||
symbol: "🌱",
|
||||
text: "danke fürs Rasen mähen",
|
||||
date: "29.06.2024"
|
||||
},
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
let params = new URLSearchParams(location.search);
|
||||
let urlTab = params.get("tab");
|
||||
urlTab ? setActiveTab(Number(urlTab)) : setActiveTab(1);
|
||||
}, [location])
|
||||
|
||||
return (
|
||||
<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="About me ..." defaultValue={item?.text ? item.text : ""} updateFormValue={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
text: v
|
||||
}))} containerStyle='tw-h-full' inputStyle='tw-h-full tw-border-t-0 tw-rounded-tl-none' />
|
||||
</div>
|
||||
{item.layer?.itemType.questlog &&
|
||||
<>
|
||||
<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="❤️" checked={activeTab == 2 && true}
|
||||
onChange={() => updateActiveTab(2)} />
|
||||
|
||||
<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">
|
||||
<table className="sm:tw-table-sm md:tw-table-md">
|
||||
<tbody>
|
||||
{attestations.map((a, i) => <tr key={i}>
|
||||
<td>
|
||||
<div className='tw-mask tw-mask-circle tw-text-xl md:tw-text-2xl tw-bg-slate-200 tw-rounded-full tw-p-2 tw-my-1 tw-mr-2'>{a.symbol}</div>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<div className='tw-mr-2' ><i>{a.text}</i></div>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="tw-avatar">
|
||||
<div className="tw-mask tw-rounded-full h-8 w-8 tw-mr-2">
|
||||
<img
|
||||
src={a.avatar}
|
||||
alt="Avatar Tailwind CSS Component" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold">{a.from}</div>
|
||||
<div className="tw-text-xs opacity-50 tw-text-zinc-500">{a.date}</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</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-border-[var(--fallback-bc,oklch(var(--bc)/0.2))] tw-rounded-box tw-h-[calc(100dvh-332px)] tw-min-h-56 tw-border-none">
|
||||
<div className='tw-h-full'>
|
||||
<div className='tw-w-full tw-h-[calc(50%-0.75em)] tw-mb-4'>
|
||||
<TagsWidget defaultTags={state.offers} onUpdate={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
offers: v
|
||||
}))} placeholder="enter your offers" containerStyle='tw-bg-transparent tw-w-full tw-h-full tw-mt-3 tw-text-xs tw-h-[calc(100%-1rem)] tw-min-h-[5em] tw-pb-2 tw-overflow-auto' />
|
||||
</div>
|
||||
<div className='tw-w-full tw-h-[calc(50%-0.75em)] '>
|
||||
<TagsWidget defaultTags={state.needs} onUpdate={(v) => setState(prevState => ({
|
||||
...prevState,
|
||||
needs: v
|
||||
}))} placeholder="enter your needs" containerStyle='tw-bg-transparent tw-w-full tw-h-full tw-mt-3 tw-text-xs tw-h-[calc(100%-1rem)] tw-min-h-[5em] tw-pb-2 tw-overflow-auto' />
|
||||
</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-340px)] tw-overflow-y-auto tw-pt-4 tw-pb-1 -tw-mx-4 tw-overflow-x-hidden fade">
|
||||
<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 tw-mb-4'>
|
||||
{state.relations && state.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={(id) => unlinkItem(id, item, updateItem)} 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 customStyle="!tw-bottom-24" collection="items" item={item} existingRelations={state.relations} triggerItemSelected={(id) => linkItem(id, item, updateItem)} colorField={item.layer.itemColorField}></ActionButton>}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
123
src/Components/Profile/Templates/TabsView.tsx
Normal file
123
src/Components/Profile/Templates/TabsView.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import { StartEndView, TextView } from '../../Map'
|
||||
import { TagView } from '../../Templates/TagView'
|
||||
import { LinkedItemsHeaderView } from '../Subcomponents/LinkedItemsHeaderView'
|
||||
import { ActionButton } from '../Subcomponents/ActionsButton'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useAddFilterTag } from '../../Map/hooks/useFilter'
|
||||
import { Item, Tag } from 'utopia-ui/dist/types'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
export const TabsView = ({ item, offers, needs, relations, updatePermission, loading, linkItem, unlinkItem }: { item: Item, offers: Array<Tag>, needs: Array<Tag>, relations: Array<Item>, updatePermission: boolean, loading: boolean, linkItem: (id: string) => Promise<void>, unlinkItem: (id: string) => Promise<void> }) => {
|
||||
|
||||
const addFilterTag = useAddFilterTag();
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [addItemPopupType, setAddItemPopupType] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
scroll();
|
||||
}, [addItemPopupType])
|
||||
|
||||
function scroll() {
|
||||
tabRef.current?.scrollIntoView();
|
||||
}
|
||||
|
||||
const tabRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
const updateActiveTab = (id: number) => {
|
||||
setActiveTab(id);
|
||||
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
let urlTab = params.get("tab");
|
||||
if (!urlTab?.includes(id.toString()))
|
||||
params.set("tab", `${id ? id : ""}`)
|
||||
navigate(location.pathname+ "?" + params.toString());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let params = new URLSearchParams(location.search);
|
||||
let urlTab = params.get("tab");
|
||||
urlTab ? setActiveTab(Number(urlTab)) : setActiveTab(1);
|
||||
}, [location])
|
||||
|
||||
return (
|
||||
<div role="tablist" className="tw-tabs tw-tabs-lifted tw-mt-2 tw-mb-2 tw-px-6">
|
||||
<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={() => {
|
||||
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>
|
||||
</>
|
||||
|
||||
|
||||
}
|
||||
|
||||
{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-mr-4 -tw-mb-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 tw-pb-4'>
|
||||
{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-p-6 tw-mr-4 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>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,88 +1,83 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { TitleCard } from '../Templates/TitleCard'
|
||||
import { TextInput } from '../Input/TextInput'
|
||||
import { TextAreaInput } from '../Input/TextAreaInput'
|
||||
import { toast } from 'react-toastify';
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../Auth';
|
||||
import * as React from 'react'
|
||||
import ReactCrop, { Crop, centerCrop, makeAspectCrop } from 'react-image-crop'
|
||||
import 'react-image-crop/dist/ReactCrop.css'
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { UserItem } from '../../types';
|
||||
import DialogModal from '../Templates/DialogModal';
|
||||
import { useAssetApi } from '../AppShell/hooks/useAssets';
|
||||
import { ColorPicker } from './ColorPicker';
|
||||
import { CardPage, MapOverlayPage } from '../Templates'
|
||||
import { useItems } from '../Map/hooks/useItems'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useState } from 'react';
|
||||
import { Item, UserItem } from '../../types';
|
||||
import { getValue } from '../../Utils/GetValue';
|
||||
import { useMap } from 'react-leaflet';
|
||||
import { LatLng } from 'leaflet';
|
||||
import { TextView } from '../Map';
|
||||
import useWindowDimensions from '../Map/hooks/useWindowDimension';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useAuth } from '../Auth';
|
||||
import { TextInput } from '../Input';
|
||||
|
||||
export function UserSettings() {
|
||||
const { user, updateUser, loading, token } = useAuth();
|
||||
const { user, updateUser, loading, token } = useAuth();
|
||||
|
||||
const [id, setId] = useState<string>("");
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [id, setId] = useState<string>("");
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
|
||||
|
||||
|
||||
const [passwordChanged, setPasswordChanged] = useState<boolean>(false);
|
||||
const [passwordChanged, setPasswordChanged] = useState<boolean>(false);
|
||||
|
||||
|
||||
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
setId(user?.id ? user.id : "");
|
||||
setEmail(user?.email ? user.email : "");
|
||||
setPassword(user?.password ? user.password : "");
|
||||
}, [user])
|
||||
React.useEffect(() => {
|
||||
setId(user?.id ? user.id : "");
|
||||
setEmail(user?.email ? user.email : "");
|
||||
setPassword(user?.password ? user.password : "");
|
||||
}, [user])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const onUpdateUser = () => {
|
||||
let changedUser = {} as UserItem;
|
||||
const onUpdateUser = () => {
|
||||
let changedUser = {} as UserItem;
|
||||
|
||||
changedUser = { id: id, email: email, ...passwordChanged && { password: password }};
|
||||
changedUser = { id: id, email: email, ...passwordChanged && { password: password } };
|
||||
|
||||
|
||||
toast.promise(
|
||||
toast.promise(
|
||||
|
||||
updateUser(changedUser),
|
||||
{
|
||||
pending: 'updating Profile ...',
|
||||
success: 'Profile updated',
|
||||
error: {
|
||||
render({ data }) {
|
||||
return `${data}`
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => navigate("/"));
|
||||
}
|
||||
updateUser(changedUser),
|
||||
{
|
||||
pending: 'updating Profile ...',
|
||||
success: 'Profile updated',
|
||||
error: {
|
||||
render({ data }) {
|
||||
return `${data}`
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => navigate("/"));
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="tw-flex-1 tw-overflow-y-auto tw-overflow-x-hidden tw-pt-8 tw-px-6 tw-bg-base-200 tw-min-w-80 tw-flex tw-justify-center" >
|
||||
<div className='tw-w-full xl:tw-max-w-6xl'>
|
||||
<TitleCard title="Settings" topMargin="tw-mt-2" className='tw-mb-6'>
|
||||
|
||||
|
||||
<div className="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 tw-gap-6">
|
||||
<TextInput type='email' placeholder="E-Mail" defaultValue={user?.email ? user.email : ""} updateFormValue={(v) => setEmail(v)} />
|
||||
<TextInput type='password' placeholder="new Password" defaultValue={user?.password ? user.password : ""} updateFormValue={(v) => {
|
||||
setPassword(v);
|
||||
setPasswordChanged(true);
|
||||
}} />
|
||||
{/* <ToogleInput updateType="syncData" labelTitle="Sync Data" defaultValue={true} updateFormValue={updateFormValue}/> */}
|
||||
return (
|
||||
<MapOverlayPage backdrop className='tw-mx-4 tw-mt-4 tw-max-h-[calc(100dvh-96px)] tw-h-fit md:tw-w-[calc(50%-32px)] tw-w-[calc(100%-32px)] tw-max-w-xl !tw-left-auto tw-top-0 tw-bottom-0'>
|
||||
<div className={`tw-text-xl tw-font-semibold`}>Settings</div>
|
||||
<div className="tw-divider tw-mt-2"></div>
|
||||
<div className="tw-grid tw-grid-cols-1 tw-gap-6">
|
||||
<TextInput type='email' placeholder="new E-Mail" defaultValue={user?.email ? user.email : ""} updateFormValue={(v) => setEmail(v)} />
|
||||
<TextInput type='password' placeholder="new Password" defaultValue={user?.password ? user.password : ""} updateFormValue={(v) => {
|
||||
setPassword(v);
|
||||
setPasswordChanged(true);
|
||||
}} />
|
||||
{/* <ToogleInput updateType="syncData" labelTitle="Sync Data" defaultValue={true} updateFormValue={updateFormValue}/> */}
|
||||
</div>
|
||||
|
||||
<div className="tw-mt-8"><button className={loading ? " tw-loading tw-btn-disabled tw-btn tw-btn-primary tw-float-right" : "tw-btn tw-btn-primary tw-float-right"} onClick={() => onUpdateUser()}>Update</button></div>
|
||||
|
||||
</TitleCard>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
</MapOverlayPage>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
export {UserSettings} from './UserSettings'
|
||||
export {ProfileSettings} from './ProfileSettings'
|
||||
export {OverlayProfile} from './OverlayProfile'
|
||||
export {OverlayProfileSettings} from './OverlayProfileSettings'
|
||||
export {OverlayUserSettings} from './OverlayUserSettings'
|
||||
export {OverlayItemProfile} from './OverlayItemProfile'
|
||||
export {OverlayItemProfileSettings} from './OverlayItemProfileSettings'
|
||||
export {PlusButton} from "./PlusButton"
|
||||
export {PlusButton} from "./Subcomponents/PlusButton"
|
||||
export {ProfileView} from "./ProfileView"
|
||||
export {ProfileForm} from "./ProfileForm"
|
||||
201
src/Components/Profile/itemFunctions.ts
Normal file
201
src/Components/Profile/itemFunctions.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { Item, Tag } from '../../types';
|
||||
import { encodeTag } from '../../Utils/FormatTags';
|
||||
import { hashTagRegex } from '../../Utils/HashTagRegex';
|
||||
import { randomColor } from '../../Utils/RandomColor';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
export const submitNewItem = async (evt: any, type: string, item, user, setLoading, tags, addTag, addItem, linkItem, resetFilterTags, layers, addItemPopupType, setAddItemPopupType) => {
|
||||
evt.preventDefault();
|
||||
const formItem: Item = {} as Item;
|
||||
Array.from(evt.target).forEach((input: HTMLInputElement) => {
|
||||
if (input.name) {
|
||||
formItem[input.name] = input.value;
|
||||
}
|
||||
});
|
||||
setLoading(true);
|
||||
formItem.text && formItem.text.toLocaleLowerCase().match(hashTagRegex)?.map(tag => {
|
||||
if (!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) {
|
||||
addTag({ id: crypto.randomUUID(), name: tag.slice(1), color: randomColor() })
|
||||
}
|
||||
});
|
||||
const uuid = crypto.randomUUID();
|
||||
|
||||
const layer = layers.find(l => l.name.toLocaleLowerCase().replace("s", "") == addItemPopupType.toLocaleLowerCase())
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
await layer?.api?.createItem!({ ...formItem, id: uuid, type: type, parent: item.id });
|
||||
await linkItem(uuid);
|
||||
success = true;
|
||||
} catch (error) {
|
||||
toast.error(error.toString());
|
||||
}
|
||||
if (success) {
|
||||
addItem({ ...formItem, id: uuid, type: type, layer: layer, user_created: user, parent: item.id });
|
||||
toast.success("New item created");
|
||||
resetFilterTags();
|
||||
}
|
||||
setLoading(false);
|
||||
setAddItemPopupType("");
|
||||
}
|
||||
|
||||
export const linkItem = async (id: string, item, updateItem) => {
|
||||
let new_relations = item.relations || [];
|
||||
new_relations?.push({ items_id: item.id, related_items_id: id })
|
||||
const updatedItem = { id: item.id, relations: new_relations }
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
await item?.layer?.api?.updateItem!(updatedItem)
|
||||
success = true;
|
||||
} catch (error) {
|
||||
toast.error(error.toString());
|
||||
}
|
||||
if (success) {
|
||||
updateItem({ ...item, relations: new_relations })
|
||||
toast.success("Item linked");
|
||||
}
|
||||
}
|
||||
|
||||
export const unlinkItem = async (id: string, item, updateItem) => {
|
||||
let new_relations = item.relations?.filter(r => r.related_items_id !== id)
|
||||
const updatedItem = { id: item.id, relations: new_relations }
|
||||
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
await item?.layer?.api?.updateItem!(updatedItem)
|
||||
success = true;
|
||||
} catch (error) {
|
||||
toast.error(error.toString());
|
||||
}
|
||||
if (success) {
|
||||
updateItem({ ...item, relations: new_relations })
|
||||
toast.success("Item unlinked");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const handleDelete = async (event: React.MouseEvent<HTMLElement>, item, setLoading, removeItem, map, navigate) => {
|
||||
event.stopPropagation();
|
||||
setLoading(true);
|
||||
let success = false;
|
||||
try {
|
||||
await item.layer?.api?.deleteItem!(item.id)
|
||||
success = true;
|
||||
} catch (error) {
|
||||
toast.error(error.toString());
|
||||
}
|
||||
if (success) {
|
||||
removeItem(item);
|
||||
toast.success("Item deleted");
|
||||
}
|
||||
setLoading(false);
|
||||
map.closePopup();
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
window.history.pushState({}, "", "/" + `${params ? `?${params}` : ""}`);
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
|
||||
export const onUpdateItem = async (state, item, tags, addTag, setLoading, navigate, updateItem, addItem, user, params) => {
|
||||
let changedItem = {} as Item;
|
||||
|
||||
let offer_updates: Array<any> = [];
|
||||
//check for new offers
|
||||
state.offers?.map(o => {
|
||||
const existingOffer = item?.offers?.find(t => t.tags_id === o.id)
|
||||
existingOffer && offer_updates.push(existingOffer.id)
|
||||
if (!existingOffer && !tags.some(t => t.id === o.id)) addTag({ ...o, offer_or_need: true })
|
||||
!existingOffer && offer_updates.push({ items_id: item?.id, tags_id: o.id })
|
||||
});
|
||||
|
||||
let needs_updates: Array<any> = [];
|
||||
|
||||
state.needs?.map(n => {
|
||||
const existingNeed = item?.needs?.find(t => t.tags_id === n.id)
|
||||
existingNeed && needs_updates.push(existingNeed.id)
|
||||
!existingNeed && needs_updates.push({ items_id: item?.id, tags_id: n.id })
|
||||
!existingNeed && !tags.some(t => t.id === n.id) && addTag({ ...n, offer_or_need: true })
|
||||
});
|
||||
|
||||
|
||||
// update profile item in current state
|
||||
changedItem = {
|
||||
id: state.id,
|
||||
group_type: state.groupType,
|
||||
status: state.status,
|
||||
name: state.name,
|
||||
subname: state.subname,
|
||||
text: state.text,
|
||||
color: state.color,
|
||||
position: item.position,
|
||||
contact: state.contact,
|
||||
telephone: state.telephone,
|
||||
...state.markerIcon && { markerIcon: state.markerIcon },
|
||||
next_appointment: state.nextAppointment,
|
||||
...state.image.length > 10 && { image: state.image },
|
||||
...state.offers.length > 0 && { offers: offer_updates },
|
||||
...state.needs.length > 0 && { needs: needs_updates }
|
||||
};
|
||||
|
||||
let offers_state: Array<any> = [];
|
||||
let needs_state: Array<any> = [];
|
||||
|
||||
await state.offers.map(o => {
|
||||
offers_state.push({ items_id: item?.id, tags_id: o.id })
|
||||
});
|
||||
|
||||
await state.needs.map(n => {
|
||||
needs_state.push({ items_id: item?.id, tags_id: n.id })
|
||||
});
|
||||
|
||||
changedItem = { ...changedItem, offers: offers_state, needs: needs_state };
|
||||
|
||||
|
||||
state.text.toLocaleLowerCase().match(hashTagRegex)?.map(tag => {
|
||||
if (!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) {
|
||||
addTag({ id: crypto.randomUUID(), name: encodeTag(tag.slice(1).toLocaleLowerCase()), color: randomColor() })
|
||||
}
|
||||
});
|
||||
|
||||
setLoading(true);
|
||||
|
||||
if (!item.new) {
|
||||
item?.layer?.api?.updateItem && toast.promise(
|
||||
item?.layer?.api?.updateItem(changedItem),
|
||||
{
|
||||
pending: 'updating Item ...',
|
||||
success: 'Item updated',
|
||||
error: {
|
||||
render({ data }) {
|
||||
return `${data}`
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => item && updateItem({ ...item, ...changedItem }))
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
navigate(`/item/${item.id}${params && "?" + params}`)
|
||||
});
|
||||
|
||||
}
|
||||
else {
|
||||
item.layer?.api?.createItem && toast.promise(
|
||||
item.layer?.api?.createItem(changedItem),
|
||||
{
|
||||
pending: 'updating Item ...',
|
||||
success: 'Item updated',
|
||||
error: {
|
||||
render({ data }) {
|
||||
return `${data}`
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => item && addItem({ ...item, ...changedItem, layer: item.layer, user_created: user, type: item.layer?.itemType }))
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
navigate(`/${params && "?" + params}`)
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { CardPage } from "./CardPage";
|
||||
import { ItemsApi } from "../../types";
|
||||
import { getValue } from "../../Utils/GetValue";
|
||||
|
||||
|
||||
type breadcrumb = {
|
||||
name: string,
|
||||
path: string
|
||||
}
|
||||
|
||||
|
||||
export const ItemViewPage = ({ api, parents, itemNameField, itemTextField, itemImageField, itemSymbolField }: { api: ItemsApi<any>, parents: Array<breadcrumb>, itemNameField: string, itemTextField: string, itemImageField: string, itemSymbolField: string }) => {
|
||||
|
||||
const [item, setItem] = useState<any>();
|
||||
|
||||
let location = useLocation();
|
||||
|
||||
|
||||
const loadProject = async () => {
|
||||
if (api?.getItem) {
|
||||
const project: unknown = await api?.getItem(location.pathname.split("/")[2]);
|
||||
setItem(project as any);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
loadProject();
|
||||
}, [api])
|
||||
|
||||
return (
|
||||
<CardPage title={getValue(item, itemNameField) || ""} parents={parents}>
|
||||
{item &&
|
||||
<>
|
||||
{getValue(item, itemImageField) ?
|
||||
<div className=' tw-h-36 flex items-center justify-center '>
|
||||
<img className='tw-h-24' src={`https://api.utopia-lab.org/assets/${getValue(item, itemImageField)}`}></img>
|
||||
</div> :
|
||||
<div className="tw-h-36 !bg-transparent tw-flex tw-items-center tw-justify-center tw-text-7xl">
|
||||
{getValue(item, itemSymbolField)}
|
||||
</div>
|
||||
}
|
||||
<p className='text-sm mb-2'>{getValue(item, itemTextField)}</p>
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
</CardPage>
|
||||
)
|
||||
}
|
||||
@ -1,173 +0,0 @@
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Item, ItemsApi } from '../../types';
|
||||
import { getValue } from '../../Utils/GetValue';
|
||||
import { TextView } from '../Map';
|
||||
import { useAssetApi } from '../AppShell/hooks/useAssets';
|
||||
import { PlusButton } from '../Profile/PlusButton';
|
||||
import { TextInput, TextAreaInput } from '../Input';
|
||||
import { useAddTag, useTags } from '../Map/hooks/useTags';
|
||||
import { useAddItem } from '../Map/hooks/useItems';
|
||||
import { useResetFilterTags } from '../Map/hooks/useFilter';
|
||||
import { toast } from 'react-toastify';
|
||||
import { hashTagRegex } from '../../Utils/HashTagRegex';
|
||||
import { randomColor } from '../../Utils/RandomColor';
|
||||
import { useAuth } from '../Auth';
|
||||
import { useLayers } from '../Map/hooks/useLayers';
|
||||
import { HeaderView } from '../Map/Subcomponents/ItemPopupComponents/HeaderView';
|
||||
|
||||
|
||||
type breadcrumb = {
|
||||
name: string,
|
||||
path: string
|
||||
}
|
||||
|
||||
|
||||
export const ItemsIndexPage = ({ api, url, parameterField, breadcrumbs, itemNameField, itemTextField, itemImageField, itemSymbolField, itemSubnameField, children }: { api: ItemsApi<any>, url: string, parameterField: string, breadcrumbs: Array<breadcrumb>, itemNameField: string, itemTextField: string, itemImageField: string, itemSymbolField: string, itemSubnameField: string, children?: ReactNode }) => {
|
||||
|
||||
console.log(itemSymbolField);
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [addItemPopupType, setAddItemPopupType] = useState<string>("");
|
||||
|
||||
const tabRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
function scroll() {
|
||||
tabRef.current?.scrollIntoView();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
scroll();
|
||||
}, [addItemPopupType])
|
||||
|
||||
const [items, setItems] = useState<any[]>([]);
|
||||
|
||||
const loadProjects = async () => {
|
||||
const items = await api?.getItems();
|
||||
setItems(items as any);
|
||||
}
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const tags = useTags();
|
||||
const addTag = useAddTag();
|
||||
const { user } = useAuth();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
loadProjects();
|
||||
}, [api])
|
||||
|
||||
const layers = useLayers();
|
||||
|
||||
const submitNewItem = async (evt: any, type: string) => {
|
||||
evt.preventDefault();
|
||||
const formItem: Item = {} as Item;
|
||||
Array.from(evt.target).forEach((input: HTMLInputElement) => {
|
||||
if (input.name) {
|
||||
formItem[input.name] = input.value;
|
||||
}
|
||||
});
|
||||
setLoading(true);
|
||||
formItem.text && formItem.text.toLocaleLowerCase().match(hashTagRegex)?.map(tag => {
|
||||
if (!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())) {
|
||||
addTag({ id: crypto.randomUUID(), name: tag.slice(1), color: randomColor() })
|
||||
}
|
||||
});
|
||||
const uuid = crypto.randomUUID();
|
||||
let success = false;
|
||||
try {
|
||||
await api?.createItem!({ ...formItem, id: uuid, type: type });
|
||||
success = true;
|
||||
} catch (error) {
|
||||
toast.error(error.toString());
|
||||
}
|
||||
if (success) {
|
||||
toast.success("New item created");
|
||||
}
|
||||
setLoading(false);
|
||||
setAddItemPopupType("");
|
||||
setItems(current => [...current, { ...formItem, id: uuid, type: type, layer: layers.find(l => l.name == addItemPopupType), user_created: user }])
|
||||
}
|
||||
|
||||
const deleteItem = async (item) => {
|
||||
setLoading(true);
|
||||
let success = false;
|
||||
try {
|
||||
await api?.deleteItem!(item.id)
|
||||
success = true;
|
||||
} catch (error) {
|
||||
toast.error(error.toString());
|
||||
}
|
||||
if (success) {
|
||||
toast.success("Item deleted");
|
||||
}
|
||||
setLoading(false);
|
||||
setItems(items.filter(i=>i.id !=item.id))
|
||||
console.log("chaka");
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<main className="tw-flex-1 tw-overflow-y-auto tw-pt-2 tw-px-6 tw-bg-base-200 tw-min-w-80 tw-flex tw-justify-center" >
|
||||
<div className=' tw-w-full xl:tw-max-w-6xl'>
|
||||
{breadcrumbs &&
|
||||
<div className="tw-text-sm tw-breadcrumbs">
|
||||
<ul>
|
||||
{breadcrumbs.map((b, i) => <li key={i}><Link to={b.path} >{b.name}</Link></li>)}
|
||||
</ul>
|
||||
</div>}
|
||||
{/**
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4 mt-2 ">
|
||||
<TextInput defaultValue='' placeholder='🔍 Search' containerStyle='lg:col-span-2' updateFormValue={(val) => { setSearch(val) }}></TextInput>
|
||||
<SelectBox updateFormValue={() => { }} placeholder="Type" containerStyle=' hidden md:grid' defaultValue='PLACEHOLDER' options={[{ name: "local", value: "local" }, { name: "project", value: "project" }]} />
|
||||
</div>
|
||||
<div className="divider" ></div>
|
||||
|
||||
*/}
|
||||
|
||||
|
||||
|
||||
<div className="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 lg:tw-grid-cols-3 tw-gap-6 tw-pt-4">
|
||||
{
|
||||
items?.map((i, k) => {
|
||||
return (
|
||||
<div key={k} 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-4 tw-mb-4 tw-h-fit' onClick={() => navigate(url + getValue(i, parameterField))}>
|
||||
<HeaderView loading={loading} item={i} api={api} itemAvatarField={itemImageField} itemNameField={itemNameField} itemSubnameField={itemSubnameField} editCallback={() => navigate("/edit-item/"+i.id)} deleteCallback={()=>deleteItem(i)}></HeaderView>
|
||||
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
|
||||
<TextView truncate item={i} itemTextField={itemTextField} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
|
||||
})
|
||||
}
|
||||
{addItemPopupType == "project" ?
|
||||
|
||||
<form ref={tabRef} autoComplete='off' onSubmit={e => submitNewItem(e, addItemPopupType)} >
|
||||
|
||||
<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>
|
||||
<TextInput type="text" placeholder="Name" dataField="name" defaultValue={""} inputStyle='' />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</form> : <></>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<PlusButton triggerAction={() => { setAddItemPopupType("project"); scroll(); }} color={'#777'} collection='items' />
|
||||
{children}
|
||||
</main>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
@ -3,7 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Item, ItemsApi, LayerProps } from '../../types';
|
||||
import { getValue } from '../../Utils/GetValue';
|
||||
import { PopupStartEndInput, StartEndView, TextView } from '../Map';
|
||||
import { PlusButton } from '../Profile/PlusButton';
|
||||
import { PlusButton } from '../Profile/Subcomponents/PlusButton';
|
||||
import { TextInput, TextAreaInput } from '../Input';
|
||||
import { useAddTag, useGetItemTags, useTags } from '../Map/hooks/useTags';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
export {CardPage} from './CardPage'
|
||||
export {TitleCard} from './TitleCard'
|
||||
export {MapOverlayPage} from './MapOverlayPage'
|
||||
export {CircleLayout} from './CircleLayout'
|
||||
export {MoonCalendar} from './MoonCalendar'
|
||||
export {ItemsIndexPage} from "./ItemsIndexPage"
|
||||
export {ItemViewPage} from "./ItemViewPage"
|
||||
export {SelectUser} from "./SelectUser"
|
||||
export {OverlayItemsIndexPage} from "./OverlayItemsIndexPage"
|
||||
export {SelectUser} from "./SelectUser"
|
||||
@ -3,7 +3,7 @@
|
||||
@tailwind utilities;
|
||||
|
||||
.fade {
|
||||
mask-image: linear-gradient(180deg, transparent, #000 2%, #000 98%, transparent);
|
||||
mask-image: linear-gradient(180deg, transparent, #000 1%, #000 99%, transparent);
|
||||
}
|
||||
|
||||
.tw-modal {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
export { UtopiaMap, Layer, Tags, Permissions, ItemForm, ItemView, PopupTextAreaInput, PopupStartEndInput, PopupTextInput, PopupButton, TextView, StartEndView, PopupCheckboxInput } from './Components/Map';
|
||||
export {AppShell, Content, SideBar} from "./Components/AppShell"
|
||||
export {AppShell, Content, SideBar, Sitemap } from "./Components/AppShell"
|
||||
export {AuthProvider, useAuth, LoginPage, SignupPage, RequestPasswordPage, SetNewPasswordPage} from "./Components/Auth"
|
||||
export {UserSettings, ProfileSettings, OverlayProfile, OverlayProfileSettings, OverlayUserSettings, OverlayItemProfile, OverlayItemProfileSettings} from './Components/Profile'
|
||||
export {UserSettings, ProfileView, ProfileForm} from './Components/Profile'
|
||||
export {Quests, Modal} from './Components/Gaming'
|
||||
export {TitleCard, CardPage, MapOverlayPage, OverlayItemsIndexPage, CircleLayout, MoonCalendar, ItemsIndexPage, ItemViewPage, SelectUser} from './Components/Templates'
|
||||
export {TitleCard, CardPage, MapOverlayPage, OverlayItemsIndexPage, MoonCalendar, SelectUser } from './Components/Templates'
|
||||
export {TextInput, TextAreaInput, SelectBox} from './Components/Input'
|
||||
|
||||
import "./index.css"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user