Merge branch 'main' into gallery-component

This commit is contained in:
Anton Tranelis 2025-02-02 18:21:27 +00:00
commit acc502edde
47 changed files with 9196 additions and 286 deletions

View File

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

View File

@ -37,7 +37,7 @@ module.exports = {
},
},
react: {
version: 'detect',
version: '18.2.0',
},
},
rules: {

View File

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

View File

@ -1,15 +1,71 @@
# 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/)
* make use of the Daisy UI [theme colors](https://daisyui.com/docs/colors/)
* make use of the Daisy UI [theme colors](https://daisyui.com/docs/colors/)

3
Components.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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>
```
![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 "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

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>

3032
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,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"
}
}

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()],
})

View File

@ -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"

View File

@ -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 = {}

View File

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

View File

@ -1,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>
)
}

View File

@ -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='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url='https://tile.osmand.net/hd/{z}/{x}/{y}.png'
/>
<MarkerClusterGroup
ref={(r) => setClusterRef(r)}
showCoverageOnHover
chunkedLoading
maxClusterRadius={50}
removeOutsideVisibleBounds={false}
>
<Outlet />
<Control position='topLeft' zIndex='1000' absolute>
<SearchControl />
<TagsControl />
</Control>
<Control position='bottomLeft' zIndex='999' absolute>
{showFilterControl && <FilterControl />}
{showLayerControl && <LayerControl />}
{showGratitudeControl && <GratitudeControl />}
</Control>
<TileLayer
maxZoom={19}
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url='https://tile.osmand.net/hd/{z}/{x}/{y}.png'
/>
<MarkerClusterGroup
ref={(r) => setClusterRef(r)}
showCoverageOnHover
chunkedLoading
maxClusterRadius={50}
removeOutsideVisibleBounds={false}
>
{Children.toArray(children).map((child) =>
isValidElement<{
setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>
itemFormPopup: ItemFormPopupProps | null
clusterRef: React.MutableRefObject<undefined>
}>(child)
? cloneElement(child, { setItemFormPopup, itemFormPopup, clusterRef })
: child,
)}
</MarkerClusterGroup>
{geo && (
<GeoJSON
data={geo}
onEachFeature={onEachFeature}
eventHandlers={{
click: (e) => {
if (selectNewItemPosition) {
e.layer.closePopup()
setMapClicked({ position: e.latlng, setItemFormPopup })
}
},
}}
/>
{Children.toArray(children).map((child) =>
isValidElement<{
setItemFormPopup: React.Dispatch<React.SetStateAction<ItemFormPopupProps>>
itemFormPopup: ItemFormPopupProps | null
clusterRef: React.MutableRefObject<undefined>
}>(child)
? cloneElement(child, { setItemFormPopup, itemFormPopup, clusterRef })
: child,
)}
<MapEventListener />
</MapContainer>
</MarkerClusterGroup>
{geo && (
<GeoJSON
data={geo}
onEachFeature={onEachFeature}
eventHandlers={{
click: (e) => {
if (selectNewItemPosition) {
e.layer.closePopup()
setMapClicked({ position: e.latlng, setItemFormPopup })
}
},
}}
/>
)}
<MapEventListener />
<AddButton triggerAction={setSelectNewItemPosition} />
{selectNewItemPosition != null && (
<SelectPosition setSelectNewItemPosition={setSelectNewItemPosition} />

View File

@ -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'>

View File

@ -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)
}

View File

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

View File

@ -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 ''
}

View File

@ -19,7 +19,6 @@ export {
export { AppShell, Content, SideBar, Sitemap } from './Components/AppShell'
export {
AuthProvider,
useAuth,
LoginPage,
SignupPage,
RequestPasswordPage,