mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
profiles, reverse, geocoder, ...
This commit is contained in:
parent
c5c6374d6d
commit
4181460ba1
@ -6,11 +6,13 @@ import { useAssetApi } from '../../../AppShell/hooks/useAssets'
|
|||||||
import DialogModal from "../../../Templates/DialogModal";
|
import DialogModal from "../../../Templates/DialogModal";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useMap } from "react-leaflet";
|
import { useMap } from "react-leaflet";
|
||||||
|
import { reverseGeocode } from "../../../../Utils/ReverseGeocoder";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function HeaderView({ item, api, editCallback, deleteCallback, setPositionCallback, itemNameField, itemSubnameField, itemAvatarField, loading, hideMenu = false, big = false, truncateSubname = true, hideSubname = false }: {
|
export function HeaderView({ item, api, editCallback, deleteCallback, setPositionCallback, itemNameField, itemSubnameField, itemAvatarField, loading, hideMenu = false, big = false, truncateSubname = true, hideSubname = false, showAddress = false }: {
|
||||||
item: Item,
|
item: Item,
|
||||||
api?: ItemsApi<any>,
|
api?: ItemsApi<any>,
|
||||||
editCallback?: any,
|
editCallback?: any,
|
||||||
@ -23,7 +25,8 @@ export function HeaderView({ item, api, editCallback, deleteCallback, setPositio
|
|||||||
hideMenu?: boolean,
|
hideMenu?: boolean,
|
||||||
big?: boolean,
|
big?: boolean,
|
||||||
hideSubname?: boolean,
|
hideSubname?: boolean,
|
||||||
truncateSubname?:boolean
|
truncateSubname?:boolean,
|
||||||
|
showAddress?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +40,17 @@ export function HeaderView({ item, api, editCallback, deleteCallback, setPositio
|
|||||||
const title = itemNameField ? getValue(item, itemNameField) : item.layer?.itemNameField && item && getValue(item, item.layer?.itemNameField);
|
const title = itemNameField ? getValue(item, itemNameField) : item.layer?.itemNameField && item && getValue(item, item.layer?.itemNameField);
|
||||||
const subtitle = itemSubnameField ? getValue(item, itemSubnameField) : item.layer?.itemSubnameField && item && getValue(item, item.layer?.itemSubnameField);
|
const subtitle = itemSubnameField ? getValue(item, itemSubnameField) : item.layer?.itemSubnameField && item && getValue(item, item.layer?.itemSubnameField);
|
||||||
|
|
||||||
|
const [address, setAdress] = React.useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
item.position && reverseGeocode(item.position?.coordinates[1],item.position?.coordinates[0]).then(address => {
|
||||||
|
setAdress(address);
|
||||||
|
});
|
||||||
|
|
||||||
|
}, [item])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const openDeleteModal = async (event: React.MouseEvent<HTMLElement>) => {
|
const openDeleteModal = async (event: React.MouseEvent<HTMLElement>) => {
|
||||||
@ -62,6 +76,9 @@ export function HeaderView({ item, api, editCallback, deleteCallback, setPositio
|
|||||||
<div className={`${big ? "xl:tw-text-3xl tw-text-2xl" : "tw-text-xl"} tw-font-semibold tw-truncate`}>
|
<div className={`${big ? "xl:tw-text-3xl tw-text-2xl" : "tw-text-xl"} tw-font-semibold tw-truncate`}>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
|
{showAddress && address && !hideSubname && <div className={`tw-text-xs tw-text-gray-500 ${truncateSubname && "tw-truncate"}`}>
|
||||||
|
{address}
|
||||||
|
</div>}
|
||||||
{subtitle && !hideSubname && <div className={`tw-text-xs tw-text-gray-500 ${truncateSubname && "tw-truncate"}`}>
|
{subtitle && !hideSubname && <div className={`tw-text-xs tw-text-gray-500 ${truncateSubname && "tw-truncate"}`}>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</div>}
|
</div>}
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
|
import { useAssetApi } from "../AppShell/hooks/useAssets";
|
||||||
|
|
||||||
const ContactInfo = ({ contact }) => (
|
const ContactInfo = ({ email, name, avatar } : {email: string, name: string, avatar: string}) => {
|
||||||
|
const assetsApi = useAssetApi();
|
||||||
|
|
||||||
|
|
||||||
|
return(
|
||||||
<div className="tw-bg-gray-100 tw-my-10 tw-p-6">
|
<div className="tw-bg-gray-100 tw-my-10 tw-p-6">
|
||||||
<h2 className="tw-text-lg tw-font-semibold">Du hast Fragen?</h2>
|
<h2 className="tw-text-lg tw-font-semibold">Du hast Fragen?</h2>
|
||||||
<div className="tw-mt-4 tw-flex tw-items-center">
|
<div className="tw-mt-4 tw-flex tw-items-center">
|
||||||
<div className="tw-w-20 tw-h-20 tw-bg-gray-200 tw-rounded-full tw-mr-5 tw-flex tw-items-center tw-justify-center">
|
<div className="tw-w-20 tw-h-20 tw-bg-gray-200 tw-rounded-full tw-mr-5 tw-flex tw-items-center tw-justify-center">
|
||||||
{contact.avatarSrc ? (
|
{avatar ? (
|
||||||
<img src={contact.avatarSrc} alt={contact.name} className="tw-w-full tw-h-full tw-rounded-full" />
|
<img src={assetsApi.url+avatar} alt={name} className="tw-w-full tw-h-full tw-rounded-full" />
|
||||||
) : (
|
) : (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" className="tw-w-6 tw-h-6"
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" className="tw-w-6 tw-h-6"
|
||||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@ -16,8 +21,8 @@ const ContactInfo = ({ contact }) => (
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="tw-text-sm">
|
<div className="tw-text-sm">
|
||||||
<p className="tw-font-semibold">{contact.name}</p>
|
<p className="tw-font-semibold">{name}</p>
|
||||||
<a href={`mailto:${contact.email}`} className="tw-mt-2 tw-text-green-500 tw-flex tw-items-center">
|
<a href={`mailto:${email}`} className="tw-mt-2 tw-text-green-500 tw-flex tw-items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||||
className="tw-w-4 tw-h-4 tw-mr-1">
|
className="tw-w-4 tw-h-4 tw-mr-1">
|
||||||
@ -25,11 +30,11 @@ const ContactInfo = ({ contact }) => (
|
|||||||
<polyline points="22,6 12,13 2,6"></polyline>
|
<polyline points="22,6 12,13 2,6"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{contact.email}
|
{email}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
|
|
||||||
export default ContactInfo;
|
export default ContactInfo;
|
||||||
@ -25,6 +25,7 @@ import { TagView } from '../Templates/TagView';
|
|||||||
import RelationCard from "./RelationCard";
|
import RelationCard from "./RelationCard";
|
||||||
import ContactInfo from "./ContactInfo";
|
import ContactInfo from "./ContactInfo";
|
||||||
import ProfileSubHeader from "./ProfileSubHeader";
|
import ProfileSubHeader from "./ProfileSubHeader";
|
||||||
|
import SocialShareBar from './SocialShareBar';
|
||||||
|
|
||||||
export function OverlayItemProfile() {
|
export function OverlayItemProfile() {
|
||||||
|
|
||||||
@ -297,41 +298,33 @@ export function OverlayItemProfile() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{item &&
|
{item &&
|
||||||
<MapOverlayPage key={item.id} padding={false}
|
<MapOverlayPage key={item.id}
|
||||||
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-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'}`}>
|
className={`${item.layer?.itemType.onepager && '!tw-p-0'} 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-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={`${item.layer?.itemType.onepager && 'tw-p-4'}`}>
|
||||||
<HeaderView api={item.layer?.api} item={item} deleteCallback={handleDelete} editCallback={() => navigate("/edit-item/" + item.id)} setPositionCallback={() => { map.closePopup(); setSelectPosition(item); navigate("/") }} big truncateSubname={false} />
|
<HeaderView showAddress api={item.layer?.api} item={item} deleteCallback={handleDelete} editCallback={() => navigate("/edit-item/" + item.id)} setPositionCallback={() => { map.closePopup(); setSelectPosition(item); navigate("/") }} big truncateSubname={false} />
|
||||||
|
<SocialShareBar url={""} title={"title"} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='tw-h-full'>
|
<div className='tw-h-full tw-overflow-y-auto fade'>
|
||||||
|
|
||||||
{item.layer?.itemType.onepager &&
|
{item.layer?.itemType.onepager &&
|
||||||
<>
|
<>
|
||||||
<ProfileSubHeader
|
|
||||||
location={d.location}
|
|
||||||
country={d.country}
|
|
||||||
countryCode={d.countryCode}
|
|
||||||
url={d.url}
|
|
||||||
title={d.title}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{d.contact && (
|
|
||||||
<ContactInfo contact={d.contact}/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Description Section */}
|
{/* Description Section */}
|
||||||
<div className="tw-my-10 tw-px-6">
|
<TextView item={item}></TextView>
|
||||||
<h2 className="tw-text-lg tw-font-semibold">Beschreibung</h2>
|
|
||||||
<p className="tw-mt-2 tw-text-sm tw-text-gray-600">
|
|
||||||
{d.description ?? 'Keine Beschreibung vorhanden'}
|
{d.contact && (
|
||||||
</p>
|
<ContactInfo name={item.user_created.first_name} avatar={item.user_created.avatar} email={item.contact}/>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Relations Section */}
|
{/* Relations Section */}
|
||||||
{d.relations && (
|
{d.relations && (
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import SocialShareButton from './SocialShareButton';
|
|||||||
const SocialShareBar = ({ url, title, platforms = ['facebook', 'twitter', 'linkedin', 'xing', 'email'] }) => {
|
const SocialShareBar = ({ url, title, platforms = ['facebook', 'twitter', 'linkedin', 'xing', 'email'] }) => {
|
||||||
return (
|
return (
|
||||||
<div className="tw-flex tw-items-center tw-justify-end tw-space-x-2">
|
<div className="tw-flex tw-items-center tw-justify-end tw-space-x-2">
|
||||||
<span className="tw-text-sm tw-font-medium tw-text-gray-700">Teilen:</span>
|
|
||||||
<div className="tw-flex tw-space-x-2">
|
<div className="tw-flex tw-space-x-2">
|
||||||
{platforms.map((platform) => (
|
{platforms.map((platform) => (
|
||||||
<SocialShareButton
|
<SocialShareButton
|
||||||
|
|||||||
9
src/Components/README.md
Normal file
9
src/Components/README.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
**AppShell** provides componentes to structure the overall layout of a singlepage application including Navbar and Sidebar
|
||||||
|
|
||||||
|
**Auth** provides the UI components for Login, Signup, Password Reset and the useAuth hook, which handls all the authentification logic and provides the user context
|
||||||
|
|
||||||
|
**Gaming** provides components for gamification
|
||||||
|
|
||||||
|
**Input**
|
||||||
|
|
||||||
|
**Map**
|
||||||
@ -3,7 +3,7 @@ import * as L from 'leaflet';
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export function MapOverlayPage({ children, className, backdrop, card = true, padding = true }: { children: React.ReactNode, className?: string, backdrop?: boolean, card?:boolean, padding?:boolean }) {
|
export function MapOverlayPage({ children, className, backdrop, card = true }: { children: React.ReactNode, className?: string, backdrop?: boolean, card?:boolean }) {
|
||||||
|
|
||||||
|
|
||||||
const closeScreen = () => {
|
const closeScreen = () => {
|
||||||
@ -32,7 +32,7 @@ export function MapOverlayPage({ children, className, backdrop, card = true, pad
|
|||||||
return (
|
return (
|
||||||
<div className={`tw-absolute tw-h-full tw-w-full tw-m-auto ${backdrop && "tw-z-[2000]"}`}>
|
<div className={`tw-absolute tw-h-full tw-w-full tw-m-auto ${backdrop && "tw-z-[2000]"}`}>
|
||||||
<div ref={backdropRef} className={`${backdrop && "tw-backdrop-brightness-75"} tw-h-full tw-w-full tw-grid tw-place-items-center tw-m-auto`} >
|
<div ref={backdropRef} className={`${backdrop && "tw-backdrop-brightness-75"} tw-h-full tw-w-full tw-grid tw-place-items-center tw-m-auto`} >
|
||||||
<div ref={overlayRef} className={`${card && "tw-card tw-card-body"} ${padding ? "tw-p-6" : "tw-p-0"} tw-shadow-xl tw-bg-base-100 ${className && className} ${!backdrop && "tw-z-[2000]"} tw-absolute tw-top-0 tw-bottom-0 tw-right-0 tw-left-0 tw-m-auto`}>
|
<div ref={overlayRef} className={`${card && "tw-card tw-card-body"} tw-shadow-xl tw-bg-base-100 tw-p-6 ${className && className} ${!backdrop && "tw-z-[2000]"} tw-absolute tw-top-0 tw-bottom-0 tw-right-0 tw-left-0 tw-m-auto`}>
|
||||||
{children}
|
{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>
|
<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>
|
||||||
@ -40,4 +40,3 @@ export function MapOverlayPage({ children, className, backdrop, card = true, pad
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
41
src/Utils/ReverseGeocoder.ts
Normal file
41
src/Utils/ReverseGeocoder.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface ReverseGeocodeResponse {
|
||||||
|
place_id: number;
|
||||||
|
licence: string;
|
||||||
|
osm_type: string;
|
||||||
|
osm_id: number;
|
||||||
|
lat: string;
|
||||||
|
lon: string;
|
||||||
|
display_name: string;
|
||||||
|
address: {
|
||||||
|
road?: string;
|
||||||
|
house_number?: string;
|
||||||
|
city?: string;
|
||||||
|
town?: string;
|
||||||
|
village?: string;
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
};
|
||||||
|
boundingbox: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reverseGeocode(lat: number, lon: number): Promise<string> {
|
||||||
|
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}&addressdetails=1`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get<ReverseGeocodeResponse>(url);
|
||||||
|
const address = response.data.address;
|
||||||
|
|
||||||
|
// Extrahiere Straße, Hausnummer und Ort
|
||||||
|
const street = address.road || '';
|
||||||
|
const houseNumber = address.house_number || '';
|
||||||
|
const city = address.city || address.town || address.village || '';
|
||||||
|
|
||||||
|
// Formatiere die Adresse
|
||||||
|
const formattedAddress = `${street} ${houseNumber}, ${city}`.trim();
|
||||||
|
return formattedAddress || '';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user