Browse Source

added frontend files and folders to git

main
suroh 10 months ago
parent
commit
246e2cc1fb
  1. 9
      README.md
  2. 14
      backend/.env.template
  3. 31
      backend/README.md
  4. 25
      backend/controllers/maps.js
  5. 18
      backend/controllers/points.js
  6. 23
      backend/db/DB.js
  7. 34
      backend/db/migrations/20230908082728_migration_create_table.js
  8. 21
      backend/db/models/map.js
  9. 7
      backend/db/models/point.js
  10. 30
      backend/express.js
  11. 17
      backend/index.js
  12. 64
      backend/knexfile.js
  13. 6
      backend/middleware/errors.js
  14. 6636
      backend/package-lock.json
  15. 38
      backend/package.json
  16. 15
      backend/routes/api.js
  17. 19
      backend/sockets.js
  18. 37
      backend/tests/db.js
  19. 93
      backend/tests/routes.js
  20. 53
      frontend/.eslintrc.cjs
  21. 24
      frontend/.gitignore
  22. 25
      frontend/README.md
  23. 26
      frontend/index.html
  24. 1939
      frontend/package-lock.json
  25. 21
      frontend/package.json
  26. 8
      frontend/public/icons/cursor.svg
  27. 17
      frontend/public/styles/main.css
  28. 1
      frontend/public/vite.svg
  29. 53
      frontend/src/EthermapApp.js
  30. 122
      frontend/src/components/MapView.js
  31. 72
      frontend/src/components/NewMapModal.js
  32. 104
      frontend/src/components/PointModal.js
  33. 21
      frontend/src/controllers/MapController.js
  34. 28
      frontend/src/controllers/PointController.js
  35. 36
      frontend/src/controllers/Router.js
  36. 9
      frontend/vite.config.js

9
README.md

@ -0,0 +1,9 @@
# ethermap
> This is very much a janky _earlydays_ project. All help is welcome!
An interactive map tool. A tool for collaborative planning on maps. Anyone can create new maps, and add, modify and delete locations on any map.
## Install
Ethermap is built in JavaScript (soz) using NodeJS. Install instructinos for the back and frontend are in their respective folders.

14
backend/.env.template

@ -0,0 +1,14 @@
# ethermap
SITE_NAME=
# Server Setup
PORT=
# Database Setup
DB_PROVIDER=
DB_HOST=
DB_PORT=
DB_NAME=
DB_USER=
DB_PASS=

31
backend/README.md

