/src/Components/Profile cleanup

This commit is contained in:
Anton Tranelis 2024-07-23 11:49:36 +02:00
parent 546fde0bf6
commit 3f88f3a801
30 changed files with 64 additions and 886 deletions

View File

@ -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 }) {

View File

@ -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>
);
}

View File

@ -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>
)
}

View File

@ -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 >
)
}

View File

@ -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>
</>
)
}

View File

@ -5,25 +5,25 @@ import { toast } from 'react-toastify';
import { useAuth } from '../Auth';
import { TextInput, TextAreaInput } from '../Input';
import ComboBoxInput from '../Input/ComboBoxInput';
import { ColorPicker } from './ColorPicker';
import { ColorPicker } from './Subcomponents/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 { AvatarWidget } from './Subcomponents/AvatarWidget';
import { encodeTag } from '../../Utils/FormatTags';
import { useLayers } from '../Map/hooks/useLayers';
import { TagsWidget } from './TagsWidget';
import { LinkedItemsHeaderView } from './LinkedItemsHeaderView';
import { TagsWidget } from './Subcomponents/TagsWidget';
import { LinkedItemsHeaderView } from './Subcomponents/LinkedItemsHeaderView';
import { TextView } from '../Map';
import { ActionButton } from './ActionsButton';
import { ActionButton } from './Subcomponents/ActionsButton';
import { useHasUserPermission } from '../Map/hooks/usePermissions';
export function OverlayItemProfileSettings({ userType }: { userType: string }) {
export function ProfileForm({ userType }: { userType: string }) {
const typeMapping = [
{ value: 'wuerdekompass', label: 'Regional-Gruppe' },

View File

@ -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>
)
}

View File

@ -14,19 +14,19 @@ 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 { ActionButton } from './Subcomponents/ActionsButton';
import { LinkedItemsHeaderView } from './Subcomponents/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 RelationCard from "./Subcomponents/RelationCard";
import ContactInfo from "./Subcomponents/ContactInfo";
import ProfileSubHeader from "./Subcomponents/ProfileSubHeader";
export function OverlayItemProfile({ userType }: { userType: string }) {
export function ProfileView({ userType }: { userType: string }) {
const [updatePermission, setUpdatePermission] = useState<boolean>(false);
const [relations, setRelations] = useState<Array<Item>>([]);

View File

@ -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,

View File

@ -1,8 +1,9 @@
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>>}) => {

View File

@ -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);

View File

@ -1,5 +1,5 @@
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();

View File

@ -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";

View File

@ -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();

View File

@ -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 { 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}) => {

View File

@ -0,0 +1,7 @@
import * as React from 'react'
export const Onepager = () => {
return (
<div>Onepager</div>
)
}

View File

@ -0,0 +1,7 @@
import * as React from 'react'
export const Simple = () => {
return (
<div>Simple</div>
)
}

View File

@ -0,0 +1,7 @@
import * as React from 'react'
export const Tabs = () => {
return (
<div>Tabs</div>
)
}

View File

@ -1,88 +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 UserSettings() {
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();
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 (
<>
<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}/> */}
</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>
</>
)
}

View File

@ -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"

View File

@ -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>
)
}

View File

@ -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';

View File

@ -3,6 +3,5 @@ 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 {OverlayItemsIndexPage} from "./OverlayItemsIndexPage"

View File

@ -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, 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 {OverlayUserSettings, ProfileView, ProfileForm} from './Components/Profile'
export {Quests, Modal} from './Components/Gaming'
export {TitleCard, CardPage, MapOverlayPage, OverlayItemsIndexPage, CircleLayout, MoonCalendar, ItemsIndexPage, ItemViewPage} from './Components/Templates'
export {TitleCard, CardPage, MapOverlayPage, OverlayItemsIndexPage, CircleLayout, MoonCalendar, ItemViewPage} from './Components/Templates'
export {TextInput, TextAreaInput, SelectBox} from './Components/Input'
import "./index.css"