adds profile page
This commit is contained in:
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;
|
||||
|
||||
@@ -26,7 +27,7 @@
|
||||
initialValues: $summitLog,
|
||||
validationSchema: summitLogSchema,
|
||||
onSubmit: async (submittedValues) => {
|
||||
if(!$form.expand.gpx_data) {
|
||||
if (!$form.expand.gpx_data) {
|
||||
$form.gpx = "";
|
||||
}
|
||||
dispatch("save", submittedValues);
|
||||
@@ -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>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
autofitBounds: options.autofitBounds ?? true,
|
||||
});
|
||||
controlElevation.clear();
|
||||
controlElevation.load(gpxData);
|
||||
controlElevation.load(gpxData);
|
||||
}
|
||||
|
||||
$: if (options) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user