@ -0,0 +1,31 @@
# ethermap api
Backend for ethermap
## Install
To install and run the backen you will need [NodeJS](https://nodejs.org/en) and `npm` installed, along with access to Postgresql server (possibility for this to be any database server). Then :
```sh
$ npm i
```
Once all the packages are installed you should setup your `.env` file (follow the `.env.template`). Once this has all the appropriate entries you can then connect and migrate the database.
```sh
$ npm run migrate:latest
```
then to run the development server you should run :
```sh
$ npm run dev
```
## Tech
The backend is made up of a REST api and websocket server. The REST api is built on [Express](https://expressjs.com/) and the websocket server is built on [socket.io](https://socket.io/).
Database interface is the ODM [objection.js](https://vincit.github.io/objection.js/). This setup might not be the best as it was adopted mid-project after starting with just [Knex](https://knexjs.org/) alone.
Tests are written in [Ava](https://github.com/avajs/ava).

25
backend/controllers/maps.js

@ -0,0 +1,25 @@
import MapModel from '../db/models/map.js'
const getAllMaps = async (req, res) => {
const maps = await MapModel.query()
res.json({ maps })
}
const getMapByName = async (req, res) => {
const name = req.params.mapName
try {
let map = await MapModel.query().where({ name }).withGraphFetched('map_points').first()
if (!map) {
const created = await MapModel.query().insert({ name })
map = await MapModel.query().findById(created.id).withGraphFetched('map_points')
}
res.json(map)
} catch (error) {
console.error(error)
}
}
export { getMapByName, getAllMaps }

18
backend/controllers/points.js

@ -0,0 +1,18 @@
import MapModel from '../db/models/map.js'
const setPoint = async (req, res, next) => {
try {
const { mapId, point } = req.body
console.log(req.body)
const map = await MapModel.query().findById(mapId)
const p = await map.$relatedQuery('map_points').insert(point)
res.json(p)
} catch (err) {
next(err)
}
}
export { setPoint }

23
backend/db/DB.js

@ -0,0 +1,23 @@
import knexConfig from '../knexfile.js'
import Knex from 'knex'
import { Model } from 'objection'
import { newDb } from 'pg-mem'
const environment = process.env.NODE_ENV || 'development'
// variable for exporting the db
let DB
if (environment == 'test') {
const mem = newDb()
DB = mem.adapters.createKnex(0, {
migrations: {
directory: './db/migrations'
},
})
Model.knex(DB)
} else {
DB = Knex(knexConfig[environment])
}
export default DB

34
backend/db/migrations/20230908082728_migration_create_table.js

@ -0,0 +1,34 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
const up = (knex) => {
return knex.schema
.createTable('maps', (table) => {
table.increments().primary()
table.string('name').notNullable().unique()
table.timestamp('created_at').defaultTo(knex.fn.now())
table.timestamp('updated_at').defaultTo(knex.fn.now())
})
.createTable('map_points', (table) => {
table.increments().primary()
table.string('name')
table.string('notes')
table.point('location')
table.timestamp('created_at').defaultTo(knex.fn.now())
table.timestamp('updated_at').defaultTo(knex.fn.now())
table.integer('map_id').references('id').inTable('maps')
})
}
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
const down = (knex) => {
return knex.schema
.raw('DROP TABLE maps CASCADE')
.dropTable('map_points')
}
export { up, down }

21
backend/db/models/map.js

@ -0,0 +1,21 @@
import { Model } from 'objection'
import Point from './point.js'
class MapModel extends Model {
static tableName = 'maps'
static get relationMappings() {
return {
map_points: {
relation: Model.HasManyRelation,
modelClass: Point,
join: {
from: 'maps.id',
to: 'map_points.map_id',
}
}
}
}
}
export default MapModel

7
backend/db/models/point.js

@ -0,0 +1,7 @@
import { Model } from 'objection'
class PointModel extends Model {
static tableName = 'map_points'
}
export default PointModel

30
backend/express.js

@ -0,0 +1,30 @@
// web server
import express from 'express'
import cors from 'cors'
// database
import DB from './db/DB.js'
import { Model } from 'objection'
// middleware
import ErrorMiddleware from './middleware/errors.js'
// database setup
Model.knex(DB)
// webserver setup
const app = express()
app.use(express.json())
app.use(cors())
// home route
app.get('/', (_, res) => res.send('ethermap'))
// routes
import apiRouter from './routes/api.js'
app.use('/api', apiRouter)
// error middleware
app.use(ErrorMiddleware)
export default app

17
backend/index.js

@ -0,0 +1,17 @@
import App from './express.js'
import 'dotenv/config'
import { Socket } from './sockets.js'
import { Server } from 'socket.io'
const server = App.listen(process.env.PORT, () => {
console.log(`Ethermap listening for connections on port ${process.env.PORT}`)
})
const io = new Server(server, {
cors: {
origin: 'http://localhost:5173'
}
})
Socket(io)

64
backend/knexfile.js

@ -0,0 +1,64 @@
// Update with your config settings.
import 'dotenv/config'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
/**
* @type { Object.<string, import("knex").Knex.Config> }
*/
export default {
development: {
client: 'pg',
connection: {
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASS,
host: process.env.DB_HOST,
port: process.env.DB_PORT
},
migrations: {
directory: __dirname + '/db/migrations'
},
seeds: {
directory: __dirname + '/db/seeds'
}
},
staging: {
client: 'pg',
connection: {
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASS,
host: process.env.DB_HOST,
port: process.env.DB_PORT
},
pool: {
min: 2,
max: 10
},
migrations: {
tableName: 'knex_migrations'
}
},
production: {
client: 'pg',
connection: {
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASS,
host: process.env.DB_HOST,
port: process.env.DB_PORT
},
pool: {
min: 2,
max: 10
},
migrations: {
tableName: 'knex_migrations'
}
}
}

6
backend/middleware/errors.js

@ -0,0 +1,6 @@
// TODO@me update error handler
export default (err, _, res) => {
res.status(500).json({ message: err.message })
}

6636
backend/package-lock.json

File diff suppressed because it is too large

38
backend/package.json

@ -0,0 +1,38 @@
{
"name": "ethermap",
"version": "0.0.1",
"description": "collaborative map tool inspired by etherpad",
"main": "index.js",
"type": "module",
"scripts": {
"dev": "nodemon index.js",
"test": "ava",
"test:routes": "ava ./tests/routes.js",
"test:db": "ava ./tests/db.js",
"migrate:latest": "knex migrate:latest",
"migrate:drop": "knex migrate:down"
},
"keywords": [
"ethermap",
"map",
"collaborative"
],
"author": "",
"license": "GPL-3.0-or-later",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"knex": "^2.5.1",
"objection": "^3.1.1",
"pg": "^8.11.3",
"socket.io": "^4.7.2"
},
"devDependencies": {
"ava": "^5.3.1",
"eslint": "^8.48.0",
"nodemon": "^3.0.1",
"pg-mem": "^2.6.13",
"supertest": "^6.3.3"
}
}

15
backend/routes/api.js

@ -0,0 +1,15 @@
import { Router } from 'express'
const router = Router()
// MAPS
import { getAllMaps } from '../controllers/maps.js'
import { getMapByName } from '../controllers/maps.js'
router.get('/maps', getAllMaps)
router.get('/map/:mapName', getMapByName)
// POINTS
import { setPoint } from '../controllers/points.js'
router.post('/point/add', setPoint)
export default router

19
backend/sockets.js

@ -0,0 +1,19 @@
export const Socket = (io) => {
io.on('connection', (socket) => {
console.log('client connected with id', socket.id)
let roomId = null
socket.on('connect-map', (mapId) => {
console.log('connect to map room', mapId)
roomId = `map-${mapId}`
socket.join(roomId)
})
socket.on('mousemove', (latlng) => {
socket.to(roomId).emit('mousemove', { id: socket.id, pos: latlng })
})
})
}

37
backend/tests/db.js

@ -0,0 +1,37 @@
// testing tools
import test from 'ava'
import db from '../db/DB.js'
// db model
import MapModel from '../db/models/map.js'
test.before(async () => {
await db.migrate.latest()
})
test('Selecting maps should return array', async t => {
const maps = await MapModel.query()
t.truthy(maps)
})
test.serial('Inserting map returns map object', async t => {
const map = await MapModel.query().insert({ name: 'milo' })
t.is(map.name, 'milo')
})
test.serial('Insert point for existing map returns point', async t => {
const map = await MapModel.query().where({ name: 'milo' }).first()
const point = await map.$relatedQuery('map_points').insert({
name: 'pointy',
location: '(50.8552,4.3454)',
})
t.is(point.name, 'pointy')
t.is(point.location, '(50.8552,4.3454)')
})
test.after(async () => {
await db.migrate.down()
})

93
backend/tests/routes.js

@ -0,0 +1,93 @@
// testing tools
import test from 'ava'
import request from 'supertest'
// express app
import App from '../express.js'
import db from '../db/DB.js'
test.before(async t => {
await db.migrate.latest()
})
test.serial('get "/" route should return body of "ethermap"', async t => {
const res = await request(App).get('/')
t.is(res.status, 200)
t.is(res.text, 'ethermap')
})
test.serial('get "/api/maps" route should return an object containing an array called "maps"', async t => {
const res = await request(App).get('/api/maps')
t.is(res.status, 200)
t.truthy(res.body.maps?.constructor === Array)
})
test.serial('get "/api/map/:mapName" route should return map with matching name', async t => {
const res = await request(App).get('/api/map/bingo')
t.is(res.status, 200)
t.is(res.body.name, 'bingo')
})
test.serial('get "/api/map/:mapName" route with different mapName should create new map with different id', async t => {
const res = await request(App).get('/api/map/cheese')
t.is(res.status, 200)
t.truthy(res.body.id)
t.not(res.body.id, 1)
})
test.serial('get "/api/map/:mapName" route with existing mapName should return same id', async t => {
const res = await request(App).get('/api/map/bingo')
t.is(res.status, 200)
t.is(res.body.id, 1)
})
test.serial('post "/api/point/add" body containing a name, location and map_id should return a point', async t => {
const { body: { id: mapId } } = await request(App).get('/api/map/bingo')
const res = await request(App)
.post('/api/point/add')
.send({
mapId,
point: {
name: 'pointy',
location: '(50.8552,4.3454)',
}
})
t.is(res.status, 200)
t.is(res.body.id, 1)
t.is(res.body.map_id, mapId)
t.is(res.body.name, 'pointy')
})
test.serial('get "/api/map/:mapName" with associated points should return a map with an array of points', async t => {
const res = await request(App).get('/api/map/bingo')
t.is(res.status, 200)
t.truthy(res.body.map_points)
t.is(res.body.map_points.length, 1)
})
test.serial('post "/api/point/add" with incorrect data keys throws 500 error', async t => {
const { body: { id: mapId } } = await request(App).get('/api/map/bingo')
const error = await request(App)
.post('/api/point/add')
.send({
mapId,
point: {
title: 'pointy',
coords: '(50.8552,4.3454)',
}
})
t.is(error.status, 500)
})
test.after(async () => {
await db.migrate.down()
})

53
frontend/.eslintrc.cjs

@ -0,0 +1,53 @@
module.exports = {
root: true,
'env': {
'browser': true,
'node': true,
'es2019': true
},
'extends': [
'eslint:recommended',
'plugin:lit/recommended',
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
'rules': {
'no-var': 'error',
'semi': [
'error',
'never',
],
'quotes': [
'error',
'single',
],
'object-curly-spacing': [
'warn',
'always',
],
'array-bracket-spacing': [
'warn',
'always',
],
'space-in-parens': [
'warn',
'never',
],
'array-bracket-newline': [
'warn',
'consistent',
],
'object-curly-newline': [
'warn',
{
'consistent': true,
},
],
'space-before-blocks': [
'warn',
'always',
],
},
};

24
frontend/.gitignore

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

25
frontend/README.md

@ -0,0 +1,25 @@
# ethermap frontend
Frontend for ethermap
## Install
To install the frontend you will need [NodeJS](https://nodejs.org/en) and `npm` installed. Then :
```sh
$ npm i
```
To run the development server run :
```sh
$ npm run dev
```
## Tech
The interface is built with [LitElement](https://lit.dev/) and setup with [Vite](https://vitejs.dev/) bundler and dev server.
Maps are rendered with [Leaflet](https://leafletjs.com).
For ethermap to work you will also need to be running the [ethermap.api]() server for REST API and Socket.io connectivity.

26
frontend/index.html

@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<!-- leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<!-- styles -->
<link rel="stylesheet" href="/styles/main.css">
<!-- javascript -->
<script type="module" src="/src/EthermapApp.js"></script>
</head>
<body>
<ethermap-app></ethermap-app>
</body>
</html>

1939
frontend/package-lock.json

File diff suppressed because it is too large

21
frontend/package.json

@ -0,0 +1,21 @@
{
"name": "ethermap.front",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"eslint-plugin-lit": "^1.9.1",
"vite": "^4.4.5"
},
"dependencies": {
"leaflet": "^1.9.4",
"leaflet-contextmenu": "^1.4.0",
"lit": "^2.8.0",
"socket.io-client": "^4.7.2"
}
}

8
frontend/public/icons/cursor.svg

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 63.269 55.583 l 25.589 -10.294 c 1.682 -0.677 1.459 -3.168 -0.362 -4.041 L 2.884 0.205 c -1.71 -0.82 -3.499 0.969 -2.679 2.679 l 41.043 85.612 c 0.873 1.821 3.364 2.044 4.041 0.362 l 10.294 -25.589 C 56.991 59.767 59.767 56.991 63.269 55.583 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 951 B

17
frontend/public/styles/main.css

@ -0,0 +1,17 @@
body {
font-family: sans-serif;
margin: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
}
nav {
padding-inline: 0.75em;
padding-block: 0.75em;
}
main.map {
display: block;
flex-grow: 1;
}

1
frontend/public/vite.svg

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

53
frontend/src/EthermapApp.js

@ -0,0 +1,53 @@
import { LitElement, html, css } from 'lit'
import './components/PointModal.js'
import Router from './controllers/Router.js'
class EthermapApp extends LitElement {
static get properties() {
return {
route: {}
}
}
constructor() {
super()
this.route = Router.render()
}
firstUpdated() {
Router.addEventListener('route-changed', () => {
console.log('route-changed')
this.route = Router.render()
})
this.addEventListener('open-modal', (evt) => {
const modal = this.shadowRoot.querySelector('point-modal')
modal.open(evt.detail.mapId, evt.detail.latlng)
})
}
render() {
return html`
<nav>toolbar</nav>
${this.route}
<point-modal></point-modal>
`
}
static get styles() {
return css`
:host {
display: flex;
flex-direction: column;
height: 100vh;
}
nav {
padding-inline: 0.75em;
padding-block: 0.75em;
}
`
}
}
window.customElements.define('ethermap-app', EthermapApp)

122
frontend/src/components/MapView.js

@ -0,0 +1,122 @@
import { unsafeCSS, LitElement, html, css } from 'lit'
import leafletCss from 'leaflet/dist/leaflet.css?inline'
import leafletContextCss from 'leaflet-contextmenu/dist/leaflet.contextmenu.css?inline'
import * as L from 'leaflet'
import 'leaflet-contextmenu'
import { io } from 'socket.io-client'
import MapController from '../controllers/MapController'
class MapView extends LitElement {
mapController = new MapController(this)
static get properties() {
return {
name: { type: String },
map: { state: true },
points: { type: Array, state: true },
leaflet: { state: true },
socket: { state: true },
users: { state: true }
}
}
constructor() {
super()
this.name = ''
this.leaflet = {}
this.points = []
this.users = []
}
firstUpdated() {
// connect to socket
this.socket = io('http://localhost:3000')
// create leaflet map
const mapEl = this.shadowRoot.querySelector('main')
this.leaflet = L.map(mapEl, {
contextmenu: true,
contextmenuWidth: 140,
contextmenuItems: [ {
text: 'create point',
callback: (evt) => {
const event = new CustomEvent('open-modal', {
bubbles: true, composed: true,
detail: { mapId: this.mapController.map.id, latlng: evt.latlng }
})
this.dispatchEvent(event)
}
} ]
}).setView([ 51.89190, 4.46920 ], 19)
// set map tiles
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(this.leaflet)
// track mouse movement
this.leaflet.on('mousemove', (evt) => {
this.socket.emit('mousemove', evt.latlng)
})
// track other users
this.socket.on('mousemove', (data) => {
// TODO@me check whether user is in view before rendering
// TODO@me restrict rendering within range of zoom-level
const { id, pos } = data
// if user is not created, then do so
if (!this.users[id]) {
const cursorIcon = L.icon({ iconUrl: '/icons/cursor.svg', iconSize: [ 20, 30 ], iconAnchor: [ 0, 8 ], className: 'user-cursor' })
this.users[id] = L.marker([ pos.lat, pos.lng ], { icon: cursorIcon, interactive: false, zIndexOffset: 1000 }).addTo(this.leaflet)
}
// update cursor's position
this.users[id].setLatLng([ pos.lat, pos.lng ])
})
// get the map with name
this.mapController.get(this.name).then(m => {
this.socket.emit('connect-map', m.id)
// set points
this.setPoints(m.map_points)
})
}
setPoints(points) {
points.forEach((p, i) => {
this.points[i] = L.marker([ p.location.x, p.location.y ]).addTo(this.leaflet)
this.points[i].bindPopup(`<h3>${p.name}</h3>${p.notes ? `<p>${p.notes}</p>` : ''}`)
})
const end = points.length - 1
this.leaflet.setView([ points[end].location.x, points[end].location.y ], 13)
}
render() {
return html`<main></main>`
}
static get styles() {
return [ unsafeCSS(leafletCss), unsafeCSS(leafletContextCss), css`
:host {
display: flex;
flex-direction: column;
flex-grow: 1;
}
main {
flex-grow: 1;
}
` ]
}
}
window.customElements.define('map-view', MapView)

72
frontend/src/components/NewMapModal.js

@ -0,0 +1,72 @@
import { LitElement, html, css } from "lit";
import Router from "../controllers/Router";
class NewMapModal extends LitElement {
static get properties() {
return {
mapName: { state: true }
}
}
constructor() {
super()
this.mapName = ''
}
navigate(evt) {
evt.preventDefault()
Router.navigate(`/m/${this.mapName}`)
}
render() {
return html`
<main>
<div class="window">
<form>
<label for="name">map name</label>
<input type="text" name="name" @input=${e => this.mapName = e.target.value}></input>
<div class="controls">
<button @click=${this.navigate}>go</button>
</div>
</form>
</div>
</main>`
}
static get styles() {
return css`
main {
position: absolute;
inset: 0;
display: grid;
align-content: center;
justify-items: center;
background: #33333350;
z-index: 1000;
}
.window {
background: white;
padding: 1em;
border: thick solid black;
}
.controls {
display: flex;
justify-content: end;
}
button {
min-width: 5ch;
}
form {
display: grid;
grid-auto-flow: rows;
gap: 0.25em;
}
`
}
}
window.customElements.define('newmap-modal', NewMapModal)

104
frontend/src/components/PointModal.js

@ -0,0 +1,104 @@
import { LitElement, html, css } from 'lit'
import { live } from 'lit/directives/live.js'
import PointController from '../controllers/PointController.js'
class PointModal extends LitElement {
pointController = new PointController(this)
static get properties() {
return {
latlng: { type: Object },
mapId: { type: Number, state: true },
id: { type: Number },
name: { type: String },
notes: { type: String },
modalEl: { state: true }
}
}
constructor() {
super()
this.id = ''
this.mapId = ''
this.name = ''
this.notes = ''
this.latlng = {}
this.modalEl = {}
}
firstUpdated() {
this.modalEl = this.shadowRoot.querySelector('dialog')
}
open(mapId, latlng) {
this.mapId = mapId
console.log(mapId)
this.latlng = { ...latlng }
this.name = ''
this.notes = ''
this.modalEl.showModal()
}
close(evt) {
evt.preventDefault()
this.modalEl.close()
}
save(evt) {
evt.preventDefault()
console.log(this.mapId)
PointController.savePoint(this.mapId, { name: this.name, notes: this.notes, location: this.latlng })
// TODO@me validate form entry
// TODO@me check if successful
this.close(evt)
}
render() {
return html`
<dialog>
<h2>create new point</h2>
<p>${this.latlng.lat}, ${this.latlng.lng}</p>
<form>
<label name="name" for="name">name</label>
<input name="name" type="text" .value=${live(this.name)} @input=${e => this.name = e.target.value}>
<label name="notes" for="notes">notes</label>
<textarea name="notes" .value=${live(this.notes)} @input=${e => this.notes = e.target.value}></textarea>
<div class="controls">
<button @click=${this.close}>cancel</button> <button @click=${this.save}>save</button>
</div>
</form>
</dialog>
`
}
static get styles() {
return css`
form {
display: grid;
grid-auto-flow: rows;
}
.controls {
display: flex;
}
.controls > :first-child {
margin-inline-start: auto;
margin-inline-end: 0.25em;
}
label, .controls {
margin-block-start: 0.25em;
}
h2 {
margin-block: 0.125em;
}
`
}
}
window.customElements.define('point-modal', PointModal)

21
frontend/src/controllers/MapController.js

@ -0,0 +1,21 @@
export default class MapController {
constructor(host) {
this.host = host
this.host.addController(this)
this.map = {}
}
async get(name) {
try {
const req = await fetch(`http://localhost:3000/api/map/${name}`)
const json = await req.json()
this.map = json
return json
} catch (err) {
console.error(err)
}
}
}

28
frontend/src/controllers/PointController.js

@ -0,0 +1,28 @@
export default class PointController {
constructor(host) {
this.host = host
this.host.addController(this)
}
static async savePoint(mapId, point) {
try {
// set location to point for db
point.location = `(${point.location.lat}, ${point.location.lng})`
const res = await fetch('http://localhost:3000/api/point/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
mapId,
point
})
})
console.log(res)
} catch (err) {
console.error(err)
}
}
}

36
frontend/src/controllers/Router.js

@ -0,0 +1,36 @@
import { html } from 'lit'
import { Router } from '@thepassle/app-tools/router.js'
// views
import '../components/MapView.js'
import '../components/NewMapModal.js'
if (!globalThis.URLPattern) {
await import('urlpattern-polyfill')
}
// router
export default new Router({
fallback: '/404',
routes: [
{
path: '/',
title: 'ethermap | index',
render: () => html`
<map-view></map-view>
<newmap-modal></newmap-modal>
`
},
{
path: '/m/:mapId',
title: ({ params }) => `ethermap | ${params.mapId}`,
render: ({ params }) => html`<map-view name=${params.mapId}></map-view>`
},
{
path: '/404',
title: 'etherpad | 404',
render: () => html`<h2>404 : page not found</h2>`
}
]
})

9
frontend/vite.config.js

@ -0,0 +1,9 @@
import { defineConfig } from 'vite'
export default defineConfig({
base: '/',
build: {
sourcemap: true,
target: [ 'esnext', 'edge100', 'firefox100', 'chrome100', 'safari18' ]
}
})
Loading…
Cancel
Save