mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
Merge main
This commit is contained in:
commit
a3d7fa8496
@ -1,3 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
examples/
|
||||
examples/
|
||||
docs/
|
||||
coverage/
|
||||
@ -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
56
.github/workflows/deploy.docs.yml
vendored
Normal 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
|
||||
32
.github/workflows/test.build.yml
vendored
32
.github/workflows/test.build.yml
vendored
@ -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
52
.github/workflows/test.docs.yml
vendored
Normal 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: ./
|
||||
75
.github/workflows/test.lint.examples.yml
vendored
Normal file
75
.github/workflows/test.lint.examples.yml
vendored
Normal 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
33
.github/workflows/test.unit.yml
vendored
Normal 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
5
.gitignore
vendored
@ -88,4 +88,7 @@ lerna-debug.log
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# docs
|
||||
/docs
|
||||
11
cypress.config.ts
Normal file
11
cypress.config.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'react',
|
||||
bundler: 'vite',
|
||||
},
|
||||
specPattern: ['**/**/*.cy.{ts,tsx}'],
|
||||
},
|
||||
})
|
||||
37
cypress/support/commands.ts
Normal file
37
cypress/support/commands.ts
Normal 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>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
12
cypress/support/component-index.html
Normal file
12
cypress/support/component-index.html
Normal 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>
|
||||
38
cypress/support/component.ts
Normal file
38
cypress/support/component.ts
Normal 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 />)
|
||||
2434
examples/2-static-layers/package-lock.json
generated
2434
examples/2-static-layers/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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
8166
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
57
package.json
57
package.json
@ -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/*",
|
||||
|
||||
142
rollup.config.js
142
rollup.config.js
@ -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
9
scripts/docs-coverage.sh
Executable 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
2
setupTest.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
import '@testing-library/jest-dom'
|
||||
@ -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
|
||||
|
||||
@ -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])
|
||||
|
||||
66
src/Components/Input/TextInput.cy.tsx
Normal file
66
src/Components/Input/TextInput.cy.tsx
Normal 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')
|
||||
})
|
||||
})
|
||||
32
src/Components/Input/TextInput.spec.tsx
Normal file
32
src/Components/Input/TextInput.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
38
src/Components/Input/__snapshots__/TextInput.spec.tsx.snap
Normal file
38
src/Components/Input/__snapshots__/TextInput.spec.tsx.snap
Normal 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>
|
||||
`;
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -57,6 +57,10 @@ export default function AddButton({
|
||||
onClick={() => {
|
||||
triggerAction(layer)
|
||||
}}
|
||||
onTouchEnd={(e) => {
|
||||
triggerAction(layer)
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={layer.menuIcon}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()))
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
@ -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'
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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'
|
||||
>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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'>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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='© <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='© <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} />
|
||||
|
||||
@ -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
|
||||
})
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
},
|
||||
|
||||
@ -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',
|
||||
}}
|
||||
>
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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',
|
||||
}}
|
||||
>
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
36
src/Components/Profile/Subcomponents/GalleryView.tsx
Normal file
36
src/Components/Profile/Subcomponents/GalleryView.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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])
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
4
src/Utils/ContainsUUID.ts
Normal file
4
src/Utils/ContainsUUID.ts
Normal 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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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 ''
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -17,4 +17,6 @@ export interface FormState {
|
||||
offers: Tag[]
|
||||
needs: Tag[]
|
||||
relations: Item[]
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
34
types/Item.d.ts → src/types/Item.d.ts
vendored
34
types/Item.d.ts → src/types/Item.d.ts
vendored
@ -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
15
src/types/ItemType.d.ts
vendored
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
0
types/Tag.d.ts → src/types/Tag.d.ts
vendored
0
types/Tag.d.ts → src/types/Tag.d.ts
vendored
@ -13,4 +13,5 @@ export interface UtopiaMapProps {
|
||||
showLayerControl?: boolean
|
||||
showGratitudeControl?: boolean
|
||||
infoText?: string
|
||||
donationWidget?: boolean
|
||||
}
|
||||
9
src/types/index.ts
Normal file
9
src/types/index.ts
Normal 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'
|
||||
@ -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
5
types/ItemType.d.ts
vendored
@ -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
23
vite.config.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user