MapOverlay Pages and Advanced Profiles

This commit is contained in:
Anton Tranelis 2024-02-05 10:06:38 +01:00
parent 2844bbfeff
commit a0113d0fc5
22 changed files with 501 additions and 37 deletions

54
package-lock.json generated
View File

@ -25,6 +25,7 @@
"react-string-replace": "^1.1.1",
"react-toastify": "^9.1.3",
"rehype-video": "^2.0.2",
"remark-breaks": "^4.0.0",
"tributejs": "^5.1.3",
"tw-elements": "^1.0.0"
},
@ -1579,6 +1580,17 @@
"node": ">=6"
}
},
"node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint": {
"version": "8.24.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz",
@ -3037,6 +3049,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mdast-util-find-and-replace": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz",
"integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==",
"dependencies": {
"@types/mdast": "^4.0.0",
"escape-string-regexp": "^5.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-from-markdown": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz",
@ -3118,6 +3145,19 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-newline-to-break": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz",
"integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-find-and-replace": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-phrasing": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.0.0.tgz",
@ -4982,6 +5022,20 @@
"url": "https://jaywcjlove.github.io/#/sponsor"
}
},
"node_modules/remark-breaks": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz",
"integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-newline-to-break": "^2.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-parse": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",

View File

@ -58,6 +58,7 @@
"react-string-replace": "^1.1.1",
"react-toastify": "^9.1.3",
"rehype-video": "^2.0.2",
"remark-breaks": "^4.0.0",
"tributejs": "^5.1.3",
"tw-elements": "^1.0.0"
}

View File

