adds profile page
This commit is contained in:
124
db/migrations/1730558039_updated_summit_logs.go
Normal file
124
db/migrations/1730558039_updated_summit_logs.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("dd2l9a4vxpy2ni8")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add
|
||||
new_distance := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "jovws28m",
|
||||
"name": "distance",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": 0,
|
||||
"max": null,
|
||||
"noDecimal": false
|
||||
}
|
||||
}`), new_distance); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(new_distance)
|
||||
|
||||
// add
|
||||
new_elevation_gain := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "m2kndtwn",
|
||||
"name": "elevation_gain",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": 0,
|
||||
"max": null,
|
||||
"noDecimal": false
|
||||
}
|
||||
}`), new_elevation_gain); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(new_elevation_gain)
|
||||
|
||||
// add
|
||||
new_elevation_loss := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "uqqo9cws",
|
||||
"name": "elevation_loss",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": 0,
|
||||
"max": null,
|
||||
"noDecimal": false
|
||||
}
|
||||
}`), new_elevation_loss); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(new_elevation_loss)
|
||||
|
||||
// add
|
||||
new_duration := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "vwxjsrae",
|
||||
"name": "duration",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": 0,
|
||||
"max": null,
|
||||
"noDecimal": false
|
||||
}
|
||||
}`), new_duration); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(new_duration)
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("dd2l9a4vxpy2ni8")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("jovws28m")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("m2kndtwn")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("uqqo9cws")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("vwxjsrae")
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
})
|
||||
}
|
||||
48
web/package-lock.json
generated
48
web/package-lock.json
generated
@@ -1,21 +1,23 @@
|
||||
{
|
||||
"name": "wanderer",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wanderer",
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@sveltejs/adapter-node": "^4.0.1",
|
||||
"@threlte/core": "^7.1.0",
|
||||
"@threlte/extras": "^8.7.5",
|
||||
"@types/chart.js": "^2.9.41",
|
||||
"@types/leaflet.awesome-markers": "^2.0.28",
|
||||
"@types/three": "^0.161.2",
|
||||
"@types/xmldom": "^0.1.34",
|
||||
"canvg": "^4.0.1",
|
||||
"chart.js": "^4.4.6",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"heic2any": "^0.0.4",
|
||||
"instead": "^1.0.3",
|
||||
@@ -31,6 +33,7 @@
|
||||
"photoswipe": "^5.4.3",
|
||||
"pocketbase": "^0.21.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"svelte-chartjs": "^3.1.5",
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"three": "^0.161.0",
|
||||
"xmldom": "^0.6.0",
|
||||
@@ -560,6 +563,11 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@kurkle/color": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
|
||||
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -1062,6 +1070,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chart.js": {
|
||||
"version": "2.9.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.41.tgz",
|
||||
"integrity": "sha512-3dvkDvueckY83UyUXtJMalYoH6faOLkWQoaTlJgB4Djde3oORmNP0Jw85HtzTuXyliUHcdp704s0mZFQKio/KQ==",
|
||||
"dependencies": {
|
||||
"moment": "^2.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
@@ -1677,6 +1693,17 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.4.6",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz",
|
||||
"integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/check-error": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
|
||||
@@ -3797,6 +3824,14 @@
|
||||
"ufo": "^1.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@@ -5226,6 +5261,15 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-chartjs": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte-chartjs/-/svelte-chartjs-3.1.5.tgz",
|
||||
"integrity": "sha512-ka2zh7v5FiwfAX1oMflZ0HkNkgjHjFqANgRyC+vNYXfxtx2ku68Zo+2KgbKeBH2nS1ThDqkIACPzGxy4T0UaoA==",
|
||||
"peerDependencies": {
|
||||
"chart.js": "^3.5.0 || ^4.0.0",
|
||||
"svelte": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.3.tgz",
|
||||
|
||||
@@ -36,10 +36,12 @@
|
||||
"@sveltejs/adapter-node": "^4.0.1",
|
||||
"@threlte/core": "^7.1.0",
|
||||
"@threlte/extras": "^8.7.5",
|
||||
"@types/chart.js": "^2.9.41",
|
||||
"@types/leaflet.awesome-markers": "^2.0.28",
|
||||
"@types/three": "^0.161.2",
|
||||
"@types/xmldom": "^0.1.34",
|
||||
"canvg": "^4.0.1",
|
||||
"chart.js": "^4.4.6",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"heic2any": "^0.0.4",
|
||||
"instead": "^1.0.3",
|
||||
@@ -55,6 +57,7 @@
|
||||
"photoswipe": "^5.4.3",
|
||||
"pocketbase": "^0.21.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"svelte-chartjs": "^3.1.5",
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"three": "^0.161.0",
|
||||
"xmldom": "^0.6.0",
|
||||
|
||||
163
web/src/lib/components/base/calendar.svelte
Normal file
163
web/src/lib/components/base/calendar.svelte
Normal file
@@ -0,0 +1,163 @@
|
||||
<script lang="ts">
|
||||
import type { SummitLog } from "$lib/models/summit_log";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { isSameDay, isToday } from "../../util/date_util";
|
||||
|
||||
export let logs: SummitLog[] = [];
|
||||
export let colorMap: Record<string, string> = {};
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
forward: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
backward: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
}>();
|
||||
|
||||
const weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||
const months = [
|
||||
"Januar",
|
||||
"Februar",
|
||||
"März",
|
||||
"April",
|
||||
"Mai",
|
||||
"Juni",
|
||||
"Juli",
|
||||
"August",
|
||||
"September",
|
||||
"Oktober",
|
||||
"November",
|
||||
"Dezember",
|
||||
];
|
||||
const today = new Date();
|
||||
let currentMonth = today.getMonth();
|
||||
let currentYear = today.getFullYear();
|
||||
let currentMonthArray: ({
|
||||
date: Date | undefined;
|
||||
today: boolean;
|
||||
log?: SummitLog;
|
||||
} | null)[];
|
||||
$: currentMonthArray = generateMonthArray(currentYear, currentMonth, logs);
|
||||
|
||||
function calculateFirstDayOfMonthDayOfWeek(year: number, month: number) {
|
||||
const date = new Date(year, month, 1);
|
||||
const day = date.getDay();
|
||||
|
||||
return day == 0 ? 6 : day - 1;
|
||||
}
|
||||
|
||||
function daysInMonth(year: number, month: number) {
|
||||
const days = new Date(year, month + 1, 0).getDate();
|
||||
return days;
|
||||
}
|
||||
|
||||
function generateMonthArray(
|
||||
year: number,
|
||||
month: number,
|
||||
logs: SummitLog[],
|
||||
) {
|
||||
const a: ({ date: Date; today: boolean; log?: SummitLog } | null)[] =
|
||||
[];
|
||||
const firstDay = calculateFirstDayOfMonthDayOfWeek(year, month);
|
||||
const totalDays = daysInMonth(year, month);
|
||||
|
||||
for (let i = 0; i < 42; i++) {
|
||||
if (i < firstDay || i - firstDay >= totalDays) {
|
||||
a.push(null);
|
||||
} else {
|
||||
const date = new Date(
|
||||
currentYear,
|
||||
currentMonth,
|
||||
i + 1 - firstDay,
|
||||
);
|
||||
const today = isToday(date);
|
||||
const logAtDate = logs.find((l) =>
|
||||
isSameDay(date, new Date(l.date)),
|
||||
);
|
||||
a.push({ date: date, today: today, log: logAtDate });
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function monthPlus() {
|
||||
if (currentMonth == 11) {
|
||||
currentYear++;
|
||||
currentMonth = 0;
|
||||
} else {
|
||||
currentMonth++;
|
||||
}
|
||||
dispatch("forward", {
|
||||
start: new Date(currentYear, currentMonth, 1),
|
||||
end: new Date(currentYear, currentMonth + 1, 0),
|
||||
});
|
||||
}
|
||||
|
||||
function monthMinus() {
|
||||
if (currentMonth == 0) {
|
||||
currentYear--;
|
||||
currentMonth = 11;
|
||||
} else {
|
||||
currentMonth--;
|
||||
}
|
||||
dispatch("backward", {
|
||||
start: new Date(currentYear, currentMonth, 1),
|
||||
end: new Date(currentYear, currentMonth + 1, 0),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="calendar-header w-full flex items-center justify-between mb-6">
|
||||
<div class="calendar-month-year basis-full">
|
||||
<span class="text-lg">{months[currentMonth]}</span>
|
||||
<span>{currentYear}</span>
|
||||
</div>
|
||||
<button class="btn-icon mr-2" on:click={monthMinus}
|
||||
><i class="fa fa-caret-left"></i></button
|
||||
>
|
||||
<button class="btn-icon" on:click={monthPlus}
|
||||
><i class="fa fa-caret-right"></i></button
|
||||
>
|
||||
</div>
|
||||
<div class="calendar-body">
|
||||
<div class="grid grid-cols-7">
|
||||
{#each weekdays as weekday, i}
|
||||
<div
|
||||
class="calendar-weekday flex items-center justify-center h-10 text-gray-500"
|
||||
>
|
||||
{weekday}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="grid grid-cols-7 grid-rows-6" style="aspect-ratio: 1.17/1">
|
||||
{#each { length: 42 } as _, i}
|
||||
<div
|
||||
class="calendar-day flex items-center justify-center rounded-xl cursor-pointer"
|
||||
class:today={currentMonthArray[i]?.today}
|
||||
style="background-color: {colorMap[
|
||||
currentMonthArray[
|
||||
i
|
||||
]?.log?.expand.trails_via_summit_logs?.at(0)?.expand
|
||||
?.category?.name ?? ''
|
||||
] ?? ''}"
|
||||
>
|
||||
{currentMonthArray[i]?.date?.getDate() ?? ""}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.calendar-weekday {
|
||||
font-weight: 600;
|
||||
}
|
||||
.calendar-month-year span {
|
||||
font-weight: 600;
|
||||
}
|
||||
.calendar-day.today {
|
||||
@apply border border-input-border;
|
||||
}
|
||||
</style>
|
||||
@@ -18,6 +18,7 @@
|
||||
];
|
||||
|
||||
const dropdownItems = [
|
||||
{ text: $_("profile"), value: "profile", icon: "user" },
|
||||
{ text: $_("settings"), value: "settings", icon: "cog" },
|
||||
{ text: $_("logout"), value: "logout", icon: "right-from-bracket" },
|
||||
];
|
||||
@@ -76,7 +77,9 @@
|
||||
});
|
||||
|
||||
function handleDropdownClick(item: { text: string; value: any }) {
|
||||
if (item.value == "logout") {
|
||||
if (item.value == "profile") {
|
||||
goto("/profile");
|
||||
} else if (item.value == "logout") {
|
||||
logout();
|
||||
window.location.href = "/";
|
||||
} else if (item.value == "settings") {
|
||||
|
||||
@@ -28,12 +28,6 @@
|
||||
{ text: $_("delete"), value: "delete" },
|
||||
];
|
||||
|
||||
let totals: {
|
||||
distance: number;
|
||||
elevationGain: number;
|
||||
duration: number;
|
||||
} | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
if (!map) {
|
||||
await initMap();
|
||||
@@ -65,13 +59,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const gpxObject = await GPX.parse(log.expand.gpx_data);
|
||||
if (gpxObject instanceof Error) {
|
||||
throw gpxObject;
|
||||
}
|
||||
|
||||
totals = gpxObject.getTotals();
|
||||
|
||||
const geoJson = gpx(
|
||||
new DOMParser().parseFromString(log.expand.gpx_data, "text/xml"),
|
||||
);
|
||||
@@ -79,7 +66,7 @@
|
||||
filter: (feature: any, layer: any) => {
|
||||
return feature.geometry.type !== "Point";
|
||||
},
|
||||
}).addTo(map)
|
||||
}).addTo(map);
|
||||
map.fitBounds(layer.getBounds());
|
||||
map.invalidateSize();
|
||||
}
|
||||
@@ -88,7 +75,6 @@
|
||||
if (layer) {
|
||||
map?.removeLayer(layer);
|
||||
}
|
||||
totals = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -117,25 +103,28 @@
|
||||
<Dropdown items={dropdownItems} on:change></Dropdown>
|
||||
{/if}
|
||||
</div>
|
||||
{#if totals}
|
||||
{#if log.distance || log.elevation_gain || log.elevation_loss || log.duration}
|
||||
<div
|
||||
class="flex mt-1 gap-x-4 text-sm text-gray-500 flex-wrap mb-2"
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-left-right mr-2"></i>{formatDistance(
|
||||
totals.distance,
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-up-down mr-2"></i>{formatElevation(
|
||||
totals.elevationGain,
|
||||
log.distance,
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-clock mr-2"></i>{formatTimeHHMM(
|
||||
totals.duration / 1000 / 60,
|
||||
log.duration,
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-arrow-trend-up mr-2"
|
||||
></i>{formatElevation(log.elevation_gain)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-arrow-trend-down mr-2"
|
||||
></i>{formatElevation(log.elevation_loss)}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="whitespace-pre-wrap">{log.text}</span>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import Modal from "../base/modal.svelte";
|
||||
import Textarea from "../base/textarea.svelte";
|
||||
import TrailPicker from "../trail/trail_picker.svelte";
|
||||
import GPX from "$lib/models/gpx/gpx";
|
||||
export let openModal: (() => void) | undefined = undefined;
|
||||
export let closeModal: (() => void) | undefined = undefined;
|
||||
|
||||
@@ -38,6 +39,27 @@
|
||||
$: if ($summitLog._gpx) {
|
||||
$form._gpx = $summitLog._gpx;
|
||||
}
|
||||
|
||||
async function handleTrailSelection(trailData: string | null) {
|
||||
if (!trailData) {
|
||||
$form.duration = undefined;
|
||||
$form.elevation_gain = undefined;
|
||||
$form.elevation_loss = undefined;
|
||||
$form.distance = undefined;
|
||||
return;
|
||||
}
|
||||
const gpxObject = await GPX.parse(trailData);
|
||||
if (gpxObject instanceof Error) {
|
||||
throw gpxObject;
|
||||
}
|
||||
|
||||
const totals = gpxObject.getTotals();
|
||||
|
||||
$form.duration = totals.duration / 1000 / 60;
|
||||
$form.elevation_gain = totals.elevationGain;
|
||||
$form.elevation_loss = totals.elevationLoss;
|
||||
$form.distance = totals.distance;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
@@ -68,6 +90,7 @@
|
||||
bind:trailFile={$form._gpx}
|
||||
bind:trailData={$form.expand.gpx_data}
|
||||
label={$_("trail", { values: { n: 1 } })}
|
||||
on:change={(e) => handleTrailSelection(e.detail)}
|
||||
></TrailPicker>
|
||||
<div class="basis-full">
|
||||
<Textarea
|
||||
|
||||
@@ -8,28 +8,30 @@
|
||||
import type { Map } from "leaflet";
|
||||
import { gpx } from "$lib/vendor/toGeoJSON/toGeoJSON";
|
||||
import { endIcon, startIcon } from "$lib/util/leaflet_util";
|
||||
import { _ } from "svelte-i18n";
|
||||
import MapWithElevation from "../trail/map_with_elevation.svelte";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { gpx2trail } from "$lib/util/gpx_util";
|
||||
|
||||
export let summitLogs: SummitLog[];
|
||||
export let showCategory: boolean = false;
|
||||
|
||||
let openModal: () => void;
|
||||
let closeModal: () => void;
|
||||
|
||||
let map: Map;
|
||||
let L: any;
|
||||
let layerGroup: any;
|
||||
|
||||
let trail: Trail | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
L = (await import("leaflet")).default;
|
||||
|
||||
map = L.map("summit-log-table-map");
|
||||
map.attributionControl.setPrefix(false);
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
}).addTo(map);
|
||||
|
||||
layerGroup = L.layerGroup();
|
||||
|
||||
layerGroup.addTo(map);
|
||||
// L = (await import("leaflet")).default;
|
||||
// map = L.map("summit-log-table-map");
|
||||
// map.attributionControl.setPrefix(false);
|
||||
// L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
// attribution: "© OpenStreetMap contributors",
|
||||
// }).addTo(map);
|
||||
// layerGroup = L.layerGroup();
|
||||
// layerGroup.addTo(map);
|
||||
});
|
||||
|
||||
async function openMap(log: SummitLog) {
|
||||
@@ -37,61 +39,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
layerGroup.clearLayers();
|
||||
const geoJson = gpx(
|
||||
new DOMParser().parseFromString(log.expand.gpx_data, "text/xml"),
|
||||
);
|
||||
const layer = L.geoJson(geoJson, {
|
||||
onEachFeature: (feature: any, layer: any) => {
|
||||
let startCoords, endCoords;
|
||||
|
||||
if (geoJson.features && geoJson.features.length > 0) {
|
||||
const geometry = geoJson.features[0].geometry;
|
||||
if (geometry.type === "LineString") {
|
||||
startCoords = geometry.coordinates[0];
|
||||
endCoords =
|
||||
geometry.coordinates[
|
||||
geometry.coordinates.length - 1
|
||||
];
|
||||
} else if (geometry.type === "MultiLineString") {
|
||||
startCoords = (geometry as any).coordinates[0][0];
|
||||
endCoords = (geometry as any).coordinates[
|
||||
geometry.coordinates.length - 1
|
||||
][
|
||||
(geometry as any).coordinates[
|
||||
geometry.coordinates.length - 1
|
||||
].length - 1
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (startCoords && endCoords) {
|
||||
const startMarker = L.marker(
|
||||
[startCoords[1], startCoords[0]],
|
||||
{
|
||||
icon: startIcon(),
|
||||
},
|
||||
);
|
||||
|
||||
const endMarker = L.marker([endCoords[1], endCoords[0]], {
|
||||
icon: endIcon(),
|
||||
});
|
||||
|
||||
layerGroup.addLayer(startMarker);
|
||||
layerGroup.addLayer(endMarker);
|
||||
}
|
||||
},
|
||||
filter: (feature: any, layer: any) => {
|
||||
return feature.geometry.type !== "Point";
|
||||
},
|
||||
}).addTo(map);
|
||||
|
||||
layerGroup.addLayer(layer);
|
||||
trail = (await gpx2trail(log.expand.gpx_data)).trail;
|
||||
trail.expand.gpx_data = log.expand.gpx_data;
|
||||
|
||||
openModal();
|
||||
await tick();
|
||||
map.fitBounds(layer.getBounds());
|
||||
|
||||
map.invalidateSize();
|
||||
return;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -99,23 +54,41 @@
|
||||
<thead>
|
||||
<tr class="text-sm">
|
||||
<th class="w-24"></th>
|
||||
<th>Date</th>
|
||||
<th>Distance</th>
|
||||
<th class="whitespace-nowrap">Elevation gain</th>
|
||||
<th>Duration</th>
|
||||
<th>{$_("date")}</th>
|
||||
<th>{$_("distance")}</th>
|
||||
<th>{$_("elevation-gain")}</th>
|
||||
<th>{$_("elevation-loss")}</th>
|
||||
<th>{$_("duration")}</th>
|
||||
{#if showCategory}
|
||||
<th>
|
||||
{$_("category")}
|
||||
</th>
|
||||
{/if}
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each summitLogs as log, i}
|
||||
<SummitLogTableRow index={i} {log} on:open={() => openMap(log)}
|
||||
<SummitLogTableRow
|
||||
index={i}
|
||||
{log}
|
||||
on:open={() => openMap(log)}
|
||||
{showCategory}
|
||||
></SummitLogTableRow>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Modal id="summit-log-table-modal" title="" bind:openModal bind:closeModal>
|
||||
<div slot="content" id="summit-log-table-map" class="h-96"></div>
|
||||
<Modal
|
||||
id="summit-log-table-modal"
|
||||
size="max-w-4xl"
|
||||
title=""
|
||||
bind:openModal
|
||||
bind:closeModal
|
||||
>
|
||||
<div slot="content" id="summit-log-table-map" class="h-[32rem]">
|
||||
<MapWithElevation {trail} bind:map></MapWithElevation>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import type { SummitLog } from "$lib/models/summit_log";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
import GPX from "$lib/models/gpx/gpx";
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
@@ -11,15 +10,11 @@
|
||||
import { gpx } from "$lib/vendor/toGeoJSON/toGeoJSON";
|
||||
import type { Map } from "leaflet";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let index: number;
|
||||
export let log: SummitLog;
|
||||
|
||||
let totals: {
|
||||
distance: number;
|
||||
elevationGain: number;
|
||||
duration: number;
|
||||
} | null = null;
|
||||
export let showCategory: boolean = false;
|
||||
|
||||
let map: Map;
|
||||
|
||||
@@ -47,13 +42,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const gpxObject = await GPX.parse(log.expand.gpx_data);
|
||||
if (gpxObject instanceof Error) {
|
||||
throw gpxObject;
|
||||
}
|
||||
|
||||
totals = gpxObject.getTotals();
|
||||
|
||||
const geoJson = gpx(
|
||||
new DOMParser().parseFromString(log.expand.gpx_data, "text/xml"),
|
||||
);
|
||||
@@ -78,12 +66,12 @@
|
||||
<button
|
||||
type="button"
|
||||
on:click={openMap}
|
||||
class="h-20 aspect-square shrink-0 rounded-xl !bg-background"
|
||||
class="h-20 aspect-square shrink-0 rounded-xl !bg-background hover:!bg-secondary-hover transition-colors"
|
||||
class:hidden={!log.expand.gpx_data}
|
||||
id="mini-map-{index}"
|
||||
></button>
|
||||
</td>
|
||||
<td class:py-4={!log.text && !log.expand.gpx_data}
|
||||
<td class:py-4={!log.expand.gpx_data}
|
||||
>{new Date(log.date).toLocaleDateString(undefined, {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
@@ -92,18 +80,30 @@
|
||||
})}</td
|
||||
>
|
||||
<td>
|
||||
{formatDistance(totals?.distance)}
|
||||
{formatDistance(log.distance)}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{formatElevation(totals?.elevationGain)}
|
||||
</td><td>
|
||||
{formatTimeHHMM(totals ? (totals?.duration / 1000 / 60) : undefined)}
|
||||
{formatElevation(log.elevation_gain)}
|
||||
</td>
|
||||
<td>
|
||||
{formatElevation(log.elevation_loss)}
|
||||
</td>
|
||||
<td>
|
||||
{formatTimeHHMM(log.duration)}
|
||||
</td>
|
||||
{#if showCategory}
|
||||
<td>
|
||||
{$_(
|
||||
log.expand.trails_via_summit_logs?.at(0)?.expand.category
|
||||
?.name ?? "-",
|
||||
)}
|
||||
</td>
|
||||
{/if}
|
||||
<td>
|
||||
{#if log.text}
|
||||
<button on:click={() => (showText = !showText)} class="btn-icon"
|
||||
><i class="fa{showText ? '' : '-regular'} fa-message"
|
||||
><i class="fa{showText ? '' : '-regular'} fa-message text-gray-500"
|
||||
></i></button
|
||||
>
|
||||
{/if}
|
||||
@@ -111,11 +111,12 @@
|
||||
</tr>
|
||||
{#if showText}
|
||||
<tr
|
||||
><td class="text-left text-sm whitespace-pre-wrap pb-4" colspan="6"
|
||||
>{log.text}</td
|
||||
><td
|
||||
class="text-left text-sm whitespace-pre-wrap pb-4"
|
||||
colspan={showCategory ? 8 : 7}>{log.text}</td
|
||||
></tr
|
||||
>
|
||||
{/if}
|
||||
<tr>
|
||||
<td colspan="6"> <hr /> </td>
|
||||
<td colspan={showCategory ? 8 : 7}> <hr class="border-input-border" /> </td>
|
||||
</tr>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
import type { Map } from "leaflet";
|
||||
import { onMount, tick } from "svelte";
|
||||
import { createEventDispatcher, onMount, tick } from "svelte";
|
||||
export let trailFile: File | null;
|
||||
export let trailData: string | undefined;
|
||||
export let label: string = "";
|
||||
@@ -12,6 +12,8 @@
|
||||
let L: any;
|
||||
let layer: any;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: if (trailData !== undefined) {
|
||||
showTrailOnMap();
|
||||
} else {
|
||||
@@ -39,6 +41,8 @@
|
||||
if (trailData) {
|
||||
trailFile = null;
|
||||
trailData = undefined;
|
||||
|
||||
dispatch("change", null)
|
||||
} else {
|
||||
document.getElementById("trail-input")!.click();
|
||||
}
|
||||
@@ -56,6 +60,8 @@
|
||||
|
||||
trailFile = files.item(0);
|
||||
trailData = await trailFile?.text();
|
||||
|
||||
dispatch("change", trailData);
|
||||
}
|
||||
|
||||
async function showTrailOnMap() {
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
"Walking": "Laufen",
|
||||
"about": "Über",
|
||||
"account-delete-confirm": "Du bist dabei, dein Konto zu löschen. Alle deine Routen werden ebenfalls gelöscht. Möchtest du fortfahren?",
|
||||
"activity": "",
|
||||
"add-entry": "Eintrag hinzufügen",
|
||||
"add-to-list": "Zu Liste hinzufügen",
|
||||
"add-waypoint": "Wegpunkt hinzufügen",
|
||||
"added-trail-to": "Route hinzugefügt zu",
|
||||
"all-activities": "",
|
||||
"alphabetical": "Alphabetisch",
|
||||
"already-account": "Du hast bereits ein Konto?",
|
||||
"altitude": "Höhe",
|
||||
@@ -59,6 +61,7 @@
|
||||
"documentation": "Dokumentation",
|
||||
"draw-a-route": "Route zeichnen",
|
||||
"driving": "Auto",
|
||||
"duration": "",
|
||||
"dutch": "Niederländisch",
|
||||
"easy": "Einfach",
|
||||
"edit": "Bearbeiten",
|
||||
@@ -66,6 +69,7 @@
|
||||
"edit-list": "Liste bearbeiten",
|
||||
"edit-waypoint": "Wegpunkt bearbeiten",
|
||||
"elevation-gain": "Höhenunterschied",
|
||||
"elevation-loss": "",
|
||||
"email": "Email",
|
||||
"english": "Englisch",
|
||||
"entry": "Eintrag",
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
"Walking": "Walking",
|
||||
"about": "About",
|
||||
"account-delete-confirm": "You are about to delete your account. All your trails will also be deleted. Do you want to proceed?",
|
||||
"activity": "{n, plural, =1 {Activity} other {Activities}}",
|
||||
"add-entry": "Add Entry",
|
||||
"add-to-list": "Add to list",
|
||||
"add-waypoint": "Add Waypoint",
|
||||
"added-trail-to": "Added trail to",
|
||||
"all-activities": "All activities",
|
||||
"alphabetical": "Alphabetical",
|
||||
"already-account": "Already have an account?",
|
||||
"altitude": "Altitude",
|
||||
@@ -59,6 +61,7 @@
|
||||
"documentation": "Documentation",
|
||||
"draw-a-route": "Draw a route",
|
||||
"driving": "Driving",
|
||||
"duration": "Duration",
|
||||
"dutch": "Dutch",
|
||||
"easy": "Easy",
|
||||
"edit": "Edit",
|
||||
@@ -66,6 +69,7 @@
|
||||
"edit-list": "Edit List",
|
||||
"edit-waypoint": "Edit Waypoint",
|
||||
"elevation-gain": "Elevation Gain",
|
||||
"elevation-loss": "Elevation Loss",
|
||||
"email": "Email",
|
||||
"english": "English",
|
||||
"entry": "Entry",
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
"Walking": "",
|
||||
"about": "Informations",
|
||||
"account-delete-confirm": "Vous êtes sur le point de supprimer votre compte. Toutes vos trails seront également supprimées. Voulez-vous continuer ?",
|
||||
"activity": "",
|
||||
"add-entry": "Ajouter une entrée",
|
||||
"add-to-list": "Ajouter à une liste",
|
||||
"add-waypoint": "Ajouter un point de repère",
|
||||
"added-trail-to": "Ajouter un itinéraire à",
|
||||
"all-activities": "",
|
||||
"alphabetical": "Alphabétique",
|
||||
"already-account": "Déjà un compte ?",
|
||||
"altitude": "Altitude",
|
||||
@@ -59,6 +61,7 @@
|
||||
"documentation": "Documentation",
|
||||
"draw-a-route": "",
|
||||
"driving": "",
|
||||
"duration": "",
|
||||
"dutch": "Néerlandais",
|
||||
"easy": "Facile",
|
||||
"edit": "Editer",
|
||||
@@ -66,6 +69,7 @@
|
||||
"edit-list": "Éditer la liste",
|
||||
"edit-waypoint": "Éditer le point de repère",
|
||||
"elevation-gain": "Gain d'altitude",
|
||||
"elevation-loss": "",
|
||||
"email": "Email",
|
||||
"english": "Anglais",
|
||||
"entry": "Entrée",
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
"Walking": "",
|
||||
"about": "A programról",
|
||||
"account-delete-confirm": "Ön most a profilját készül törölni. Minden nyomvonala törlődik. Szeretné folytatni?",
|
||||
"activity": "",
|
||||
"add-entry": "Bejegyzés hozzáadása",
|
||||
"add-to-list": "Hozzáadás a listához",
|
||||
"add-waypoint": "Útvonalpont hozzáadása",
|
||||
"added-trail-to": "Hozzáadott nyomvonal a",
|
||||
"all-activities": "",
|
||||
"alphabetical": "Betűrendben",
|
||||
"already-account": "Már rendelkezik fiókkal?",
|
||||
"altitude": "Magasság",
|
||||
@@ -59,6 +61,7 @@
|
||||
"documentation": "Dokumentáció",
|
||||
"draw-a-route": "",
|
||||
"driving": "",
|
||||
"duration": "",
|
||||
"dutch": "Holland",
|
||||
"easy": "Könnyű",
|
||||
"edit": "Szerkesztés",
|
||||
@@ -66,6 +69,7 @@
|
||||
"edit-list": "Lista szerkesztése",
|
||||
"edit-waypoint": "Útvonalpont szerkesztése",
|
||||
"elevation-gain": "Magasságnövekedés",
|
||||
"elevation-loss": "",
|
||||
"email": "Email",
|
||||
"english": "Angol",
|
||||
"entry": "Bejegyzés",
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
"Walking": "Camminare",
|
||||
"about": "Su di noi",
|
||||
"account-delete-confirm": "Stai per eliminare il tuo account. Tutti i tuoi percorsi saranno cancellati. Vuoi procedere?",
|
||||
"activity": "",
|
||||
"add-entry": "Aggiungi voce",
|
||||
"add-to-list": "Aggiungi alla lista",
|
||||
"add-waypoint": "Aggiungere un waypoint",
|
||||
"added-trail-to": "Percorso aggiunto a",
|
||||
"all-activities": "",
|
||||
"alphabetical": "Alfabetico",
|
||||
"already-account": "Hai già un account?",
|
||||
"altitude": "Altitudine",
|
||||
@@ -59,6 +61,7 @@
|
||||
"documentation": "Documentazione",
|
||||
"draw-a-route": "Disegna un percorso",
|
||||
"driving": "Guida",
|
||||
"duration": "",
|
||||
"dutch": "Olandese",
|
||||
"easy": "Facile",
|
||||
"edit": "Modifica",
|
||||
@@ -66,6 +69,7 @@
|
||||
"edit-list": "Modifica lista",
|
||||
"edit-waypoint": "Modifica waypoint",
|
||||
"elevation-gain": "Guadagno di quota",
|
||||
"elevation-loss": "",
|
||||
"email": "Email",
|
||||
"english": "Inglese",
|
||||
"entry": "Voce",
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
"Walking": "",
|
||||
"about": "Over",
|
||||
"account-delete-confirm": "Je staat op het punt je account te verwijderen. Al je wandelroutes worden hierdoor eveneens verwijderd. Wil je doorgaan?",
|
||||
"activity": "",
|
||||
"add-entry": "Item toevoegen",
|
||||
"add-to-list": "Toevoegen aan lijst",
|
||||
"add-waypoint": "Routepunt toevoegen",
|
||||
"added-trail-to": "Wandelroute toegevoegd aan",
|
||||
"all-activities": "",
|
||||
"alphabetical": "Alfabetisch",
|
||||
"already-account": "Heb je al een account?",
|
||||
"altitude": "Hoogte",
|
||||
@@ -59,6 +61,7 @@
|
||||
"documentation": "Documentatie",
|
||||
"draw-a-route": "",
|
||||
"driving": "",
|
||||
"duration": "",
|
||||
"dutch": "Nederlands",
|
||||
"easy": "Makkelijk",
|
||||
"edit": "Bewerken",
|
||||
@@ -66,6 +69,7 @@
|
||||
"edit-list": "Lijst aanpassen",
|
||||
"edit-waypoint": "Routepunt aanpassen",
|
||||
"elevation-gain": "Hoogteverschil",
|
||||
"elevation-loss": "",
|
||||
"email": "E-mail",
|
||||
"english": "Engels",
|
||||
"entry": "Item",
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
"Walking": "",
|
||||
"about": "Na temat",
|
||||
"account-delete-confirm": "Usuniesz swoje konto. Wszystkie twoje ścieżki zostaną usunięte. Czy chcesz kontynuować?",
|
||||
"activity": "",
|
||||
"add-entry": "Dodaj Pozycję",
|
||||
"add-to-list": "Dodaj do listy",
|
||||
"add-waypoint": "Dodaj Punkt",
|
||||
"added-trail-to": "Dodaj ścieżkę do",
|
||||
"all-activities": "",
|
||||
"alphabetical": "Alfabetyczne",
|
||||
"already-account": "Czy masz już konto?",
|
||||
"altitude": "Wysokość",
|
||||
@@ -59,6 +61,7 @@
|
||||
"documentation": "Dokumentacja",
|
||||
"draw-a-route": "",
|
||||
"driving": "",
|
||||
"duration": "",
|
||||
"dutch": "Niderlandzki",
|
||||
"easy": "Łatwy",
|
||||
"edit": "Edytuj",
|
||||
@@ -66,6 +69,7 @@
|
||||
"edit-list": "Edytuj Listę",
|
||||
"edit-waypoint": "Edytuj Punkt",
|
||||
"elevation-gain": "Wzrost Wysokości",
|
||||
"elevation-loss": "",
|
||||
"email": "Email",
|
||||
"english": "Angielski",
|
||||
"entry": "Pozycja",
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
"Walking": "",
|
||||
"about": "Sobre",
|
||||
"account-delete-confirm": "Você está prestes a excluir sua conta. Todas as suas trilhas também serão excluídas. Queres prosseguir?",
|
||||
"activity": "",
|
||||
"add-entry": "Adicionar entrada",
|
||||
"add-to-list": "Adicionar à lista",
|
||||
"add-waypoint": "Adicionar ponto de vista",
|
||||
"added-trail-to": "Trilha adicionada para",
|
||||
"all-activities": "",
|
||||
"alphabetical": "Alfabético",
|
||||
"already-account": "Já tem uma conta?",
|
||||
"altitude": "Altitude",
|
||||
@@ -59,6 +61,7 @@
|
||||
"documentation": "Documentação",
|
||||
"draw-a-route": "",
|
||||
"driving": "",
|
||||
"duration": "",
|
||||
"dutch": "Holandês",
|
||||
"easy": "Fácil",
|
||||
"edit": "Editar",
|
||||
@@ -66,6 +69,7 @@
|
||||
"edit-list": "Editar lista",
|
||||
"edit-waypoint": "Editar ponto de passagem",
|
||||
"elevation-gain": "Ganho de elevação",
|
||||
"elevation-loss": "",
|
||||
"email": "Email",
|
||||
"english": "Inglês",
|
||||
"entry": "Entrada",
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
"Walking": "",
|
||||
"about": "关于",
|
||||
"account-delete-confirm": "您现在要删除当前账户,所有的路线都会删除无法恢复,确认继续操作吗?",
|
||||
"activity": "",
|
||||
"add-entry": "添加日程",
|
||||
"add-to-list": "添加到列表",
|
||||
"add-waypoint": "添加坐标",
|
||||
"added-trail-to": "添加路线到",
|
||||
"all-activities": "",
|
||||
"alphabetical": "字母",
|
||||
"already-account": "已注册账户?",
|
||||
"altitude": "海拔",
|
||||
@@ -59,6 +61,7 @@
|
||||
"documentation": "文档",
|
||||
"draw-a-route": "",
|
||||
"driving": "",
|
||||
"duration": "",
|
||||
"dutch": "荷兰语",
|
||||
"easy": "简单",
|
||||
"edit": "编辑",
|
||||
@@ -66,6 +69,7 @@
|
||||
"edit-list": "编辑列表",
|
||||
"edit-waypoint": "编辑坐标",
|
||||
"elevation-gain": "上升海拔",
|
||||
"elevation-loss": "",
|
||||
"email": "电子邮箱",
|
||||
"english": "英语",
|
||||
"entry": "日程",
|
||||
|
||||
@@ -74,6 +74,7 @@ export default class GPX {
|
||||
|
||||
getTotals() {
|
||||
let totalElevationGain = 0;
|
||||
let totalElevationLoss = 0;
|
||||
let totalDuration = 0;
|
||||
let totalDistance = 0;
|
||||
|
||||
@@ -99,6 +100,8 @@ export default class GPX {
|
||||
const elevationDiff = elevation - previousElevation;
|
||||
if (elevationDiff > 0) {
|
||||
totalElevationGain += elevationDiff;
|
||||
} else {
|
||||
totalElevationLoss += Math.abs(elevationDiff)
|
||||
}
|
||||
|
||||
const distance = calculateDistance(
|
||||
@@ -112,7 +115,7 @@ export default class GPX {
|
||||
}
|
||||
}
|
||||
|
||||
return { distance: totalDistance, elevationGain: totalElevationGain, duration: totalDuration }
|
||||
return { distance: totalDistance, elevationGain: totalElevationGain, elevationLoss: totalElevationLoss, duration: totalDuration }
|
||||
}
|
||||
|
||||
static parse(gpxString: string): Promise<GPX | Error> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Trail } from "./trail";
|
||||
|
||||
class SummitLog {
|
||||
id?: string;
|
||||
@@ -5,10 +6,15 @@ class SummitLog {
|
||||
text?: string;
|
||||
gpx?: string;
|
||||
_gpx: File | null;
|
||||
distance?: number
|
||||
elevation_gain?: number
|
||||
elevation_loss?: number
|
||||
duration?: number
|
||||
author?: string;
|
||||
|
||||
expand: {
|
||||
gpx_data?: string;
|
||||
trails_via_summit_logs?: Trail[];
|
||||
}
|
||||
|
||||
constructor(date: string, params?: { id?: string, text?: string }) {
|
||||
|
||||
@@ -7,4 +7,5 @@ export type User = {
|
||||
password: string,
|
||||
avatar?: string;
|
||||
language?: string;
|
||||
created?: string;
|
||||
}
|
||||
@@ -4,6 +4,23 @@ import { ClientResponseError } from "pocketbase";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
|
||||
export const summitLog: Writable<SummitLog> = writable(new SummitLog(new Date().toISOString().substring(0, 10)));
|
||||
export const summitLogs: Writable<SummitLog[]> = writable([]);
|
||||
|
||||
export async function summit_logs_index(f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
const r = await f('/api/v1/summit-log?', {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
throw new ClientResponseError(await r.json())
|
||||
}
|
||||
|
||||
const fetchedSummitLogs: SummitLog[] = await r.json();
|
||||
|
||||
summitLogs.set(fetchedSummitLogs);
|
||||
|
||||
return fetchedSummitLogs;
|
||||
}
|
||||
|
||||
export async function summit_logs_create(summitLog: SummitLog) {
|
||||
summitLog.author = pb.authStore.model!.id
|
||||
|
||||
@@ -154,7 +154,7 @@ export async function trails_show(id: string, loadGPX?: boolean, f: (url: Reques
|
||||
response.expand.gpx_data = gpxData;
|
||||
|
||||
|
||||
for (const log of response.expand.summit_logs) {
|
||||
for (const log of response.expand.summit_logs ?? []) {
|
||||
const gpxData: string = await fetchGPX(log, f);
|
||||
|
||||
if (!log.expand) {
|
||||
@@ -165,7 +165,7 @@ export async function trails_show(id: string, loadGPX?: boolean, f: (url: Reques
|
||||
}
|
||||
|
||||
response.expand.waypoints = response.expand.waypoints || [];
|
||||
response.expand.summit_logs = response.expand.summit_logs.sort((a: SummitLog, b: SummitLog) => Date.parse(a.date) - Date.parse(b.date)) || [];
|
||||
response.expand.summit_logs = response.expand.summit_logs?.sort((a: SummitLog, b: SummitLog) => Date.parse(a.date) - Date.parse(b.date)) || [];
|
||||
|
||||
trail.set(response);
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Settings } from "$lib/models/settings";
|
||||
import type { User } from "$lib/models/user";
|
||||
import { pb } from "$lib/pocketbase";
|
||||
import { ClientResponseError, type AuthMethodsList } from "pocketbase";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { settings_create } from "./settings_store";
|
||||
|
||||
export const currentUser: Writable<User | null> = writable<User | null>()
|
||||
|
||||
|
||||
28
web/src/lib/util/date_util.ts
Normal file
28
web/src/lib/util/date_util.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export function isToday(date: Date) {
|
||||
const today = new Date();
|
||||
return date.setHours(0, 0, 0, 0) == today.setHours(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
export function dateExistsInList(targetDate: Date, dateList: Date[]): boolean {
|
||||
const targetYear = targetDate.getFullYear();
|
||||
const targetMonth = targetDate.getMonth();
|
||||
const targetDay = targetDate.getDate();
|
||||
|
||||
for (const date of dateList) {
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth();
|
||||
const day = date.getDate();
|
||||
|
||||
if (year === targetYear && month === targetMonth && day === targetDay) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isSameDay(d1: Date, d2: Date) {
|
||||
return d1.getFullYear() === d2.getFullYear() &&
|
||||
d1.getMonth() === d2.getMonth() &&
|
||||
d1.getDate() === d2.getDate();
|
||||
}
|
||||
@@ -4,7 +4,10 @@ import { error, json, type RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
export async function GET(event: RequestEvent) {
|
||||
try {
|
||||
const r: SummitLog[] = await pb.collection('summit_logs').getFullList<SummitLog>()
|
||||
const r: SummitLog[] = await pb.collection('summit_logs').getFullList<SummitLog>({
|
||||
expand: "trails_via_summit_logs.category",
|
||||
sort: "+date"
|
||||
})
|
||||
return json(r)
|
||||
} catch (e: any) {
|
||||
throw error(e.status, e);
|
||||
|
||||
306
web/src/routes/profile/+page.svelte
Normal file
306
web/src/routes/profile/+page.svelte
Normal file
@@ -0,0 +1,306 @@
|
||||
<script lang="ts">
|
||||
import Calendar from "$lib/components/base/calendar.svelte";
|
||||
import Select, {
|
||||
type SelectItem,
|
||||
} from "$lib/components/base/select.svelte";
|
||||
import SummitLogTable from "$lib/components/summit_log/summit_log_table.svelte";
|
||||
import { summitLogs } from "$lib/stores/summit_log_store";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import {
|
||||
ArcElement,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
Chart as ChartJS,
|
||||
Legend,
|
||||
LinearScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "chart.js";
|
||||
import { Bar, Pie } from "svelte-chartjs";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ArcElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
BarElement,
|
||||
);
|
||||
|
||||
const barChartSelectItems: SelectItem[] = [
|
||||
{
|
||||
text: $_("distance"),
|
||||
value: "distance",
|
||||
},
|
||||
{
|
||||
text: $_("duration"),
|
||||
value: "duration",
|
||||
},
|
||||
{
|
||||
text: $_("elevation-gain"),
|
||||
value: "elevation_gain",
|
||||
},
|
||||
{
|
||||
text: $_("elevation-loss"),
|
||||
value: "elevation_loss",
|
||||
},
|
||||
];
|
||||
|
||||
let barChartSelectedValue = barChartSelectItems[0].value;
|
||||
|
||||
const categoryColors = [
|
||||
"#fb8500",
|
||||
"#ffb703",
|
||||
"#06d6a0",
|
||||
"#219ebc",
|
||||
"#8ecae6",
|
||||
"#ffafcc",
|
||||
];
|
||||
|
||||
$: categories = $summitLogs.reduce(
|
||||
(acc, log) => {
|
||||
const cat =
|
||||
log.expand.trails_via_summit_logs?.at(0)?.expand.category
|
||||
?.name ?? "unknown";
|
||||
acc[$_(cat)] = (acc[$_(cat)] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
|
||||
$: categoryLabels = Object.keys(categories).sort();
|
||||
$: categoryValues = Object.values(categories);
|
||||
$: categoryColorMap = Object.fromEntries(
|
||||
categoryLabels.map((label, index) => [
|
||||
label,
|
||||
categoryColors[index % categoryColors.length],
|
||||
]),
|
||||
);
|
||||
|
||||
$: categoryChartData = {
|
||||
labels: categoryLabels,
|
||||
datasets: [
|
||||
{
|
||||
data: categoryValues,
|
||||
backgroundColor: categoryColors,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
$: barChartUnit =
|
||||
barChartSelectedValue == "duration"
|
||||
? "min"
|
||||
: barChartSelectedValue == "distance"
|
||||
? "km"
|
||||
: "m";
|
||||
|
||||
$: barChartDataByDate = $summitLogs.reduce(
|
||||
(acc, log) => {
|
||||
const date = new Date(log.date).toLocaleDateString(undefined, {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
});
|
||||
acc[date] =
|
||||
(acc[date] || 0) + ((log as any)[barChartSelectedValue] ?? 0);
|
||||
if (barChartSelectedValue === "distance") {
|
||||
acc[date] = acc[date] / 1000;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
|
||||
$: barChartLabels = Object.keys(barChartDataByDate); // Dates as labels
|
||||
$: barChartValues = Object.values(barChartDataByDate);
|
||||
|
||||
$: barChartData = {
|
||||
labels: barChartLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: barChartSelectItems.find(
|
||||
(i) => i.value === barChartSelectedValue,
|
||||
)?.text,
|
||||
data: barChartValues,
|
||||
backgroundColor: "#3388ff",
|
||||
borderRadius: 15,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
$: totalDistance = $summitLogs.reduce(
|
||||
(sum, log) => sum + (log.distance ?? 0),
|
||||
0,
|
||||
);
|
||||
|
||||
$: totalDuration = $summitLogs.reduce(
|
||||
(sum, log) => sum + (log.duration ?? 0),
|
||||
0,
|
||||
);
|
||||
|
||||
$: totalElevationGain = $summitLogs.reduce(
|
||||
(sum, log) => sum + (log.elevation_gain ?? 0),
|
||||
0,
|
||||
);
|
||||
|
||||
$: totalElevationLoss = $summitLogs.reduce(
|
||||
(sum, log) => sum + (log.elevation_loss ?? 0),
|
||||
0,
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-5 gap-4 items-start max-w-6xl mx-auto">
|
||||
<div
|
||||
class="border border-input-border rounded-xl p-6 col-span-2 row-span-2 space-y-6"
|
||||
>
|
||||
{#if $currentUser}
|
||||
<div class="flex items-center gap-x-6">
|
||||
<img
|
||||
class="rounded-full w-24 aspect-square overflow-hidden"
|
||||
src={getFileURL($currentUser, $currentUser.avatar) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-2xl font-semibold col-start-2">
|
||||
{$currentUser.username}
|
||||
</h4>
|
||||
<p class="text-sm">
|
||||
<span class="text-gray-500">Joined:</span>
|
||||
{new Date(
|
||||
$currentUser.created ?? "",
|
||||
).toLocaleDateString(undefined, {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
timeZone: "UTC",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Calendar logs={$summitLogs} colorMap={categoryColorMap}></Calendar>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center gap-6 border border-input-border rounded-xl p-6"
|
||||
>
|
||||
<span class="text-gray-500 font-semibold text-lg self-start"
|
||||
><i class="fa fa-hashtag mr-3"></i>{$_("activity", {
|
||||
values: { n: 2 },
|
||||
})}</span
|
||||
>
|
||||
<p class="text-4xl font-bold">{$summitLogs.length}</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center gap-6 border border-input-border rounded-xl p-6"
|
||||
>
|
||||
<span class="text-gray-500 font-semibold text-lg self-start"
|
||||
><i class="fa fa-left-right mr-3"></i>{$_("distance")}</span
|
||||
>
|
||||
<p class="text-4xl font-bold">{formatDistance(totalDistance)}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-center gap-6 border border-input-border rounded-xl p-6"
|
||||
>
|
||||
<span class="text-gray-500 font-semibold text-lg self-start"
|
||||
><i class="fa fa-clock mr-3"></i>{$_("duration")}</span
|
||||
>
|
||||
<p class="text-4xl font-bold">{formatTimeHHMM(totalDuration)}</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center gap-6 border border-input-border rounded-xl p-6"
|
||||
>
|
||||
<span class="text-gray-500 font-semibold text-lg self-start"
|
||||
><i class="fa fa-arrow-trend-up mr-3"></i>{$_(
|
||||
"elevation-gain",
|
||||
)}</span
|
||||
>
|
||||
<p class="text-4xl font-bold">{formatElevation(totalElevationGain)}</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center gap-6 border border-input-border rounded-xl p-6"
|
||||
>
|
||||
<span class="text-gray-500 font-semibold text-lg self-start"
|
||||
><i class="fa fa-arrow-trend-down mr-3"></i>{$_(
|
||||
"elevation-loss",
|
||||
)}</span
|
||||
>
|
||||
<p class="text-4xl font-bold">{formatElevation(totalElevationLoss)}</p>
|
||||
</div>
|
||||
<div
|
||||
class="col-start-1 col-span-1 border border-input-border rounded-xl p-6 space-y-4"
|
||||
>
|
||||
<span class="text-gray-500 font-semibold text-lg"
|
||||
><i class="fa fa-person-hiking mr-3"></i>{$_("categories")}</span
|
||||
>
|
||||
<Pie
|
||||
data={categoryChartData}
|
||||
options={{
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "bottom",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="h-full col-span-3 space-y-2 border border-input-border rounded-xl p-6"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 font-semibold text-lg"
|
||||
><i class="fa fa-calendar mr-3"></i>{$_("activity", {
|
||||
values: { n: 1 },
|
||||
})}</span
|
||||
>
|
||||
<Select
|
||||
bind:value={barChartSelectedValue}
|
||||
items={barChartSelectItems}
|
||||
></Select>
|
||||
</div>
|
||||
|
||||
<Bar
|
||||
data={barChartData}
|
||||
options={{
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (item) =>
|
||||
`${item.dataset.label}: ${item.formattedValue} ${barChartUnit}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
ticks: {
|
||||
callback: function (value, index, ticks) {
|
||||
return value + " " + barChartUnit;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
></Bar>
|
||||
</div>
|
||||
|
||||
<div class="col-span-5 border border-input-border rounded-xl p-6 space-y-6">
|
||||
<span class="text-gray-500 font-semibold text-lg"
|
||||
><i class="fa fa-table mr-3"></i>{$_("all-activities")}</span
|
||||
>
|
||||
<SummitLogTable summitLogs={$summitLogs} showCategory></SummitLogTable>
|
||||
</div>
|
||||
</div>
|
||||
23
web/src/routes/profile/+page.ts
Normal file
23
web/src/routes/profile/+page.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { summit_logs_index, summitLogs } from "$lib/stores/summit_log_store";
|
||||
import { fetchGPX } from "$lib/stores/trail_store";
|
||||
import { type ServerLoad } from "@sveltejs/kit";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
export const load: ServerLoad = async ({ params, locals, fetch }) => {
|
||||
const logs = await summit_logs_index(fetch);
|
||||
|
||||
for (const log of logs) {
|
||||
if (!log.gpx) {
|
||||
continue
|
||||
}
|
||||
const gpxData: string = await fetchGPX(log as any, fetch);
|
||||
|
||||
if (!log.expand) {
|
||||
log.expand = {};
|
||||
}
|
||||
log.expand.gpx_data = gpxData;
|
||||
}
|
||||
|
||||
console.log(logs);
|
||||
|
||||
};
|
||||
@@ -428,7 +428,6 @@
|
||||
|
||||
function saveSummitLog(e: CustomEvent<SummitLog>) {
|
||||
const savedSummitLog = e.detail;
|
||||
console.log(savedSummitLog);
|
||||
|
||||
let editedSummitLogIndex = $form.expand.summit_logs.findIndex(
|
||||
(s) => s.id == savedSummitLog.id,
|
||||
|
||||
Reference in New Issue
Block a user