mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
a lot of things
This commit is contained in:
parent
d5ffc462cf
commit
1e8e68302c
@ -3,7 +3,7 @@ import { useLayers } from '../hooks/useLayers'
|
||||
import { useHasUserPermission } from '../hooks/usePermissions';
|
||||
|
||||
|
||||
export default function AddButton({ setSelectNewItemPosition }: { setSelectNewItemPosition: React.Dispatch<React.SetStateAction<any>> }) {
|
||||
export default function AddButton({ triggerAction }: { triggerAction: React.Dispatch<React.SetStateAction<any>> }) {
|
||||
|
||||
const layers = useLayers();
|
||||
const hasUserPermission = useHasUserPermission();
|
||||
@ -35,7 +35,7 @@ export default function AddButton({ setSelectNewItemPosition }: { setSelectNewIt
|
||||
<button tabIndex={0}
|
||||
className="tw-z-500 tw-border-0 tw-pl-2 tw-p-0 tw-mb-3 tw-w-10 tw-h-10 tw-cursor-pointer tw-rounded-full tw-mouse tw-drop-shadow-md tw-transition tw-ease-in tw-duration-200 focus:tw-outline-none"
|
||||
style={{ backgroundColor: layer.menuColor }}
|
||||
onClick={() => { setSelectNewItemPosition(layer) }}>
|
||||
onClick={() => { triggerAction(layer) }}>
|
||||
<layer.menuIcon className="tw-h-6 tw-w-6 tw-text-white" ></layer.menuIcon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -15,9 +15,10 @@ import { useNavigate } from "react-router-dom";
|
||||
|
||||
|
||||
|
||||
export function HeaderView({ item, setItemFormPopup }: {
|
||||
export function HeaderView({ item, setItemFormPopup, hideMenu=false }: {
|
||||
item: Item,
|
||||
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
|
||||
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>,
|
||||
hideMenu?: boolean
|
||||
}) {
|
||||
|
||||
|
||||
@ -68,7 +69,7 @@ export function HeaderView({ item, setItemFormPopup }: {
|
||||
const openEditPopup = (event: React.MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
map.closePopup();
|
||||
if (setItemFormPopup)
|
||||
if (setItemFormPopup && item.position)
|
||||
setItemFormPopup({ position: new LatLng(item.position.coordinates[1], item.position.coordinates[0]), layer: item.layer!, item: item, setItemFormPopup: setItemFormPopup })
|
||||
}
|
||||
|
||||
@ -79,7 +80,7 @@ export function HeaderView({ item, setItemFormPopup }: {
|
||||
<div className='tw-col-span-5'>
|
||||
<div className="tw-flex tw-flex-row">{
|
||||
avatar ?
|
||||
<div className="tw-w-10 tw-rounded-full">
|
||||
<div className="tw-w-10 tw-min-w-[2.5em] tw-rounded-full">
|
||||
<img className="tw-rounded-full" src={`${avatar}?width=80&height=80`} />
|
||||
</div>
|
||||
:
|
||||
@ -92,7 +93,8 @@ export function HeaderView({ item, setItemFormPopup }: {
|
||||
<div className='tw-col-span-1'>
|
||||
{(item.layer?.api?.deleteItem || item.layer?.api?.updateItem)
|
||||
&& ((user && owner?.id === user.id) || owner == undefined)
|
||||
&& (hasUserPermission(item.layer.api?.collectionName!, "delete") || hasUserPermission(item.layer.api?.collectionName!, "update")) &&
|
||||
&& (hasUserPermission(item.layer.api?.collectionName!, "delete") || hasUserPermission(item.layer.api?.collectionName!, "update"))
|
||||
&& !hideMenu &&
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="tw-h-5 tw-w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
@ -125,7 +127,7 @@ export function HeaderView({ item, setItemFormPopup }: {
|
||||
</div>
|
||||
</div>
|
||||
<DialogModal isOpened={modalOpen} title="Are you sure?" showCloseButton={false} onClose={ () => (setModalOpen(false)) }>
|
||||
<span>Do you want to delte <b>{item.name}</b>?</span>
|
||||
<span>Do you want to delete <b>{item.name}</b>?</span>
|
||||
<div className="tw-grid">
|
||||
<div className="tw-flex tw-justify-between">
|
||||
<label className="tw-btn tw-mt-4 tw-btn-error" onClick={removeItemFromMap}>Yes</label>
|
||||
|
||||
@ -11,10 +11,7 @@ import { decodeTag } from '../../../../Utils/FormatTags';
|
||||
|
||||
export const TextView = ({ item, truncate = false, itemTextField}: { item?: Item, truncate?: boolean,itemTextField?: string }) => {
|
||||
const tags = useTags();
|
||||
const addFilterTag = useAddFilterTag();
|
||||
|
||||
console.log(item);
|
||||
|
||||
const addFilterTag = useAddFilterTag();
|
||||
|
||||
let text = "";
|
||||
|
||||
|
||||
@ -89,7 +89,7 @@ function UtopiaMap({
|
||||
<ItemsProvider initialItems={[]}>
|
||||
<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}>
|
||||
<MapContainer ref={mapDivRef} style={{ height: height, width: width }} center={new LatLng(center[0], center[1])} zoom={zoom} zoomControl={false} maxZoom={19}>
|
||||
<Outlet></Outlet>
|
||||
<Control position='topLeft' zIndex="1000">
|
||||
<SearchControl clusterRef={clusterRef} />
|
||||
@ -100,6 +100,7 @@ function UtopiaMap({
|
||||
<LayerControl></LayerControl>
|
||||
</Control>
|
||||
<TileLayer
|
||||
maxZoom={19}
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://tile.osmand.net/hd/{z}/{x}/{y}.png" />
|
||||
<MarkerClusterGroup ref={clusterRef} showCoverageOnHover chunkedLoading maxClusterRadius={50} removeOutsideVisibleBounds={false}>
|
||||
@ -112,7 +113,7 @@ function UtopiaMap({
|
||||
</MarkerClusterGroup>
|
||||
<MapEventListener setSelectNewItemPosition={setSelectNewItemPosition} selectNewItemPosition={selectNewItemPosition} setItemFormPopup={setItemFormPopup} />
|
||||
</MapContainer>
|
||||
<AddButton setSelectNewItemPosition={setSelectNewItemPosition}></AddButton>
|
||||
<AddButton triggerAction={setSelectNewItemPosition}></AddButton>
|
||||
{selectNewItemPosition != null &&
|
||||
<div className="tw-button tw-z-1000 tw-absolute tw-right-5 tw-top-4 tw-drop-shadow-md">
|
||||
<div className="tw-alert tw-bg-base-100 tw-text-base-content">
|
||||
|
||||
161
src/Components/Profile/AvatarWidget.tsx
Normal file
161
src/Components/Profile/AvatarWidget.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
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";
|
||||
|
||||
|
||||
export const AvatarWidget = ({avatar, setAvatar}:{avatar:string, setAvatar : React.Dispatch<React.SetStateAction<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 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)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{!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} className='tw-h-20 tw-w-20 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>
|
||||
|
||||
}
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -39,10 +39,11 @@ export const ColorPicker = ({ color, onChange, className }) => {
|
||||
/>
|
||||
|
||||
{isOpen && (
|
||||
<div className="popover" ref={popover}>
|
||||
<HexColorPicker color={color} onChange={onChange} />
|
||||
<div className="popover tw-z-[10000]" ref={popover}>
|
||||
<HexColorPicker color={color} onChange={onChange} onClick={() => toggle(false)}/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
266
src/Components/Profile/OverlayItemProfile.tsx
Normal file
266
src/Components/Profile/OverlayItemProfile.tsx
Normal file
@ -0,0 +1,266 @@
|
||||
import * as React from 'react'
|
||||
import { MapOverlayPage, TitleCard } from '../Templates'
|
||||
import { useAddItem, useItems, useRemoveItem, useUpdateItem } from '../Map/hooks/useItems'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Item, Tag, UserItem } from '../../types';
|
||||
import { getValue } from '../../Utils/GetValue';
|
||||
import { useMap } from 'react-leaflet';
|
||||
import { LatLng } from 'leaflet';
|
||||
import { PopupStartEndInput, StartEndView, TextView } from '../Map';
|
||||
import useWindowDimensions from '../Map/hooks/useWindowDimension';
|
||||
import { useAddTag, useTags } from '../Map/hooks/useTags';
|
||||
import { useAddFilterTag, useResetFilterTags } from '../Map/hooks/useFilter';
|
||||
import { HeaderView } from '../Map/Subcomponents/ItemPopupComponents/HeaderView';
|
||||
import { useHasUserPermission } from '../Map/hooks/usePermissions';
|
||||
import PlusButton from './PlusButton';
|
||||
import { TextAreaInput, TextInput } from '../Input';
|
||||
import { hashTagRegex } from '../../Utils/HashTagRegex';
|
||||
import { randomColor } from '../../Utils/RandomColor';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useAuth } from '../Auth';
|
||||
import { useLayers } from '../Map/hooks/useLayers';
|
||||
|
||||
export function OverlayItemProfile() {
|
||||
|
||||
const location = useLocation();
|
||||
const items = useItems();
|
||||
const updateItem = useUpdateItem();
|
||||
const [item, setItem] = useState<Item>({} as Item)
|
||||
const map = useMap();
|
||||
const windowDimension = useWindowDimensions();
|
||||
|
||||
const layers = useLayers();
|
||||
|
||||
|
||||
const tags = useTags();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [owner, setOwner] = useState<UserItem>();
|
||||
const [offers, setOffers] = useState<Array<Tag>>([]);
|
||||
const [needs, setNeeds] = useState<Array<Tag>>([]);
|
||||
const [relations, setRelations] = useState<Array<Item>>([]);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
|
||||
const [addItemPopupType, setAddItemPopupType] = useState<string>("");
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const addTag = useAddTag();
|
||||
const resetFilterTags = useResetFilterTags();
|
||||
|
||||
const addItem = useAddItem();
|
||||
const { user } = useAuth();
|
||||
|
||||
const hasUserPermission = useHasUserPermission();
|
||||
|
||||
const tabRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function scroll() {
|
||||
tabRef.current?.scrollTo(0, 800);
|
||||
}
|
||||
|
||||
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 && item?.position.coordinates[0])
|
||||
map.setView(new LatLng(item?.position.coordinates[1]!, item?.position.coordinates[0]! + x / 4))
|
||||
}, [location, items, activeTab])
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTab(1);
|
||||
}, [location])
|
||||
|
||||
useEffect(() => {
|
||||
setOffers([]);
|
||||
setNeeds([]);
|
||||
setRelations([]);
|
||||
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.relations?.map(r => {
|
||||
const item = items.find(i => i.id == r.related_items_id)
|
||||
item && setRelations(current => [...current, item])
|
||||
})
|
||||
}, [item])
|
||||
|
||||
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 item?.layer?.api?.createItem!({ ...formItem, id: uuid, type: type });
|
||||
await linkItem(uuid);
|
||||
success = true;
|
||||
} catch (error) {
|
||||
toast.error(error.toString());
|
||||
}
|
||||
if (success) {
|
||||
addItem({ ...formItem, id: uuid, type: type, layer: item?.layer, user_created: user });
|
||||
toast.success("New item created");
|
||||
resetFilterTags();
|
||||
}
|
||||
setLoading(false);
|
||||
setAddItemPopupType("");
|
||||
}
|
||||
|
||||
const linkItem = async (id: string) => {
|
||||
let new_relations = item.relations;
|
||||
new_relations?.push({ items_id: item.id, related_items_id: id })
|
||||
|
||||
const updatedItem = { id: item.id, relations: new_relations }
|
||||
|
||||
await item?.layer?.api?.updateItem!(updatedItem)
|
||||
|
||||
updateItem({ ...item, relations: new_relations })
|
||||
}
|
||||
|
||||
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-w-20 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>
|
||||
{(item.layer?.api?.updateItem && hasUserPermission(item.layer.api?.collectionName!, "update")) ?
|
||||
<a className='tw-self-center tw-btn tw-btn-sm tw-mr-4 tw-cursor-pointer' onClick={() => navigate("/edit-item/" + item.id)}>
|
||||
<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="Info" 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 [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]" aria-label="Projects" checked={activeTab == 2 && true} onChange={() => setActiveTab(2)} />
|
||||
<div ref={tabRef} role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-rounded-box tw-h-[calc(100dvh-268px)] tw-overflow-y-auto tw-pt-4 tw-pb-1 -tw-mx-4" >
|
||||
<div className='tw-h-full'>
|
||||
<div className='tw-grid tw-grid-cols-1 sm:tw-grid-cols-2 md:tw-grid-cols-1 lg:tw-grid-cols-1 xl:tw-grid-cols-2'>
|
||||
{relations && relations.map(i => {
|
||||
if (i.type == 'project') return (
|
||||
|
||||
<div key={i.id} 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-mx-4 tw-p-4 tw-mb-4 tw-h-fit' onClick={() => navigate('/item/' + i.id)}>
|
||||
<HeaderView item={i} />
|
||||
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
|
||||
<TextView truncate item={i} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
else return null
|
||||
})}
|
||||
{addItemPopupType == "project" ?
|
||||
<form 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-mx-4 tw-p-6 tw-mb-4'>
|
||||
<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> : <></>
|
||||
}
|
||||
<PlusButton triggerAction={() => { setAddItemPopupType("project"); scroll() }} color={item.color}></PlusButton>
|
||||
|
||||
</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="Events" 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 tw-pt-4 tw-pb-1">
|
||||
<div className='tw-h-full'>
|
||||
<div className='tw-grid tw-grid-cols-1 sm:tw-grid-cols-2 md:tw-grid-cols-1 lg:tw-grid-cols-1 xl:tw-grid-cols-2'>
|
||||
{relations && relations.map(i => {
|
||||
if (i.type == 'event') return (
|
||||
|
||||
<div key={i.id} 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-mx-4 tw-p-6 tw-mb-4' onClick={() => navigate('/item/' + i.id)}>
|
||||
<HeaderView item={i} hideMenu />
|
||||
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
|
||||
<StartEndView item={i}></StartEndView>
|
||||
<TextView truncate item={i} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
else return null
|
||||
})}
|
||||
{addItemPopupType == "event" ?
|
||||
<form 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-mx-4 tw-p-4 tw-mb-4'>
|
||||
<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='' />
|
||||
<PopupStartEndInput></PopupStartEndInput>
|
||||
<TextAreaInput placeholder="Text" dataField="text" defaultValue={""} inputStyle='tw-h-40 tw-mt-5' />
|
||||
<div className='tw-flex tw-justify-center'>
|
||||
<button className={loading ? 'tw-btn tw-btn-disabled tw-mt-5 tw-place-self-center' : 'tw-btn tw-mt-5 tw-place-self-center'} type='submit'>{loading ? <span className="tw-loading tw-loading-spinner"></span> : 'Save'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form> : <></>
|
||||
}
|
||||
<PlusButton triggerAction={() => { setAddItemPopupType("event"); scroll() }} color={item.color}></PlusButton>
|
||||
|
||||
</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="Friends" checked={activeTab == 4 && true} onChange={() => setActiveTab(4)} />
|
||||
<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">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</MapOverlayPage >
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
145
src/Components/Profile/OverlayItemProfileSettings.tsx
Normal file
145
src/Components/Profile/OverlayItemProfileSettings.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import * as React from 'react'
|
||||
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 DialogModal from '../Templates/DialogModal';
|
||||
import { hashTagRegex } from '../../Utils/HashTagRegex';
|
||||
import { useAddTag, useTags } from '../Map/hooks/useTags';
|
||||
import { randomColor } from '../../Utils/RandomColor';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Geometry, Item, Tag, UserItem } from '../../types';
|
||||
import { MapOverlayPage } from '../Templates';
|
||||
import { TagsWidget } from './TagsWidget';
|
||||
import { decodeTag, encodeTag } from '../../Utils/FormatTags';
|
||||
import { AvatarWidget } from './AvatarWidget';
|
||||
|
||||
|
||||
export function OverlayItemProfileSettings() {
|
||||
|
||||
const [id, setId] = useState<string>("");
|
||||
const [name, setName] = useState<string>("");
|
||||
const [text, setText] = useState<string>("");
|
||||
const [image, setImage] = useState<string>("");
|
||||
const [color, setColor] = useState<string>("");
|
||||
|
||||
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
|
||||
|
||||
|
||||
const updateItem = useUpdateItem();
|
||||
|
||||
const location = useLocation();
|
||||
const items = useItems();
|
||||
const [item, setItem] = useState<Item>({} as Item)
|
||||
|
||||
const tags = useTags();
|
||||
const addTag = useAddTag();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const itemId = location.pathname.split("/")[2];
|
||||
const item = items.find(i => i.id === itemId);
|
||||
item && setItem(item);
|
||||
|
||||
}, [location, items, activeTab])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (item.layer?.itemColorField) setColor(getValue(item, item.layer?.itemColorField));
|
||||
else setColor(item.layer?.markerDefaultColor || "#000")
|
||||
|
||||
setId(item?.id ? item.id : "");
|
||||
setName(item?.name ? item.name : "");
|
||||
setText(item?.text ? item.text : "");
|
||||
setImage(item?.image ? item?.image : "");
|
||||
}, [item])
|
||||
|
||||
|
||||
const onUpdateItem = async () => {
|
||||
let changedItem = {} as Item;
|
||||
|
||||
console.log(item.position);
|
||||
|
||||
|
||||
|
||||
changedItem = { id: id, name: name, text: text, color: color, position: item.position, ...image.length > 10 && { image: image }};
|
||||
// update profile item in current state
|
||||
//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, 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()})
|
||||
}
|
||||
});
|
||||
|
||||
setLoading(true);
|
||||
|
||||
item?.layer?.api?.updateItem && toast.promise(
|
||||
item?.layer?.api?.updateItem(changedItem),
|
||||
{
|
||||
pending: 'updating Item ...',
|
||||
success: 'Item updated',
|
||||
error: {
|
||||
render({ data }) {
|
||||
return `${data}`
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(() => item && updateItem({...item, ...changedItem}))
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
navigate("/item/"+item.id)});
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<MapOverlayPage backdrop className='tw-mx-4 tw-mt-4 tw-mb-12 tw-overflow-x-hidden tw-max-h-[calc(100dvh-96px)] !tw-h-[calc(100dvh-96px)] tw-w-[calc(100%-32px)] md:tw-w-[calc(50%-32px)] tw-max-w-3xl !tw-left-auto tw-top-0 tw-bottom-0'>
|
||||
<div className='tw-flex tw-flex-col tw-h-full'>
|
||||
<div className="tw-flex">
|
||||
<AvatarWidget avatar={image} setAvatar={setImage}/>
|
||||
<ColorPicker color={color} onChange={setColor} className={"-tw-left-6 tw-top-14 -tw-mr-6"} />
|
||||
<TextInput placeholder="Name" defaultValue={item?.name ? item.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={item?.text ? item.text : ""} updateFormValue={(v) => {console.log(v);setText(v)}} containerStyle='tw-h-full' inputStyle='tw-h-full tw-border-t-0 tw-rounded-tl-none' />
|
||||
</div>
|
||||
|
||||
<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="Projects" 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>
|
||||
|
||||
<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="Events" 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">
|
||||
</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="Friends" checked={activeTab == 4 && true} onChange={() => setActiveTab(4)} />
|
||||
<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">
|
||||
</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={() => onUpdateItem()}>Update</button></div>
|
||||
|
||||
</div>
|
||||
|
||||
</MapOverlayPage>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ export function OverlayProfile() {
|
||||
const bounds = map.getBounds();
|
||||
const x = bounds.getEast() - bounds.getWest()
|
||||
if (windowDimension.width > 768)
|
||||
if (item?.position.coordinates[0])
|
||||
if (item?.position?.coordinates[0])
|
||||
map.setView(new LatLng(item?.position.coordinates[1]!, item?.position.coordinates[0]! + x / 4))
|
||||
}, [location, items, activeTab])
|
||||
|
||||
@ -75,7 +75,7 @@ export function OverlayProfile() {
|
||||
<>
|
||||
<div className='tw-flex tw-flex-row'>
|
||||
<div className="tw-grow">
|
||||
<p className="tw-text-4xl">{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>
|
||||
<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")}>
|
||||
|
||||
@ -2,13 +2,10 @@ import * as React from 'react'
|
||||
import { useItems, useUpdateItem } from '../Map/hooks/useItems'
|
||||
import { 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';
|
||||
@ -17,11 +14,12 @@ import { Tag, UserItem } from '../../types';
|
||||
import { MapOverlayPage } from '../Templates';
|
||||
import { TagsWidget } from './TagsWidget';
|
||||
import { decodeTag, encodeTag } from '../../Utils/FormatTags';
|
||||
import { AvatarWidget } from './AvatarWidget';
|
||||
|
||||
|
||||
export function OverlayProfileSettings() {
|
||||
|
||||
const { user, updateUser, loading, token } = useAuth();
|
||||
const { user, updateUser, loading } = useAuth();
|
||||
|
||||
const [id, setId] = useState<string>("");
|
||||
const [name, setName] = useState<string>("");
|
||||
@ -35,12 +33,7 @@ export function OverlayProfileSettings() {
|
||||
const [activeTab, setActiveTab] = useState<number>(1);
|
||||
|
||||
|
||||
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();
|
||||
|
||||
@ -67,98 +60,6 @@ export function OverlayProfileSettings() {
|
||||
setContact(user?.contact ? user.contact : "");
|
||||
}, [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 = async () => {
|
||||
let changedUser = {} as UserItem;
|
||||
@ -228,33 +129,7 @@ export function OverlayProfileSettings() {
|
||||
<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">
|
||||
{!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>
|
||||
|
||||
}
|
||||
<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>
|
||||
@ -289,22 +164,6 @@ export function OverlayProfileSettings() {
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
23
src/Components/Profile/PlusButton.tsx
Normal file
23
src/Components/Profile/PlusButton.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { useLayers } from "../Map/hooks/useLayers";
|
||||
import { useHasUserPermission } from "../Map/hooks/usePermissions";
|
||||
|
||||
export default function PlusButton({ triggerAction, color }: { triggerAction: any, color: string }) {
|
||||
|
||||
const hasUserPermission = useHasUserPermission();
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>{hasUserPermission("items", "create") &&
|
||||
<div className="tw-dropdown tw-dropdown-top tw-dropdown-end tw-dropdown-hover tw-z-500 tw-absolute tw-right-4 tw-bottom-4" >
|
||||
<button tabIndex={0} className="tw-z-500 tw-btn tw-btn-circle tw-shadow" onClick={() => { triggerAction() }} style={{ backgroundColor: color, color: "#fff"}}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" className="tw-w-5 tw-h-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
@ -2,4 +2,6 @@ export {UserSettings} from './UserSettings'
|
||||
export {ProfileSettings} from './ProfileSettings'
|
||||
export {OverlayProfile} from './OverlayProfile'
|
||||
export {OverlayProfileSettings} from './OverlayProfileSettings'
|
||||
export {OverlayUserSettings} from './OverlayUserSettings'
|
||||
export {OverlayUserSettings} from './OverlayUserSettings'
|
||||
export {OverlayItemProfile} from './OverlayItemProfile'
|
||||
export {OverlayItemProfileSettings} from './OverlayItemProfileSettings'
|
||||
@ -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, OverlayProfile, OverlayProfileSettings, OverlayUserSettings} from './Components/Profile'
|
||||
export {UserSettings, ProfileSettings, OverlayProfile, OverlayProfileSettings, OverlayUserSettings, OverlayItemProfile, OverlayItemProfileSettings} from './Components/Profile'
|
||||
export {Quests, Modal} from './Components/Gaming'
|
||||
export {TitleCard, CardPage, MapOverlayPage, CircleLayout, MoonCalendar, ItemsIndexPage, ItemViewPage} from './Components/Templates'
|
||||
export {TextInput, TextAreaInput, SelectBox} from './Components/Input'
|
||||
|
||||
10
src/types.ts
10
src/types.ts
@ -42,7 +42,7 @@ export class Item {
|
||||
id: string ;
|
||||
name: string;
|
||||
text: string;
|
||||
position: Geometry;
|
||||
position?: Geometry;
|
||||
date_created?: string;
|
||||
date_updated?: string | null;
|
||||
start?: string;
|
||||
@ -50,6 +50,7 @@ export class Item {
|
||||
api?: ItemsApi<any>;
|
||||
tags?: string[];
|
||||
layer?: LayerProps;
|
||||
relations?: Relation[];
|
||||
[key: string]: any;
|
||||
constructor(id:string,name:string,text:string,position:Geometry, layer?: LayerProps, api?: ItemsApi<any>){
|
||||
this.id = id;
|
||||
@ -123,10 +124,15 @@ export type Profile = {
|
||||
|
||||
export type Permission = {
|
||||
id?: string;
|
||||
role: string;S
|
||||
role: string;
|
||||
collection: string;
|
||||
action: PermissionAction
|
||||
}
|
||||
|
||||
|
||||
export type PermissionAction = "create"|"read"|"update"|"delete";
|
||||
|
||||
export type Relation = {
|
||||
related_items_id: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user