mirror of
https://github.com/utopia-os/utopia-ui.git
synced 2025-12-13 07:46:10 +00:00
Merge branch 'main' into gallery-component
This commit is contained in:
commit
acc502edde
@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
examples/
|
||||
@ -37,7 +37,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
react: {
|
||||
version: 'detect',
|
||||
version: '18.2.0',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
|
||||
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
|
||||
|
||||
@ -1,14 +1,70 @@
|
||||
# Contribution Guide
|
||||
|
||||
## Open ToDos
|
||||
## Setup Dev Environment
|
||||
|
||||
[Kanban Board](https://github.com/orgs/utopia-os/projects/2/)
|
||||
**Utopia UI** is just a React component library. To run it locally while making changes to it, you need to link it to a React app that uses its components. For this purpose, it is recommended to clone the `utopia-map` repository and link `utopia-ui` inside of it. This guide explains how to set everything up.
|
||||
|
||||
## Code
|
||||
### Setup `utopia-ui`
|
||||
|
||||
* use named exports
|
||||
1. **Clone the `utopia-ui` repository:**
|
||||
```bash
|
||||
git clone https://github.com/utopia-os/utopia-ui.git
|
||||
cd utopia-ui
|
||||
```
|
||||
|
||||
## Layout
|
||||
2. **Install dependencies:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Build `utopia-ui`:**
|
||||
Run the build script to prepare `utopia-ui` for use:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
4. **Link `utopia-ui` globally:**
|
||||
This makes the local version of `utopia-ui` available to be linked into other projects:
|
||||
```bash
|
||||
npm link
|
||||
```
|
||||
|
||||
|
||||
### 2. Setup `utopia-map` and Link `utopia-ui`
|
||||
|
||||
1. **Clone the `utopia-map` repository:**
|
||||
```bash
|
||||
git clone https://github.com/utopia-os/utopia-map.git
|
||||
cd utopia-map
|
||||
```
|
||||
|
||||
2. **Install dependencies:**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Link `utopia-ui` into `utopia-map`:**
|
||||
Use `npm link` to connect your local `utopia-ui` instance to `utopia-map`:
|
||||
```bash
|
||||
npm link utopia-ui
|
||||
```
|
||||
|
||||
4. **Start the development environment:**
|
||||
Run the local development environment for `utopia-map` to test changes in `utopia-ui`:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. Developing and Testing Changes
|
||||
|
||||
While working on `utopia-ui`, any changes you make need to be reflected in `utopia-map`. To ensure this `utopia-ui` has a watcher script, run it to
|
||||
automatically rebuild when files change:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
|
||||
## Layout System
|
||||
|
||||
* use [heroicons](https://heroicons.com/) or alternatively [React Icons](https://react-icons.github.io/react-icons/)
|
||||
* use [Daisy UI](https://daisyui.com/) with [tailwindcss](https://tailwindcss.com/)
|
||||
|
||||
3
Components.svg
Normal file
3
Components.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 15 KiB |
98
README.md
98
README.md
@ -26,103 +26,13 @@ It is the base of [Utopia Map](https://github.com/utopia-os/utopia-map) and [Uto
|
||||
|
||||
## 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.
|
||||
1. If you want to use **Utopia UI** in your project, check out [`/exampes`](/examples) to see how to use its components.
|
||||
|
||||
For this tutorial we use Vite to create an empty React app called "utopia-static-map"
|
||||
2. If you like to contribute to our library, see the [Contribution Guide](/CONTRIBUTING.md) to see how to setup a development environment on your local machine.
|
||||
|
||||
```shell
|
||||
npm create vite@latest utopia-static-map -- --template react
|
||||
```
|
||||
## Components
|
||||
|
||||
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 content in two 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>
|
||||
|
||||
```
|
||||

|
||||
|
||||
## 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
24
examples/1-basic-map/.gitignore
vendored
Normal 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?
|
||||
1
examples/1-basic-map/.nvmrc
Normal file
1
examples/1-basic-map/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
v18.19.1
|
||||
42
examples/1-basic-map/README.md
Normal file
42
examples/1-basic-map/README.md
Normal 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 "1-static-map"
|
||||
|
||||
```shell
|
||||
npm create vite@latest 1-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 1-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 [Example 2](../2-static-layers/) we gonna add some static data to our map
|
||||
25
examples/1-basic-map/eslint.config.js
Normal file
25
examples/1-basic-map/eslint.config.js
Normal 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 }],
|
||||
},
|
||||
},
|
||||
)
|
||||
13
examples/1-basic-map/index.html
Normal file
13
examples/1-basic-map/index.html
Normal 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
5350
examples/1-basic-map/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
examples/1-basic-map/package.json
Normal file
30
examples/1-basic-map/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
examples/1-basic-map/public/vite.svg
Normal file
1
examples/1-basic-map/public/vite.svg
Normal 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 |
10
examples/1-basic-map/src/App.tsx
Normal file
10
examples/1-basic-map/src/App.tsx
Normal 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
|
||||
9
examples/1-basic-map/src/main.tsx
Normal file
9
examples/1-basic-map/src/main.tsx
Normal 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>,
|
||||
)
|
||||
1
examples/1-basic-map/src/vite-env.d.ts
vendored
Normal file
1
examples/1-basic-map/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
26
examples/1-basic-map/tsconfig.app.json
Normal file
26
examples/1-basic-map/tsconfig.app.json
Normal 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"]
|
||||
}
|
||||
7
examples/1-basic-map/tsconfig.json
Normal file
7
examples/1-basic-map/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
examples/1-basic-map/tsconfig.node.json
Normal file
24
examples/1-basic-map/tsconfig.node.json
Normal 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"]
|
||||
}
|
||||
7
examples/1-basic-map/vite.config.ts
Normal file
7
examples/1-basic-map/vite.config.ts
Normal 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
24
examples/2-static-layers/.gitignore
vendored
Normal 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?
|
||||
1
examples/2-static-layers/.nvmrc
Normal file
1
examples/2-static-layers/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
v18.19.1
|
||||
62
examples/2-static-layers/README.md
Normal file
62
examples/2-static-layers/README.md
Normal 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
|
||||
```
|
||||
25
examples/2-static-layers/eslint.config.js
Normal file
25
examples/2-static-layers/eslint.config.js
Normal 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 }],
|
||||
},
|
||||
},
|
||||
)
|
||||
13
examples/2-static-layers/index.html
Normal file
13
examples/2-static-layers/index.html
Normal 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>
|
||||
3032
examples/2-static-layers/package-lock.json
generated
Normal file
3032
examples/2-static-layers/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
examples/2-static-layers/package.json
Normal file
29
examples/2-static-layers/package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
1
examples/2-static-layers/public/vite.svg
Normal file
1
examples/2-static-layers/public/vite.svg
Normal 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 |
23
examples/2-static-layers/src/App.tsx
Normal file
23
examples/2-static-layers/src/App.tsx
Normal 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
|
||||
9
examples/2-static-layers/src/main.tsx
Normal file
9
examples/2-static-layers/src/main.tsx
Normal 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>,
|
||||
)
|
||||
23
examples/2-static-layers/src/sample-data.ts
Normal file
23
examples/2-static-layers/src/sample-data.ts
Normal 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",
|
||||
}
|
||||
]
|
||||
1
examples/2-static-layers/src/vite-env.d.ts
vendored
Normal file
1
examples/2-static-layers/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
26
examples/2-static-layers/tsconfig.app.json
Normal file
26
examples/2-static-layers/tsconfig.app.json
Normal 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"]
|
||||
}
|
||||
7
examples/2-static-layers/tsconfig.json
Normal file
7
examples/2-static-layers/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
examples/2-static-layers/tsconfig.node.json
Normal file
24
examples/2-static-layers/tsconfig.node.json
Normal 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"]
|
||||
}
|
||||
7
examples/2-static-layers/vite.config.ts
Normal file
7
examples/2-static-layers/vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
@ -9,7 +9,8 @@
|
||||
"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 .",
|
||||
"update": "npx npm-check-updates"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
/* 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 */
|
||||
@ -8,8 +7,7 @@
|
||||
/* 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'
|
||||
@ -79,8 +77,6 @@ export const Layer = ({
|
||||
const addPopup = useAddPopup()
|
||||
const leafletRefs = useLeafletRefs()
|
||||
|
||||
const location = useLocation()
|
||||
|
||||
const allTagsLoaded = useAllTagsLoaded()
|
||||
const allItemsLoaded = useAllItemsLoaded()
|
||||
|
||||
@ -92,8 +88,6 @@ export const Layer = ({
|
||||
const [newTagsToAdd, setNewTagsToAdd] = useState<Tag[]>([])
|
||||
const [tagsReady, setTagsReady] = useState<boolean>(false)
|
||||
|
||||
const map = useMap()
|
||||
|
||||
const isLayerVisible = useIsLayerVisible()
|
||||
|
||||
const isGroupTypeVisible = useIsGroupTypeVisible()
|
||||
@ -170,64 +164,6 @@ export const Layer = ({
|
||||
// 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 = {}
|
||||
|
||||
@ -135,6 +135,27 @@
|
||||
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,3 +1,6 @@
|
||||
import { LatLng } from 'leaflet'
|
||||
import { MapContainer } from 'react-leaflet'
|
||||
|
||||
import { ContextWrapper } from '#components/AppShell/ContextWrapper'
|
||||
|
||||
import { UtopiaMapInner } from './UtopiaMapInner'
|
||||
@ -7,10 +10,37 @@ 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,
|
||||
}: 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}
|
||||
>
|
||||
{children}
|
||||
</UtopiaMapInner>
|
||||
</MapContainer>
|
||||
</ContextWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,29 +6,24 @@
|
||||
/* 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'
|
||||
import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from 'react'
|
||||
import { TileLayer, useMapEvents, GeoJSON, useMap } from 'react-leaflet'
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
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 { getValue } from '#utils/GetValue'
|
||||
|
||||
import { useClusterRef, useSetClusterRef } from './hooks/useClusterRef'
|
||||
import { useAddVisibleLayer } from './hooks/useFilter'
|
||||
import { useLayers } from './hooks/useLayers'
|
||||
import { useLeafletRefs } from './hooks/useLeafletRefs'
|
||||
import {
|
||||
useSelectPosition,
|
||||
useSetMapClicked,
|
||||
@ -48,13 +43,7 @@ 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,
|
||||
@ -62,7 +51,6 @@ export function UtopiaMapInner({
|
||||
showLayerControl = true,
|
||||
infoText,
|
||||
}: UtopiaMapProps) {
|
||||
// Hooks that rely on contexts, called after ContextWrapper is provided
|
||||
const selectNewItemPosition = useSelectPosition()
|
||||
const setSelectNewItemPosition = useSetSelectPosition()
|
||||
const setClusterRef = useSetClusterRef()
|
||||
@ -72,6 +60,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))
|
||||
@ -105,9 +97,64 @@ 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
|
||||
else if (item?.layer?.itemNameField) title = getValue(item, item.layer.itemNameField)
|
||||
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
|
||||
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])
|
||||
|
||||
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 +177,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)}
|
||||
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} />
|
||||
|
||||
@ -1,11 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
/* eslint-disable @typescript-eslint/prefer-optional-chain */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/require-await */
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { useRef, useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
@ -19,7 +11,7 @@ import { MapOverlayPage } from './MapOverlayPage'
|
||||
import type { Item } from '#types/Item'
|
||||
import type { ItemsApi } from '#types/ItemsApi'
|
||||
|
||||
export const AttestationForm = ({ api }: { api?: ItemsApi<any> }) => {
|
||||
export const AttestationForm = ({ api }: { api?: ItemsApi<unknown> }) => {
|
||||
const items = useItems()
|
||||
const appState = useAppState()
|
||||
const [users, setUsers] = useState<Item[]>()
|
||||
@ -46,10 +38,12 @@ export const AttestationForm = ({ api }: { api?: ItemsApi<any> }) => {
|
||||
setInputValue(event.target.value)
|
||||
}
|
||||
|
||||
const sendAttestation = async () => {
|
||||
const sendAttestation = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const to: any[] = []
|
||||
users?.map((u) => to.push({ directus_users_id: u.user_created?.id }))
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
api?.createItem &&
|
||||
toast
|
||||
.promise(
|
||||
@ -65,6 +59,7 @@ export const AttestationForm = ({ api }: { api?: ItemsApi<any> }) => {
|
||||
success: 'Attestation created',
|
||||
error: {
|
||||
render({ data }) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
return `${data}`
|
||||
},
|
||||
},
|
||||
@ -73,8 +68,10 @@ export const AttestationForm = ({ api }: { api?: ItemsApi<any> }) => {
|
||||
.then(() =>
|
||||
navigate(
|
||||
'/item/' +
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
|
||||
items.find(
|
||||
(i) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
i.user_created?.id === to[0].directus_users_id &&
|
||||
i.layer?.itemType.name === 'player',
|
||||
)?.id +
|
||||
@ -92,29 +89,28 @@ export const AttestationForm = ({ api }: { api?: ItemsApi<any> }) => {
|
||||
<div className='tw-text-center tw-text-xl tw-font-bold'>Gratitude</div>
|
||||
<div className='tw-text-center tw-text-base tw-text-gray-400'>to</div>
|
||||
<div className='tw-flex tw-flex-row tw-justify-center tw-items-center tw-flex-wrap'>
|
||||
{users &&
|
||||
users.map(
|
||||
(u, k) => (
|
||||
<div key={k} className='tw-flex tw-items-center tw-space-x-3 tw-mx-2 tw-my-1'>
|
||||
{u.image ? (
|
||||
<div className='tw-avatar'>
|
||||
<div className='tw-mask tw-mask-circle tw-w-8 tw-h-8'>
|
||||
<img
|
||||
src={appState.assetsApi.url + u.image + '?width=40&heigth=40'}
|
||||
alt='Avatar'
|
||||
/>
|
||||
</div>
|
||||
{users?.map(
|
||||
(u, k) => (
|
||||
<div key={k} className='tw-flex tw-items-center tw-space-x-3 tw-mx-2 tw-my-1'>
|
||||
{u.image ? (
|
||||
<div className='tw-avatar'>
|
||||
<div className='tw-mask tw-mask-circle tw-w-8 tw-h-8'>
|
||||
<img
|
||||
src={appState.assetsApi.url + u.image + '?width=40&heigth=40'}
|
||||
alt='Avatar'
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className='tw-mask tw-mask-circle tw-text-xl md:tw-text-2xl tw-bg-slate-200 tw-rounded-full tw-w-8 tw-h-8'></div>
|
||||
)}
|
||||
<div>
|
||||
<div className='tw-font-bold'>{u.name}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='tw-mask tw-mask-circle tw-text-xl md:tw-text-2xl tw-bg-slate-200 tw-rounded-full tw-w-8 tw-h-8'></div>
|
||||
)}
|
||||
<div>
|
||||
<div className='tw-font-bold'>{u.name}</div>
|
||||
</div>
|
||||
),
|
||||
', ',
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
', ',
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='tw-w-full'>
|
||||
|
||||
@ -1,21 +1,23 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import { useState } from 'react'
|
||||
|
||||
interface Props {
|
||||
selectedEmoji: string
|
||||
selectedColor: string
|
||||
selectedShape: string
|
||||
setSelectedEmoji: (emoji: string) => void
|
||||
setSelectedColor: (color: string) => void
|
||||
setSelectedShape: (shape: string) => void
|
||||
}
|
||||
|
||||
export const EmojiPicker = ({
|
||||
// eslint-disable-next-line react/prop-types
|
||||
selectedEmoji,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
selectedColor,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
selectedShape,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
setSelectedEmoji,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
setSelectedColor,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
setSelectedShape,
|
||||
}) => {
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const emojis = [
|
||||
@ -77,17 +79,17 @@ export const EmojiPicker = ({
|
||||
setIsOpen(!isOpen)
|
||||
}
|
||||
|
||||
const selectEmoji = (emoji) => {
|
||||
const selectEmoji = (emoji: string) => {
|
||||
setSelectedEmoji(emoji)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const selectShape = (shape) => {
|
||||
const selectShape = (shape: string) => {
|
||||
setSelectedShape(shape)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const selectColor = (color) => {
|
||||
const selectColor = (color: string) => {
|
||||
setSelectedColor(color)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@ -61,6 +61,14 @@ 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>'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@ export {
|
||||
export { AppShell, Content, SideBar, Sitemap } from './Components/AppShell'
|
||||
export {
|
||||
AuthProvider,
|
||||
useAuth,
|
||||
LoginPage,
|
||||
SignupPage,
|
||||
RequestPasswordPage,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user