Merge main

This commit is contained in:
Maximilian Harz 2025-01-25 10:26:33 +01:00
commit 0c60f9fc7f
136 changed files with 12083 additions and 568 deletions

View File

@ -1,2 +1,3 @@
node_modules/
dist/
examples/

View File

@ -12,7 +12,7 @@ module.exports = {
'plugin:import/recommended',
'plugin:import/typescript',
// 'plugin:promise/recommended',
// 'plugin:security/recommended-legacy',
'plugin:security/recommended-legacy',
'plugin:react/recommended',
],
parserOptions: {
@ -23,9 +23,9 @@ module.exports = {
plugins: [
'@typescript-eslint',
'import',
// 'promise',
// 'security',
// 'no-catch-all',
'promise',
'security',
'no-catch-all',
'react',
'react-hooks',
],
@ -44,7 +44,7 @@ module.exports = {
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies
'react/react-in-jsx-scope': 'off', // Disable requirement for React import
// 'no-catch-all/no-catch-all': 'error',
'no-catch-all/no-catch-all': 'error',
'no-console': 'error',
'no-debugger': 'error',
camelcase: 'error',
@ -79,7 +79,7 @@ module.exports = {
'import/no-relative-parent-imports': [
'error',
{
ignore: ['#[src,root,components,utils]/*'],
ignore: ['#[src,types,root,components,utils]/*'],
},
],
'import/no-self-import': 'error',
@ -122,21 +122,21 @@ module.exports = {
},
],
'import/prefer-default-export': 'off',
// // promise
// 'promise/catch-or-return': 'error',
// 'promise/no-return-wrap': 'error',
// 'promise/param-names': 'error',
// 'promise/always-return': 'error',
// 'promise/no-native': 'off',
// 'promise/no-nesting': 'warn',
// 'promise/no-promise-in-callback': 'warn',
// 'promise/no-callback-in-promise': 'warn',
// 'promise/avoid-new': 'warn',
// 'promise/no-new-statics': 'error',
// 'promise/no-return-in-finally': 'warn',
// 'promise/valid-params': 'warn',
// 'promise/prefer-await-to-callbacks': 'error',
// 'promise/no-multiple-resolved': 'error',
// promise
'promise/catch-or-return': 'error',
'promise/no-return-wrap': 'error',
'promise/param-names': 'error',
'promise/always-return': 'error',
'promise/no-native': 'off',
'promise/no-nesting': 'warn',
'promise/no-promise-in-callback': 'warn',
'promise/no-callback-in-promise': 'warn',
'promise/avoid-new': 'warn',
'promise/no-new-statics': 'error',
'promise/no-return-in-finally': 'warn',
'promise/valid-params': 'warn',
'promise/prefer-await-to-callbacks': 'error',
'promise/no-multiple-resolved': 'error',
},
overrides: [
{
@ -156,6 +156,7 @@ module.exports = {
'plugin:@typescript-eslint/strict',
],
rules: {
'@typescript-eslint/consistent-type-imports': 'error',
// allow explicitly defined dangling promises
'@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
'no-void': ['error', { allowAsStatement: true }],

View File

@ -33,6 +33,7 @@ jobs:
docker
release
workflow
source
other
# Configure that a scope must always be provided.
requireScope: true
@ -73,4 +74,4 @@ jobs:
# special "[WIP]" prefix to indicate this state. This will avoid the
# validation of the PR title and the pull request checks remain pending.
# Note that a second check will be reported if this is enabled.
wip: true
wip: true

108
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,108 @@
# Code of Conduct
## Purpose
Our community is dedicated to fostering an open, inclusive, and respectful environment. This Code of Conduct outlines our expectations for all participants and the steps for addressing any unacceptable behavior. By adhering to these guidelines, we can create a space where collaboration and learning thrive.
---
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Ensure that your contributions are relevant to the discussion and meeting agendas.
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Repeatedly deviating from the agenda during meetings, interrupting discussions, or monopolizing speaking time with irrelevant or incoherent contributions.
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
---
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines when determining consequences for violations of this Code of Conduct.
### 1. Correction
**Community Impact:** Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
**Consequence:** A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact:** A violation through a single incident or series of actions.
**Consequence:** A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
**Community Impact:** A serious violation of community standards, including sustained inappropriate behavior.
**Consequence:** A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact:** Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence:** A permanent ban from any sort of public interaction within the community.
---
## Conflict Resolution Process
To address and resolve conflicts effectively, the community will follow these steps:
### Step 1: Direct Communication
If a conflict arises, the involved parties are encouraged to address the issue directly and respectfully with one another to seek resolution. This conversation should focus on the behavior or issue at hand, avoiding personal attacks or assumptions.
### Step 2: Mediation
If direct communication does not resolve the issue, either party may request mediation from a neutral community leader. The mediator will facilitate a structured conversation to help clarify misunderstandings and find common ground.
### Step 3: Formal Review
If the conflict remains unresolved, the issue will be escalated to the community leadership team for review. The leadership team will gather relevant information, including accounts from all parties involved, and make a decision based on the Code of Conduct and the communitys best interests.
### Step 4: Resolution and Follow-Up
The leadership team will communicate their decision and any actions to be taken to the involved parties. Follow-up will be conducted to ensure that the resolution is effective and that no further issues arise.
---
## Scope
This Code of Conduct applies to all community spaces, including but not limited to:
- Online forums, repositories, and communication channels.
- In-person or virtual meetings and events.
- External channels where the community is represented (e.g., social media).
---
## Reporting
If you experience or witness behavior that violates this Code of Conduct, please report it to the community leaders via [info@utopia-lab.org](mailto:info@utopia-lab.org). All reports will be handled with discretion and confidentiality.
---
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.

3
Components.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

115
README.md
View File

@ -1,125 +1,36 @@
# Utopia UI [![npm version](https://img.shields.io/npm/v/utopia-ui.svg)](https://www.npmjs.com/package/utopia-ui)
# Utopia UI [![npm version](https://img.shields.io/npm/v/utopia-ui.svg)](https://www.npmjs.com/package/utopia-ui) ![Build Status](https://img.shields.io/github/actions/workflow/status/utopia-os/utopia-ui/test.build.yml?branch=main) ![License](https://img.shields.io/github/license/utopia-os/utopia-ui)
**UI Framework for Real-Life-Networking-Apps**
*Real change happens in real life when we meet in person and connect as local communities manifesting their ideas with the earth. When we help each other to step out of capitalism and individualism and start building common infrastructure to meet human needs in harmony with Mother Earth.*
*Real change happens in real life when we meet in person and connect as local communities manifesting their ideas with the earth. When we help each other to step out of our bubbles at home and start building common infrastructure to meet human needs in harmony with Mother Earth.*
*That is why Utopia UI exists. It is a UI kit for minimalist, fast, intuitive and mobile-first map apps, as a tool for local connection and decentralised networking. It can work with any backend or p2p database and any kind of data structure.*
*That is why Utopia UI exists. It is a UI kit for minimalist, fast, intuitive and mobile-first map apps, as a tool for local connection and decentralised networking. We believe in maps as the perfect link between digital tools and real life action*
*It can work with any backend or p2p database and any kind of data structure.*
## Mission
Utopia UIs mission is to provide open source building blocks to create beautiful applications with a focus on real life impact, local communities and gamification.
The building blocks are designed to allow different networks and communities to assemble their map and app for their specific needs and purpose.
Utopia Game is one of the apps made with Utopia UI. It is an attempt to use gamification to get users to take action and make the map even more alive. Check it out at [utopia-game.org](https://utopia-game.org/) or see the code in the [repository](https://github.com/utopia-os/utopia-game)
It is the base of [Utopia Map](https://github.com/utopia-os/utopia-map) and [Utopia Game](https://github.com/utopia-os/utopia-game).
## Features
* Interactive Component Map with customizable Layers (like Projects, Event, People)
* Flexible API-Interface to make it work with every backend or p2p database
* Create, Update, Delete Items
* User Authentification API-Interface
* User Profiles
* App Shell
* User authentification API-Interface
* Customizable Profiles for users and other items
* App shell with navigation bar and sidebar
## Getting Started
### Basic Map
In this tutorial we learn how we create a basic React app with a Map component using [utopia-ui](https://github.com/utopia-os/utopia-ui) library.
See our [`/exampes`](/examples)
For this tutorial we use Vite to create an empty React app called "utopia-static-map"
## Components
```shell
npm create vite@latest utopia-static-map -- --template react
```
We open our new app in the terminal and install the [utopia-ui](https://github.com/utopia-os/utopia-ui) package
```shell
cd utopia-static-map
npm install utopia-ui
```
We open our `src/App.jsx` and we replace the content with
```jsx
import { UtopiaMap } from "utopia-ui"
function App() {
return (
<UtopiaMap center={[50.6, 9.5]} zoom={5} height='100dvh' width="100dvw">
</UtopiaMap>
)
}
export default App
```
Then we start the development server to check out the result in our browser:
```shell
npm run dev
```
And can open our first map app in the browser 🙂
### Static Layers
Now we add some static layer.
First we put some sample data in a new file called `src/sample-data.js`
```javascript
export const places = [{
"id": 51,
"name": "Stadtgemüse",
"text": "Stadtgemüse Fulda ist eine Gemüsegärtnerei in Maberzell, die es sich zur Aufgabe gemacht hat, die Stadt und seine Bewohner:innen mit regionalem, frischem und natürlich angebautem Gemüse mittels Gemüsekisten zu versorgen. Es gibt also jede Woche, von Frühjahr bis Herbst, angepasst an die Saison eine Kiste mit schmackhaftem und frischem Gemüse für euch, welche ihr direkt vor Ort abholen könnt. \r\n\r\nhttps://stadtgemuese-fulda.de",
"position": { "type": "Point", "coordinates": [9.632435, 50.560342] },
},
{
"id": 166,
"name": "Weidendom",
"text": "free camping",
"position": { "type": "Point", "coordinates": [9.438793, 50.560112] },
}];
export const events = [
{
"id": 423,
"name": "Hackathon",
"text": "still in progress",
"position": { "type": "Point", "coordinates": [10.5, 51.62] },
"start": "2022-03-25T12:00:00",
"end": "2022-05-12T12:00:00",
}
]
```
We want to create two Layers. One we want to call *Places* and the other *Events*
we import our sample data to the `src/App.jsx`
```jsx
import { events, places } from "./sample-data"
```
and than we create our two `<Layer>` inside of our `<UtopiaMap>` component
```jsx
<UtopiaMap center={[50.6, 15.5]} zoom={5} height='100dvh' width="100dvw">
<Layer
name='events'
markerIcon='calendar'
markerShape='square'
markerDefaultColor='#700'
data={events} />
<Layer
name='places'
markerIcon='point'
markerShape='circle'
markerDefaultColor='#007'
data={places} />
</UtopiaMap>
```
![Utopia UI Components](Components.svg)
## Map Component
The map shows various Layers (like places, events, profiles ...) of Items at their respective position whith nice and informative Popup and Profiles.

24
examples/1-basic-map/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1 @@
v18.19.1

View File

@ -0,0 +1,42 @@
# Example 1: Basic Map
In this example we see how we create a basic React app with a Map component using [utopia-ui](https://github.com/utopia-os/utopia-ui) library.
For this example we use Vite to create an empty React app called "utopia-static-map"
```shell
npm create vite@latest utopia-static-map -- --template react-ts
```
We open our new app in the terminal and install the [utopia-ui](https://github.com/utopia-os/utopia-ui) package
```shell
cd utopia-static-map
npm install utopia-ui
```
We open our `src/App.tsx` and we replace the content with
```tsx
import { UtopiaMap } from "utopia-ui"
function App() {
return (
<UtopiaMap center={[50.6, 9.5]} zoom={5} height='100dvh' width="100dvw">
</UtopiaMap>
)
}
export default App
```
Then we start the development server to check out the result in our browser:
```shell
npm run dev
```
And can open our first map app in the browser 🙂
In [Tutorial 2](../2-static-layers/) we gonna add some static data to our map

View File

@ -0,0 +1,25 @@
import js from '@eslint/js'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import globals from 'globals'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
},
},
)

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

5350
examples/1-basic-map/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
{
"name": "tutorial-1-basic-map",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"utopia-ui": "^3.0.35"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.2",
"vite": "^6.0.5"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,10 @@
import { UtopiaMap } from "utopia-ui"
function App() {
return (
<UtopiaMap center={[50.6, 9.5]} zoom={5} height='100dvh' width="100dvw">
</UtopiaMap>
)
}
export default App

View File

@ -0,0 +1,9 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

24
examples/2-static-layers/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1 @@
v18.19.1

View File

@ -0,0 +1,62 @@
# Example 2: Static Layers
[Example 1](/1-basic-map) shows us how we create a basic map app with [utopia-ui](https://github.com/utopia-os/utopia-ui). Now we add some static layer.
First we put some sample data in a new file called `src/sample-data.ts`
```javascript
export const places = [{
"id": 51,
"name": "Stadtgemüse",
"text": "Stadtgemüse Fulda ist eine Gemüsegärtnerei in Maberzell, die es sich zur Aufgabe gemacht hat, die Stadt und seine Bewohner:innen mit regionalem, frischem und natürlich angebautem Gemüse mittels Gemüsekisten zu versorgen. Es gibt also jede Woche, von Frühjahr bis Herbst, angepasst an die Saison eine Kiste mit schmackhaftem und frischem Gemüse für euch, welche ihr direkt vor Ort abholen könnt. \r\n\r\nhttps://stadtgemuese-fulda.de",
"position": { "type": "Point", "coordinates": [9.632435, 50.560342] },
},
{
"id": 166,
"name": "Weidendom",
"text": "free camping",
"position": { "type": "Point", "coordinates": [9.438793, 50.560112] },
}];
export const events = [
{
"id": 423,
"name": "Hackathon",
"text": "still in progress",
"position": { "type": "Point", "coordinates": [10.5, 51.62] },
"start": "2022-03-25T12:00:00",
"end": "2022-05-12T12:00:00",
}
]
```
We want to create two Layers. One we want to call *Places* and the other *Events*
we import our sample data to the `src/App.tsx`
```tsx
import { events, places } from "./sample-data"
```
and than we create our two `<Layer>` inside of our `<UtopiaMap>` component
```tsx
<UtopiaMap center={[50.6, 15.5]} zoom={5} height='100dvh' width="100dvw">
<Layer
name='events'
markerIcon='calendar'
markerShape='square'
markerDefaultColor='#700'
data={events} />
<Layer
name='places'
markerIcon='point'
markerShape='circle'
markerDefaultColor='#007'
data={places} />
</UtopiaMap>
```
And we see our map with two layers:
```shell
npm run dev
```

View File

@ -0,0 +1,25 @@
import js from '@eslint/js'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import globals from 'globals'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
},
},
)

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

5350
examples/2-static-layers/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
{
"name": "tutorial-2-static-layer",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"utopia-ui": "^3.0.35"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.2",
"vite": "^6.0.5"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,23 @@
import { UtopiaMap, Layer } from "utopia-ui"
import { events, places } from "./sample-data"
function App() {
return (
<UtopiaMap center={[50.6, 15.5]} zoom={5} height='100dvh' width="100dvw">
<Layer
name='events'
markerIcon='calendar'
markerShape='square'
markerDefaultColor='#700'
data={events} />
<Layer
name='places'
markerIcon='point'
markerShape='circle'
markerDefaultColor='#007'
data={places} />
</UtopiaMap>
)
}
export default App

View File

@ -0,0 +1,9 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@ -0,0 +1,23 @@
export const places = [{
"id": 51,
"name": "Stadtgemüse",
"text": "Stadtgemüse Fulda ist eine Gemüsegärtnerei in Maberzell, die es sich zur Aufgabe gemacht hat, die Stadt und seine Bewohner:innen mit regionalem, frischem und natürlich angebautem Gemüse mittels Gemüsekisten zu versorgen. Es gibt also jede Woche, von Frühjahr bis Herbst, angepasst an die Saison eine Kiste mit schmackhaftem und frischem Gemüse für euch, welche ihr direkt vor Ort abholen könnt. \r\n\r\nhttps://stadtgemuese-fulda.de",
"position": { "type": "Point", "coordinates": [9.632435, 50.560342] },
},
{
"id": 166,
"name": "Weidendom",
"text": "free camping",
"position": { "type": "Point", "coordinates": [9.438793, 50.560112] },
}];
export const events = [
{
"id": 423,
"name": "Hackathon",
"text": "still in progress",
"position": { "type": "Point", "coordinates": [10.5, 51.62] },
"start": "2022-03-25T12:00:00",
"end": "2022-05-12T12:00:00",
}
]

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

130
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "utopia-ui",
"version": "3.0.19",
"version": "3.0.34",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "utopia-ui",
"version": "3.0.19",
"version": "3.0.34",
"license": "GPL-3.0-only",
"dependencies": {
"@heroicons/react": "^2.0.17",
@ -27,7 +27,8 @@
"react-toastify": "^9.1.3",
"remark-breaks": "^4.0.0",
"tributejs": "^5.1.3",
"tw-elements": "^1.0.0"
"tw-elements": "^1.0.0",
"utopia-ui": "^3.0.35"
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.1",
@ -45,9 +46,12 @@
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-json": "^3.1.0",
"eslint-plugin-no-catch-all": "^1.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.31.8",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-security": "^3.0.1",
"eslint-plugin-yml": "^1.14.0",
"postcss": "^8.4.21",
"prettier": "^3.3.3",
@ -1351,9 +1355,9 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"dependencies": {
"path-key": "^3.1.0",
@ -2371,6 +2375,16 @@
"node": ">=10"
}
},
"node_modules/eslint-plugin-no-catch-all": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-no-catch-all/-/eslint-plugin-no-catch-all-1.1.0.tgz",
"integrity": "sha512-VkP62jLTmccPrFGN/W6V7a3SEwdtTZm+Su2k4T3uyJirtkm0OMMm97h7qd8pRFAHus/jQg9FpUpLRc7sAylBEQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"eslint": ">=2.0.0"
}
},
"node_modules/eslint-plugin-prettier": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz",
@ -2408,7 +2422,6 @@
"integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==",
"dev": true,
"license": "ISC",
"peer": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@ -2488,6 +2501,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/eslint-plugin-security": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-3.0.1.tgz",
"integrity": "sha512-XjVGBhtDZJfyuhIxnQ/WMm385RbX3DBu7H1J7HNNhmB2tnGxMeqVSnYv79oAj992ayvIBZghsymwkYFS6cGH4Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"safe-regex": "^2.1.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-yml": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-yml/-/eslint-plugin-yml-1.14.0.tgz",
@ -4759,9 +4788,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
@ -6015,6 +6044,23 @@
"react": ">=18"
}
},
"node_modules/react-photo-album": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/react-photo-album/-/react-photo-album-3.0.2.tgz",
"integrity": "sha512-w3+8i6aj9l1jRfcubgVbAlBGSdtiXcqWdcwZcH4/Bavc+v7X7h+S3TkQ723pvDABjhaaxS168g9ECEBP6xnKrQ==",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/react": ">=18",
"react": ">=18"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-router": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.16.0.tgz",
@ -6092,6 +6138,16 @@
"node": ">=8.10.0"
}
},
"node_modules/regexp-tree": {
"version": "0.1.27",
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
"integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
"dev": true,
"license": "MIT",
"bin": {
"regexp-tree": "bin/regexp-tree"
}
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz",
@ -6405,6 +6461,16 @@
"integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==",
"dev": true
},
"node_modules/safe-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
"integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
"dev": true,
"license": "MIT",
"dependencies": {
"regexp-tree": "~0.1.1"
}
},
"node_modules/safe-regex-test": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
@ -7304,6 +7370,38 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/utopia-ui": {
"version": "3.0.35",
"resolved": "https://registry.npmjs.org/utopia-ui/-/utopia-ui-3.0.35.tgz",
"integrity": "sha512-PtXvwskYuS4ro/gRWoNkKvn/lC0vW6m9ipwnXb7a4u/95wGWDXajhTh70MbAujTszxSH2iBRY6ZTsSFuREHCJQ==",
"dependencies": {
"@heroicons/react": "^2.0.17",
"@tanstack/react-query": "^5.17.8",
"@types/offscreencanvas": "^2019.7.1",
"axios": "^1.6.5",
"date-fns": "^3.3.1",
"leaflet": "^1.9.4",
"leaflet.locatecontrol": "^0.79.0",
"prop-types": "^15.8.1",
"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",
"yet-another-react-lightbox": "^3.21.7"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
},
"node_modules/vfile": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz",
@ -7483,6 +7581,18 @@
"node": ">= 14"
}
},
"node_modules/yet-another-react-lightbox": {
"version": "3.21.7",
"resolved": "https://registry.npmjs.org/yet-another-react-lightbox/-/yet-another-react-lightbox-3.21.7.tgz",
"integrity": "sha512-dcdokNuCIl92f0Vl+uzeKULnQhztIGpoZFUMvtVNUPmtwsQWpqWufeieDPeg9JtFyVCcbj4vYw3V00DS0QNoWA==",
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "utopia-ui",
"version": "3.0.19",
"version": "3.0.34",
"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/",
@ -33,9 +33,12 @@
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-json": "^3.1.0",
"eslint-plugin-no-catch-all": "^1.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.31.8",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-security": "^3.0.1",
"eslint-plugin-yml": "^1.14.0",
"postcss": "^8.4.21",
"prettier": "^3.3.3",
@ -76,6 +79,7 @@
"#components/*": "./src/Components/*",
"#utils/*": "./src/Utils/*",
"#src/*": "./src/*",
"#types/*": "./types/*",
"#root/*": "./*"
}
}

