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 { useNavigate } from "react-router-dom";
|
||||
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,
|
||||
api?: ItemsApi<any>,
|
||||
editCallback?: any,
|
||||
@ -23,7 +25,8 @@ export function HeaderView({ item, api, editCallback, deleteCallback, setPositio
|
||||
hideMenu?: boolean,
|
||||
big?: 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 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>) => {
|
||||
@ -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`}>
|
||||
{title}
|
||||
</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}
|
||||
</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">
|
||||
<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-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 ? (
|
||||
<img src={contact.avatarSrc} alt={contact.name} className="tw-w-full tw-h-full tw-rounded-full" />
|
||||
{avatar ? (
|
||||
<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"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@ -16,8 +21,8 @@ const ContactInfo = ({ contact }) => (
|
||||
)}
|
||||
</div>
|
||||
<div className="tw-text-sm">
|
||||
<p className="tw-font-semibold">{contact.name}</p>
|
||||
<a href={`mailto:${contact.email}`} className="tw-mt-2 tw-text-green-500 tw-flex tw-items-center">
|
||||
<p className="tw-font-semibold">{name}</p>
|
||||
<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"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
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>
|
||||
</svg>
|
||||
|
||||
{contact.email}
|
||||
{email}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)}
|
||||
|
||||
export default ContactInfo;
|
||||
@ -25,6 +25,7 @@ import { TagView } from '../Templates/TagView';
|
||||
import RelationCard from "./RelationCard";
|
||||
import ContactInfo from "./ContactInfo";
|
||||
import ProfileSubHeader from "./ProfileSubHeader";
|
||||
import SocialShareBar from './SocialShareBar';
|
||||
|
||||
export function OverlayItemProfile() {
|
||||
|
||||
@ -297,41 +298,33 @@ export function OverlayItemProfile() {
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{item &&
|
||||
<MapOverlayPage key={item.id} padding={false}
|
||||
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'}`}>
|
||||
<MapOverlayPage key={item.id}
|
||||
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">
|
||||
<HeaderView api={item.layer?.api} item={item} deleteCallback={handleDelete} editCallback={() => navigate("/edit-item/" + item.id)} setPositionCallback={() => { map.closePopup(); setSelectPosition(item); navigate("/") }} big truncateSubname={false} />
|
||||
</div>
|
||||
<div className={`${item.layer?.itemType.onepager && 'tw-p-4'}`}>
|
||||
<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 className='tw-h-full'>
|
||||
</div>
|
||||
|
||||
<div className='tw-h-full tw-overflow-y-auto fade'>
|
||||
|
||||
{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 */}
|
||||
<div className="tw-my-10 tw-px-6">
|
||||
<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'}
|
||||
</p>
|
||||
</div>
|
||||
<TextView item={item}></TextView>
|
||||
|
||||
|
||||
{d.contact && (
|
||||
<ContactInfo name={item.user_created.first_name} avatar={item.user_created.avatar} email={item.contact}/>
|
||||
)}
|
||||
|
||||
{/* Relations Section */}
|
||||
{d.relations && (
|
||||
|
||||
@ -3,7 +3,6 @@ import SocialShareButton from './SocialShareButton';
|
||||
const SocialShareBar = ({ url, title, platforms = ['facebook', 'twitter', 'linkedin', 'xing', 'email'] }) => {
|
||||
return (
|
||||
<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">
|
||||
{platforms.map((platform) => (
|
||||
<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 { 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 = () => {
|
||||
@ -32,12 +32,11 @@ export function MapOverlayPage({ children, className, backdrop, card = true, pad
|
||||
return (
|
||||
<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={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}
|
||||
<button className="tw-btn tw-btn-sm tw-btn-circle tw-btn-ghost tw-absolute tw-right-2 tw-top-2" onClick={() => closeScreen()}>✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
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