mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
merged
This commit is contained in:
commit
5a84c4f180
5
package-lock.json
generated
5
package-lock.json
generated
@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "utopia-ui",
|
"name": "utopia-ui",
|
||||||
"version": "3.0.0-alpha.253",
|
|
||||||
|
"version": "3.0.0-alpha.257",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "utopia-ui",
|
"name": "utopia-ui",
|
||||||
"version": "3.0.0-alpha.253",
|
"version": "3.0.0-alpha.257",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.0.17",
|
"@heroicons/react": "^2.0.17",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "utopia-ui",
|
"name": "utopia-ui",
|
||||||
"version": "3.0.0-alpha.253",
|
"version": "3.0.0-alpha.257",
|
||||||
"description": "Reuseable React Components to build mapping apps for real life communities and networks",
|
"description": "Reuseable React Components to build mapping apps for real life communities and networks",
|
||||||
"repository": "https://github.com/utopia-os/utopia-ui",
|
"repository": "https://github.com/utopia-os/utopia-ui",
|
||||||
"homepage:": "https://utopia-os.org/",
|
"homepage:": "https://utopia-os.org/",
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import Tribute from "tributejs";
|
import Tribute from "tributejs";
|
||||||
import { useTags } from "../Map/hooks/useTags";
|
import { useTags } from "../Map/hooks/useTags";
|
||||||
|
|
||||||
|
|
||||||
type TextAreaProps = {
|
type TextAreaProps = {
|
||||||
labelTitle?: string;
|
labelTitle?: string;
|
||||||
labelStyle?: string;
|
labelStyle?: string;
|
||||||
@ -19,21 +18,20 @@ interface KeyValue {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function TextAreaInput({ labelTitle, dataField, labelStyle, containerStyle, inputStyle, defaultValue, placeholder, updateFormValue }: TextAreaProps) {
|
export function TextAreaInput({ labelTitle, dataField, labelStyle, containerStyle, inputStyle, defaultValue, placeholder, updateFormValue }: TextAreaProps) {
|
||||||
|
|
||||||
const ref = useRef<HTMLTextAreaElement>(null);
|
const ref = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const [inputValue, setInputValue] = useState<string>(defaultValue);
|
||||||
|
|
||||||
// prevent react18 from calling useEffect twice
|
// prevent react18 from calling useEffect twice
|
||||||
const init = useRef(false)
|
const init = useRef(false);
|
||||||
|
|
||||||
const tags = useTags();
|
const tags = useTags();
|
||||||
|
|
||||||
let values: KeyValue[] = [];
|
let values: KeyValue[] = [];
|
||||||
|
|
||||||
tags.map(tag => {
|
tags.forEach(tag => {
|
||||||
values.push({ key: tag.name, value: tag.name, color: tag.color })
|
values.push({ key: tag.name, value: tag.name, color: tag.color });
|
||||||
})
|
});
|
||||||
|
|
||||||
var tribute = new Tribute({
|
var tribute = new Tribute({
|
||||||
containerClass: 'tw-z-3000 tw-bg-base-100 tw-p-2 tw-rounded-lg tw-shadow',
|
containerClass: 'tw-z-3000 tw-bg-base-100 tw-p-2 tw-rounded-lg tw-shadow',
|
||||||
@ -45,28 +43,47 @@ export function TextAreaInput({ labelTitle, dataField, labelStyle, containerStyl
|
|||||||
return ""
|
return ""
|
||||||
},
|
},
|
||||||
menuItemTemplate: function (item) {
|
menuItemTemplate: function (item) {
|
||||||
return `<span style="color: ${item.original.color}; padding: 5px; boarder-radius: 3px;">#${item.string}</span>`;
|
return `<span style="color: ${item.original.color}; padding: 5px; border-radius: 3px;">#${item.string}</span>`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!init.current) {
|
if (!init.current) {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
tribute.attach(ref.current);
|
tribute.attach(ref.current);
|
||||||
}
|
}
|
||||||
init.current = true;
|
init.current = true;
|
||||||
}
|
}
|
||||||
}, [ref])
|
}, [ref]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputValue(defaultValue);
|
||||||
|
}, [defaultValue]);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setInputValue(newValue);
|
||||||
|
if (updateFormValue) {
|
||||||
|
updateFormValue(newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`tw-form-control tw-w-full ${containerStyle ? containerStyle : ""}`}>
|
<div className={`tw-form-control tw-w-full ${containerStyle ? containerStyle : ""}`}>
|
||||||
{labelTitle ? <label className="tw-label">
|
{labelTitle ? (
|
||||||
<span className={"tw-label-text tw-text-base-content " + labelStyle}>{labelTitle}</span>
|
<label className="tw-label">
|
||||||
</label> : ""}
|
<span className={`tw-label-text tw-text-base-content ${labelStyle}`}>{labelTitle}</span>
|
||||||
<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>
|
</label>
|
||||||
|
) : null}
|
||||||
|
<textarea
|
||||||
|
required
|
||||||
|
ref={ref}
|
||||||
|
value={inputValue}
|
||||||
|
name={dataField}
|
||||||
|
className={`tw-textarea tw-textarea-bordered tw-w-full tw-leading-5 ${inputStyle || ""}`}
|
||||||
|
placeholder={placeholder || ""}
|
||||||
|
onChange={handleChange}
|
||||||
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -167,7 +167,7 @@ export const SearchControl = () => {
|
|||||||
))}
|
))}
|
||||||
{isGeoCoordinate(value) &&
|
{isGeoCoordinate(value) &&
|
||||||
<div className='tw-flex tw-flex-row hover:tw-font-bold tw-cursor-pointer' onClick={() => {
|
<div className='tw-flex tw-flex-row hover:tw-font-bold tw-cursor-pointer' onClick={() => {
|
||||||
L.marker(new LatLng(extractCoordinates(value)![0], extractCoordinates(value)![1]), { icon: MarkerIconFactory("circle", "#777", "RGBA(35, 31, 32, 0.2)", "circle-solid") }).addTo(map).bindPopup(`<h3 class="tw-text-base tw-font-bold">${extractCoordinates(value)![0]}, ${extractCoordinates(value)![1]}</h3>`).openPopup().addEventListener("popupclose", (e) => { console.log(e.target.remove()) });
|
L.marker(new LatLng(extractCoordinates(value)![0], extractCoordinates(value)![1]), { icon: MarkerIconFactory("circle", "#777", "RGBA(35, 31, 32, 0.2)", "point") }).addTo(map).bindPopup(`<h3 class="tw-text-base tw-font-bold">${extractCoordinates(value)![0]}, ${extractCoordinates(value)![1]}</h3>`).openPopup().addEventListener("popupclose", (e) => { console.log(e.target.remove()) });
|
||||||
map.setView(new LatLng(extractCoordinates(value)![0], extractCoordinates(value)![1]), 15, { duration: 1 })
|
map.setView(new LatLng(extractCoordinates(value)![0], extractCoordinates(value)![1]), 15, { duration: 1 })
|
||||||
}}>
|
}}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="tw-text-current tw-mr-2 tw-mt-0 tw-w-4">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="tw-text-current tw-mr-2 tw-mt-0 tw-w-4">
|
||||||
|
|||||||
16
src/Components/Map/Subcomponents/SelectPosition.tsx
Normal file
16
src/Components/Map/Subcomponents/SelectPosition.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
export const SelectPosition = ({ setSelectNewItemPosition }: { setSelectNewItemPosition }) => {
|
||||||
|
return (
|
||||||
|
<div className="tw-animate-pulseGrow tw-button tw-z-1000 tw-absolute tw-right-5 tw-top-4 tw-drop-shadow-md">
|
||||||
|
<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={() => {
|
||||||
|
setSelectNewItemPosition(null)
|
||||||
|
}}>
|
||||||
|
<p className='tw-text-center '>✕</p></label>
|
||||||
|
<div className="tw-alert tw-bg-base-100 tw-text-base-content">
|
||||||
|
<div>
|
||||||
|
<span className="tw-text-lg">Select position on the map!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -13,14 +13,15 @@ import { SearchControl } from "./Subcomponents/Controls/SearchControl";
|
|||||||
import { Control } from "./Subcomponents/Controls/Control";
|
import { Control } from "./Subcomponents/Controls/Control";
|
||||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { TagsControl } from "./Subcomponents/Controls/TagsControl";
|
import { TagsControl } from "./Subcomponents/Controls/TagsControl";
|
||||||
import { useSelectPosition, useSetMapClicked,useSetSelectPosition } from "./hooks/useSelectPosition";
|
import { useSelectPosition, useSetMapClicked, useSetSelectPosition } from "./hooks/useSelectPosition";
|
||||||
import { useClusterRef, useSetClusterRef } from "./hooks/useClusterRef";
|
import { useClusterRef, useSetClusterRef } from "./hooks/useClusterRef";
|
||||||
import { Feature, Geometry as GeoJSONGeometry } from 'geojson';
|
import { Feature, Geometry as GeoJSONGeometry } from 'geojson';
|
||||||
import {FilterControl} from "./Subcomponents/Controls/FilterControl";
|
import { FilterControl } from "./Subcomponents/Controls/FilterControl";
|
||||||
import {LayerControl} from "./Subcomponents/Controls/LayerControl";
|
import { LayerControl } from "./Subcomponents/Controls/LayerControl";
|
||||||
import { useLayers } from "./hooks/useLayers";
|
import { useLayers } from "./hooks/useLayers";
|
||||||
import { useAddVisibleLayer } from "./hooks/useFilter";
|
import { useAddVisibleLayer } from "./hooks/useFilter";
|
||||||
import { GratitudeControl } from "./Subcomponents/Controls/GratitudeControl";
|
import { GratitudeControl } from "./Subcomponents/Controls/GratitudeControl";
|
||||||
|
import { SelectPosition } from "./Subcomponents/SelectPosition";
|
||||||
|
|
||||||
// for refreshing map on resize (needs to be implemented)
|
// for refreshing map on resize (needs to be implemented)
|
||||||
const mapDivRef = React.createRef();
|
const mapDivRef = React.createRef();
|
||||||
@ -32,9 +33,9 @@ function UtopiaMap({
|
|||||||
zoom = 10,
|
zoom = 10,
|
||||||
children,
|
children,
|
||||||
geo,
|
geo,
|
||||||
showFilterControl=false,
|
showFilterControl = false,
|
||||||
showLayerControl = true
|
showLayerControl = true
|
||||||
}
|
}
|
||||||
: UtopiaMapProps) {
|
: UtopiaMapProps) {
|
||||||
|
|
||||||
function MapEventListener() {
|
function MapEventListener() {
|
||||||
@ -52,7 +53,9 @@ function UtopiaMap({
|
|||||||
|
|
||||||
const resetMetaTags = () => {
|
const resetMetaTags = () => {
|
||||||
let params = new URLSearchParams(window.location.search);
|
let params = new URLSearchParams(window.location.search);
|
||||||
window.history.pushState({}, "", `/` + `${params.toString() !== "" ? `?${params}` : ""}`)
|
if (!location.pathname.includes("/item/")) {
|
||||||
|
window.history.pushState({}, "", `/` + `${params.toString() !== "" ? `?${params}` : ""}`)
|
||||||
|
}
|
||||||
document.title = document.title.split("-")[0];
|
document.title = document.title.split("-")[0];
|
||||||
document.querySelector('meta[property="og:title"]')?.setAttribute("content", document.title);
|
document.querySelector('meta[property="og:title"]')?.setAttribute("content", document.title);
|
||||||
document.querySelector('meta[property="og:description"]')?.setAttribute("content", `${document.querySelector('meta[name="description"]')?.getAttribute("content")}`);
|
document.querySelector('meta[property="og:description"]')?.setAttribute("content", `${document.querySelector('meta[name="description"]')?.getAttribute("content")}`);
|
||||||
@ -75,9 +78,9 @@ function UtopiaMap({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
layers.map(l => addVisibleLayer(l))
|
layers.map(l => addVisibleLayer(l))
|
||||||
|
|
||||||
}, [layers])
|
}, [layers])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -100,7 +103,7 @@ function UtopiaMap({
|
|||||||
{/*{!embedded && (*/}
|
{/*{!embedded && (*/}
|
||||||
{/* <QuestControl></QuestControl>*/}
|
{/* <QuestControl></QuestControl>*/}
|
||||||
{/*)}*/}
|
{/*)}*/}
|
||||||
{showFilterControl && <FilterControl/>}
|
{showFilterControl && <FilterControl />}
|
||||||
{/*todo: needed layer handling is located LayerControl*/}
|
{/*todo: needed layer handling is located LayerControl*/}
|
||||||
{showLayerControl && <LayerControl></LayerControl>}
|
{showLayerControl && <LayerControl></LayerControl>}
|
||||||
{<GratitudeControl/>}
|
{<GratitudeControl/>}
|
||||||
@ -125,19 +128,9 @@ function UtopiaMap({
|
|||||||
}} />}
|
}} />}
|
||||||
<MapEventListener />
|
<MapEventListener />
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
<AddButton triggerAction={setSelectNewItemPosition}></AddButton>
|
<AddButton triggerAction={setSelectNewItemPosition}></AddButton>
|
||||||
{selectNewItemPosition != null &&
|
{selectNewItemPosition != null &&
|
||||||
<div className="tw-button tw-z-1000 tw-absolute tw-right-5 tw-top-4 tw-drop-shadow-md">
|
<SelectPosition setSelectNewItemPosition={setSelectNewItemPosition} />
|
||||||
<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={() => {
|
|
||||||
setSelectNewItemPosition(null)
|
|
||||||
}}>
|
|
||||||
<p className='tw-text-center '>✕</p></label>
|
|
||||||
<div className="tw-alert tw-bg-base-100 tw-text-base-content">
|
|
||||||
<div>
|
|
||||||
<span>Select {selectNewItemPosition.name} position!</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,7 @@ function useSelectPositionManager(): {
|
|||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectPosition && markerClicked && 'text' in selectPosition) {
|
if (selectPosition && markerClicked && 'text' in selectPosition && markerClicked.id !==selectPosition.id) {
|
||||||
itemUpdateParent({ ...selectPosition, parent: markerClicked.id })
|
itemUpdateParent({ ...selectPosition, parent: markerClicked.id })
|
||||||
}
|
}
|
||||||
}, [markerClicked])
|
}, [markerClicked])
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { useTags } from '../Map/hooks/useTags';
|
|||||||
|
|
||||||
export function ProfileView({ userType, attestationApi }: { userType: string , attestationApi?: ItemsApi<any>}) {
|
export function ProfileView({ userType, attestationApi }: { userType: string , attestationApi?: ItemsApi<any>}) {
|
||||||
|
|
||||||
const [item, setItem] = useState<Item>({} as Item)
|
const [item, setItem] = useState<Item>()
|
||||||
const [updatePermission, setUpdatePermission] = useState<boolean>(false);
|
const [updatePermission, setUpdatePermission] = useState<boolean>(false);
|
||||||
const [relations, setRelations] = useState<Array<Item>>([]);
|
const [relations, setRelations] = useState<Array<Item>>([]);
|
||||||
const [offers, setOffers] = useState<Array<Tag>>([]);
|
const [offers, setOffers] = useState<Array<Tag>>([]);
|
||||||
@ -68,15 +68,15 @@ export function ProfileView({ userType, attestationApi }: { userType: string , a
|
|||||||
setNeeds([]);
|
setNeeds([]);
|
||||||
setRelations([]);
|
setRelations([]);
|
||||||
|
|
||||||
item.layer?.itemOffersField && getValue(item, item.layer.itemOffersField)?.map(o => {
|
item?.layer?.itemOffersField && getValue(item, item.layer.itemOffersField)?.map(o => {
|
||||||
const tag = tags.find(t => t.id === o.tags_id);
|
const tag = tags.find(t => t.id === o.tags_id);
|
||||||
tag && setOffers(current => [...current, tag])
|
tag && setOffers(current => [...current, tag])
|
||||||
})
|
})
|
||||||
item.layer?.itemNeedsField && getValue(item, item.layer.itemNeedsField)?.map(n => {
|
item?.layer?.itemNeedsField && getValue(item, item.layer.itemNeedsField)?.map(n => {
|
||||||
const tag = tags.find(t => t.id === n.tags_id);
|
const tag = tags.find(t => t.id === n.tags_id);
|
||||||
tag && setNeeds(current => [...current, tag])
|
tag && setNeeds(current => [...current, tag])
|
||||||
})
|
})
|
||||||
item.relations?.map(r => {
|
item?.relations?.map(r => {
|
||||||
const item = items.find(i => i.id == r.related_items_id)
|
const item = items.find(i => i.id == r.related_items_id)
|
||||||
item && setRelations(current => [...current, item])
|
item && setRelations(current => [...current, item])
|
||||||
})
|
})
|
||||||
@ -133,7 +133,7 @@ export function ProfileView({ userType, attestationApi }: { userType: string , a
|
|||||||
}, [selectPosition])
|
}, [selectPosition])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTemplate(item.layer?.itemType.template || userType);
|
setTemplate(item?.layer?.itemType.template || userType);
|
||||||
}, [userType, item])
|
}, [userType, item])
|
||||||
|
|
||||||
const [urlParams, setUrlParams] = useState(new URLSearchParams(location.search));
|
const [urlParams, setUrlParams] = useState(new URLSearchParams(location.search));
|
||||||
@ -141,7 +141,7 @@ export function ProfileView({ userType, attestationApi }: { userType: string , a
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{item &&
|
{item &&
|
||||||
<MapOverlayPage key={item.id} className={`!tw-p-0 tw-mx-4 tw-mt-4 tw-mb-4 md:tw-w-[calc(50%-32px)] tw-w-[calc(100%-32px)] tw-min-w-80 tw-max-w-3xl !tw-left-0 sm:!tw-left-auto tw-top-0 tw-bottom-0 tw-transition-opacity tw-duration-500 ${!selectPosition ? 'tw-opacity-100 tw-pointer-events-auto' : 'tw-opacity-0 tw-pointer-events-none'}`}>
|
<MapOverlayPage key={item.id} className={`!tw-p-0 tw-mx-4 tw-mt-4 tw-mb-4 md:tw-w-[calc(50%-32px)] tw-w-[calc(100%-32px)] tw-min-w-80 tw-max-w-3xl !tw-left-0 sm:!tw-left-auto tw-top-0 tw-bottom-0 tw-transition-opacity tw-duration-500 ${!selectPosition ? 'tw-opacity-100 tw-pointer-events-auto' : 'tw-opacity-0 tw-pointer-events-none'}`}>
|
||||||
<>
|
<>
|
||||||
<div className={`tw-px-6 tw-pt-6`}>
|
<div className={`tw-px-6 tw-pt-6`}>
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useState } from "react";
|
import { useState, useCallback, useRef } from "react";
|
||||||
import ReactCrop, { Crop, centerCrop, makeAspectCrop } from 'react-image-crop';
|
import ReactCrop, { Crop, centerCrop, makeAspectCrop } from 'react-image-crop';
|
||||||
import { useAssetApi } from '../../AppShell/hooks/useAssets';
|
import { useAssetApi } from '../../AppShell/hooks/useAssets';
|
||||||
|
import 'react-image-crop/dist/ReactCrop.css';
|
||||||
import DialogModal from "../../Templates/DialogModal";
|
import DialogModal from "../../Templates/DialogModal";
|
||||||
import 'react-image-crop/dist/ReactCrop.css'
|
|
||||||
|
|
||||||
|
|
||||||
export const AvatarWidget = ({avatar, setAvatar}:{avatar:string, setAvatar : React.Dispatch<React.SetStateAction<any>>}) => {
|
|
||||||
|
|
||||||
|
interface AvatarWidgetProps {
|
||||||
|
avatar: string;
|
||||||
|
setAvatar: React.Dispatch<React.SetStateAction<any>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AvatarWidget: React.FC<AvatarWidgetProps> = ({ avatar, setAvatar }) => {
|
||||||
const [crop, setCrop] = useState<Crop>();
|
const [crop, setCrop] = useState<Crop>();
|
||||||
const [image, setImage] = useState<string>("");
|
const [image, setImage] = useState<string>("");
|
||||||
const [cropModalOpen, setCropModalOpen] = useState<boolean>(false);
|
const [cropModalOpen, setCropModalOpen] = useState<boolean>(false);
|
||||||
@ -16,31 +18,38 @@ export const AvatarWidget = ({avatar, setAvatar}:{avatar:string, setAvatar : Rea
|
|||||||
|
|
||||||
const assetsApi = useAssetApi();
|
const assetsApi = useAssetApi();
|
||||||
|
|
||||||
|
const imgRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
const onImageChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files && event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const validFormats = ["image/jpeg", "image/png"];
|
||||||
|
const maxSizeMB = 10;
|
||||||
|
const maxSizeBytes = maxSizeMB * 1024 * 1024;
|
||||||
|
|
||||||
const imgRef = React.useRef<HTMLImageElement>(null)
|
if (!validFormats.includes(file.type)) {
|
||||||
|
alert("Unsupported file format. Please upload a JPEG or PNG image.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const onImageChange = (event) => {
|
if (file.size > maxSizeBytes) {
|
||||||
if (event.target.files && event.target.files[0]) {
|
alert(`File size exceeds ${maxSizeMB}MB. Please upload a smaller image.`);
|
||||||
setImage(URL.createObjectURL(event.target.files[0]));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setImage(URL.createObjectURL(file));
|
||||||
|
setCropModalOpen(true);
|
||||||
|
} else {
|
||||||
|
alert("No file selected or an error occurred while selecting the file.");
|
||||||
}
|
}
|
||||||
setCropModalOpen(true);
|
}, []);
|
||||||
}
|
|
||||||
|
|
||||||
function onImageLoad(e: React.SyntheticEvent<HTMLImageElement>) {
|
const onImageLoad = useCallback((e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||||
const { width, height } = e.currentTarget
|
const { width, height } = e.currentTarget;
|
||||||
|
setCrop(centerAspectCrop(width, height, 1));
|
||||||
|
}, []);
|
||||||
|
|
||||||
setCrop(centerAspectCrop(width, height, 1))
|
const centerAspectCrop = (mediaWidth: number, mediaHeight: number, aspect: number) => {
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 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(
|
return centerCrop(
|
||||||
makeAspectCrop(
|
makeAspectCrop(
|
||||||
{
|
{
|
||||||
@ -53,22 +62,50 @@ export const AvatarWidget = ({avatar, setAvatar}:{avatar:string, setAvatar : Rea
|
|||||||
),
|
),
|
||||||
mediaWidth,
|
mediaWidth,
|
||||||
mediaHeight,
|
mediaHeight,
|
||||||
)
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function resizeImage(image: HTMLImageElement, maxWidth: number, maxHeight: number): Promise<HTMLImageElement> {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
let width = image.width;
|
||||||
|
let height = image.height;
|
||||||
|
|
||||||
|
if (width > maxWidth) {
|
||||||
|
height *= maxWidth / width;
|
||||||
|
width = maxWidth;
|
||||||
|
}
|
||||||
|
if (height > maxHeight) {
|
||||||
|
width *= maxHeight / height;
|
||||||
|
height = maxHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
if (ctx) {
|
||||||
|
ctx.drawImage(image, 0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizedImage = new Image();
|
||||||
|
resizedImage.src = canvas.toDataURL();
|
||||||
|
|
||||||
|
await resizedImage.decode();
|
||||||
|
return resizedImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderCrop() {
|
const renderCrop = useCallback(async () => {
|
||||||
// get the image element
|
|
||||||
const image = imgRef.current;
|
const image = imgRef.current;
|
||||||
if (crop && image) {
|
if (crop && image) {
|
||||||
|
const resizedImage = await resizeImage(image, 1024, 1024); // Bildgröße vor dem Zuschneiden reduzieren
|
||||||
|
const scaleX = resizedImage.naturalWidth / resizedImage.width;
|
||||||
|
const scaleY = resizedImage.naturalHeight / resizedImage.height;
|
||||||
|
|
||||||
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(
|
const canvas = new OffscreenCanvas(
|
||||||
crop.width * scaleX,
|
crop.width * scaleX,
|
||||||
crop.height * scaleY,
|
crop.height * scaleY
|
||||||
)
|
);
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
const pixelRatio = window.devicePixelRatio;
|
const pixelRatio = window.devicePixelRatio;
|
||||||
canvas.width = crop.width * pixelRatio * scaleX;
|
canvas.width = crop.width * pixelRatio * scaleX;
|
||||||
@ -76,9 +113,8 @@ export const AvatarWidget = ({avatar, setAvatar}:{avatar:string, setAvatar : Rea
|
|||||||
|
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||||
|
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
image,
|
resizedImage,
|
||||||
crop.x * scaleX,
|
crop.x * scaleX,
|
||||||
crop.y * scaleY,
|
crop.y * scaleY,
|
||||||
crop.width * scaleX,
|
crop.width * scaleX,
|
||||||
@ -89,28 +125,27 @@ export const AvatarWidget = ({avatar, setAvatar}:{avatar:string, setAvatar : Rea
|
|||||||
crop.height * scaleY
|
crop.height * scaleY
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = await canvas.convertToBlob();
|
const blob = await canvas.convertToBlob();
|
||||||
await resizeBlob(blob);
|
await resizeBlob(blob);
|
||||||
setCropping(false);
|
setCropping(false);
|
||||||
setImage("");
|
setImage("");
|
||||||
}
|
}
|
||||||
}
|
}, [crop]);
|
||||||
|
|
||||||
async function resizeBlob(blob) {
|
const resizeBlob = useCallback(async (blob: Blob) => {
|
||||||
var img = new Image();
|
const img = new Image();
|
||||||
img.src = URL.createObjectURL(blob);
|
img.src = URL.createObjectURL(blob);
|
||||||
await img.decode();
|
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 canvas = new OffscreenCanvas(400, 400);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
ctx?.drawImage(img, 0, 0, 400, 400);
|
||||||
|
|
||||||
|
const resizedBlob = await canvas.convertToBlob();
|
||||||
|
const asset = await assetsApi.upload(resizedBlob, "avatar");
|
||||||
|
setAvatar(asset.id);
|
||||||
|
}, [assetsApi, setAvatar]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -135,11 +170,9 @@ export const AvatarWidget = ({avatar, setAvatar}:{avatar:string, setAvatar : Rea
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
: <div className='tw-w-20 tw-flex tw-items-center tw-justify-center'>
|
: <div className='tw-w-20 tw-flex tw-items-center tw-justify-center'>
|
||||||
<span className="tw-loading tw-loading-spinner"></span>
|
<span className="tw-loading tw-loading-spinner"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
}
|
}
|
||||||
<DialogModal
|
<DialogModal
|
||||||
title=""
|
title=""
|
||||||
@ -159,5 +192,5 @@ export const AvatarWidget = ({avatar, setAvatar}:{avatar:string, setAvatar : Rea
|
|||||||
}}>Select</button>
|
}}>Select</button>
|
||||||
</DialogModal>
|
</DialogModal>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from "react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
import { TextAreaInput, TextInput } from "../../Input"
|
import { TextAreaInput } from "../../Input"
|
||||||
import { PopupStartEndInput, TextView } from "../../Map"
|
import { PopupStartEndInput, TextView } from "../../Map"
|
||||||
import { ActionButton } from "../Subcomponents/ActionsButton"
|
import { ActionButton } from "../Subcomponents/ActionsButton"
|
||||||
import { LinkedItemsHeaderView } from "../Subcomponents/LinkedItemsHeaderView"
|
import { LinkedItemsHeaderView } from "../Subcomponents/LinkedItemsHeaderView"
|
||||||
@ -15,46 +15,50 @@ export const TabsForm = ({ item, state, setState, updatePermission, linkItem, un
|
|||||||
|
|
||||||
const updateActiveTab = useCallback((id: number) => {
|
const updateActiveTab = useCallback((id: number) => {
|
||||||
setActiveTab(id);
|
setActiveTab(id);
|
||||||
|
|
||||||
let params = new URLSearchParams(window.location.search);
|
let params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
params.set("tab", `${id}`);
|
params.set("tab", `${id}`);
|
||||||
const newUrl = location.pathname + "?" + params.toString();
|
const newUrl = location.pathname + "?" + params.toString();
|
||||||
window.history.pushState({}, '', newUrl);
|
window.history.pushState({}, '', newUrl);
|
||||||
setUrlParams(params);
|
setUrlParams(params);
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let params = new URLSearchParams(location.search);
|
let params = new URLSearchParams(location.search);
|
||||||
let urlTab = params.get("tab");
|
let urlTab = params.get("tab");
|
||||||
setActiveTab(urlTab ? Number(urlTab) : 1);
|
setActiveTab(urlTab ? Number(urlTab) : 1);
|
||||||
}, [location.search]);
|
}, [location.search]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div role="tablist" className="tw-tabs tw-tabs-lifted tw-mt-3">
|
<div role="tablist" className="tw-tabs tw-tabs-lifted tw-mt-3">
|
||||||
<input type="radio" name="my_tabs_2" role="tab" className={`tw-tab [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]`} aria-label="Info" checked={activeTab == 1 && true} onChange={() => updateActiveTab(1)} />
|
<input type="radio" name="my_tabs_2" role="tab" className={`tw-tab [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]`} aria-label="Info" checked={activeTab == 1 && true} onChange={() => updateActiveTab(1)} />
|
||||||
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-border-[var(--fallback-bc,oklch(var(--bc)/0.2))] tw-rounded-box tw-h-[calc(100dvh-332px)] tw-min-h-56 tw-border-none">
|
<div role="tabpanel" className="tw-tab-content tw-bg-base-100 tw-border-[var(--fallback-bc,oklch(var(--bc)/0.2))] tw-rounded-box tw-h-[calc(100dvh-332px)] tw-min-h-56 tw-border-none">
|
||||||
<div className={`tw-flex tw-flex-col tw-h-full ${item.layer.itemType.show_start_end_input && "tw-pt-4"}`}>
|
<div className={`tw-flex tw-flex-col tw-h-full ${item.layer.itemType.show_start_end_input && "tw-pt-4"}`}>
|
||||||
{item.layer.itemType.show_start_end_input &&
|
{item.layer.itemType.show_start_end_input &&
|
||||||
<PopupStartEndInput
|
<PopupStartEndInput
|
||||||
item={item}
|
item={item}
|
||||||
showLabels={false}
|
showLabels={false}
|
||||||
updateEndValue={(e) => setState(prevState => ({
|
updateEndValue={(e) => setState(prevState => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
end: e
|
end: e
|
||||||
}))}
|
}))}
|
||||||
updateStartValue={(s) => setState(prevState => ({
|
updateStartValue={(s) => setState(prevState => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
start: s
|
start: s
|
||||||
}))}></PopupStartEndInput>
|
}))}></PopupStartEndInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
<TextAreaInput placeholder="about ..." defaultValue={item?.text ? item.text : ""} updateFormValue={(v) => setState(prevState => ({
|
<TextAreaInput placeholder="about ..."
|
||||||
...prevState,
|
defaultValue={item?.text ? item.text : ""}
|
||||||
text: v
|
updateFormValue={(v) => setState(prevState => ({
|
||||||
}))} containerStyle='tw-grow' inputStyle={`tw-h-full ${!item.layer.itemType.show_start_end_input && "tw-border-t-0 tw-rounded-tl-none"}`} />
|
...prevState,
|
||||||
|
text: v
|
||||||
|
}))}
|
||||||
|
containerStyle='tw-grow'
|
||||||
|
inputStyle={`tw-h-full ${!item.layer.itemType.show_start_end_input && "tw-border-t-0 tw-rounded-tl-none"}`} />
|
||||||
<div>
|
<div>
|
||||||
<TextAreaInput
|
<TextAreaInput
|
||||||
placeholder="contact info ..."
|
placeholder="contact info ..."
|
||||||
defaultValue={state.contact || ""}
|
defaultValue={state.contact || ""}
|
||||||
updateFormValue={(c) => setState(prevState => ({
|
updateFormValue={(c) => setState(prevState => ({
|
||||||
|
|||||||
@ -125,18 +125,18 @@ export const onUpdateItem = async (state, item, tags, addTag, setLoading, naviga
|
|||||||
changedItem = {
|
changedItem = {
|
||||||
id: state.id,
|
id: state.id,
|
||||||
name: state.name,
|
name: state.name,
|
||||||
...state.subname && {subname: state.subname},
|
subname: state.subname,
|
||||||
...state.text && {text: state.text},
|
text: state.text,
|
||||||
...state.color && {color: state.color},
|
...state.color && {color: state.color},
|
||||||
position: item.position,
|
position: item.position,
|
||||||
...state.groupType && {group_type: state.groupType},
|
...state.groupType && {group_type: state.groupType},
|
||||||
...state.status && {status: state.status},
|
...state.status && {status: state.status},
|
||||||
...state.contact && {contact: state.contact},
|
contact: state.contact,
|
||||||
...state.telephone && {telephone: state.telephone},
|
telephone: state.telephone,
|
||||||
...state.end && {end: state.end},
|
...state.end && {end: state.end},
|
||||||
...state.start && {start: state.start},
|
...state.start && {start: state.start},
|
||||||
...state.markerIcon && { markerIcon: state.markerIcon },
|
...state.markerIcon && { markerIcon: state.markerIcon },
|
||||||
...state.nextAppointment && {next_appointment: state.nextAppointment},
|
next_appointment: state.nextAppointment,
|
||||||
...state.image.length > 10 && { image: state.image },
|
...state.image.length > 10 && { image: state.image },
|
||||||
...state.offers.length > 0 && { offers: offer_updates },
|
...state.offers.length > 0 && { offers: offer_updates },
|
||||||
...state.needs.length > 0 && { needs: needs_updates }
|
...state.needs.length > 0 && { needs: needs_updates }
|
||||||
|
|||||||
@ -56,6 +56,16 @@ module.exports = {
|
|||||||
'map': "1.4em"
|
'map': "1.4em"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
keyframes: {
|
||||||
|
pulseGrow: {
|
||||||
|
'0%, 100%': { transform: 'scale(1.00)' },
|
||||||
|
'80%': { transform: 'scale(1.00)' },
|
||||||
|
'90%': { transform: 'scale(0.95)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
pulseGrow: 'pulseGrow 2s ease-in-out infinite',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require("daisyui"),
|
require("daisyui"),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user