View File

@ -21,6 +21,9 @@ export default {
external: [
'react',
'react-dom',
'react-markdown',
'react/jsx-runtime',
'remark-breaks',
'leaflet',
'react-leaflet',
'react-toastify',
@ -34,5 +37,16 @@ export default {
'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',
],
}

View File

@ -1,9 +1,9 @@
import { AssetsApi } from '#src/types'
import { ContextWrapper } from './ContextWrapper'
import NavBar from './NavBar'
import { SetAppState } from './SetAppState'
import type { AssetsApi } from '#types/AssetsApi'
export function AppShell({
appName,
children,

View File

@ -28,6 +28,7 @@ export const ContextWrapper = ({ children }) => {
try {
// eslint-disable-next-line react-hooks/rules-of-hooks
location = useLocation()
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (e) {
location = null
}

View File

@ -1,14 +1,3 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-floating-promises */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
import QuestionMarkIcon from '@heroicons/react/24/outline/QuestionMarkCircleIcon'
import { useEffect, useRef, useState } from 'react'
import { Link, useLocation } from 'react-router-dom'
@ -16,7 +5,8 @@ import { toast } from 'react-toastify'
import { useAuth } from '#components/Auth'
import { useItems } from '#components/Map/hooks/useItems'
import { Item } from '#src/types'
import type { Item } from '#types/Item'
export default function NavBar({ appName, userType }: { appName: string; userType: string }) {
const { isAuthenticated, user, logout } = useAuth()
@ -30,19 +20,19 @@ export default function NavBar({ appName, userType }: { appName: string; userTyp
items.find((i) => i.user_created?.id === user.id && i.layer?.itemType.name === userType)
profile
? setUserProfile(profile)
: setUserProfile({ id: crypto.randomUUID(), name: user?.first_name, text: '' })
: setUserProfile({ id: crypto.randomUUID(), name: user?.first_name ?? '', text: '' })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, items])
useEffect(() => {}, [userProfile])
// useEffect(() => {}, [userProfile])
const nameRef = useRef<any>(null)
const nameRef = useRef<HTMLHeadingElement>(null)
const [nameWidth, setNameWidth] = useState<number>(0)
const location = useLocation()
const [showNav, setShowNav] = useState<boolean>(false)
useEffect(() => {
showNav && nameRef && setNameWidth(nameRef.current.scrollWidth)
showNav && nameRef.current && setNameWidth(nameRef.current.scrollWidth)
}, [nameRef, appName, showNav])
useEffect(() => {
@ -51,8 +41,8 @@ export default function NavBar({ appName, userType }: { appName: string; userTyp
embedded !== 'true' && setShowNav(true)
}, [location])
const onLogout = () => {
toast.promise(logout(), {
const onLogout = async () => {
await toast.promise(logout(), {
success: {
render() {
return 'Bye bye'
@ -62,7 +52,7 @@ export default function NavBar({ appName, userType }: { appName: string; userTyp
},
error: {
render({ data }) {
return `${data}`
return JSON.stringify(data)
},
},
pending: 'logging out ..',
@ -122,7 +112,7 @@ export default function NavBar({ appName, userType }: { appName: string; userTyp
to={`${userProfile.id && '/item/' + userProfile.id}`}
className='tw-flex tw-items-center'
>
{userProfile?.image && (
{userProfile.image && (
<div className='tw-avatar'>
<div className='tw-w-10 tw-rounded-full'>
<img src={'https://api.utopia-lab.org/assets/' + userProfile.image} />
@ -155,7 +145,7 @@ export default function NavBar({ appName, userType }: { appName: string; userTyp
<li>
<a
onClick={() => {
onLogout()
void onLogout()
}}
>
Logout

View File

@ -1,9 +1,9 @@
import { useEffect } from 'react'
import { AssetsApi } from '#src/types'
import { useSetAppState } from './hooks/useAppState'
import type { AssetsApi } from '#types/AssetsApi'
export const SetAppState = ({
assetsApi,
userType,

View File

@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { useCallback, useState, createContext, useContext } from 'react'
import { AssetsApi } from '#src/types'
import type { AssetsApi } from '#types/AssetsApi'
interface AppState {
assetsApi: AssetsApi

View File

@ -3,7 +3,7 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { useCallback, useState, createContext, useContext } from 'react'
import { AssetsApi } from '#src/types'
import type { AssetsApi } from '#types/AssetsApi'
type UseAssetManagerResult = ReturnType<typeof useAssetsManager>

View File

@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
@ -7,7 +5,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createContext, useState, useContext, useEffect } from 'react'
import { UserApi, UserItem } from '#src/types'
import type { UserApi } from '#types/UserApi'
import type { UserItem } from '#types/UserItem'
interface AuthProviderProps {
userApi: UserApi
@ -69,6 +68,7 @@ export const AuthProvider = ({ userApi, children }: AuthProviderProps) => {
setLoading(false)
return me
} else return undefined
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
setLoading(false)
return undefined
@ -79,7 +79,7 @@ export const AuthProvider = ({ userApi, children }: AuthProviderProps) => {
setLoading(true)
try {
const res = await userApi.login(credentials.email, credentials.password)
setToken(res.access_token)
setToken(res?.access_token)
return await loadUser()
} catch (error: any) {
setLoading(false)

View File

@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { useEffect } from 'react'
export function Modal({

View File

@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { useEffect, useState } from 'react'
import { useAuth } from '#components/Auth'
import { useItems } from '#components/Map/hooks/useItems'
import { Item } from '#src/types'
import { useQuestsOpen, useSetQuestOpen } from './hooks/useQuests'
import type { Item } from '#types/Item'
export function Quests() {
const questsOpen = useQuestsOpen()
const setQuestsOpen = useSetQuestOpen()

View File

@ -70,6 +70,7 @@ export const Autocomplete = ({
break
case 'Enter':
if (filteredSuggestions.length > 0) {
// eslint-disable-next-line security/detect-object-injection
onSelected(filteredSuggestions[heighlightedSuggestion])
setHeighlightedSuggestion(0)
}

View File

@ -15,6 +15,7 @@ type TextAreaProps = {
inputStyle?: string
defaultValue: string
placeholder?: string
required?: boolean
updateFormValue?: (value: string) => void
}
@ -30,6 +31,7 @@ export function TextAreaInput({
inputStyle,
defaultValue,
placeholder,
required = true,
updateFormValue,
}: TextAreaProps) {
const ref = useRef<HTMLTextAreaElement>(null)
@ -90,7 +92,7 @@ export function TextAreaInput({
</label>
) : null}
<textarea
required
required={required}
ref={ref}
value={inputValue}
name={dataField}

View File

@ -13,6 +13,8 @@ type InputTextProps = {
defaultValue?: string
placeholder?: string
autocomplete?: string
pattern?: string
required?: boolean
updateFormValue?: (value: string) => void
}
@ -26,6 +28,8 @@ export function TextInput({
defaultValue,
placeholder,
autocomplete,
pattern,
required = true,
updateFormValue,
}: InputTextProps) {
const [inputValue, setInputValue] = useState<string>(defaultValue || '')
@ -50,7 +54,8 @@ export function TextInput({
</label>
) : null}
<input
required
required={required}
pattern={pattern}
type={type || 'text'}
name={dataField}
value={inputValue}

View File

@ -1,7 +1,7 @@
import { node, string } from 'prop-types'
import { Children, cloneElement, isValidElement, useEffect } from 'react'
import { Item } from '#src/types'
import type { Item } from '#types/Item'
export const ItemForm = ({
children,

View File

@ -1,7 +1,7 @@
import { node, string } from 'prop-types'
import { Children, cloneElement, isValidElement } from 'react'
import { Item } from '#src/types'
import type { Item } from '#types/Item'
export const ItemView = ({ children, item }: { children?: React.ReactNode; item?: Item }) => {
return (

View File

@ -7,12 +7,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Popup } from 'leaflet'
import { Children, isValidElement, useEffect, useState } from 'react'
import { Marker, Tooltip, useMap, useMapEvents } from 'react-leaflet'
import { useLocation } from 'react-router-dom'
import { Item, LayerProps, Tag } from '#src/types'
import { encodeTag } from '#utils/FormatTags'
import { getValue } from '#utils/GetValue'
import { hashTagRegex } from '#utils/HashTagRegex'
@ -32,6 +30,11 @@ import { useAddTag, useAllTagsLoaded, useGetItemTags, useTags } from './hooks/us
import { ItemFormPopup } from './Subcomponents/ItemFormPopup'
import { ItemViewPopup } from './Subcomponents/ItemViewPopup'
import type { Item } from '#types/Item'
import type { LayerProps } from '#types/LayerProps'
import type { Tag } from '#types/Tag'
import type { Popup } from 'leaflet'
export const Layer = ({
data,
children,
@ -196,6 +199,7 @@ export const Layer = ({
} 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 &&
@ -261,20 +265,27 @@ export const Layer = ({
)
.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.tags) {
// eslint-disable-next-line security/detect-object-injection
item[itemTextField] = item[itemTextField] + '\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)} `)
}
// eslint-disable-next-line security/detect-object-injection
return item[itemTextField]
})
}
if (allTagsLoaded && allItemsLoaded) {
// eslint-disable-next-line security/detect-object-injection
item[itemTextField].match(hashTagRegex)?.map((tag) => {
if (
!tags.find(

View File

@ -1,10 +1,12 @@
import { useEffect } from 'react'
import { useAuth } from '#components/Auth'
import { ItemsApi, Permission } from '#src/types'
import { useSetPermissionData, useSetPermissionApi, useSetAdminRole } from './hooks/usePermissions'
import type { ItemsApi } from '#types/ItemsApi'
import type { Permission } from '#types/Permission'
export function Permissions({
data,
api,

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { useLayers } from '#components/Map/hooks/useLayers'
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'

View File

@ -22,7 +22,6 @@ import { useItems } from '#components/Map/hooks/useItems'
import { useLeafletRefs } from '#components/Map/hooks/useLeafletRefs'
import { useTags } from '#components/Map/hooks/useTags'
import useWindowDimensions from '#components/Map/hooks/useWindowDimension'
import { Item } from '#src/types'
import { decodeTag } from '#utils/FormatTags'
import { getValue } from '#utils/GetValue'
import MarkerIconFactory from '#utils/MarkerIconFactory'
@ -30,6 +29,8 @@ import MarkerIconFactory from '#utils/MarkerIconFactory'
import { LocateControl } from './LocateControl'
import { SidebarControl } from './SidebarControl'
import type { Item } from '#types/Item'
export const SearchControl = () => {
const windowDimensions = useWindowDimensions()
const [popupOpen, setPopupOpen] = useState(false)
@ -63,6 +64,7 @@ export const SearchControl = () => {
try {
const { data } = await axios.get(`https://photon.komoot.io/api/?q=${value}&limit=5`)
setGeoResults(data.features)
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
@ -331,6 +333,7 @@ export const SearchControl = () => {
function isGeoCoordinate(input) {
const geokoordinatenRegex =
// eslint-disable-next-line security/detect-unsafe-regex
/^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?),\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$/
return geokoordinatenRegex.test(input)
}

View File

@ -4,7 +4,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from 'react'
@ -17,10 +16,12 @@ import { TextInput } from '#components/Input/TextInput'
import { useResetFilterTags } from '#components/Map/hooks/useFilter'
import { useAddItem, useItems, useRemoveItem, useUpdateItem } from '#components/Map/hooks/useItems'
import { useAddTag, useTags } from '#components/Map/hooks/useTags'
import { Geometry, Item, ItemFormPopupProps } from '#src/types'
import { hashTagRegex } from '#utils/HashTagRegex'
import { randomColor } from '#utils/RandomColor'
import type { Item } from '#types/Item'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
export function ItemFormPopup(props: ItemFormPopupProps) {
const [spinner, setSpinner] = useState(false)
@ -50,7 +51,7 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
formItem[input.name] = input.value
}
})
formItem.position = new Geometry(props.position.lng, props.position.lat)
formItem.position = { type: 'Point', coordinates: [props.position.lng, props.position.lat] }
evt.preventDefault()
setSpinner(true)
@ -70,6 +71,7 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
try {
await props.layer.api?.updateItem!({ ...formItem, id: props.item.id })
success = true
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
toast.error(error.toString())
}
@ -101,6 +103,7 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
name: formItem.name ? formItem.name : user?.first_name,
}))
success = true
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
toast.error(error.toString())
}
@ -109,8 +112,8 @@ export function ItemFormPopup(props: ItemFormPopupProps) {
if (!props.layer.onlyOnePerOwner || !item) {
addItem({
...formItem,
name: formItem.name ? formItem.name : user?.first_name,
user_created: user,
name: (formItem.name ? formItem.name : user?.first_name) ?? '',
user_created: user ?? undefined,
type: props.layer.itemType,
id: uuid,
layer: props.layer,

View File

@ -15,9 +15,11 @@ 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 { Item, ItemsApi } from '#src/types'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
import type { ItemsApi } from '#types/ItemsApi'
export function HeaderView({
item,
api,
@ -103,6 +105,7 @@ export function HeaderView({
<div className={`${avatar ? 'tw-ml-2' : ''} tw-overflow-hidden`}>
<div
className={`${big ? 'xl:tw-text-3xl tw-text-2xl' : 'tw-text-xl'} tw-font-semibold tw-truncate`}
title={title}
>
{title}
</div>

View File

@ -3,9 +3,10 @@
import { Link } from 'react-router-dom'
import { useGetItemTags } from '#components/Map/hooks/useTags'
import { Item } from '#src/types'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
export const PopupButton = ({
url,
parameterField,

View File

@ -1,4 +1,4 @@
import { Item } from '#src/types'
import type { Item } from '#types/Item'
export const PopupCheckboxInput = ({
dataField,

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/prefer-optional-chain */
import { TextInput } from '#components/Input'
import { Item } from '#src/types'
import type { Item } from '#types/Item'
interface StartEndInputProps {
item?: Item

View File

@ -1,5 +1,6 @@
import { TextAreaInput } from '#components/Input'
import { Item } from '#src/types'
import type { Item } from '#types/Item'
export const PopupTextAreaInput = ({
dataField,

View File

@ -1,5 +1,6 @@
import { TextInput } from '#components/Input'
import { Item } from '#src/types'
import type { Item } from '#types/Item'
export const PopupTextInput = ({
dataField,

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/prefer-optional-chain */
import { Item } from '#src/types'
import type { Item } from '#types/Item'
export const StartEndView = ({ item }: { item?: Item }) => {
return (

View File

@ -1,7 +1,5 @@
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
/* eslint-disable @typescript-eslint/no-unsafe-return */
@ -13,12 +11,14 @@ import remarkBreaks from 'remark-breaks'
import { useAddFilterTag } from '#components/Map/hooks/useFilter'
import { useTags } from '#components/Map/hooks/useTags'
import { Item } from '#src/types'
import { decodeTag } from '#utils/FormatTags'
import { getValue } from '#utils/GetValue'
import { hashTagRegex } from '#utils/HashTagRegex'
import { fixUrls, mailRegex } from '#utils/ReplaceURLs'
import type { Item } from '#types/Item'
import type { Tag } from '#types/Tag'
export const TextView = ({
item,
truncate = false,
@ -76,47 +76,54 @@ export const TextView = ({
})
}
// eslint-disable-next-line react/prop-types
const CustomH1 = ({ children }) => <h1 className='tw-text-xl tw-font-bold'>{children}</h1>
// eslint-disable-next-line react/prop-types
const CustomH2 = ({ children }) => <h2 className='tw-text-lg tw-font-bold'>{children}</h2>
// eslint-disable-next-line react/prop-types
const CustomH3 = ({ children }) => <h3 className='tw-text-base tw-font-bold'>{children}</h3>
// eslint-disable-next-line react/prop-types
const CustomH4 = ({ children }) => <h4 className='tw-text-base tw-font-bold'>{children}</h4>
// eslint-disable-next-line react/prop-types
const CustomH5 = ({ children }) => <h5 className='tw-text-sm tw-font-bold'>{children}</h5>
// eslint-disable-next-line react/prop-types
const CustomH6 = ({ children }) => <h6 className='tw-text-sm tw-font-bold'>{children}</h6>
// eslint-disable-next-line react/prop-types
const CustomParagraph = ({ children }) => <p className='!tw-my-2'>{children}</p>
// eslint-disable-next-line react/prop-types
const CustomUnorderdList = ({ children }) => (
<ul className='tw-list-disc tw-list-inside'>{children}</ul>
)
// eslint-disable-next-line react/prop-types
const CustomOrderdList = ({ children }) => (
<ol className='tw-list-decimal tw-list-inside'>{children}</ol>
)
// eslint-disable-next-line react/prop-types
const CustomHorizontalRow = ({ children }) => <hr className='tw-border-current'>{children}</hr>
// eslint-disable-next-line react/prop-types
const CustomImage = ({ alt, src, title }) => (
<img className='tw-max-w-full tw-rounded tw-shadow' src={src} alt={alt} title={title} />
)
// eslint-disable-next-line react/prop-types
const CustomExternalLink = ({ href, children }) => (
<a className='tw-font-bold tw-underline' href={href} target='_blank' rel='noreferrer'>
{' '}
{children}
</a>
)
/* eslint-disable react/prop-types */
const CustomHashTagLink = ({ children, tag, item }) => {
const CustomHashTagLink = ({
children,
tag,
item,
}: {
children: string
tag: Tag
item?: Item
}) => {
return (
<a
style={{ color: tag ? tag.color : '#faa', fontWeight: 'bold', cursor: 'pointer' }}
key={tag ? tag.name + item.id : item.id}
key={tag ? tag.name + item?.id : item?.id}
onClick={(e) => {
e.stopPropagation()
addFilterTag(tag)
@ -126,7 +133,6 @@ export const TextView = ({
</a>
)
}
/* eslint-enable react/prop-types */
// eslint-disable-next-line react/display-name
const MemoizedVideoEmbed = memo(({ url }: { url: string }) => (
@ -144,32 +150,34 @@ export const TextView = ({
remarkPlugins={[remarkBreaks]}
components={{
p: CustomParagraph,
a: ({ href, children }) => {
// eslint-disable-next-line react/prop-types
a: ({ href, children }: { href: string; children: string }) => {
const isYouTubeVideo = href?.startsWith('https://www.youtube.com/watch?v=')
// eslint-disable-next-line react/prop-types
const isRumbleVideo = href?.startsWith('https://rumble.com/embed/')
if (isYouTubeVideo) {
// eslint-disable-next-line react/prop-types
const videoId = href?.split('v=')[1].split('&')[0]
const youtubeEmbedUrl = `https://www.youtube-nocookie.com/embed/${videoId}`
return <MemoizedVideoEmbed url={youtubeEmbedUrl}></MemoizedVideoEmbed>
}
if (isRumbleVideo) {
return <MemoizedVideoEmbed url={href!}></MemoizedVideoEmbed>
return <MemoizedVideoEmbed url={href}></MemoizedVideoEmbed>
}
// eslint-disable-next-line react/prop-types
if (href?.startsWith('#')) {
console.log(href.slice(1).toLowerCase())
console.log(tags)
const tag = tags.find(
(t) => t.name.toLowerCase() === decodeURI(href).slice(1).toLowerCase(),
)
return (
<CustomHashTagLink tag={tag} item={item}>
{children}
</CustomHashTagLink>
)
if (tag)
return (
<CustomHashTagLink tag={tag} item={item}>
{children}
</CustomHashTagLink>
)
else return children
} else {
return <CustomExternalLink href={href}>{children}</CustomExternalLink>
}

View File

@ -15,12 +15,14 @@ import { toast } from 'react-toastify'
import { useRemoveItem, useUpdateItem } from '#components/Map/hooks/useItems'
import { useSetSelectPosition } from '#components/Map/hooks/useSelectPosition'
import { Item, ItemFormPopupProps } from '#src/types'
import { timeAgo } from '#utils/TimeAgo'
import { HeaderView } from './ItemPopupComponents/HeaderView'
import { TextView } from './ItemPopupComponents/TextView'
import type { Item } from '#types/Item'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
export interface ItemViewPopupProps {
item: Item
children?: React.ReactNode
@ -63,6 +65,7 @@ export const ItemViewPopup = forwardRef((props: ItemViewPopupProps, ref: any) =>
props.item.layer?.onlyOnePerOwner &&
(await props.item.layer.api?.updateItem!({ id: props.item.id, position: null }))
success = true
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
toast.error(error.toString())
}

View File

@ -1,11 +1,12 @@
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { ItemsApi, Tag } from '#src/types'
import { useAddFilterTag, useFilterTags, useResetFilterTags } from './hooks/useFilter'
import { useSetTagData, useSetTagApi, useTags } from './hooks/useTags'
import type { ItemsApi } from '#types/ItemsApi'
import type { Tag } from '#types/Tag'
export function Tags({ data, api }: { data?: Tag[]; api?: ItemsApi<Tag> }) {
const setTagData = useSetTagData()
const setTagApi = useSetTagApi()

View File

@ -1,7 +1,9 @@
import { ContextWrapper } from '#components/AppShell/ContextWrapper'
import { UtopiaMapProps } from '#src/types'
import { UtopiaMapInner } from './UtopiaMapInner'
import type { UtopiaMapProps } from '#types/UtopiaMapProps'
// eslint-disable-next-line import/no-unassigned-import
import 'react-toastify/dist/ReactToastify.css'

View File

@ -23,8 +23,6 @@ import MarkerClusterGroup from 'react-leaflet-cluster'
import { Outlet } from 'react-router-dom'
import { toast } from 'react-toastify'
import { ItemFormPopupProps, UtopiaMapProps } from '#src/types'
// eslint-disable-next-line import/no-unassigned-import
import './UtopiaMap.css'
@ -46,6 +44,8 @@ import { TagsControl } from './Subcomponents/Controls/TagsControl'
import { TextView } from './Subcomponents/ItemPopupComponents/TextView'
import { SelectPosition } from './Subcomponents/SelectPosition'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
import type { UtopiaMapProps } from '#types/UtopiaMapProps'
import type { Feature, Geometry as GeoJSONGeometry } from 'geojson'
const mapDivRef = createRef()

View File

@ -7,11 +7,12 @@
import { useCallback, useReducer, createContext, useContext, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { LayerProps, Tag } from '#src/types'
import { useLayers } from './useLayers'
import useWindowDimensions from './useWindowDimension'
import type { LayerProps } from '#types/LayerProps'
import type { Tag } from '#types/Tag'
type ActionType =
| { type: 'ADD_TAG'; tag: Tag }
| { type: 'REMOVE_TAG'; name: string }

View File

@ -3,17 +3,16 @@
/* 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-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* 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'
import { Item, LayerProps } from '#src/types'
import { useAddLayer } from './useLayers'
import type { Item } from '#types/Item'
import type { LayerProps } from '#types/LayerProps'
type ActionType =
| { type: 'ADD'; item: Item }
| { type: 'UPDATE'; item: Item }
@ -82,13 +81,11 @@ function useItemsManager(initialItems: Item[]): {
},
},
})
if (result) {
result.map((item) => {
dispatch({ type: 'ADD', item: { ...item, layer } })
return null
})
setallItemsLoaded(true)
}
result.map((item) => {
dispatch({ type: 'ADD', item: { ...item, layer } })
return null
})
setallItemsLoaded(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

View File

@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { useCallback, useReducer, createContext, useContext } from 'react'
import { LayerProps } from '#src/types'
import type { LayerProps } from '#types/LayerProps'
interface ActionType {
type: 'ADD LAYER'

View File

@ -1,10 +1,10 @@
/* eslint-disable react/prop-types */
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-empty-function */
import { Marker, Popup } from 'leaflet'
import { useCallback, useReducer, createContext, useContext } from 'react'
import { Item } from '#src/types'
import type { Item } from '#types/Item'
import type { Marker, Popup } from 'leaflet'
interface LeafletRef {
item: Item

View File

@ -1,6 +1,5 @@
/* eslint-disable react/prop-types */
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
/* eslint-disable @typescript-eslint/no-unsafe-call */
@ -10,7 +9,12 @@
import { useCallback, useReducer, createContext, useContext, useState } from 'react'
import { useAuth } from '#components/Auth/useAuth'
import { Item, ItemsApi, LayerProps, Permission, PermissionAction } from '#src/types'
import type { Item } from '#types/Item'
import type { ItemsApi } from '#types/ItemsApi'
import type { LayerProps } from '#types/LayerProps'
import type { Permission } from '#types/Permission'
import type { PermissionAction } from '#types/PermissionAction'
type ActionType = { type: 'ADD'; permission: Permission } | { type: 'REMOVE'; id: string }
@ -57,12 +61,10 @@ function usePermissionsManager(initialPermissions: Permission[]): {
const setPermissionApi = useCallback(async (api: ItemsApi<Permission>) => {
const result = await api.getItems()
if (result) {
result.map((permission) => {
dispatch({ type: 'ADD', permission })
return null
})
}
result.map((permission) => {
dispatch({ type: 'ADD', permission })
return null
})
}, [])
const setPermissionData = useCallback((data: Permission[]) => {

View File

@ -8,15 +8,18 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { LatLng } from 'leaflet'
import { createContext, useContext, useEffect, useState } from 'react'
import { toast } from 'react-toastify'
import { Geometry, Item, LayerProps, ItemFormPopupProps } from '#src/types'
import { useUpdateItem } from './useItems'
import { useHasUserPermission } from './usePermissions'
import type { Item } from '#types/Item'
import type { ItemFormPopupProps } from '#types/ItemFormPopupProps'
import type { LayerProps } from '#types/LayerProps'
import type { Point } from 'geojson'
import type { LatLng } from 'leaflet'
interface PolygonClickedProps {
position: LatLng
setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps | null>>
@ -67,7 +70,11 @@ function useSelectPositionManager(): {
}
if ('text' in selectPosition) {
const position =
mapClicked?.position.lng && new Geometry(mapClicked.position.lng, mapClicked.position.lat)
mapClicked?.position.lng &&
({
type: 'Point',
coordinates: [mapClicked.position.lng, mapClicked.position.lat],
} as Point)
position && itemUpdatePosition({ ...selectPosition, position })
setSelectPosition(null)
}
@ -89,6 +96,7 @@ function useSelectPositionManager(): {
position: null,
})
success = true
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
toast.error(error.toString())
}
@ -113,6 +121,7 @@ function useSelectPositionManager(): {
position: updatedItem.position,
})
success = true
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
toast.error(error.toString())
}
@ -134,6 +143,7 @@ function useSelectPositionManager(): {
try {
await markerClicked.layer?.api?.updateItem!(updatedItem)
success = true
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
toast.error(error.toString())
}

View File

@ -1,6 +1,5 @@
/* eslint-disable react/prop-types */
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-floating-promises */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
@ -11,10 +10,13 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { useCallback, useReducer, createContext, useContext, useState } from 'react'
import { Item, ItemsApi, Tag } from '#src/types'
import { getValue } from '#utils/GetValue'
import { hashTagRegex } from '#utils/HashTagRegex'
import type { Item } from '#types/Item'
import type { ItemsApi } from '#types/ItemsApi'
import type { Tag } from '#types/Tag'
type ActionType = { type: 'ADD'; tag: Tag } | { type: 'REMOVE'; id: string }
type UseTagManagerResult = ReturnType<typeof useTagsManager>

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
@ -15,7 +14,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 { Item, Tag } from '#src/types'
import { getValue } from '#utils/GetValue'
import { linkItem, onUpdateItem, unlinkItem } from './itemFunctions'
@ -25,6 +23,9 @@ import { OnepagerForm } from './Templates/OnepagerForm'
import { SimpleForm } from './Templates/SimpleForm'
import { TabsForm } from './Templates/TabsForm'
import type { Item } from '#types/Item'
import type { Tag } from '#types/Tag'
export function ProfileForm() {
const [state, setState] = useState({
color: '',
@ -78,7 +79,7 @@ export function ProfileForm() {
const layer = layers.find((l) => l.itemType.name === appState.userType)
setItem({
id: crypto.randomUUID(),
name: user ? user.first_name : '',
name: user?.first_name ?? '',
text: '',
layer,
new: true,
@ -148,58 +149,64 @@ export function ProfileForm() {
backdrop
className='tw-mx-4 tw-mt-4 tw-mb-4 tw-overflow-x-hidden tw-w-[calc(100%-32px)] md:tw-w-[calc(50%-32px)] tw-max-w-3xl !tw-left-auto tw-top-0 tw-bottom-0'
>
<div className='tw-flex tw-flex-col tw-h-full'>
<FormHeader item={item} state={state} setState={setState} />
<form
className='tw-h-full'
onSubmit={(e) => {
e.preventDefault()
void onUpdateItem(
state,
item,
tags,
addTag,
setLoading,
navigate,
updateItem,
addItem,
user,
urlParams,
)
}}
>
<div className='tw-flex tw-flex-col tw-h-full'>
<FormHeader item={item} state={state} setState={setState} />
{template === 'onepager' && (
<OnepagerForm item={item} state={state} setState={setState}></OnepagerForm>
)}
{template === 'onepager' && (
<OnepagerForm item={item} state={state} setState={setState}></OnepagerForm>
)}
{template === 'simple' && <SimpleForm state={state} setState={setState}></SimpleForm>}
{template === 'simple' && <SimpleForm state={state} setState={setState}></SimpleForm>}
{template === 'flex' && (
<FlexForm item={item} state={state} setState={setState}></FlexForm>
)}
{template === 'flex' && (
<FlexForm item={item} state={state} setState={setState}></FlexForm>
)}
{template === 'tabs' && (
<TabsForm
loading={loading}
item={item}
state={state}
setState={setState}
updatePermission={updatePermission}
linkItem={(id) => linkItem(id, item, updateItem)}
unlinkItem={(id) => unlinkItem(id, item, updateItem)}
setUrlParams={setUrlParams}
></TabsForm>
)}
{template === 'tabs' && (
<TabsForm
loading={loading}
item={item}
state={state}
setState={setState}
updatePermission={updatePermission}
linkItem={(id) => linkItem(id, item, updateItem)}
unlinkItem={(id) => unlinkItem(id, item, updateItem)}
setUrlParams={setUrlParams}
></TabsForm>
)}
<div className='tw-mt-4'>
<button
className={loading ? ' tw-loading tw-btn tw-float-right' : 'tw-btn tw-float-right'}
onClick={() =>
onUpdateItem(
state,
item,
tags,
addTag,
setLoading,
navigate,
updateItem,
addItem,
user,
urlParams,
)
}
style={{
backgroundColor: `${item.layer?.itemColorField && getValue(item, item.layer?.itemColorField) ? getValue(item, item.layer?.itemColorField) : getItemTags(item) && getItemTags(item)[0] && getItemTags(item)[0].color ? getItemTags(item)[0].color : item?.layer?.markerDefaultColor}`,
color: '#fff',
}}
>
Update
</button>
<div className='tw-mt-4'>
<button
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}`,
color: '#fff',
}}
>
Update
</button>
</div>
</div>
</div>
</form>
</MapOverlayPage>
</>
)

View File

@ -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 { Item, ItemsApi, Tag } from '#src/types'
import { getValue } from '#utils/GetValue'
import { handleDelete, linkItem, unlinkItem } from './itemFunctions'
@ -30,6 +29,10 @@ import { OnepagerView } from './Templates/OnepagerView'
import { SimpleView } from './Templates/SimpleView'
import { TabsView } from './Templates/TabsView'
import type { Item } from '#types/Item'
import type { ItemsApi } from '#types/ItemsApi'
import type { Tag } from '#types/Tag'
export function ProfileView({ attestationApi }: { attestationApi?: ItemsApi<any> }) {
const [item, setItem] = useState<Item>()
const [updatePermission, setUpdatePermission] = useState<boolean>(false)
@ -64,7 +67,9 @@ export function ProfileView({ attestationApi }: { attestationApi?: ItemsApi<any>
console.log(value)
setAttestations(value)
return null
})
// eslint-disable-next-line promise/prefer-await-to-callbacks
.catch((error) => {
// eslint-disable-next-line no-console
console.error('Error fetching items:', error)

View File

@ -10,9 +10,10 @@ 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 { Item } from '#src/types'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
export function ActionButton({
item,
triggerAddButton,

View File

@ -1,8 +1,6 @@
/* eslint-disable react/prop-types */
/* eslint-disable @typescript-eslint/no-floating-promises */
/* 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/prefer-optional-chain */
import { useState, useCallback, useRef } from 'react'
import { ReactCrop, centerCrop, makeAspectCrop } from 'react-image-crop'

View File

@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { TextInput } from '#components/Input'
import { FormState } from '#src/types'
import type { FormState } from '#types/FormState'
export const ContactInfoForm = ({
state,
@ -22,6 +23,7 @@ export const ContactInfoForm = ({
<TextInput
placeholder='Email'
type='email'
required={false}
defaultValue={state.contact}
updateFormValue={(v) =>
setState((prevState) => ({
@ -41,6 +43,9 @@ export const ContactInfoForm = ({
</label>
<TextInput
placeholder='Telefonnummer'
type='tel'
required={false}
pattern='^\+?[0-9\s\-]{7,15}$'
defaultValue={state.telephone}
updateFormValue={(v) =>
setState((prevState) => ({

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/restrict-plus-operands */
import { useEffect, useState } from 'react'
@ -8,7 +7,8 @@ import { Link } from 'react-router-dom'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import { useItems } from '#components/Map/hooks/useItems'
import { Item } from '#src/types'
import type { Item } from '#types/Item'
export const ContactInfoView = ({ item, heading }: { item: Item; heading: string }) => {
const appState = useAppState()

View File

@ -44,6 +44,7 @@ export const FormHeader = ({ item, state, setState }) => {
/>
<TextInput
placeholder='Subtitle'
required={false}
defaultValue={item?.subname ? item.subname : ''}
updateFormValue={(v) =>
setState((prevState) => ({

View File

@ -1,7 +1,7 @@
import { Item } from '#src/types'
import SocialShareBar from './SocialShareBar'
import type { Item } from '#types/Item'
export const GroupSubHeaderView = ({
item,
shareBaseUrl,

View File

@ -4,7 +4,9 @@
import { useEffect } from 'react'
import ComboBoxInput from '#components/Input/ComboBoxInput'
import { Item, FormState } from '#src/types'
import type { FormState } from '#types/FormState'
import type { Item } from '#types/Item'
interface groupType {
groupTypes_id: {
@ -30,13 +32,16 @@ export const GroupSubheaderForm = ({
useEffect(() => {
if (groupTypes && groupStates) {
const groupType = groupTypes.find((gt) => gt.groupTypes_id.name === state.group_type)
// eslint-disable-next-line no-console
console.log(state.group_type)
const customImage = !groupTypes.some(
(gt) => gt.groupTypes_id.image === state.image || !state.image,
)
setState((prevState) => ({
...prevState,
color: groupType?.groupTypes_id.color || groupTypes[0].groupTypes_id.color,
marker_icon: groupType?.groupTypes_id.markerIcon || groupTypes[0].groupTypes_id.markerIcon,
image: groupType?.groupTypes_id.image || groupTypes[0].groupTypes_id.image,
image: customImage
? state.image
: groupType?.groupTypes_id.image || groupTypes[0].groupTypes_id.image,
status: state.status || groupStates[0],
group_type: state.group_type || groupTypes[0].groupTypes_id.name,
}))

View File

@ -8,9 +8,10 @@
import { useEffect } from 'react'
import { useAppState } from '#components/AppShell/hooks/useAppState'
import { Item } from '#src/types'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
export function LinkedItemsHeaderView({
item,
unlinkCallback,

View File

@ -0,0 +1,34 @@
import { useState } from 'react'
export const MarkdownHint = () => {
const [expended, setExpended] = useState<boolean>(false)
return (
<div
onClick={() => setExpended(true)}
title='Markdown is supported'
className='flex tw-flex-row tw-text-gray-400 tw-cursor-pointer tw-items-center'
>
<svg
aria-hidden='true'
height='16'
viewBox='0 0 16 16'
version='1.1'
width='16'
data-view-component='true'
className='octicon octicon-markdown'
fill='rgb(156 163 175 / var(--tw-text-opacity))'
>
<path d='M14.85 3c.63 0 1.15.52 1.14 1.15v7.7c0 .63-.51 1.15-1.15 1.15H1.15C.52 13 0 12.48 0 11.84V4.15C0 3.52.52 3 1.15 3ZM9 11V5H7L5.5 7 4 5H2v6h2V8l1.5 1.92L7 8v3Zm2.99.5L14.5 8H13V5h-2v3H9.5Z'></path>
</svg>
{expended && (
<a
href='https://www.markdownguide.org/cheat-sheet/#basic-syntax'
target='_blank'
rel='noreferrer'
>
<span className='Button-label tw-ml-1'>Markdown is support</span>{' '}
</a>
)}
</div>
)
}

View File

@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { useHasUserPermission } from '#components/Map/hooks/usePermissions'
import { LayerProps } from '#src/types'
import type { LayerProps } from '#types/LayerProps'
export function PlusButton({
layer,

View File

@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { PopupStartEndInput } from '#components/Map'
import { Item } from '#src/types'
import type { Item } from '#types/Item'
export const ProfileStartEndForm = ({
item,

View File

@ -1,5 +1,6 @@
import { StartEndView } from '#components/Map'
import { Item } from '#src/types'
import type { Item } from '#types/Item'
export const ProfileStartEndView = ({ item }: { item: Item }) => {
return (

View File

@ -5,9 +5,12 @@
import { useEffect, useState } from 'react'
import { TextAreaInput } from '#components/Input'
import { FormState } from '#src/types'
import { getValue } from '#utils/GetValue'
import { MarkdownHint } from './MarkdownHint'
import type { FormState } from '#types/FormState'
export const ProfileTextForm = ({
state,
setState,
@ -15,6 +18,7 @@ export const ProfileTextForm = ({
heading,
size,
hideInputLabel,
required,
}: {
state: FormState
setState: React.Dispatch<React.SetStateAction<any>>
@ -22,6 +26,7 @@ export const ProfileTextForm = ({
heading: string
size: string
hideInputLabel: boolean
required?: boolean
}) => {
const [field, setField] = useState<string>(dataField || 'text')
@ -33,12 +38,15 @@ export const ProfileTextForm = ({
return (
<div className='tw-h-full tw-flex tw-flex-col tw-mt-4'>
<label
htmlFor='nextAppointment'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
>
{heading || 'Text'}:
</label>
<div className='tw-flex tw-justify-between tw-items-center'>
<label
htmlFor='nextAppointment'
className='tw-block tw-text-sm tw-font-medium tw-text-gray-500 tw-mb-1'
>
{heading || 'Text'}:
</label>
<MarkdownHint />
</div>
<TextAreaInput
placeholder={'...'}
defaultValue={getValue(state, field)}
@ -51,6 +59,7 @@ export const ProfileTextForm = ({
labelStyle={hideInputLabel ? 'tw-hidden' : ''}
containerStyle={size === 'full' ? 'tw-grow tw-h-full' : ''}
inputStyle={size === 'full' ? 'tw-h-full' : 'tw-h-24'}
required={required}
/>
</div>
)

View File

@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { TextView } from '#components/Map'
import { Item } from '#src/types'
import { getValue } from '#utils/GetValue'
import type { Item } from '#types/Item'
export const ProfileTextView = ({
item,
dataField,

View File

@ -16,7 +16,9 @@ const SocialShareBar = ({
.writeText(url)
.then(() => {
toast.success('link copied to clipboard')
return null
})
// eslint-disable-next-line promise/prefer-await-to-callbacks
.catch((error: never) => {
toast.error('Fehler beim Kopieren des Links: ', error)
})

View File

@ -84,6 +84,7 @@ const SocialShareButton = ({
url: string
title: string
}) => {
// eslint-disable-next-line security/detect-object-injection
const config = platformConfigs[platform]
if (!config) {

View File

@ -8,10 +8,11 @@ import { useEffect, useState } from 'react'
import { Autocomplete } from '#components/Input/Autocomplete'
import { useTags } from '#components/Map/hooks/useTags'
import { Tag } from '#src/types'
import { decodeTag, encodeTag } from '#utils/FormatTags'
import { randomColor } from '#utils/RandomColor'
import type { Tag } from '#types/Tag'
// eslint-disable-next-line react/prop-types
export const TagsWidget = ({ placeholder, containerStyle, defaultTags, onUpdate }) => {
const [input, setInput] = useState('')

View File

@ -7,7 +7,9 @@ import { ContactInfoForm } from '#components/Profile/Subcomponents/ContactInfoFo
import { GroupSubheaderForm } from '#components/Profile/Subcomponents/GroupSubheaderForm'
import { ProfileStartEndForm } from '#components/Profile/Subcomponents/ProfileStartEndForm'
import { ProfileTextForm } from '#components/Profile/Subcomponents/ProfileTextForm'
import { Item, FormState } from '#src/types'
import type { FormState } from '#types/FormState'
import type { Item } from '#types/Item'
const componentMap = {
groupSubheaders: GroupSubheaderForm,

View File

@ -6,7 +6,8 @@ import { ContactInfoView } from '#components/Profile/Subcomponents/ContactInfoVi
import { GroupSubHeaderView } from '#components/Profile/Subcomponents/GroupSubHeaderView'
import { ProfileStartEndView } from '#components/Profile/Subcomponents/ProfileStartEndView'
import { ProfileTextView } from '#components/Profile/Subcomponents/ProfileTextView'
import { Item } from '#src/types'
import type { Item } from '#types/Item'
const componentMap = {
groupSubheaders: GroupSubHeaderView,

View File

@ -3,7 +3,9 @@
import { TextAreaInput } from '#components/Input'
import { ContactInfoForm } from '#components/Profile/Subcomponents/ContactInfoForm'
import { GroupSubheaderForm } from '#components/Profile/Subcomponents/GroupSubheaderForm'
import { FormState, Item } from '#src/types'
import type { FormState } from '#types/FormState'
import type { Item } from '#types/Item'
export const OnepagerForm = ({
item,

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