finishes trail create
This commit is contained in:
16
.vscode/tasks.json
vendored
Normal file
16
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "npm",
|
||||||
|
"script": "dev",
|
||||||
|
"path": "web",
|
||||||
|
"problemMatcher": [
|
||||||
|
"$vite"
|
||||||
|
],
|
||||||
|
"label": "npm: dev - web",
|
||||||
|
"detail": "vite dev",
|
||||||
|
"isBackground": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
58
db/pb_migrations/1706529850_updated_trails.js
Normal file
58
db/pb_migrations/1706529850_updated_trails.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
|
||||||
|
// remove
|
||||||
|
collection.schema.removeField("jjiidvbm")
|
||||||
|
|
||||||
|
// add
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "mcqce8l9",
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "text",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"pattern": ""
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
}, (db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
|
||||||
|
// add
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "jjiidvbm",
|
||||||
|
"name": "thumbnail",
|
||||||
|
"type": "file",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"mimeTypes": [
|
||||||
|
"image/png",
|
||||||
|
"image/vnd.mozilla.apng",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/webp",
|
||||||
|
"image/svg+xml"
|
||||||
|
],
|
||||||
|
"thumbs": [],
|
||||||
|
"maxSelect": 1,
|
||||||
|
"maxSize": 5242880,
|
||||||
|
"protected": false
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// remove
|
||||||
|
collection.schema.removeField("mcqce8l9")
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
})
|
||||||
76
db/pb_migrations/1706533600_updated_waypoints.js
Normal file
76
db/pb_migrations/1706533600_updated_waypoints.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("goeo2ubp103rzp9")
|
||||||
|
|
||||||
|
// update
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "ygotgxzy",
|
||||||
|
"name": "lat",
|
||||||
|
"type": "number",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"noDecimal": false
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// update
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "q0ygnxd2",
|
||||||
|
"name": "lon",
|
||||||
|
"type": "number",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": null,
|
||||||
|
"max": null,
|
||||||
|
"noDecimal": false
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
}, (db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("goeo2ubp103rzp9")
|
||||||
|
|
||||||
|
// update
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "ygotgxzy",
|
||||||
|
"name": "lat",
|
||||||
|
"type": "number",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": 0,
|
||||||
|
"max": null,
|
||||||
|
"noDecimal": false
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// update
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "q0ygnxd2",
|
||||||
|
"name": "lon",
|
||||||
|
"type": "number",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"min": 0,
|
||||||
|
"max": null,
|
||||||
|
"noDecimal": false
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
})
|
||||||
16
db/pb_migrations/1706548284_updated_trails.js
Normal file
16
db/pb_migrations/1706548284_updated_trails.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
|
||||||
|
collection.createRule = ""
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
}, (db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
|
||||||
|
collection.createRule = null
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
})
|
||||||
84
db/pb_migrations/1706551417_updated_trails.js
Normal file
84
db/pb_migrations/1706551417_updated_trails.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
|
||||||
|
// update
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "e1lwowvd",
|
||||||
|
"name": "summit_logs",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "dd2l9a4vxpy2ni8",
|
||||||
|
"cascadeDelete": true,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": null,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// update
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "ppq2sist",
|
||||||
|
"name": "waypoints",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "goeo2ubp103rzp9",
|
||||||
|
"cascadeDelete": true,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": null,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
}, (db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
|
||||||
|
// update
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "e1lwowvd",
|
||||||
|
"name": "summit_logs",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "dd2l9a4vxpy2ni8",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": null,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// update
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "ppq2sist",
|
||||||
|
"name": "waypoints",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "goeo2ubp103rzp9",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": null,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
})
|
||||||
16
db/pb_migrations/1706552026_updated_waypoints.js
Normal file
16
db/pb_migrations/1706552026_updated_waypoints.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("goeo2ubp103rzp9")
|
||||||
|
|
||||||
|
collection.createRule = ""
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
}, (db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("goeo2ubp103rzp9")
|
||||||
|
|
||||||
|
collection.createRule = null
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
})
|
||||||
84
db/pb_migrations/1706552830_updated_trails.js
Normal file
84
db/pb_migrations/1706552830_updated_trails.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
|
||||||
|
// update
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "e1lwowvd",
|
||||||
|
"name": "summit_logs",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "dd2l9a4vxpy2ni8",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": null,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// update
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "ppq2sist",
|
||||||
|
"name": "waypoints",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "goeo2ubp103rzp9",
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": null,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
}, (db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
|
||||||
|
// update
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "e1lwowvd",
|
||||||
|
"name": "summit_logs",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "dd2l9a4vxpy2ni8",
|
||||||
|
"cascadeDelete": true,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": null,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// update
|
||||||
|
collection.schema.addField(new SchemaField({
|
||||||
|
"system": false,
|
||||||
|
"id": "ppq2sist",
|
||||||
|
"name": "waypoints",
|
||||||
|
"type": "relation",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"collectionId": "goeo2ubp103rzp9",
|
||||||
|
"cascadeDelete": true,
|
||||||
|
"minSelect": null,
|
||||||
|
"maxSelect": null,
|
||||||
|
"displayFields": null
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
})
|
||||||
16
db/pb_migrations/1706552880_updated_summit_log.js
Normal file
16
db/pb_migrations/1706552880_updated_summit_log.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("dd2l9a4vxpy2ni8")
|
||||||
|
|
||||||
|
collection.createRule = ""
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
}, (db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("dd2l9a4vxpy2ni8")
|
||||||
|
|
||||||
|
collection.createRule = null
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
})
|
||||||
16
db/pb_migrations/1706552897_updated_summit_log.js
Normal file
16
db/pb_migrations/1706552897_updated_summit_log.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("dd2l9a4vxpy2ni8")
|
||||||
|
|
||||||
|
collection.name = "summit_logs"
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
}, (db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("dd2l9a4vxpy2ni8")
|
||||||
|
|
||||||
|
collection.name = "summit_log"
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
})
|
||||||
16
db/pb_migrations/1706553833_updated_trails.js
Normal file
16
db/pb_migrations/1706553833_updated_trails.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
|
||||||
|
collection.updateRule = ""
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
}, (db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
|
||||||
|
collection.updateRule = null
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
})
|
||||||
16
db/pb_migrations/1706557823_updated_trails.js
Normal file
16
db/pb_migrations/1706557823_updated_trails.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
|
||||||
|
collection.deleteRule = ""
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
}, (db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
|
||||||
|
collection.deleteRule = null
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
})
|
||||||
18
db/pb_migrations/1706558119_updated_waypoints.js
Normal file
18
db/pb_migrations/1706558119_updated_waypoints.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("goeo2ubp103rzp9")
|
||||||
|
|
||||||
|
collection.updateRule = ""
|
||||||
|
collection.deleteRule = ""
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
}, (db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("goeo2ubp103rzp9")
|
||||||
|
|
||||||
|
collection.updateRule = null
|
||||||
|
collection.deleteRule = null
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
})
|
||||||
18
db/pb_migrations/1706558125_updated_summit_logs.js
Normal file
18
db/pb_migrations/1706558125_updated_summit_logs.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("dd2l9a4vxpy2ni8")
|
||||||
|
|
||||||
|
collection.updateRule = ""
|
||||||
|
collection.deleteRule = ""
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
}, (db) => {
|
||||||
|
const dao = new Dao(db)
|
||||||
|
const collection = dao.findCollectionByNameOrId("dd2l9a4vxpy2ni8")
|
||||||
|
|
||||||
|
collection.updateRule = null
|
||||||
|
collection.deleteRule = null
|
||||||
|
|
||||||
|
return dao.saveCollection(collection)
|
||||||
|
})
|
||||||
35
web/package-lock.json
generated
35
web/package-lock.json
generated
@@ -10,12 +10,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||||
"@types/leaflet.awesome-markers": "^2.0.28",
|
"@types/leaflet.awesome-markers": "^2.0.28",
|
||||||
|
"crypto-random-string": "^5.0.0",
|
||||||
|
"date-fns": "^3.3.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet-gpx": "^1.7.0",
|
"leaflet-gpx": "^1.7.0",
|
||||||
"leaflet.awesome-markers": "^2.0.5",
|
"leaflet.awesome-markers": "^2.0.5",
|
||||||
"photoswipe": "^5.4.3",
|
"photoswipe": "^5.4.3",
|
||||||
"pocketbase": "^0.21.0",
|
"pocketbase": "^0.21.0",
|
||||||
"svelte-forms-lib": "^2.0.1",
|
|
||||||
"yup": "^1.3.3"
|
"yup": "^1.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1341,6 +1342,20 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto-random-string": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"type-fest": "^2.12.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-tree": {
|
"node_modules/css-tree": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||||
@@ -1366,6 +1381,15 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
@@ -1408,6 +1432,7 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -3131,14 +3156,6 @@
|
|||||||
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0"
|
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-forms-lib": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/svelte-forms-lib/-/svelte-forms-lib-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-kwbJ3ynsepsrrJyAMrvSc0Lj/myc9vfI2DL8OKxgArZimrNYsRh1gENYhvrcKEI3BiZrv8q3VFfmGo/GMyk7Zg==",
|
|
||||||
"dependencies": {
|
|
||||||
"dequal": "^2.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/svelte-hmr": {
|
"node_modules/svelte-hmr": {
|
||||||
"version": "0.15.3",
|
"version": "0.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
||||||
|
|||||||
@@ -33,12 +33,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||||
"@types/leaflet.awesome-markers": "^2.0.28",
|
"@types/leaflet.awesome-markers": "^2.0.28",
|
||||||
|
"crypto-random-string": "^5.0.0",
|
||||||
|
"date-fns": "^3.3.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet-gpx": "^1.7.0",
|
"leaflet-gpx": "^1.7.0",
|
||||||
"leaflet.awesome-markers": "^2.0.5",
|
"leaflet.awesome-markers": "^2.0.5",
|
||||||
"photoswipe": "^5.4.3",
|
"photoswipe": "^5.4.3",
|
||||||
"pocketbase": "^0.21.0",
|
"pocketbase": "^0.21.0",
|
||||||
"svelte-forms-lib": "^2.0.1",
|
|
||||||
"yup": "^1.3.3"
|
"yup": "^1.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
web/src/app.d.ts
vendored
6
web/src/app.d.ts
vendored
@@ -1,3 +1,6 @@
|
|||||||
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
|
|
||||||
// See https://kit.svelte.dev/docs/types#app
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
@@ -7,6 +10,9 @@ declare global {
|
|||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
|
interface Locals {
|
||||||
|
pb: PocketBase
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,21 @@
|
|||||||
@import 'tailwindcss/components';
|
@import 'tailwindcss/components';
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@apply text-white rounded-lg px-4 py-2 bg-primary font-semibold transition-all hover:bg-gray-900 focus:ring-4 ring-gray-400
|
@apply h-10 text-white rounded-lg px-4 py-2 bg-primary font-semibold transition-all hover:bg-gray-900 focus:ring-4 ring-gray-400
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-large {
|
.btn-large {
|
||||||
@apply py-3
|
@apply h-12 leading-8
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
@apply rounded-lg px-4 py-2 border border-primary font-medium transition-all hover:bg-gray-100 focus:ring-4 ring-gray-200
|
@apply rounded-lg px-4 py-2 border border-primary font-medium transition-all hover:bg-gray-100 focus:ring-4 ring-gray-200
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-disabled {
|
||||||
|
@apply bg-gray-500 hover:bg-gray-500
|
||||||
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
@apply p-4 rounded-t-lg
|
@apply p-4 rounded-t-lg
|
||||||
}
|
}
|
||||||
@@ -29,3 +33,62 @@
|
|||||||
.tab-active:hover {
|
.tab-active:hover {
|
||||||
@apply bg-primary
|
@apply bg-primary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip:before,
|
||||||
|
.tooltip:after {
|
||||||
|
display: block;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
transform: translate3d(0, -10px, 0);
|
||||||
|
transition: all .15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip:before {
|
||||||
|
background: rgba(36, 39, 52, 0.75);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
content: attr(data-title);
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
top: 24px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip:hover:after,
|
||||||
|
.tooltip:hover:before {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner:after {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 6px solid #fff;
|
||||||
|
border-color: #fff transparent #fff transparent;
|
||||||
|
animation: spinner 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinner {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
web/src/hooks.server.ts
Normal file
23
web/src/hooks.server.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
|
export async function handle({ event, resolve }) {
|
||||||
|
event.locals.pb = new PocketBase('http://127.0.0.1:8090');
|
||||||
|
|
||||||
|
// load the store data from the request cookie string
|
||||||
|
event.locals.pb.authStore.loadFromCookie(event.request.headers.get('cookie') || '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// get an up-to-date auth store state by verifying and refreshing the loaded auth model (if any)
|
||||||
|
event.locals.pb.authStore.isValid && await event.locals.pb.collection('users').authRefresh();
|
||||||
|
} catch (_) {
|
||||||
|
// clear the auth store on failed refresh
|
||||||
|
event.locals.pb.authStore.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await resolve(event);
|
||||||
|
|
||||||
|
// send back the default 'pb_auth' cookie to the client with the latest store state
|
||||||
|
response.headers.append('set-cookie', event.locals.pb.authStore.exportToCookie());
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
31
web/src/lib/components/base/button.svelte
Normal file
31
web/src/lib/components/base/button.svelte
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let type: "button" | "submit" | "reset" | null | undefined =
|
||||||
|
undefined;
|
||||||
|
export let icon: string = "";
|
||||||
|
export let extraClasses: string = "";
|
||||||
|
export let primary: boolean = false;
|
||||||
|
export let secondary: boolean = false;
|
||||||
|
export let large: boolean = false;
|
||||||
|
export let loading: boolean = false;
|
||||||
|
export let disabled: boolean = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class={extraClasses}
|
||||||
|
class:btn-primary={primary}
|
||||||
|
class:btn-secondary={secondary}
|
||||||
|
class:btn-large={large}
|
||||||
|
class:btn-disabled={disabled || loading}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
on:click
|
||||||
|
{type}
|
||||||
|
>
|
||||||
|
{#if !loading}
|
||||||
|
{#if icon}
|
||||||
|
<i class="fa fa-{icon} mr-2"></i>
|
||||||
|
{/if}
|
||||||
|
<slot />
|
||||||
|
{:else}
|
||||||
|
<div class="spinner"></div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
29
web/src/lib/components/base/datepicker.svelte
Normal file
29
web/src/lib/components/base/datepicker.svelte
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let name: string = "";
|
||||||
|
export let value: string | number | Date = "";
|
||||||
|
export let label: string = "";
|
||||||
|
export let error: string = "";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if label.length}
|
||||||
|
<p class="text-sm font-medium pb-1">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
{name}
|
||||||
|
class="bg-gray-50 border rounded-md p-3 transition-colors focus:border-primary focus:outline-none focus:ring-0 w-full"
|
||||||
|
class:border-red-400={error.length > 0}
|
||||||
|
class:bg-red-50={error.length > 0}
|
||||||
|
type="date"
|
||||||
|
bind:value
|
||||||
|
on:change
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="textfield-error text-xs text-red-400">
|
||||||
|
{error}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
@@ -7,7 +7,9 @@
|
|||||||
|
|
||||||
let isOpen = false;
|
let isOpen = false;
|
||||||
|
|
||||||
function toggleMenu() {
|
function toggleMenu(e: MouseEvent) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
isOpen = !isOpen;
|
isOpen = !isOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@
|
|||||||
isOpen = false;
|
isOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleItemClick(item: { text: string, value: any }) {
|
function handleItemClick(item: { text: string; value: any }) {
|
||||||
dispatch("change", item);
|
dispatch("change", item);
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}
|
}
|
||||||
@@ -27,6 +29,7 @@
|
|||||||
<button
|
<button
|
||||||
class="hover:bg-gray-100 w-8 h-8 rounded-full"
|
class="hover:bg-gray-100 w-8 h-8 rounded-full"
|
||||||
on:click={toggleMenu}
|
on:click={toggleMenu}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<i class="fa fa-ellipsis-vertical"></i>
|
<i class="fa fa-ellipsis-vertical"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -42,8 +45,8 @@
|
|||||||
<li
|
<li
|
||||||
class="menu-item p-4 cursor-pointer hover:bg-gray-100 focus:bg-gray-200 transition-colors"
|
class="menu-item p-4 cursor-pointer hover:bg-gray-100 focus:bg-gray-200 transition-colors"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
on:mouseup={() => handleItemClick(item)}
|
on:mouseup|stopPropagation={() => handleItemClick(item)}
|
||||||
on:keydown={() => handleItemClick(item)}
|
on:keydown|stopPropagation={() => handleItemClick(item)}
|
||||||
>
|
>
|
||||||
{item.text}
|
{item.text}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
export let name: string = "";
|
||||||
export let value: any;
|
export let value: any;
|
||||||
export let items: { text: string; value: any }[] = [];
|
export let items: { text: string; value: any }[] = [];
|
||||||
export let label: string = "";
|
export let label: string = "";
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<select
|
<select
|
||||||
|
{name}
|
||||||
class="bg-gray-50 h-10 w-full px-4 border-r-8 border-transparent outline outline-1 outline-gray-200 rounded-md focus:outline-primary transition-colors"
|
class="bg-gray-50 h-10 w-full px-4 border-r-8 border-transparent outline outline-1 outline-gray-200 rounded-md focus:outline-primary transition-colors"
|
||||||
bind:value
|
bind:value
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
export let name: string = "";
|
||||||
export let value: string | number = "";
|
export let value: string | number = "";
|
||||||
export let placeholder: string = "";
|
export let placeholder: string = "";
|
||||||
export let label: string = "";
|
export let label: string = "";
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
<i class="fa fa-{icon}"></i>
|
<i class="fa fa-{icon}"></i>
|
||||||
{/if}
|
{/if}
|
||||||
<input
|
<input
|
||||||
|
{name}
|
||||||
class="bg-gray-50 border rounded-md p-3 transition-colors focus:border-primary focus:outline-none focus:ring-0 w-full"
|
class="bg-gray-50 border rounded-md p-3 transition-colors focus:border-primary focus:outline-none focus:ring-0 w-full"
|
||||||
class:border-red-400={error.length > 0}
|
class:border-red-400={error.length > 0}
|
||||||
class:bg-red-50={error.length > 0}
|
class:bg-red-50={error.length > 0}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
export let name: string = "";
|
||||||
export let value: string | number = "";
|
export let value: string | number = "";
|
||||||
export let placeholder: string = "";
|
export let placeholder: string = "";
|
||||||
export let rows: number = 3;
|
export let rows: number = 3;
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<textarea
|
<textarea
|
||||||
|
{name}
|
||||||
class="bg-gray-50 border rounded-md p-3 resize-none transition-colors focus:border-primary focus:outline-none focus:ring-0 w-full"
|
class="bg-gray-50 border rounded-md p-3 resize-none transition-colors focus:border-primary focus:outline-none focus:ring-0 w-full"
|
||||||
{rows}
|
{rows}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
|
|||||||
44
web/src/lib/components/photo_card.svelte
Normal file
44
web/src/lib/components/photo_card.svelte
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
export let src: string;
|
||||||
|
export let isThumbnail: boolean = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function handleThumbnailClick() {
|
||||||
|
dispatch("thumbnail", src);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteClick() {
|
||||||
|
dispatch("delete", src);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="group relative h-32 w-32 rounded-xl bg-cover bg-no-repeat"
|
||||||
|
style="background-image: url({src});"
|
||||||
|
>
|
||||||
|
{#if isThumbnail}
|
||||||
|
<i
|
||||||
|
class="fa fa-file-image absolute top-2 right-2 text-primary bg-white rounded-full px-[10px] py-2 shadow-lg"
|
||||||
|
></i>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
class="flex opacity-0 group-hover:opacity-100 absolute top-0 w-full h-full bg-white bg-opacity-75 items-center justify-center gap-6 transition-all"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tooltip"
|
||||||
|
data-title="Make thumbnail"
|
||||||
|
on:click={handleThumbnailClick}
|
||||||
|
><i class="fa fa-file-image text-primary"></i></button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="tooltip"
|
||||||
|
data-title="Delete"
|
||||||
|
on:click={handleDeleteClick}
|
||||||
|
type="button"><i class="fa fa-trash text-red-500"></i></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
24
web/src/lib/components/summit_log/summit_log_card.svelte
Normal file
24
web/src/lib/components/summit_log/summit_log_card.svelte
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { SummitLog } from "$lib/models/summit_log";
|
||||||
|
import { format, parse } from "date-fns";
|
||||||
|
import Dropdown from "../base/dropdown.svelte";
|
||||||
|
|
||||||
|
export let log: SummitLog;
|
||||||
|
export let mode: "show" | "edit" = "show";
|
||||||
|
|
||||||
|
const dropdownItems = [
|
||||||
|
{ text: "Edit", value: "edit" },
|
||||||
|
{ text: "Delete", value: "delete" },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-4 my-2 border rounded-xl">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<h5 class="font-medium mr-2">{format(log.date, 'dd.MM.yyyy')}</h5>
|
||||||
|
|
||||||
|
{#if mode == "edit"}
|
||||||
|
<Dropdown items={dropdownItems} on:change></Dropdown>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<span>{log.text}</span>
|
||||||
|
</div>
|
||||||
67
web/src/lib/components/summit_log/summit_log_modal.svelte
Normal file
67
web/src/lib/components/summit_log/summit_log_modal.svelte
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
import { summitLogSchema, type SummitLog } from "$lib/models/summit_log";
|
||||||
|
import { summitLog } from "$lib/stores/summit_log_store";
|
||||||
|
import { createForm } from "$lib/vendor/svelte-form-lib/index";
|
||||||
|
import Datepicker from "../base/datepicker.svelte";
|
||||||
|
import Modal from "../base/modal.svelte";
|
||||||
|
import TextField from "../base/text_field.svelte";
|
||||||
|
import { util } from "$lib/vendor/svelte-form-lib/util";
|
||||||
|
|
||||||
|
export let openModal: (() => void) | undefined = undefined;
|
||||||
|
export let closeModal: (() => void) | undefined = undefined;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const { form, errors, handleChange, handleSubmit } = createForm<SummitLog>({
|
||||||
|
initialValues: $summitLog,
|
||||||
|
validationSchema: summitLogSchema,
|
||||||
|
onSubmit: async (submittedValues) => {
|
||||||
|
dispatch("save", submittedValues);
|
||||||
|
closeModal!();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
$: form.set(util.cloneDeep($summitLog));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
id="summit-log-modal"
|
||||||
|
title="Add Entry"
|
||||||
|
let:openModal
|
||||||
|
bind:openModal
|
||||||
|
bind:closeModal
|
||||||
|
>
|
||||||
|
<slot {openModal} />
|
||||||
|
<form
|
||||||
|
id="summit-log-form"
|
||||||
|
slot="content"
|
||||||
|
class="modal-content space-y-4"
|
||||||
|
on:submit={handleSubmit}
|
||||||
|
>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<Datepicker
|
||||||
|
name="date"
|
||||||
|
label="Date"
|
||||||
|
bind:value={$form.date}
|
||||||
|
error={$errors.date}
|
||||||
|
on:change={handleChange}
|
||||||
|
></Datepicker>
|
||||||
|
<div class="basis-full">
|
||||||
|
<TextField
|
||||||
|
name="text"
|
||||||
|
label="Text"
|
||||||
|
bind:value={$form.text}
|
||||||
|
error={$errors.text}
|
||||||
|
on:change={handleChange}
|
||||||
|
></TextField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div slot="footer" class="flex items-center gap-4">
|
||||||
|
<button class="btn-secondary" on:click={closeModal}>Cancel</button>
|
||||||
|
<button class="btn-primary" type="submit" form="summit-log-form"
|
||||||
|
>Save</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { SummitLog } from "$lib/models/summit_log";
|
|
||||||
import { formatISODate } from "$lib/util/format_util";
|
|
||||||
|
|
||||||
export let log: SummitLog;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="p-4 my-2 border rounded-xl">
|
|
||||||
<h5 class="font-medium mr-2">{formatISODate(log.date)}</h5>
|
|
||||||
<span>{log.text}</span>
|
|
||||||
</div>
|
|
||||||
@@ -2,17 +2,26 @@
|
|||||||
import type { Trail } from "$lib/models/trail";
|
import type { Trail } from "$lib/models/trail";
|
||||||
import { getFileURL } from "$lib/util/file_util";
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||||
|
import Dropdown from "../base/dropdown.svelte";
|
||||||
|
|
||||||
export let trail: Trail;
|
export let trail: Trail;
|
||||||
|
|
||||||
|
const dropdownItems = [
|
||||||
|
{ text: "Edit", value: "edit" },
|
||||||
|
{ text: "Delete", value: "delete" },
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class="trail-card rounded-2xl shadow-md w-72 cursor-pointer">
|
||||||
class="trail-card rounded-2xl shadow-md w-72 overflow-hidden cursor-pointer"
|
<div class="w-full h-48 overflow-hidden rounded-t-2xl">
|
||||||
>
|
<img src={getFileURL(trail, trail.thumbnail)} alt="" />
|
||||||
<img class="w-full h-48" src={getFileURL(trail, trail.thumbnail)} alt="" />
|
</div>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div>
|
<div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
<h4 class="font-semibold text-lg">{trail.name}</h4>
|
<h4 class="font-semibold text-lg">{trail.name}</h4>
|
||||||
|
<Dropdown on:change items={dropdownItems}></Dropdown>
|
||||||
|
</div>
|
||||||
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
|
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mt-2 gap-4 text-sm text-gray-500">
|
<div class="flex mt-2 gap-4 text-sm text-gray-500">
|
||||||
@@ -2,11 +2,12 @@
|
|||||||
import { Waypoint, waypointSchema } from "$lib/models/waypoint";
|
import { Waypoint, waypointSchema } from "$lib/models/waypoint";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
import { createForm } from "svelte-forms-lib";
|
import { createForm } from "$lib/vendor/svelte-form-lib/index";
|
||||||
import Modal from "../base/modal.svelte";
|
import Modal from "../base/modal.svelte";
|
||||||
import TextField from "../base/text_field.svelte";
|
import TextField from "../base/text_field.svelte";
|
||||||
import Textarea from "../base/textarea.svelte";
|
import Textarea from "../base/textarea.svelte";
|
||||||
import { waypoint } from "$lib/stores/waypoint_store";
|
import { waypoint } from "$lib/stores/waypoint_store";
|
||||||
|
import { util } from "$lib/vendor/svelte-form-lib/util";
|
||||||
|
|
||||||
export let openModal: (() => void) | undefined = undefined;
|
export let openModal: (() => void) | undefined = undefined;
|
||||||
export let closeModal: (() => void) | undefined = undefined;
|
export let closeModal: (() => void) | undefined = undefined;
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
closeModal!();
|
closeModal!();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
$: form.set($waypoint);
|
$: form.set(util.cloneDeep($waypoint));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<div class="basis-full">
|
<div class="basis-full">
|
||||||
<TextField
|
<TextField
|
||||||
|
name="name"
|
||||||
label="Name"
|
label="Name"
|
||||||
bind:value={$form.name}
|
bind:value={$form.name}
|
||||||
error={$errors.name}
|
error={$errors.name}
|
||||||
@@ -49,6 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
|
name="icon"
|
||||||
label="Icon"
|
label="Icon"
|
||||||
bind:value={$form.icon}
|
bind:value={$form.icon}
|
||||||
icon={$form.icon}
|
icon={$form.icon}
|
||||||
@@ -58,6 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
|
name="description"
|
||||||
label="Description"
|
label="Description"
|
||||||
bind:value={$form.description}
|
bind:value={$form.description}
|
||||||
error={$errors.description}
|
error={$errors.description}
|
||||||
@@ -65,12 +69,14 @@
|
|||||||
></Textarea>
|
></Textarea>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<TextField
|
<TextField
|
||||||
|
name="lat"
|
||||||
label="Latitude"
|
label="Latitude"
|
||||||
bind:value={$form.lat}
|
bind:value={$form.lat}
|
||||||
error={$errors.lat}
|
error={$errors.lat}
|
||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
></TextField>
|
></TextField>
|
||||||
<TextField
|
<TextField
|
||||||
|
name="lon"
|
||||||
label="Longitude"
|
label="Longitude"
|
||||||
bind:value={$form.lon}
|
bind:value={$form.lon}
|
||||||
error={$errors.lat}
|
error={$errors.lat}
|
||||||
|
|||||||
@@ -1,7 +1,26 @@
|
|||||||
interface SummitLog {
|
import { parse } from "date-fns";
|
||||||
id: string;
|
import { date, number, object, string } from "yup";
|
||||||
|
|
||||||
|
class SummitLog {
|
||||||
|
id?: string;
|
||||||
date: string;
|
date: string;
|
||||||
text: string;
|
text?: string;
|
||||||
|
|
||||||
|
constructor(date: string, params?: { id?: string, text?: string }) {
|
||||||
|
this.date = date;
|
||||||
|
this.id = params?.id;
|
||||||
|
this.text = params?.text ?? "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type {SummitLog}
|
const summitLogSchema = object<SummitLog>({
|
||||||
|
id: string().optional(),
|
||||||
|
date: date().transform((value, originalValue, context) => {
|
||||||
|
if (context.isType(value)) return value;
|
||||||
|
return parse(originalValue, 'dd.MM.yyyy', new Date());
|
||||||
|
}).required('Required').typeError('Invalid Date'),
|
||||||
|
text: string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export { SummitLog, summitLogSchema }
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { array, number, object, string } from "yup";
|
||||||
import type { Category } from "./category";
|
import type { Category } from "./category";
|
||||||
import type { SummitLog } from "./summit_log";
|
import type { SummitLog } from "./summit_log";
|
||||||
import type { Waypoint } from "./waypoint";
|
import type { Waypoint } from "./waypoint";
|
||||||
@@ -20,7 +21,10 @@ class Trail {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
_photoFiles: File[]
|
||||||
|
|
||||||
constructor(name: string,
|
constructor(name: string,
|
||||||
|
params?: {
|
||||||
id?: string,
|
id?: string,
|
||||||
location?: string,
|
location?: string,
|
||||||
distance?: number,
|
distance?: number,
|
||||||
@@ -34,24 +38,40 @@ class Trail {
|
|||||||
summit_logs?: SummitLog[],
|
summit_logs?: SummitLog[],
|
||||||
tags?: string[],
|
tags?: string[],
|
||||||
description?: string
|
description?: string
|
||||||
) {
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
this.location = location;
|
|
||||||
this.distance = distance;
|
|
||||||
this.elevation_gain = elevation_gain;
|
|
||||||
this.duration = duration;
|
|
||||||
this.thumbnail = thumbnail;
|
|
||||||
this.photos = photos ?? [];
|
|
||||||
this.gpx = gpx;
|
|
||||||
this.expand = {
|
|
||||||
category: category,
|
|
||||||
waypoints: waypoints ?? [],
|
|
||||||
summit_logs: summit_logs ?? []
|
|
||||||
}
|
}
|
||||||
this.tags = tags ?? []
|
|
||||||
this.description = description ?? "";
|
) {
|
||||||
|
this.id = params?.id;
|
||||||
|
this.name = name;
|
||||||
|
this.location = params?.location;
|
||||||
|
this.distance = params?.distance;
|
||||||
|
this.elevation_gain = params?.elevation_gain;
|
||||||
|
this.duration = params?.duration;
|
||||||
|
this.thumbnail = params?.thumbnail;
|
||||||
|
this.photos = params?.photos ?? [];
|
||||||
|
this.gpx = params?.gpx;
|
||||||
|
this.expand = {
|
||||||
|
category: params?.category,
|
||||||
|
waypoints: params?.waypoints ?? [],
|
||||||
|
summit_logs: params?.summit_logs ?? []
|
||||||
|
}
|
||||||
|
this.tags = params?.tags ?? []
|
||||||
|
this.description = params?.description ?? "";
|
||||||
|
this._photoFiles = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Trail };
|
const trailSchema = object<SummitLog>({
|
||||||
|
id: string().optional(),
|
||||||
|
name: string().required("Required"),
|
||||||
|
location: string().optional(),
|
||||||
|
distance: number().optional(),
|
||||||
|
elevation_gain: number().optional(),
|
||||||
|
duration: number().optional(),
|
||||||
|
thumbnail: string().optional(),
|
||||||
|
photos: array(string()).optional(),
|
||||||
|
gpx: string().optional(),
|
||||||
|
description: string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Trail, trailSchema };
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ const waypointSchema = object<Waypoint>({
|
|||||||
id: string().optional(),
|
id: string().optional(),
|
||||||
name: string().optional(),
|
name: string().optional(),
|
||||||
description: string().optional(),
|
description: string().optional(),
|
||||||
lat: number().min(0).required('Required').typeError('Invalid latitude'),
|
lat: number().required('Required').typeError('Invalid latitude'),
|
||||||
lon: number().min(0).required('Required').typeError('Invalid longitude'),
|
lon: number().required('Required').typeError('Invalid longitude'),
|
||||||
icon: string().optional()
|
icon: string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
21
web/src/lib/stores/summit_log_store.ts
Normal file
21
web/src/lib/stores/summit_log_store.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { pb } from "$lib/constants";
|
||||||
|
import { SummitLog } from "$lib/models/summit_log";
|
||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
|
export const summitLog: Writable<SummitLog> = writable(new SummitLog(new Date().toISOString()));
|
||||||
|
|
||||||
|
export async function summit_logs_create(bodyParams?: { [key: string]: any; } | FormData) {
|
||||||
|
const model = await pb
|
||||||
|
.collection("summit_logs")
|
||||||
|
.create<SummitLog>(bodyParams);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function summit_logs_delete(id: string) {
|
||||||
|
const success = await pb
|
||||||
|
.collection("summit_logs_delete")
|
||||||
|
.delete(id);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
@@ -23,6 +23,31 @@ export async function trails_show(id: string, loadGPX?: boolean) {
|
|||||||
trail.set(response);
|
trail.set(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function trails_create(bodyParams?: { [key: string]: any; } | FormData) {
|
||||||
|
const model = await pb
|
||||||
|
.collection("trails")
|
||||||
|
.create<Trail>(bodyParams);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function trails_update(id: string, bodyParams?: { [key: string]: any; } | FormData) {
|
||||||
|
const model = await pb
|
||||||
|
.collection("trails")
|
||||||
|
.update<Trail>(id, bodyParams);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function trails_delete(id: string) {
|
||||||
|
const success = await pb
|
||||||
|
.collection("trails")
|
||||||
|
.delete(id);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchGPX(trail: Trail) {
|
async function fetchGPX(trail: Trail) {
|
||||||
if (!trail.gpx) {
|
if (!trail.gpx) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -1,4 +1,22 @@
|
|||||||
|
import { pb } from "$lib/constants";
|
||||||
import { Waypoint } from "$lib/models/waypoint";
|
import { Waypoint } from "$lib/models/waypoint";
|
||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
export const waypoint: Writable<Waypoint> = writable(new Waypoint(0, 0));
|
export const waypoint: Writable<Waypoint> = writable(new Waypoint(0, 0));
|
||||||
|
|
||||||
|
export async function waypoints_create(bodyParams?: { [key: string]: any; } | FormData) {
|
||||||
|
|
||||||
|
const model = await pb
|
||||||
|
.collection("waypoints")
|
||||||
|
.create<Waypoint>(bodyParams);
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waypoints_delete(id: string) {
|
||||||
|
const success = await pb
|
||||||
|
.collection("waypoints")
|
||||||
|
.delete(id);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { Waypoint } from "$lib/models/waypoint";
|
|
||||||
|
|
||||||
export function formatTimeHHMM(minutes?: number) {
|
export function formatTimeHHMM(minutes?: number) {
|
||||||
if (!minutes) {
|
if (!minutes) {
|
||||||
@@ -22,16 +21,3 @@ export function formatMeters(meters?: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatISODate(isoTimestamp?: string) {
|
|
||||||
if(!isoTimestamp) {
|
|
||||||
return "-"
|
|
||||||
}
|
|
||||||
const date = new Date(isoTimestamp);
|
|
||||||
|
|
||||||
const day = date.getDate().toString().padStart(2, '0');
|
|
||||||
const month = (date.getMonth() + 1).toString().padStart(2, '0'); // Month is zero-based
|
|
||||||
const year = date.getFullYear();
|
|
||||||
|
|
||||||
return `${day}.${month}.${year}`;
|
|
||||||
}
|
|
||||||
255
web/src/lib/vendor/svelte-form-lib/create-form.js
vendored
Normal file
255
web/src/lib/vendor/svelte-form-lib/create-form.js
vendored
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import {derived, writable, get} from 'svelte/store';
|
||||||
|
import {util} from './util';
|
||||||
|
|
||||||
|
const NO_ERROR = '';
|
||||||
|
const IS_TOUCHED = true;
|
||||||
|
|
||||||
|
function isCheckbox(element) {
|
||||||
|
return element.getAttribute && element.getAttribute('type') === 'checkbox';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFileInput(element) {
|
||||||
|
return element.getAttribute && element.getAttribute('type') === 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveValue(element) {
|
||||||
|
if (isFileInput(element)) {
|
||||||
|
return element.files;
|
||||||
|
} else if (isCheckbox(element)) {
|
||||||
|
return element.checked;
|
||||||
|
} else {
|
||||||
|
return element.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createForm = (config) => {
|
||||||
|
let initialValues = config.initialValues || {};
|
||||||
|
|
||||||
|
const validationSchema = config.validationSchema;
|
||||||
|
const validateFunction = config.validate;
|
||||||
|
const onSubmit = config.onSubmit;
|
||||||
|
|
||||||
|
const getInitial = {
|
||||||
|
values: () => util.cloneDeep(initialValues),
|
||||||
|
errors: () =>
|
||||||
|
validationSchema
|
||||||
|
? util.getErrorsFromSchema(initialValues, validationSchema.fields)
|
||||||
|
: util.assignDeep(initialValues, NO_ERROR),
|
||||||
|
touched: () => util.assignDeep(initialValues, !IS_TOUCHED),
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = writable(getInitial.values());
|
||||||
|
const errors = writable(getInitial.errors());
|
||||||
|
const touched = writable(getInitial.touched());
|
||||||
|
|
||||||
|
const isSubmitting = writable(false);
|
||||||
|
const isValidating = writable(false);
|
||||||
|
|
||||||
|
const isValid = derived(errors, ($errors) => {
|
||||||
|
const noErrors = util
|
||||||
|
.getValues($errors)
|
||||||
|
.every((field) => field === NO_ERROR);
|
||||||
|
return noErrors;
|
||||||
|
});
|
||||||
|
|
||||||
|
const modified = derived(form, ($form) => {
|
||||||
|
const object = util.assignDeep($form, false);
|
||||||
|
|
||||||
|
for (let key in $form) {
|
||||||
|
object[key] = !util.deepEqual($form[key], initialValues[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return object;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isModified = derived(modified, ($modified) => {
|
||||||
|
return util.getValues($modified).includes(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
function validateField(field) {
|
||||||
|
return util
|
||||||
|
.subscribeOnce(form)
|
||||||
|
.then((values) => validateFieldValue(field, values[field]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateFieldValue(field, value) {
|
||||||
|
updateTouched(field, true);
|
||||||
|
|
||||||
|
if (validationSchema) {
|
||||||
|
isValidating.set(true);
|
||||||
|
|
||||||
|
return validationSchema
|
||||||
|
.validateAt(field, get(form))
|
||||||
|
.then(() => util.update(errors, field, ''))
|
||||||
|
.catch((error) => util.update(errors, field, error.message))
|
||||||
|
.finally(() => {
|
||||||
|
isValidating.set(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validateFunction) {
|
||||||
|
isValidating.set(true);
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => validateFunction({[field]: value}))
|
||||||
|
.then((errs) =>
|
||||||
|
util.update(errors, field, !util.isNullish(errs) ? errs[field] : ''),
|
||||||
|
)
|
||||||
|
.finally(() => {
|
||||||
|
isValidating.set(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateValidateField(field, value) {
|
||||||
|
updateField(field, value);
|
||||||
|
return validateFieldValue(field, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChange(event) {
|
||||||
|
const element = event.target;
|
||||||
|
const field = element.name || element.id;
|
||||||
|
const value = resolveValue(element);
|
||||||
|
|
||||||
|
return updateValidateField(field, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(event) {
|
||||||
|
if (event && event.preventDefault) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitting.set(true);
|
||||||
|
|
||||||
|
return util.subscribeOnce(form).then((values) => {
|
||||||
|
if (typeof validateFunction === 'function') {
|
||||||
|
isValidating.set(true);
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => validateFunction(values))
|
||||||
|
.then((error) => {
|
||||||
|
if (util.isNullish(error) || util.getValues(error).length === 0) {
|
||||||
|
return clearErrorsAndSubmit(values);
|
||||||
|
} else {
|
||||||
|
errors.set(error);
|
||||||
|
isSubmitting.set(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => isValidating.set(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationSchema) {
|
||||||
|
isValidating.set(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
validationSchema
|
||||||
|
.validate(values, {abortEarly: false})
|
||||||
|
.then(() => clearErrorsAndSubmit(values))
|
||||||
|
// eslint-disable-next-line unicorn/catch-error-name
|
||||||
|
.catch((yupErrors) => {
|
||||||
|
if (yupErrors && yupErrors.inner) {
|
||||||
|
const updatedErrors = getInitial.errors();
|
||||||
|
|
||||||
|
yupErrors.inner.map((error) =>
|
||||||
|
util.set(updatedErrors, error.path, error.message),
|
||||||
|
);
|
||||||
|
|
||||||
|
errors.set(updatedErrors);
|
||||||
|
}
|
||||||
|
isSubmitting.set(false);
|
||||||
|
})
|
||||||
|
.finally(() => isValidating.set(false))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clearErrorsAndSubmit(values);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
form.set(getInitial.values());
|
||||||
|
errors.set(getInitial.errors());
|
||||||
|
touched.set(getInitial.touched());
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearErrorsAndSubmit(values) {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => errors.set(getInitial.errors()))
|
||||||
|
.then(() => onSubmit(values, form, errors))
|
||||||
|
.finally(() => isSubmitting.set(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to imperatively update the value of a form field
|
||||||
|
*/
|
||||||
|
function updateField(field, value) {
|
||||||
|
util.update(form, field, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to imperatively update the touched value of a form field
|
||||||
|
*/
|
||||||
|
function updateTouched(field, value) {
|
||||||
|
util.update(touched, field, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the initial values and reset form. Used to dynamically display new form values
|
||||||
|
*/
|
||||||
|
function updateInitialValues(newValues) {
|
||||||
|
initialValues = newValues;
|
||||||
|
|
||||||
|
handleReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
modified,
|
||||||
|
isValid,
|
||||||
|
isSubmitting,
|
||||||
|
isValidating,
|
||||||
|
isModified,
|
||||||
|
handleChange,
|
||||||
|
handleSubmit,
|
||||||
|
handleReset,
|
||||||
|
updateField,
|
||||||
|
updateValidateField,
|
||||||
|
updateTouched,
|
||||||
|
validateField,
|
||||||
|
updateInitialValues,
|
||||||
|
state: derived(
|
||||||
|
[
|
||||||
|
form,
|
||||||
|
errors,
|
||||||
|
touched,
|
||||||
|
modified,
|
||||||
|
isValid,
|
||||||
|
isValidating,
|
||||||
|
isSubmitting,
|
||||||
|
isModified,
|
||||||
|
],
|
||||||
|
([
|
||||||
|
$form,
|
||||||
|
$errors,
|
||||||
|
$touched,
|
||||||
|
$modified,
|
||||||
|
$isValid,
|
||||||
|
$isValidating,
|
||||||
|
$isSubmitting,
|
||||||
|
$isModified,
|
||||||
|
]) => ({
|
||||||
|
form: $form,
|
||||||
|
errors: $errors,
|
||||||
|
touched: $touched,
|
||||||
|
modified: $modified,
|
||||||
|
isValid: $isValid,
|
||||||
|
isSubmitting: $isSubmitting,
|
||||||
|
isValidating: $isValidating,
|
||||||
|
isModified: $isModified,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
102
web/src/lib/vendor/svelte-form-lib/index.d.ts
vendored
Normal file
102
web/src/lib/vendor/svelte-form-lib/index.d.ts
vendored
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/// <reference lib="svelte2tsx" />
|
||||||
|
import type {SvelteComponentTyped} from 'svelte';
|
||||||
|
import type {Readable, Writable} from 'svelte/store';
|
||||||
|
import type {ObjectSchema} from 'yup';
|
||||||
|
|
||||||
|
export type FormProps<Inf = Record<string, unknown>> = {
|
||||||
|
context?: FormState;
|
||||||
|
initialValues?: Inf;
|
||||||
|
onSubmit?: ((values: Inf) => any) | ((values: Inf) => Promise<any>);
|
||||||
|
validate?: (values: Inf) => any | undefined;
|
||||||
|
validationSchema?: ObjectSchema<any>;
|
||||||
|
} & svelte.JSX.HTMLAttributes<HTMLFormElement>;
|
||||||
|
|
||||||
|
type FieldProperties = {
|
||||||
|
name: string;
|
||||||
|
type?: string;
|
||||||
|
value?: string;
|
||||||
|
} & svelte.JSX.HTMLProps<HTMLInputElement>;
|
||||||
|
|
||||||
|
type SelectProperties = {
|
||||||
|
name: string;
|
||||||
|
} & svelte.JSX.HTMLProps<HTMLSelectElement>;
|
||||||
|
|
||||||
|
type ErrorProperties = {
|
||||||
|
name: string;
|
||||||
|
} & svelte.JSX.HTMLProps<HTMLDivElement>;
|
||||||
|
|
||||||
|
type TextareaProperties = {
|
||||||
|
name: string;
|
||||||
|
} & svelte.JSX.HTMLProps<HTMLTextAreaElement>;
|
||||||
|
|
||||||
|
type FormState<Inf = Record<string, any>> = {
|
||||||
|
form: Writable<Inf>;
|
||||||
|
errors: Writable<Record<keyof Inf, string>>;
|
||||||
|
touched: Writable<Record<keyof Inf, boolean>>;
|
||||||
|
modified: Readable<Record<keyof Inf, boolean>>;
|
||||||
|
isValid: Readable<boolean>;
|
||||||
|
isSubmitting: Writable<boolean>;
|
||||||
|
isValidating: Writable<boolean>;
|
||||||
|
isModified: Readable<boolean>;
|
||||||
|
updateField: (field: keyof Inf, value: any) => void;
|
||||||
|
updateValidateField: (field: keyof Inf, value: any) => void;
|
||||||
|
updateTouched: (field: keyof Inf, value: any) => void;
|
||||||
|
validateField: (field: keyof Inf) => Promise<any>;
|
||||||
|
updateInitialValues: (newValues: Inf) => void;
|
||||||
|
handleReset: () => void;
|
||||||
|
state: Readable<{
|
||||||
|
form: Inf;
|
||||||
|
errors: Record<keyof Inf, string>;
|
||||||
|
touched: Record<keyof Inf, boolean>;
|
||||||
|
modified: Record<keyof Inf, boolean>;
|
||||||
|
isValid: boolean;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
isValidating: boolean;
|
||||||
|
isModified: boolean;
|
||||||
|
}>;
|
||||||
|
handleChange: (event: Event) => any;
|
||||||
|
handleSubmit: (event: Event) => any;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare function createForm<Inf = Record<string, any>>(formProperties: {
|
||||||
|
initialValues: Inf;
|
||||||
|
onSubmit: (values: Inf) => any | Promise<any>;
|
||||||
|
validate?: (values: Inf) => any | undefined;
|
||||||
|
validationSchema?: ObjectSchema<any>;
|
||||||
|
}): FormState<Inf>;
|
||||||
|
|
||||||
|
declare class Form extends SvelteComponentTyped<
|
||||||
|
FormProps,
|
||||||
|
Record<string, unknown>,
|
||||||
|
{
|
||||||
|
default: FormState;
|
||||||
|
}
|
||||||
|
> {}
|
||||||
|
|
||||||
|
declare class Field extends SvelteComponentTyped<
|
||||||
|
FieldProperties,
|
||||||
|
Record<string, unknown>,
|
||||||
|
Record<string, unknown>
|
||||||
|
> {}
|
||||||
|
|
||||||
|
declare class Textarea extends SvelteComponentTyped<
|
||||||
|
TextareaProperties,
|
||||||
|
Record<string, unknown>,
|
||||||
|
Record<string, unknown>
|
||||||
|
> {}
|
||||||
|
|
||||||
|
declare class Select extends SvelteComponentTyped<
|
||||||
|
SelectProperties,
|
||||||
|
Record<string, unknown>,
|
||||||
|
{default: any}
|
||||||
|
> {}
|
||||||
|
|
||||||
|
declare class ErrorMessage extends SvelteComponentTyped<
|
||||||
|
ErrorProperties,
|
||||||
|
Record<string, unknown>,
|
||||||
|
{default: any}
|
||||||
|
> {}
|
||||||
|
|
||||||
|
declare const key: {};
|
||||||
|
|
||||||
|
export {createForm, key, Form, Field, Select, ErrorMessage, Textarea};
|
||||||
1
web/src/lib/vendor/svelte-form-lib/index.js
vendored
Normal file
1
web/src/lib/vendor/svelte-form-lib/index.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {createForm} from './create-form';
|
||||||
135
web/src/lib/vendor/svelte-form-lib/util.js
vendored
Normal file
135
web/src/lib/vendor/svelte-form-lib/util.js
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { dequal as isEqual } from 'dequal/lite';
|
||||||
|
|
||||||
|
function subscribeOnce(observable) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
observable.subscribe(resolve)(); // immediately invoke to unsubscribe
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(object, path, value) {
|
||||||
|
object.update((o) => {
|
||||||
|
set(o, path, value);
|
||||||
|
return o;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneDeep(object) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(object));
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNullish(value) {
|
||||||
|
return value === undefined || value === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEmpty(object) {
|
||||||
|
return isNullish(object) || Object.keys(object).length <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValues(object) {
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
for (const [, value] of Object.entries(object)) {
|
||||||
|
const values = typeof value === 'object' ? getValues(value) : [value];
|
||||||
|
results = [...results, ...values];
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: refactor this so as not to rely directly on yup's API
|
||||||
|
// This should use dependency injection, with a default callback which may assume
|
||||||
|
// yup as the validation schema
|
||||||
|
function getErrorsFromSchema(initialValues, schema, errors = {}) {
|
||||||
|
for (const key in schema) {
|
||||||
|
switch (true) {
|
||||||
|
case schema[key].type === 'object' && !isEmpty(schema[key].fields): {
|
||||||
|
errors[key] = getErrorsFromSchema(
|
||||||
|
initialValues[key],
|
||||||
|
schema[key].fields,
|
||||||
|
{ ...errors[key] },
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case schema[key].type === 'array': {
|
||||||
|
const values =
|
||||||
|
initialValues && initialValues[key] ? initialValues[key] : [];
|
||||||
|
errors[key] = values.map((value) => {
|
||||||
|
const innerError = getErrorsFromSchema(
|
||||||
|
value,
|
||||||
|
schema[key].innerType.fields,
|
||||||
|
{ ...errors[key] },
|
||||||
|
);
|
||||||
|
|
||||||
|
return Object.keys(innerError).length > 0 ? innerError : '';
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
errors[key] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deepEqual = isEqual;
|
||||||
|
|
||||||
|
function assignDeep(object, value) {
|
||||||
|
if (Array.isArray(object)) {
|
||||||
|
return object.map((o) => assignDeep(o, value));
|
||||||
|
}
|
||||||
|
const copy = {};
|
||||||
|
for (const key in object) {
|
||||||
|
copy[key] =
|
||||||
|
typeof object[key] === 'object' && !isNullish(object[key]) ? assignDeep(object[key], value) : value;
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(object, path, value) {
|
||||||
|
if (new Object(object) !== object) return object;
|
||||||
|
|
||||||
|
if (!Array.isArray(path)) {
|
||||||
|
path = path.toString().match(/[^.[\]]+/g) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = path
|
||||||
|
.slice(0, -1)
|
||||||
|
// TODO: replace this reduce with something more readable
|
||||||
|
// eslint-disable-next-line unicorn/no-array-reduce
|
||||||
|
.reduce(
|
||||||
|
(accumulator, key, index) =>
|
||||||
|
new Object(accumulator[key]) === accumulator[key]
|
||||||
|
? accumulator[key]
|
||||||
|
: (accumulator[key] =
|
||||||
|
Math.trunc(Math.abs(path[index + 1])) === +path[index + 1]
|
||||||
|
? []
|
||||||
|
: {}),
|
||||||
|
object,
|
||||||
|
);
|
||||||
|
|
||||||
|
result[path[path.length - 1]] = value;
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const util = {
|
||||||
|
assignDeep,
|
||||||
|
cloneDeep,
|
||||||
|
deepEqual,
|
||||||
|
getErrorsFromSchema,
|
||||||
|
getValues,
|
||||||
|
isEmpty,
|
||||||
|
isNullish,
|
||||||
|
set,
|
||||||
|
subscribeOnce,
|
||||||
|
update,
|
||||||
|
};
|
||||||
@@ -1,8 +1,37 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CategoryCard from "$lib/components/category_card.svelte";
|
import CategoryCard from "$lib/components/category_card.svelte";
|
||||||
import TrailCard from "$lib/components/trail_card.svelte";
|
import TrailCard from "$lib/components/trail/trail_card.svelte";
|
||||||
|
import { pb } from "$lib/constants";
|
||||||
|
import type { Trail } from "$lib/models/trail";
|
||||||
import { categories } from "$lib/stores/category_store";
|
import { categories } from "$lib/stores/category_store";
|
||||||
import { trails } from "$lib/stores/trail_store";
|
import { summit_logs_delete } from "$lib/stores/summit_log_store";
|
||||||
|
import {
|
||||||
|
trails,
|
||||||
|
trails_delete,
|
||||||
|
trails_index,
|
||||||
|
} from "$lib/stores/trail_store";
|
||||||
|
import { waypoints_delete } from "$lib/stores/waypoint_store";
|
||||||
|
|
||||||
|
async function handleDropdownClick(
|
||||||
|
currentTrail: Trail,
|
||||||
|
item: { text: string; value: any },
|
||||||
|
) {
|
||||||
|
if (item.value == "delete") {
|
||||||
|
if (currentTrail.expand.waypoints) {
|
||||||
|
for (const waypoint of currentTrail.expand.waypoints) {
|
||||||
|
waypoints_delete(waypoint.id!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentTrail.expand.summit_logs) {
|
||||||
|
for (const summit_log of currentTrail.expand.summit_logs) {
|
||||||
|
summit_logs_delete(summit_log.id!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trails_delete(currentTrail.id!);
|
||||||
|
await trails_index();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="hero flex justify-center items-center" style="height: 50vh">
|
<section class="hero flex justify-center items-center" style="height: 50vh">
|
||||||
@@ -26,7 +55,12 @@
|
|||||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 justify-items-center gap-8 py-8"
|
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 justify-items-center gap-8 py-8"
|
||||||
>
|
>
|
||||||
{#each $trails as trail}
|
{#each $trails as trail}
|
||||||
<a href="/trail/{trail.id}"> <TrailCard {trail}></TrailCard></a>
|
<a href="/trail/{trail.id}">
|
||||||
|
<TrailCard
|
||||||
|
{trail}
|
||||||
|
on:change={(e) => handleDropdownClick(trail, e.detail)}
|
||||||
|
></TrailCard></a
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { categories_index } from "$lib/stores/category_store";
|
import { categories_index } from "$lib/stores/category_store";
|
||||||
import { trails_index } from "$lib/stores/trail_store";
|
import { trails_index } from "$lib/stores/trail_store";
|
||||||
import type { Load } from "@sveltejs/kit";
|
import type { ServerLoad } from "@sveltejs/kit";
|
||||||
|
|
||||||
export const load: Load = async ({ params }) => {
|
export const load: ServerLoad = async ({ params, locals }) => {
|
||||||
await trails_index()
|
await trails_index()
|
||||||
await categories_index()
|
await categories_index()
|
||||||
};
|
};
|
||||||
@@ -1,29 +1,80 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Modal from "$lib/components/base/modal.svelte";
|
import Button from "$lib/components/base/button.svelte";
|
||||||
import Select from "$lib/components/base/select.svelte";
|
import Select from "$lib/components/base/select.svelte";
|
||||||
import TextField from "$lib/components/base/text_field.svelte";
|
import TextField from "$lib/components/base/text_field.svelte";
|
||||||
import Textarea from "$lib/components/base/textarea.svelte";
|
import Textarea from "$lib/components/base/textarea.svelte";
|
||||||
|
import PhotoCard from "$lib/components/photo_card.svelte";
|
||||||
|
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
|
||||||
|
import SummitLogModal from "$lib/components/summit_log/summit_log_modal.svelte";
|
||||||
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||||
import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte";
|
import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte";
|
||||||
import { Trail } from "$lib/models/trail";
|
import { pb } from "$lib/constants";
|
||||||
|
import { SummitLog } from "$lib/models/summit_log";
|
||||||
|
import { Trail, trailSchema } from "$lib/models/trail";
|
||||||
import { Waypoint } from "$lib/models/waypoint";
|
import { Waypoint } from "$lib/models/waypoint";
|
||||||
import { categories } from "$lib/stores/category_store";
|
import { categories } from "$lib/stores/category_store";
|
||||||
import { trail } from "$lib/stores/trail_store";
|
import { summitLog, summit_logs_create } from "$lib/stores/summit_log_store";
|
||||||
import { waypoint } from "$lib/stores/waypoint_store";
|
import { trail, trails_create, trails_update } from "$lib/stores/trail_store";
|
||||||
|
import { waypoint, waypoints_create } from "$lib/stores/waypoint_store";
|
||||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||||
|
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||||
|
import cryptoRandomString from "crypto-random-string";
|
||||||
|
import { format } from "date-fns";
|
||||||
import type { GPX, Icon, LeafletEvent, Map } from "leaflet";
|
import type { GPX, Icon, LeafletEvent, Map } from "leaflet";
|
||||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import type { W } from "vitest/dist/reporters-rzC174PQ.js";
|
|
||||||
|
|
||||||
let L: any;
|
let L: any;
|
||||||
let map: Map;
|
let map: Map;
|
||||||
|
|
||||||
let gpxLayer: GPX;
|
let gpxLayer: GPX;
|
||||||
|
|
||||||
let openWayPointModal: () => void;
|
let openWaypointModal: () => void;
|
||||||
|
let openSummitLogModal: () => void;
|
||||||
|
|
||||||
|
let loading = false;
|
||||||
|
const { form, errors, handleChange, handleSubmit } = createForm<Trail>({
|
||||||
|
initialValues: new Trail("adsf", { category: $categories[0] }),
|
||||||
|
validationSchema: trailSchema,
|
||||||
|
onSubmit: async (trail) => {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const form = document.getElementById(
|
||||||
|
"trail-form",
|
||||||
|
) as HTMLFormElement;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
formData.set("category", trail.expand.category!.id);
|
||||||
|
|
||||||
|
for (const file of trail._photoFiles) {
|
||||||
|
formData.append("photos", file);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const waypoint of trail.expand.waypoints) {
|
||||||
|
const model = await waypoints_create({...waypoint, marker: undefined});
|
||||||
|
formData.append("waypoints", model.id!);
|
||||||
|
}
|
||||||
|
for (const summitLog of trail.expand.summit_logs) {
|
||||||
|
const model = await summit_logs_create(summitLog);
|
||||||
|
formData.append("summit_logs", model.id!);
|
||||||
|
}
|
||||||
|
const model = await trails_create(formData);
|
||||||
|
|
||||||
|
const thumbnailIndex = trail.photos.findIndex(
|
||||||
|
(p) => p == trail.thumbnail,
|
||||||
|
);
|
||||||
|
let thumbnail: string | undefined = "/imgs/thumbnail.jpg";
|
||||||
|
if (thumbnailIndex >= 0) {
|
||||||
|
thumbnail = model.photos.at(thumbnailIndex);
|
||||||
|
}
|
||||||
|
await trails_update(model.id!, { thumbnail: thumbnail });
|
||||||
|
} catch (e) {
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
L = (await import("leaflet")).default;
|
L = (await import("leaflet")).default;
|
||||||
@@ -50,7 +101,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$trail.gpx = selectedFile?.name;
|
$form.gpx = selectedFile?.name;
|
||||||
|
$form.expand.waypoints = [];
|
||||||
|
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
|
|
||||||
@@ -96,14 +148,14 @@
|
|||||||
e.point._latlng.lng,
|
e.point._latlng.lng,
|
||||||
{ name: e.point.options.title, marker: e.point },
|
{ name: e.point.options.title, marker: e.point },
|
||||||
);
|
);
|
||||||
$trail.expand.waypoints.push(waypoint);
|
$form.expand.waypoints.push(waypoint);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("loaded", function (e: LeafletEvent) {
|
.on("loaded", function (e: LeafletEvent) {
|
||||||
map.fitBounds(e.target.getBounds());
|
map.fitBounds(e.target.getBounds());
|
||||||
$trail.distance = e.target.get_distance();
|
$form.distance = e.target.get_distance();
|
||||||
$trail.elevation_gain = e.target.get_elevation_gain();
|
$form.elevation_gain = e.target.get_elevation_gain();
|
||||||
$trail.duration = e.target.get_total_time() / 1000 / 60;
|
$form.duration = e.target.get_total_time() / 1000 / 60;
|
||||||
})
|
})
|
||||||
.addTo(map);
|
.addTo(map);
|
||||||
};
|
};
|
||||||
@@ -119,10 +171,10 @@
|
|||||||
) {
|
) {
|
||||||
if (e.detail.value === "edit") {
|
if (e.detail.value === "edit") {
|
||||||
waypoint.set(currentWaypoint);
|
waypoint.set(currentWaypoint);
|
||||||
openWayPointModal();
|
openWaypointModal();
|
||||||
} else if (e.detail.value === "delete") {
|
} else if (e.detail.value === "delete") {
|
||||||
currentWaypoint.marker?.remove();
|
currentWaypoint.marker?.remove();
|
||||||
$trail.expand.waypoints = $trail.expand.waypoints.filter(
|
$form.expand.waypoints = $form.expand.waypoints.filter(
|
||||||
(w) =>
|
(w) =>
|
||||||
w.lat !== currentWaypoint.lat &&
|
w.lat !== currentWaypoint.lat &&
|
||||||
w.lon !== currentWaypoint.lon,
|
w.lon !== currentWaypoint.lon,
|
||||||
@@ -130,39 +182,136 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findMapCenterAndOpenModal() {
|
function beforeWaypointModalOpen() {
|
||||||
const mapCenter = map.getCenter();
|
const mapCenter = map.getCenter();
|
||||||
waypoint.set(new Waypoint(mapCenter.lat, mapCenter.lng));
|
waypoint.set(new Waypoint(mapCenter.lat, mapCenter.lng));
|
||||||
openWayPointModal();
|
openWaypointModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveWaypoint(e: CustomEvent<Waypoint>) {
|
function saveWaypoint(e: CustomEvent<Waypoint>) {
|
||||||
const savedWaypoint = e.detail;
|
const savedWaypoint = e.detail;
|
||||||
|
let editedWaypointIndex = $form.expand.waypoints.findIndex(
|
||||||
|
(s) => s.id == savedWaypoint.id,
|
||||||
|
);
|
||||||
|
|
||||||
if ($trail.expand.waypoints.includes(savedWaypoint)) {
|
if (editedWaypointIndex >= 0) {
|
||||||
$trail.expand.waypoints = $trail.expand.waypoints;
|
$form.expand.waypoints[editedWaypointIndex].marker?.remove();
|
||||||
savedWaypoint.marker?.remove();
|
$form.expand.waypoints[editedWaypointIndex] = savedWaypoint;
|
||||||
} else {
|
} else {
|
||||||
$trail.expand.waypoints = [
|
savedWaypoint.id = cryptoRandomString({ length: 15 });
|
||||||
...$trail.expand.waypoints,
|
|
||||||
savedWaypoint,
|
$form.expand.waypoints = [...$form.expand.waypoints, savedWaypoint];
|
||||||
];
|
|
||||||
}
|
}
|
||||||
const marker = createMarkerFromWaypoint(L, savedWaypoint);
|
const marker = createMarkerFromWaypoint(L, savedWaypoint);
|
||||||
marker.addTo(map);
|
marker.addTo(map);
|
||||||
savedWaypoint.marker = marker;
|
savedWaypoint.marker = marker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openPhotoBrowser() {
|
||||||
|
document.getElementById("photoInput")!.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePhotoSelection() {
|
||||||
|
const files = (
|
||||||
|
document.getElementById("photoInput") as HTMLInputElement
|
||||||
|
).files;
|
||||||
|
|
||||||
|
if (!files) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
$form._photoFiles.push(file);
|
||||||
|
|
||||||
|
(function (file) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function (e) {
|
||||||
|
if (e.target?.result) {
|
||||||
|
if ($form.photos.length == 0) {
|
||||||
|
$form.thumbnail = e.target.result as string;
|
||||||
|
}
|
||||||
|
$form.photos = [
|
||||||
|
...$form.photos,
|
||||||
|
e.target.result as string,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
})(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePhotoThumbnail(index: number) {
|
||||||
|
$form.thumbnail = $form.photos[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePhotoDelete(index: number) {
|
||||||
|
const photoToDelete = $form.photos[index];
|
||||||
|
let reassignThumbnail: boolean = false;
|
||||||
|
if ($form.thumbnail == photoToDelete) {
|
||||||
|
reassignThumbnail = true;
|
||||||
|
}
|
||||||
|
$form.photos.splice(index, 1);
|
||||||
|
$form._photoFiles.splice(index, 1);
|
||||||
|
$form.photos = $form.photos;
|
||||||
|
if (reassignThumbnail) {
|
||||||
|
$form.thumbnail = $form.photos[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeSummitLogModalOpen() {
|
||||||
|
summitLog.set(new SummitLog(new Date().toISOString()));
|
||||||
|
openSummitLogModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSummitLog(e: CustomEvent<SummitLog>) {
|
||||||
|
const savedSummitLog = e.detail;
|
||||||
|
savedSummitLog.date = format(savedSummitLog.date, "yyyy-MM-dd");
|
||||||
|
let editedSummitLogIndex = $form.expand.summit_logs.findIndex(
|
||||||
|
(s) => s.id == savedSummitLog.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (editedSummitLogIndex >= 0) {
|
||||||
|
$form.expand.summit_logs[editedSummitLogIndex] = savedSummitLog;
|
||||||
|
} else {
|
||||||
|
savedSummitLog.id = cryptoRandomString({ length: 15 });
|
||||||
|
$form.expand.summit_logs = [
|
||||||
|
...$form.expand.summit_logs,
|
||||||
|
savedSummitLog,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSummitLogMenuClick(
|
||||||
|
currentSummitLog: SummitLog,
|
||||||
|
index: number,
|
||||||
|
e: CustomEvent<{ text: string; value: string }>,
|
||||||
|
) {
|
||||||
|
if (e.detail.value === "edit") {
|
||||||
|
summitLog.set(currentSummitLog);
|
||||||
|
openSummitLogModal();
|
||||||
|
} else if (e.detail.value === "delete") {
|
||||||
|
$form.expand.summit_logs.splice(index, 1);
|
||||||
|
$form.expand.summit_logs = $form.expand.summit_logs;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="grid grid-cols-[400px_1fr]">
|
<main class="grid grid-cols-[400px_1fr]">
|
||||||
<form class="overflow-scroll flex flex-col gap-4 px-8">
|
<form
|
||||||
|
id="trail-form"
|
||||||
|
class="overflow-y-scroll overflow-x-hidden flex flex-col gap-4 px-8"
|
||||||
|
on:submit={handleSubmit}
|
||||||
|
>
|
||||||
<h3 class="text-xl font-semibold">Pick a trail</h3>
|
<h3 class="text-xl font-semibold">Pick a trail</h3>
|
||||||
<button class="btn-primary" on:click={openFileBrowser}
|
<button class="btn-primary" type="button" on:click={openFileBrowser}
|
||||||
>Upload GPX</button
|
>Upload GPX</button
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
name="gpx"
|
||||||
id="fileInput"
|
id="fileInput"
|
||||||
|
accept=".gpx"
|
||||||
style="display: none;"
|
style="display: none;"
|
||||||
on:change={handleFileSelection}
|
on:change={handleFileSelection}
|
||||||
/>
|
/>
|
||||||
@@ -171,33 +320,48 @@
|
|||||||
<div class="flex gap-4 justify-around">
|
<div class="flex gap-4 justify-around">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<span>Distance</span>
|
<span>Distance</span>
|
||||||
<span class="font-medium">{formatMeters($trail.distance)}</span>
|
<span class="font-medium">{formatMeters($form.distance)}</span>
|
||||||
|
<input type="hidden" name="distance" value={$form.distance} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<span>Elevation gain</span>
|
<span>Elevation gain</span>
|
||||||
<span class="font-medium"
|
<span class="font-medium"
|
||||||
>{formatMeters($trail.elevation_gain)}</span
|
>{formatMeters($form.elevation_gain)}</span
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="elevation_gain"
|
||||||
|
value={$form.elevation_gain}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<span>Est. duration</span>
|
<span>Est. duration</span>
|
||||||
<span class="font-medium"
|
<span class="font-medium">{formatTimeHHMM($form.duration)}</span
|
||||||
>{formatTimeHHMM($trail.duration)}</span
|
|
||||||
>
|
>
|
||||||
|
<input type="hidden" name="duration" value={$form.duration} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TextField label="Name*" bind:value={$trail.name}></TextField>
|
<TextField
|
||||||
<Textarea label="Describe your trail" bind:value={$trail.description}
|
name="name"
|
||||||
|
label="Name"
|
||||||
|
error={$errors.name}
|
||||||
|
bind:value={$form.name}
|
||||||
|
></TextField>
|
||||||
|
<Textarea
|
||||||
|
name="description"
|
||||||
|
label="Describe your trail"
|
||||||
|
bind:value={$form.description}
|
||||||
></Textarea>
|
></Textarea>
|
||||||
<Select
|
<Select
|
||||||
|
name="category"
|
||||||
label="Category"
|
label="Category"
|
||||||
bind:value={$trail.expand.category}
|
bind:value={$form.expand.category}
|
||||||
items={$categories.map((c) => ({ text: c.name, value: c }))}
|
items={$categories.map((c) => ({ text: c.name, value: c }))}
|
||||||
></Select>
|
></Select>
|
||||||
<hr />
|
<hr />
|
||||||
<h3 class="text-xl font-semibold">Waypoints</h3>
|
<h3 class="text-xl font-semibold">Waypoints</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{#each $trail.expand.waypoints as waypoint, i}
|
{#each $form.expand.waypoints as waypoint, i}
|
||||||
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
|
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
|
||||||
<WaypointCard
|
<WaypointCard
|
||||||
{waypoint}
|
{waypoint}
|
||||||
@@ -207,14 +371,73 @@
|
|||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
<button class="btn-secondary" on:click={findMapCenterAndOpenModal}
|
<button
|
||||||
|
class="btn-secondary"
|
||||||
|
type="button"
|
||||||
|
on:click={beforeWaypointModalOpen}
|
||||||
><i class="fa fa-plus mr-2"></i>Add Waypoint</button
|
><i class="fa fa-plus mr-2"></i>Add Waypoint</button
|
||||||
>
|
>
|
||||||
|
<hr />
|
||||||
|
<h3 class="text-xl font-semibold">Photos</h3>
|
||||||
|
<div class="flex gap-4 max-w-full overflow-x-scroll shrink-0">
|
||||||
|
<button
|
||||||
|
class="btn-secondary h-32 w-32 shrink-0 grow-0 basis-auto"
|
||||||
|
type="button"
|
||||||
|
on:click={openPhotoBrowser}><i class="fa fa-plus"></i></button
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="photoInput"
|
||||||
|
accept="image/*"
|
||||||
|
multiple={true}
|
||||||
|
style="display: none;"
|
||||||
|
on:change={handlePhotoSelection}
|
||||||
|
/>
|
||||||
|
{#each $form.photos as photo, i}
|
||||||
|
<div class="shrink-0 grow-0 basis-auto">
|
||||||
|
<PhotoCard
|
||||||
|
src={photo}
|
||||||
|
on:delete={() => handlePhotoDelete(i)}
|
||||||
|
isThumbnail={$form.thumbnail === photo}
|
||||||
|
on:thumbnail={() => makePhotoThumbnail(i)}
|
||||||
|
></PhotoCard>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<h3 class="text-xl font-semibold">Summit Book</h3>
|
||||||
|
<ul>
|
||||||
|
{#each $form.expand.summit_logs as log, i}
|
||||||
|
<li>
|
||||||
|
<SummitLogCard
|
||||||
|
{log}
|
||||||
|
mode="edit"
|
||||||
|
on:change={(e) => handleSummitLogMenuClick(log, i, e)}
|
||||||
|
></SummitLogCard>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<button
|
||||||
|
class="btn-secondary"
|
||||||
|
type="button"
|
||||||
|
on:click={beforeSummitLogModalOpen}
|
||||||
|
><i class="fa fa-plus mr-2"></i>Add Entry</button
|
||||||
|
>
|
||||||
|
<hr />
|
||||||
|
<Button
|
||||||
|
primary={true}
|
||||||
|
large={true}
|
||||||
|
type="submit"
|
||||||
|
extraClasses="mb-2"
|
||||||
|
{loading}>Save Trail</Button
|
||||||
|
>
|
||||||
</form>
|
</form>
|
||||||
<div class="rounded-xl" id="map"></div>
|
<div class="rounded-xl" id="map"></div>
|
||||||
</main>
|
</main>
|
||||||
<WaypointModal bind:openModal={openWayPointModal} on:save={saveWaypoint}
|
<WaypointModal bind:openModal={openWaypointModal} on:save={saveWaypoint}
|
||||||
></WaypointModal>
|
></WaypointModal>
|
||||||
|
<SummitLogModal bind:openModal={openSummitLogModal} on:save={saveSummitLog}
|
||||||
|
></SummitLogModal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#map,
|
#map,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { Trail } from "$lib/models/trail";
|
||||||
import { categories_index } from "$lib/stores/category_store";
|
import { categories_index } from "$lib/stores/category_store";
|
||||||
|
import { trail } from "$lib/stores/trail_store";
|
||||||
import type { Load } from "@sveltejs/kit";
|
import type { Load } from "@sveltejs/kit";
|
||||||
|
|
||||||
export const load: Load = async ({ params }) => {
|
export const load: Load = async ({ params }) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SummitLogCard from "$lib/components/summit_log_card.svelte";
|
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
|
||||||
import Tabs from "$lib/components/tabs.svelte";
|
import Tabs from "$lib/components/tabs.svelte";
|
||||||
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||||
import { trail } from "$lib/stores/trail_store";
|
import { trail } from "$lib/stores/trail_store";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"extends": "./.svelte-kit/tsconfig.json",
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
"checkJs": false,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user