Merge branch 'main' into fix-sourcemap

This commit is contained in:
Max 2025-06-09 11:55:33 +02:00 committed by GitHub
commit e4e7965e6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
146 changed files with 3922 additions and 3000 deletions

View File

@ -70,3 +70,7 @@ Tags, colors and clusters help to retain the overview.
<a href="https://opencollective.com/utopia-project">
<img width="300" src="https://opencollective.com/utopia-project/donate/button@2x.png?color=blue" />
</a>
---
This project is tested with BrowserStack

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M160 32V64H288V32C288 14.33 302.3 0 320 0C337.7 0 352 14.33 352 32V64H400C426.5 64 448 85.49 448 112V160H0V112C0 85.49 21.49 64 48 64H96V32C96 14.33 110.3 0 128 0C145.7 0 160 14.33 160 32zM0 192H448V464C448 490.5 426.5 512 400 512H48C21.49 512 0 490.5 0 464V192zM64 304C64 312.8 71.16 320 80 320H112C120.8 320 128 312.8 128 304V272C128 263.2 120.8 256 112 256H80C71.16 256 64 263.2 64 272V304zM192 304C192 312.8 199.2 320 208 320H240C248.8 320 256 312.8 256 304V272C256 263.2 248.8 256 240 256H208C199.2 256 192 263.2 192 272V304zM336 256C327.2 256 320 263.2 320 272V304C320 312.8 327.2 320 336 320H368C376.8 320 384 312.8 384 304V272C384 263.2 376.8 256 368 256H336zM64 432C64 440.8 71.16 448 80 448H112C120.8 448 128 440.8 128 432V400C128 391.2 120.8 384 112 384H80C71.16 384 64 391.2 64 400V432zM208 384C199.2 384 192 391.2 192 400V432C192 440.8 199.2 448 208 448H240C248.8 448 256 440.8 256 432V400C256 391.2 248.8 384 240 384H208zM320 432C320 440.8 327.2 448 336 448H368C376.8 448 384 440.8 384 432V400C384 391.2 376.8 384 368 384H336C327.2 384 320 391.2 320 400V432z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg fill="currentColor" width="13" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256z"/></svg>

After

Width:  |  Height:  |  Size: 220 B

View File