@ -68,8 +68,8 @@ export default function NavBar({ appName, nameWidth = 200}: { appName: string, n
</svg>
</label>
<ul tabIndex={0} className="tw-menu tw-menu-compact tw-dropdown-content tw-mt-3 tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-w-52 !tw-z-[10000]">
<li><Link to={"/profile"}>Profile</Link></li>
<li><Link to={"/settings"}>Settings</Link></li>
<li><Link to={"/profile-settings"}>Profile</Link></li>
<li><Link to={"/user-settings"}>Settings</Link></li>
<li><a onClick={() => { onLogout() }}>Logout</a></li>
</ul>
</div>

View File

@ -38,7 +38,7 @@ export function LoginPage() {
}
return (
<MapOverlayPage>
<MapOverlayPage backdrop className='tw-max-w-xs tw-h-fit'>
<h2 className='tw-text-2xl tw-font-semibold tw-mb-2 tw-text-center'>Login</h2>
<input type="email" placeholder="E-Mail" value={email} onChange={e => setEmail(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" />
<input type="password" placeholder="Password" onChange={e => setPassword(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" />

View File

@ -36,7 +36,7 @@ export function RequestPasswordPage({reset_url}) {
}
return (
<MapOverlayPage>
<MapOverlayPage backdrop className='tw-max-w-xs tw-h-fit'>
<h2 className='tw-text-2xl tw-font-semibold tw-mb-2 tw-text-center'>Reset Password</h2>
<input type="email" placeholder="E-Mail" value={email} onChange={e => setEmail(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" />
<div className="tw-card-actions tw-mt-4">

View File

@ -38,7 +38,7 @@ export function SetNewPasswordPage() {
}
return (
<MapOverlayPage>
<MapOverlayPage backdrop className='tw-max-w-xs tw-h-fit'>
<h2 className='tw-text-2xl tw-font-semibold tw-mb-2 tw-text-center'>Set new Password</h2>
<input type="password" placeholder="Password" onChange={e => setPassword(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" />
<div className="tw-card-actions tw-mt-4">

View File

@ -41,7 +41,7 @@ export function SignupPage() {
return (
<MapOverlayPage>
<MapOverlayPage backdrop className='tw-max-w-xs tw-h-fit'>
<h2 className='tw-text-2xl tw-font-semibold tw-mb-2 tw-text-center'>Sign Up</h2>
<input type="text" placeholder="Name" value={userName} onChange={e => setUserName(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" />
<input type="email" placeholder="E-Mail" value={email} onChange={e => setEmail(e.target.value)} className="tw-input tw-input-bordered tw-w-full tw-max-w-xs" />

View File

@ -64,7 +64,7 @@ export function TextAreaInput({ labelTitle, dataField, labelStyle, containerStyl
{labelTitle ? <label className="tw-label">
<span className={"tw-label-text tw-text-base-content " + labelStyle}>{labelTitle}</span>
</label> : ""}
<textarea required ref={ref} defaultValue={defaultValue} name={dataField} className={`tw-textarea tw-textarea-bordered tw-w-full tw-leading-5 ${inputStyle ? inputStyle : ""}`} placeholder={placeholder || ""} onChange={(e) => updateFormValue && updateFormValue(e.target.value)}></textarea>
<textarea required ref={ref} defaultValue={defaultValue} name={dataField} className={`tw-textarea tw-textarea-bordered tw-w-full tw-leading-5 ${inputStyle ? inputStyle : ""}`} placeholder={placeholder || ""} onChange={(e) => updateFormValue && updateFormValue(e.target.value)}></textarea>
</div>
)
}

View File

@ -89,7 +89,7 @@ export function HeaderView({ item, setItemFormPopup }: {
</div>
<div className='tw-col-span-1'>
{(item.layer?.api?.deleteItem || item.layer?.api?.updateItem)
&& ((user && owner === user.id) || owner == undefined)
&& ((user && owner?.id === user.id) || owner == undefined)
&& (hasUserPermission(item.layer.api?.collectionName!, "delete") || hasUserPermission(item.layer.api?.collectionName!, "update")) &&
<div className="tw-dropdown tw-dropdown-bottom">
<label tabIndex={0} className="tw-bg-base-100 tw-btn tw-m-1 tw-leading-3 tw-border-none tw-min-h-0 tw-h-6">

View File

@ -5,8 +5,8 @@ import { useAddFilterTag } from '../../hooks/useFilter';
import { hashTagRegex } from '../../../../Utils/HashTagRegex';
import { fixUrls, mailRegex } from '../../../../Utils/ReplaceURLs';
import Markdown from 'react-markdown'
import rehypeVideo from 'rehype-video';
import { getValue } from '../../../../Utils/GetValue';
import remarkBreaks from 'remark-breaks'
export const TextView = ({ item, truncate = false}: { item?: Item, truncate?: boolean }) => {
const tags = useTags();
@ -60,7 +60,7 @@ export const TextView = ({ item, truncate = false}: { item?: Item, truncate?: bo
<h6 className="tw-text-sm tw-font-bold">{children}</h6>
);
const CustomParagraph = ({ children }) => (
<p className="!tw-my-1">{children}</p>
<p className="!tw-my-2">{children}</p>
);
const CustomUnorderdList = ({ children }) => (
<ul className="tw-list-disc tw-list-inside">{children}</ul>
@ -80,13 +80,10 @@ export const TextView = ({ item, truncate = false}: { item?: Item, truncate?: bo
/>
);
const CustomExternalLink = ({ href, children }) => (
<a
<a className='tw-font-bold'
href={href}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
> {children}</a>
);
const CustomHashTagLink = ({ children, tag, item }) => {
return (
@ -101,9 +98,10 @@ export const TextView = ({ item, truncate = false}: { item?: Item, truncate?: bo
}}>{children}</a>
)};
return (
//@ts-ignore
<Markdown rehypePlugins={[rehypeVideo]} components={{
<Markdown className={`tw-text-map tw-leading-map `} remarkPlugins={[remarkBreaks]} components={{
p: CustomParagraph,
a: ({ href, children }) => {
// Prüft, ob der Link ein YouTube-Video ist

View File

@ -23,7 +23,7 @@ export const ItemViewPopup = React.forwardRef((props: ItemViewPopupProps, ref: a
<LeafletPopup ref={ref} maxHeight={377} minWidth={275} maxWidth={275} autoPanPadding={[20, 80]}>
<div className='tw-bg-base-100 tw-text-base-content'>
<HeaderView item={props.item} setItemFormPopup={props.setItemFormPopup} />
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64'>
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
{props.children ?
React.Children.toArray(props.children).map((child) =>
@ -38,7 +38,7 @@ export const ItemViewPopup = React.forwardRef((props: ItemViewPopupProps, ref: a
}
</div>
<div className='tw-flex -tw-mb-1 tw-flex-row tw-mr-2'>
<div className='tw-flex -tw-mb-1 tw-flex-row tw-mr-2 tw-mt-1'>
{

View File

@ -57,10 +57,8 @@ function UtopiaMap({
},
moveend: (e) => {
console.log(e);
}
})
return null
@ -83,7 +81,7 @@ function UtopiaMap({
return (
<>
<Outlet></Outlet>
<LayersProvider initialLayers={[]}>
<TagsProvider initialTags={[]}>
<PermissionsProvider initialPermissions={[]}>
@ -92,6 +90,7 @@ function UtopiaMap({
<LeafletRefsProvider initialLeafletRefs={{}}>
<div className={(selectNewItemPosition != null ? "crosshair-cursor-enabled" : undefined)}>
<MapContainer ref={mapDivRef} style={{ height: height, width: width }} center={new LatLng(center[0], center[1])} zoom={zoom} zoomControl={false}>
<Outlet></Outlet>
<Control position='topLeft' zIndex="1000">
<SearchControl clusterRef={clusterRef} />
<TagsControl />

View File

@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import * as React from "react";
import { HexColorPicker } from "react-colorful";
import "./ColorPicker.css"
@ -11,8 +11,27 @@ export const ColorPicker = ({ color, onChange, className }) => {
const close = useCallback(() => toggle(false), []);
useClickOutside(popover, close);
const colorPickerRef = React.useRef<HTMLDivElement>(null)
useEffect(() => {
// Füge dem Color-Picker explizit Event-Listener hinzu
const colorPickerElement = colorPickerRef.current;
if (colorPickerElement) {
const enablePropagation = (event) => {
// Verhindere, dass Leaflet die Propagation stoppt
event.stopPropagation = () => {};
};
// Event-Listener für den Color-Picker
['click', 'dblclick', 'mousedown', 'touchstart'].forEach(eventType => {
colorPickerElement.addEventListener(eventType, enablePropagation, true);
});
}
}, []);
return (
<div className={`picker ${className}`}>
<div ref={colorPickerRef} className={`picker ${className}`}>
<div
className="swatch"
style={{ backgroundColor: color }}

View File

@ -0,0 +1,50 @@
import * as React from 'react'
import { CardPage, MapOverlayPage } from '../Templates'
import { useItems } from '../Map/hooks/useItems'
import { useLocation } from 'react-router-dom'
import { useState } from 'react';
import { Item } 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';
export function OverlayProfile() {
const location = useLocation();
const items = useItems();
const [item, setItem] = useState<Item>({} as Item)
const map = useMap();
const windowDimension = useWindowDimensions();
React.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])
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-xl !tw-left-auto tw-top-0 tw-bottom-0'>
{item &&
<>
<div className="flex flex-row tw-w-full">
<p className="text-4xl">{item.layer?.itemAvatarField && getValue(item, item.layer.itemAvatarField) && <img className='h-20 rounded-full 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>
<div className='tw-overflow-y-auto tw-h-full tw-pt-4 fade'>
<TextView item={item} />
</div>
</>
}
</MapOverlayPage>
)
}

View File

@ -0,0 +1,238 @@
import * as React from 'react'
import { useItems, useUpdateItem } from '../Map/hooks/useItems'
import { useEffect, useState } from 'react';
import { getValue } from '../../Utils/GetValue';
import ReactCrop, { Crop, centerCrop, makeAspectCrop } from 'react-image-crop';
import { toast } from 'react-toastify';
import { useAssetApi } from '../AppShell/hooks/useAssets';
import { useAuth } from '../Auth';
import { TextInput, TextAreaInput } from '../Input';
import { ColorPicker } from './ColorPicker';
import DialogModal from '../Templates/DialogModal';
import { hashTagRegex } from '../../Utils/HashTagRegex';
import { useAddTag, useTags } from '../Map/hooks/useTags';
import { randomColor } from '../../Utils/RandomColor';
import { useNavigate } from 'react-router-dom';
import { UserItem } from '../../types';
import { MapOverlayPage } from '../Templates';
export function OverlayProfileSettings() {
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 items = useItems();
const updateItem = useUpdateItem();
const tags = useTags();
const addTag = useAddTag();
const navigate = useNavigate();
React.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 = React.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 } };
const item = items.find(i => i.layer?.itemOwnerField && getValue(i, i.layer?.itemOwnerField).id === id);
if (item && item.layer && item.layer.itemOwnerField) item[item.layer.itemOwnerField] = changedUser;
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).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-xl !tw-left-auto tw-top-0 tw-bottom-0'>
<div className='tw-flex tw-flex-col tw-h-full'>
<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 tw-grow">
<TextAreaInput placeholder="About me, Contact, #Tags, ..." defaultValue={user?.description ? user.description : ""} updateFormValue={(v) => setText(v)} containerStyle='tw-h-full' inputStyle='tw-h-full'/>
</div>
</div>
<div className="tw-mt-2"><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>
<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>
</>
)
}

View File

@ -0,0 +1,83 @@
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 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>
)
}

View File

@ -1,2 +1,5 @@
export {UserSettings} from './UserSettings'
export {ProfileSettings} from './ProfileSettings'
export {ProfileSettings} from './ProfileSettings'
export {OverlayProfile} from './OverlayProfile'
export {OverlayProfileSettings} from './OverlayProfileSettings'
export {OverlayUserSettings} from './OverlayUserSettings'

View File

@ -12,7 +12,7 @@ export function CardPage({title, hideTitle, children, parent} : {
return (
<main className="tw-flex-1 tw-overflow-y-auto tw-overflow-x-hidden tw-pt-2 tw-px-6 tw-bg-base-200 tw-min-w-80 tw-flex tw-justify-center" >
<main className="tw-flex-1 tw-overflow-y-auto tw-overflow-x-hidden tw-pt-2 tw-px-6 tw-min-w-80 tw-flex tw-justify-center" >
<div className='tw-w-full xl:tw-max-w-6xl '>
<div className="tw-text-sm tw-breadcrumbs">
<ul>

View File

@ -1,8 +1,9 @@
import * as L from 'leaflet';
import * as React from 'react'
import { useNavigate } from 'react-router-dom';
export function MapOverlayPage({children} : {children: React.ReactNode}) {
export function MapOverlayPage({ children, className, backdrop }: { children: React.ReactNode, className?: string, backdrop?: boolean }) {
const closeScreen = () => {
@ -11,20 +12,28 @@ export function MapOverlayPage({children} : {children: React.ReactNode}) {
const navigate = useNavigate();
const overlayRef = React.createRef<HTMLDivElement>()
React.useEffect(() => {
if (overlayRef.current !== null) {
L.DomEvent.disableClickPropagation(overlayRef.current)
L.DomEvent.disableScrollPropagation(overlayRef.current)
}
}, [overlayRef])
return (
<div className="tw-absolute tw-z-1000 tw-h-full tw-w-full tw-m-auto">
<div className='tw-backdrop-brightness-75 tw-h-full tw-w-full tw-grid tw-place-items-center tw-m-auto'
>
<div className='tw-card tw-shadow-xl tw-bg-base-100 tw-p-4 tw-max-w-xs tw-absolute tw-top-0 tw-bottom-0 tw-right-0 tw-left-0 tw-m-auto tw-h-fit '>
<div className="tw-card-body tw-p-2">
{children}
<button className="tw-btn tw-btn-sm tw-btn-circle tw-btn-ghost tw-absolute tw-right-2 tw-top-2" onClick={() => closeScreen()}></button>
<div className={`tw-absolute tw-h-full tw-w-full tw-m-auto ${backdrop && "tw-z-[2000]"}`}>
<div className={`${backdrop && "tw-backdrop-brightness-50"} tw-h-full tw-w-full tw-grid tw-place-items-center tw-m-auto`} >
<div ref={overlayRef} className={`tw-card tw-shadow-xl tw-bg-base-100 tw-p-4 ${className && className} ${!backdrop && "tw-z-[2000]"} tw-absolute tw-top-0 tw-bottom-0 tw-right-0 tw-left-0 tw-m-auto`}>
<div className="tw-card-body tw-p-2 tw-h-full">
{children}
<button className="tw-btn tw-btn-sm tw-btn-circle tw-btn-ghost tw-absolute tw-right-2 tw-top-2" onClick={() => closeScreen()}></button>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -2,6 +2,10 @@
@tailwind components;
@tailwind utilities;
.fade {
mask-image: linear-gradient(180deg,transparent, #000 3%, #000 97%, transparent);
}
.tw-modal {
z-index: 1200 !important;
}

View File

@ -1,7 +1,7 @@
export { UtopiaMap, Layer, Tags, Permissions, ItemForm, ItemView, PopupTextAreaInput, PopupStartEndInput, PopupTextInput, PopupButton, TextView, StartEndView } from './Components/Map';
export {AppShell, Content, SideBar} from "./Components/AppShell"
export {AuthProvider, useAuth, LoginPage, SignupPage, RequestPasswordPage, SetNewPasswordPage} from "./Components/Auth"
export {UserSettings, ProfileSettings} from './Components/Profile'
export {UserSettings, ProfileSettings, OverlayProfile, OverlayProfileSettings, OverlayUserSettings} from './Components/Profile'
export {Quests, Modal} from './Components/Gaming'
export {TitleCard, CardPage} from './Components/Templates'
export {TextInput, TextAreaInput, SelectBox} from './Components/Input'

View File

@ -27,6 +27,12 @@ module.exports = {
},
fontFamily: {
'sans': ["Helvetica", "sans-serif", 'Roboto'],
},
fontSize: {
'map': "13px"
},
lineHeight: {
'map': "1.4em"
}
},
},