flex profiles

This commit is contained in:
Anton Tranelis 2024-11-05 09:15:20 +01:00
parent 9b120d6e83
commit 3f701233a1
27 changed files with 556 additions and 406 deletions

2
package-lock.json generated
View File

@ -7,7 +7,7 @@
"": {
"name": "utopia-ui",
"version": "3.0.10",
"license": "MIT",
"license": "GPL-3.0-only",
"dependencies": {
"@heroicons/react": "^2.0.17",
"@tanstack/react-query": "^5.17.8",

View File

@ -1,6 +1,6 @@
import * as React from 'react'
import NavBar from './NavBar'
import { SetAssetsApi } from './SetAssetsApi'
import { SetAppState } from './SetAppState'
import { AssetsApi } from '../../types'
import { ContextWrapper } from './ContextWrapper'
@ -18,7 +18,7 @@ export function AppShell({
return (
<ContextWrapper>
<div className='tw-flex tw-flex-col tw-h-full'>
<SetAssetsApi assetsApi={assetsApi} />
<SetAppState assetsApi={assetsApi} userType={userType} />
<NavBar userType={userType} appName={appName}></NavBar>
<div id='app-content' className='tw-flex-grow'>
{children}

View File

@ -9,7 +9,7 @@ import { LeafletRefsProvider } from '../Map/hooks/useLeafletRefs'
import { PermissionsProvider } from '../Map/hooks/usePermissions'
import { SelectPositionProvider } from '../Map/hooks/useSelectPosition'
import { TagsProvider } from '../Map/hooks/useTags'
import { AssetsProvider } from './hooks/useAssets'
import { AppStateProvider } from './hooks/useAppState'
import { useContext, createContext } from 'react'
import { BrowserRouter as Router, useLocation } from 'react-router-dom'
@ -71,7 +71,7 @@ export const Wrappers = ({ children }) => {
<SelectPositionProvider>
<LeafletRefsProvider initialLeafletRefs={{}}>
<QueryClientProvider client={queryClient}>
<AssetsProvider>
<AppStateProvider>
<ClusterRefProvider>
<QuestsProvider initialOpen={true}>
<ToastContainer
@ -89,7 +89,7 @@ export const Wrappers = ({ children }) => {
{children}
</QuestsProvider>
</ClusterRefProvider>
</AssetsProvider>
</AppStateProvider>
</QueryClientProvider>
</LeafletRefsProvider>
</SelectPositionProvider>

View File

@ -0,0 +1,24 @@
import { useSetAppState } from './hooks/useAppState'
import { AssetsApi } from '../../types'
import { useEffect } from 'react'
export const SetAppState = ({
assetsApi,
userType,
}: {
assetsApi: AssetsApi
userType: string
}) => {
const setAppState = useSetAppState()
useEffect(() => {
setAppState({ assetsApi })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assetsApi])
useEffect(() => {
setAppState({ userType })
}, [setAppState, userType])
return <></>
}

View File

@ -1,14 +0,0 @@
import { useSetAssetApi } from './hooks/useAssets'
import { AssetsApi } from '../../types'
import { useEffect } from 'react'
export const SetAssetsApi = ({ assetsApi }: { assetsApi: AssetsApi }) => {
const setAssetsApi = useSetAssetApi()
useEffect(() => {
setAssetsApi(assetsApi)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assetsApi])
return <></>
}

View File

@ -0,0 +1,50 @@
import { useCallback, useState, createContext, useContext } from 'react'
import * as React from 'react'
import { AssetsApi } from '../../../types'
type AppState = {
assetsApi: AssetsApi
userType: string
}
type UseAppManagerResult = ReturnType<typeof useAppManager>
const initialAppState: AppState = {
assetsApi: {} as AssetsApi,
userType: '',
}
const AppContext = createContext<UseAppManagerResult>({
state: initialAppState,
setAppState: () => {},
})
function useAppManager(): {
state: AppState
setAppState: (newState: Partial<AppState>) => void
} {
const [state, setState] = useState<AppState>(initialAppState)
const setAppState = useCallback((newState: Partial<AppState>) => {
setState((prevState) => ({
...prevState,
...newState,
}))
}, [])
return { state, setAppState }
}
export const AppStateProvider: React.FunctionComponent<{
children?: React.ReactNode
}> = ({ children }) => <AppContext.Provider value={useAppManager()}>{children}</AppContext.Provider>
export const useAppState = (): AppState => {
const { state } = useContext(AppContext)
return state
}
export const useSetAppState = (): UseAppManagerResult['setAppState'] => {
const { setAppState } = useContext(AppContext)
return setAppState
}

View File

@ -1,40 +0,0 @@
import { useCallback, useState, createContext, useContext } from 'react'
import * as React from 'react'
import { AssetsApi } from '../../../types'
type UseAssetManagerResult = ReturnType<typeof useAssetsManager>
const AssetContext = createContext<UseAssetManagerResult>({
api: {} as AssetsApi,
setAssetsApi: () => {},
})
function useAssetsManager(): {
api: AssetsApi
setAssetsApi: (api: AssetsApi) => void
} {
const [api, setApi] = useState<AssetsApi>({} as AssetsApi)
const setAssetsApi = useCallback((api: AssetsApi) => {
setApi(api)
}, [])
return { api, setAssetsApi }
}
export const AssetsProvider: React.FunctionComponent<{
children?: React.ReactNode
}> = ({ children }) => (
<AssetContext.Provider value={useAssetsManager()}>{children}</AssetContext.Provider>
)
export const useAssetApi = (): AssetsApi => {
const { api } = useContext(AssetContext)
return api
}
export const useSetAssetApi = (): UseAssetManagerResult['setAssetsApi'] => {
const { setAssetsApi } = useContext(AssetContext)
return setAssetsApi
}

View File

@ -2,7 +2,7 @@ import * as React from 'react'
import { Item, ItemsApi } from '../../../../types'
import { useHasUserPermission } from '../../hooks/usePermissions'
import { getValue } from '../../../../Utils/GetValue'
import { useAssetApi } from '../../../AppShell/hooks/useAssets'
import { useAppState } from '../../../AppShell/hooks/useAppState'
import DialogModal from '../../../Templates/DialogModal'
import { useNavigate } from 'react-router-dom'
@ -41,17 +41,17 @@ export function HeaderView({
const hasUserPermission = useHasUserPermission()
const navigate = useNavigate()
const assetsApi = useAssetApi()
const appState = useAppState()
const avatar =
itemAvatarField && getValue(item, itemAvatarField)
? assetsApi.url +
? appState.assetsApi.url +
getValue(item, itemAvatarField) +
`${big ? '?width=160&heigth=160' : '?width=80&heigth=80'}`
: item.layer?.itemAvatarField &&
item &&
getValue(item, item.layer?.itemAvatarField) &&
assetsApi.url +
appState.assetsApi.url +
getValue(item, item.layer?.itemAvatarField) +
`${big ? '?width=160&heigth=160' : '?width=80&heigth=80'}`
const title = itemNameField

View File

@ -1,4 +1,3 @@
/* eslint-disable no-constant-condition */
import { useItems, useUpdateItem, useAddItem } from '../Map/hooks/useItems'
import { useEffect, useState } from 'react'
import { getValue } from '../../Utils/GetValue'
@ -14,21 +13,23 @@ import { linkItem, onUpdateItem, unlinkItem } from './itemFunctions'
import { SimpleForm } from './Templates/SimpleForm'
import { TabsForm } from './Templates/TabsForm'
import { FormHeader } from './Subcomponents/FormHeader'
import { useAppState } from '../AppShell/hooks/useAppState'
import { FlexForm } from './Templates/FlexForm'
export function ProfileForm({ userType }: { userType: string }) {
export function ProfileForm() {
const [state, setState] = useState({
color: '',
id: '',
groupType: 'wuerdekompass',
group_type: 'wuerdekompass',
status: 'active',
name: '',
subname: '',
text: '',
contact: '',
telephone: '',
nextAppointment: '',
next_appointment: '',
image: '',
markerIcon: '',
marker_icon: '',
offers: [] as Tag[],
needs: [] as Tag[],
relations: [] as Item[],
@ -50,13 +51,13 @@ export function ProfileForm({ userType }: { userType: string }) {
const hasUserPermission = useHasUserPermission()
const getItemTags = useGetItemTags()
const items = useItems()
const appState = useAppState()
const [urlParams, setUrlParams] = useState(new URLSearchParams(location.search))
useEffect(() => {
item && hasUserPermission('items', 'update', item) && setUpdatePermission(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item])
}, [hasUserPermission, item])
useEffect(() => {
const itemId = location.pathname.split('/')[2]
@ -64,9 +65,8 @@ export function ProfileForm({ userType }: { userType: string }) {
const item = items.find((i) => i.id === itemId)
item && setItem(item)
const layer = layers.find((l) => l.itemType.name === userType)
!item &&
if (!item) {
const layer = layers.find((l) => l.itemType.name === appState.userType)
setItem({
id: crypto.randomUUID(),
name: user ? user.first_name : '',
@ -74,6 +74,7 @@ export function ProfileForm({ userType }: { userType: string }) {
layer,
new: true,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items])
@ -107,16 +108,16 @@ export function ProfileForm({ userType }: { userType: string }) {
setState({
color: newColor,
id: item?.id ?? '',
groupType: item?.group_type ?? 'wuerdekompass',
group_type: item?.group_type ?? 'wuerdekompass',
status: item?.status ?? 'active',
name: item?.name ?? '',
subname: item?.subname ?? '',
text: item?.text ?? '',
contact: item?.contact ?? '',
telephone: item?.telephone ?? '',
nextAppointment: item?.next_appointment ?? '',
next_appointment: item?.next_appointment ?? '',
image: item?.image ?? '',
markerIcon: item?.marker_icon ?? '',
marker_icon: item?.marker_icon ?? '',
offers,
needs,
relations,
@ -129,8 +130,8 @@ export function ProfileForm({ userType }: { userType: string }) {
const [template, setTemplate] = useState<string>('')
useEffect(() => {
setTemplate(item.layer?.itemType.template || userType)
}, [userType, item])
setTemplate(item.layer?.itemType.template || appState.userType)
}, [appState.userType, item])
return (
<>
@ -147,6 +148,10 @@ export function ProfileForm({ userType }: { userType: string }) {
{template === 'simple' && <SimpleForm state={state} setState={setState}></SimpleForm>}
{template === 'flex' && (
<FlexForm item={item} state={state} setState={setState}></FlexForm>
)}
{template === 'tabs' && (
<TabsForm
loading={loading}
@ -177,14 +182,10 @@ export function ProfileForm({ userType }: { userType: string }) {
urlParams,
)
}
style={
true
? {
backgroundColor: `${item.layer?.itemColorField && getValue(item, item.layer?.itemColorField) ? getValue(item, item.layer?.itemColorField) : getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item?.layer?.markerDefaultColor}`,
color: '#fff',
}
: { color: '#fff' }
}
style={{
backgroundColor: `${item.layer?.itemColorField && getValue(item, item.layer?.itemColorField) ? getValue(item, item.layer?.itemColorField) : getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item?.layer?.markerDefaultColor}`,
color: '#fff',
}}
>
Update
</button>

View File

@ -16,14 +16,10 @@ import { OnepagerView } from './Templates/OnepagerView'
import { SimpleView } from './Templates/SimpleView'
import { handleDelete, linkItem, unlinkItem } from './itemFunctions'
import { useTags } from '../Map/hooks/useTags'
import { FlexView } from './Templates/FlexView'
import { useAppState } from '../AppShell/hooks/useAppState'
export function ProfileView({
userType,
attestationApi,
}: {
userType: string
attestationApi?: ItemsApi<any>
}) {
export function ProfileView({ attestationApi }: { attestationApi?: ItemsApi<any> }) {
const [item, setItem] = useState<Item>()
const [updatePermission, setUpdatePermission] = useState<boolean>(false)
const [relations, setRelations] = useState<Array<Item>>([])
@ -44,6 +40,7 @@ export function ProfileView({
const setSelectPosition = useSetSelectPosition()
const clusterRef = useClusterRef()
const leafletRefs = useLeafletRefs()
const appState = useAppState()
const [attestations, setAttestations] = useState<Array<any>>([])
@ -149,8 +146,8 @@ export function ProfileView({
}, [selectPosition])
useEffect(() => {
setTemplate(item?.layer?.itemType.template || userType)
}, [userType, item])
setTemplate(item?.layer?.itemType.template || appState.userType)
}, [appState.userType, item])
return (
<>
@ -176,13 +173,14 @@ export function ProfileView({
/>
</div>
{template === 'onepager' && <OnepagerView item={item} userType={userType} />}
{template === 'onepager' && <OnepagerView item={item} />}
{template === 'simple' && <SimpleView item={item} />}
{template === 'flex' && <FlexView item={item} />}
{template === 'tabs' && (
<TabsView
userType={userType}
attestations={attestations}
item={item}
loading={loading}

View File

@ -1,7 +1,7 @@
import * as React from 'react'
import { useState, useCallback, useRef } from 'react'
import ReactCrop, { Crop, centerCrop, makeAspectCrop } from 'react-image-crop'
import { useAssetApi } from '../../AppShell/hooks/useAssets'
import { useAppState } from '../../AppShell/hooks/useAppState'
import 'react-image-crop/dist/ReactCrop.css'
import DialogModal from '../../Templates/DialogModal'
@ -16,7 +16,7 @@ export const AvatarWidget: React.FC<AvatarWidgetProps> = ({ avatar, setAvatar })
const [cropModalOpen, setCropModalOpen] = useState<boolean>(false)
const [cropping, setCropping] = useState<boolean>(false)
const assetsApi = useAssetApi()
const appState = useAppState()
const imgRef = useRef<HTMLImageElement>(null)
@ -146,10 +146,10 @@ export const AvatarWidget: React.FC<AvatarWidgetProps> = ({ avatar, setAvatar })
ctx?.drawImage(img, 0, 0, 400, 400)
const resizedBlob = await canvas.convertToBlob()
const asset = await assetsApi.upload(resizedBlob, 'avatar')
const asset = await appState.assetsApi.upload(resizedBlob, 'avatar')
setAvatar(asset.id)
},
[assetsApi, setAvatar],
[appState.assetsApi, setAvatar],
)
return (
@ -180,7 +180,10 @@ export const AvatarWidget: React.FC<AvatarWidgetProps> = ({ avatar, setAvatar })
</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' />
<img
src={appState.assetsApi.url + avatar}
className='tw-h-20 tw-w-20 tw-rounded-full'
/>
</div>
) : (
<div className='tw-h-20 tw-w-20'>

View File

@ -1,33 +1,46 @@
import { Link } from 'react-router-dom'
import { useAssetApi } from '../../AppShell/hooks/useAssets'
import { useAppState } from '../../AppShell/hooks/useAppState'
import { Item } from '../../../types'
import { useEffect, useState } from 'react'
import { useItems } from '../../Map/hooks/useItems'
const ContactInfo = ({
email,
telephone,
name,
avatar,
link,
}: {
email: string
telephone: string
name: string
avatar: string
link?: string
}) => {
const assetsApi = useAssetApi()
const ContactInfo = ({ item }: { item: Item }) => {
const appState = useAppState()
const [profileOwner, setProfileOwner] = useState<Item>()
const items = useItems()
useEffect(() => {
console.log(
'user:',
items.find(
(i) =>
i.user_created?.id === item.user_created?.id &&
i.layer?.itemType.name === appState.userType,
),
)
setProfileOwner(
items.find(
(i) =>
i.user_created?.id === item.user_created?.id &&
i.layer?.itemType.name === appState.userType,
),
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item, items])
return (
<div className='tw-bg-base-200 tw-mb-6 tw-mt-6 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'>
{avatar && (
<ConditionalLink url={link}>
{profileOwner?.image && (
<ConditionalLink url={'/item/' + profileOwner?.id}>
<div className='tw-mr-5 tw-flex tw-items-center tw-justify-center'>
<div className='tw-avatar'>
<div className='tw-w-20 tw-h-20 tw-bg-gray-200 rounded-full tw-flex tw-items-center tw-justify-center overflow-hidden'>
<img
src={assetsApi.url + avatar}
alt={name}
src={appState.assetsApi.url + profileOwner?.image}
alt={profileOwner?.name}
className='tw-w-full tw-h-full tw-object-cover'
/>
</div>
@ -36,11 +49,11 @@ const ContactInfo = ({
</ConditionalLink>
)}
<div className='tw-text-sm tw-flex-grow'>
<p className='tw-font-semibold'>{name}</p>
{email && (
<p className='tw-font-semibold'>{profileOwner?.name}</p>
{item.contact && (
<p>
<a
href={`mailto:${email}`}
href={`mailto:${item.contact}`}
className='tw-mt-2 tw-text-green-500 tw-inline-flex tw-items-center'
>
<svg
@ -56,14 +69,14 @@ const ContactInfo = ({
<path d='M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z'></path>
<polyline points='22,6 12,13 2,6'></polyline>
</svg>
{email}
{item.contact}
</a>
</p>
)}
{telephone && (
{item.telephone && (
<p>
<a
href={`tel:${telephone}`}
href={`tel:${item.telephone}`}
className='tw-mt-2 tw-text-green-500 tw-inline-flex tw-items-center tw-whitespace-nowrap'
>
<svg
@ -78,7 +91,7 @@ const ContactInfo = ({
>
<path d='M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 16.92z' />
</svg>
{telephone}
{item.telephone}
</a>
</p>
)}

View File

@ -0,0 +1,53 @@
import * as React from 'react'
import { TextInput } from '../../Input'
import { FormState } from '../Templates/OnepagerForm'
export const ContactInfoForm = ({
state,
setState,
}: {
state: FormState
setState: React.Dispatch<React.SetStateAction<any>>
}) => {
return (
<div className='tw-space-y-6'>
<div>
<label
htmlFor='email'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
>
Email-Adresse (Kontakt):
</label>
<TextInput
placeholder='Email'
defaultValue={state.contact}
updateFormValue={(v) =>
setState((prevState) => ({
...prevState,
contact: v,
}))
}
/>
</div>
<div>
<label
htmlFor='telephone'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
>
Telefonnummer (Kontakt):
</label>
<TextInput
placeholder='Telefonnummer'
defaultValue={state.telephone}
updateFormValue={(v) =>
setState((prevState) => ({
...prevState,
telephone: v,
}))
}
/>
</div>
</div>
)
}

View File

@ -0,0 +1,42 @@
/* eslint-disable camelcase */
import { Item } from '../../../types'
import SocialShareBar from './SocialShareBar'
const statusMapping = {
in_planning: 'in Planung',
paused: 'pausiert',
active: 'aktiv',
}
export const GroupSubHeaderView = ({
item,
share_base_url,
}: {
item: Item
share_base_url: string
}) => (
<div className='tw-px-6'>
<div className='tw-float-left tw-mt-2 tw-mb-4 tw-flex tw-items-center'>
{item.status && (
<div className='tw-mt-1.5'>
<span className='tw-text-sm tw-text-current tw-bg-base-300 tw-rounded tw-py-0.5 tw-px-2 tw-inline-flex tw-items-center tw-mr-2'>
<span
className={`tw-w-2 tw-h-2 ${item.status === 'in_planning' && 'tw-bg-blue-700'} ${item.status === 'paused' && 'tw-bg-orange-400'} ${item.status === 'active' && 'tw-bg-green-500'} tw-rounded-full tw-mr-1.5`}
></span>
{statusMapping[item.status]}
</span>
</div>
)}
{item.group_type && (
<div className='tw-mt-1.5'>
<span className='tw-text-sm tw-text-current tw-bg-base-300 tw-rounded tw-py-1 tw-px-2'>
{item.group_type}
</span>
</div>
)}
</div>
<div>
<SocialShareBar url={share_base_url + item.slug} title={item.name} />
</div>
</div>
)

View File

@ -0,0 +1,102 @@
import * as React from 'react'
import ComboBoxInput from '../../Input/ComboBoxInput'
import { Item } from '../../../types'
import { useEffect } from 'react'
import { FormState } from '../Templates/OnepagerForm'
const typeMapping = [
{ value: 'wuerdekompass', label: 'Regional-Gruppe' },
{ value: 'themenkompass', label: 'Themen-Gruppe' },
{ value: 'liebevoll.jetzt', label: 'liebevoll.jetzt' },
]
const statusMapping = [
{ value: 'active', label: 'aktiv' },
{ value: 'in_planning', label: 'in Planung' },
{ value: 'paused', label: 'pausiert' },
]
export const GroupSubheaderForm = ({
state,
setState,
item,
}: {
state: FormState
setState: React.Dispatch<React.SetStateAction<any>>
item: Item
}) => {
useEffect(() => {
switch (state.group_type) {
case 'wuerdekompass':
setState((prevState) => ({
...prevState,
color: item?.layer?.menuColor || '#1A5FB4',
marker_icon: 'group',
image: '59e6a346-d1ee-4767-9e42-fc720fb535c9',
}))
break
case 'themenkompass':
setState((prevState) => ({
...prevState,
color: '#26A269',
marker_icon: 'group',
image: '59e6a346-d1ee-4767-9e42-fc720fb535c9',
}))
break
case 'liebevoll.jetzt':
setState((prevState) => ({
...prevState,
color: '#E8B620',
marker_icon: 'liebevoll.jetzt',
image: 'e735b96c-507b-471c-8317-386ece0ca51d',
}))
break
default:
break
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.group_type])
return (
<div className='tw-grid tw-grid-cols-1 md:tw-grid-cols-2 tw-gap-6'>
<div>
<label
htmlFor='groupType'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
>
Gruppenart:
</label>
<ComboBoxInput
id='groupType'
options={typeMapping}
value={state.group_type}
onValueChange={(v) =>
setState((prevState) => ({
...prevState,
group_type: v,
}))
}
/>
</div>
<div>
<label
htmlFor='status'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
>
Gruppenstatus:
</label>
<ComboBoxInput
id='status'
options={statusMapping}
value={state.status}
onValueChange={(v) =>
setState((prevState) => ({
...prevState,
status: v,
}))
}
/>
</div>
</div>
)
}

View File

@ -1,7 +1,7 @@
import { useEffect } from 'react'
import { getValue } from '../../../Utils/GetValue'
import { Item } from '../../../types'
import { useAssetApi } from '../../AppShell/hooks/useAssets'
import { useAppState } from '../../AppShell/hooks/useAppState'
export function LinkedItemsHeaderView({
item,
@ -20,15 +20,15 @@ export function LinkedItemsHeaderView({
loading?: boolean
unlinkPermission: boolean
}) {
const assetsApi = useAssetApi()
const appState = useAppState()
const avatar =
itemAvatarField && getValue(item, itemAvatarField)
? assetsApi.url + getValue(item, itemAvatarField)
? appState.assetsApi.url + getValue(item, itemAvatarField)
: item.layer?.itemAvatarField &&
item &&
getValue(item, item.layer?.itemAvatarField) &&
assetsApi.url + getValue(item, item.layer?.itemAvatarField)
appState.assetsApi.url + getValue(item, item.layer?.itemAvatarField)
const title = itemNameField
? getValue(item, itemNameField)
: item.layer?.itemNameField && item && getValue(item, item.layer?.itemNameField)

View File

@ -1,54 +0,0 @@
import SocialShareBar from './SocialShareBar'
/* const flags = {
de: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 3" className="tw-w-5 tw-h-3">
<rect width="5" height="3" fill="#FFCE00" />
<rect width="5" height="2" fill="#DD0000" />
<rect width="5" height="1" fill="#000000" />
</svg>
),
at: (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 3" className="tw-w-5 tw-h-3">
<rect width="5" height="3" fill="#ED2939" />
<rect width="5" height="2" fill="#FFFFFF" />
<rect width="5" height="1" fill="#ED2939" />
</svg>
)
}; */
const statusMapping = {
in_planning: 'in Planung',
paused: 'pausiert',
active: 'aktiv',
}
// eslint-disable-next-line react/prop-types
const SubHeader = ({ type, status, url, title }) => (
<div>
<div className='tw-float-left tw-mt-2 tw-mb-4 tw-flex tw-items-center'>
{status && (
<div className='tw-mt-1.5'>
<span className='tw-text-sm tw-text-current tw-bg-base-300 tw-rounded tw-py-0.5 tw-px-2 tw-inline-flex tw-items-center tw-mr-2'>
<span
className={`tw-w-2 tw-h-2 ${status === 'in_planning' && 'tw-bg-blue-700'} ${status === 'paused' && 'tw-bg-orange-400'} ${status === 'active' && 'tw-bg-green-500'} tw-rounded-full tw-mr-1.5`}
></span>
{statusMapping[status]}
</span>
</div>
)}
{type && (
<div className='tw-mt-1.5'>
<span className='tw-text-sm tw-text-current tw-bg-base-300 tw-rounded tw-py-1 tw-px-2'>
{type}
</span>
</div>
)}
</div>
<div>
<SocialShareBar url={url} title={title} />
</div>
</div>
)
export default SubHeader

View File

@ -0,0 +1,48 @@
/* eslint-disable camelcase */
import * as React from 'react'
import { TextAreaInput } from '../../Input'
import { FormState } from '../Templates/OnepagerForm'
import { getValue } from '../../../Utils/GetValue'
import { useEffect, useState } from 'react'
export const ProfileTextForm = ({
state,
setState,
data_field,
section_name,
}: {
state: FormState
setState: React.Dispatch<React.SetStateAction<any>>
data_field?: string
section_name: string
}) => {
const [field, setField] = useState<string>(data_field || 'text')
useEffect(() => {
if (!data_field) {
setField('text')
}
}, [data_field])
return (
<div>
<label
htmlFor='nextAppointment'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
>
{section_name || 'Text'}:
</label>
<TextAreaInput
placeholder={'...'}
defaultValue={getValue(state, field)}
updateFormValue={(v) =>
setState((prevState) => ({
...prevState,
[field]: v,
}))
}
inputStyle='tw-h-24'
/>
</div>
)
}

View File

@ -0,0 +1,23 @@
/* eslint-disable camelcase */
import { Item } from '../../../types'
import { getValue } from '../../../Utils/GetValue'
import { TextView } from '../../Map'
export const ProfileTextView = ({
item,
data_field,
section_name,
}: {
item: Item
data_field: string
section_name: string
}) => {
return (
<div className='tw-my-10 tw-mt-2 tw-px-6'>
<h2 className='tw-text-lg tw-font-semibold'>{section_name}</h2>
<div className='tw-mt-2 tw-text-sm'>
<TextView rawText={data_field ? getValue(item, data_field) : getValue(item, 'text')} />
</div>
</div>
)
}

View File

@ -0,0 +1,41 @@
import * as React from 'react'
import { Item } from '../../../types'
import { FormState } from './OnepagerForm'
import { GroupSubheaderForm } from '../Subcomponents/GroupSubheaderForm'
import { ContactInfoForm } from '../Subcomponents/ContactInfoForm'
import { ProfileTextForm } from '../Subcomponents/ProfileTextForm'
const componentMap = {
group_subheaders: GroupSubheaderForm,
texts: ProfileTextForm,
contact_infos: ContactInfoForm,
// weitere Komponenten hier
}
export const FlexForm = ({
item,
state,
setState,
}: {
state: FormState
setState: React.Dispatch<React.SetStateAction<any>>
item: Item
}) => {
return (
<div className='tw-space-y-6 tw-mt-6'>
{item.layer?.itemType.profile_template.map((templateItem) => {
const TemplateComponent = componentMap[templateItem.collection]
return TemplateComponent ? (
<TemplateComponent
key={templateItem.id}
state={state}
setState={setState}
{...templateItem.item}
/>
) : (
<div key={templateItem.id}>Component not found</div>
)
})}
</div>
)
}

View File

@ -0,0 +1,27 @@
import { GroupSubHeaderView } from '../Subcomponents/GroupSubHeaderView'
import ContactInfo from '../Subcomponents/ContactInfo'
import { ProfileTextView } from '../Subcomponents/ProfileTextView'
import { Item } from '../../../types'
const componentMap = {
group_subheaders: GroupSubHeaderView,
texts: ProfileTextView,
contact_infos: ContactInfo,
// weitere Komponenten hier
}
export const FlexView = ({ item }: { item: Item }) => {
console.log(item)
return (
<div className='tw-h-full tw-overflow-y-auto fade'>
{item.layer?.itemType.profile_template.map((templateItem) => {
const TemplateComponent = componentMap[templateItem.collection]
return TemplateComponent ? (
<TemplateComponent key={templateItem.id} item={item} {...templateItem.item} />
) : (
<div key={templateItem.id}>Component not found</div>
)
})}
</div>
)
}

View File

@ -1,178 +1,40 @@
import * as React from 'react'
import { useEffect } from 'react'
import { Item, Tag } from '../../../types'
import { TextAreaInput, TextInput } from '../../Input'
import ComboBoxInput from '../../Input/ComboBoxInput'
import { TextAreaInput } from '../../Input'
import { GroupSubheaderForm } from '../Subcomponents/GroupSubheaderForm'
import { ContactInfoForm } from '../Subcomponents/ContactInfoForm'
export type FormState = {
color: string
id: string
group_type: string
status: string
name: string
subname: string
text: string
contact: string
telephone: string
next_appointment: string
image: string
marker_icon: string
offers: Tag[]
needs: Tag[]
relations: Item[]
}
export const OnepagerForm = ({
item,
state,
setState,
}: {
state: {
color: string
id: string
groupType: string
status: string
name: string
subname: string
text: string
contact: string
telephone: string
nextAppointment: string
image: string
markerIcon: string
offers: Tag[]
needs: Tag[]
relations: Item[]
}
state: FormState
setState: React.Dispatch<React.SetStateAction<any>>
item: Item
}) => {
useEffect(() => {
switch (state.groupType) {
case 'wuerdekompass':
setState((prevState) => ({
...prevState,
color: item?.layer?.menuColor || '#1A5FB4',
markerIcon: 'group',
image: '59e6a346-d1ee-4767-9e42-fc720fb535c9',
}))
break
case 'themenkompass':
setState((prevState) => ({
...prevState,
color: '#26A269',
markerIcon: 'group',
image: '59e6a346-d1ee-4767-9e42-fc720fb535c9',
}))
break
case 'liebevoll.jetzt':
setState((prevState) => ({
...prevState,
color: '#E8B620',
markerIcon: 'liebevoll.jetzt',
image: 'e735b96c-507b-471c-8317-386ece0ca51d',
}))
break
default:
break
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.groupType])
const typeMapping = [
{ value: 'wuerdekompass', label: 'Regional-Gruppe' },
{ value: 'themenkompass', label: 'Themen-Gruppe' },
{ value: 'liebevoll.jetzt', label: 'liebevoll.jetzt' },
]
const statusMapping = [
{ value: 'active', label: 'aktiv' },
{ value: 'in_planning', label: 'in Planung' },
{ value: 'paused', label: 'pausiert' },
]
return (
<div className='tw-space-y-6 tw-mt-6'>
<div className='tw-grid tw-grid-cols-1 md:tw-grid-cols-2 tw-gap-6'>
<div>
<label
htmlFor='groupType'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
>
Gruppenart:
</label>
<ComboBoxInput
id='groupType'
options={typeMapping}
value={state.groupType}
onValueChange={(v) =>
setState((prevState) => ({
...prevState,
groupType: v,
}))
}
/>
</div>
<div>
<label
htmlFor='status'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
>
Gruppenstatus:
</label>
<ComboBoxInput
id='status'
options={statusMapping}
value={state.status}
onValueChange={(v) =>
setState((prevState) => ({
...prevState,
status: v,
}))
}
/>
</div>
</div>
<div>
<label
htmlFor='email'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
>
Email-Adresse (Kontakt):
</label>
<TextInput
placeholder='Email'
defaultValue={state.contact}
updateFormValue={(v) =>
setState((prevState) => ({
...prevState,
contact: v,
}))
}
/>
</div>
<div>
<label
htmlFor='telephone'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
>
Telefonnummer (Kontakt):
</label>
<TextInput
placeholder='Telefonnummer'
defaultValue={state.telephone}
updateFormValue={(v) =>
setState((prevState) => ({
...prevState,
telephone: v,
}))
}
/>
</div>
<div>
<label
htmlFor='nextAppointment'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
>
Nächste Termine:
</label>
<TextAreaInput
placeholder='Nächste Termine'
defaultValue={state.nextAppointment}
updateFormValue={(v) =>
setState((prevState) => ({
...prevState,
nextAppointment: v,
}))
}
inputStyle='tw-h-24'
/>
</div>
<GroupSubheaderForm state={state} setState={setState} item={item}></GroupSubheaderForm>
<ContactInfoForm state={state} setState={setState}></ContactInfoForm>
<div>
<label

View File

@ -1,51 +1,16 @@
import { Item } from '../../../types'
import { TextView } from '../../Map'
import ContactInfo from '../Subcomponents/ContactInfo'
import ProfileSubHeader from '../Subcomponents/ProfileSubHeader'
import { useEffect, useState } from 'react'
import { useItems } from '../../Map/hooks/useItems'
export const OnepagerView = ({ item, userType }: { item: Item; userType: string }) => {
const [profileOwner, setProfileOwner] = useState<Item>()
const items = useItems()
useEffect(() => {
setProfileOwner(
items.find(
(i) => i.user_created?.id === item.user_created?.id && i.layer?.itemType.name === userType,
),
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item, items])
const typeMapping = {
wuerdekompass: 'Regional-Gruppe',
themenkompass: 'Themenkompass-Gruppe',
'liebevoll.jetzt': 'liebevoll.jetzt',
}
const groupType = item.group_type ? item.group_type : 'default'
const groupTypeText = typeMapping[groupType]
import { GroupSubHeaderView } from '../Subcomponents/GroupSubHeaderView'
export const OnepagerView = ({ item }: { item: Item }) => {
return (
<div className='tw-h-full tw-overflow-y-auto fade'>
<div className='tw-px-6'>
<ProfileSubHeader
type={groupTypeText}
status={item.status}
url={`https://www.wuerdekompass.org/aktivitaeten/gruppensuche/#/gruppe/${item.slug}`}
title={item.name}
/>
</div>
{item.user_created.first_name && (
<ContactInfo
link={`/item/${profileOwner?.id}`}
name={profileOwner?.name ? profileOwner.name : item.user_created.first_name}
avatar={profileOwner?.image ? profileOwner.image : item.user_created.avatar}
email={item.contact}
telephone={item.telephone}
/>
)}
<GroupSubHeaderView
item={item}
share_base_url={`https://www.wuerdekompass.org/aktivitaeten/gruppensuche/#/gruppe/${item.slug}`}
/>
{item.user_created.first_name && <ContactInfo item={item} />}
{/* Description Section */}
<div className='tw-my-10 tw-mt-2 tw-px-6 tw-text-sm '>
<TextView rawText={item.text || 'Keine Beschreibung vorhanden'} />

View File

@ -7,12 +7,11 @@ import { useAddFilterTag } from '../../Map/hooks/useFilter'
import { Item, Tag } from '../../../types'
import { Link, useNavigate } from 'react-router-dom'
import { useItems } from '../../Map/hooks/useItems'
import { useAssetApi } from '../../AppShell/hooks/useAssets'
import { useAppState } from '../../AppShell/hooks/useAppState'
import { timeAgo } from '../../../Utils/TimeAgo'
export const TabsView = ({
attestations,
userType,
item,
offers,
needs,
@ -23,7 +22,6 @@ export const TabsView = ({
unlinkItem,
}: {
attestations: Array<any>
userType: string
item: Item
offers: Array<Tag>
needs: Array<Tag>
@ -40,9 +38,11 @@ export const TabsView = ({
const [addItemPopupType] = useState<string>('')
const items = useItems()
const assetsApi = useAssetApi()
const appState = useAppState()
const getUserProfile = (id: string) => {
return items.find((i) => i.user_created.id === id && i.layer?.itemType.name === userType)
return items.find(
(i) => i.user_created.id === id && i.layer?.itemType.name === appState.userType,
)
}
useEffect(() => {
@ -146,7 +146,10 @@ export const TabsView = ({
<div className='tw-avatar'>
<div className='tw-mask tw-rounded-full h-8 w-8 tw-mr-2'>
<img
src={assetsApi.url + getUserProfile(a.user_created.id)?.image}
src={
appState.assetsApi.url +
getUserProfile(a.user_created.id)?.image
}
alt='Avatar Tailwind CSS Component'
/>
</div>

View File

@ -175,7 +175,7 @@ export const onUpdateItem = async (
...(state.end && { end: state.end }),
...(state.start && { start: state.start }),
...(state.markerIcon && { markerIcon: state.markerIcon }),
next_appointment: state.nextAppointment,
next_appointment: state.next_appointment,
...(state.image.length > 10 && { image: state.image }),
...(state.offers.length > 0 && { offers: offerUpdates }),
...(state.needs.length > 0 && { needs: needsUpdates }),

View File

@ -1,7 +1,7 @@
import * as React from 'react'
import { MapOverlayPage } from './MapOverlayPage'
import { useItems } from '../Map/hooks/useItems'
import { useAssetApi } from '../AppShell/hooks/useAssets'
import { useAppState } from '../AppShell/hooks/useAppState'
import { EmojiPicker } from './EmojiPicker'
import { useNavigate } from 'react-router-dom'
import { useRef, useState, useEffect } from 'react'
@ -10,7 +10,7 @@ import { toast } from 'react-toastify'
export const AttestationForm = ({ api }: { api?: ItemsApi<any> }) => {
const items = useItems()
const assetsApi = useAssetApi()
const appState = useAppState()
const [users, setUsers] = useState<Array<Item>>()
const navigate = useNavigate()
@ -88,7 +88,10 @@ export const AttestationForm = ({ api }: { api?: ItemsApi<any> }) => {
{u.image ? (
<div className='tw-avatar'>
<div className='tw-mask tw-mask-circle tw-w-8 tw-h-8'>
<img src={assetsApi.url + u.image + '?width=40&heigth=40'} alt='Avatar' />
<img
src={appState.assetsApi.url + u.image + '?width=40&heigth=40'}
alt='Avatar'
/>
</div>
</div>
) : (

View File

@ -1,13 +1,13 @@
import { useState } from 'react'
import { MapOverlayPage } from './MapOverlayPage'
import { useItems } from '../Map/hooks/useItems'
import { useAssetApi } from '../AppShell/hooks/useAssets'
import { useAppState } from '../AppShell/hooks/useAppState'
import { Link } from 'react-router-dom'
export const SelectUser = ({ userType }: { userType: string }) => {
export const SelectUser = () => {
const appState = useAppState()
const items = useItems()
const users = items.filter((i) => i.layer?.itemType.name === userType)
const assetsApi = useAssetApi()
const users = items.filter((i) => i.layer?.itemType.name === appState.userType)
const [selectedUsers, setSelectedUsers] = useState<Array<string>>([])
@ -35,7 +35,7 @@ export const SelectUser = ({ userType }: { userType: string }) => {
<div className='tw-avatar'>
<div className='tw-mask tw-mask-circle tw-w-8 tw-h-8'>
<img
src={assetsApi.url + u.image + '?width=40&heigth=40'}
src={appState.assetsApi.url + u.image + '?width=40&heigth=40'}
alt='Avatar'
/>
</div>