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": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@types/leaflet.awesome-markers": "^2.0.28",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-gpx": "^1.7.0",
|
||||
"leaflet.awesome-markers": "^2.0.5",
|
||||
"photoswipe": "^5.4.3",
|
||||
"pocketbase": "^0.21.0",
|
||||
"svelte-forms-lib": "^2.0.1",
|
||||
"yup": "^1.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1341,6 +1342,20 @@
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
@@ -1366,6 +1381,15 @@
|
||||
"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": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
@@ -1408,6 +1432,7 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -3131,14 +3156,6 @@
|
||||
"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": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz",
|
||||
|
||||
@@ -33,12 +33,13 @@
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@types/leaflet.awesome-markers": "^2.0.28",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-gpx": "^1.7.0",
|
||||
"leaflet.awesome-markers": "^2.0.5",
|
||||
"photoswipe": "^5.4.3",
|
||||
"pocketbase": "^0.21.0",
|
||||
"svelte-forms-lib": "^2.0.1",
|
||||
"yup": "^1.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
8
web/src/app.d.ts
vendored
8
web/src/app.d.ts
vendored
@@ -1,3 +1,6 @@
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
@@ -7,7 +10,10 @@ declare global {
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
interface Locals {
|
||||
pb: PocketBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
export { };
|
||||
|
||||
@@ -3,17 +3,21 @@
|
||||
@import 'tailwindcss/components';
|
||||
|
||||
.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 {
|
||||
@apply py-3
|
||||
@apply h-12 leading-8
|
||||
}
|
||||
|
||||
.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
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
@apply bg-gray-500 hover:bg-gray-500
|
||||
}
|
||||
|
||||
.tab {
|
||||
@apply p-4 rounded-t-lg
|
||||
}
|
||||
@@ -29,3 +33,62 @@
|
||||
.tab-active:hover {
|
||||
@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;
|
||||
|
||||
function toggleMenu() {
|
||||
function toggleMenu(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
isOpen = !isOpen;
|
||||
}
|
||||
|
||||
@@ -15,7 +17,7 @@
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
function handleItemClick(item: { text: string, value: any }) {
|
||||
function handleItemClick(item: { text: string; value: any }) {
|
||||
dispatch("change", item);
|
||||
closeMenu();
|
||||
}
|
||||
@@ -27,6 +29,7 @@
|
||||
<button
|
||||
class="hover:bg-gray-100 w-8 h-8 rounded-full"
|
||||
on:click={toggleMenu}
|
||||
type="button"
|
||||
>
|
||||
<i class="fa fa-ellipsis-vertical"></i>
|
||||
</button>
|
||||
@@ -42,8 +45,8 @@
|
||||
<li
|
||||
class="menu-item p-4 cursor-pointer hover:bg-gray-100 focus:bg-gray-200 transition-colors"
|
||||
tabindex="0"
|
||||
on:mouseup={() => handleItemClick(item)}
|
||||
on:keydown={() => handleItemClick(item)}
|
||||
on:mouseup|stopPropagation={() => handleItemClick(item)}
|
||||
on:keydown|stopPropagation={() => handleItemClick(item)}
|
||||
>
|
||||
{item.text}
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
export let name: string = "";
|
||||
export let value: any;
|
||||
export let items: { text: string; value: any }[] = [];
|
||||
export let label: string = "";
|
||||
@@ -11,6 +12,7 @@
|
||||
</p>
|
||||
{/if}
|
||||
<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"
|
||||
bind:value
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
export let name: string = "";
|
||||
export let value: string | number = "";
|
||||
export let placeholder: string = "";
|
||||
export let label: string = "";
|
||||
@@ -17,6 +18,7 @@
|
||||
<i class="fa fa-{icon}"></i>
|
||||
{/if}
|
||||
<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}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
export let name: string = "";
|
||||
export let value: string | number = "";
|
||||
export let placeholder: string = "";
|
||||
export let rows: number = 3;
|
||||
@@ -13,6 +14,7 @@
|
||||
</p>
|
||||
{/if}
|
||||
<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"
|
||||
{rows}
|
||||
{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 { getFileURL } from "$lib/util/file_util";
|
||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||
import Dropdown from "../base/dropdown.svelte";
|
||||
|
||||
export let trail: Trail;
|
||||
|
||||
const dropdownItems = [
|
||||
{ text: "Edit", value: "edit" },
|
||||
{ text: "Delete", value: "delete" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="trail-card rounded-2xl shadow-md w-72 overflow-hidden cursor-pointer"
|
||||
>
|
||||
<img class="w-full h-48" src={getFileURL(trail, trail.thumbnail)} alt="" />
|
||||
<div class="trail-card rounded-2xl shadow-md w-72 cursor-pointer">
|
||||
<div class="w-full h-48 overflow-hidden rounded-t-2xl">
|
||||
<img src={getFileURL(trail, trail.thumbnail)} alt="" />
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div>
|
||||
<h4 class="font-semibold text-lg">{trail.name}</h4>
|
||||
<div class="flex justify-between items-center">
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex mt-2 gap-4 text-sm text-gray-500">
|
||||
@@ -2,11 +2,12 @@
|
||||
import { Waypoint, waypointSchema } from "$lib/models/waypoint";
|
||||
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 TextField from "../base/text_field.svelte";
|
||||
import Textarea from "../base/textarea.svelte";
|
||||
import { waypoint } from "$lib/stores/waypoint_store";
|
||||
import { util } from "$lib/vendor/svelte-form-lib/util";
|
||||
|
||||
export let openModal: (() => void) | undefined = undefined;
|
||||
export let closeModal: (() => void) | undefined = undefined;
|
||||
@@ -21,7 +22,7 @@
|
||||
closeModal!();
|
||||
},
|
||||
});
|
||||
$: form.set($waypoint);
|
||||
$: form.set(util.cloneDeep($waypoint));
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
@@ -41,6 +42,7 @@
|
||||
<div class="flex gap-4">
|
||||
<div class="basis-full">
|
||||
<TextField
|
||||
name="name"
|
||||
label="Name"
|
||||
bind:value={$form.name}
|
||||
error={$errors.name}
|
||||
@@ -49,6 +51,7 @@
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
name="icon"
|
||||
label="Icon"
|
||||
bind:value={$form.icon}
|
||||
icon={$form.icon}
|
||||
@@ -58,6 +61,7 @@
|
||||
</div>
|
||||
|
||||
<Textarea
|
||||
name="description"
|
||||
label="Description"
|
||||
bind:value={$form.description}
|
||||
error={$errors.description}
|
||||
@@ -65,12 +69,14 @@
|
||||
></Textarea>
|
||||
<div class="flex gap-4">
|
||||
<TextField
|
||||
name="lat"
|
||||
label="Latitude"
|
||||
bind:value={$form.lat}
|
||||
error={$errors.lat}
|
||||
on:change={handleChange}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="lon"
|
||||
label="Longitude"
|
||||
bind:value={$form.lon}
|
||||
error={$errors.lat}
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
interface SummitLog {
|
||||
id: string;
|
||||
date: string;
|
||||
text: string;
|
||||
import { parse } from "date-fns";
|
||||
import { date, number, object, string } from "yup";
|
||||
|
||||
class SummitLog {
|
||||
id?: string;
|
||||
date: 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 { SummitLog } from "./summit_log";
|
||||
import type { Waypoint } from "./waypoint";
|
||||
@@ -20,38 +21,57 @@ class Trail {
|
||||
tags?: string[];
|
||||
description?: string;
|
||||
|
||||
_photoFiles: File[]
|
||||
|
||||
constructor(name: string,
|
||||
id?: string,
|
||||
location?: string,
|
||||
distance?: number,
|
||||
elevation_gain?: number,
|
||||
duration?: number,
|
||||
thumbnail?: string,
|
||||
photos?: string[],
|
||||
gpx?: string,
|
||||
category?: Category,
|
||||
waypoints?: Waypoint[],
|
||||
summit_logs?: SummitLog[],
|
||||
tags?: 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 ?? []
|
||||
params?: {
|
||||
id?: string,
|
||||
location?: string,
|
||||
distance?: number,
|
||||
elevation_gain?: number,
|
||||
duration?: number,
|
||||
thumbnail?: string,
|
||||
photos?: string[],
|
||||
gpx?: string,
|
||||
category?: Category,
|
||||
waypoints?: Waypoint[],
|
||||
summit_logs?: SummitLog[],
|
||||
tags?: string[],
|
||||
description?: string
|
||||
}
|
||||
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(),
|
||||
name: string().optional(),
|
||||
description: string().optional(),
|
||||
lat: number().min(0).required('Required').typeError('Invalid latitude'),
|
||||
lon: number().min(0).required('Required').typeError('Invalid longitude'),
|
||||
lat: number().required('Required').typeError('Invalid latitude'),
|
||||
lon: number().required('Required').typeError('Invalid longitude'),
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!trail.gpx) {
|
||||
return "";
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
import { pb } from "$lib/constants";
|
||||
import { Waypoint } from "$lib/models/waypoint";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
|
||||
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,7 +1,6 @@
|
||||
import type { Waypoint } from "$lib/models/waypoint";
|
||||
|
||||
export function formatTimeHHMM(minutes?: number) {
|
||||
if(!minutes) {
|
||||
if (!minutes) {
|
||||
return "-";
|
||||
}
|
||||
const m = minutes % 60;
|
||||
@@ -12,7 +11,7 @@ export function formatTimeHHMM(minutes?: number) {
|
||||
}
|
||||
|
||||
export function formatMeters(meters?: number) {
|
||||
if(!meters) {
|
||||
if (!meters) {
|
||||
return "-";
|
||||
}
|
||||
if (meters % 1 === 0) {
|
||||
@@ -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">
|
||||
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 { 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>
|
||||
|
||||
<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"
|
||||
>
|
||||
{#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}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { categories_index } from "$lib/stores/category_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 categories_index()
|
||||
};
|
||||
@@ -1,29 +1,80 @@
|
||||
<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 TextField from "$lib/components/base/text_field.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 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 { categories } from "$lib/stores/category_store";
|
||||
import { trail } from "$lib/stores/trail_store";
|
||||
import { waypoint } from "$lib/stores/waypoint_store";
|
||||
import { summitLog, summit_logs_create } from "$lib/stores/summit_log_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 { 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 "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { onMount } from "svelte";
|
||||
import type { W } from "vitest/dist/reporters-rzC174PQ.js";
|
||||
|
||||
let L: any;
|
||||
let map: Map;
|
||||
|
||||
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 () => {
|
||||
L = (await import("leaflet")).default;
|
||||
@@ -50,7 +101,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
$trail.gpx = selectedFile?.name;
|
||||
$form.gpx = selectedFile?.name;
|
||||
$form.expand.waypoints = [];
|
||||
|
||||
var reader = new FileReader();
|
||||
|
||||
@@ -96,14 +148,14 @@
|
||||
e.point._latlng.lng,
|
||||
{ name: e.point.options.title, marker: e.point },
|
||||
);
|
||||
$trail.expand.waypoints.push(waypoint);
|
||||
$form.expand.waypoints.push(waypoint);
|
||||
}
|
||||
})
|
||||
.on("loaded", function (e: LeafletEvent) {
|
||||
map.fitBounds(e.target.getBounds());
|
||||
$trail.distance = e.target.get_distance();
|
||||
$trail.elevation_gain = e.target.get_elevation_gain();
|
||||
$trail.duration = e.target.get_total_time() / 1000 / 60;
|
||||
$form.distance = e.target.get_distance();
|
||||
$form.elevation_gain = e.target.get_elevation_gain();
|
||||
$form.duration = e.target.get_total_time() / 1000 / 60;
|
||||
})
|
||||
.addTo(map);
|
||||
};
|
||||
@@ -119,10 +171,10 @@
|
||||
) {
|
||||
if (e.detail.value === "edit") {
|
||||
waypoint.set(currentWaypoint);
|
||||
openWayPointModal();
|
||||
openWaypointModal();
|
||||
} else if (e.detail.value === "delete") {
|
||||
currentWaypoint.marker?.remove();
|
||||
$trail.expand.waypoints = $trail.expand.waypoints.filter(
|
||||
$form.expand.waypoints = $form.expand.waypoints.filter(
|
||||
(w) =>
|
||||
w.lat !== currentWaypoint.lat &&
|
||||
w.lon !== currentWaypoint.lon,
|
||||
@@ -130,39 +182,136 @@
|
||||
}
|
||||
}
|
||||
|
||||
function findMapCenterAndOpenModal() {
|
||||
function beforeWaypointModalOpen() {
|
||||
const mapCenter = map.getCenter();
|
||||
waypoint.set(new Waypoint(mapCenter.lat, mapCenter.lng));
|
||||
openWayPointModal();
|
||||
openWaypointModal();
|
||||
}
|
||||
|
||||
function saveWaypoint(e: CustomEvent<Waypoint>) {
|
||||
const savedWaypoint = e.detail;
|
||||
let editedWaypointIndex = $form.expand.waypoints.findIndex(
|
||||
(s) => s.id == savedWaypoint.id,
|
||||
);
|
||||
|
||||
if ($trail.expand.waypoints.includes(savedWaypoint)) {
|
||||
$trail.expand.waypoints = $trail.expand.waypoints;
|
||||
savedWaypoint.marker?.remove();
|
||||
if (editedWaypointIndex >= 0) {
|
||||
$form.expand.waypoints[editedWaypointIndex].marker?.remove();
|
||||
$form.expand.waypoints[editedWaypointIndex] = savedWaypoint;
|
||||
} else {
|
||||
$trail.expand.waypoints = [
|
||||
...$trail.expand.waypoints,
|
||||
savedWaypoint,
|
||||
];
|
||||
savedWaypoint.id = cryptoRandomString({ length: 15 });
|
||||
|
||||
$form.expand.waypoints = [...$form.expand.waypoints, savedWaypoint];
|
||||
}
|
||||
const marker = createMarkerFromWaypoint(L, savedWaypoint);
|
||||
marker.addTo(map);
|
||||
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>
|
||||
|
||||
<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>
|
||||
<button class="btn-primary" on:click={openFileBrowser}
|
||||
<button class="btn-primary" type="button" on:click={openFileBrowser}
|
||||
>Upload GPX</button
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
name="gpx"
|
||||
id="fileInput"
|
||||
accept=".gpx"
|
||||
style="display: none;"
|
||||
on:change={handleFileSelection}
|
||||
/>
|
||||
@@ -171,33 +320,48 @@
|
||||
<div class="flex gap-4 justify-around">
|
||||
<div class="flex flex-col items-center">
|
||||
<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 class="flex flex-col items-center">
|
||||
<span>Elevation gain</span>
|
||||
<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 class="flex flex-col items-center">
|
||||
<span>Est. duration</span>
|
||||
<span class="font-medium"
|
||||
>{formatTimeHHMM($trail.duration)}</span
|
||||
<span class="font-medium">{formatTimeHHMM($form.duration)}</span
|
||||
>
|
||||
<input type="hidden" name="duration" value={$form.duration} />
|
||||
</div>
|
||||
</div>
|
||||
<TextField label="Name*" bind:value={$trail.name}></TextField>
|
||||
<Textarea label="Describe your trail" bind:value={$trail.description}
|
||||
<TextField
|
||||
name="name"
|
||||
label="Name"
|
||||
error={$errors.name}
|
||||
bind:value={$form.name}
|
||||
></TextField>
|
||||
<Textarea
|
||||
name="description"
|
||||
label="Describe your trail"
|
||||
bind:value={$form.description}
|
||||
></Textarea>
|
||||
<Select
|
||||
name="category"
|
||||
label="Category"
|
||||
bind:value={$trail.expand.category}
|
||||
bind:value={$form.expand.category}
|
||||
items={$categories.map((c) => ({ text: c.name, value: c }))}
|
||||
></Select>
|
||||
<hr />
|
||||
<h3 class="text-xl font-semibold">Waypoints</h3>
|
||||
<ul>
|
||||
{#each $trail.expand.waypoints as waypoint, i}
|
||||
{#each $form.expand.waypoints as waypoint, i}
|
||||
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
|
||||
<WaypointCard
|
||||
{waypoint}
|
||||
@@ -207,14 +371,73 @@
|
||||
</li>
|
||||
{/each}
|
||||
</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
|
||||
>
|
||||
<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>
|
||||
<div class="rounded-xl" id="map"></div>
|
||||
</main>
|
||||
<WaypointModal bind:openModal={openWayPointModal} on:save={saveWaypoint}
|
||||
<WaypointModal bind:openModal={openWaypointModal} on:save={saveWaypoint}
|
||||
></WaypointModal>
|
||||
<SummitLogModal bind:openModal={openSummitLogModal} on:save={saveSummitLog}
|
||||
></SummitLogModal>
|
||||
|
||||
<style>
|
||||
#map,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Trail } from "$lib/models/trail";
|
||||
import { categories_index } from "$lib/stores/category_store";
|
||||
import { trail } from "$lib/stores/trail_store";
|
||||
import type { Load } from "@sveltejs/kit";
|
||||
|
||||
export const load: Load = async ({ params }) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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 WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||
import { trail } from "$lib/stores/trail_store";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"checkJs": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
Reference in New Issue
Block a user