mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2026-03-01 12:44:17 +00:00
Merge branch 'main' into fix-sourcemap
This commit is contained in:
commit
e4e7965e6a
@ -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
|
||||
|
||||
1349
examples/2-static-layers/package-lock.json
generated
1349
examples/2-static-layers/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
1
examples/2-static-layers/public/calendar.svg
Normal file
1
examples/2-static-layers/public/calendar.svg
Normal 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 |
1
examples/2-static-layers/public/point.svg
Normal file
1
examples/2-static-layers/public/point.svg
Normal 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 |
@ -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
32
examples/README.md
Normal 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
1336
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -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",
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
// eslint-disable-next-line import/no-commonjs
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -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 <></>
|
||||
}
|
||||
|
||||
@ -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()}
|
||||
/>
|
||||
|
||||
119
src/Components/AppShell/UserControl.tsx
Normal file
119
src/Components/AppShell/UserControl.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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>({
|
||||
|
||||
12
src/Components/AppShell/hooks/useTheme.tsx
Normal file
12
src/Components/AppShell/hooks/useTheme.tsx
Normal 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])
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 ></button> */}{' '}
|
||||
{/** <button className='tw:btn tw:btn-xs tw:btn-neutral tw:w-fit tw:self-center tw:mt-1'>Next ></button> */}{' '}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@ -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)}>
|
||||
|
||||
@ -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}>
|
||||
|
||||
65
src/Components/Input/RichTextEditor.tsx
Normal file
65
src/Components/Input/RichTextEditor.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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=""
|
||||
/>
|
||||
|
||||
@ -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"
|
||||
|
||||
8
src/Components/Item/PopupForm.tsx
Normal file
8
src/Components/Item/PopupForm.tsx
Normal 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>
|
||||
}
|
||||
182
src/Components/Item/PopupView.tsx
Normal file
182
src/Components/Item/PopupView.tsx
Normal 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>
|
||||
)
|
||||
})
|
||||
}
|
||||
7
src/Components/Item/TemplateItemContext.ts
Normal file
7
src/Components/Item/TemplateItemContext.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
import type { Item } from '#types/Item'
|
||||
|
||||
const ItemContext = createContext<Item | undefined>(undefined)
|
||||
|
||||
export default ItemContext
|
||||
22
src/Components/Item/index.tsx
Normal file
22
src/Components/Item/index.tsx
Normal 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)
|
||||
15
src/Components/Item/templateify.tsx
Normal file
15
src/Components/Item/templateify.tsx
Normal 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'>>
|
||||
}
|
||||
@ -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'
|
||||
@ -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'
|
||||
@ -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
|
||||
}
|
||||
|
||||
22
src/Components/Map/LayerContext.ts
Normal file
22
src/Components/Map/LayerContext.ts
Normal 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
|
||||
@ -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()
|
||||
|
||||
0
src/Components/Map/ProfileView.tsx
Normal file
0
src/Components/Map/ProfileView.tsx
Normal 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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
}}
|
||||
|
||||
@ -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'}` }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -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)}
|
||||
>
|
||||
✕
|
||||
|
||||
@ -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>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -23,7 +23,7 @@ export const PopupTextInput = ({
|
||||
placeholder={placeholder}
|
||||
inputStyle={style}
|
||||
type='text'
|
||||
containerStyle={'tw-mt-4'}
|
||||
containerStyle={'tw:mt-4'}
|
||||
></TextInput>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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() : ''}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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'
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 <></>
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 })
|
||||
}
|
||||
},
|
||||
}}
|
||||
|
||||
@ -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) {
|
||||
|
||||
34
src/Components/Map/hooks/usePopupForm.tsx
Normal file
34
src/Components/Map/hooks/usePopupForm.tsx
Normal 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 }
|
||||
}
|
||||
@ -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 &&
|
||||
({
|
||||
|
||||
@ -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'
|
||||
*/
|
||||
|
||||
44
src/Components/Profile/ItemFunctions.spec.tsx
Normal file
44
src/Components/Profile/ItemFunctions.spec.tsx
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
38
src/Components/Profile/Subcomponents/CrowdfundingForm.tsx
Normal file
38
src/Components/Profile/Subcomponents/CrowdfundingForm.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
192
src/Components/Profile/Subcomponents/CrowdfundingView.tsx
Normal file
192
src/Components/Profile/Subcomponents/CrowdfundingView.tsx
Normal 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 Collective</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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 <></>
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)}
|
||||
>
|
||||
✕
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user