89
db/migrations/1725619780_updated_trails.go
Normal file
89
db/migrations/1725619780_updated_trails.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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("e864strfxo14pm4")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update
|
||||||
|
edit_photos := &schema.SchemaField{}
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"system": false,
|
||||||
|
"id": "aqbpyawe",
|
||||||
|
"name": "photos",
|
||||||
|
"type": "file",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"mimeTypes": [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/vnd.mozilla.apng",
|
||||||
|
"image/png",
|
||||||
|
"image/webp",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/heic"
|
||||||
|
],
|
||||||
|
"thumbs": [],
|
||||||
|
"maxSelect": 99,
|
||||||
|
"maxSize": 5242880,
|
||||||
|
"protected": false
|
||||||
|
}
|
||||||
|
}`), edit_photos); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection.Schema.AddField(edit_photos)
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
}, func(db dbx.Builder) error {
|
||||||
|
dao := daos.New(db);
|
||||||
|
|
||||||
|
collection, err := dao.FindCollectionByNameOrId("e864strfxo14pm4")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update
|
||||||
|
edit_photos := &schema.SchemaField{}
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"system": false,
|
||||||
|
"id": "aqbpyawe",
|
||||||
|
"name": "photos",
|
||||||
|
"type": "file",
|
||||||
|
"required": false,
|
||||||
|
"presentable": false,
|
||||||
|
"unique": false,
|
||||||
|
"options": {
|
||||||
|
"mimeTypes": [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/vnd.mozilla.apng",
|
||||||
|
"image/png",
|
||||||
|
"image/webp",
|
||||||
|
"image/svg+xml"
|
||||||
|
],
|
||||||
|
"thumbs": [],
|
||||||
|
"maxSelect": 99,
|
||||||
|
"maxSize": 5242880,
|
||||||
|
"protected": false
|
||||||
|
}
|
||||||
|
}`), edit_photos); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collection.Schema.AddField(edit_photos)
|
||||||
|
|
||||||
|
return dao.SaveCollection(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"@types/xmldom": "^0.1.34",
|
"@types/xmldom": "^0.1.34",
|
||||||
"canvg": "^4.0.1",
|
"canvg": "^4.0.1",
|
||||||
"crypto-random-string": "^5.0.0",
|
"crypto-random-string": "^5.0.0",
|
||||||
|
"heic2any": "^0.0.4",
|
||||||
"instead": "^1.0.3",
|
"instead": "^1.0.3",
|
||||||
"isomorphic-xml2js": "^0.1.3",
|
"isomorphic-xml2js": "^0.1.3",
|
||||||
"jspdf": "^2.5.1",
|
"jspdf": "^2.5.1",
|
||||||
@@ -2813,6 +2814,11 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/heic2any": {
|
||||||
|
"version": "0.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/heic2any/-/heic2any-0.0.4.tgz",
|
||||||
|
"integrity": "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA=="
|
||||||
|
},
|
||||||
"node_modules/html-encoding-sniffer": {
|
"node_modules/html-encoding-sniffer": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"@types/xmldom": "^0.1.34",
|
"@types/xmldom": "^0.1.34",
|
||||||
"canvg": "^4.0.1",
|
"canvg": "^4.0.1",
|
||||||
"crypto-random-string": "^5.0.0",
|
"crypto-random-string": "^5.0.0",
|
||||||
|
"heic2any": "^0.0.4",
|
||||||
"instead": "^1.0.3",
|
"instead": "^1.0.3",
|
||||||
"isomorphic-xml2js": "^0.1.3",
|
"isomorphic-xml2js": "^0.1.3",
|
||||||
"jspdf": "^2.5.1",
|
"jspdf": "^2.5.1",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getFileURL, readAsDataURLAsync } from "$lib/util/file_util";
|
import { getFileURL, readAsDataURLAsync } from "$lib/util/file_util";
|
||||||
|
import { onMount } from "svelte";
|
||||||
import PhotoCard from "../photo_card.svelte";
|
import PhotoCard from "../photo_card.svelte";
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
document.getElementById(`${id}-photo-input`)!.click();
|
document.getElementById(`${id}-photo-input`)!.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePhotoSelection(files?: FileList | null) {
|
async function handlePhotoSelection(files?: FileList | null) {
|
||||||
if (!files) {
|
if (!files) {
|
||||||
files = (
|
files = (
|
||||||
document.getElementById(`${id}-photo-input`) as HTMLInputElement
|
document.getElementById(`${id}-photo-input`) as HTMLInputElement
|
||||||
@@ -54,13 +55,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
let photoFile = file;
|
||||||
if (!file.type.startsWith("image")) {
|
if (!file.type.startsWith("image")) {
|
||||||
continue;
|
continue;
|
||||||
|
} else if (file.type === "image/heic") {
|
||||||
|
const heic2any = (await import("heic2any")).default
|
||||||
|
photoFile = new File(
|
||||||
|
[
|
||||||
|
(await heic2any({
|
||||||
|
blob: file,
|
||||||
|
toType: "image/jpeg",
|
||||||
|
})) as Blob,
|
||||||
|
],
|
||||||
|
file.name,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!photoFiles) {
|
if (!photoFiles) {
|
||||||
photoFiles = [];
|
photoFiles = [];
|
||||||
}
|
}
|
||||||
photoFiles = [...photoFiles, file];
|
photoFiles = [...photoFiles, photoFile];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
$: thumbnail = trail.photos.length
|
$: thumbnail = trail.photos.length
|
||||||
? getFileURL(trail, trail.photos[trail.thumbnail])
|
? getFileURL(trail, trail.photos[trail.thumbnail])
|
||||||
: "/imgs/default_thumbnail.webp";
|
: "/imgs/default_thumbnail.webp";
|
||||||
|
|
||||||
|
$: trailIsShared = (trail.expand?.trail_share_via_trail?.length ?? 0) > 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -27,12 +29,18 @@
|
|||||||
>
|
>
|
||||||
<img src={thumbnail} alt="" />
|
<img src={thumbnail} alt="" />
|
||||||
</div>
|
</div>
|
||||||
{#if trail.public || trail.expand?.trail_share_via_trail?.length}
|
{#if trail.public || trailIsShared}
|
||||||
<div
|
<div
|
||||||
class="flex absolute top-4 right-4 w-8 h-8 rounded-full items-center justify-center bg-background text-content"
|
class="flex absolute top-4 right-4 {trail.public && trailIsShared
|
||||||
|
? 'w-14'
|
||||||
|
: 'w-8'} h-8 rounded-full items-center justify-center bg-background text-content"
|
||||||
>
|
>
|
||||||
{#if trail.public}
|
{#if trail.public}
|
||||||
<span class="tooltip" data-title={$_("public")}>
|
<span
|
||||||
|
class="tooltip"
|
||||||
|
class:mr-2={trail.public && trailIsShared}
|
||||||
|
data-title={$_("public")}
|
||||||
|
>
|
||||||
<i class="fa fa-globe"></i>
|
<i class="fa fa-globe"></i>
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -109,14 +109,23 @@
|
|||||||
gpx(new DOMParser().parseFromString(fileData, "text/xml")),
|
gpx(new DOMParser().parseFromString(fileData, "text/xml")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!exportSettings.photos && !exportSettings.summitLog) {
|
||||||
|
const blob = new Blob([fileData], {
|
||||||
|
type: "text/plain",
|
||||||
|
});
|
||||||
|
saveAs(blob, `${trail.name}.${exportSettings.fileFormat}`);
|
||||||
|
} else {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
zip.file(`${trail.name}.${exportSettings.fileFormat}`, fileData);
|
zip.file(
|
||||||
|
`${trail.name}.${exportSettings.fileFormat}`,
|
||||||
|
fileData,
|
||||||
|
);
|
||||||
if (exportSettings.photos) {
|
if (exportSettings.photos) {
|
||||||
const photoFolder = zip.folder($_("photos"));
|
const photoFolder = zip.folder($_("photos"));
|
||||||
for (const photo of trail.photos) {
|
for (const photo of trail.photos) {
|
||||||
const photoURL = getFileURL(trail, photo);
|
const photoURL = getFileURL(trail, photo);
|
||||||
const photoBlob = await fetch(photoURL).then((response) =>
|
const photoBlob = await fetch(photoURL).then(
|
||||||
response.blob(),
|
(response) => response.blob(),
|
||||||
);
|
);
|
||||||
const photoData = new File([photoBlob], photo);
|
const photoData = new File([photoBlob], photo);
|
||||||
photoFolder?.file(photo, photoData, { base64: true });
|
photoFolder?.file(photo, photoData, { base64: true });
|
||||||
@@ -134,6 +143,7 @@
|
|||||||
}
|
}
|
||||||
const blob = await zip.generateAsync({ type: "blob" });
|
const blob = await zip.generateAsync({ type: "blob" });
|
||||||
saveAs(blob, `${trail.name}.zip`);
|
saveAs(blob, `${trail.name}.zip`);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
show_toast({
|
show_toast({
|
||||||
|
|||||||
@@ -47,6 +47,9 @@
|
|||||||
...($currentUser ? [$_("comment", { values: { n: 2 } })] : []),
|
...($currentUser ? [$_("comment", { values: { n: 2 } })] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const trailIsShared =
|
||||||
|
(trail.expand?.trail_share_via_trail?.length ?? 0) > 0;
|
||||||
|
|
||||||
let map: Map;
|
let map: Map;
|
||||||
|
|
||||||
let activeTab = 0;
|
let activeTab = 0;
|
||||||
@@ -161,7 +164,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="trail-info-panel mx-auto border border-input-border rounded-3xl h-full" style="max-width: min(100%, 64rem);"
|
class="trail-info-panel mx-auto border border-input-border rounded-3xl h-full"
|
||||||
|
style="max-width: min(100%, 64rem);"
|
||||||
>
|
>
|
||||||
<div class="trail-info-panel-header">
|
<div class="trail-info-panel-header">
|
||||||
<section class="relative h-80">
|
<section class="relative h-80">
|
||||||
@@ -169,19 +173,23 @@
|
|||||||
<div
|
<div
|
||||||
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
|
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
|
||||||
></div>
|
></div>
|
||||||
{#if trail.public || trail.expand?.trail_share_via_trail?.length}
|
{#if trail.public || trailIsShared}
|
||||||
<div
|
<div
|
||||||
class="flex absolute top-8 right-10 w-8 h-8 rounded-full items-center justify-center bg-white text-primary"
|
class="flex absolute top-8 right-6 {trail.public &&
|
||||||
|
trailIsShared
|
||||||
|
? 'w-16'
|
||||||
|
: 'w-8'} h-8 rounded-full items-center justify-center bg-white text-primary"
|
||||||
>
|
>
|
||||||
{#if trail.public}
|
{#if trail.public}
|
||||||
<span
|
<span
|
||||||
class="tooltip text-2xl"
|
class="tooltip text-2xl"
|
||||||
|
class:mr-3={trail.public && trailIsShared}
|
||||||
data-title={$_("public")}
|
data-title={$_("public")}
|
||||||
>
|
>
|
||||||
<i class="fa fa-globe"></i>
|
<i class="fa fa-globe"></i>
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if trail.expand?.trail_share_via_trail?.length}
|
{#if trailIsShared}
|
||||||
<TrailShareInfo {trail} large={true}></TrailShareInfo>
|
<TrailShareInfo {trail} large={true}></TrailShareInfo>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -266,9 +274,7 @@
|
|||||||
<hr class="border-separator" />
|
<hr class="border-separator" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<section
|
<section class="trail-info-panel-tabs px-4 py-2 bg-background sticky top-0">
|
||||||
class="trail-info-panel-tabs px-4 py-2 bg-background sticky top-0"
|
|
||||||
>
|
|
||||||
<Tabs {tabs} bind:activeTab></Tabs>
|
<Tabs {tabs} bind:activeTab></Tabs>
|
||||||
</section>
|
</section>
|
||||||
<section class="trail-info-panel-content px-8">
|
<section class="trail-info-panel-content px-8">
|
||||||
|
|||||||
@@ -42,10 +42,10 @@
|
|||||||
selectedDisplayOption = storedDisplayOption;
|
selectedDisplayOption = storedDisplayOption;
|
||||||
}
|
}
|
||||||
if (storedSort) {
|
if (storedSort) {
|
||||||
filter.sort = storedSort as typeof filter.sort
|
filter.sort = storedSort as typeof filter.sort;
|
||||||
}
|
}
|
||||||
if (storedSortOrder) {
|
if (storedSortOrder) {
|
||||||
filter.sortOrder = storedSortOrder as typeof filter.sortOrder
|
filter.sortOrder = storedSortOrder as typeof filter.sortOrder;
|
||||||
}
|
}
|
||||||
if (storedSort || storedSortOrder) {
|
if (storedSort || storedSortOrder) {
|
||||||
dispatch("update", filter);
|
dispatch("update", filter);
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setSort() {
|
function setSort() {
|
||||||
localStorage.setItem("sort", filter.sort)
|
localStorage.setItem("sort", filter.sort);
|
||||||
dispatch("update", filter);
|
dispatch("update", filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
} else {
|
} else {
|
||||||
filter.sortOrder = "+";
|
filter.sortOrder = "+";
|
||||||
}
|
}
|
||||||
localStorage.setItem("sort_order", filter.sortOrder)
|
localStorage.setItem("sort_order", filter.sortOrder);
|
||||||
dispatch("update", filter);
|
dispatch("update", filter);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -130,6 +130,11 @@
|
|||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
<Pagination
|
||||||
|
page={pagination.page}
|
||||||
|
totalPages={pagination.totalPages}
|
||||||
|
on:pagination
|
||||||
|
></Pagination>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
|
||||||
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
|
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
|
||||||
<div id="trail-details" class=" sticky top-0 min-h-[600px]">
|
<div id="trail-details" class=" sticky top-[62px]">
|
||||||
<MapWithElevation trail={$trail} bind:markers></MapWithElevation>
|
<MapWithElevation trail={$trail} bind:markers></MapWithElevation>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<style>
|
<style>
|
||||||
@media only screen and (min-width: 768px) {
|
@media only screen and (min-width: 768px) {
|
||||||
#trail-details {
|
#trail-details {
|
||||||
max-height: calc(100vh - 124px);
|
height: calc(100vh - 124px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ export const load: ServerLoad = async ({ params, locals, url, fetch }) => {
|
|||||||
if (paramCategory) {
|
if (paramCategory) {
|
||||||
filter.category.push(paramCategory);
|
filter.category.push(paramCategory);
|
||||||
}
|
}
|
||||||
const response = await trails_search_filter(filter, 1, fetch);
|
const page = url.searchParams.get("page") ?? "1";
|
||||||
|
|
||||||
|
const response = await trails_search_filter(filter, parseInt(page), fetch);
|
||||||
await categories_index(fetch)
|
await categories_index(fetch)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
|
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
|
||||||
import TrailList from "$lib/components/trail/trail_list.svelte";
|
import TrailList from "$lib/components/trail/trail_list.svelte";
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function handleFilterUpdate() {
|
async function handleFilterUpdate() {
|
||||||
const response = await trails_search_filter(filter, 1);
|
const response = await trails_search_filter(filter, pagination.page);
|
||||||
pagination.page = response.page;
|
pagination.page = response.page;
|
||||||
pagination.totalPages = response.totalPages;
|
pagination.totalPages = response.totalPages;
|
||||||
}
|
}
|
||||||
@@ -29,6 +30,8 @@
|
|||||||
async function paginate(page: number) {
|
async function paginate(page: number) {
|
||||||
pagination.page = page;
|
pagination.page = page;
|
||||||
const response = await trails_search_filter(filter, page);
|
const response = await trails_search_filter(filter, page);
|
||||||
|
$page.url.searchParams.set("page", page.toString());
|
||||||
|
goto(`?${$page.url.searchParams.toString()}`);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -37,7 +40,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main
|
<main
|
||||||
class="grid grid-cols-1 md:grid-cols-[300px_1fr] gap-8 max-w-7xl mx-6 md:mx-auto"
|
class="grid grid-cols-1 md:grid-cols-[300px_1fr] items-start gap-8 max-w-7xl mx-6 md:mx-auto"
|
||||||
>
|
>
|
||||||
<TrailFilterPanel
|
<TrailFilterPanel
|
||||||
categories={$categories}
|
categories={$categories}
|
||||||
|
|||||||
Reference in New Issue
Block a user