Merge main

This commit is contained in:
Maximilian Harz 2025-02-17 18:32:14 +01:00
commit a3d7fa8496
86 changed files with 7902 additions and 4448 deletions

View File

@ -1,3 +1,5 @@
node_modules/
dist/
examples/
examples/
docs/
coverage/

View File

@ -109,7 +109,12 @@ module.exports = {
'import/no-duplicates': 'error',
'import/no-named-default': 'error',
'import/no-namespace': 'error',
'import/no-unassigned-import': 'error',
'import/no-unassigned-import': [
'error',
{
allow: ['**/*.css'],
},
],
'import/order': [
'error',
{

56
.github/workflows/deploy.docs.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: deploy:docs
on:
push:
branches:
- main
jobs:
# Build job
build:
# Specify runner + build & upload the static files as an artifact
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.0.3
with:
node-version-file: './.tool-versions'
- name: Install Dependencies & Build Library
run: |
npm install
npm run build
npm link
working-directory: ./
- name: Build static files
id: build
run: npm install && npm run docs:generate
working-directory: ./
- name: Upload static files as artifact
id: deployment
uses: actions/upload-pages-artifact@v3.0.1
with:
path: docs/
# Deploy job
deploy:
# Add a dependency to the build job
needs: build
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
# Deploy to the github-pages environment
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
# Specify runner + deployment step
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4.0.5

View File

@ -28,6 +28,34 @@ jobs:
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.0.3
with:
node-version-file: './.tool-versions'
- name: Build
run: npm install && npm run build
- name: Install Dependencies & Build Library
run: |
npm install
npm run build
npm link
working-directory: ./
build-examples:
if: needs.files-changed.outputs.build == 'true'
name: Test Example Apps
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
app: [examples/1-basic-map, examples/2-static-layers] # Aktualisierte Pfade der Beispiel-Apps
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Set Up Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Link Utopia-UI in Example App
run: |
cd ${{ matrix.app }}
npm install
npm link utopia-ui
npm run build

52
.github/workflows/test.docs.yml vendored Normal file
View File

@ -0,0 +1,52 @@
name: test:docs
on: push
jobs:
files-changed:
name: Detect File Changes - docs
runs-on: ubuntu-latest
outputs:
docs: ${{ steps.filter.outputs.docs }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
filters: |
docs:
- '.github/workflows/**/*'
- '**/*'
# build:
# if: needs.files-changed.outputs.frontend == 'true'
# name: Build - Frontend
# needs: files-changed
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
# - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.0.3
# with:
# node-version-file: './.tool-versions'
# - name: Frontend | Build
# run: npm install && npm run build
# working-directory: ./frontend
docs:
if: needs.files-changed.outputs.docs == 'true'
name: Docs
needs: files-changed
runs-on: ubuntu-latest
env:
COVERAGE_REQUIRED: 0
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.0.3
with:
node-version-file: './.tool-versions'
- name: Docs
run: |
npm install
npm run docs:generate
./scripts/docs-coverage.sh
working-directory: ./

View File

@ -0,0 +1,75 @@
name: test::examples
on: push
jobs:
files-changed:
name: Detect File Changes - lint
runs-on: ubuntu-latest
outputs:
lint: ${{ steps.filter.outputs.lint }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
filters: |
lint:
- '.github/workflows/**/*'
- '**/*'
# build:
# if: needs.files-changed.outputs.frontend == 'true'
# name: Build - Frontend
# needs: files-changed
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
# - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.0.3
# with:
# node-version-file: './.tool-versions'
# - name: Frontend | Build
# run: npm install && npm run build
# working-directory: ./frontend
lint-example-1-basic-map:
if: needs.files-changed.outputs.lint == 'true'
name: Lint Example 1 - Basic Map
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.0.3
with:
node-version-file: './.tool-versions'
- name: Lint
run: npm install && npm run lint
working-directory: ./examples/1-basic-map
lint-example-2-static-layers:
if: needs.files-changed.outputs.lint == 'true'
name: Lint Example 2 - Static Layers
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.0.3
with:
node-version-file: './.tool-versions'
- name: Lint
run: npm install && npm run lint
working-directory: ./examples/2-static-layers
# unit:
# if: needs.files-changed.outputs.frontend == 'true'
# name: Unit - Frontend
# needs: files-changed
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
# - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.0.3
# with:
# node-version-file: './.tool-versions'
# - name: Frontend | Unit
# run: npm install && npm run test:unit
# working-directory: ./frontend

33
.github/workflows/test.unit.yml vendored Normal file
View File

@ -0,0 +1,33 @@
name: test:unit
on: push
jobs:
files-changed:
name: Detect File Changes - unit
runs-on: ubuntu-latest
outputs:
unit: ${{ steps.filter.outputs.unit }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
filters: |
unit:
- '.github/workflows/**/*'
- '**/*'
unit:
if: needs.files-changed.outputs.unit == 'true'
name: Unit
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.1.7
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.0.3
with:
node-version-file: './.tool-versions'
- name: Unit
run: npm install && npm run test:unit
working-directory: ./

5
.gitignore vendored
View File

@ -88,4 +88,7 @@ lerna-debug.log
# System Files
.DS_Store
Thumbs.db
Thumbs.db
# docs
/docs

11
cypress.config.ts Normal file
View File

@ -0,0 +1,11 @@
import { defineConfig } from 'cypress'
export default defineConfig({
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
specPattern: ['**/**/*.cy.{ts,tsx}'],
},
})

View File

@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>

View File

@ -0,0 +1,38 @@
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
// eslint-disable-next-line import/no-unassigned-import
import './commands'
import { mount } from 'cypress/react'
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
Cypress.Commands.add('mount', mount)
// Example use:
// cy.mount(<MyComponent />)

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,7 @@
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"utopia-ui": "^3.0.35"
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",

8166
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,31 @@
{
"name": "utopia-ui",
"version": "3.0.34",
"version": "3.0.59",
"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/",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"homepage": "https://utopia-os.org/",
"module": "./dist/index.esm.js",
"main": "./dist/index.cjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs"
}
},
"type": "module",
"scripts": {
"build": "rollup -c",
"start": "rollup -c -w",
"test:lint:eslint": "eslint --ext .ts,.tsx,.js,.jsx,.cjs,.mjs,.json,.yml,.yaml --max-warnings 0 ."
"test:lint:eslint": "eslint --ext .ts,.tsx,.js,.jsx,.cjs,.mjs,.json,.yml,.yaml --max-warnings 0 .",
"lint": "npm run test:lint:eslint",
"lintfix": "npm run test:lint:eslint -- --fix",
"test:component": "cypress run --component --browser electron",
"test:unit": "npm run test:unit:dev -- run --coverage",
"test:unit:dev": "vitest",
"docs:generate": "typedoc --plugin typedoc-plugin-coverage src/index.tsx",
"update": "npx npm-check-updates"
},
"files": [
"dist"
@ -19,13 +35,22 @@
"license": "GPL-3.0-only",
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.1",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-typescript": "^12.1.2",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/geojson": "^7946.0.14",
"@types/leaflet": "^1.7.11",
"@types/leaflet.markercluster": "^1.5.5",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.0.5",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@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",
"eslint": "^8.24.0",
"eslint-config-prettier": "^9.1.0",
@ -41,15 +66,21 @@
"eslint-plugin-react-refresh": "^0.4.18",
"eslint-plugin-security": "^3.0.1",
"eslint-plugin-yml": "^1.14.0",
"globals": "^15.14.0",
"happy-dom": "^16.8.1",
"postcss": "^8.4.21",
"prettier": "^3.3.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rollup": "^2.75.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rollup": "^4.34.6",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-typescript2": "^0.32.1",
"tailwindcss": "^3.3.1",
"typescript": "^4.7.4"
"typedoc": "^0.27.6",
"typedoc-plugin-coverage": "^3.4.1",
"typescript": "^5.7.3",
"vite": "^6.0.11",
"vitest": "^3.0.5"
},
"peerDependencies": {
"react": "^18.2.0",
@ -64,17 +95,19 @@
"leaflet": "^1.9.4",
"leaflet.locatecontrol": "^0.79.0",
"prop-types": "^15.8.1",
"radash": "^12.1.0",
"react-colorful": "^5.6.1",
"react-image-crop": "^10.1.8",
"react-leaflet": "^4.2.1",
"react-leaflet-cluster": "^2.1.0",
"react-markdown": "^9.0.1",
"react-photo-album": "^3.0.2",
"react-router-dom": "^6.16.0",
"react-string-replace": "^1.1.1",
"react-toastify": "^9.1.3",
"remark-breaks": "^4.0.0",
"tributejs": "^5.1.3",
"tw-elements": "^1.0.0"
"tw-elements": "^1.0.0",
"yet-another-react-lightbox": "^3.21.7"
},
"imports": {
"#components/*": "./src/Components/*",

View File

@ -1,52 +1,92 @@
import postcss from 'rollup-plugin-postcss'
import typescript from 'rollup-plugin-typescript2'
import path from 'path'
import { fileURLToPath } from 'url'
export default {
input: 'src/index.tsx',
output: [
{
dir: 'dist/',
format: 'esm',
exports: 'named',
sourcemap: true,
strict: false,
},
],
plugins: [
postcss({
plugins: [],
}),
typescript(),
],
external: [
'react',
'react-dom',
'react-markdown',
'react/jsx-runtime',
'remark-breaks',
'leaflet',
'react-leaflet',
'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',
'@heroicons/react/24/outline/ChevronRightIcon',
'@heroicons/react/24/outline',
'date-fns',
'@heroicons/react/24/outline/InformationCircleIcon',
'@heroicons/react/24/outline/QuestionMarkCircleIcon',
'@heroicons/react/24/outline/ChevronDownIcon',
'axios',
'react-image-crop',
'react-image-crop/dist/ReactCrop.css',
'react-colorful',
'leaflet.locatecontrol/dist/L.Control.Locate.css',
],
}
import alias from '@rollup/plugin-alias'
import resolve from '@rollup/plugin-node-resolve'
import typescript from '@rollup/plugin-typescript'
import { dts } from 'rollup-plugin-dts'
import postcss from 'rollup-plugin-postcss'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const aliasConfig = alias({
entries: [{ find: '#types', replacement: path.resolve(__dirname, 'src/types') }],
})
export default [
{
input: 'src/index.tsx',
output: [
{
file: 'dist/index.esm.js',
format: 'esm',
sourcemap: true,
},
{
file: 'dist/index.cjs',
format: 'cjs',
sourcemap: true,
},
],
plugins: [
aliasConfig,
resolve({
extensions: ['.ts', '.tsx'],
}),
postcss({
plugins: [],
}),
typescript({
tsconfig: './tsconfig.json',
}),
],
external: [
'react',
'react-dom',
'react-markdown',
'react/jsx-runtime',
'remark-breaks',
'leaflet',
'react-leaflet',
'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',
'@heroicons/react/24/outline/ChevronRightIcon',
'@heroicons/react/24/outline',
'date-fns',
'@heroicons/react/24/outline/InformationCircleIcon',
'@heroicons/react/24/outline/QuestionMarkCircleIcon',
'@heroicons/react/24/outline/ChevronDownIcon',
'axios',
'react-image-crop',
'react-image-crop/dist/ReactCrop.css',
'react-colorful',
'leaflet.locatecontrol/dist/L.Control.Locate.css',
'yet-another-react-lightbox',
'react-photo-album',
],
},
{
input: 'src/index.tsx',
output: [{ file: 'dist/index.d.ts', format: 'es' }],
plugins: [
aliasConfig,
dts({
respectExternal: true,
compilerOptions: {
skipLibCheck: true,
},
}),
],
external: [/\.css$/, /\.d\.ts$/], // ✅ `.d.ts` als extern behandeln
},
]

9
scripts/docs-coverage.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
COVERAGE=$(sed -nE 's/.*>([0-9]{1,3})%<.*/\1/p' docs/coverage.svg | head -1)
if (( $COVERAGE >= $COVERAGE_REQUIRED )) then
exit 0;
else
echo "Coverage: $COVERAGE/$COVERAGE_REQUIRED%";
exit 1;
fi

2
setupTest.ts Normal file
View File

@ -0,0 +1,2 @@
// eslint-disable-next-line import/no-unassigned-import
import '@testing-library/jest-dom'

View File

@ -62,7 +62,7 @@ export default function NavBar({ appName, userType }: { appName: string; userTyp
if (showNav) {
return (
<>
<div className='tw-navbar tw-bg-base-100 tw-z-[10000] tw-shadow-xl tw-relative'>
<div className='tw-navbar tw-bg-base-100 tw-z-[9998] tw-shadow-xl tw-relative'>
<button
className='tw-btn tw-btn-square tw-btn-ghost'
data-te-sidenav-toggle-ref

View File

@ -3,7 +3,6 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
import { useEffect, useRef, useState } from 'react'
import Tribute from 'tributejs'
import { useTags } from '#components/Map/hooks/useTags'
@ -37,9 +36,6 @@ export function TextAreaInput({
const ref = useRef<HTMLTextAreaElement>(null)
const [inputValue, setInputValue] = useState<string>(defaultValue)
// prevent react18 from calling useEffect twice
const init = useRef(false)
const tags = useTags()
const values: KeyValue[] = []
@ -48,30 +44,6 @@ export function TextAreaInput({
values.push({ key: tag.name, value: tag.name, color: tag.color })
})
const tribute = new Tribute({
containerClass: 'tw-z-3000 tw-bg-base-100 tw-p-2 tw-rounded-lg tw-shadow',
selectClass: 'tw-font-bold',
trigger: '#',
values,
menuShowMinLength: 3,
noMatchTemplate: () => {
return ''
},
menuItemTemplate: function (item) {
return `<span style="color: ${item.original.color}; padding: 5px; border-radius: 3px;">#${item.string}</span>`
},
})
useEffect(() => {
if (!init.current) {
if (ref.current) {
tribute.attach(ref.current)
}
init.current = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref])
useEffect(() => {
setInputValue(defaultValue)
}, [defaultValue])

View File

@ -0,0 +1,66 @@
/// <reference types="cypress" />
import { mount } from 'cypress/react'
import { TextInput } from './TextInput'
describe('<TextInput />', () => {
it('renders with default props', () => {
mount(<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')
})
it('renders with given labelTitle', () => {
mount(<TextInput labelTitle='Test Title' />)
cy.get('label').should('contain.text', 'Test Title')
})
it('renders with given type', () => {
mount(<TextInput type='email' />)
cy.get('input').should('have.attr', 'type', 'email')
})
it('accepts user input', () => {
mount(<TextInput dataField='test-input' />)
cy.get('input[name="test-input"]').type('Hello Test')
cy.get('input[name="test-input"]').should('have.value', 'Hello Test')
})
it('renders a label, if labelTitle is set', () => {
mount(<TextInput dataField='test-input' labelTitle='Test Label' />)
cy.contains('Test Label').should('exist')
})
it('handles default value correctly', () => {
mount(<TextInput dataField='test-input' defaultValue='Default Value' />)
cy.get('input[name="test-input"]').should('have.value', 'Default Value')
})
it('calls updateFormValue on change', () => {
const onChangeSpy = cy.spy().as('updateFormValueSpy')
mount(<TextInput dataField='test-input' updateFormValue={onChangeSpy} />)
cy.get('input[name="test-input"]').type('Test')
cy.get('@updateFormValueSpy').should('have.been.calledWith', 'Test')
})
it('accepts a specific input type', () => {
mount(<TextInput dataField='test-input' type='email' />)
cy.get('input[name="test-input"]').should('have.attr', 'type', 'email')
})
it('respects the autocomplete attribute', () => {
mount(<TextInput dataField='test-input' autocomplete='off' />)
cy.get('input[name="test-input"]').should('have.attr', 'autocomplete', 'off')
})
it('updates form value on change', () => {
const updateFormValue = cy.stub()
mount(<TextInput updateFormValue={updateFormValue} />)
cy.get('input').type('Hello')
cy.wrap(updateFormValue).should('have.been.calledWith', 'Hello')
})
})

View File

@ -0,0 +1,32 @@
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { TextInput } from './TextInput'
describe('<TextInput />', () => {
let wrapper = render(<TextInput />)
beforeEach(() => {
wrapper = render(<TextInput />)
})
it('renders properly', () => {
expect(wrapper.container.firstChild).toMatchSnapshot()
})
describe('handleChange', () => {
it('calls updateFormValue with new value', () => {
const updateFormValue = vi.fn()
wrapper.rerender(<TextInput updateFormValue={updateFormValue} />)
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'test' } })
expect(updateFormValue).toBeCalledWith('test')
})
})
describe('labelTitle', () => {
it('sets label', () => {
wrapper.rerender(<TextInput labelTitle='My Title' />)
expect(wrapper.container.firstChild).toMatchSnapshot()
})
})
})

View File

@ -0,0 +1,38 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<TextInput /> > labelTitle > sets label 1`] = `
<div
class="tw-form-control undefined"
>
<label
class="tw-label"
>
<span
class="tw-label-text tw-text-base-content undefined"
>
My Title
</span>
</label>
<input
class="tw-input tw-input-bordered tw-w-full "
placeholder=""
required=""
type="text"
value=""
/>
</div>
`;
exports[`<TextInput /> > renders properly 1`] = `
<div
class="tw-form-control undefined"
>
<input
class="tw-input tw-input-bordered tw-w-full "
placeholder=""
required=""
type="text"
value=""
/>
</div>
`;

View File

@ -1,4 +1,3 @@
import { node, string } from 'prop-types'
import { Children, cloneElement, isValidElement, useEffect } from 'react'
import type { Item } from '#types/Item'
@ -33,11 +32,4 @@ export const ItemForm = ({
)
}
ItemForm.propTypes = {
children: node,
__TYPE: string,
}
ItemForm.defaultProps = {
__TYPE: 'ItemForm',
}
ItemForm.__TYPE = 'ItemForm'

View File

@ -1,4 +1,3 @@
import { node, string } from 'prop-types'
import { Children, cloneElement, isValidElement } from 'react'
import type { Item } from '#types/Item'
@ -8,18 +7,11 @@ export const ItemView = ({ children, item }: { children?: React.ReactNode; item?
<div>
{children
? Children.toArray(children).map((child) =>
isValidElement<{ item: Item }>(child) ? cloneElement(child, { item }) : '',
isValidElement<{ item: Item }>(child) ? cloneElement(child, { item }) : null,
)
: ''}
: null}
</div>
)
}
ItemView.propTypes = {
children: node,
__TYPE: string,
}
ItemView.defaultProps = {
__TYPE: 'ItemView',
}
ItemView.__TYPE = 'ItemView'

View File

@ -1,18 +1,9 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Children, isValidElement, useEffect, useState } from 'react'
import { Marker, Tooltip, useMap, useMapEvents } from 'react-leaflet'
import { useLocation } from 'react-router-dom'
import { Marker, Tooltip } from 'react-leaflet'
import { encodeTag } from '#utils/FormatTags'
import { getValue } from '#utils/GetValue'
import { hashTagRegex } from '#utils/HashTagRegex'
import MarkerIconFactory from '#utils/MarkerIconFactory'
import { randomColor } from '#utils/RandomColor'
@ -34,6 +25,7 @@ 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 const Layer = ({
data,
@ -42,23 +34,12 @@ export const Layer = ({
menuIcon = 'MapPinIcon',
menuText = 'add new place',
menuColor = '#2E7D32',
markerIcon = 'circle-solid',
markerIcon = 'point',
markerShape = 'circle',
markerDefaultColor = '#777',
markerDefaultColor2 = 'RGBA(35, 31, 32, 0.2)',
api,
itemType,
itemNameField = 'name',
itemSubnameField,
itemTextField = 'text',
itemAvatarField,
itemColorField,
itemOwnerField,
itemLatitudeField = 'position.coordinates.1',
itemLongitudeField = 'position.coordinates.0',
itemTagsField,
itemOffersField,
itemNeedsField,
onlyOnePerOwner = false,
customEditLink,
customEditParameter,
@ -79,8 +60,6 @@ export const Layer = ({
const addPopup = useAddPopup()
const leafletRefs = useLeafletRefs()
const location = useLocation()
const allTagsLoaded = useAllTagsLoaded()
const allItemsLoaded = useAllItemsLoaded()
@ -92,8 +71,6 @@ export const Layer = ({
const [newTagsToAdd, setNewTagsToAdd] = useState<Tag[]>([])
const [tagsReady, setTagsReady] = useState<boolean>(false)
const map = useMap()
const isLayerVisible = useIsLayerVisible()
const isGroupTypeVisible = useIsGroupTypeVisible()
@ -115,16 +92,8 @@ export const Layer = ({
markerDefaultColor2,
api,
itemType,
itemNameField,
itemSubnameField,
itemTextField,
itemAvatarField,
itemColorField,
itemOwnerField,
itemTagsField,
itemOffersField,
itemNeedsField,
onlyOnePerOwner,
// Can we just use editCallback for all cases?
customEditLink,
customEditParameter,
// eslint-disable-next-line camelcase
@ -132,6 +101,7 @@ export const Layer = ({
listed,
setItemFormPopup,
itemFormPopup,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
clusterRef,
})
api &&
@ -148,15 +118,6 @@ export const Layer = ({
markerDefaultColor2,
api,
itemType,
itemNameField,
itemSubnameField,
itemTextField,
itemAvatarField,
itemColorField,
itemOwnerField,
itemTagsField,
itemOffersField,
itemNeedsField,
onlyOnePerOwner,
customEditLink,
customEditParameter,
@ -165,69 +126,12 @@ export const Layer = ({
listed,
setItemFormPopup,
itemFormPopup,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
clusterRef,
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, api])
useMapEvents({
popupopen: (e) => {
const item = Object.entries(leafletRefs).find((r) => r[1].popup === e.popup)?.[1].item
if (item?.layer?.name === name && window.location.pathname.split('/')[1] !== item.id) {
const params = new URLSearchParams(window.location.search)
if (!location.pathname.includes('/item/')) {
window.history.pushState(
{},
'',
`/${item.id}` + `${params.toString() !== '' ? `?${params}` : ''}`,
)
}
let title = ''
if (item.name) title = item.name
else if (item.layer.itemNameField) title = getValue(item, item.layer.itemNameField)
document.title = `${document.title.split('-')[0]} - ${title}`
}
},
})
const openPopup = () => {
if (
window.location.pathname.split('/').length <= 1 ||
window.location.pathname.split('/')[1] === ''
) {
map.closePopup()
} else {
if (window.location.pathname.split('/')[1]) {
const id = window.location.pathname.split('/')[1]
// eslint-disable-next-line security/detect-object-injection
const ref = leafletRefs[id]
if (ref?.marker && ref.item.layer?.name === name) {
ref.marker &&
clusterRef.hasLayer(ref.marker) &&
clusterRef?.zoomToShowLayer(ref.marker, () => {
ref.marker.openPopup()
})
let title = ''
if (ref.item.name) title = ref.item.name
else if (ref.item.layer.itemNameField)
title = getValue(ref.item.name, ref.item.layer.itemNameField)
document.title = `${document.title.split('-')[0]} - ${title}`
document
.querySelector('meta[property="og:title"]')
?.setAttribute('content', ref.item.name)
document
.querySelector('meta[property="og:description"]')
?.setAttribute('content', ref.item.text)
}
}
}
}
useEffect(() => {
openPopup()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [leafletRefs, location])
useEffect(() => {
if (tagsReady) {
const processedTags = {}
@ -264,29 +168,19 @@ export const Layer = ({
visibleGroupTypes.length === 0,
)
.map((item: Item) => {
if (getValue(item, itemLongitudeField) && getValue(item, itemLatitudeField)) {
// eslint-disable-next-line security/detect-object-injection
if (getValue(item, itemTextField)) item[itemTextField] = getValue(item, itemTextField)
// eslint-disable-next-line security/detect-object-injection
else item[itemTextField] = ''
if (item.position?.coordinates[0] && item.position?.coordinates[1]) {
if (item.tags) {
// eslint-disable-next-line security/detect-object-injection
item[itemTextField] = item[itemTextField] + '\n\n'
item.text += '\n\n'
item.tags.map((tag) => {
// eslint-disable-next-line security/detect-object-injection
if (!item[itemTextField].includes(`#${encodeTag(tag)}`)) {
// eslint-disable-next-line security/detect-object-injection
return (item[itemTextField] = item[itemTextField] + `#${encodeTag(tag)} `)
if (!item.text?.includes(`#${encodeTag(tag)}`)) {
item.text += `#${encodeTag(tag)}`
}
// eslint-disable-next-line security/detect-object-injection
return item[itemTextField]
return item.text
})
}
if (allTagsLoaded && allItemsLoaded) {
// eslint-disable-next-line security/detect-object-injection
item[itemTextField].match(hashTagRegex)?.map((tag) => {
item.text?.match(hashTagRegex)?.map((tag) => {
if (
!tags.find(
(t) => t.name.toLocaleLowerCase() === tag.slice(1).toLocaleLowerCase(),
@ -309,20 +203,19 @@ export const Layer = ({
const itemTags = getItemTags(item)
const latitude =
itemLatitudeField && item ? getValue(item, itemLatitudeField) : undefined
const longitude =
itemLongitudeField && item ? getValue(item, itemLongitudeField) : undefined
const latitude = item.position.coordinates[1]
const longitude = item.position.coordinates[0]
let color1 = markerDefaultColor
let color2 = markerDefaultColor2
if (itemColorField && getValue(item, itemColorField) != null)
color1 = getValue(item, itemColorField)
else if (itemTags && itemTags[0]) {
if (item.color) {
color1 = item.color
} else if (itemTags[0]) {
color1 = itemTags[0].color
}
if (itemTags && itemTags[0] && itemColorField) color2 = itemTags[0].color
else if (itemTags && itemTags[1]) {
if (itemTags[0] && item.color) {
color2 = itemTags[0].color
} else if (itemTags[1]) {
color2 = itemTags[1].color
}
return (
@ -348,10 +241,10 @@ export const Layer = ({
>
{children &&
Children.toArray(children).some(
(child) => isValidElement(child) && child.props.__TYPE === 'ItemView',
(child) => isComponentWithType(child) && child.type.__TYPE === 'ItemView',
) ? (
Children.toArray(children).map((child) =>
isValidElement(child) && child.props.__TYPE === 'ItemView' ? (
isComponentWithType(child) && child.type.__TYPE === 'ItemView' ? (
<ItemViewPopup
ref={(r) => {
if (!(item.id in leafletRefs && leafletRefs[item.id].popup === r)) {
@ -364,9 +257,7 @@ export const Layer = ({
>
{child}
</ItemViewPopup>
) : (
''
),
) : null,
)
) : (
<>
@ -382,8 +273,9 @@ export const Layer = ({
/>
</>
)}
<Tooltip offset={[0, -38]} direction='top'>
{item.name ? item.name : getValue(item, itemNameField)}
{item.name}
</Tooltip>
</Marker>
)
@ -396,10 +288,10 @@ export const Layer = ({
itemFormPopup.layer.name === name &&
(children &&
Children.toArray(children).some(
(child) => isValidElement(child) && child.props.__TYPE === 'ItemForm',
(child) => isComponentWithType(child) && child.type.__TYPE === 'ItemForm',
) ? (
Children.toArray(children).map((child) =>
isValidElement(child) && child.props.__TYPE === 'ItemForm' ? (
isComponentWithType(child) && child.type.__TYPE === 'ItemForm' ? (
<ItemFormPopup
key={setItemFormPopup?.name}
position={itemFormPopup.position}
@ -426,3 +318,7 @@ export const Layer = ({
</>
)
}
function isComponentWithType(node: ReactNode): node is ReactElement & { type: { __TYPE: string } } {
return isValidElement(node) && typeof node.type !== 'string' && '__TYPE' in node.type
}

View File

@ -57,6 +57,10 @@ export default function AddButton({
onClick={() => {
triggerAction(layer)
}}
onTouchEnd={(e) => {
triggerAction(layer)
e.preventDefault()
}}
>
<img
src={layer.menuIcon}

View File

@ -9,7 +9,6 @@ import { useEffect, useRef, useState } from 'react'
import { useMap, useMapEvents } from 'react-leaflet'
// eslint-disable-next-line import/no-unassigned-import
import 'leaflet.locatecontrol'
// eslint-disable-next-line import/no-unassigned-import
import 'leaflet.locatecontrol/dist/L.Control.Locate.css'
// Converts leaflet.locatecontrol to a React Component

View File

@ -23,7 +23,6 @@ import { useLeafletRefs } from '#components/Map/hooks/useLeafletRefs'
import { useTags } from '#components/Map/hooks/useTags'
import useWindowDimensions from '#components/Map/hooks/useWindowDimension'
import { decodeTag } from '#utils/FormatTags'
import { getValue } from '#utils/GetValue'
import MarkerIconFactory from '#utils/MarkerIconFactory'
import { LocateControl } from './LocateControl'
@ -73,12 +72,10 @@ export const SearchControl = () => {
searchGeo()
setItemsResults(
items.filter((item) => {
if (item.layer?.itemNameField) item.name = getValue(item, item.layer.itemNameField)
if (item.layer?.itemTextField) item.text = getValue(item, item.layer.itemTextField)
return (
value.length > 2 &&
((item.layer?.listed && item.name.toLowerCase().includes(value.toLowerCase())) ||
item.text.toLowerCase().includes(value.toLowerCase()))
item.text?.toLowerCase().includes(value.toLowerCase()))
)
}),
)

View File

@ -189,7 +189,7 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
key={props.position.toString()}
placeholder='Text'
dataField='text'
defaultValue={props.item ? props.item.text : ''}
defaultValue={props.item?.text ?? ''}
inputStyle='tw-h-40 tw-mt-5'
/>
</>

View File

@ -9,13 +9,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
import DialogModal from '#components/Templates/DialogModal'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
import type { ItemsApi } from '#types/ItemsApi'
@ -26,9 +25,6 @@ export function HeaderView({
editCallback,
deleteCallback,
setPositionCallback,
itemNameField,
itemSubnameField,
itemAvatarField,
loading,
hideMenu = false,
big = false,
@ -41,9 +37,6 @@ export function HeaderView({
editCallback?: any
deleteCallback?: any
setPositionCallback?: any
itemNameField?: string
itemAvatarField?: string
itemSubnameField?: string
loading?: boolean
hideMenu?: boolean
big?: boolean
@ -57,23 +50,17 @@ export function HeaderView({
const navigate = useNavigate()
const appState = useAppState()
const [imageLoaded, setImageLoaded] = useState(false)
useEffect(() => {
setImageLoaded(false)
}, [item])
const avatar =
itemAvatarField && getValue(item, itemAvatarField)
? appState.assetsApi.url +
getValue(item, itemAvatarField) +
`${big ? '?width=160&heigth=160' : '?width=80&heigth=80'}`
: item.layer?.itemAvatarField &&
item &&
getValue(item, item.layer?.itemAvatarField) &&
appState.assetsApi.url +
getValue(item, item.layer?.itemAvatarField) +
`${big ? '?width=160&heigth=160' : '?width=80&heigth=80'}`
const title = itemNameField
? getValue(item, itemNameField)
: item.layer?.itemNameField && item && getValue(item, item.layer.itemNameField)
const subtitle = itemSubnameField
? getValue(item, itemSubnameField)
: item.layer?.itemSubnameField && item && getValue(item, item.layer.itemSubnameField)
item.image &&
appState.assetsApi.url + item.image + `${big ? '?width=160&heigth=160' : '?width=80&heigth=80'}`
const title = item.name
const subtitle = item.subname
const [address] = useState<string>('')
@ -92,13 +79,21 @@ export function HeaderView({
{avatar && (
<div className='tw-avatar'>
<div
className={`${big ? 'tw-w-20' : 'tw-w-10'} tw-inline tw-items-center tw-justify-center overflow-hidden`}
className={`${
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'}
src={avatar}
alt={item.name + ' logo'}
onLoad={() => setImageLoaded(true)}
onError={() => setImageLoaded(false)}
style={{ display: imageLoaded ? 'block' : 'none' }}
/>
{!imageLoaded && (
<div className='tw-w-full tw-h-full tw-bg-gray-200 tw-rounded-full' />
)}
</div>
</div>
)}
@ -154,7 +149,7 @@ export function HeaderView({
onClick={(e) =>
item.layer?.customEditLink
? navigate(
`${item.layer.customEditLink}${item.layer.customEditParameter ? `/${getValue(item, item.layer.customEditParameter)}${params && '?' + params}` : ''} `,
`${item.layer.customEditLink}${item.layer.customEditParameter ? `/${item.id}${params && '?' + params}` : ''} `,
)
: editCallback(e)
}

View File

@ -3,7 +3,6 @@
import { Link } from 'react-router-dom'
import { useGetItemTags } from '#components/Map/hooks/useTags'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
@ -11,23 +10,21 @@ export const PopupButton = ({
url,
parameterField,
text,
colorField,
item,
}: {
url: string
parameterField?: string
text: string
colorField?: string
item?: Item
}) => {
const params = new URLSearchParams(window.location.search)
const getItemTags = useGetItemTags()
return (
<Link to={`${url}/${parameterField ? getValue(item, parameterField) : ''}?${params}`}>
<Link to={`${url}/${parameterField ? item?.id : ''}?${params}`}>
<button
style={{
backgroundColor: `${colorField && getValue(item, colorField) ? getValue(item, colorField) : item && getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item?.layer?.markerDefaultColor}`,
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'
>

View File

@ -12,7 +12,6 @@ import remarkBreaks from 'remark-breaks'
import { useAddFilterTag } from '#components/Map/hooks/useFilter'
import { useTags } from '#components/Map/hooks/useTags'
import { decodeTag } from '#utils/FormatTags'
import { getValue } from '#utils/GetValue'
import { hashTagRegex } from '#utils/HashTagRegex'
import { fixUrls, mailRegex } from '#utils/ReplaceURLs'
@ -21,32 +20,37 @@ import type { Tag } from '#types/Tag'
export const TextView = ({
item,
itemId,
text,
truncate = false,
itemTextField,
rawText,
}: {
item?: Item
itemId: string
text?: string
truncate?: boolean
itemTextField?: string
rawText?: string
}) => {
if (item) {
text = item.text
itemId = item.id
}
const tags = useTags()
const addFilterTag = useAddFilterTag()
let text = ''
let innerText = ''
let replacedText = ''
if (rawText) {
text = replacedText = rawText
} else if (itemTextField && item) {
text = getValue(item, itemTextField)
} else {
text = item?.layer?.itemTextField && item ? getValue(item, item.layer.itemTextField) : ''
innerText = replacedText = rawText
} else if (text) {
innerText = text
}
if (item && text && truncate) text = truncateText(removeMarkdownKeepLinksAndParagraphs(text), 100)
if (innerText && truncate)
innerText = truncateText(removeMarkdownKeepLinksAndParagraphs(innerText), 100)
if (item && text) replacedText = fixUrls(text)
if (innerText) replacedText = fixUrls(innerText)
if (replacedText) {
replacedText = replacedText.replace(/(?<!\]?\()https?:\/\/[^\s)]+(?!\))/g, (url) => {
@ -114,16 +118,16 @@ export const TextView = ({
const CustomHashTagLink = ({
children,
tag,
item,
itemId,
}: {
children: string
tag: Tag
item?: Item
itemId: string
}) => {
return (
<a
style={{ color: tag ? tag.color : '#faa', fontWeight: 'bold', cursor: 'pointer' }}
key={tag ? tag.name + item?.id : item?.id}
key={tag ? tag.name + itemId : itemId}
onClick={(e) => {
e.stopPropagation()
addFilterTag(tag)
@ -173,7 +177,7 @@ export const TextView = ({
)
if (tag)
return (
<CustomHashTagLink tag={tag} item={item}>
<CustomHashTagLink tag={tag} itemId={itemId}>
{children}
</CustomHashTagLink>
)

View File

@ -67,6 +67,7 @@ export const ItemViewPopup = forwardRef((props: ItemViewPopupProps, ref: any) =>
success = true
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
toast.error(error.toString())
}
if (success) {
@ -104,7 +105,7 @@ export const ItemViewPopup = forwardRef((props: ItemViewPopupProps, ref: any) =>
: '',
)
) : (
<TextView item={props.item} />
<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'>

View File

@ -134,6 +134,27 @@
left: 4px;
width: 24px;
}
.shop-icon {
position: relative;
top: -34px;
left: 4px;
width: 24px;
}
.plant-icon {
position: relative;
top: -34px;
left: 4px;
width: 24px;
}
.circle-dot-icon {
position: relative;
top: -36px;
left: 4px;
width: 24px;
}
.leaflet-popup-scrolled {
overflow-x: hidden;

View File

@ -1,16 +1,46 @@
import { LatLng } from 'leaflet'
import { MapContainer } from 'react-leaflet'
import { ContextWrapper } from '#components/AppShell/ContextWrapper'
import { UtopiaMapInner } from './UtopiaMapInner'
import type { UtopiaMapProps } from '#types/UtopiaMapProps'
// eslint-disable-next-line import/no-unassigned-import
import 'react-toastify/dist/ReactToastify.css'
function UtopiaMap(props: UtopiaMapProps) {
function UtopiaMap({
height = '500px',
width = '100%',
center = [50.6, 9.5],
zoom = 10,
children,
geo,
showFilterControl = false,
showGratitudeControl = false,
showLayerControl = true,
infoText,
donationWidget,
}: UtopiaMapProps) {
return (
<ContextWrapper>
<UtopiaMapInner {...props} />
<MapContainer
style={{ height, width }}
center={new LatLng(center[0], center[1])}
zoom={zoom}
zoomControl={false}
maxZoom={19}
>
<UtopiaMapInner
geo={geo}
showFilterControl={showFilterControl}
showGratitudeControl={showGratitudeControl}
showLayerControl={showLayerControl}
infoText={infoText}
donationWidget={donationWidget}
>
{children}
</UtopiaMapInner>
</MapContainer>
</ContextWrapper>
)
}

View File

@ -6,29 +6,20 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { LatLng } from 'leaflet'
import {
Children,
cloneElement,
createRef,
isValidElement,
useEffect,
useRef,
useState,
} from 'react'
import { TileLayer, MapContainer, useMapEvents, GeoJSON } from 'react-leaflet'
// eslint-disable-next-line import/no-unassigned-import
import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from 'react'
import { TileLayer, useMapEvents, GeoJSON, useMap } from 'react-leaflet'
import 'leaflet/dist/leaflet.css'
import MarkerClusterGroup from 'react-leaflet-cluster'
import { Outlet } from 'react-router-dom'
import { Outlet, useLocation } from 'react-router-dom'
import { toast } from 'react-toastify'
// eslint-disable-next-line import/no-unassigned-import
import './UtopiaMap.css'
import { containsUUID } from '#utils/ContainsUUID'
import { useClusterRef, useSetClusterRef } from './hooks/useClusterRef'
import { useAddVisibleLayer } from './hooks/useFilter'
import { useLayers } from './hooks/useLayers'
import { useLeafletRefs } from './hooks/useLeafletRefs'
import {
useSelectPosition,
useSetMapClicked,
@ -41,6 +32,7 @@ import { GratitudeControl } from './Subcomponents/Controls/GratitudeControl'
import { LayerControl } from './Subcomponents/Controls/LayerControl'
import { SearchControl } from './Subcomponents/Controls/SearchControl'
import { TagsControl } from './Subcomponents/Controls/TagsControl'
import { PopupButton } from './Subcomponents/ItemPopupComponents/PopupButton'
import { TextView } from './Subcomponents/ItemPopupComponents/TextView'
import { SelectPosition } from './Subcomponents/SelectPosition'
@ -48,21 +40,14 @@ import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
import type { UtopiaMapProps } from '#types/UtopiaMapProps'
import type { Feature, Geometry as GeoJSONGeometry } from 'geojson'
const mapDivRef = createRef()
export function UtopiaMapInner({
height = '500px',
width = '100%',
center = [50.6, 9.5],
zoom = 10,
children,
geo,
showFilterControl = false,
showGratitudeControl = false,
showLayerControl = true,
infoText,
donationWidget,
}: UtopiaMapProps) {
// Hooks that rely on contexts, called after ContextWrapper is provided
const selectNewItemPosition = useSelectPosition()
const setSelectNewItemPosition = useSetSelectPosition()
const setClusterRef = useSetClusterRef()
@ -72,6 +57,10 @@ export function UtopiaMapInner({
const layers = useLayers()
const addVisibleLayer = useAddVisibleLayer()
const leafletRefs = useLeafletRefs()
const location = useLocation()
const map = useMap()
useEffect(() => {
layers.forEach((layer) => addVisibleLayer(layer))
@ -81,10 +70,22 @@ export function UtopiaMapInner({
const init = useRef(false)
useEffect(() => {
if (!init.current) {
infoText &&
donationWidget &&
setTimeout(() => {
toast(<TextView rawText={infoText} />, { autoClose: false })
}, 4000)
toast(
<>
<TextView itemId='' rawText={'## Do you like this Map?'} />
<div>
<TextView
itemId=''
rawText={'Support us building free opensource maps and help us grow 🌱☀️'}
/>
<PopupButton url={'https://opencollective.com/utopia-project'} text={'Donate'} />
</div>
</>,
{ autoClose: false },
)
}, 600000)
init.current = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -105,9 +106,61 @@ export function UtopiaMapInner({
return null
}
useMapEvents({
popupopen: (e) => {
const item = Object.entries(leafletRefs).find((r) => r[1].popup === e.popup)?.[1].item
if (window.location.pathname.split('/')[1] !== item?.id) {
const params = new URLSearchParams(window.location.search)
if (!location.pathname.includes('/item/')) {
window.history.pushState(
{},
'',
`/${item?.id}` + `${params.toString() !== '' ? `?${params}` : ''}`,
)
}
let title = ''
if (item?.name) title = item.name
document.title = `${document.title.split('-')[0]} - ${title}`
}
},
})
const openPopup = () => {
if (!containsUUID(window.location.pathname)) {
map.closePopup()
} else {
if (window.location.pathname.split('/')[1]) {
const id = window.location.pathname.split('/')[1]
// eslint-disable-next-line security/detect-object-injection
const ref = leafletRefs[id]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (ref) {
clusterRef.hasLayer(ref.marker) &&
clusterRef?.zoomToShowLayer(ref.marker, () => {
ref.marker.openPopup()
})
let title = ''
if (ref.item.name) title = ref.item.name
document.title = `${document.title.split('-')[0]} - ${title}`
document
.querySelector('meta[property="og:title"]')
?.setAttribute('content', ref.item.name)
document
.querySelector('meta[property="og:description"]')
?.setAttribute('content', ref.item.text ?? '')
}
}
}
}
useEffect(() => {
openPopup()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [leafletRefs, location])
const resetMetaTags = () => {
const params = new URLSearchParams(window.location.search)
if (!window.location.pathname.includes('/item/')) {
if (!containsUUID(window.location.pathname)) {
window.history.pushState({}, '', '/' + `${params.toString() !== '' ? `?${params}` : ''}`)
}
document.title = document.title.split('-')[0]
@ -130,62 +183,53 @@ export function UtopiaMapInner({
<div
className={`tw-h-full ${selectNewItemPosition != null ? 'crosshair-cursor-enabled' : undefined}`}
>
<MapContainer
ref={mapDivRef}
style={{ height, width }}
center={new LatLng(center[0], center[1])}
zoom={zoom}
zoomControl={false}
<Outlet />
<Control position='topLeft' zIndex='1000' absolute>
<SearchControl />
<TagsControl />
</Control>
<Control position='bottomLeft' zIndex='999' absolute>
{showFilterControl && <FilterControl />}
{showLayerControl && <LayerControl />}
{showGratitudeControl && <GratitudeControl />}
</Control>
<TileLayer
maxZoom={19}
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url='https://tile.osmand.net/hd/{z}/{x}/{y}.png'
/>
<MarkerClusterGroup
ref={(r) => setClusterRef(r as any)}
showCoverageOnHover
chunkedLoading
maxClusterRadius={50}
removeOutsideVisibleBounds={false}
>
<Outlet />
<Control position='topLeft' zIndex='1000' absolute>
<SearchControl />
<TagsControl />
</Control>
<Control position='bottomLeft' zIndex='999' absolute>
{showFilterControl && <FilterControl />}
{showLayerControl && <LayerControl />}
{showGratitudeControl && <GratitudeControl />}
</Control>
<TileLayer
maxZoom={19}
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url='https://tile.osmand.net/hd/{z}/{x}/{y}.png'
/>
<MarkerClusterGroup
ref={(r) => setClusterRef(r)}
showCoverageOnHover
chunkedLoading
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,
)}
</MarkerClusterGroup>
{geo && (
<GeoJSON
data={geo}
onEachFeature={onEachFeature}
eventHandlers={{
click: (e) => {
if (selectNewItemPosition) {
e.layer.closePopup()
setMapClicked({ position: e.latlng, setItemFormPopup })
}
},
}}
/>
{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,
)}
<MapEventListener />
</MapContainer>
</MarkerClusterGroup>
{geo && (
<GeoJSON
data={geo}
onEachFeature={onEachFeature}
eventHandlers={{
click: (e) => {
if (selectNewItemPosition) {
e.layer.closePopup()
setMapClicked({ position: e.latlng, setItemFormPopup })
}
},
}}
/>
)}
<MapEventListener />
<AddButton triggerAction={setSelectNewItemPosition} />
{selectNewItemPosition != null && (
<SelectPosition setSelectNewItemPosition={setSelectNewItemPosition} />

View File

@ -3,7 +3,7 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-misused-promises */
import { useCallback, useReducer, createContext, useContext, useState } from 'react'
import { toast } from 'react-toastify'
@ -82,6 +82,7 @@ function useItemsManager(initialItems: Item[]): {
},
})
result.map((item) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
dispatch({ type: 'ADD', item: { ...item, layer } })
return null
})

View File

@ -63,7 +63,7 @@ function useSelectPositionManager(): {
if ('menuIcon' in selectPosition) {
mapClicked &&
mapClicked.setItemFormPopup({
layer: selectPosition as LayerProps,
layer: selectPosition,
position: mapClicked.position,
})
setSelectPosition(null)
@ -98,6 +98,7 @@ function useSelectPositionManager(): {
success = true
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
toast.error(error.toString())
}
if (success) {
@ -123,6 +124,7 @@ function useSelectPositionManager(): {
success = true
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
toast.error(error.toString())
}
if (success) {
@ -145,6 +147,7 @@ function useSelectPositionManager(): {
success = true
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
toast.error(error.toString())
}
if (success) {

View File

@ -5,12 +5,8 @@
/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { useCallback, useReducer, createContext, useContext, useState } from 'react'
import { getValue } from '#utils/GetValue'
import { hashTagRegex } from '#utils/HashTagRegex'
import type { Item } from '#types/Item'
@ -96,8 +92,7 @@ function useTagsManager(initialTags: Tag[]): {
const getItemTags = useCallback(
(item: Item) => {
const text =
item.layer?.itemTextField && item ? getValue(item, item.layer.itemTextField) : undefined
const text = item.text
const itemTagStrings = text?.match(hashTagRegex)
const itemTags: Tag[] = []
itemTagStrings?.map((tag) => {
@ -108,18 +103,15 @@ function useTagsManager(initialTags: Tag[]): {
}
return null
})
item.layer?.itemOffersField &&
getValue(item, item.layer.itemOffersField)?.map((o) => {
const offer = tags.find((t) => t.id === o.tags_id)
offer && itemTags.push(offer)
return null
})
item.layer?.itemNeedsField &&
getValue(item, item.layer.itemNeedsField)?.map((n) => {
const need = tags.find((t) => t.id === n.tags_id)
need && itemTags.push(need)
return null
})
// Could be refactored as it occurs in multiple places
item.offers?.forEach((o) => {
const offer = tags.find((t) => t.id === o.tags_id)
offer && itemTags.push(offer)
})
item.needs?.forEach((n) => {
const need = tags.find((t) => t.id === n.tags_id)
need && itemTags.push(need)
})
return itemTags
},

View File

@ -1,8 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
@ -14,7 +11,6 @@ import { useLayers } from '#components/Map/hooks/useLayers'
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
import { useAddTag, useGetItemTags, useTags } from '#components/Map/hooks/useTags'
import { MapOverlayPage } from '#components/Templates'
import { getValue } from '#utils/GetValue'
import { linkItem, onUpdateItem, unlinkItem } from './itemFunctions'
import { FormHeader } from './Subcomponents/FormHeader'
@ -23,11 +19,12 @@ import { OnepagerForm } from './Templates/OnepagerForm'
import { SimpleForm } from './Templates/SimpleForm'
import { TabsForm } from './Templates/TabsForm'
import type { FormState } from '#types/FormState'
import type { Item } from '#types/Item'
import type { Tag } from '#types/Tag'
export function ProfileForm() {
const [state, setState] = useState({
const [state, setState] = useState<FormState>({
color: '',
id: '',
group_type: 'wuerdekompass',
@ -91,11 +88,10 @@ export function ProfileForm() {
useEffect(() => {
const newColor =
item.layer?.itemColorField && getValue(item, item.layer.itemColorField)
? getValue(item, item.layer.itemColorField)
: getItemTags(item) && getItemTags(item)[0]?.color
? getItemTags(item)[0].color
: item.layer?.markerDefaultColor
item.color ??
(getItemTags(item) && getItemTags(item)[0]?.color
? getItemTags(item)[0].color
: item.layer?.markerDefaultColor)
const offers = (item.offers ?? []).reduce((acc: Tag[], o) => {
const offer = tags.find((t) => t.id === o.tags_id)
@ -116,7 +112,7 @@ export function ProfileForm() {
}, [])
setState({
color: newColor,
color: newColor ?? '',
id: item?.id ?? '',
group_type: item?.group_type ?? '',
status: item?.status ?? '',
@ -127,7 +123,8 @@ export function ProfileForm() {
telephone: item?.telephone ?? '',
next_appointment: item?.next_appointment ?? '',
image: item?.image ?? '',
marker_icon: item?.marker_icon ?? '',
// Do we actually mean marker_icon here?
marker_icon: item?.markerIcon ?? '',
offers,
needs,
relations,
@ -140,7 +137,7 @@ export function ProfileForm() {
const [template, setTemplate] = useState<string>('')
useEffect(() => {
setTemplate(item.layer?.itemType.template || appState.userType)
setTemplate(item.layer?.itemType.template ?? appState.userType)
}, [appState.userType, item])
return (
@ -198,7 +195,8 @@ export function ProfileForm() {
className={loading ? ' tw-loading tw-btn tw-float-right' : 'tw-btn tw-float-right'}
type='submit'
style={{
backgroundColor: `${item.layer?.itemColorField && getValue(item, item.layer?.itemColorField) ? getValue(item, item.layer?.itemColorField) : getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item?.layer?.markerDefaultColor}`,
// We could refactor this, it is used several times at different locations
backgroundColor: `${item.color ?? (getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item?.layer?.markerDefaultColor)}`,
color: '#fff',
}}
>

View File

@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/await-thenable */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-floating-promises */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { LatLng } from 'leaflet'
@ -21,7 +21,6 @@ import { useSelectPosition, useSetSelectPosition } from '#components/Map/hooks/u
import { useTags } from '#components/Map/hooks/useTags'
import { HeaderView } from '#components/Map/Subcomponents/ItemPopupComponents/HeaderView'
import { MapOverlayPage } from '#components/Templates'
import { getValue } from '#utils/GetValue'
import { handleDelete, linkItem, unlinkItem } from './itemFunctions'
import { FlexView } from './Templates/FlexView'
@ -32,6 +31,7 @@ import { TabsView } from './Templates/TabsView'
import type { Item } from '#types/Item'
import type { ItemsApi } from '#types/ItemsApi'
import type { Tag } from '#types/Tag'
import type { Marker } from 'leaflet'
export function ProfileView({ attestationApi }: { attestationApi?: ItemsApi<any> }) {
const [item, setItem] = useState<Item>()
@ -88,30 +88,25 @@ export function ProfileView({ attestationApi }: { attestationApi?: ItemsApi<any>
setNeeds([])
setRelations([])
item?.layer?.itemOffersField &&
getValue(item, item.layer.itemOffersField)?.map((o) => {
const tag = tags.find((t) => t.id === o.tags_id)
tag && setOffers((current) => [...current, tag])
return null
})
item?.layer?.itemNeedsField &&
getValue(item, item.layer.itemNeedsField)?.map((n) => {
const tag = tags.find((t) => t.id === n.tags_id)
tag && setNeeds((current) => [...current, tag])
return null
})
item?.relations?.map((r) => {
item?.offers?.forEach((o) => {
const tag = tags.find((t) => t.id === o.tags_id)
tag && setOffers((current) => [...current, tag])
})
item?.needs?.forEach((n) => {
const tag = tags.find((t) => t.id === n.tags_id)
tag && setNeeds((current) => [...current, tag])
})
item?.relations?.forEach((r) => {
const item = items.find((i) => i.id === r.related_items_id)
item && setRelations((current) => [...current, item])
return null
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item, items])
useEffect(() => {
const setMap = async (marker, x) => {
await map.setView(
const setMap = (marker: Marker, x: number) => {
map.setView(
new LatLng(item?.position?.coordinates[1]!, item?.position?.coordinates[0]! + x / 4),
undefined,
)
@ -164,7 +159,7 @@ export function ProfileView({ attestationApi }: { attestationApi?: ItemsApi<any>
}, [selectPosition])
useEffect(() => {
setTemplate(item?.layer?.itemType.template || appState.userType)
setTemplate(item?.layer?.itemType.template ?? appState.userType)
}, [appState.userType, item])
return (

View File

@ -10,7 +10,6 @@ import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
import { useGetItemTags } from '#components/Map/hooks/useTags'
import { HeaderView } from '#components/Map/Subcomponents/ItemPopupComponents/HeaderView'
import DialogModal from '#components/Templates/DialogModal'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
@ -20,7 +19,6 @@ export function ActionButton({
triggerItemSelected,
existingRelations,
itemType,
colorField,
collection = 'items',
customStyle,
}: {
@ -28,7 +26,6 @@ export function ActionButton({
triggerItemSelected?: any
existingRelations: Item[]
itemType?: string
colorField?: string
collection?: string
customStyle?: string
item: Item
@ -45,6 +42,12 @@ export function ActionButton({
.filter((i) => !existingRelations.some((s) => s.id === i.id))
.filter((i) => i.id !== item.id)
const backgroundColor =
item.color ??
(getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color
? getItemTags(item)[0].color
: item.layer?.markerDefaultColor)
return (
<>
{hasUserPermission(collection, 'update', item) && (
@ -58,7 +61,7 @@ export function ActionButton({
setModalOpen(true)
}}
style={{
backgroundColor: `${colorField && getValue(item, colorField) ? getValue(item, colorField) : getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item.layer?.markerDefaultColor}`,
backgroundColor,
color: '#fff',
}}
>
@ -82,7 +85,7 @@ export function ActionButton({
triggerAddButton()
}}
style={{
backgroundColor: `${colorField && getValue(item, colorField) ? getValue(item, colorField) : getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item.layer?.markerDefaultColor}`,
backgroundColor,
color: '#fff',
}}
>

View File

@ -6,7 +6,6 @@ import { useState, useCallback, useRef } from 'react'
import { ReactCrop, centerCrop, makeAspectCrop } from 'react-image-crop'
import { useAppState } from '#components/AppShell/hooks/useAppState'
// eslint-disable-next-line import/no-unassigned-import
import 'react-image-crop/dist/ReactCrop.css'
import DialogModal from '#components/Templates/DialogModal'

View File

@ -5,7 +5,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { HexColorPicker } from 'react-colorful'
// eslint-disable-next-line import/no-unassigned-import
import './ColorPicker.css'
import useClickOutside from '#components/Profile/hooks/useClickOutside'

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
import { useEffect, useState } from 'react'

View File

@ -0,0 +1,36 @@
import { useState } from 'react'
import { RowsPhotoAlbum } from 'react-photo-album'
import ReactLightbox from 'yet-another-react-lightbox'
import 'yet-another-react-lightbox/styles.css'
import 'react-photo-album/rows.css'
import { useAppState } from '#components/AppShell/hooks/useAppState'
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 {
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')
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>
)
}

View File

@ -4,46 +4,29 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { useEffect } from 'react'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
export function LinkedItemsHeaderView({
item,
unlinkCallback,
itemNameField,
itemAvatarField,
loading,
unlinkPermission,
itemSubnameField,
}: {
item: Item
unlinkCallback?: any
itemNameField?: string
itemAvatarField?: string
itemSubnameField?: string
loading?: boolean
unlinkPermission: boolean
}) {
const appState = useAppState()
const avatar =
itemAvatarField && getValue(item, itemAvatarField)
? appState.assetsApi.url + getValue(item, itemAvatarField)
: item.layer?.itemAvatarField &&
item &&
getValue(item, item.layer?.itemAvatarField) &&
appState.assetsApi.url + getValue(item, item.layer?.itemAvatarField)
const title = itemNameField
? getValue(item, itemNameField)
: item.layer?.itemNameField && item && getValue(item, item.layer.itemNameField)
const subtitle = itemSubnameField
? getValue(item, itemSubnameField)
: item.layer?.itemSubnameField && item && getValue(item, item.layer.itemSubnameField)
const avatar = appState.assetsApi.url + item.image
const title = item.name
const subtitle = item.subname
useEffect(() => {}, [item])

View File

@ -5,7 +5,6 @@
import { useEffect, useState } from 'react'
import { TextAreaInput } from '#components/Input'
import { getValue } from '#utils/GetValue'
import { MarkdownHint } from './MarkdownHint'
@ -14,6 +13,7 @@ import type { FormState } from '#types/FormState'
export const ProfileTextForm = ({
state,
setState,
// Is this really used?
dataField,
heading,
size,
@ -49,7 +49,8 @@ export const ProfileTextForm = ({
</div>
<TextAreaInput
placeholder={'...'}
defaultValue={getValue(state, field)}
// eslint-disable-next-line security/detect-object-injection
defaultValue={state[field]}
updateFormValue={(v) =>
setState((prevState) => ({
...prevState,

View File

@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { get } from 'radash'
import { TextView } from '#components/Map'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
export const ProfileTextView = ({
item,
dataField,
dataField = 'text',
heading,
hideWhenEmpty,
}: {
@ -15,13 +15,19 @@ export const ProfileTextView = ({
heading: string
hideWhenEmpty: boolean
}) => {
const text = get(item, dataField)
if (typeof text !== 'string') {
throw new Error('ProfileTextView: text is not a string')
}
return (
<div className='tw-my-10 tw-mt-2 tw-px-6'>
{!(getValue(item, dataField) === '' && hideWhenEmpty) && (
{!(text === '' && hideWhenEmpty) && (
<h2 className='tw-text-lg tw-font-semibold'>{heading}</h2>
)}
<div className='tw-mt-2 tw-text-sm'>
<TextView rawText={dataField ? getValue(item, dataField) : getValue(item, 'text')} />
<TextView itemId={item.id} rawText={text} />
</div>
</div>
)

View File

@ -1,7 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { ContactInfoForm } from '#components/Profile/Subcomponents/ContactInfoForm'
import { GroupSubheaderForm } from '#components/Profile/Subcomponents/GroupSubheaderForm'

View File

@ -1,19 +1,20 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { ContactInfoView } from '#components/Profile/Subcomponents/ContactInfoView'
import { GalleryView } from '#components/Profile/Subcomponents/GalleryView'
import { GroupSubHeaderView } from '#components/Profile/Subcomponents/GroupSubHeaderView'
import { ProfileStartEndView } from '#components/Profile/Subcomponents/ProfileStartEndView'
import { ProfileTextView } from '#components/Profile/Subcomponents/ProfileTextView'
import type { Item } from '#types/Item'
import type { Key } from 'react'
const componentMap = {
groupSubheaders: GroupSubHeaderView,
texts: ProfileTextView,
contactInfos: ContactInfoView,
startEnd: ProfileStartEndView,
gallery: GalleryView,
// weitere Komponenten hier
}
@ -22,14 +23,17 @@ export const FlexView = ({ item }: { item: Item }) => {
console.log(item)
return (
<div className='tw-h-full tw-overflow-y-auto fade'>
{item.layer?.itemType.profileTemplate.map((templateItem) => {
const TemplateComponent = componentMap[templateItem.collection]
return TemplateComponent ? (
<TemplateComponent key={templateItem.id} item={item} {...templateItem.item} />
) : (
<div key={templateItem.id}>Component not found</div>
)
})}
{item.layer?.itemType.profileTemplate.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(templateItem: { collection: string | number; id: Key | null | undefined; item: any }) => {
const TemplateComponent = componentMap[templateItem.collection]
return TemplateComponent ? (
<TemplateComponent key={templateItem.id} item={item} {...templateItem.item} />
) : (
<div key={templateItem.id}>Component not found</div>
)
},
)}
</div>
)
}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { TextView } from '#components/Map'
import { ContactInfoView } from '#components/Profile/Subcomponents/ContactInfoView'
@ -16,14 +15,14 @@ export const OnepagerView = ({ item }: { item: Item }) => {
{item.user_created?.first_name && <ContactInfoView heading='Du hast Fragen?' item={item} />}
{/* Description Section */}
<div className='tw-my-10 tw-mt-2 tw-px-6 tw-text-sm '>
<TextView rawText={item.text || 'Keine Beschreibung vorhanden'} />
<TextView itemId={item.id} rawText={item.text ?? 'Keine Beschreibung vorhanden'} />
</div>
{/* Next Appointment Section */}
{item.next_appointment && (
<div className='tw-my-10 tw-px-6'>
<h2 className='tw-text-lg tw-font-semibold'>Nächste Termine</h2>
<div className='tw-mt-2 tw-text-sm'>
<TextView rawText={item.next_appointment} />
<TextView itemId={item.id} rawText={item.next_appointment} />
</div>
</div>
)}

View File

@ -5,7 +5,7 @@ import type { Item } from '#types/Item'
export const SimpleView = ({ item }: { item: Item }) => {
return (
<div className='tw-mt-8 tw-h-full tw-overflow-y-auto fade tw-px-6'>
<TextView item={item} />
<TextView text={item.text} itemId={item.id} />
</div>
)
}

View File

@ -113,6 +113,7 @@ export const TabsForm = ({
}
inputStyle='tw-h-24'
containerStyle='tw-pt-4'
required={false}
/>
</div>
</div>
@ -172,7 +173,7 @@ export const TabsForm = ({
name='my_tabs_2'
role='tab'
className='tw-tab [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]'
aria-label='Relations'
aria-label='Links'
checked={activeTab === 7 && true}
onChange={() => updateActiveTab(7)}
/>
@ -196,7 +197,7 @@ export const TabsForm = ({
loading={loading}
/>
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
<TextView truncate item={i} />
<TextView truncate itemId={item.id} />
</div>
</div>
))}
@ -207,7 +208,6 @@ export const TabsForm = ({
item={item}
existingRelations={state.relations}
triggerItemSelected={(id) => linkItem(id, item, updateItem)}
colorField={item.layer.itemColorField}
></ActionButton>
)}
</div>

View File

@ -108,9 +108,9 @@ export const TabsView = ({
<StartEndView item={item}></StartEndView>
</div>
)}
<TextView item={item} />
<TextView text={item.text} itemId={item.id} />
<div className='tw-h-4'></div>
<TextView item={item} itemTextField='contact' />
<TextView text={item.contact} itemId={item.id} />
</div>
{item.layer?.itemType.questlog && (
<>
@ -121,7 +121,7 @@ export const TabsView = ({
className={
'tw-tab tw-font-bold !tw-ps-2 !tw-pe-2 [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]'
}
aria-label={`${item.layer.itemType.icon_as_labels && activeTab !== 2 ? '❤️' : '❤️\u00A0Credibility'}`}
aria-label={`${item.layer.itemType.icon_as_labels && activeTab !== 2 ? '❤️' : '❤️\u00A0Trust'}`}
checked={activeTab === 2 && true}
onChange={() => updateActiveTab(2)}
/>
@ -161,7 +161,7 @@ export const TabsView = ({
appState.assetsApi.url +
getUserProfile(a.user_created.id)?.image
}
alt='Avatar Tailwind CSS Component'
alt='Avatar'
/>
</div>
</div>
@ -243,7 +243,7 @@ export const TabsView = ({
name='my_tabs_2'
role='tab'
className='tw-tab tw-font-bold !tw-ps-2 !tw-pe-2 [--tab-border-color:var(--fallback-bc,oklch(var(--bc)/0.2))]'
aria-label={`${item.layer.itemType.icon_as_labels && activeTab !== 7 ? '🔗' : '🔗\u00A0Relations'}`}
aria-label={`${item.layer.itemType.icon_as_labels && activeTab !== 7 ? '🔗' : '🔗\u00A0Links'}`}
checked={activeTab === 7 && true}
onChange={() => updateActiveTab(7)}
/>
@ -267,7 +267,7 @@ export const TabsView = ({
loading={loading}
/>
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
<TextView truncate item={i} />
<TextView truncate text={i.text} itemId={item.id} />
</div>
</div>
))}
@ -277,7 +277,6 @@ export const TabsView = ({
item={item}
existingRelations={relations}
triggerItemSelected={linkItem}
colorField={item.layer.itemColorField}
></ActionButton>
)}
</div>

View File

@ -46,7 +46,6 @@ const DialogModal = ({
<dialog
className={`${className ?? ''} tw-card tw-shadow-xl tw-absolute tw-right-0 tw-top-0 tw-bottom-0 tw-left-0 tw-m-auto tw-transition-opacity tw-duration-300 tw-p-4 tw-max-w-xl tw-bg-base-100`}
ref={ref}
// eslint-disable-next-line react/no-unknown-property
onCancel={onClose}
onClick={(e) =>
ref.current && !isClickInsideRectangle(e, ref.current) && closeOnClickOutside && onClose()

View File

@ -2,14 +2,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useNavigate } from 'react-router-dom'
import { StartEndView, TextView } from '#components/Map'
import useWindowDimensions from '#components/Map/hooks/useWindowDimension'
import { HeaderView } from '#components/Map/Subcomponents/ItemPopupComponents/HeaderView'
import { getValue } from '#utils/GetValue'
import { DateUserInfo } from './DateUserInfo'
@ -19,13 +17,11 @@ export const ItemCard = ({
i,
loading,
url,
parameterField,
deleteCallback,
}: {
i: Item
loading: boolean
url: string
parameterField: string
deleteCallback: any
}) => {
const navigate = useNavigate()
@ -35,27 +31,23 @@ export const ItemCard = ({
<div
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-p-4 tw-mb-4 tw-h-fit'
onClick={() => {
// We could have an onClick callback instead
const params = new URLSearchParams(window.location.search)
if (windowDimensions.width < 786 && i.position)
navigate('/' + getValue(i, parameterField) + `${params ? `?${params}` : ''}`)
else navigate(url + getValue(i, parameterField) + `${params ? `?${params}` : ''}`)
navigate('/' + i.id + `${params ? `?${params}` : ''}`)
else navigate(url + i.id + `${params ? `?${params}` : ''}`)
}}
>
<HeaderView
loading={loading}
item={i}
api={i.layer?.api}
itemAvatarField={i.layer?.itemAvatarField}
itemNameField={i.layer?.itemNameField}
itemSubnameField={i.layer?.itemSubnameField}
editCallback={() => navigate('/edit-item/' + i.id)}
deleteCallback={() => deleteCallback(i)}
></HeaderView>
<div className='tw-overflow-y-auto tw-overflow-x-hidden tw-max-h-64 fade'>
{i.layer?.itemType.show_start_end && <StartEndView item={i}></StartEndView>}
{i.layer?.itemType.show_text && (
<TextView truncate item={i} itemTextField={i.layer.itemTextField} />
)}
{i.layer?.itemType.show_text && <TextView truncate text={i.text} itemId={i.id} />}
</div>
<DateUserInfo item={i}></DateUserInfo>
</div>

View File

@ -8,7 +8,6 @@ import { useNavigate } from 'react-router-dom'
import { useItems } from '#components/Map/hooks/useItems'
import { useTags } from '#components/Map/hooks/useTags'
import { getValue } from '#utils/GetValue'
import { MapOverlayPage } from './MapOverlayPage'
import { TagView } from './TagView'
@ -42,21 +41,16 @@ export const MarketView = () => {
useEffect(() => {
setOffers([])
setNeeds([])
items.map((i) => {
i.layer?.itemOffersField &&
getValue(i, i.layer.itemOffersField)?.map((o) => {
const tag = tags.find((t) => t.id === o.tags_id)
tag && setOffers((current) => [...current, tag])
return null
})
i.layer?.itemNeedsField &&
getValue(i, i.layer.itemNeedsField)?.map((n) => {
const tag = tags.find((t) => t.id === n.tags_id)
tag && setNeeds((current) => [...current, tag])
return null
})
return null
})
for (const item of items) {
item.offers?.forEach((o) => {
const tag = tags.find((t) => t.id === o.tags_id)
tag && setOffers((current) => [...current, tag])
})
item.needs?.forEach((n) => {
const tag = tags.find((t) => t.id === n.tags_id)
tag && setNeeds((current) => [...current, tag])
})
}
// eslint-disable-next-line no-console
console.log(offers)

View File

@ -30,12 +30,10 @@ import type { Item } from '#types/Item'
export const OverlayItemsIndexPage = ({
url,
layerName,
parameterField,
plusButton = true,
}: {
layerName: string
url: string
parameterField: string
plusButton?: boolean
}) => {
const [loading, setLoading] = useState<boolean>(false)
@ -165,7 +163,6 @@ export const OverlayItemsIndexPage = ({
i={i}
loading={loading}
url={url}
parameterField={parameterField}
deleteCallback={() => deleteItem(i)}
/>
</div>

View File

@ -0,0 +1,4 @@
export function containsUUID(str: string): boolean {
const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i
return uuidRegex.test(str)
}

View File

@ -1,14 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
export function getValue(obj, path) {
if (!obj || typeof path !== 'string') return undefined
const pathArray = path.split('.') // Use a different variable for the split path
for (let i = 0, len = pathArray.length; i < len; i++) {
if (!obj) return undefined // Check if obj is falsy at each step
// eslint-disable-next-line security/detect-object-injection
obj = obj[pathArray[i]] // Dive one level deeper
}
return obj // Return the final value
}

View File

@ -61,6 +61,16 @@ const addIcon = (icon: string) => {
return '<svg class="flower-icon" stroke="currentColor" fill="#fff" stroke-width="0" viewBox="0 0 256 256" height="1.5em" width="1.5em" xmlns="http://www.w3.org/2000/svg"><path d="M210.35,129.36c-.81-.47-1.7-.92-2.62-1.36.92-.44,1.81-.89,2.62-1.36a40,40,0,1,0-40-69.28c-.81.47-1.65,1-2.48,1.59.08-1,.13-2,.13-3a40,40,0,0,0-80,0c0,.94,0,1.94.13,3-.83-.57-1.67-1.12-2.48-1.59a40,40,0,1,0-40,69.28c.81.47,1.7.92,2.62,1.36-.92.44-1.81.89-2.62,1.36a40,40,0,1,0,40,69.28c.81-.47,1.65-1,2.48-1.59-.08,1-.13,2-.13,2.95a40,40,0,0,0,80,0c0-.94-.05-1.94-.13-2.95.83.57,1.67,1.12,2.48,1.59A39.79,39.79,0,0,0,190.29,204a40.43,40.43,0,0,0,10.42-1.38,40,40,0,0,0,9.64-73.28ZM104,128a24,24,0,1,1,24,24A24,24,0,0,1,104,128Zm74.35-56.79a24,24,0,1,1,24,41.57c-6.27,3.63-18.61,6.13-35.16,7.19A40,40,0,0,0,154.53,98.1C163.73,84.28,172.08,74.84,178.35,71.21ZM128,32a24,24,0,0,1,24,24c0,7.24-4,19.19-11.36,34.06a39.81,39.81,0,0,0-25.28,0C108,75.19,104,63.24,104,56A24,24,0,0,1,128,32ZM44.86,80a24,24,0,0,1,32.79-8.79c6.27,3.63,14.62,13.07,23.82,26.89A40,40,0,0,0,88.81,120c-16.55-1.06-28.89-3.56-35.16-7.18A24,24,0,0,1,44.86,80ZM77.65,184.79a24,24,0,1,1-24-41.57c6.27-3.63,18.61-6.13,35.16-7.19a40,40,0,0,0,12.66,21.87C92.27,171.72,83.92,181.16,77.65,184.79ZM128,224a24,24,0,0,1-24-24c0-7.24,4-19.19,11.36-34.06a39.81,39.81,0,0,0,25.28,0C148,180.81,152,192.76,152,200A24,24,0,0,1,128,224Zm83.14-48a24,24,0,0,1-32.79,8.79c-6.27-3.63-14.62-13.07-23.82-26.89A40,40,0,0,0,167.19,136c16.55,1.06,28.89,3.56,35.16,7.18A24,24,0,0,1,211.14,176Z"></path></svg>'
case 'network':
return '<svg class="network-icon" stroke="currentColor" fill="#fff" stroke-width="0" viewBox="0 0 256 256" height="1.5em" width="1.5em" xmlns="http://www.w3.org/2000/svg"><path d="M212,200a36,36,0,1,1-69.85-12.25l-53-34.05a36,36,0,1,1,0-51.4l53-34a36.09,36.09,0,1,1,8.67,13.45l-53,34.05a36,36,0,0,1,0,24.5l53,34.05A36,36,0,0,1,212,200Z"></path></svg>'
case 'crosshair':
return '<svg class="network-icon" stroke="currentColor" fill="#fff" stroke-width="0" viewBox="0 0 512 512" height="1.5em" width="1.5em" xmlns="http://www.w3.org/2000/svg"><path d="M256 0c17.7 0 32 14.3 32 32l0 10.4c93.7 13.9 167.7 88 181.6 181.6l10.4 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-10.4 0c-13.9 93.7-88 167.7-181.6 181.6l0 10.4c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-10.4C130.3 455.7 56.3 381.7 42.4 288L32 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l10.4 0C56.3 130.3 130.3 56.3 224 42.4L224 32c0-17.7 14.3-32 32-32zM107.4 288c12.5 58.3 58.4 104.1 116.6 116.6l0-20.6c0-17.7 14.3-32 32-32s32 14.3 32 32l0 20.6c58.3-12.5 104.1-58.4 116.6-116.6L384 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l20.6 0C392.1 165.7 346.3 119.9 288 107.4l0 20.6c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-20.6C165.7 119.9 119.9 165.7 107.4 224l20.6 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-20.6 0zM256 224a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"></path></svg>'
case 'shop':
return '<svg class="shop-icon" stroke="currentColor" fill="#fff" stroke-width="0" viewBox="0 0 640 512" height="1.25em" width="1.25em" xmlns="http://www.w3.org/2000/svg"><path d="M36.8 192l566.3 0c20.3 0 36.8-16.5 36.8-36.8c0-7.3-2.2-14.4-6.2-20.4L558.2 21.4C549.3 8 534.4 0 518.3 0L121.7 0c-16 0-31 8-39.9 21.4L6.2 134.7c-4 6.1-6.2 13.2-6.2 20.4C0 175.5 16.5 192 36.8 192zM64 224l0 160 0 80c0 26.5 21.5 48 48 48l224 0c26.5 0 48-21.5 48-48l0-80 0-160-64 0 0 160-192 0 0-160-64 0zm448 0l0 256c0 17.7 14.3 32 32 32s32-14.3 32-32l0-256-64 0z"></path></svg>'
case 'plant':
return '<svg class="plant-icon" stroke="currentColor" stroke-width="0" fill="#fff" viewBox="0 0 256 256" height="1.5em" width="1.5em" xmlns="http://www.w3.org/2000/svg"><path d="M205.41,159.07a60.9,60.9,0,0,1-31.83,8.86,71.71,71.71,0,0,1-27.36-5.66A55.55,55.55,0,0,0,136,194.51V224a8,8,0,0,1-8.53,8,8.18,8.18,0,0,1-7.47-8.25V211.31L81.38,172.69A52.5,52.5,0,0,1,63.44,176a45.82,45.82,0,0,1-23.92-6.67C17.73,156.09,6,125.62,8.27,87.79a8,8,0,0,1,7.52-7.52c37.83-2.23,68.3,9.46,81.5,31.25A46,46,0,0,1,103.74,140a4,4,0,0,1-6.89,2.43l-19.2-20.1a8,8,0,0,0-11.31,11.31l53.88,55.25c.06-.78.13-1.56.21-2.33a68.56,68.56,0,0,1,18.64-39.46l50.59-53.46a8,8,0,0,0-11.31-11.32l-49,51.82a4,4,0,0,1-6.78-1.74c-4.74-17.48-2.65-34.88,6.4-49.82,17.86-29.48,59.42-45.26,111.18-42.22a8,8,0,0,1,7.52,7.52C250.67,99.65,234.89,141.21,205.41,159.07Z"></path></svg>'
case 'circle-dot':
return '<svg class="circle-dot-icon" stroke="#fff" fill="transparent" stroke-width="2.5" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="1.55em" width="1.55em" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="1"></circle></svg>'
case 'cannabis':
return '<svg class="network-icon" stroke="currentColor" fill="#fff" stroke-width="0" viewBox="0 0 512 512" height="1.5em" width="1.5em" xmlns="http://www.w3.org/2000/svg"><path d="M256 0c5.3 0 10.3 2.7 13.3 7.1c15.8 23.5 36.7 63.7 49.2 109c7.2 26.4 11.8 55.2 10.4 84c11.5-8.8 23.7-16.7 35.8-23.6c41-23.3 84.4-36.9 112.2-42.5c5.2-1 10.7 .6 14.4 4.4s5.4 9.2 4.4 14.5c-5.6 27.7-19.3 70.9-42.7 111.7c-9.1 15.9-19.9 31.7-32.4 46.3c27.8 6.6 52.4 17.3 67.2 25.5c5.1 2.8 8.2 8.2 8.2 14s-3.2 11.2-8.2 14c-15.2 8.4-40.9 19.5-69.8 26.1c-20.2 4.6-42.9 7.2-65.2 4.6l8.3 33.1c1.5 6.1-.6 12.4-5.5 16.4s-11.6 4.6-17.2 1.9L280 417.2l0 70.8c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-70.8-58.5 29.1c-5.6 2.8-12.3 2.1-17.2-1.9s-7-10.3-5.5-16.4l8.3-33.1c-22.2 2.6-45 0-65.2-4.6c-28.9-6.6-54.6-17.6-69.8-26.1c-5.1-2.8-8.2-8.2-8.2-14s3.2-11.2 8.2-14c14.8-8.2 39.4-18.8 67.2-25.5C78.9 296.3 68.1 280.5 59 264.6c-23.4-40.8-37.1-84-42.7-111.7c-1.1-5.2 .6-10.7 4.4-14.5s9.2-5.4 14.4-4.4c27.9 5.5 71.2 19.2 112.2 42.5c12.1 6.9 24.3 14.7 35.8 23.6c-1.4-28.7 3.1-57.6 10.4-84c12.5-45.3 33.4-85.5 49.2-109c3-4.4 8-7.1 13.3-7.1z"></path></svg>'
default:
return ''
}

View File

@ -1,4 +1,3 @@
// eslint-disable-next-line import/no-unassigned-import
import './index.css'
export {
@ -19,7 +18,6 @@ export {
export { AppShell, Content, SideBar, Sitemap } from './Components/AppShell'
export {
AuthProvider,
useAuth,
LoginPage,
SignupPage,
RequestPasswordPage,
@ -39,6 +37,8 @@ export {
} from './Components/Templates'
export { TextInput, TextAreaInput, SelectBox } from './Components/Input'
export * from './types'
declare global {
interface Window {
my_modal_3: {

View File

@ -17,4 +17,6 @@ export interface FormState {
offers: Tag[]
needs: Tag[]
relations: Item[]
start: string
end: string
}

View File

@ -1,14 +1,26 @@
import type { ItemsApi } from './ItemsApi'
import type { ItemType } from './ItemType'
import type { LayerProps } from './LayerProps'
import type { Relation } from './Relation'
import type { UserItem } from './UserItem'
import type { Point } from 'geojson'
type TagIds = { tags_id: string }[]
interface GalleryItem {
directus_files_id: {
id: number
width: number
height: number
}
}
export interface Item {
id: string
name: string
text: string
position?: Point
text?: string
data?: string
position?: Point | null
date_created?: string
date_updated?: string | null
start?: string
@ -24,8 +36,22 @@ export interface Item {
slug?: string
user_created?: UserItem
image?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
group_type?: string
offers?: TagIds
needs?: TagIds
status?: string
color?: string
markerIcon?: string
avatar?: string
new?: boolean
contact?: string
telephone?: string
next_appointment?: string
type?: ItemType
gallery?: GalleryItem[]
// {
// coordinates: [number, number]
/* constructor(
id: string,
name: string,

15
src/types/ItemType.d.ts vendored Normal file
View File

@ -0,0 +1,15 @@
import type { Key } from 'react'
export interface ItemType {
name: string
show_start_end: boolean
show_text: boolean
// eslint-disable-next-line @typescript-eslint/no-explicit-any
profileTemplate: { collection: string | number; id: Key | null | undefined; item: any }[]
offers_and_needs: boolean
icon_as_labels: unknown
relations: boolean
template: string
show_start_end_input: boolean
questlog: boolean
}

View File

@ -2,7 +2,7 @@ export interface ItemsApi<T> {
getItems(): Promise<T[]>
getItem?(id: string): Promise<T>
createItem?(item: T): Promise<T>
updateItem?(item: T): Promise<T>
updateItem?(item: Partial<T>): Promise<T>
deleteItem?(id: string): Promise<boolean>
collectionName?: string
}

View File

@ -18,17 +18,6 @@ export interface LayerProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
api?: ItemsApi<any>
itemType: ItemType
itemNameField?: string
itemSubnameField?: string
itemTextField?: string
itemAvatarField?: string
itemColorField?: string
itemOwnerField?: string
itemTagsField?: string
itemLatitudeField?: string
itemLongitudeField?: string
itemOffersField?: string
itemNeedsField?: string
onlyOnePerOwner?: boolean
customEditLink?: string
customEditParameter?: string

View File

@ -13,4 +13,5 @@ export interface UtopiaMapProps {
showLayerControl?: boolean
showGratitudeControl?: boolean
infoText?: string
donationWidget?: boolean
}

9
src/types/index.ts Normal file
View File

@ -0,0 +1,9 @@
export type { ItemsApi } from './ItemsApi'
export type { Tag } from './Tag'
export type { Item } from './Item'
export type { Permission } from './Permission'
export type { LayerProps } from './LayerProps'
export type { UserApi } from './UserApi'
export type { UserItem } from './UserItem'
export type { UtopiaMapProps } from './UtopiaMapProps'
export type { AssetsApi } from './AssetsApi'

View File

@ -1,14 +1,15 @@
{
"compilerOptions": {
"outDir": "dist",
"declaration": true,
"declarationDir": "dist/types", // 🔹 Muss innerhalb von dist/ liegen
"emitDeclarationOnly": true, // Nur `.d.ts` generieren, kein JavaScript
"module": "esnext",
"target": "es5",
"target": "ESNext",
"lib": ["es6", "dom","es2015", "es2016", "es2017", "es2020"],
"sourceMap": true,
"allowJs": false,
"jsx": "react-jsx",
"declaration": true,
"declarationDir": "./types",
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
@ -16,18 +17,20 @@
"strictNullChecks": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"paths": {
"#components/*": ["./src/Components/*"],
"#utils/*": ["./src/Utils/*"],
"#src/*": ["./src/*"],
"#types/*": ["./types/*"],
"#types/*": ["./src/types/*"],
"#root/*": ["./*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist", "example", "rollup.config.mjss"],
"typeRoots": [
"./types",
"./node_modules/@types/"
]
}
"include": ["src", "vite.config.ts", "setupTest.ts", "cypress.config.ts", "cypress/support/commands.ts", "cypress/support/component.ts"],
"exclude": ["node_modules", "dist", "example", "rollup.config.mjss"],
"typeRoots": [
"./types",
"./node_modules/@types/"
]
}

5
types/ItemType.d.ts vendored
View File

@ -1,5 +0,0 @@
export interface ItemType {
name: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
}

23
vite.config.ts Normal file
View File

@ -0,0 +1,23 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import { configDefaults } from 'vitest/config'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'happy-dom',
setupFiles: ['setupTest.ts'],
coverage: {
all: true,
include: ['src/**/*.{js,jsx,ts,tsx}'],
exclude: [...configDefaults.exclude],
thresholds: {
lines: 0,
functions: 66,
branches: 66,
statements: 0,
},
},
},
})