mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-12 23:36:00 +00:00
MapOverlay Pages and Advanced Profiles
This commit is contained in:
parent
2844bbfeff
commit
a0113d0fc5
54
package-lock.json
generated
54
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'>
|
||||
|
||||
|
||||
{
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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 }}
|
||||
|
||||
50
src/Components/Profile/OverlayProfile.tsx
Normal file
50
src/Components/Profile/OverlayProfile.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
238
src/Components/Profile/OverlayProfileSettings.tsx
Normal file
238
src/Components/Profile/OverlayProfileSettings.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
83
src/Components/Profile/OverlayUserSettings.tsx
Normal file
83
src/Components/Profile/OverlayUserSettings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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'
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -27,6 +27,12 @@ module.exports = {
|
||||
},
|
||||
fontFamily: {
|
||||
'sans': ["Helvetica", "sans-serif", 'Roboto'],
|
||||
},
|
||||
fontSize: {
|
||||
'map': "13px"
|
||||
},
|
||||
lineHeight: {
|
||||
'map': "1.4em"
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user