@ -40,7 +40,11 @@ function App() {
<UtopiaMap center={[50.6, 15.5]} zoom={5} height='100dvh' width="100dvw">
<Layer
name='events'
markerIcon='calendar'
markerIcon={
{image: "calendar.svg",
size: 13
}
}
markerShape='square'
markerDefaultColor='#700'
data={events}
@ -51,7 +55,9 @@ function App() {
/>
<Layer
name='places'
markerIcon='point'
markerIcon={
{image: "point.svg"}
}
markerShape='circle'
markerDefaultColor='#007'
data={places}

32
examples/README.md Normal file
View File

@ -0,0 +1,32 @@
# Examples
Here is a collection of executable examples. Building on each other, they show the features available in the Utipia-ui library.
You can run them and try them out locally in the browser.
## Running the examples
These examples depend on the `/dist` of the root project. You have to run `npm run build` in the root project before you can run the examples:
Using the example [3 - Tags](./3-tags):
```sh
# in root directory install and build the library
npm install
npm run build
# change to specific example directory
cd ./examples/3-tags
# install and run the example code
npm install && npm run dev
# call up the running example in the browser at http://localhost:5173/
## Roadmap
- [x] Basic Map
- [x] Static Layers
- [x] Tags Custom Views & Forms
- [ ] APIs Integration
- [ ] Permissions
- [ ] Custom Views & Forms
- [ ] AppShell

1336
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "utopia-ui",
"version": "3.0.78",
"version": "3.0.96",
"description": "Reuseable React Components to build mapping apps for real life communities and networks",
"repository": "https://github.com/utopia-os/utopia-ui",
"homepage": "https://utopia-os.org/",
@ -12,6 +12,11 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs"
},
"./Profile": {
"types": "./dist/Profile.d.ts",
"import": "./dist/Profile.esm.js",
"require": "./dist/Profile.cjs.js"
}
},
"type": "module",
@ -24,6 +29,7 @@
"test:component": "cypress run --component --browser electron",
"test:unit": "npm run test:unit:dev -- run --coverage",
"test:unit:dev": "vitest",
"test:unit:update": "npm run test:unit:dev -- run --coverage -u",
"docs:generate": "typedoc --includeVersion --navigation.includeCategories true --plugin typedoc-plugin-missing-exports --plugin typedoc-plugin-coverage src/index.tsx",
"update": "npx npm-check-updates"
},
@ -38,6 +44,7 @@
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-typescript": "^12.1.2",
"@tailwindcss/postcss": "^4.0.14",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/geojson": "^7946.0.14",
@ -49,9 +56,8 @@
"@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "^3.0.5",
"autoprefixer": "^10.4.14",
"cypress": "^14.0.3",
"daisyui": "^4.6.1",
"daisyui": "^5.0.6",
"eslint": "^8.24.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
@ -76,7 +82,7 @@
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-svg": "^2.0.0",
"tailwindcss": "^3.3.1",
"tailwindcss": "^4.0.14",
"typedoc": "^0.27.6",
"typedoc-plugin-coverage": "^3.4.1",
"typedoc-plugin-missing-exports": "^3.1.0",

View File

@ -1,7 +1,6 @@
// eslint-disable-next-line import/no-commonjs
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
'@tailwindcss/postcss': {},
},
}

View File

@ -17,17 +17,22 @@ const aliasConfig = alias({
export default [
{
input: 'src/index.tsx',
input: {
index: 'src/index.tsx',
Profile: 'src/Components/Profile/index.tsx',
},
output: [
{
file: 'dist/index.esm.js',
dir: 'dist/',
format: 'esm',
sourcemap: true,
entryFileNames: '[name].esm.js',
},
{
file: 'dist/index.cjs',
dir: 'dist/',
format: 'cjs',
sourcemap: true,
entryFileNames: '[name].cjs.js',
},
],
plugins: [
@ -55,11 +60,9 @@ export default [
'react-toastify',
'react-string-replace',
'react-toastify/dist/ReactToastify.css',
'tw-elements',
'react-router-dom',
'react-leaflet-cluster',
'@tanstack/react-query',
'tributejs',
'prop-types',
'leaflet/dist/leaflet.css',
'@heroicons/react/20/solid',
@ -80,8 +83,15 @@ export default [
],
},
{
input: 'dist/types/src/index.d.ts',
output: [{ file: 'dist/index.d.ts', format: 'es' }],
input: {
index: path.resolve(__dirname, 'dist/types/src/index.d.ts'),
Profile: path.resolve(__dirname, 'dist/types/src/Components/Profile/index.d.ts'),
},
output: {
dir: path.resolve(__dirname, 'dist'),
format: 'es',
entryFileNames: '[name].d.ts',
},
plugins: [
aliasConfig,
dts({
@ -90,7 +100,7 @@ export default [
},
}),
],
external: [/\.css$/], //, /\.d\.ts$/
external: [/\.css$/],
watch: false,
},
]

View File

@ -13,17 +13,25 @@ export function AppShell({
appName,
children,
assetsApi,
embedded,
openCollectiveApiKey,
}: {
appName: string
children: React.ReactNode
assetsApi: AssetsApi
embedded?: boolean
openCollectiveApiKey?: string
}) {
return (
<ContextWrapper>
<div className='tw-flex tw-flex-col tw-h-full'>
<SetAppState assetsApi={assetsApi} />
<div className='tw:flex tw:flex-col tw:h-full'>
<SetAppState
assetsApi={assetsApi}
embedded={embedded}
openCollectiveApiKey={openCollectiveApiKey}
/>
<NavBar appName={appName}></NavBar>
<div id='app-content' className='tw-flex'>
<div id='app-content' className='tw:flex'>
{children}
</div>
</div>

View File

@ -12,7 +12,7 @@ export function Content({ children }: ContentProps) {
return (
<div
className={`${appState.sideBarOpen && !appState.sideBarSlim ? 'tw-ml-48' : appState.sideBarOpen && appState.sideBarSlim ? 'tw-ml-14' : ''} tw-flex tw-flex-col tw-w-full tw-h-full tw-bg-base-200 tw-relative tw-transition-all tw-duration-300`}
className={`${appState.sideBarOpen && !appState.sideBarSlim ? 'tw:ml-48' : appState.sideBarOpen && appState.sideBarSlim ? 'tw:ml-14' : ''} tw:w-full tw:h-full tw:bg-base-200 tw:relative tw:transition-all tw:duration-300`}
>
{children}
</div>

View File

@ -10,6 +10,7 @@ import { ItemsProvider } from '#components/Map/hooks/useItems'
import { LayersProvider } from '#components/Map/hooks/useLayers'
import { LeafletRefsProvider } from '#components/Map/hooks/useLeafletRefs'
import { PermissionsProvider } from '#components/Map/hooks/usePermissions'
import { PopupFormProvider } from '#components/Map/hooks/usePopupForm'
import { SelectPositionProvider } from '#components/Map/hooks/useSelectPosition'
import { TagsProvider } from '#components/Map/hooks/useTags'
@ -22,7 +23,7 @@ const ContextCheckContext = createContext(false)
const CloseButton = ({ closeToast }: CloseButtonProps) => (
<button
className='tw-btn tw-btn-sm tw-btn-circle tw-btn-ghost tw-absolute tw-right-2 tw-top-2 focus:tw-outline-none'
className='tw:btn tw:btn-sm tw:btn-circle tw:btn-ghost tw:absolute tw:right-2 tw:top-2 tw:focus:outline-hidden'
onClick={closeToast}
>
@ -66,22 +67,24 @@ export const Wrappers = ({ children }) => {
<QueryClientProvider client={queryClient}>
<AppStateProvider>
<ClusterRefProvider>
<QuestsProvider initialOpen={true}>
<ToastContainer
position='top-right'
autoClose={2000}
hideProgressBar
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme='light'
closeButton={CloseButton}
/>
{children}
</QuestsProvider>
<PopupFormProvider>
<QuestsProvider initialOpen={true}>
<ToastContainer
position='top-right'
autoClose={2000}
hideProgressBar
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme='light'
closeButton={CloseButton}
/>
{children}
</QuestsProvider>
</PopupFormProvider>
</ClusterRefProvider>
</AppStateProvider>
</QueryClientProvider>

View File

@ -1,23 +1,14 @@
import Bars3Icon from '@heroicons/react/16/solid/Bars3Icon'
import EllipsisVerticalIcon from '@heroicons/react/16/solid/EllipsisVerticalIcon'
import QuestionMarkIcon from '@heroicons/react/24/outline/QuestionMarkCircleIcon'
import { useEffect, useRef, useState } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { toast } from 'react-toastify'
import { Link } from 'react-router-dom'
import { useAuth } from '#components/Auth/useAuth'
import { useItems } from '#components/Map/hooks/useItems'
import { ThemeControl } from '#components/Templates/ThemeControl'
import { useAppState, useSetAppState } from './hooks/useAppState'
import type { Item } from '#types/Item'
import { UserControl } from './UserControl'
export default function NavBar({ appName }: { appName: string }) {
const { isAuthenticated, user, logout } = useAuth()
const [userProfile, setUserProfile] = useState<Item>({} as Item)
const items = useItems()
const appState = useAppState()
const setAppState = useSetAppState()
@ -25,151 +16,49 @@ export default function NavBar({ appName }: { appName: string }) {
setAppState({ sideBarOpen: !appState.sideBarOpen })
}
useEffect(() => {
const profile =
user && items.find((i) => i.user_created?.id === user.id && i.layer?.userProfileLayer)
profile
? setUserProfile(profile)
: setUserProfile({ id: crypto.randomUUID(), name: user?.first_name ?? '', text: '' })
}, [user, items])
const nameRef = useRef<HTMLHeadingElement>(null)
const [nameWidth, setNameWidth] = useState<number>(0)
const location = useLocation()
const [showNav, setShowNav] = useState<boolean>(false)
useEffect(() => {
showNav && nameRef.current && setNameWidth(nameRef.current.scrollWidth)
}, [nameRef, appName, showNav])
!appState.embedded && nameRef.current && setNameWidth(nameRef.current.scrollWidth)
}, [nameRef, appName, appState.embedded])
useEffect(() => {
const params = new URLSearchParams(location.search)
const embedded = params.get('embedded')
embedded !== 'true' && setShowNav(true)
}, [location])
const onLogout = async () => {
await toast.promise(logout(), {
success: {
render() {
return 'Bye bye'
},
// other options
icon: '👋',
},
error: {
render({ data }) {
return JSON.stringify(data)
},
},
pending: 'logging out ..',
})
}
if (showNav) {
if (!appState.embedded) {
return (
<>
<div className='tw-navbar tw-bg-base-100 tw-z-[9998] tw-shadow-xl tw-relative'>
<div className='tw:navbar tw:bg-base-100 tw:z-9998 tw:shadow-xl tw:relative tw:p-0'>
<button
className='tw-btn tw-btn-square tw-btn-ghost'
className='tw:btn tw:btn-square tw:btn-ghost tw:ml-3'
aria-controls='#sidenav'
aria-haspopup='true'
onClick={() => toggleSidebar()}
>
<Bars3Icon className='tw-inline-block tw-w-5 tw-h-5' />
<Bars3Icon className='tw:inline-block tw:w-5 tw:h-5' />
</button>
<div className='tw-flex-1 tw-mr-2'>
<div className='tw:flex-1 tw:mr-2'>
<div
className={'tw-flex-1 tw-truncate tw-grid tw-grid-flow-col'}
className={'tw:flex-1 tw:truncate tw:grid tw:grid-flow-col'}
style={{ maxWidth: nameWidth + 60 }}
>
<Link
className='tw-btn tw-btn-ghost tw-px-2 tw-normal-case tw-text-xl tw-flex-1 tw-truncate'
className='tw:btn tw:btn-ghost tw:px-2 tw:normal-case tw:text-xl tw:flex-1 tw:truncate'
to={'/'}
>
<h1 ref={nameRef} className='tw-truncate'>
<h1 ref={nameRef} className='tw:truncate'>
{appName}
</h1>
</Link>
<button
className='tw-btn tw-px-2 tw-btn-ghost'
className='tw:btn tw:px-2 tw:btn-ghost'
onClick={() => window.my_modal_3.showModal()}
>
<QuestionMarkIcon className='tw-h-5 tw-w-5' />
<QuestionMarkIcon className='tw:h-5 tw:w-5' />
</button>
</div>
</div>
{isAuthenticated ? (
<div className='tw-flex-none'>
<Link
to={`${userProfile.id && '/item/' + userProfile.id}`}
className='tw-flex tw-items-center'
>
{userProfile.image && (
<div className='tw-avatar'>
<div className='tw-w-10 tw-rounded-full'>
<img src={appState.assetsApi.url + userProfile.image} />
</div>
</div>
)}
<div className='tw-ml-2 tw-mr-2'>{userProfile.name || user?.first_name}</div>
</Link>
<div className='tw-dropdown tw-dropdown-end'>
<label tabIndex={0} className='tw-btn tw-btn-ghost tw-btn-square'>
<EllipsisVerticalIcon className='tw-h-5 tw-w-5' />
</label>
<ul
tabIndex={0}
className='tw-menu tw-menu-compact tw-dropdown-content tw-mt-3 tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-w-52 !tw-z-[10000]'
>
<li>
<Link to={`${userProfile.id && '/edit-item/' + userProfile.id}`}>Profile</Link>
</li>
<li>
<Link to={'/user-settings'}>Settings</Link>
</li>
<li>
<a
onClick={() => {
void onLogout()
}}
>
Logout
</a>
</li>
</ul>
</div>
</div>
) : (
<div>
<div className='tw-hidden md:tw-flex'>
<Link to={'/login'}>
<div className='tw-btn tw-btn-ghost tw-mr-2'>Login</div>
</Link>
<Link to={'/signup'}>
<div className='tw-btn tw-btn-ghost tw-mr-2'>Sign Up</div>
</Link>
</div>
<div className='tw-dropdown tw-dropdown-end'>
<label tabIndex={1} className='tw-btn tw-btn-ghost md:tw-hidden'>
<EllipsisVerticalIcon className='tw-h-5 tw-w-5' />
</label>
<ul
tabIndex={1}
className='tw-menu tw-dropdown-content tw-mt-3 tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-w-52 !tw-z-[10000]'
>
<li>
<Link to={'/login'}>Login</Link>
</li>
<li>
<Link to={'/signup'}>Sign Up</Link>
</li>
</ul>
</div>
</div>
)}
{appState.showThemeControl && <ThemeControl />}
<UserControl />
</div>
</>
)

View File

@ -4,13 +4,28 @@ import { useSetAppState } from './hooks/useAppState'
import type { AssetsApi } from '#types/AssetsApi'
export const SetAppState = ({ assetsApi }: { assetsApi: AssetsApi }) => {
export const SetAppState = ({
assetsApi,
embedded,
openCollectiveApiKey,
}: {
assetsApi: AssetsApi
embedded?: boolean
openCollectiveApiKey?: string
}) => {
const setAppState = useSetAppState()
useEffect(() => {
setAppState({ assetsApi })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assetsApi])
}, [assetsApi, setAppState])
useEffect(() => {
setAppState({ embedded })
}, [embedded, setAppState])
useEffect(() => {
setAppState({ openCollectiveApiKey })
}, [openCollectiveApiKey, setAppState])
return <></>
}

View File

@ -1,5 +1,4 @@
import ChevronRightIcon from '@heroicons/react/24/outline/ChevronRightIcon'
import { useState, useEffect } from 'react'
import { NavLink, useLocation } from 'react-router-dom'
import { useAppState, useSetAppState } from './hooks/useAppState'
@ -19,14 +18,6 @@ export interface Route {
export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoutes?: Route[] }) {
const location = useLocation()
const [embedded, setEmbedded] = useState<boolean>(true)
useEffect(() => {
const params = new URLSearchParams(location.search)
const embedded = params.get('embedded')
embedded !== 'true' && setEmbedded(false)
}, [location])
const params = new URLSearchParams(window.location.search)
const appState = useAppState()
@ -43,19 +34,16 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
return (
<nav
id='sidenav'
className={`${appState.sideBarOpen ? 'tw-translate-x-0' : '-tw-translate-x-full'}
${appState.sideBarSlim ? 'tw-w-14' : 'tw-w-48'}
${embedded ? 'tw-mt-0 tw-h-[100dvh]' : 'tw-mt-16 tw-h-[calc(100dvh-64px)]'}
tw-fixed tw-left-0 tw-transition-all tw-duration-300 tw-top-0 tw-z-[10035]
tw-overflow-hidden tw-shadow-xl dark:tw-bg-zinc-800`}
className={`${appState.sideBarOpen ? 'tw:translate-x-0' : 'tw:-translate-x-full'}
${appState.sideBarSlim ? 'tw:w-14' : 'tw:w-48'}
${appState.embedded ? 'tw:mt-5.5 tw:h-[calc(100dvh-22px)]' : 'tw:mt-16 tw:h-[calc(100dvh-64px)]'}
tw:fixed tw:left-0 tw:transition-all tw:duration-300 tw:top-0 tw:z-10035
tw:overflow-hidden tw:shadow-xl tw:dark:bg-zinc-800`}
>
<div
className={`tw-flex tw-flex-col ${embedded ? 'tw-h-full' : 'tw-h-[calc(100dvh-64px)]'}`}
className={`tw:flex tw:flex-col ${appState.embedded ? 'tw:h-full' : 'tw:h-[calc(100dvh-64px)]'}`}
>
<ul
className='tw-menu tw-w-full tw-bg-base-100 tw-text-base-content tw-p-0'
data-te-sidenav-menu-ref
>
<ul className='tw:menu tw:w-full tw:bg-base-100 tw:text-base-content tw:p-0'>
{routes.map((route, k) => {
return (
<li className='' key={k}>
@ -68,7 +56,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
to={`${route.path}${params && '?' + params.toString()}`}
className={({ isActive }) =>
`${isActive ? 'tw-font-semibold tw-bg-base-200 !tw-rounded-none' : 'tw-font-normal !tw-rounded-none'}`
`${isActive ? 'tw:font-semibold tw:bg-base-200 tw:rounded-none!' : 'tw:font-normal tw:rounded-none!'}`
}
onClick={() => {
if (screen.width < 640 && !appState.sideBarSlim) toggleSidebarOpen()
@ -76,7 +64,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
>
{route.icon}
<span
className={`${appState.sideBarSlim ? 'tw-hidden' : ''}`}
className={`${appState.sideBarSlim ? 'tw:hidden' : ''}`}
data-te-sidenav-slim='false'
>
{route.name}
@ -84,7 +72,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
{(location.pathname.includes(route.path) && route.path.length > 1) ||
location.pathname === route.path ? (
<span
className='tw-absolute tw-inset-y-0 tw-left-0 tw-w-1 tw-rounded-tr-md tw-rounded-br-md tw-bg-primary '
className='tw:absolute tw:inset-y-0 tw:left-0 tw:w-1 tw:rounded-tr-md tw:rounded-br-md tw:bg-primary '
aria-hidden='true'
></span>
) : null}
@ -97,12 +85,12 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
<div
id='slim-toggler'
className='tw-w-full tw-bg-base-100 tw-flex-1 tw-grid tw-place-items-end'
className='tw:w-full tw:bg-base-100 tw:flex-1 tw:grid tw:place-items-end'
aria-haspopup='true'
>
<div className='tw-w-full'>
<div className='tw:w-full'>
<ul
className='tw-menu tw-w-full tw-bg-base-100 tw-text-base-content tw-p-0 tw-mb-0'
className='tw:menu tw:w-full tw:bg-base-100 tw:text-base-content tw:p-0 tw:mb-0'
data-te-sidenav-menu-ref
>
{bottomRoutes?.map((route, k) => {
@ -116,7 +104,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
target={route.blank ? '_blank' : '_self'}
to={route.path}
className={({ isActive }) =>
`${isActive ? 'tw-font-semibold tw-bg-base-200 !tw-rounded-none' : 'tw-font-normal !tw-rounded-none'}`
`${isActive ? 'tw:font-semibold tw:bg-base-200 tw:rounded-none!' : 'tw:font-normal tw:rounded-none!'}`
}
onClick={() => {
if (screen.width < 640 && !appState.sideBarSlim) toggleSidebarOpen()
@ -124,7 +112,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
>
{route.icon}
<span
className={`${appState.sideBarSlim ? 'tw-hidden' : ''}`}
className={`${appState.sideBarSlim ? 'tw:hidden' : ''}`}
data-te-sidenav-slim='false'
>
{route.name}
@ -132,7 +120,7 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
{(location.pathname.includes(route.path) && route.path.length > 1) ||
location.pathname === route.path ? (
<span
className='tw-absolute tw-inset-y-0 tw-left-0 tw-w-1 tw-rounded-tr-md tw-rounded-br-md tw-bg-primary '
className='tw:absolute tw:inset-y-0 tw:left-0 tw:w-1 tw:rounded-tr-md tw:rounded-br-md tw:bg-primary '
aria-hidden='true'
></span>
) : null}
@ -145,8 +133,8 @@ export function SideBar({ routes, bottomRoutes }: { routes: Route[]; bottomRoute
<ChevronRightIcon
className={
'tw-w-5 tw-h-5 tw-mb-4 tw-mr-4 tw-cursor-pointer tw-float-right tw-delay-400 tw-duration-500 tw-transition-all ' +
(!appState.sideBarSlim ? 'tw-rotate-180' : '')
'tw:w-5 tw:h-5 tw:mb-4 tw:mr-5 tw:mt-2 tw:cursor-pointer tw:float-right tw:delay-400 tw:duration-500 tw:transition-all ' +
(!appState.sideBarSlim ? 'tw:rotate-180' : '')
}
onClick={() => toggleSidebarSlim()}
/>

View File

@ -0,0 +1,119 @@
import EllipsisVerticalIcon from '@heroicons/react/16/solid/EllipsisVerticalIcon'
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { toast } from 'react-toastify'
import { useAuth } from '#components/Auth/useAuth'
import { useItems } from '#components/Map/hooks/useItems'
import { useAppState } from './hooks/useAppState'
import type { Item } from '#types/Item'
export const UserControl = () => {
const { isAuthenticated, user, logout } = useAuth()
const appState = useAppState()
const [userProfile, setUserProfile] = useState<Item>({} as Item)
const items = useItems()
useEffect(() => {
const profile =
user && items.find((i) => i.user_created?.id === user.id && i.layer?.userProfileLayer)
profile
? setUserProfile(profile)
: setUserProfile({ id: crypto.randomUUID(), name: user?.first_name ?? '', text: '' })
}, [user, items])
const onLogout = async () => {
await toast.promise(logout(), {
success: {
render() {
return 'Bye bye'
},
// other options
icon: '👋',
},
error: {
render({ data }) {
return JSON.stringify(data)
},
},
pending: 'logging out ..',
})
}
return (
<>
{isAuthenticated ? (
<div className='tw:flex tw:mr-2'>
<Link
to={`${userProfile.id && '/item/' + userProfile.id}`}
className='tw:flex tw:items-center'
>
{userProfile.image && (
<div className='tw:avatar'>
<div className='tw:w-10 tw:rounded-full'>
<img src={appState.assetsApi.url + userProfile.image} />
</div>
</div>
)}
<div className='tw:ml-2 tw:mr-2'>{userProfile.name || user?.first_name}</div>
</Link>
<div className='tw:dropdown tw:dropdown-end'>
<label tabIndex={0} className='tw:btn tw:btn-ghost tw:btn-square'>
<EllipsisVerticalIcon className='tw:h-5 tw:w-5' />
</label>
<ul
tabIndex={0}
className='tw:menu tw:menu-compact tw:dropdown-content tw:mt-4 tw:p-2 tw:shadow tw:bg-base-100 tw:rounded-box tw:w-52 tw:z-10000!'
>
<li>
<Link to={`${userProfile.id && '/edit-item/' + userProfile.id}`}>Profile</Link>
</li>
<li>
<Link to={'/user-settings'}>Settings</Link>
</li>
<li>
<a
onClick={() => {
void onLogout()
}}
>
Logout
</a>
</li>
</ul>
</div>
</div>
) : (
<div className='tw:mr-2 tw:flex tw:items-center'>
<div className='tw:hidden tw:md:flex'>
<Link to={'/login'}>
<div className='tw:self-center tw:btn tw:btn-ghost tw:mr-2'>Login</div>
</Link>
<Link to={'/signup'}>
<div className='tw:btn tw:btn-ghost tw:mr-2'>Sign Up</div>
</Link>
</div>
<div className='tw:dropdown tw:dropdown-end'>
<label tabIndex={1} className='tw:btn tw:btn-ghost tw:md:hidden'>
<EllipsisVerticalIcon className='tw:h-5 tw:w-5' />
</label>
<ul
tabIndex={1}
className='tw:menu tw:dropdown-content tw:mt-4 tw:p-2 tw:shadow tw:bg-base-100 tw:rounded-box tw:w-52 tw:z-10000!'
>
<li>
<Link to={'/login'}>Login</Link>
</li>
<li>
<Link to={'/signup'}>Sign Up</Link>
</li>
</ul>
</div>
</div>
)}
</>
)
}

View File

@ -8,6 +8,9 @@ interface AppState {
assetsApi: AssetsApi
sideBarOpen: boolean
sideBarSlim: boolean
showThemeControl: boolean
embedded: boolean
openCollectiveApiKey: string
}
type UseAppManagerResult = ReturnType<typeof useAppManager>
@ -16,6 +19,9 @@ const initialAppState: AppState = {
assetsApi: {} as AssetsApi,
sideBarOpen: false,
sideBarSlim: false,
showThemeControl: false,
embedded: false,
openCollectiveApiKey: '',
}
const AppContext = createContext<UseAppManagerResult>({

View File

@ -0,0 +1,12 @@
import { useEffect } from 'react'
export const useTheme = (defaultTheme = 'default') => {
useEffect(() => {
const savedTheme = localStorage.getItem('theme')
const initialTheme = savedTheme ? (JSON.parse(savedTheme) as string) : defaultTheme
if (initialTheme !== 'default') {
document.documentElement.setAttribute('data-theme', defaultTheme)
localStorage.setItem('theme', JSON.stringify(initialTheme))
}
}, [defaultTheme])
}

View File

@ -53,39 +53,39 @@ export function LoginPage() {
}, [onLogin])
return (
<MapOverlayPage backdrop className='tw-max-w-xs tw-h-fit'>
<h2 className='tw-text-2xl tw-font-semibold tw-mb-2 tw-text-center'>Login</h2>
<MapOverlayPage backdrop className='tw:max-w-xs tw:h-fit'>
<h2 className='tw:text-2xl tw:font-semibold tw:mb-2 tw:text-center'>Login</h2>
<input
type='email'
placeholder='E-Mail'
value={email}
onChange={(e) => setEmail(e.target.value)}
className='tw-input tw-input-bordered tw-w-full tw-max-w-xs'
className='tw:input tw:input-bordered tw:w-full tw:max-w-xs'
/>
<input
type='password'
placeholder='Password'
onChange={(e) => setPassword(e.target.value)}
className='tw-input tw-input-bordered tw-w-full tw-max-w-xs'
className='tw:input tw:input-bordered tw:w-full tw:max-w-xs'
/>
<div className='tw-text-right tw-text-primary'>
<div className='tw:text-right tw:text-primary'>
<Link to='/reset-password'>
<span className='tw-text-sm tw-inline-block hover:tw-text-primary hover:tw-underline hover:tw-cursor-pointer tw-transition tw-duration-200'>
<span className='tw:text-sm tw:inline-block tw:hover:text-primary tw:hover:underline tw:hover:cursor-pointer tw:transition tw:duration-200'>
Forgot Password?
</span>
</Link>
</div>
<div className='tw-card-actions'>
<div className='tw:card-actions'>
<button
className={
loading
? 'tw-btn tw-btn-disabled tw-btn-block tw-btn-primary'
: 'tw-btn tw-btn-primary tw-btn-block'
? 'tw:btn tw:btn-disabled tw:btn-block tw:btn-primary'
: 'tw:btn tw:btn-primary tw:btn-block'
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={() => onLogin()}
>
{loading ? <span className='tw-loading tw-loading-spinner'></span> : 'Login'}
{loading ? <span className='tw:loading tw:loading-spinner'></span> : 'Login'}
</button>
</div>
</MapOverlayPage>

View File

@ -36,26 +36,26 @@ export function RequestPasswordPage({ resetUrl }: { resetUrl: string }) {
}
return (
<MapOverlayPage backdrop className='tw-max-w-xs tw-h-fit'>
<h2 className='tw-text-2xl tw-font-semibold tw-mb-2 tw-text-center'>Reset Password</h2>
<MapOverlayPage backdrop className='tw:max-w-xs tw:h-fit'>
<h2 className='tw:text-2xl tw:font-semibold tw:mb-2 tw:text-center'>Reset Password</h2>
<input
type='email'
placeholder='E-Mail'
value={email}
onChange={(e) => setEmail(e.target.value)}
className='tw-input tw-input-bordered tw-w-full tw-max-w-xs'
className='tw:input tw:input-bordered tw:w-full tw:max-w-xs'
/>
<div className='tw-card-actions tw-mt-4'>
<div className='tw:card-actions tw:mt-4'>
<button
className={
loading
? 'tw-btn tw-btn-disabled tw-btn-block tw-btn-primary'
: 'tw-btn tw-btn-primary tw-btn-block'
? 'tw:btn tw:btn-disabled tw:btn-block tw:btn-primary'
: 'tw:btn tw:btn-primary tw:btn-block'
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={() => onReset()}
>
{loading ? <span className='tw-loading tw-loading-spinner'></span> : 'Send'}
{loading ? <span className='tw:loading tw:loading-spinner'></span> : 'Send'}
</button>
</div>
</MapOverlayPage>

View File

@ -36,25 +36,25 @@ export function SetNewPasswordPage() {
}
return (
<MapOverlayPage backdrop className='tw-max-w-xs tw-h-fit'>
<h2 className='tw-text-2xl tw-font-semibold tw-mb-2 tw-text-center'>Set new Password</h2>
<MapOverlayPage backdrop className='tw:max-w-xs tw:h-fit'>
<h2 className='tw:text-2xl tw:font-semibold tw:mb-2 tw:text-center'>Set new Password</h2>
<input
type='password'
placeholder='Password'
onChange={(e) => setPassword(e.target.value)}
className='tw-input tw-input-bordered tw-w-full tw-max-w-xs'
className='tw:input tw:input-bordered tw:w-full tw:max-w-xs'
/>
<div className='tw-card-actions tw-mt-4'>
<div className='tw:card-actions tw:mt-4'>
<button
className={
loading
? 'tw-btn tw-btn-disabled tw-btn-block tw-btn-primary'
: 'tw-btn tw-btn-primary tw-btn-block'
? 'tw:btn tw:btn-disabled tw:btn-block tw:btn-primary'
: 'tw:btn tw:btn-primary tw:btn-block'
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={() => onReset()}
>
{loading ? <span className='tw-loading tw-loading-spinner'></span> : 'Set'}
{loading ? <span className='tw:loading tw:loading-spinner'></span> : 'Set'}
</button>
</div>
</MapOverlayPage>

View File

@ -55,39 +55,39 @@ export function SignupPage() {
}, [onRegister])
return (
<MapOverlayPage backdrop className='tw-max-w-xs tw-h-fit'>
<h2 className='tw-text-2xl tw-font-semibold tw-mb-2 tw-text-center'>Sign Up</h2>
<MapOverlayPage backdrop className='tw:max-w-xs tw:h-fit'>
<h2 className='tw:text-2xl tw:font-semibold tw:mb-2 tw:text-center'>Sign Up</h2>
<input
type='text'
placeholder='Name'
value={userName}
onChange={(e) => setUserName(e.target.value)}
className='tw-input tw-input-bordered tw-w-full tw-max-w-xs'
className='tw:input tw:input-bordered tw:w-full tw:max-w-xs'
/>
<input
type='email'
placeholder='E-Mail'
value={email}
onChange={(e) => setEmail(e.target.value)}
className='tw-input tw-input-bordered tw-w-full tw-max-w-xs'
className='tw:input tw:input-bordered tw:w-full tw:max-w-xs'
/>
<input
type='password'
placeholder='Password'
onChange={(e) => setPassword(e.target.value)}
className='tw-input tw-input-bordered tw-w-full tw-max-w-xs'
className='tw:input tw:input-bordered tw:w-full tw:max-w-xs'
/>
<div className='tw-card-actions tw-mt-4'>
<div className='tw:card-actions tw:mt-4'>
<button
className={
loading
? 'tw-btn tw-btn-disabled tw-btn-block tw-btn-primary'
: 'tw-btn tw-btn-primary tw-btn-block'
? 'tw:btn tw:btn-disabled tw:btn-block tw:btn-primary'
: 'tw:btn tw:btn-primary tw:btn-block'
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={() => onRegister()}
>
{loading ? <span className='tw-loading tw-loading-spinner'></span> : 'Sign Up'}
{loading ? <span className='tw:loading tw:loading-spinner'></span> : 'Sign Up'}
</button>
</div>
</MapOverlayPage>

View File

@ -20,14 +20,14 @@ export function Modal({
return (
<>
{/* You can open the modal using ID.showModal() method */}
<dialog id='my_modal_3' className='tw-modal tw-transition-all tw-duration-300'>
<form method='dialog' className='tw-modal-box tw-transition-none'>
<button className='tw-btn tw-btn-sm tw-btn-circle tw-btn-ghost tw-absolute tw-right-2 tw-top-2 focus:tw-outline-none'>
<dialog id='my_modal_3' className='tw:modal tw:transition-all tw:duration-300'>
<form method='dialog' className='tw:modal-box tw:transition-none'>
<button className='tw:btn tw:btn-sm tw:btn-circle tw:btn-ghost tw:absolute tw:right-2 tw:top-2 tw:focus:outline-hidden'>
</button>
{children}
</form>
<form method='dialog' className='tw-modal-backdrop'>
<form method='dialog' className='tw:modal-backdrop'>
<button>close</button>
</form>
</dialog>

View File

@ -39,56 +39,56 @@ export function Quests() {
return (
<>
{questsOpen ? (
<div className='tw-card tw-w-48 tw-bg-base-100 tw-shadow-xl tw-absolute tw-bottom-4 tw-left-4 tw-z-[2000]'>
<div className='tw-card-body tw-p-4 tw-pt-0'>
<div className='tw-card-actions tw-justify-end'>
<div className='tw:card tw:w-48 tw:bg-base-100 tw:shadow-xl tw:absolute tw:bottom-4 tw:left-4 tw:z-2000'>
<div className='tw:card-body tw:p-4 tw:pt-0'>
<div className='tw:card-actions tw:justify-end'>
<label
className='tw-btn tw-btn-sm tw-btn-circle tw-btn-ghost tw-absolute tw-right-1 tw-top-1'
className='tw:btn tw:btn-sm tw:btn-circle tw:btn-ghost tw:absolute tw:right-1 tw:top-1'
onClick={() => setQuestsOpen(false)}
>
</label>
</div>
<h2 className='tw-card-title tw-m-auto '>
<h2 className='tw:card-title tw:m-auto '>
Level 1
<QuestionMarkCircleIcon />
</h2>
<ul className='tw-flex-row'>
<ul className='tw:flex-row'>
<li>
<label className='tw-label tw-justify-normal tw-pt-1 tw-pb-0'>
<label className='tw:label tw:justify-normal tw:pt-1 tw:pb-0'>
<input
type='checkbox'
readOnly={true}
className='tw-checkbox tw-checkbox-xs tw-checkbox-success'
className='tw:checkbox tw:checkbox-xs tw:checkbox-success'
checked={isAuthenticated || false}
/>
<span className='tw-text-sm tw-label-text tw-mx-2'>Sign Up</span>
<span className='tw:text-sm tw:label-text tw:mx-2'>Sign Up</span>
</label>
</li>
<li>
<label className='tw-label tw-justify-normal tw-pt-1 tw-pb-0'>
<label className='tw:label tw:justify-normal tw:pt-1 tw:pb-0'>
<input
type='checkbox'
readOnly={true}
className='tw-checkbox tw-checkbox-xs tw-checkbox-success'
className='tw:checkbox tw:checkbox-xs tw:checkbox-success'
checked={!!profile?.text}
/>
<span className='tw-text-sm tw-label-text tw-mx-2'>Fill Profile</span>
<span className='tw:text-sm tw:label-text tw:mx-2'>Fill Profile</span>
</label>
</li>
<li>
<label className='tw-label tw-justify-normal tw-pt-1 tw-pb-0'>
<label className='tw:label tw:justify-normal tw:pt-1 tw:pb-0'>
<input
type='checkbox'
readOnly={true}
className='tw-checkbox tw-checkbox-xs tw-checkbox-success'
className='tw:checkbox tw:checkbox-xs tw:checkbox-success'
checked={!!profile?.image}
/>
<span className='tw-text-sm tw-label-text tw-mx-2'>Upload Avatar</span>
<span className='tw:text-sm tw:label-text tw:mx-2'>Upload Avatar</span>
</label>
</li>
</ul>
{/** <button className='tw-btn tw-btn-xs tw-btn-neutral tw-w-fit tw-self-center tw-mt-1'>Next &gt;</button> */}{' '}
{/** <button className='tw:btn tw:btn-xs tw:btn-neutral tw:w-fit tw:self-center tw:mt-1'>Next &gt;</button> */}{' '}
</div>
</div>
) : (

View File

@ -91,9 +91,10 @@ export const Autocomplete = ({
onChange={(e) => handleChange(e)}
tabIndex='-1'
onKeyDown={handleKeyDown}
className='tw:border-none tw:focus:outline-none tw:focus:ring-0 tw:mt-5'
/>
<ul
className={`tw-absolute tw-z-[4000] ${filteredSuggestions.length > 0 && 'tw-bg-base-100 tw-rounded-xl tw-p-2'}`}
className={`tw:absolute tw:z-4000 ${filteredSuggestions.length > 0 && 'tw:bg-base-100 tw:rounded-xl tw:p-2'}`}
>
{filteredSuggestions.map((suggestion, index) => (
<li key={index} onClick={() => handleSuggestionClick(suggestion)}>

View File

@ -7,16 +7,15 @@ interface ComboBoxProps {
const ComboBoxInput = ({ id, options, value, onValueChange }: ComboBoxProps) => {
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value
onValueChange(value)
onValueChange(e.target.value)
}
return (
<select
id={id}
className='tw-form-select tw-block tw-w-full tw-py-2 tw-px-4 tw-border tw-border-gray-300 rounded-md tw-shadow-sm tw-text-sm focus:tw-outline-none focus:tw-ring-indigo-500 focus:tw-border-indigo-500 sm:tw-text-sm'
className='tw:form-select tw:block tw:w-full tw:py-2 tw:px-4 tw:border tw:border-gray-300 rounded-md tw:shadow-sm tw:text-sm tw:focus:outline-hidden tw:focus:ring-indigo-500 tw:focus:border-indigo-500 tw:sm:text-sm'
onChange={handleChange}
defaultValue={value}
value={value} // ← hier controlled statt defaultValue
>
{options.map((o) => (
<option value={o} key={o}>

View File

@ -0,0 +1,65 @@
import { useEffect, useRef, useState } from 'react'
interface TextAreaProps {
labelTitle?: string
labelStyle?: string
containerStyle?: string
dataField?: string
inputStyle?: string
defaultValue: string
placeholder?: string
required?: boolean
size?: string
updateFormValue?: (value: string) => void
}
/**
* @category Input
*/
export function RichTextEditor({
labelTitle,
dataField,
labelStyle,
containerStyle,
inputStyle,
defaultValue,
placeholder,
required = true,
updateFormValue,
}: TextAreaProps) {
const ref = useRef<HTMLTextAreaElement>(null)
const [inputValue, setInputValue] = useState<string>(defaultValue)
useEffect(() => {
setInputValue(defaultValue)
}, [defaultValue])
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value
setInputValue(newValue)
if (updateFormValue) {
updateFormValue(newValue)
}
}
return (
<div className={`tw:form-control tw:w-full ${containerStyle ?? ''}`}>
{labelTitle ? (
<label className='tw:label'>
<span className={`tw:label-text tw:text-base-content ${labelStyle ?? ''}`}>
{labelTitle}
</span>
</label>
) : null}
<textarea
required={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>
)
}

View File

@ -42,10 +42,10 @@ export function TextAreaInput({
}
return (
<div className={`tw-form-control tw-w-full ${containerStyle ?? ''}`}>
<div className={`tw:form-control tw:w-full ${containerStyle ?? ''}`}>
{labelTitle ? (
<label className='tw-label'>
<span className={`tw-label-text tw-text-base-content ${labelStyle ?? ''}`}>
<label className='tw:label'>
<span className={`tw:label-text tw:text-base-content ${labelStyle ?? ''}`}>
{labelTitle}
</span>
</label>
@ -55,7 +55,7 @@ export function TextAreaInput({
ref={ref}
value={inputValue}
name={dataField}
className={`tw-textarea tw-textarea-bordered tw-w-full tw-leading-5 ${inputStyle ?? ''}`}
className={`tw:textarea tw:textarea-bordered tw:w-full tw:leading-5 ${inputStyle ?? ''}`}
placeholder={placeholder ?? ''}
onChange={handleChange}
></textarea>

View File

@ -9,9 +9,9 @@ describe('<TextInput />', () => {
cy.get('input').should('have.attr', 'type', 'text')
cy.get('input').should('have.attr', 'placeholder', '')
cy.get('input').should('have.attr', 'required')
cy.get('input').should('have.class', 'tw-input')
cy.get('input').should('have.class', 'tw-input-bordered')
cy.get('input').should('have.class', 'tw-w-full')
cy.get('input').should('have.class', 'input')
cy.get('input').should('have.class', 'input-bordered')
cy.get('input').should('have.class', 'tw:w-full')
})
it('renders with given labelTitle', () => {

View File

@ -47,10 +47,10 @@ export function TextInput({
}
return (
<div className={`tw-form-control ${containerStyle ?? ''}`}>
<div className={`tw:form-control ${containerStyle ?? ''}`}>
{labelTitle ? (
<label className='tw-label'>
<span className={`tw-label-text tw-text-base-content ${labelStyle ?? ''}`}>
<label className='tw:label'>
<span className={`tw:label-text tw:text-base-content ${labelStyle ?? ''}`}>
{labelTitle}
</span>
</label>
@ -64,7 +64,7 @@ export function TextInput({
placeholder={placeholder ?? ''}
autoComplete={autocomplete}
onChange={handleChange}
className={`tw-input tw-input-bordered tw-w-full ${inputStyle ?? ''}`}
className={`tw:input tw:input-bordered tw:w-full ${inputStyle ?? ''}`}
/>
</div>
)

View File

@ -2,7 +2,7 @@
exports[`<ComboBoxInput /> > renders properly 1`] = `
<select
class="tw-form-select tw-block tw-w-full tw-py-2 tw-px-4 tw-border tw-border-gray-300 rounded-md tw-shadow-sm tw-text-sm focus:tw-outline-none focus:tw-ring-indigo-500 focus:tw-border-indigo-500 sm:tw-text-sm"
class="tw:form-select tw:block tw:w-full tw:py-2 tw:px-4 tw:border tw:border-gray-300 rounded-md tw:shadow-sm tw:text-sm tw:focus:outline-hidden tw:focus:ring-indigo-500 tw:focus:border-indigo-500 tw:sm:text-sm"
>
<option
value="Option 1"

View File

@ -2,19 +2,19 @@
exports[`<TextAreaInput /> > labelTitle > sets label 1`] = `
<div
class="tw-form-control tw-w-full "
class="tw:form-control tw:w-full "
>
<label
class="tw-label"
class="tw:label"
>
<span
class="tw-label-text tw-text-base-content "
class="tw:label-text tw:text-base-content "
>
My Title
</span>
</label>
<textarea
class="tw-textarea tw-textarea-bordered tw-w-full tw-leading-5 "
class="tw:textarea tw:textarea-bordered tw:w-full tw:leading-5 "
placeholder=""
required=""
/>
@ -23,10 +23,10 @@ exports[`<TextAreaInput /> > labelTitle > sets label 1`] = `
exports[`<TextAreaInput /> > renders properly 1`] = `
<div
class="tw-form-control tw-w-full "
class="tw:form-control tw:w-full "
>
<textarea
class="tw-textarea tw-textarea-bordered tw-w-full tw-leading-5 "
class="tw:textarea tw:textarea-bordered tw:w-full tw:leading-5 "
placeholder=""
required=""
/>

View File

@ -2,19 +2,19 @@
exports[`<TextInput /> > labelTitle > sets label 1`] = `
<div
class="tw-form-control "
class="tw:form-control "
>
<label
class="tw-label"
class="tw:label"
>
<span
class="tw-label-text tw-text-base-content "
class="tw:label-text tw:text-base-content "
>
My Title
</span>
</label>
<input
class="tw-input tw-input-bordered tw-w-full "
class="tw:input tw:input-bordered tw:w-full "
placeholder=""
required=""
type="text"
@ -25,10 +25,10 @@ exports[`<TextInput /> > labelTitle > sets label 1`] = `
exports[`<TextInput /> > renders properly 1`] = `
<div
class="tw-form-control "
class="tw:form-control "
>
<input
class="tw-input tw-input-bordered tw-w-full "
class="tw:input tw:input-bordered tw:w-full "
placeholder=""
required=""
type="text"

View File

@ -0,0 +1,8 @@
import { ItemFormPopup } from '#components/Map/Subcomponents/ItemFormPopup'
/**
* @category Item
*/
export const PopupForm = ({ children }: { children?: React.ReactNode }) => {
return <ItemFormPopup>{children}</ItemFormPopup>
}

View File

@ -0,0 +1,182 @@
import { useContext, useMemo, useState } from 'react'
import { Marker, Tooltip } from 'react-leaflet'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import {
useFilterTags,
useIsLayerVisible,
useIsGroupTypeVisible,
useVisibleGroupType,
} from '#components/Map/hooks/useFilter'
import { useItems, useAllItemsLoaded } from '#components/Map/hooks/useItems'
import { useAddMarker, useAddPopup, useLeafletRefs } from '#components/Map/hooks/useLeafletRefs'
import { useSetMarkerClicked, useSelectPosition } from '#components/Map/hooks/useSelectPosition'
import { useGetItemTags, useAllTagsLoaded, useTags } from '#components/Map/hooks/useTags'
import LayerContext from '#components/Map/LayerContext'
import { ItemViewPopup } from '#components/Map/Subcomponents/ItemViewPopup'
import { encodeTag } from '#utils/FormatTags'
import { hashTagRegex } from '#utils/HashTagRegex'
import MarkerIconFactory from '#utils/MarkerIconFactory'
import { randomColor } from '#utils/RandomColor'
import TemplateItemContext from './TemplateItemContext'
import type { Item } from '#types/Item'
import type { Tag } from '#types/Tag'
import type { Popup } from 'leaflet'
/**
* @category Item
*/
export const PopupView = ({ children }: { children?: React.ReactNode }) => {
const layerContext = useContext(LayerContext)
const { name, markerDefaultColor, markerDefaultColor2, markerShape, markerIcon } = layerContext
const filterTags = useFilterTags()
const appState = useAppState()
const items = useItems()
const getItemTags = useGetItemTags()
const addMarker = useAddMarker()
const addPopup = useAddPopup()
const leafletRefs = useLeafletRefs()
const allTagsLoaded = useAllTagsLoaded()
const allItemsLoaded = useAllItemsLoaded()
const setMarkerClicked = useSetMarkerClicked()
const selectPosition = useSelectPosition()
const tags = useTags()
const [newTagsToAdd, setNewTagsToAdd] = useState<Tag[]>([])
const [tagsReady, setTagsReady] = useState<boolean>(false)
const isLayerVisible = useIsLayerVisible()
const isGroupTypeVisible = useIsGroupTypeVisible()
const visibleGroupTypes = useVisibleGroupType()
const visibleItems = useMemo(
() =>
items
.filter((item) => item.layer?.name === name)
.filter((item) =>
filterTags.length === 0
? item
: filterTags.some((tag) =>
getItemTags(item).some(
(filterTag) =>
filterTag.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase(),
),
),
)
.filter((item) => item.layer && isLayerVisible(item.layer))
.filter(
(item) =>
(item.group_type && isGroupTypeVisible(item.group_type)) ||
visibleGroupTypes.length === 0,
),
[
filterTags,
getItemTags,
isGroupTypeVisible,
isLayerVisible,
items,
name,
visibleGroupTypes.length,
],
)
return visibleItems.map((item: Item) => {
if (!(item.position?.coordinates[0] && item.position.coordinates[1])) return null
if (item.tags) {
item.text += '\n\n'
item.tags.map((tag) => {
if (!item.text?.includes(`#${encodeTag(tag)}`)) {
item.text += `#${encodeTag(tag)}`
}
return item.text
})
}
if (allTagsLoaded && allItemsLoaded) {
item.text?.match(hashTagRegex)?.map((tag) => {
if (
!tags.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase()) &&
!newTagsToAdd.find((t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase())
) {
const newTag = {
id: crypto.randomUUID(),
name: tag.slice(1),
color: randomColor(),
}
setNewTagsToAdd((current) => [...current, newTag])
}
return null
})
!tagsReady && setTagsReady(true)
}
const itemTags = getItemTags(item)
const latitude = item.position.coordinates[1]
const longitude = item.position.coordinates[0]
let color1 = markerDefaultColor
let color2 = markerDefaultColor2
if (item.color) {
color1 = item.color
} else if (itemTags[0]) {
color1 = itemTags[0].color
}
if (itemTags[0] && item.color) {
color2 = itemTags[0].color
} else if (itemTags[1]) {
color2 = itemTags[1].color
}
return (
<TemplateItemContext.Provider value={item} key={item.id}>
<Marker
ref={(r) => {
if (!(item.id in leafletRefs && leafletRefs[item.id].marker === r)) {
r && addMarker(item, r)
}
}}
eventHandlers={{
click: () => {
selectPosition && setMarkerClicked(item)
},
}}
icon={MarkerIconFactory(
markerShape,
color1,
color2,
item.markerIcon ?? markerIcon,
appState.assetsApi.url,
)}
position={[latitude, longitude]}
>
<ItemViewPopup
ref={(r: Popup | null) => {
if (!(item.id in leafletRefs && leafletRefs[item.id].popup === r)) {
r && addPopup(item, r)
}
}}
item={item}
>
{children}
</ItemViewPopup>
<Tooltip offset={[0, -38]} direction='top'>
{item.name}
</Tooltip>
</Marker>
</TemplateItemContext.Provider>
)
})
}

View File

@ -0,0 +1,7 @@
import { createContext } from 'react'
import type { Item } from '#types/Item'
const ItemContext = createContext<Item | undefined>(undefined)
export default ItemContext

View File

@ -0,0 +1,22 @@
import {
TextView as PlainTextView,
StartEndView as PlainStartEndView,
PopupTextInput as PlainPopupTextInput,
PopupButton as PlainPopupButton,
PopupCheckboxInput as PlainPopupCheckboxInput,
PopupTextAreaInput as PlainPopupTextAreaInput,
PopupStartEndInput as PlainPopupStartEndInput,
} from '#components/Map/Subcomponents/ItemPopupComponents'
import { templateify } from './templateify'
export { PopupForm } from './PopupForm'
export { PopupView } from './PopupView'
export const TextView = templateify(PlainTextView)
export const StartEndView = templateify(PlainStartEndView)
export const PopupTextInput = templateify(PlainPopupTextInput)
export const PopupButton = templateify(PlainPopupButton)
export const PopupCheckboxInput = templateify(PlainPopupCheckboxInput)
export const PopupTextAreaInput = templateify(PlainPopupTextAreaInput)
export const PopupStartEndInput = templateify(PlainPopupStartEndInput)

View File

@ -0,0 +1,15 @@
import { useContext } from 'react'
import ItemContext from './TemplateItemContext'
import type { Item } from '#types/Item'
export function templateify<T extends { item?: Item }>(Component: React.ComponentType<T>) {
const TemplateComponent = (props: T) => {
const item = useContext(ItemContext)
return <Component {...props} item={item} />
}
return TemplateComponent as React.ComponentType<Omit<T, 'item'>>
}

View File

@ -1,38 +0,0 @@
import { Children, cloneElement, isValidElement, useEffect } from 'react'
import type { Item } from '#types/Item'
/**
* @category Map
*/
export const ItemForm = ({
children,
item,
title,
setPopupTitle,
}: {
children?: React.ReactNode
item?: Item
title?: string
setPopupTitle?: React.Dispatch<React.SetStateAction<string>>
}) => {
useEffect(() => {
setPopupTitle && title && setPopupTitle(title)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [title])
return (
<div>
{children
? Children.toArray(children).map((child) =>
isValidElement<{ item: Item; test: string }>(child)
? cloneElement(child, { item, test: 'test' })
: '',
)
: ''}
</div>
)
}
ItemForm.__TYPE = 'ItemForm'

View File

@ -1,20 +0,0 @@
import { Children, cloneElement, isValidElement } from 'react'
import type { Item } from '#types/Item'
/**
* @category Map
*/
export const ItemView = ({ children, item }: { children?: React.ReactNode; item?: Item }) => {
return (
<div>
{children
? Children.toArray(children).map((child) =>
isValidElement<{ item: Item }>(child) ? cloneElement(child, { item }) : null,
)
: null}
</div>
)
}
ItemView.__TYPE = 'ItemView'

View File

@ -1,31 +1,11 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
import { Children, isValidElement, useEffect, useState } from 'react'
import { Marker, Tooltip } from 'react-leaflet'
import { useEffect, useState } from 'react'
import { encodeTag } from '#utils/FormatTags'
import { hashTagRegex } from '#utils/HashTagRegex'
import MarkerIconFactory from '#utils/MarkerIconFactory'
import { randomColor } from '#utils/RandomColor'
import { useSetItemsApi, useSetItemsData } from './hooks/useItems'
import { useAddTag } from './hooks/useTags'
import LayerContext from './LayerContext'
import {
useFilterTags,
useIsGroupTypeVisible,
useIsLayerVisible,
useVisibleGroupType,
} from './hooks/useFilter'
import { useAllItemsLoaded, useItems, useSetItemsApi, useSetItemsData } from './hooks/useItems'
import { useAddMarker, useAddPopup, useLeafletRefs } from './hooks/useLeafletRefs'
import { useSelectPosition, useSetMarkerClicked } from './hooks/useSelectPosition'
import { useAddTag, useAllTagsLoaded, useGetItemTags, useTags } from './hooks/useTags'
import { ItemFormPopup } from './Subcomponents/ItemFormPopup'
import { ItemViewPopup } from './Subcomponents/ItemViewPopup'
import type { Item } from '#types/Item'
import type { LayerProps } from '#types/LayerProps'
import type { Tag } from '#types/Tag'
import type { Popup } from 'leaflet'
import type { ReactElement, ReactNode } from 'react'
export type { Point } from 'geojson'
export type { Item } from '#types/Item'
@ -43,7 +23,7 @@ export const Layer = ({
menuIcon = 'MapPinIcon',
menuText = 'add new place',
menuColor = '#2E7D32',
markerIcon = 'point',
markerIcon,
markerShape = 'circle',
markerDefaultColor = '#777',
markerDefaultColor2 = 'RGBA(35, 31, 32, 0.2)',
@ -55,36 +35,13 @@ export const Layer = ({
// eslint-disable-next-line camelcase
public_edit_items,
listed = true,
setItemFormPopup,
itemFormPopup,
clusterRef,
}: LayerProps) => {
const filterTags = useFilterTags()
const items = useItems()
const setItemsApi = useSetItemsApi()
const setItemsData = useSetItemsData()
const getItemTags = useGetItemTags()
const addMarker = useAddMarker()
const addPopup = useAddPopup()
const leafletRefs = useLeafletRefs()
const allTagsLoaded = useAllTagsLoaded()
const allItemsLoaded = useAllItemsLoaded()
const setMarkerClicked = useSetMarkerClicked()
const selectPosition = useSelectPosition()
const tags = useTags()
const addTag = useAddTag()
const [newTagsToAdd, setNewTagsToAdd] = useState<Tag[]>([])
const [tagsReady, setTagsReady] = useState<boolean>(false)
const isLayerVisible = useIsLayerVisible()
const isGroupTypeVisible = useIsGroupTypeVisible()
const visibleGroupTypes = useVisibleGroupType()
const [newTagsToAdd] = useState<Tag[]>([])
const [tagsReady] = useState<boolean>(false)
useEffect(() => {
data &&
@ -108,10 +65,6 @@ export const Layer = ({
// eslint-disable-next-line camelcase
public_edit_items,
listed,
setItemFormPopup,
itemFormPopup,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
clusterRef,
})
api &&
setItemsApi({
@ -133,10 +86,6 @@ export const Layer = ({
// eslint-disable-next-line camelcase
public_edit_items,
listed,
setItemFormPopup,
itemFormPopup,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
clusterRef,
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, api])
@ -156,178 +105,17 @@ export const Layer = ({
}, [tagsReady])
return (
<>
{items &&
items
.filter((item) => item.layer?.name === name)
.filter((item) =>
filterTags.length === 0
? item
: filterTags.some((tag) =>
getItemTags(item).some(
(filterTag) =>
filterTag.name.toLocaleLowerCase() === tag.name.toLocaleLowerCase(),
),
),
)
.filter((item) => item.layer && isLayerVisible(item.layer))
.filter(
(item) =>
(item.group_type && isGroupTypeVisible(item.group_type)) ||
visibleGroupTypes.length === 0,
)
.map((item: Item) => {
if (item.position?.coordinates[0] && item.position?.coordinates[1]) {
if (item.tags) {
item.text += '\n\n'
item.tags.map((tag) => {
if (!item.text?.includes(`#${encodeTag(tag)}`)) {
item.text += `#${encodeTag(tag)}`
}
return item.text
})
}
if (allTagsLoaded && allItemsLoaded) {
item.text?.match(hashTagRegex)?.map((tag) => {
if (
!tags.find(
(t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase(),
) &&
!newTagsToAdd.find(
(t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase(),
)
) {
const newTag = {
id: crypto.randomUUID(),
name: tag.slice(1),
color: randomColor(),
}
setNewTagsToAdd((current) => [...current, newTag])
}
return null
})
!tagsReady && setTagsReady(true)
}
const itemTags = getItemTags(item)
const latitude = item.position.coordinates[1]
const longitude = item.position.coordinates[0]
let color1 = markerDefaultColor
let color2 = markerDefaultColor2
if (item.color) {
color1 = item.color
} else if (itemTags[0]) {
color1 = itemTags[0].color
}
if (itemTags[0] && item.color) {
color2 = itemTags[0].color
} else if (itemTags[1]) {
color2 = itemTags[1].color
}
return (
<Marker
ref={(r) => {
if (!(item.id in leafletRefs && leafletRefs[item.id].marker === r)) {
r && addMarker(item, r)
}
}}
eventHandlers={{
click: () => {
selectPosition && setMarkerClicked(item)
},
}}
icon={MarkerIconFactory(
markerShape,
color1,
color2,
item.markerIcon ? item.markerIcon : markerIcon,
)}
key={item.id}
position={[latitude, longitude]}
>
{children &&
Children.toArray(children).some(
(child) => isComponentWithType(child) && child.type.__TYPE === 'ItemView',
) ? (
Children.toArray(children).map((child) =>
isComponentWithType(child) && child.type.__TYPE === 'ItemView' ? (
<ItemViewPopup
ref={(r) => {
if (!(item.id in leafletRefs && leafletRefs[item.id].popup === r)) {
r && addPopup(item, r as Popup)
}
}}
key={item.id + item.name}
item={item}
setItemFormPopup={setItemFormPopup}
>
{child}
</ItemViewPopup>
) : null,
)
) : (
<>
<ItemViewPopup
key={item.id + item.name}
ref={(r) => {
if (!(item.id in leafletRefs && leafletRefs[item.id].popup === r)) {
r && addPopup(item, r as Popup)
}
}}
item={item}
setItemFormPopup={setItemFormPopup}
/>
</>
)}
<Tooltip offset={[0, -38]} direction='top'>
{item.name}
</Tooltip>
</Marker>
)
} else return null
})}
{
// {children}}
}
{itemFormPopup &&
itemFormPopup.layer.name === name &&
(children &&
Children.toArray(children).some(
(child) => isComponentWithType(child) && child.type.__TYPE === 'ItemForm',
) ? (
Children.toArray(children).map((child) =>
isComponentWithType(child) && child.type.__TYPE === 'ItemForm' ? (
<ItemFormPopup
key={setItemFormPopup?.name}
position={itemFormPopup.position}
layer={itemFormPopup.layer}
setItemFormPopup={setItemFormPopup}
item={itemFormPopup.item}
>
{child}
</ItemFormPopup>
) : (
''
),
)
) : (
<>
<ItemFormPopup
position={itemFormPopup.position}
layer={itemFormPopup.layer}
setItemFormPopup={setItemFormPopup}
item={itemFormPopup.item}
/>
</>
))}
</>
<LayerContext.Provider
value={{
name,
markerDefaultColor,
markerDefaultColor2,
markerShape,
markerIcon,
menuText,
}}
>
{children}
</LayerContext.Provider>
)
}
function isComponentWithType(node: ReactNode): node is ReactElement & { type: { __TYPE: string } } {
return isValidElement(node) && typeof node.type !== 'string' && '__TYPE' in node.type
}

View File

@ -0,0 +1,22 @@
import { createContext } from 'react'
import type { MarkerIcon } from '#types/MarkerIcon'
interface LayerContextType {
name: string
markerDefaultColor: string
markerDefaultColor2: string
markerShape: string
menuText: string
markerIcon?: MarkerIcon
}
const LayerContext = createContext<LayerContextType>({
name: '',
markerDefaultColor: '',
markerDefaultColor2: '',
markerShape: '',
menuText: '',
})
export default LayerContext

View File

@ -7,21 +7,46 @@ import { useSetPermissionData, useSetPermissionApi, useSetAdminRole } from './ho
import type { ItemsApi } from '#types/ItemsApi'
import type { Permission } from '#types/Permission'
/**
* @category Types
*/
export interface PermissionsProps {
data?: Permission[]
api?: ItemsApi<Permission>
adminRole?: string
}
export type { Permission } from '#types/Permission'
export type { ItemsApi } from '#types/ItemsApi'
/**
* This Components injects Permissions comming from an {@link ItemsApi | `API`}
* ```tsx
* <Permissions api={itemsApiInstance} adminRole="8141dee8-8e10-48d0-baf1-680aea271298" />
* ```
* or from on {@link Permission| `Array`}
* ```tsx
* <Permissions data={permissions} adminRole="8141dee8-8e10-48d0-baf1-680aea271298" />
* ```
* Can be child of {@link AppShell | `AppShell`}
* ```tsx
* <AppShell>
* ...
* <Permissions api={itemsApiInstance} adminRole="8141dee8-8e10-48d0-baf1-680aea271298" />
* </AppShell>
* ```
* Or child of {@link UtopiaMap | `UtopiaMap`}
* ```tsx
* <UtopiaMap>
* ...
* <Permissions api={itemsApiInstance} adminRole="8141dee8-8e10-48d0-baf1-680aea271298" />
* </UtopiaMap>
* ```
* @category Map
*/
export function Permissions({ data, api, adminRole }: PermissionsProps) {
export function Permissions({
data,
api,
adminRole,
}: {
/** Array with all the permissions inside */
data?: Permission[]
/** API to fetch all the permissions from a server */
api?: ItemsApi<Permission>
/** UUID of the admin role which has always all the permissions */
adminRole?: string
}) {
const setPermissionData = useSetPermissionData()
const setPermissionApi = useSetPermissionApi()
const setAdminRole = useSetAdminRole()

View File

View File

@ -31,11 +31,14 @@ export default function AddButton({
return (
<>
{canAddItems() ? (
<div className='tw-dropdown tw-dropdown-top tw-dropdown-end tw-dropdown-hover tw-z-500 tw-absolute tw-right-4 tw-bottom-4'>
<label tabIndex={0} className='tw-z-500 tw-btn tw-btn-circle tw-shadow tw-bg-base-100'>
<SVG src={PlusSVG} className='tw-h-5 tw-w-5' />
<div className='tw:dropdown tw:dropdown-top tw:dropdown-end tw:dropdown-hover tw:z-500 tw:absolute tw:right-4 tw:bottom-4'>
<label
tabIndex={0}
className='tw:z-500 tw:btn tw:btn-circle tw:btn-lg tw:shadow tw:bg-base-100'
>
<SVG src={PlusSVG} className='tw:h-5 tw:w-5' />
</label>
<ul tabIndex={0} className='tw-dropdown-content tw-pr-1 tw-list-none'>
<ul tabIndex={0} className='tw:dropdown-content tw:pr-1 tw:list-none'>
{layers.map(
(layer) =>
layer.api?.createItem &&
@ -43,10 +46,10 @@ export default function AddButton({
layer.listed && (
<li key={layer.name}>
<a>
<div className='tw-tooltip tw-tooltip-left' data-tip={layer.menuText}>
<div className='tw:tooltip tw:tooltip-left' data-tip={layer.menuText}>
<button
tabIndex={0}
className='tw-z-500 tw-border-0 tw-pl-2 tw-p-0 tw-mb-3 tw-w-10 tw-h-10 tw-cursor-pointer tw-rounded-full tw-mouse tw-drop-shadow-md tw-transition tw-ease-in tw-duration-200 focus:tw-outline-none'
className='tw:z-500 tw:border-0 tw:pl-2 tw:p-0 tw:mb-3 tw:w-10 tw:h-10 tw:cursor-pointer tw:rounded-full tw:mouse tw:drop-shadow-md tw:transition tw:ease-in tw:duration-200 tw:focus:outline-hidden'
style={{ backgroundColor: layer.menuColor || '#777' }}
onClick={() => {
triggerAction(layer)
@ -58,7 +61,7 @@ export default function AddButton({
>
<img
src={layer.menuIcon}
className='tw-h-6 tw-w-6 tw-text-white'
className='tw:h-6 tw:w-6 tw:text-white'
style={{ filter: 'invert(100%) brightness(200%)' }}
/>
</button>

View File

@ -26,7 +26,7 @@ export const Control = ({
<div
ref={controlContainerRef}
style={{ zIndex }}
className={`${absolute && 'tw-absolute'} tw-z-[999] tw-flex-col ${position === 'topLeft' && 'tw-top-4 tw-left-4'} ${position === 'bottomLeft' && 'tw-bottom-4 tw-left-4'} ${position === 'topRight' && 'tw-bottom-4 tw-right-4'} ${position === 'bottomRight' && 'tw-bottom-4 tw-right-4'}`}
className={`${absolute && 'tw:absolute'} tw:z-999 tw:flex-col ${position === 'topLeft' && 'tw:top-4 tw:left-4'} ${position === 'bottomLeft' && 'tw:bottom-4 tw:left-4'} ${position === 'topRight' && 'tw:bottom-4 tw:right-4'} ${position === 'bottomRight' && 'tw:bottom-4 tw:right-4'}`}
>
{children}
</div>

View File

@ -28,32 +28,32 @@ export function FilterControl() {
const visibleGroupTypes = useVisibleGroupType()
return (
<div className='tw-card tw-bg-base-100 tw-shadow-xl tw-mt-2 tw-w-fit'>
<div className='tw:card tw:bg-base-100 tw:shadow-xl tw:mt-2 tw:w-fit'>
{open ? (
<div className='tw-card-body tw-pr-4 tw-min-w-[8rem] tw-p-2 tw-w-fit tw-transition-all tw-duration-300'>
<div className='tw:card-body tw:pr-4 tw:min-w-[8rem] tw:p-2 tw:w-fit tw:transition-all tw:duration-300'>
<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'
className='tw:btn tw:btn-sm tw:rounded-2xl tw:btn-circle tw:btn-ghost tw:hover:bg-transparent tw:absolute tw:right-0 tw:top-0 tw:text-gray-600'
onClick={() => {
setOpen(false)
}}
>
<p className='tw-text-center '></p>
<p className='tw:text-center '></p>
</label>
<ul className='tw-flex-row'>
<ul className='tw:flex-row'>
{groupTypes.map((groupType) => (
<li key={groupType.value}>
<label
htmlFor={groupType.value}
className='tw-label tw-justify-normal tw-pt-1 tw-pb-1'
className='tw:label tw:justify-normal tw:pt-1 tw:pb-1'
>
<input
id={groupType.value}
onChange={() => toggleVisibleGroupType(groupType.value)}
type='checkbox'
className='tw-checkbox tw-checkbox-xs tw-checkbox-success'
className='tw:checkbox tw:checkbox-xs tw:checkbox-success'
checked={isGroupTypeVisible(groupType.value)}
/>
<span className='tw-text-sm tw-label-text tw-mx-2 tw-cursor-pointer'>
<span className='tw:text-sm tw:label-text tw:mx-2 tw:cursor-pointer'>
{groupType.text}
</span>
</label>
@ -62,17 +62,17 @@ export function FilterControl() {
</ul>
</div>
) : (
<div className='tw-indicator'>
<div className='tw:indicator'>
{visibleGroupTypes.length < groupTypes.length && (
<span className='tw-indicator-item tw-badge tw-badge-success tw-h-4 tw-p-2 tw-translate-x-1/3 -tw-translate-y-1/3 tw-border-0'></span>
<span className='tw:indicator-item tw:badge tw:badge-success tw:h-4 tw:p-2 tw:translate-x-1/3 tw:-translate-y-1/3 tw:border-0'></span>
)}
<div
className='tw-card-body hover:tw-bg-slate-300 tw-card tw-p-2 tw-h-10 tw-w-10 tw-transition-all tw-duration-300 hover:tw-cursor-pointer'
className='tw:card-body tw:hover:bg-slate-300 tw:card tw:p-2 tw:h-10 tw:w-10 tw:transition-all tw:duration-300 tw:hover:cursor-pointer'
onClick={() => {
setOpen(true)
}}
>
<FunnelIcon className='size-6 tw-stroke-[2.5]' />
<FunnelIcon className='size-6 tw:stroke-[2.5]' />
</div>
</div>
)}

View File

@ -9,15 +9,15 @@ export const GratitudeControl = () => {
if (isAuthenticated) {
return (
<div className='tw-card tw-bg-base-100 tw-shadow-xl tw-mt-2 tw-w-fit'>
<div className='tw:card tw:bg-base-100 tw:shadow-xl tw:mt-2 tw:w-fit'>
{
<div
className='tw-card-body hover:tw-bg-slate-300 tw-card tw-p-2 tw-h-10 tw-w-10 tw-transition-all tw-duration-300 hover:tw-cursor-pointer'
className='tw:card-body tw:hover:bg-slate-300 tw:card tw:p-2 tw:h-10 tw:w-10 tw:transition-all tw:duration-300 tw:hover:cursor-pointer'
onClick={() => {
navigate('/select-user')
}}
>
<HeartIcon className='tw-stroke-[2.5]' />
<HeartIcon className='tw:stroke-[2.5]' />
</div>
}
</div>

View File

@ -5,8 +5,8 @@ import LayerSVG from '#assets/layer.svg'
import { useIsLayerVisible, useToggleVisibleLayer } from '#components/Map/hooks/useFilter'
import { useLayers } from '#components/Map/hooks/useLayers'
export function LayerControl() {
const [open, setOpen] = useState(false)
export function LayerControl({ expandLayerControl = false }: { expandLayerControl: boolean }) {
const [open, setOpen] = useState(expandLayerControl)
const layers = useLayers()
@ -14,34 +14,34 @@ export function LayerControl() {
const toggleVisibleLayer = useToggleVisibleLayer()
return (
<div className='tw-card tw-bg-base-100 tw-shadow-xl tw-mt-2 tw-w-fit'>
<div className='tw:card tw:bg-base-100 tw:shadow-xl tw:mt-2 tw:w-fit'>
{open ? (
<div className='tw-card-body tw-pr-4 tw-min-w-[8rem] tw-p-2 tw-transition-all tw-w-fit tw-duration-300'>
<div className='tw:card-body tw:pr-4 tw:min-w-[8rem] tw:p-2 tw:transition-all tw:w-fit tw:duration-300'>
<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'
className='tw:btn tw:btn-sm tw:rounded-2xl tw:btn-circle tw:btn-ghost tw:hover:bg-transparent tw:absolute tw:right-0 tw:top-0 tw:text-gray-600'
onClick={() => {
setOpen(false)
}}
>
<p className='tw-text-center '></p>
<p className='tw:text-center '></p>
</label>
<ul className='tw-flex-row'>
<ul className='tw:flex-row'>
{layers.map(
(layer) =>
layer.listed && (
<li key={layer.name}>
<label
htmlFor={layer.name}
className='tw-label tw-justify-normal tw-pt-1 tw-pb-1'
className='tw:label tw:justify-normal tw:pt-1 tw:pb-1 tw:text-base-content'
>
<input
id={layer.name}
onChange={() => toggleVisibleLayer(layer)}
type='checkbox'
className='tw-checkbox tw-checkbox-xs tw-checkbox-success'
className='tw:checkbox tw:checkbox-xs tw:checkbox-success'
checked={isLayerVisible(layer)}
/>
<span className='tw-text-sm tw-label-text tw-mx-2 tw-cursor-pointer'>
<span className='tw:text-sm tw:label-text tw:mx-2 tw:cursor-pointer'>
{layer.name}
</span>
</label>
@ -52,7 +52,7 @@ export function LayerControl() {
</div>
) : (
<div
className='tw-card-body hover:tw-bg-slate-300 tw-card tw-p-2 tw-h-10 tw-w-10 tw-transition-all tw-duration-300 hover:tw-cursor-pointer'
className='tw:card-body tw:hover:bg-slate-300 tw:card tw:p-2 tw:h-10 tw:w-10 tw:transition-all tw:duration-300 tw:hover:cursor-pointer'
onClick={() => {
setOpen(true)
}}

View File

@ -43,9 +43,9 @@ export const LocateControl = () => {
return (
<>
<div className='tw-card tw-h-12 tw-w-12 tw-bg-base-100 tw-shadow-xl tw-items-center tw-justify-center hover:tw-bg-slate-300 hover:tw-cursor-pointer tw-transition-all tw-duration-300 tw-ml-2'>
<div className='tw:card tw:flex-none tw:h-12 tw:w-12 tw:bg-base-100 tw:shadow-xl tw:items-center tw:justify-center tw:hover:bg-slate-300 tw:hover:cursor-pointer tw:transition-all tw:duration-300 tw:ml-2'>
<div
className='tw-card-body tw-card tw-p-2 tw-h-10 tw-w-10 '
className='tw:card-body tw:card tw:p-2 tw:h-10 tw:w-10 '
onClick={() => {
if (active) {
lc.stop()
@ -57,11 +57,11 @@ export const LocateControl = () => {
}}
>
{loading ? (
<span className='tw-loading tw-loading-spinner tw-loading-md tw-mt-1'></span>
<span className='tw:loading tw:loading-spinner tw:loading-md tw:mt-1'></span>
) : (
<SVG
src={TargetSVG}
className='tw-mt-1 tw-p-[1px]'
className='tw:mt-1 tw:p-[1px]'
style={{ fill: `${active ? '#fc8702' : 'currentColor'}` }}
/>
)}

View File

@ -11,14 +11,14 @@ export function QuestControl() {
''
) : (
<div
className='tw-card tw-bg-base-100 tw-shadow-xl tw-my-2 tw-w-10'
className='tw:card tw:bg-base-100 tw:shadow-xl tw:my-2 tw:w-10'
onClick={(e) => e.stopPropagation()}
>
<div
className='tw-card-body hover:tw-bg-slate-300 tw-rounded-2xl tw-p-2 tw-h-10 tw-w-10 tw-transition-all tw-duration-300 hover:tw-cursor-pointer'
className='tw:card-body tw:hover:bg-slate-300 tw:rounded-2xl tw:p-2 tw:h-10 tw:w-10 tw:transition-all tw:duration-300 tw:hover:cursor-pointer'
onClick={() => setQuestsOpen(true)}
>
<img src={FistSVG} alt='Quests' className='tw-h-[2em]' />
<img src={FistSVG} alt='Quests' className='tw:h-[2em]' />
</div>
</div>
)}

View File

@ -14,11 +14,12 @@ import FlagIcon from '@heroicons/react/24/outline/FlagIcon'
import MagnifyingGlassIcon from '@heroicons/react/24/outline/MagnifyingGlassIcon'
import axios from 'axios'
import { LatLng, LatLngBounds, marker } from 'leaflet'
import { useEffect, useRef, useState } from 'react'
import { useRef, useState } from 'react'
import SVG from 'react-inlinesvg'
import { useMap, useMapEvents } from 'react-leaflet'
import { useLocation, useNavigate } from 'react-router-dom'
import { useNavigate } from 'react-router-dom'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import { useDebounce } from '#components/Map/hooks/useDebounce'
import { useAddFilterTag } from '#components/Map/hooks/useFilter'
import { useItems } from '#components/Map/hooks/useItems'
@ -48,6 +49,7 @@ export const SearchControl = () => {
const items = useItems()
const leafletRefs = useLeafletRefs()
const addFilterTag = useAddFilterTag()
const appState = useAppState()
useMapEvents({
popupopen: () => {
@ -97,28 +99,20 @@ export const SearchControl = () => {
}
const searchInput = useRef<HTMLInputElement>(null)
const [embedded, setEmbedded] = useState<boolean>(true)
const location = useLocation()
useEffect(() => {
const params = new URLSearchParams(location.search)
const embedded = params.get('embedded')
embedded !== 'true' && setEmbedded(false)
}, [location])
return (
<>
{!(windowDimensions.height < 500 && popupOpen && hideSuggestions) && (
<div className='tw-w-[calc(100vw-2rem)] tw-max-w-[22rem] '>
<div className='tw-flex tw-flex-row'>
{embedded && <SidebarControl />}
<div className='tw-relative'>
<div className='tw:w-[calc(100vw-2rem)] tw:max-w-[22rem] '>
<div className='tw:flex tw:flex-row'>
{appState.embedded && <SidebarControl />}
<div className='tw:relative tw:shrink tw:max-w-69 tw:w-full'>
<input
type='text'
placeholder='search ...'
autoComplete='off'
value={value}
className='tw-input tw-input-bordered tw-grow tw-shadow-xl tw-rounded-lg tw-pr-12'
className='tw:input tw:input-bordered tw:h-12 tw:grow tw:shadow-xl tw:rounded-box tw:pr-12 tw:w-full'
ref={searchInput}
onChange={(e) => setValue(e.target.value)}
onFocus={() => {
@ -129,7 +123,7 @@ export const SearchControl = () => {
/>
{value.length > 0 && (
<button
className='tw-btn tw-btn-sm tw-btn-circle tw-absolute tw-right-2 tw-top-2'
className='tw:btn tw:btn-sm tw:btn-circle tw:absolute tw:right-2 tw:top-2'
onClick={() => setValue('')}
>
@ -146,13 +140,13 @@ export const SearchControl = () => {
value.length === 0 ? (
''
) : (
<div className='tw-card tw-card-body tw-bg-base-100 tw-p-4 tw-mt-2 tw-shadow-xl tw-overflow-y-auto tw-max-h-[calc(100dvh-152px)] tw-absolute tw-z-3000'>
<div className='tw:card tw:card-body tw:bg-base-100 tw:p-4 tw:mt-2 tw:shadow-xl tw:overflow-y-auto tw:max-h-[calc(100dvh-152px)] tw:absolute tw:z-3000 tw:w-83'>
{tagsResults.length > 0 && (
<div className='tw-flex tw-flex-wrap'>
<div className='tw:flex tw:flex-wrap'>
{tagsResults.slice(0, 3).map((tag) => (
<div
key={tag.name}
className='tw-rounded-2xl tw-text-white tw-p-1 tw-px-4 tw-shadow-md tw-card tw-mr-2 tw-mb-2 tw-cursor-pointer'
className='tw:rounded-2xl tw:text-white tw:p-1 tw:px-4 tw:shadow-md tw:card tw:mr-2 tw:mb-2 tw:cursor-pointer'
style={{ backgroundColor: tag.color }}
onClick={() => {
addFilterTag(tag)
@ -165,12 +159,12 @@ export const SearchControl = () => {
)}
{itemsResults.length > 0 && tagsResults.length > 0 && (
<hr className='tw-opacity-50'></hr>
<hr className='tw:opacity-50'></hr>
)}
{itemsResults.slice(0, 5).map((item) => (
<div
key={item.id}
className='tw-cursor-pointer hover:tw-font-bold tw-flex tw-flex-row'
className='tw:cursor-pointer tw:hover:font-bold tw:flex tw:flex-row'
onClick={() => {
const marker = Object.entries(leafletRefs).find((r) => r[1].item === item)?.[1]
.marker
@ -186,7 +180,7 @@ export const SearchControl = () => {
{item.layer?.menuIcon ? (
<SVG
src={item.layer.menuIcon}
className='tw-text-current tw-mr-2 tw-mt-0 tw-w-5'
className='tw:text-current tw:mr-2 tw:mt-0 tw:w-5 tw:h-5'
preProcessor={(code: string): string => {
code = code.replace(/fill=".*?"/g, 'fill="currentColor"')
code = code.replace(/stroke=".*?"/g, 'stroke="currentColor"')
@ -194,13 +188,13 @@ export const SearchControl = () => {
}}
/>
) : (
<div className='tw-w-5' />
<div className='tw:w-5' />
)}
<div>
<div className='tw-text-sm tw-overflow-hidden tw-text-ellipsis tw-whitespace-nowrap tw-max-w-[17rem]'>
<div className='tw:text-sm tw:overflow-hidden tw:text-ellipsis tw:whitespace-nowrap tw:max-w-[17rem]'>
{item.name}
</div>
<div className='tw-text-xs tw-overflow-hidden tw-text-ellipsis tw-whitespace-nowrap tw-max-w-[17rem]'>
<div className='tw:text-xs tw:overflow-hidden tw:text-ellipsis tw:whitespace-nowrap tw:max-w-[17rem]'>
{item.text}
</div>
</div>
@ -208,20 +202,20 @@ export const SearchControl = () => {
))}
{Array.from(geoResults).length > 0 &&
(itemsResults.length > 0 || tagsResults.length > 0) && (
<hr className='tw-opacity-50'></hr>
<hr className='tw:opacity-50'></hr>
)}
{Array.from(geoResults).map((geo) => (
<div
className='tw-flex tw-flex-row hover:tw-font-bold tw-cursor-pointer'
className='tw:flex tw:flex-row tw:hover:font-bold tw:cursor-pointer'
key={Math.random()}
onClick={() => {
searchInput.current?.blur()
marker(new LatLng(geo.geometry.coordinates[1], geo.geometry.coordinates[0]), {
icon: MarkerIconFactory('circle', '#777', 'RGBA(35, 31, 32, 0.2)', 'point'),
icon: MarkerIconFactory('circle', '#777', 'RGBA(35, 31, 32, 0.2)'),
})
.addTo(map)
.bindPopup(
`<h3 class="tw-text-base tw-font-bold">${geo?.properties.name ? geo?.properties.name : value}<h3>${capitalizeFirstLetter(geo?.properties?.osm_value)}`,
`<h3 class="tw:text-base tw:font-bold">${geo?.properties.name ? geo?.properties.name : value}<h3>${capitalizeFirstLetter(geo?.properties?.osm_value)}`,
)
.openPopup()
.addEventListener('popupclose', (e) => {
@ -244,12 +238,12 @@ export const SearchControl = () => {
hide()
}}
>
<MagnifyingGlassIcon className='tw-text-current tw-mr-2 tw-mt-0 tw-w-5' />
<MagnifyingGlassIcon className='tw:text-current tw:mr-2 tw:mt-0 tw:w-5' />
<div>
<div className='tw-text-sm tw-overflow-hidden tw-text-ellipsis tw-whitespace-nowrap tw-max-w-[17rem]'>
<div className='tw:text-sm tw:overflow-hidden tw:text-ellipsis tw:whitespace-nowrap tw:max-w-[17rem]'>
{geo?.properties.name ? geo?.properties.name : value}
</div>
<div className='tw-text-xs tw-overflow-hidden tw-text-ellipsis tw-whitespace-nowrap tw-max-w-[17rem]'>
<div className='tw:text-xs tw:overflow-hidden tw:text-ellipsis tw:whitespace-nowrap tw:max-w-[17rem]'>
{geo?.properties?.city && `${capitalizeFirstLetter(geo?.properties?.city)}, `}{' '}
{geo?.properties?.osm_value &&
geo?.properties?.osm_value !== 'yes' &&
@ -267,17 +261,17 @@ export const SearchControl = () => {
))}
{isGeoCoordinate(value) && (
<div
className='tw-flex tw-flex-row hover:tw-font-bold tw-cursor-pointer'
className='tw:flex tw:flex-row tw:hover:font-bold tw:cursor-pointer'
onClick={() => {
marker(
new LatLng(extractCoordinates(value)![0], extractCoordinates(value)![1]),
{
icon: MarkerIconFactory('circle', '#777', 'RGBA(35, 31, 32, 0.2)', 'point'),
icon: MarkerIconFactory('circle', '#777', 'RGBA(35, 31, 32, 0.2)'),
},
)
.addTo(map)
.bindPopup(
`<h3 class="tw-text-base tw-font-bold">${extractCoordinates(value)![0]}, ${extractCoordinates(value)![1]}</h3>`,
`<h3 class="tw:text-base tw:font-bold">${extractCoordinates(value)![0]}, ${extractCoordinates(value)![1]}</h3>`,
)
.openPopup()
.addEventListener('popupclose', (e) => {
@ -291,12 +285,12 @@ export const SearchControl = () => {
)
}}
>
<FlagIcon className='tw-text-current tw-mr-2 tw-mt-0 tw-w-4' />
<FlagIcon className='tw:text-current tw:mr-2 tw:mt-0 tw:w-4' />
<div>
<div className='tw-text-sm tw-overflow-hidden tw-text-ellipsis tw-whitespace-nowrap tw-max-w-[17rem]'>
<div className='tw:text-sm tw:overflow-hidden tw:text-ellipsis tw:whitespace-nowrap tw:max-w-[17rem]'>
{value}
</div>
<div className='tw-text-xs tw-overflow-hidden tw-text-ellipsis tw-whitespace-nowrap tw-max-w-[17rem]'>
<div className='tw:text-xs tw:overflow-hidden tw:text-ellipsis tw:whitespace-nowrap tw:max-w-[17rem]'>
{'Coordiante'}
</div>
</div>

View File

@ -1,21 +1,21 @@
import Bars3Icon from '@heroicons/react/16/solid/Bars3Icon'
import { useAppState, useSetAppState } from '#components/AppShell/hooks/useAppState'
// Converts leaflet.locatecontrol to a React Component
export const SidebarControl = () => {
const appState = useAppState()
const setAppState = useSetAppState()
const toggleSidebar = () => {
setAppState({ sideBarOpen: !appState.sideBarOpen })
}
return (
<>
<div className='tw-card tw-bg-base-100 tw-shadow-xl tw-items-center tw-justify-center hover:tw-bg-slate-300 hover:tw-cursor-pointer tw-transition-all tw-duration-300 tw-mr-2 tw-h-12 tw-w-12 '>
<div className='tw-card-body tw-card tw-p-0'>
<button
className='tw-btn tw-btn-square tw-btn-ghost tw-rounded-2xl'
data-te-sidenav-toggle-ref
data-te-target='#sidenav'
aria-controls='#sidenav'
aria-haspopup='true'
>
<Bars3Icon className='tw-inline-block tw-w-5 tw-h-5' />
</button>
</div>
<div
className='tw:card tw:justify-center tw:items-center tw:bg-base-100 tw:flex-none tw:shadow-xl tw:px-0 tw:hover:bg-slate-300 tw:hover:cursor-pointer tw:transition-all tw:duration-300 tw:mr-2 tw:h-12 tw:w-12 '
onClick={() => toggleSidebar()}
>
<Bars3Icon className='tw:inline-block tw:w-5 tw:h-5' />
</div>
</>
)

View File

@ -6,16 +6,16 @@ export const TagsControl = () => {
const removeFilterTag = useRemoveFilterTag()
return (
<div className='tw-flex tw-flex-wrap tw-mt-4 tw-w-[calc(100vw-2rem)] tw-max-w-xs'>
<div className='tw:flex tw:flex-wrap tw:mt-4 tw:w-[calc(100vw-2rem)] tw:max-w-xs'>
{filterTags.map((tag) => (
<div
key={tag.id}
className='tw-rounded-2xl tw-text-white tw-p-2 tw-px-4 tw-shadow-xl tw-card tw-mr-2 tw-mb-2'
className='tw:rounded-2xl tw:text-white tw:p-2 tw:px-4 tw:shadow-xl tw:card tw:mr-2 tw:mb-2'
style={{ backgroundColor: tag.color }}
>
<div className='tw-card-actions tw-justify-end'>
<div className='tw:card-actions tw:justify-end'>
<label
className='tw-btn tw-btn-xs tw-btn-circle tw-absolute tw--right-2 tw--top-2 tw-bg-white tw-text-gray-600'
className='tw:btn tw:btn-xs tw:btn-circle tw:absolute tw:-right-2 tw:-top-2 tw:bg-white tw:text-gray-600'
onClick={() => removeFilterTag(tag.name)}
>

View File

@ -2,31 +2,39 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from 'react'
import { useContext, useEffect, useRef, useState } from 'react'
import { Popup as LeafletPopup, useMap } from 'react-leaflet'
import { toast } from 'react-toastify'
import { useAuth } from '#components/Auth/useAuth'
import { TextAreaInput } from '#components/Input/TextAreaInput'
import { TextInput } from '#components/Input/TextInput'
import TemplateItemContext from '#components/Item/TemplateItemContext'
import { useResetFilterTags } from '#components/Map/hooks/useFilter'
import { useAddItem, useItems, useRemoveItem, useUpdateItem } from '#components/Map/hooks/useItems'
import { useAddItem, useItems, useUpdateItem } from '#components/Map/hooks/useItems'
import { usePopupForm } from '#components/Map/hooks/usePopupForm'
import { useAddTag, useTags } from '#components/Map/hooks/useTags'
import LayerContext from '#components/Map/LayerContext'
import { hashTagRegex } from '#utils/HashTagRegex'
import { randomColor } from '#utils/RandomColor'
import type { Item } from '#types/Item'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
export function ItemFormPopup(props: ItemFormPopupProps) {
interface Props {
children?: React.ReactNode
}
export function ItemFormPopup(props: Props) {
const layerContext = useContext(LayerContext)
const { menuText, name: activeLayerName } = layerContext
const { popupForm, setPopupForm } = usePopupForm()
const [spinner, setSpinner] = useState(false)
const [popupTitle, setPopupTitle] = useState<string>('')
const formRef = useRef<HTMLFormElement>(null)
const map = useMap()
@ -35,8 +43,6 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
const updateItem = useUpdateItem()
const items = useItems()
const removeItem = useRemoveItem()
const tags = useTags()
const addTag = useAddTag()
@ -45,13 +51,19 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
const { user } = useAuth()
const handleSubmit = async (evt: any) => {
if (!popupForm) {
throw new Error('Popup form is not defined')
}
const formItem: Item = {} as Item
Array.from(evt.target).forEach((input: HTMLInputElement) => {
if (input.name) {
formItem[input.name] = input.value
}
})
formItem.position = { type: 'Point', coordinates: [props.position.lng, props.position.lat] }
formItem.position = {
type: 'Point',
coordinates: [popupForm.position.lng, popupForm.position.lat],
}
evt.preventDefault()
const name = formItem.name ? formItem.name : user?.first_name
@ -73,32 +85,32 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
return null
})
if (props.item) {
if (popupForm.item) {
let success = false
try {
await props.layer.api?.updateItem!({ ...formItem, id: props.item.id })
await popupForm.layer.api?.updateItem!({ ...formItem, id: popupForm.item.id })
success = true
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
toast.error(error.toString())
}
if (success) {
updateItem({ ...props.item, ...formItem })
updateItem({ ...popupForm.item, ...formItem })
toast.success('Item updated')
}
setSpinner(false)
map.closePopup()
} else {
const item = items.find((i) => i.user_created?.id === user?.id && i.layer === props.layer)
const item = items.find((i) => i.user_created?.id === user?.id && i.layer === popupForm.layer)
const uuid = crypto.randomUUID()
let success = false
try {
props.layer.userProfileLayer &&
popupForm.layer.userProfileLayer &&
item &&
(await props.layer.api?.updateItem!({ ...formItem, id: item.id }))
;(!props.layer.userProfileLayer || !item) &&
(await props.layer.api?.createItem!({
(await popupForm.layer.api?.updateItem!({ ...formItem, id: item.id }))
;(!popupForm.layer.userProfileLayer || !item) &&
(await popupForm.layer.api?.createItem!({
...formItem,
name,
id: uuid,
@ -109,14 +121,14 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
toast.error(error.toString())
}
if (success) {
if (props.layer.userProfileLayer && item) updateItem({ ...item, ...formItem })
if (!props.layer.userProfileLayer || !item) {
if (popupForm.layer.userProfileLayer && item) updateItem({ ...item, ...formItem })
if (!popupForm.layer.userProfileLayer || !item) {
addItem({
...formItem,
name: (formItem.name ? formItem.name : user?.first_name) ?? '',
user_created: user ?? undefined,
id: uuid,
layer: props.layer,
layer: popupForm.layer,
public_edit: !user,
})
}
@ -126,7 +138,7 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
setSpinner(false)
map.closePopup()
}
props.setItemFormPopup!(null)
setPopupForm(null)
}
const resetPopup = () => {
@ -137,77 +149,75 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
useEffect(() => {
resetPopup()
}, [props.position])
}, [popupForm?.position])
return (
<LeafletPopup
minWidth={275}
maxWidth={275}
autoPanPadding={[20, 80]}
eventHandlers={{
remove: () => {
setTimeout(function () {
resetPopup()
}, 100)
},
}}
position={props.position}
>
<form ref={formRef} onReset={resetPopup} autoComplete='off' onSubmit={(e) => handleSubmit(e)}>
{props.item ? (
<div className='tw-h-3'></div>
) : (
<div className='tw-flex tw-justify-center'>
<b className='tw-text-xl tw-text-center tw-font-bold'>{props.layer.menuText}</b>
popupForm &&
popupForm.layer.name === activeLayerName && (
<LeafletPopup
minWidth={275}
maxWidth={275}
autoPanPadding={[20, 80]}
eventHandlers={{
remove: () => {
setTimeout(function () {
resetPopup()
}, 100)
},
}}
position={popupForm.position}
>
<form
ref={formRef}
onReset={resetPopup}
autoComplete='off'
onSubmit={(e) => handleSubmit(e)}
>
{popupForm.item ? (
<div className='tw:h-3'></div>
) : (
<div className='tw:flex tw:justify-center'>
<b className='tw:text-xl tw:text-center tw:font-bold'>{menuText}</b>
</div>
)}
{props.children ? (
<TemplateItemContext.Provider value={popupForm.item}>
{props.children}
</TemplateItemContext.Provider>
) : (
<>
<TextInput
type='text'
placeholder='Name'
dataField='name'
defaultValue={popupForm.item ? popupForm.item.name : ''}
inputStyle=''
/>
<TextAreaInput
key={popupForm.position.toString()}
placeholder='Text'
dataField='text'
defaultValue={popupForm.item?.text ?? ''}
inputStyle='tw:h-40 tw:mt-5'
/>
</>
)}
<div className='tw:flex tw:justify-center'>
<button
className={
spinner
? 'tw:btn tw:btn-disabled tw:mt-5 tw:place-self-center'
: 'tw:btn tw:mt-5 tw:place-self-center'
}
type='submit'
>
{spinner ? <span className='tw:loading tw:loading-spinner'></span> : 'Save'}
</button>
</div>
)}
{props.children ? (
Children.toArray(props.children).map((child) =>
isValidElement<{
item: Item
test: string
setPopupTitle: React.Dispatch<React.SetStateAction<string>>
}>(child)
? cloneElement(child, {
item: props.item,
key: props.position.toString(),
setPopupTitle,
})
: '',
)
) : (
<>
<TextInput
type='text'
placeholder='Name'
dataField='name'
defaultValue={props.item ? props.item.name : ''}
inputStyle=''
/>
<TextAreaInput
key={props.position.toString()}
placeholder='Text'
dataField='text'
defaultValue={props.item?.text ?? ''}
inputStyle='tw-h-40 tw-mt-5'
/>
</>
)}
<div className='tw-flex tw-justify-center'>
<button
className={
spinner
? 'tw-btn tw-btn-disabled tw-mt-5 tw-place-self-center'
: 'tw-btn tw-mt-5 tw-place-self-center'
}
type='submit'
>
{spinner ? <span className='tw-loading tw-loading-spinner'></span> : 'Save'}
</button>
</div>
</form>
</LeafletPopup>
</form>
</LeafletPopup>
)
)
}

View File

@ -57,9 +57,7 @@ export function HeaderView({
const [imageLoaded, setImageLoaded] = useState(false)
const avatar =
item.image &&
appState.assetsApi.url + item.image + `${big ? '?width=160&heigth=160' : '?width=80&heigth=80'}`
const avatar = item.image && appState.assetsApi.url + item.image + '?width=160&heigth=160'
const title = item.name
const subtitle = item.subname
@ -74,18 +72,18 @@ export function HeaderView({
return (
<>
<div className='tw-flex tw-flex-row'>
<div className={'tw-grow tw-max-w-[calc(100%-60px)] }'}>
<div className='tw:flex tw:flex-row'>
<div className={'tw:grow tw:max-w-[calc(100%-60px)] }'}>
<div className='flex items-center'>
{avatar && (
<div className='tw-avatar'>
<div className='tw:avatar'>
<div
className={`${
big ? 'tw-w-20' : 'tw-w-10'
} tw-inline tw-items-center tw-justify-center overflow-hidden`}
big ? 'tw:w-20' : 'tw:w-10'
} tw:inline tw:items-center tw:justify-center overflow-hidden`}
>
<img
className={'tw-w-full tw-h-full tw-object-cover tw-rounded-full'}
className={'tw:w-full tw:h-full tw:object-cover tw:rounded-full'}
src={avatar}
alt={item.name + ' logo'}
onLoad={() => setImageLoaded(true)}
@ -93,53 +91,53 @@ export function HeaderView({
style={{ display: imageLoaded ? 'block' : 'none' }}
/>
{!imageLoaded && (
<div className='tw-w-full tw-h-full tw-bg-gray-200 tw-rounded-full' />
<div className='tw:w-full tw:h-full tw:bg-gray-200 tw:rounded-full' />
)}
</div>
</div>
)}
<div className={`${avatar ? 'tw-ml-2' : ''} tw-overflow-hidden`}>
<div className={`${avatar ? 'tw:ml-2' : ''} tw:overflow-hidden`}>
<div
className={`${big ? 'xl:tw-text-3xl tw-text-2xl' : 'tw-text-xl'} tw-font-semibold tw-truncate`}
className={`${big ? 'tw:xl:text-3xl tw:text-2xl' : 'tw:text-xl'} tw:font-semibold tw:truncate`}
title={title}
>
{title}
</div>
{showAddress && address && !hideSubname && (
<div className={`tw-text-xs tw-text-gray-500 ${truncateSubname && 'tw-truncate'}`}>
<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'}`}>
<div className={`tw:text-xs tw:opacity-50 ${truncateSubname && 'tw:truncate'}`}>
{subtitle}
</div>
)}
</div>
</div>
</div>
<div onClick={(e) => e.stopPropagation()} className={`${big ? 'tw-mt-5' : 'tw-mt-1'}`}>
<div onClick={(e) => e.stopPropagation()} className={`${big ? 'tw:mt-5' : 'tw:mt-1'}`}>
{(api?.deleteItem || item.layer?.api?.updateItem) &&
(hasUserPermission(api?.collectionName!, 'delete', item) ||
hasUserPermission(api?.collectionName!, 'update', item)) &&
!hideMenu && (
<div className='tw-dropdown tw-dropdown-bottom'>
<div className='tw:dropdown tw:dropdown-bottom'>
<label
tabIndex={0}
className='tw-bg-base-100 tw-btn tw-m-1 tw-leading-3 tw-border-none tw-min-h-0 tw-h-6'
className='tw:bg-base-100 tw:btn tw:m-1 tw:leading-3 tw:border-none tw:min-h-0 tw:h-6'
>
<EllipsisVerticalIcon className='tw-h-5 tw-w-5' />
<EllipsisVerticalIcon className='tw:h-5 tw:w-5' />
</label>
<ul
tabIndex={0}
className='tw-dropdown-content tw-menu tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-z-1000'
className='tw:dropdown-content tw:menu tw:p-2 tw:shadow tw:bg-base-100 tw:rounded-box tw:z-1000'
>
{api?.updateItem &&
hasUserPermission(api.collectionName!, 'update', item) &&
editCallback && (
<li>
<a
className='!tw-text-base-content tw-cursor-pointer'
className='tw:text-base-content! tw:cursor-pointer'
onClick={(e) =>
item.layer?.customEditLink
? navigate(
@ -148,7 +146,7 @@ export function HeaderView({
: editCallback(e)
}
>
<PencilIcon className='tw-h-5 tw-w-5' />
<PencilIcon className='tw:h-5 tw:w-5' />
</a>
</li>
)}
@ -157,10 +155,10 @@ export function HeaderView({
setPositionCallback && (
<li>
<a
className='!tw-text-base-content tw-cursor-pointer'
className='tw:text-base-content! tw:cursor-pointer'
onClick={setPositionCallback}
>
<SVG src={TargetDotSVG} className='tw-w-5 tw-h-5' />
<SVG src={TargetDotSVG} className='tw:w-5 tw:h-5' />
</a>
</li>
)}
@ -168,11 +166,11 @@ export function HeaderView({
hasUserPermission(api.collectionName!, 'delete', item) &&
deleteCallback && (
<li>
<a className='tw-cursor-pointer !tw-text-error' onClick={openDeleteModal}>
<a className='tw:cursor-pointer tw:text-error!' onClick={openDeleteModal}>
{loading ? (
<span className='tw-loading tw-loading-spinner tw-loading-sm'></span>
<span className='tw:loading tw:loading-spinner tw:loading-sm'></span>
) : (
<TrashIcon className='tw-h-5 tw-w-5' />
<TrashIcon className='tw:h-5 tw:w-5' />
)}
</a>
</li>
@ -192,10 +190,10 @@ export function HeaderView({
<span>
Do you want to delete <b>{item.name}</b>?
</span>
<div className='tw-grid'>
<div className='tw-flex tw-justify-between'>
<div className='tw:grid'>
<div className='tw:flex tw:justify-between'>
<label
className='tw-btn tw-mt-4 tw-btn-error'
className='tw:btn tw:mt-4 tw:btn-error'
onClick={(e) => {
deleteCallback(e)
setModalOpen(false)
@ -203,7 +201,7 @@ export function HeaderView({
>
Yes
</label>
<label className='tw-btn tw-mt-4' onClick={() => setModalOpen(false)}>
<label className='tw:btn tw:mt-4' onClick={() => setModalOpen(false)}>
No
</label>
</div>

View File

@ -29,7 +29,7 @@ export const PopupButton = ({
style={{
backgroundColor: `${item?.color ?? (item && (getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : (item?.layer?.markerDefaultColor ?? '#000')))}`,
}}
className='tw-btn tw-text-white tw-btn-sm tw-float-right tw-mt-1'
className='tw:btn tw:text-white tw:btn-sm tw:float-right tw:mt-1'
>
{text}
</button>

View File

@ -13,15 +13,15 @@ export const PopupCheckboxInput = ({
item?: Item
}) => {
return (
<label htmlFor={item?.id} className='tw-label tw-justify-normal tw-pt-1 tw-pb-1'>
<label htmlFor={item?.id} className='tw:label tw:justify-normal tw:pt-1 tw:pb-1'>
<input
id={item?.id}
type='checkbox'
name={dataField}
className='tw-checkbox tw-checkbox-xs tw-checkbox-success'
className='tw:checkbox tw:checkbox-xs tw:checkbox-success'
checked={item?.public_edit}
/>
<span className='tw-text-sm tw-label-text tw-mx-2 tw-cursor-pointer'>{label}</span>
<span className='tw:text-sm tw:label-text tw:mx-2 tw:cursor-pointer'>{label}</span>
</label>
)
}

View File

@ -3,7 +3,7 @@ import { TextInput } from '#components/Input'
import type { Item } from '#types/Item'
interface StartEndInputProps {
export interface StartEndInputProps {
item?: Item
showLabels?: boolean
updateStartValue?: (value: string) => void
@ -20,12 +20,12 @@ export const PopupStartEndInput = ({
updateEndValue,
}: StartEndInputProps) => {
return (
<div className='tw-grid tw-grid-cols-2 tw-gap-2'>
<div className='tw:grid tw:grid-cols-2 tw:gap-2'>
<TextInput
type='date'
placeholder='start'
dataField='start'
inputStyle='tw-text-sm tw-px-2'
inputStyle='tw:text-sm tw:px-2'
labelTitle={showLabels ? 'start' : ''}
defaultValue={item && item.start ? item.start.substring(0, 10) : ''}
autocomplete='one-time-code'
@ -35,7 +35,7 @@ export const PopupStartEndInput = ({
type='date'
placeholder='end'
dataField='end'
inputStyle='tw-text-sm tw-px-2'
inputStyle='tw:text-sm tw:px-2'
labelTitle={showLabels ? 'end' : ''}
defaultValue={item && item.end ? item.end.substring(0, 10) : ''}
autocomplete='one-time-code'

View File

@ -23,7 +23,7 @@ export const PopupTextInput = ({
placeholder={placeholder}
inputStyle={style}
type='text'
containerStyle={'tw-mt-4'}
containerStyle={'tw:mt-4'}
></TextInput>
)
}

View File

@ -8,23 +8,23 @@ import type { Item } from '#types/Item'
*/
export const StartEndView = ({ item }: { item?: Item }) => {
return (
<div className='tw-flex tw-flex-row tw-mb-4 tw-mt-1'>
<div className='tw-basis-2/5 tw-flex tw-flex-row'>
<CalendarIcon className='tw-h-4 tw-w-4 tw-mr-2' />
<div className='tw:flex tw:flex-row tw:mb-4 tw:mt-1'>
<div className='tw:basis-2/5 tw:flex tw:flex-row'>
<CalendarIcon className='tw:h-4 tw:w-4 tw:mr-2' />
<time
className='tw-align-middle'
className='tw:align-middle'
dateTime={item && item.start ? item.start.substring(0, 10) : ''}
>
{item && item.start ? new Date(item.start).toLocaleDateString() : ''}
</time>
</div>
<div className='tw-basis-1/5 tw-place-content-center'>
<div className='tw:basis-1/5 tw:place-content-center'>
<span>-</span>
</div>
<div className='tw-basis-2/5 tw-flex tw-flex-row'>
<CalendarIcon className='tw-h-4 tw-w-4 tw-mr-2' />
<div className='tw:basis-2/5 tw:flex tw:flex-row'>
<CalendarIcon className='tw:h-4 tw:w-4 tw:mr-2' />
<time
className='tw-align-middle'
className='tw:align-middle'
dateTime={item && item.end ? item.end.substring(0, 10) : ''}
>
{item && item.end ? new Date(item.end).toLocaleDateString() : ''}

View File

@ -1,11 +1,9 @@
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { memo } from 'react'
import Markdown from 'react-markdown'
import remarkBreaks from 'remark-breaks'
@ -27,14 +25,12 @@ export const TextView = ({
text,
truncate = false,
rawText,
itemTextField,
}: {
item?: Item
itemId?: string
text?: string
truncate?: boolean
rawText?: string
itemTextField?: string
}) => {
if (item) {
text = item.text
@ -42,8 +38,6 @@ export const TextView = ({
}
const tags = useTags()
const addFilterTag = useAddFilterTag()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const itemTextFieldDummy = itemTextField
let innerText = ''
let replacedText = ''
@ -87,53 +81,15 @@ export const TextView = ({
})
}
const CustomH1 = ({ children }) => <h1 className='tw-text-xl tw-font-bold'>{children}</h1>
const CustomH2 = ({ children }) => <h2 className='tw-text-lg tw-font-bold'>{children}</h2>
const CustomH3 = ({ children }) => <h3 className='tw-text-base tw-font-bold'>{children}</h3>
const CustomH4 = ({ children }) => <h4 className='tw-text-base tw-font-bold'>{children}</h4>
const CustomH5 = ({ children }) => <h5 className='tw-text-sm tw-font-bold'>{children}</h5>
const CustomH6 = ({ children }) => <h6 className='tw-text-sm tw-font-bold'>{children}</h6>
const CustomParagraph = ({ children }) => <p className='!tw-my-2'>{children}</p>
const CustomUnorderdList = ({ children }) => (
<ul className='tw-list-disc tw-list-inside'>{children}</ul>
)
const CustomOrderdList = ({ children }) => (
<ol className='tw-list-decimal tw-list-inside'>{children}</ol>
)
const CustomHorizontalRow = ({ children }) => <hr className='tw-border-current'>{children}</hr>
// eslint-disable-next-line react/prop-types
const CustomImage = ({ alt, src, title }) => (
<img className='tw-max-w-full tw-rounded tw-shadow' src={src} alt={alt} title={title} />
)
const CustomExternalLink = ({ href, children }) => (
<a className='tw-font-bold tw-underline' href={href} target='_blank' rel='noreferrer'>
{' '}
{children}
</a>
)
const CustomHashTagLink = ({
children,
tag,
itemId,
}: {
children: string
tag: Tag
itemId?: string
}) => {
const HashTag = ({ children, tag, itemId }: { children: string; tag: Tag; itemId?: string }) => {
return (
<a
style={{ color: tag ? tag.color : '#faa', fontWeight: 'bold', cursor: 'pointer' }}
className='hashtag'
style={
tag && {
color: tag.color,
}
}
key={tag ? tag.name + itemId : itemId}
onClick={(e) => {
e.stopPropagation()
@ -145,64 +101,48 @@ export const TextView = ({
)
}
// eslint-disable-next-line react/display-name
const MemoizedVideoEmbed = memo(({ url }: { url: string }) => (
<iframe
className='tw-w-full'
src={url}
allow='fullscreen; picture-in-picture'
allowFullScreen
/>
))
const Link = ({ href, children }: { href: string; children: string }) => {
// Youtube
if (href.startsWith('https://www.youtube.com/watch?v=')) {
const videoId = href?.split('v=')[1].split('&')[0]
const youtubeEmbedUrl = `https://www.youtube-nocookie.com/embed/${videoId}`
return (
<iframe src={youtubeEmbedUrl} allow='fullscreen; picture-in-picture' allowFullScreen />
)
}
// Rumble
if (href.startsWith('https://rumble.com/embed/')) {
return <iframe src={href} allow='fullscreen; picture-in-picture' allowFullScreen />
}
// HashTag
if (href.startsWith('#')) {
const tag = tags.find((t) => t.name.toLowerCase() === decodeURI(href).slice(1).toLowerCase())
if (tag)
return (
<HashTag tag={tag} itemId={itemId}>
{children}
</HashTag>
)
else return children
}
// Default: Link
return (
<a href={href} target='_blank' rel='noreferrer'>
{children}
</a>
)
}
return (
<Markdown
className={'tw-text-map tw-leading-map tw-text-sm'}
className={'markdown tw:text-map tw:leading-map tw:text-sm'}
remarkPlugins={[remarkBreaks]}
components={{
p: CustomParagraph,
a: ({ href, children }: { href: string; children: string }) => {
const isYouTubeVideo = href?.startsWith('https://www.youtube.com/watch?v=')
const isRumbleVideo = href?.startsWith('https://rumble.com/embed/')
if (isYouTubeVideo) {
const videoId = href?.split('v=')[1].split('&')[0]
const youtubeEmbedUrl = `https://www.youtube-nocookie.com/embed/${videoId}`
return <MemoizedVideoEmbed url={youtubeEmbedUrl}></MemoizedVideoEmbed>
}
if (isRumbleVideo) {
return <MemoizedVideoEmbed url={href}></MemoizedVideoEmbed>
}
if (href?.startsWith('#')) {
console.log(href.slice(1).toLowerCase())
console.log(tags)
const tag = tags.find(
(t) => t.name.toLowerCase() === decodeURI(href).slice(1).toLowerCase(),
)
if (tag)
return (
<CustomHashTagLink tag={tag} itemId={itemId}>
{children}
</CustomHashTagLink>
)
else return children
} else {
return <CustomExternalLink href={href}>{children}</CustomExternalLink>
}
},
ul: CustomUnorderdList,
ol: CustomOrderdList,
img: CustomImage,
hr: CustomHorizontalRow,
h1: CustomH1,
h2: CustomH2,
h3: CustomH3,
h4: CustomH4,
h5: CustomH5,
h6: CustomH6,
a: Link,
}}
>
{replacedText}

View File

@ -0,0 +1,7 @@
export { PopupTextAreaInput } from './PopupTextAreaInput'
export { PopupStartEndInput } from './PopupStartEndInput'
export { PopupTextInput } from './PopupTextInput'
export { PopupCheckboxInput } from './PopupCheckboxInput'
export { TextView } from './TextView'
export { StartEndView } from './StartEndView'
export { PopupButton } from './PopupButton'

View File

@ -8,12 +8,13 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { LatLng } from 'leaflet'
import { Children, cloneElement, forwardRef, isValidElement, useState } from 'react'
import { forwardRef, useState } from 'react'
import { Popup as LeafletPopup, useMap } from 'react-leaflet'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { useRemoveItem, useUpdateItem } from '#components/Map/hooks/useItems'
import { usePopupForm } from '#components/Map/hooks/usePopupForm'
import { useSetSelectPosition } from '#components/Map/hooks/useSelectPosition'
import { timeAgo } from '#utils/TimeAgo'
@ -21,12 +22,10 @@ import { HeaderView } from './ItemPopupComponents/HeaderView'
import { TextView } from './ItemPopupComponents/TextView'
import type { Item } from '#types/Item'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
export interface ItemViewPopupProps {
item: Item
children?: React.ReactNode
setItemFormPopup?: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
}
// eslint-disable-next-line react/display-name
@ -37,22 +36,26 @@ export const ItemViewPopup = forwardRef((props: ItemViewPopupProps, ref: any) =>
const updadateItem = useUpdateItem()
const navigate = useNavigate()
const setSelectPosition = useSetSelectPosition()
const { setPopupForm } = usePopupForm()
const [infoExpanded, setInfoExpanded] = useState<boolean>(false)
const handleEdit = (event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation()
map.closePopup()
props.setItemFormPopup &&
props.setItemFormPopup({
position: new LatLng(
props.item.position?.coordinates[1]!,
props.item.position?.coordinates[0]!,
),
layer: props.item.layer!,
item: props.item,
setItemFormPopup: props.setItemFormPopup,
})
if (!props.item.layer) {
throw new Error('Layer is not defined')
}
setPopupForm({
position: new LatLng(
props.item.position?.coordinates[1]!,
props.item.position?.coordinates[0]!,
),
layer: props.item.layer,
item: props.item,
})
}
const handleDelete = async (event: React.MouseEvent<HTMLElement>) => {
@ -84,7 +87,7 @@ export const ItemViewPopup = forwardRef((props: ItemViewPopupProps, ref: any) =>
return (
<LeafletPopup ref={ref} maxHeight={377} minWidth={275} maxWidth={275} autoPanPadding={[20, 80]}>
<div className='tw-bg-base-100 tw-text-base-content'>
<div className='tw:bg-base-100 tw:text-base-content'>
<HeaderView
api={props.item.layer?.api}
item={props.item}
@ -97,33 +100,25 @@ export const ItemViewPopup = forwardRef((props: ItemViewPopupProps, ref: any) =>
}}
loading={loading}
/>
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
{props.children ? (
Children.toArray(props.children).map((child) =>
isValidElement<{ item: Item; test: string }>(child)
? cloneElement(child, { item: props.item })
: '',
)
) : (
<TextView text={props.item.text} itemId={props.item.id} />
)}
<div className='tw:overflow-y-auto tw:overflow-x-hidden tw:max-h-64 fade'>
{props.children ?? <TextView text={props.item.text} itemId={props.item.id} />}
</div>
<div className='tw-flex -tw-mb-1 tw-flex-row tw-mr-2 tw-mt-1'>
<div className='tw:flex tw:-mb-1 tw:flex-row tw:mr-2 tw:mt-1'>
{infoExpanded ? (
<p
className={'tw-italic tw-min-h-[21px] !tw-my-0 tw-text-gray-500'}
className={'tw:italic tw:min-h-[21px] tw:my-0! tw:opacity-50'}
>{`${props.item.date_updated && props.item.date_updated !== props.item.date_created ? 'updated' : 'posted'} ${props.item && props.item.user_created && props.item.user_created.first_name ? `by ${props.item.user_created.first_name}` : ''} ${props.item.date_updated ? timeAgo(props.item.date_updated) : timeAgo(props.item.date_created!)}`}</p>
) : (
<p
className='!tw-my-0 tw-min-h-[21px] tw-font-bold tw-cursor-pointer tw-text-gray-500'
className='tw:my-0! tw:min-h-[21px] tw:font-bold tw:cursor-pointer tw:text-gray-500'
onClick={() => setInfoExpanded(true)}
>
</p>
)}
<div className='tw-grow'></div>
<div className='tw:grow'></div>
{
//* * <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="tw-place-self-end tw-w-4 tw-h-4 tw-mb-1 tw-cursor-pointer"><path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" /></svg> */
//* * <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="tw:place-self-end tw:w-4 tw:h-4 tw:mb-1 tw:cursor-pointer"><path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" /></svg> */
}
</div>
</div>

View File

@ -1,18 +1,18 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
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'>
<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'
className='tw:btn tw:btn-sm tw:rounded-2xl tw:btn-circle tw:btn-ghost tw:hover:bg-transparent tw:absolute tw:right-0 tw:top-0 tw:text-gray-600'
onClick={() => {
setSelectNewItemPosition(null)
}}
>
<p className='tw-text-center '></p>
<p className='tw:text-center '></p>
</label>
<div className='tw-alert tw-bg-base-100 tw-text-base-content'>
<div className='tw:alert tw:bg-base-100 tw:text-base-content'>
<div>
<span className='tw-text-lg'>Select position on the map!</span>
<span className='tw:text-lg'>Select position on the map!</span>
</div>
</div>
</div>

View File

@ -1,13 +1,33 @@
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { useAddFilterTag, useFilterTags, useResetFilterTags } from './hooks/useFilter'
import { useSetTagData, useSetTagApi, useTags } from './hooks/useTags'
import { useSetTagData, useSetTagApi } from './hooks/useTags'
import type { ItemsApi } from '#types/ItemsApi'
import type { Tag } from '#types/Tag'
/**
* This Components injects Tags comming from an {@link ItemsApi | `API`}
* ```tsx
* <Tags api={tagsApi} />
* ```
* or from on {@link Tag| `Array`}
* ```tsx
* <Tags data={tags} />
* ```
* Can be child of {@link AppShell | `AppShell`}
* ```tsx
* <AppShell>
* ...
* <Tags api={tagsApi} />
* </AppShell>
* ```
* Or child of {@link UtopiaMap | `UtopiaMap`}
* ```tsx
* <UtopiaMap>
* ...
* <Tags api={tagsApi} />
* </UtopiaMap>
* ```
* @category Map
*/
export function Tags({ data, api }: { data?: Tag[]; api?: ItemsApi<Tag> }) {
@ -20,36 +40,5 @@ export function Tags({ data, api }: { data?: Tag[]; api?: ItemsApi<Tag> }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [api, data])
const location = useLocation()
const addFilterTag = useAddFilterTag()
const resetFilterTags = useResetFilterTags()
const tags = useTags()
const filterTags = useFilterTags()
useEffect(() => {
const params = new URLSearchParams(location.search)
const urlTags = params.get('tags')
const decodedTags = urlTags ? decodeURIComponent(urlTags) : ''
const decodedTagsArray = decodedTags.split(';')
if (
decodedTagsArray.some(
(ut) => !filterTags.find((ft) => ut.toLocaleLowerCase() === ft.name.toLocaleLowerCase()),
) ||
filterTags.some(
(ft) =>
!decodedTagsArray.find((ut) => ut.toLocaleLowerCase() === ft.name.toLocaleLowerCase()),
)
) {
resetFilterTags()
decodedTagsArray.map((urlTag) => {
const tag = tags.find((t) => t.name.toLocaleLowerCase() === urlTag.toLocaleLowerCase())
tag && addFilterTag(tag)
return null
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location, tags])
return <></>
}

View File

@ -5,9 +5,40 @@ import { ContextWrapper } from '#components/AppShell/ContextWrapper'
import { UtopiaMapInner } from './UtopiaMapInner'
import type { UtopiaMapProps } from '#types/UtopiaMapProps'
import type { GeoJsonObject } from 'geojson'
/**
* This component creates the map.
* ```tsx
* <UtopiaMap center={[50.6, 9.5]} zoom={5} height="100dvh" width="100dvw" />
* ```
* You can define its {@link Layer | `Layers`} as supcomponents.
* ```tsx
* <UtopiaMap center={[50.6, 15.5]} zoom={5} height="100dvh" width="100dvw">
* <Layer
* name="events"
* markerIcon="calendar"
* markerShape="square"
* markerDefaultColor="#700"
* data={events}
* />
* <Layer
* name="places"
* markerIcon="point"
* markerShape="circle"
* markerDefaultColor="#007"
* data={places}
* />
* </UtopiaMap>
* ```
* You can also pass {@link Tags | `Tags`} or {@link Permissions | `Permissions`} as subcomponents.
* ```tsx
* <UtopiaMap center={[50.6, 15.5]} zoom={5} height="100dvh" width="100dvw">
* ...
* <Tags data={tags} />
* <Permissions data={permissions} />
* </UtopiaMap>
* ```
* @category Map
*/
function UtopiaMap({
@ -20,16 +51,48 @@ function UtopiaMap({
showFilterControl = false,
showGratitudeControl = false,
showLayerControl = true,
infoText,
showZoomControl = false,
showThemeControl = false,
defaultTheme,
donationWidget,
}: UtopiaMapProps) {
expandLayerControl,
}: {
/** height of the map (default '500px') */
height?: string
/** width of the map (default '100%') */
width?: string
/** initial centered position of the map (default [50.6, 9.5]) */
center?: [number, number]
/** initial zoom level of the map (default 10) */
zoom?: number
/** React child-components */
children?: React.ReactNode
/** GeoJSON to display on the map */
geo?: GeoJsonObject
/** show the filter control widget (default false) */
showFilterControl?: boolean
/** show the gratitude control widget (default false) */
showLayerControl?: boolean
/** show the layer control widget (default true) */
showGratitudeControl?: boolean
/** show zoom control widget (default false) */
showZoomControl?: boolean
/** show a widget to switch the theme */
showThemeControl?: boolean
/** the defaut theme */
defaultTheme?: string
/** ask to donate to the Utopia Project OpenCollective campaign (default false) */
donationWidget?: boolean
/** open layer control on map initialisation */
expandLayerControl?: boolean
}) {
return (
<ContextWrapper>
<MapContainer
style={{ height, width }}
center={new LatLng(center[0], center[1])}
zoom={zoom}
zoomControl={false}
zoomControl={showZoomControl}
maxZoom={19}
>
<UtopiaMapInner
@ -37,8 +100,10 @@ function UtopiaMap({
showFilterControl={showFilterControl}
showGratitudeControl={showGratitudeControl}
showLayerControl={showLayerControl}
infoText={infoText}
donationWidget={donationWidget}
showThemeControl={showThemeControl}
defaultTheme={defaultTheme}
expandLayerControl={expandLayerControl}
>
{children}
</UtopiaMapInner>

View File

@ -6,23 +6,33 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from 'react'
import { useEffect, useRef } from 'react'
import { TileLayer, useMapEvents, GeoJSON, useMap } from 'react-leaflet'
import MarkerClusterGroup from 'react-leaflet-cluster'
import { Outlet, useLocation } from 'react-router-dom'
import { toast } from 'react-toastify'
import { useSetAppState } from '#components/AppShell/hooks/useAppState'
import { useTheme } from '#components/AppShell/hooks/useTheme'
import { containsUUID } from '#utils/ContainsUUID'
import { useClusterRef, useSetClusterRef } from './hooks/useClusterRef'
import { useAddVisibleLayer } from './hooks/useFilter'
import {
useAddFilterTag,
useAddVisibleLayer,
useFilterTags,
useResetFilterTags,
useToggleVisibleLayer,
} from './hooks/useFilter'
import { useLayers } from './hooks/useLayers'
import { useLeafletRefs } from './hooks/useLeafletRefs'
import { usePopupForm } from './hooks/usePopupForm'
import {
useSelectPosition,
useSetMapClicked,
useSetSelectPosition,
} from './hooks/useSelectPosition'
import { useTags } from './hooks/useTags'
import AddButton from './Subcomponents/AddButton'
import { Control } from './Subcomponents/Controls/Control'
import { FilterControl } from './Subcomponents/Controls/FilterControl'
@ -33,9 +43,7 @@ import { TagsControl } from './Subcomponents/Controls/TagsControl'
import { TextView } from './Subcomponents/ItemPopupComponents/TextView'
import { SelectPosition } from './Subcomponents/SelectPosition'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
import type { UtopiaMapProps } from '#types/UtopiaMapProps'
import type { Feature, Geometry as GeoJSONGeometry } from 'geojson'
import type { Feature, Geometry as GeoJSONGeometry, GeoJsonObject } from 'geojson'
export function UtopiaMapInner({
children,
@ -43,27 +51,46 @@ export function UtopiaMapInner({
showFilterControl = false,
showGratitudeControl = false,
showLayerControl = true,
showThemeControl = false,
defaultTheme = '',
donationWidget,
}: UtopiaMapProps) {
expandLayerControl,
}: {
children?: React.ReactNode
geo?: GeoJsonObject
showFilterControl?: boolean
showLayerControl?: boolean
showGratitudeControl?: boolean
donationWidget?: boolean
showThemeControl?: boolean
defaultTheme?: string
expandLayerControl?: boolean
}) {
const selectNewItemPosition = useSelectPosition()
const setSelectNewItemPosition = useSetSelectPosition()
const setClusterRef = useSetClusterRef()
const clusterRef = useClusterRef()
const setMapClicked = useSetMapClicked()
const [itemFormPopup, setItemFormPopup] = useState<ItemFormPopupProps | null>(null)
const { setPopupForm } = usePopupForm()
const layers = useLayers()
const addVisibleLayer = useAddVisibleLayer()
const leafletRefs = useLeafletRefs()
const location = useLocation()
const map = useMap()
useTheme(defaultTheme)
useEffect(() => {
layers.forEach((layer) => addVisibleLayer(layer))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [layers])
const setAppState = useSetAppState()
useEffect(() => {
setAppState({ showThemeControl })
}, [setAppState, showThemeControl])
const init = useRef(false)
useEffect(() => {
if (!init.current) {
@ -80,7 +107,7 @@ export function UtopiaMapInner({
}
/>
<a href='https://opencollective.com/utopia-project'>
<div className='tw-btn tw-btn-sm tw-float-right tw-btn-primary'>Donate</div>
<div className='tw:btn tw:btn-sm tw:float-right tw:btn-primary'>Donate</div>
</a>
</div>
</>,
@ -99,7 +126,7 @@ export function UtopiaMapInner({
// eslint-disable-next-line no-console
console.log(e.latlng.lat + ',' + e.latlng.lng)
if (selectNewItemPosition) {
setMapClicked({ position: e.latlng, setItemFormPopup })
setMapClicked({ position: e.latlng, setItemFormPopup: setPopupForm })
}
},
moveend: () => {},
@ -180,10 +207,65 @@ export function UtopiaMapInner({
}
}
const addFilterTag = useAddFilterTag()
const resetFilterTags = useResetFilterTags()
const tags = useTags()
const filterTags = useFilterTags()
useEffect(() => {
const params = new URLSearchParams(location.search)
const urlTags = params.get('tags')
const decodedTags = urlTags ? decodeURIComponent(urlTags) : ''
const decodedTagsArray = decodedTags.split(';').filter(Boolean)
const urlDiffersFromState =
decodedTagsArray.some(
(ut) => !filterTags.find((ft) => ut.toLowerCase() === ft.name.toLowerCase()),
) ||
filterTags.some(
(ft) => !decodedTagsArray.find((ut) => ut.toLowerCase() === ft.name.toLowerCase()),
)
if (urlDiffersFromState) {
resetFilterTags()
decodedTagsArray.forEach((urlTag) => {
const match = tags.find((t) => t.name.toLowerCase() === urlTag.toLowerCase())
if (match) addFilterTag(match)
})
}
}, [location, tags, filterTags, addFilterTag, resetFilterTags])
const toggleVisibleLayer = useToggleVisibleLayer()
const allLayers = useLayers()
const initializedRef = useRef(false)
useEffect(() => {
if (initializedRef.current || allLayers.length === 0) return
const params = new URLSearchParams(location.search)
const urlLayersParam = params.get('layers')
if (!urlLayersParam) {
initializedRef.current = true
return
}
const urlLayerNames = urlLayersParam.split(',').filter(Boolean)
const layerNamesToHide = allLayers
.map((l) => l.name)
.filter((name) => !urlLayerNames.includes(name))
layerNamesToHide.forEach((name) => {
const match = allLayers.find((l) => l.name === name)
if (match) toggleVisibleLayer(match)
})
initializedRef.current = true
}, [location, allLayers, toggleVisibleLayer])
return (
<div
className={`tw-h-full ${selectNewItemPosition != null ? 'crosshair-cursor-enabled' : undefined}`}
>
<div className={`tw:h-full ${selectNewItemPosition != null ? 'crosshair-cursor-enabled' : ''}`}>
<Outlet />
<Control position='topLeft' zIndex='1000' absolute>
<SearchControl />
@ -191,7 +273,7 @@ export function UtopiaMapInner({
</Control>
<Control position='bottomLeft' zIndex='999' absolute>
{showFilterControl && <FilterControl />}
{showLayerControl && <LayerControl />}
{showLayerControl && <LayerControl expandLayerControl={expandLayerControl ?? false} />}
{showGratitudeControl && <GratitudeControl />}
</Control>
<TileLayer
@ -206,15 +288,7 @@ export function UtopiaMapInner({
maxClusterRadius={50}
removeOutsideVisibleBounds={false}
>
{Children.toArray(children).map((child) =>
isValidElement<{
setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>
itemFormPopup: ItemFormPopupProps | null
clusterRef: React.MutableRefObject<undefined>
}>(child)
? cloneElement(child, { setItemFormPopup, itemFormPopup, clusterRef })
: child,
)}
{children}
</MarkerClusterGroup>
{geo && (
<GeoJSON
@ -224,7 +298,7 @@ export function UtopiaMapInner({
click: (e) => {
if (selectNewItemPosition) {
e.layer.closePopup()
setMapClicked({ position: e.latlng, setItemFormPopup })
setMapClicked({ position: e.latlng, setItemFormPopup: setPopupForm })
}
},
}}

View File

@ -4,7 +4,7 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable no-case-declarations */
import { useCallback, useReducer, createContext, useContext, useState } from 'react'
import { useCallback, useReducer, createContext, useContext, useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useLayers } from './useLayers'
@ -100,6 +100,28 @@ function useFilterManager(initialTags: Tag[]): {
}
}, initialLayers)
const allLayers = useLayers()
useEffect(() => {
if (allLayers.length === 0) return
const visibleNames = visibleLayers.map((l) => l.name)
const allNames = allLayers.map((l) => l.name)
const params = new URLSearchParams(location.search)
const allVisible =
visibleNames.length === allNames.length &&
visibleNames.every((name) => allNames.includes(name))
if (allVisible) {
params.delete('layers')
} else {
params.set('layers', visibleNames.join(','))
}
navigate(`${location.pathname}?${params.toString()}`, { replace: true })
}, [visibleLayers, allLayers, navigate])
const [visibleGroupTypes, dispatchGroupTypes] = useReducer(
(state: string[], action: ActionType) => {
switch (action.type) {

View File

@ -0,0 +1,34 @@
import { createContext, useContext, useState } from 'react'
import type { PopupFormState } from '#types/PopupFormState'
type UsePopupFormManagerResult = ReturnType<typeof usePopupFormManager>
const PoupFormContext = createContext<UsePopupFormManagerResult>({
popupForm: {} as PopupFormState | null,
setPopupForm: () => {
/* empty function */
},
})
function usePopupFormManager(): {
popupForm: PopupFormState | null
setPopupForm: React.Dispatch<React.SetStateAction<PopupFormState | null>>
} {
const [popupForm, setPopupForm] = useState<PopupFormState | null>(null)
return { popupForm, setPopupForm }
}
interface Props {
children?: React.ReactNode
}
export const PopupFormProvider: React.FunctionComponent<Props> = ({ children }: Props) => (
<PoupFormContext.Provider value={usePopupFormManager()}>{children}</PoupFormContext.Provider>
)
export const usePopupForm = (): UsePopupFormManagerResult => {
const { popupForm, setPopupForm } = useContext(PoupFormContext)
return { popupForm, setPopupForm }
}

View File

@ -15,14 +15,14 @@ import { useUpdateItem } from './useItems'
import { useHasUserPermission } from './usePermissions'
import type { Item } from '#types/Item'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
import type { LayerProps } from '#types/LayerProps'
import type { PopupFormState } from '#types/PopupFormState'
import type { Point } from 'geojson'
import type { LatLng } from 'leaflet'
interface PolygonClickedProps {
position: LatLng
setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
setItemFormPopup: React.Dispatch<React.SetStateAction<PopupFormState | null>>
}
type UseSelectPositionManagerResult = ReturnType<typeof useSelectPositionManager>
@ -60,7 +60,9 @@ function useSelectPositionManager(): {
useEffect(() => {
if (selectPosition != null) {
// selectPosition can be null, Layer or Item
if ('menuIcon' in selectPosition) {
// if selectPosition is a Layer
mapClicked &&
mapClicked.setItemFormPopup({
layer: selectPosition,
@ -69,6 +71,7 @@ function useSelectPositionManager(): {
setSelectPosition(null)
}
if ('text' in selectPosition) {
// if selectPosition is an Item
const position =
mapClicked?.position.lng &&
({

View File

@ -2,8 +2,7 @@ export { UtopiaMap } from './UtopiaMap'
export * from './Layer'
export { Tags } from './Tags'
export * from './Permissions'
export { ItemForm } from './ItemForm'
export { ItemView } from './ItemView'
/*
export { PopupTextAreaInput } from './Subcomponents/ItemPopupComponents/PopupTextAreaInput'
export { PopupStartEndInput } from './Subcomponents/ItemPopupComponents/PopupStartEndInput'
export { PopupTextInput } from './Subcomponents/ItemPopupComponents/PopupTextInput'
@ -11,3 +10,4 @@ export { PopupCheckboxInput } from './Subcomponents/ItemPopupComponents/PopupChe
export { TextView } from './Subcomponents/ItemPopupComponents/TextView'
export { StartEndView } from './Subcomponents/ItemPopupComponents/StartEndView'
export { PopupButton } from './Subcomponents/ItemPopupComponents/PopupButton'
*/

View File

@ -0,0 +1,44 @@
import { describe, it, expect, vi } from 'vitest'
import { linkItem } from './itemFunctions'
const toastErrorMock: (t: string) => void = vi.fn()
const toastSuccessMock: (t: string) => void = vi.fn()
vi.mock('react-toastify', () => ({
toast: {
error: (t: string) => toastErrorMock(t),
success: (t: string) => toastSuccessMock(t),
},
}))
describe('linkItem', () => {
const id = 'some-id'
let updateApi: () => void = vi.fn()
const item = { layer: { api: { updateItem: () => updateApi() } } }
const updateItem = vi.fn()
beforeEach(() => {
updateApi = vi.fn()
vi.clearAllMocks()
})
describe('api rejects', () => {
it('toasts an error', async () => {
updateApi = vi.fn().mockRejectedValue('autsch')
await linkItem(id, item, updateItem)
expect(toastErrorMock).toHaveBeenCalledWith('autsch')
expect(updateItem).not.toHaveBeenCalled()
expect(toastSuccessMock).not.toHaveBeenCalled()
})
})
describe('api resolves', () => {
it('toasts success and calls updateItem()', async () => {
await linkItem(id, item, updateItem)
expect(toastErrorMock).not.toHaveBeenCalled()
expect(updateItem).toHaveBeenCalledTimes(1)
expect(toastSuccessMock).toHaveBeenCalledWith('Item linked')
})
})
})

View File

@ -21,6 +21,7 @@ import { TabsForm } from './Templates/TabsForm'
import type { FormState } from '#types/FormState'
import type { Item } from '#types/Item'
import type { MarkerIcon } from '#types/MarkerIcon'
import type { Tag } from '#types/Tag'
/**
@ -39,12 +40,13 @@ export function ProfileForm() {
telephone: '',
next_appointment: '',
image: '',
marker_icon: '',
marker_icon: {} as MarkerIcon,
offers: [] as Tag[],
needs: [] as Tag[],
relations: [] as Item[],
start: '',
end: '',
openCollectiveSlug: '',
})
const [updatePermission, setUpdatePermission] = useState<boolean>(false)
@ -131,12 +133,13 @@ export function ProfileForm() {
next_appointment: item?.next_appointment ?? '',
image: item?.image ?? '',
// Do we actually mean marker_icon here?
marker_icon: item?.markerIcon ?? '',
marker_icon: item?.markerIcon,
offers,
needs,
relations,
start: item.start ?? '',
end: item.end ?? '',
openCollectiveSlug: item.openCollectiveSlug ?? '',
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item, tags, items])
@ -155,10 +158,10 @@ export function ProfileForm() {
<>
<MapOverlayPage
backdrop
className='tw-mx-4 tw-mt-4 tw-mb-4 tw-overflow-x-hidden tw-w-[calc(100%-32px)] md:tw-w-[calc(50%-32px)] tw-max-w-3xl !tw-left-auto tw-top-0 tw-bottom-0'
className='tw:mx-4 tw:mt-4 tw:mb-4 tw:overflow-x-hidden tw:w-[calc(100%-32px)] tw:md:w-[calc(50%-32px)] tw:max-w-3xl tw:left-auto! tw:top-0 tw:bottom-0'
>
<form
className='tw-h-full'
className='tw:h-full'
onSubmit={(e) => {
e.preventDefault()
void onUpdateItem(
@ -175,7 +178,7 @@ export function ProfileForm() {
)
}}
>
<div className='tw-flex tw-flex-col tw-h-full'>
<div className='tw:flex tw:flex-col tw:h-full'>
<FormHeader item={item} state={state} setState={setState} />
{template === 'onepager' && (
@ -201,9 +204,9 @@ export function ProfileForm() {
></TabsForm>
)}
<div className='tw-mt-4'>
<div className='tw:mt-4 tw:flex-none'>
<button
className={loading ? ' tw-loading tw-btn tw-float-right' : 'tw-btn tw-float-right'}
className={`${loading ? ' tw:loading tw:btn tw:float-right' : 'tw:btn tw:float-right'}`}
type='submit'
style={{
// We could refactor this, it is used several times at different locations

View File

@ -174,10 +174,10 @@ export function ProfileView({ attestationApi }: { attestationApi?: ItemsApi<any>
{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'}`}
className={`tw:p-0! tw:overflow-scroll tw:m-4! tw:md:w-[calc(50%-32px)] tw:w-[calc(100%-32px)] tw:min-w-80 tw:max-w-3xl tw:left-0! tw:sm: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'}>
<HeaderView
api={item.layer?.api}
item={item}

View File

@ -54,11 +54,11 @@ export function ActionButton({
<>
{hasUserPermission(collection, 'update', item) && (
<>
<div className={`tw-absolute tw-right-4 tw-bottom-4 tw-flex tw-flex-col ${customStyle}`}>
<div className={`tw:absolute tw:right-6 tw:bottom-4 tw:flex tw:flex-col ${customStyle}`}>
{triggerItemSelected && (
<button
tabIndex={0}
className='tw-z-500 tw-btn tw-btn-circle tw-shadow'
className='tw:z-500 tw:btn tw:btn-circle tw:shadow'
onClick={() => {
setModalOpen(true)
}}
@ -67,13 +67,13 @@ export function ActionButton({
color: '#fff',
}}
>
<LinkIcon className='tw-h-5 tw-w-5 tw-stroke-[2.5]' />
<LinkIcon className='tw:h-5 tw:w-5 tw:stroke-[2.5]' />
</button>
)}
{triggerAddButton && (
<button
tabIndex={0}
className='tw-z-500 tw-btn tw-btn-circle tw-shadow tw-mt-2'
className='tw:z-500 tw:btn tw:btn-circle tw:shadow tw:mt-2'
onClick={() => {
triggerAddButton()
}}
@ -82,7 +82,7 @@ export function ActionButton({
color: '#fff',
}}
>
<PlusIcon className='tw-w-5 tw-h-5 tw-stroke-[2.5]' />
<PlusIcon className='tw:w-5 tw:h-5 tw:stroke-[2.5]' />
</button>
)}
</div>
@ -90,17 +90,17 @@ export function ActionButton({
title={'Select'}
isOpened={modalOpen}
onClose={() => setModalOpen(false)}
className='tw-w-xl sm:tw-w-2xl tw-min-h-80 tw-bg-base-200'
className='tw:w-xl tw:sm:w-2xl tw:min-h-80 tw:bg-base-200'
>
<TextInput
defaultValue=''
placeholder='🔍 Search'
containerStyle='lg:col-span-2 tw-m-4 '
containerStyle='lg:col-span-2 tw:m-4 '
updateFormValue={(val) => {
setSearch(val)
}}
></TextInput>
<div className='tw-grid tw-grid-cols-1 sm:tw-grid-cols-2'>
<div className='tw:grid tw:grid-cols-1 tw:sm:grid-cols-2'>
{filterdItems
.filter((item) => {
return search === ''
@ -110,7 +110,7 @@ export function ActionButton({
.map((i) => (
<div
key={i.id}
className='tw-cursor-pointer tw-card tw-border-[1px] tw-border-base-300 tw-card-body tw-shadow-xl tw-bg-base-100 tw-text-base-content tw-mx-4 tw-p-4 tw-mb-4 tw-h-fit'
className='tw:cursor-pointer tw:card tw:border-[1px] tw:border-base-300 tw:card-body tw:shadow-xl tw:bg-base-100 tw:text-base-content tw:mx-4 tw:p-4 tw:mb-4 tw:h-fit'
onClick={() => {
triggerItemSelected(i.id)
setModalOpen(false)

View File

@ -166,28 +166,28 @@ export const AvatarWidget: React.FC<AvatarWidgetProps> = ({ avatar, setAvatar })
<input
type='file'
accept='image/*'
className='tw-file-input tw-w-full tw-max-w-xs'
className='tw:file-input tw:w-full tw:max-w-xs'
onChange={onImageChange}
/>
<div className='button tw-btn tw-btn-lg tw-btn-circle tw-animate-none'>
<ArrowUpTrayIcon className='tw-w-6 tw-h-6' />
<div className='button tw:btn tw:btn-lg tw:btn-circle tw:animate-none'>
<ArrowUpTrayIcon className='tw:w-6 tw:h-6' />
</div>
{avatar ? (
<div className='tw-h-20 tw-w-20'>
<div className='tw:h-20 tw:w-20'>
<img
src={appState.assetsApi.url + avatar}
className='tw-h-20 tw-w-20 tw-rounded-full'
className='tw:h-20 tw:w-20 tw:rounded-full'
/>
</div>
) : (
<div className='tw-h-20 tw-w-20'>
<img src={UserSVG} className='tw-rounded-full'></img>
<div className='tw:h-20 tw:w-20'>
<img src={UserSVG} className='tw:rounded-full'></img>
</div>
)}
</label>
) : (
<div className='tw-w-20 tw-flex tw-items-center tw-justify-center'>
<span className='tw-loading tw-loading-spinner'></span>
<div className='tw:w-20 tw:flex tw:items-center tw:justify-center'>
<span className='tw:loading tw:loading-spinner'></span>
</div>
)}
<DialogModal
@ -203,7 +203,7 @@ export const AvatarWidget: React.FC<AvatarWidgetProps> = ({ avatar, setAvatar })
<img src={image} ref={imgRef} onLoad={onImageLoad} />
</ReactCrop>
<button
className={'tw-btn tw-btn-primary'}
className={'tw:btn tw:btn-primary'}
onClick={() => {
setCropping(true)
setCropModalOpen(false)

View File

@ -38,7 +38,7 @@ export const ColorPicker = ({ color, onChange, className }) => {
<div className='swatch' style={{ backgroundColor: color }} onClick={() => toggle(true)} />
{isOpen && (
<div className='popover tw-z-[10000]' ref={popover}>
<div className='popover tw:z-10000' ref={popover}>
<HexColorPicker color={color} onChange={onChange} onClick={() => toggle(false)} />
</div>
)}

View File

@ -12,11 +12,11 @@ export const ContactInfoForm = ({
setState: React.Dispatch<React.SetStateAction<any>>
}) => {
return (
<div className='tw-mt-4 tw-space-y-4'>
<div className='tw:mt-4 tw:space-y-4'>
<div>
<label
htmlFor='email'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
className='tw:block tw:text-sm tw:font-medium tw:text-gray-500 tw:mb-1'
>
Email-Adresse (Kontakt):
</label>
@ -37,7 +37,7 @@ export const ContactInfoForm = ({
<div>
<label
htmlFor='telephone'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
className='tw:block tw:text-sm tw:font-medium tw:text-gray-500 tw:mb-1'
>
Telefonnummer (Kontakt):
</label>

View File

@ -24,33 +24,33 @@ export const ContactInfoView = ({ item, heading }: { item: Item; heading: string
}, [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'>{heading}</h2>
<div className='tw-mt-4 tw-flex tw-items-center'>
<div className='tw:bg-base-200 tw:mb-6 tw:mt-6 tw:p-6'>
<h2 className='tw:text-lg tw:font-semibold'>{heading}</h2>
<div className='tw:mt-4 tw:flex tw:items-center'>
{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'>
<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={appState.assetsApi.url + profileOwner?.image}
alt={profileOwner?.name}
className='tw-w-full tw-h-full tw-object-cover'
className='tw:w-full tw:h-full tw:object-cover'
/>
</div>
</div>
</div>
</ConditionalLink>
)}
<div className='tw-text-sm tw-flex-grow'>
<p className='tw-font-semibold'>{profileOwner?.name}</p>
<div className='tw:text-sm tw:grow'>
<p className='tw:font-semibold'>{profileOwner?.name}</p>
{item.contact && (
<p>
<a
href={`mailto:${item.contact}`}
className='tw-mt-2 tw-text-green-500 tw-inline-flex tw-items-center'
className='tw:mt-2 tw:text-green-500 tw:inline-flex tw:items-center'
>
<EnvelopeIcon className='tw-w-4 tw-h-4 tw-mr-1' />
<EnvelopeIcon className='tw:w-4 tw:h-4 tw:mr-1' />
{item.contact}
</a>
</p>
@ -59,9 +59,9 @@ export const ContactInfoView = ({ item, heading }: { item: Item; heading: string
<p>
<a
href={`tel:${item.telephone}`}
className='tw-mt-2 tw-text-green-500 tw-inline-flex tw-items-center tw-whitespace-nowrap'
className='tw:mt-2 tw:text-green-500 tw:inline-flex tw:items-center tw:whitespace-nowrap'
>
<PhoneIcon className='tw-w-4 tw-h-4 tw-mr-1' />
<PhoneIcon className='tw:w-4 tw:h-4 tw:mr-1' />
{item.telephone}
</a>
</p>

View File

@ -0,0 +1,38 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { TextInput } from '#components/Input'
import type { FormState } from '#types/FormState'
export const CrowdfundingForm = ({
state,
setState,
}: {
state: FormState
setState: React.Dispatch<React.SetStateAction<any>>
}) => {
return (
<div className='tw:mt-4 tw:space-y-4'>
<div>
<label
htmlFor='OpenCollectiveSlug'
className='tw:block tw:text-sm tw:font-medium tw:text-gray-500 tw:mb-1'
>
Open Collective Slug:
</label>
<TextInput
placeholder='Open Collective Slug'
type='text'
required={false}
defaultValue={state.openCollectiveSlug}
updateFormValue={(v) =>
setState((prevState) => ({
...prevState,
openCollectiveSlug: v,
}))
}
/>
</div>
</div>
)
}

View File

@ -0,0 +1,192 @@
import axios from 'axios'
import { useState, useEffect } from 'react'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import type { Item } from '#types/Item'
interface AccountData {
account: {
name: string
type: string
stats: {
balance: {
valueInCents: number
currency: string
} | null
totalAmountReceived: {
valueInCents: number
currency: string
}
totalAmountSpent: {
valueInCents: number
currency: string
}
contributionsCount: number
contributorsCount: number
}
}
}
interface GraphQLResponse<T> {
data?: T
errors?: { message: string }[]
}
const GET_TRANSACTIONS = `
query GetAccountStats($slug: String!) {
account(slug: $slug) {
name
type
stats {
balance {
valueInCents
currency
}
totalAmountReceived(net: true) {
valueInCents
currency
}
totalAmountSpent {
valueInCents
currency
}
contributionsCount
contributorsCount
}
}
}
`
const formatCurrency = (valueInCents: number, currency: string) => {
const value = valueInCents / 100
const options: Intl.NumberFormatOptions = {
style: 'currency',
currency,
...(Math.abs(value) >= 1000 ? { minimumFractionDigits: 0, maximumFractionDigits: 0 } : {}),
}
return new Intl.NumberFormat('de-DE', options).format(value)
}
export const CrowdfundingView = ({ item }: { item: Item }) => {
// Hier wird slug aus dem Item extrahiert.
const slug = item.openCollectiveSlug
const appState = useAppState()
const token = appState.openCollectiveApiKey
const graphqlClient = axios.create({
baseURL: 'https://api.opencollective.com/graphql/v2',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
})
const [data, setData] = useState<AccountData | null>(null)
const [loading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchData = async () => {
setLoading(true)
setError(null)
try {
const response = await graphqlClient.post<GraphQLResponse<AccountData>>('', {
query: GET_TRANSACTIONS,
variables: { slug },
})
if (response.data.errors?.length) {
setError(response.data.errors[0].message)
} else {
setData(response.data.data ?? null)
}
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message)
} else {
throw err
}
}
setLoading(false)
}
if (slug) {
void fetchData()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [slug])
if (!slug) return null
if (loading)
return (
<div className='tw:flex tw:justify-center'>
<span className='tw:loading tw:loading-spinner tw:loading-lg tw:text-neutral-content'></span>
</div>
)
if (error) {
return <p className='tw:text-center tw:text-lg tw:text-red-500'>Error: {error}</p>
}
if (!data?.account) {
return (
<p className='tw:text-center tw:text-lg tw:text-red-500'>
No data available for this account.
</p>
)
}
const { stats } = data.account
const balanceValueInCents = stats.balance?.valueInCents ?? 0
const currency = stats.balance?.currency ?? 'USD'
const currentBalance = balanceValueInCents
return (
<div className='tw:mx-6 tw:mb-6'>
<div className='tw:card tw:bg-base-200 tw:w-fit tw:max-w-full tw:shadow'>
<div className='tw:stats tw:bg-base-200 tw:stats-horizontal tw:rounded-b-none'>
<div className='tw:stat tw:p-3'>
<div className='tw:stat-title'>Current Balance</div>
<div className='tw:stat-value tw:text-xl lg:tw:text-3xl'>
{formatCurrency(currentBalance, currency)}
</div>
</div>
<div className='tw:stat tw:p-3'>
<div className='tw:stat-title'>Received</div>
<div className='tw:stat-value tw:text-green-500 tw:text-xl lg:tw:text-3xl'>
{formatCurrency(stats.totalAmountReceived.valueInCents, currency)}
</div>
</div>
<div className='tw:stat tw:p-3'>
<div className='tw:stat-title'>Spent</div>
<div className='tw:stat-value tw:text-red-500 tw:text-xl lg:tw:text-3xl'>
{formatCurrency(stats.totalAmountReceived.valueInCents - currentBalance, currency)}
</div>
</div>
</div>
<hr className='tw:border-1 tw:border-current/10 tw:border-dashed'></hr>
<div className='tw:m-4 tw:items-center'>
<a href={`https://opencollective.com/${slug}/donate`} target='_blank' rel='noreferrer'>
<button className='tw:btn tw:btn-sm tw:btn-primary tw:float-right tw:ml-4'>
Donate
</button>
</a>
<div className='tw:flex-1 tw:mr-4'>
Support{' '}
<a
className='tw:font-bold'
href={`https://opencollective.com/${slug}`}
target='_blank'
rel='noreferrer'
>
{data.account.name}
</a>{' '}
on <span className='tw:font-bold'>Open&nbsp;Collective</span>
</div>
</div>
</div>
</div>
)
}

View File

@ -10,50 +10,54 @@ import { ColorPicker } from './ColorPicker'
export const FormHeader = ({ item, state, setState }) => {
return (
<div className='tw-flex'>
<AvatarWidget
avatar={state.image}
setAvatar={(i) =>
setState((prevState) => ({
...prevState,
image: i,
}))
}
/>
<ColorPicker
color={state.color}
onChange={(c) =>
setState((prevState) => ({
...prevState,
color: c,
}))
}
className={'-tw-left-6 tw-top-14 -tw-mr-6'}
/>
<div className='tw-grow tw-mr-4'>
<TextInput
placeholder='Name'
defaultValue={item?.name ? item.name : ''}
updateFormValue={(v) =>
<div className='tw:flex-none'>
<div className='tw:flex'>
<AvatarWidget
avatar={state.image}
setAvatar={(i) =>
setState((prevState) => ({
...prevState,
name: v,
image: i,
}))
}
containerStyle='tw-grow tw-input-md'
/>
<TextInput
placeholder='Subtitle'
required={false}
defaultValue={item?.subname ? item.subname : ''}
updateFormValue={(v) =>
<ColorPicker
color={state.color}
onChange={(c) =>
setState((prevState) => ({
...prevState,
subname: v,
color: c,
}))
}
containerStyle='tw-grow tw-input-sm tw-px-4 tw-mt-1'
className={'tw:-left-6 tw:top-14 tw:-mr-6'}
/>
<div className='tw:grow tw:mr-4 tw:pt-1'>
<TextInput
placeholder='Name'
defaultValue={item?.name ? item.name : ''}
updateFormValue={(v) =>
setState((prevState) => ({
...prevState,
name: v,
}))
}
containerStyle='tw:grow tw:px-4'
inputStyle='tw:input-md'
/>
<TextInput
placeholder='Subtitle'
required={false}
defaultValue={item?.subname ? item.subname : ''}
updateFormValue={(v) =>
setState((prevState) => ({
...prevState,
subname: v,
}))
}
containerStyle='tw:grow tw:px-4 tw:mt-1'
inputStyle='tw:input-sm'
/>
</div>
</div>
</div>
)

View File

@ -9,26 +9,25 @@ import type { Item } from '#types/Item'
export const GalleryView = ({ item }: { item: Item }) => {
const [index, setIndex] = useState(-1)
const appState = useAppState()
const images = item.gallery?.map((i, j) => {
return {
const images =
item.gallery?.map((i, j) => ({
src: appState.assetsApi.url + `${i.directus_files_id.id}.jpg`,
width: i.directus_files_id.width,
height: i.directus_files_id.height,
index: j,
}
})
})) ?? []
if (!images) throw new Error('GalleryView: images is undefined')
if (images.length > 0)
return (
<div className='tw:mx-6 tw:mb-6'>
<RowsPhotoAlbum
photos={images}
targetRowHeight={150}
onClick={({ index: current }) => setIndex(current)}
/>
return (
<div className='tw-mx-6 tw-mb-6'>
<RowsPhotoAlbum
photos={images}
targetRowHeight={150}
onClick={({ index: current }) => setIndex(current)}
/>
<ReactLightbox index={index} slides={images} open={index >= 0} close={() => setIndex(-1)} />
</div>
)
<ReactLightbox index={index} slides={images} open={index >= 0} close={() => setIndex(-1)} />
</div>
)
else return <></>
}

View File

@ -11,18 +11,18 @@ export const GroupSubHeaderView = ({
shareBaseUrl: string
platforms?: string[]
}) => (
<div className='tw-px-6'>
<div className='tw-float-left tw-mt-2 tw-mb-4 tw-flex tw-items-center'>
<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'>
<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'>
{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'>
<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>

View File

@ -12,7 +12,7 @@ interface groupType {
groupTypes_id: {
name: string
color: string
image: string
image: { id: string }
markerIcon: string
}
}
@ -30,10 +30,10 @@ export const GroupSubheaderForm = ({
groupTypes?: groupType[]
}) => {
useEffect(() => {
if (groupTypes && groupStates) {
if (groupTypes && groupStates && state.name !== '') {
const groupType = groupTypes.find((gt) => gt.groupTypes_id.name === state.group_type)
const customImage = !groupTypes.some(
(gt) => gt.groupTypes_id.image === state.image || !state.image,
(gt) => gt.groupTypes_id.image.id === state.image || !state.image,
)
setState((prevState) => ({
...prevState,
@ -41,7 +41,7 @@ export const GroupSubheaderForm = ({
marker_icon: groupType?.groupTypes_id.markerIcon || groupTypes[0].groupTypes_id.markerIcon,
image: customImage
? state.image
: groupType?.groupTypes_id.image || groupTypes[0].groupTypes_id.image,
: groupType?.groupTypes_id.image.id || groupTypes[0].groupTypes_id.image.id,
status: state.status || groupStates[0],
group_type: state.group_type || groupTypes[0].groupTypes_id.name,
}))
@ -51,11 +51,11 @@ export const GroupSubheaderForm = ({
}, [state.group_type, groupTypes])
return (
<div className='tw-grid tw-grid-cols-1 md:tw-grid-cols-2 tw-gap-6'>
<div className='tw:grid tw:grid-cols-1 tw:md:grid-cols-2 tw:gap-6'>
<div>
<label
htmlFor='status'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
className='tw:block tw:text-sm tw:font-medium tw:text-gray-500 tw:mb-1'
>
Gruppenstatus:
</label>
@ -74,7 +74,7 @@ export const GroupSubheaderForm = ({
<div>
<label
htmlFor='groupType'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
className='tw:block tw:text-sm tw:font-medium tw:text-gray-500 tw:mb-1'
>
Gruppenart:
</label>

View File

@ -33,47 +33,47 @@ export function LinkedItemsHeaderView({
return (
<>
<div className='tw-flex tw-flex-row'>
<div className={'tw-grow tw-max-w-[calc(100%-60px)] }'}>
<div className='tw:flex tw:flex-row'>
<div className={'tw:grow tw:max-w-[calc(100%-60px)] }'}>
<div className='flex items-center'>
{avatar && (
<img
className={'tw-w-10 tw-inline tw-rounded-full'}
className={'tw:w-10 tw:inline tw:rounded-full'}
src={avatar}
alt={item.name + ' logo'}
/>
)}
<div className={`${avatar ? 'tw-ml-2' : ''} tw-overflow-hidden`}>
<div className={'tw-text-xl tw-font-semibold tw-truncate'}>{title}</div>
<div className={`${avatar ? 'tw:ml-2' : ''} tw:overflow-hidden`}>
<div className={'tw:text-xl tw:font-semibold tw:truncate'}>{title}</div>
{subtitle && (
<div className='tw-text-xs tw-truncate tw-text-gray-500 '>{subtitle}</div>
<div className='tw:text-xs tw:truncate tw:text-gray-500 '>{subtitle}</div>
)}
</div>
</div>
</div>
<div className='tw-col-span-1' onClick={(e) => e.stopPropagation()}>
<div className='tw:col-span-1' onClick={(e) => e.stopPropagation()}>
{unlinkPermission && (
<div className='tw-dropdown tw-dropdown-bottom'>
<div className='tw:dropdown tw:dropdown-bottom'>
<label
tabIndex={0}
className=' tw-btn tw-m-1 tw-leading-3 tw-border-none tw-min-h-0 tw-h-6'
className=' tw:btn tw:m-1 tw:leading-3 tw:border-none tw:min-h-0 tw:h-6'
>
<EllipsisVerticalIcon className='tw-h-5 tw-w-5' />
<EllipsisVerticalIcon className='tw:h-5 tw:w-5' />
</label>
<ul
tabIndex={0}
className='tw-dropdown-content tw-menu tw-p-2 tw-shadow tw-bg-base-100 tw-rounded-box tw-z-1000'
className='tw:dropdown-content tw:menu tw:p-2 tw:shadow tw:bg-base-100 tw:rounded-box tw:z-1000'
>
{true && (
<li>
<a
className='tw-cursor-pointer !tw-text-error'
className='tw:cursor-pointer tw:text-error!'
onClick={() => unlinkCallback(item.id)}
>
{loading ? (
<span className='tw-loading tw-loading-spinner tw-loading-sm'></span>
<span className='tw:loading tw:loading-spinner tw:loading-sm'></span>
) : (
<LinkSlashIcon className='tw-h-5 tw-w-5 tw-stroke-[3]' />
<LinkSlashIcon className='tw:h-5 tw:w-5 tw:stroke-3' />
)}
</a>
</li>

View File

@ -8,16 +8,16 @@ export const MarkdownHint = () => {
<div
onClick={() => setExpended(true)}
title='Markdown is supported'
className='flex tw-flex-row tw-text-gray-400 tw-cursor-pointer tw-items-center'
className='flex tw:flex-row tw:text-gray-400 tw:cursor-pointer tw:items-center'
>
<img src={MarkdownSVG} alt='Markdown' className='octicon octicon-markdown tw-gray-400' />
<img src={MarkdownSVG} alt='Markdown' className='octicon octicon-markdown tw:gray-400' />
{expended && (
<a
href='https://www.markdownguide.org/cheat-sheet/#basic-syntax'
target='_blank'
rel='noreferrer'
>
<span className='Button-label tw-ml-1'>Markdown is support</span>{' '}
<span className='Button-label tw:ml-1'>Markdown is support</span>{' '}
</a>
)}
</div>

View File

@ -21,16 +21,16 @@ export function PlusButton({
return (
<>
{hasUserPermission(collection, 'create', undefined, layer) && (
<div className='tw-dropdown tw-dropdown-top tw-dropdown-end tw-dropdown-hover tw-z-3000 tw-absolute tw-right-4 tw-bottom-4'>
<div className='tw:dropdown tw:dropdown-top tw:dropdown-end tw:dropdown-hover tw:z-3000 tw:absolute tw:right-4 tw:bottom-4'>
<button
tabIndex={0}
className='tw-z-500 tw-btn tw-btn-circle tw-shadow'
className='tw:z-500 tw:btn tw:btn-circle tw:shadow'
onClick={() => {
triggerAction()
}}
style={{ backgroundColor: color, color: '#fff' }}
>
<PlusIcon className='tw-w-5 tw-h-5 tw-stroke-[2.5]' />
<PlusIcon className='tw:w-5 tw:h-5 tw:stroke-[2.5]' />
</button>
</div>
)}

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { PopupStartEndInput } from '#components/Map'
import { PopupStartEndInput } from '#components/Map/Subcomponents/ItemPopupComponents'
import type { Item } from '#types/Item'

View File

@ -1,10 +1,10 @@
import { StartEndView } from '#components/Map'
import { StartEndView } from '#components/Map/Subcomponents/ItemPopupComponents'
import type { Item } from '#types/Item'
export const ProfileStartEndView = ({ item }: { item: Item }) => {
return (
<div className='tw-mt-2 tw-px-6 tw-max-w-xs'>
<div className='tw:mt-2 tw:px-6 tw:max-w-xs'>
<StartEndView item={item}></StartEndView>
</div>
)

View File

@ -4,7 +4,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useState } from 'react'
import { TextAreaInput } from '#components/Input'
import { RichTextEditor } from '#components/Input/RichTextEditor'
import { MarkdownHint } from './MarkdownHint'
@ -37,17 +37,17 @@ export const ProfileTextForm = ({
}, [dataField])
return (
<div className='tw-h-full tw-flex tw-flex-col tw-mt-4'>
<div className='tw-flex tw-justify-between tw-items-center'>
<div className='tw:h-full tw:flex tw:flex-col tw:mt-4'>
<div className='tw:flex tw:justify-between tw:items-center'>
<label
htmlFor='nextAppointment'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
className='tw:block tw:text-sm tw:font-medium tw:text-gray-500 tw:mb-1'
>
{heading || 'Text'}:
</label>
<MarkdownHint />
</div>
<TextAreaInput
<RichTextEditor
placeholder={'...'}
// eslint-disable-next-line security/detect-object-injection
defaultValue={state[field]}
@ -57,9 +57,9 @@ export const ProfileTextForm = ({
[field]: v,
}))
}
labelStyle={hideInputLabel ? 'tw-hidden' : ''}
containerStyle={size === 'full' ? 'tw-grow tw-h-full' : ''}
inputStyle={size === 'full' ? 'tw-h-full' : 'tw-h-24'}
labelStyle={hideInputLabel ? 'tw:hidden' : ''}
containerStyle={size === 'full' ? 'tw:grow tw:h-full' : ''}
inputStyle={size === 'full' ? 'tw:h-full' : 'tw:h-24'}
required={required}
/>
</div>

View File

@ -1,6 +1,6 @@
import { get } from 'radash'
import { TextView } from '#components/Map'
import { TextView } from '#components/Map/Subcomponents/ItemPopupComponents'
import type { Item } from '#types/Item'
@ -20,11 +20,11 @@ export const ProfileTextView = ({
const parsedText = typeof text !== 'string' ? '' : text
return (
<div className='tw-my-10 tw-mt-2 tw-px-6'>
<div className='tw:my-10 tw:mt-2 tw:px-6'>
{!(text === '' && hideWhenEmpty) && (
<h2 className='tw-text-lg tw-font-semibold'>{heading}</h2>
<h2 className='tw:text-lg tw:font-semibold'>{heading}</h2>
)}
<div className='tw-mt-2 tw-text-sm'>
<div className='tw:mt-2 tw:text-sm'>
<TextView itemId={item.id} rawText={parsedText} />
</div>
</div>

View File

@ -1,15 +1,15 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// eslint-disable-next-line react/prop-types
const RelationCard = ({ title, description, imageSrc }) => (
<div className={`tw-mb-6 ${imageSrc ? 'md:tw-flex md:tw-space-x-4' : ''}`}>
<div className={`tw:mb-6 ${imageSrc ? 'tw:md:flex tw:md:space-x-4' : ''}`}>
{imageSrc && (
<div className='md:tw-w-1/2 tw-mb-4 md:tw-mb-0'>
<img src={imageSrc} alt={title} className='tw-w-full tw-h-32 tw-object-cover' />
<div className='tw:md:w-1/2 tw:mb-4 tw:md:mb-0'>
<img src={imageSrc} alt={title} className='tw:w-full tw:h-32 tw:object-cover' />
</div>
)}
<div className={imageSrc ? 'md:tw-w-1/2' : 'tw-w-full'}>
<h3 className='tw-text-lg tw-font-semibold'>{title}</h3>
<p className='tw-mt-2 tw-text-sm tw-text-gray-600'>{description}</p>
<div className={imageSrc ? 'tw:md:w-1/2' : 'tw:w-full'}>
<h3 className='tw:text-lg tw:font-semibold'>{title}</h3>
<p className='tw:mt-2 tw:text-sm tw:text-gray-600'>{description}</p>
</div>
</div>
)

View File

@ -27,7 +27,7 @@ const SocialShareBar = ({
})
}
return (
<div className='tw-flex tw-place-content-end tw-justify-end tw-space-x-2 tw-grow tw-min-w-fit tw-pl-2'>
<div className='tw:flex tw:place-content-end tw:justify-end tw:space-x-2 tw:grow tw:min-w-fit tw:pl-2'>
{platforms.map((platform) => (
<SocialShareButton key={platform} platform={platform} url={url} title={title} />
))}
@ -36,7 +36,7 @@ const SocialShareBar = ({
href={`mailto:?subject=${title}&body=${url}`}
target='_blank'
rel='noopener noreferrer'
className='tw-w-8 tw-h-8 tw-mt-2 tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-white hover:tw-cursor-pointer'
className='tw:w-8 tw:h-8 tw:mt-2 tw:rounded-full tw:flex tw:items-center tw:justify-center tw:text-white tw:hover:cursor-pointer'
style={{
color: 'white',
backgroundColor: '#444',
@ -44,13 +44,13 @@ const SocialShareBar = ({
onClick={() => copyLink()}
title='share link via email'
>
<img src={ChevronSVG} alt='\/' className='tw-h-4 tw-w-4' />
<img src={ChevronSVG} alt='\/' className='tw:h-4 tw:w-4' />
</a>
)}
{platforms.includes('clipboard') && (
<div
rel='noopener noreferrer'
className='tw-w-8 tw-h-8 tw-mt-2 tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-white hover:tw-cursor-pointer'
className='tw:w-8 tw:h-8 tw:mt-2 tw:rounded-full tw:flex tw:items-center tw:justify-center tw:text-white tw:hover:cursor-pointer'
style={{
color: 'white',
backgroundColor: '#888',
@ -58,7 +58,7 @@ const SocialShareBar = ({
onClick={() => copyLink()}
title='copy Link'
>
<img src={ClipboardSVG} className='tw-w-5' />
<img src={ClipboardSVG} className='tw:w-5' />
</div>
)}
</div>

View File

@ -71,14 +71,14 @@ const SocialShareButton = ({
href={finalShareUrl}
target='_blank'
rel='noopener noreferrer'
className='tw-w-8 tw-h-8 tw-mt-2 tw-rounded-full tw-flex tw-items-center tw-justify-center tw-text-white'
className='tw:w-8 tw:h-8 tw:mt-2 tw:rounded-full tw:flex tw:items-center tw:justify-center tw:text-white'
style={{
color: 'white',
backgroundColor: bgColor,
}}
title={`share link on ${platform}`}
>
{cloneElement(icon, { className: 'tw-w-4 tw-h-4 tw-fill-current' })}
{cloneElement(icon, { className: 'tw:w-4 tw:h-4 tw:fill-current' })}
</a>
)
}

View File

@ -95,7 +95,7 @@ export const TagsWidget = ({ placeholder, containerStyle, defaultTags, onUpdate
onKeyDown,
onKeyUp,
onChange,
className: 'tw-bg-transparent tw-w-fit tw-mt-5 tw-h-fit',
className: 'tw:bg-transparent tw:w-fit tw:mt-5 tw:h-fit',
}
/* eslint-disable react/prop-types */
@ -107,18 +107,18 @@ export const TagsWidget = ({ placeholder, containerStyle, defaultTags, onUpdate
setFocusInput(false)
}, 200)
}}
className={`tw-input tw-input-bordered tw-cursor-text ${containerStyle}`}
className={`tw:textarea tw:cursor-text ${containerStyle}`}
>
<div className='tw-flex tw-flex-wrap tw-h-fit'>
<div className='tw:flex tw:flex-wrap tw:h-fit'>
{defaultTags.map((tag) => (
<div
key={tag.name}
className='tw-rounded-2xl tw-text-white tw-p-2 tw-px-4 tw-shadow-xl tw-card tw-mt-3 tw-mr-4'
className='tw:rounded-2xl tw:text-white tw:p-2 tw:px-4 tw:shadow-xl tw:card tw:mt-3 tw:mr-4'
style={{ backgroundColor: tag.color ? tag.color : '#666' }}
>
<div className='tw-card-actions tw-justify-end'>
<div className='tw:card-actions tw:justify-end'>
<label
className='tw-btn tw-btn-xs tw-btn-circle tw-absolute tw--right-2 tw--top-2 tw-bg-white tw-text-gray-600'
className='tw:btn tw:btn-xs tw:btn-circle tw:absolute tw:-right-2 tw:-top-2 tw:bg-white tw:text-gray-600'
onClick={() => deleteTag(tag)}
>

View File

@ -2,6 +2,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ContactInfoForm } from '#components/Profile/Subcomponents/ContactInfoForm'
import { CrowdfundingForm } from '#components/Profile/Subcomponents/CrowdfundingForm'
import { GroupSubheaderForm } from '#components/Profile/Subcomponents/GroupSubheaderForm'
import { ProfileStartEndForm } from '#components/Profile/Subcomponents/ProfileStartEndForm'
import { ProfileTextForm } from '#components/Profile/Subcomponents/ProfileTextForm'
@ -14,6 +15,7 @@ const componentMap = {
texts: ProfileTextForm,
contactInfos: ContactInfoForm,
startEnd: ProfileStartEndForm,
crowdfundings: CrowdfundingForm,
// weitere Komponenten hier
}
@ -27,7 +29,7 @@ export const FlexForm = ({
item: Item
}) => {
return (
<div className='tw-mt-6 tw-flex tw-flex-col tw-h-full'>
<div className='tw:mt-6 tw:flex tw:flex-col tw:h-full'>
{item.layer?.itemType.profileTemplate.map((templateItem) => {
const TemplateComponent = componentMap[templateItem.collection]
return TemplateComponent ? (
@ -39,7 +41,9 @@ export const FlexForm = ({
{...templateItem.item}
/>
) : (
<div key={templateItem.id}>Component not found</div>
<div className='tw:mt-2' key={templateItem.id}>
{templateItem.collection} form not found
</div>
)
})}
</div>

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ContactInfoView } from '#components/Profile/Subcomponents/ContactInfoView'
import { CrowdfundingView } from '#components/Profile/Subcomponents/CrowdfundingView'
import { GalleryView } from '#components/Profile/Subcomponents/GalleryView'
import { GroupSubHeaderView } from '#components/Profile/Subcomponents/GroupSubHeaderView'
import { ProfileStartEndView } from '#components/Profile/Subcomponents/ProfileStartEndView'
@ -15,14 +16,13 @@ const componentMap = {
contactInfos: ContactInfoView,
startEnd: ProfileStartEndView,
gallery: GalleryView,
crowdfundings: CrowdfundingView,
// weitere Komponenten hier
}
export const FlexView = ({ item }: { item: Item }) => {
// eslint-disable-next-line no-console
console.log(item)
return (
<div className='tw-h-full tw-overflow-y-auto fade'>
<div className='tw:h-full tw:overflow-y-auto fade'>
{item.layer?.itemType.profileTemplate.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(templateItem: { collection: string | number; id: Key | null | undefined; item: any }) => {
@ -30,7 +30,9 @@ export const FlexView = ({ item }: { item: Item }) => {
return TemplateComponent ? (
<TemplateComponent key={templateItem.id} item={item} {...templateItem.item} />
) : (
<div key={templateItem.id}>Component not found</div>
<div className='tw:mx-6 tw:mb-6' key={templateItem.id}>
{templateItem.collection} view not found
</div>
)
},
)}

Some files were not shown because too many files have changed in this diff Show More