adds trails page

This commit is contained in:
Christian Beutel
2024-02-04 22:48:04 +01:00
parent c370690747
commit ec33723bdc
19 changed files with 799 additions and 30 deletions

View File

@@ -0,0 +1,54 @@
<script lang="ts">
import * as noUiSlider from "noUiSlider";
import "nouislider/dist/nouislider.css";
import { onMount } from "svelte";
export let minValue = 0;
export let maxValue = 100;
export let currentMin = minValue;
export let currentMax = maxValue;
let sliderContainer: any;
onMount(() => {
const updateValues = (values: string[]) => {
currentMin = parseFloat(values[0]);
currentMax = parseFloat(values[1]);
};
noUiSlider.create(sliderContainer, {
start: [currentMin, currentMax],
connect: true,
range: {
min: minValue,
max: maxValue,
},
});
sliderContainer.noUiSlider.on("update", updateValues);
});
</script>
<div class="my-4" id="slider" bind:this={sliderContainer}></div>
<style>
:global(.noUi-horizontal) {
height: 6px;
}
:global(.noUi-connect) {
@apply bg-primary;
}
:global(.noUi-horizontal .noUi-handle) {
@apply rounded-full w-7 h-7 -top-3;
}
:global(.noUi-handle::before) {
@apply content-none;
}
:global(.noUi-handle::after) {
@apply content-none;
}
</style>

View File

@@ -30,7 +30,7 @@
<button class="flex items-center justify-center" on:click={toggleMenu} type="button">
<slot>
<i
class="fa fa-ellipsis-vertical text-{size} hover:bg-gray-300 px-[14px] py-2 rounded-full hover:bg-opacity-50"
class="fa fa-ellipsis-vertical text-{size} hover:bg-gray-300 hover:bg-opacity-50 px-[14px] py-2 rounded-full "
></i>
</slot>
</button>

View File

@@ -0,0 +1,41 @@
<script context="module" lang="ts">
export type RadioItem = {
text: string;
value: string;
icon?: string;
};
</script>
<script lang="ts">
import { createEventDispatcher } from "svelte";
export let items: RadioItem[];
export let name: string;
export let selected: number = 0;
const dispatch = createEventDispatcher();
function handleRadioChange(radioIndex: number) {
selected = radioIndex;
dispatch('change', items[selected])
}
</script>
{#each items as item, i}
<div class="flex items-center mb-4">
<input
id="{name}-radio-{i}"
name="{name}-radio"
type="radio"
checked={i == selected}
value={item.value}
class="w-4 h-4 text-primary bg-gray-100 border-gray-300 focus:ring-gray-400 focus:ring-2"
on:change={() => handleRadioChange(i)}
/>
<label
for="{name}-radio-{i}"
class="ms-2 text-sm text-gray-900 dark:text-gray-300"
>{item.text}</label
>
</div>
{/each}

View File

@@ -0,0 +1,110 @@
<script context="module" lang="ts">
export type SearchItem = {
text: string;
description?: string;
value: any;
icon: string;
};
</script>
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition";
import TextField from "./text_field.svelte";
export let maxSearchLength: number = 5;
export let value: string = "";
export let items: SearchItem[] = [];
export let placeholder: string = "Search...";
export let large: boolean = false;
const dispatch = createEventDispatcher();
let lastSearch: string = "";
let searching: boolean = false;
let typingTimer!: any;
$: dropDownOpen = value.length > 0 && items.length > 0 && searching;
function onSearchType() {
clearTimeout(typingTimer);
if (Math.abs(value.length - lastSearch.length) > maxSearchLength) {
update(value);
return;
}
typingTimer = setTimeout(() => {
update(value);
}, 500);
}
function update(q: string) {
lastSearch = q;
dispatch("update", q);
}
function handleItemClick(item: SearchItem) {
searching = false;
dispatch("click", item);
}
function clear() {
value = "";
update(value);
}
</script>
<div class="relative text-gray-600">
<span class="absolute top-1/2 -translate-y-1/2 left-0 pl-4">
<i class="fa fa-search" class:text-2xl={large}></i>
</span>
{#if value.length > 0}
<button
class="absolute top-1/2 -translate-y-1/2 right-0 h-6 w-6 mr-4 hover:bg-gray-300 hover:bg-opacity-50 rounded-full"
on:click={clear}
in:fade={{ duration: 150 }}
out:fade={{ duration: 150 }}
>
<i class="fa fa-close text-sm"></i>
</button>
{/if}
<TextField
type="search"
name="q"
autocomplete="off"
extraClasses="{large ? 'pl-14 text-2xl min-w-80 w-[33vw] max-w-[532px]' : 'pl-10'}"
{placeholder}
bind:value
on:input={onSearchType}
on:focusin={() => (searching = true)}
></TextField>
{#if dropDownOpen}
<ul
class="menu absolute bg-white border rounded-xl shadow-md overflow-hidden text-black w-full"
class:none={!dropDownOpen}
style="z-index: 1001"
>
{#each items as item}
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<li
class="menu-item flex items-center px-4 py-3 cursor-pointer hover:bg-gray-100 focus:bg-gray-200 transition-colors"
tabindex="0"
on:mouseup|stopPropagation={() => handleItemClick(item)}
on:keydown|stopPropagation={() => handleItemClick(item)}
>
<i class="fa fa-{item.icon} mr-6"></i>
<div>
<p>{item.text}</p>
{#if item.description}
<p class="text-sm text-gray-500">
{item.description}
</p>
{/if}
</div>
</li>
{/each}
</ul>
{/if}
</div>

View File

@@ -0,0 +1,52 @@
<script lang="ts">
import noUiSlider from "noUiSlider";
import "nouislider/dist/nouislider.css";
import { onMount } from "svelte";
export let minValue = 0;
export let maxValue = 100;
export let currentValue = maxValue / 2;
let sliderContainer: any;
onMount(() => {
const updateValues = (values: string[]) => {
currentValue = parseFloat(values[0]);
};
noUiSlider.create(sliderContainer, {
start: currentValue,
connect: [true, false],
range: {
min: minValue,
max: maxValue,
},
});
sliderContainer.noUiSlider.on("update", updateValues);
});
</script>
<div class="my-4" id="slider" bind:this={sliderContainer}></div>
<style>
:global(.noUi-horizontal) {
height: 6px;
}
:global(.noUi-connect) {
@apply bg-primary;
}
:global(.noUi-horizontal .noUi-handle) {
@apply rounded-full w-7 h-7 -top-3;
}
:global(.noUi-handle::before) {
@apply content-none;
}
:global(.noUi-handle::after) {
@apply content-none;
}
</style>

View File

@@ -6,8 +6,8 @@
export let error: string = "";
export let icon: string = "";
export let extraClasses: string = "";
export let type: "text" | "password" = "text";
export let type: "text" | "password" | "search" = "text";
export let autocomplete: "on" | "off" = "on"
function typeAction(node: HTMLInputElement) {
node.type = type;
}
@@ -28,9 +28,13 @@
class="bg-gray-50 border rounded-md p-3 transition-colors focus:border-primary focus:outline-none focus:ring-0 w-full {extraClasses}"
class:border-red-400={error.length > 0}
class:bg-red-50={error.length > 0}
{autocomplete}
use:typeAction
bind:value
on:change
on:input
on:focusin
on:focusout
{placeholder}
/>
</div>

View File

@@ -21,7 +21,7 @@
<a href="/"><LogoText></LogoText></a>
{#if $currentUser}
<menu class="flex gap-8">
<a class="font-semibold" href="">Trails</a>
<a class="font-semibold" href="/trails">Trails</a>
<a class="font-semibold" href="">Map</a>
<a class="font-semibold" href="">Categories</a>
<a class="font-semibold" href="">Favorites</a>

View File

@@ -5,6 +5,7 @@
import Dropdown from "../base/dropdown.svelte";
export let trail: Trail;
export let mode: "show" | "edit" = "show";
const dropdownItems = [
{ text: "Edit", value: "edit" },
@@ -12,15 +13,15 @@
];
</script>
<div class="trail-card rounded-2xl shadow-md w-72 cursor-pointer">
<div class="w-full h-48 overflow-hidden rounded-t-2xl">
<div class="trail-card rounded-2xl shadow-md sm:w-72 cursor-pointer">
<div class="w-full min-h-40 max-h-48 overflow-hidden rounded-t-2xl">
<img src={trail.thumbnail} alt="" />
</div>
<div class="p-4">
<div>
<div class="flex justify-between items-center">
<h4 class="font-semibold text-lg">{trail.name}</h4>
{#if $currentUser && $currentUser.id == trail.author}
{#if $currentUser && $currentUser.id == trail.author && mode == "edit"}
<Dropdown on:change items={dropdownItems}></Dropdown>
{/if}
</div>

View File

@@ -85,4 +85,22 @@ const trailSchema = object<SummitLog>({
description: string().optional()
});
export { Trail, trailSchema };
interface TrailFilter {
q: string,
category: string[],
near: {
lat?: number,
lon?: number,
distance: number
}
distanceMin: number,
distanceMax: number,
eleavationGainMin: number;
elevationGainMax: number;
completed?: boolean;
}
export { Trail, trailSchema };
export type { TrailFilter };

View File

@@ -90,7 +90,7 @@ export async function trails_create(trail: Trail, formData: { [key: string]: any
model = await pb
.collection("trails")
.update<Trail>(model.id!, { thumbnail: thumbnail });
.update<Trail>(model.id!, { thumbnail: thumbnail }, { expand: "category" });
index_trail(model);
@@ -190,6 +190,8 @@ export async function trails_delete(trail: Trail) {
}
}
ms.index('trails').deleteDocument(trail.id!)
const success = await pb
.collection("trails")
.delete(trail.id!);

View File

@@ -0,0 +1,247 @@
export const country_codes = {
AF: 'Afghanistan',
AX: 'Aland Islands',
AL: 'Albania',
DZ: 'Algeria',
AS: 'American Samoa',
AD: 'Andorra',
AO: 'Angola',
AI: 'Anguilla',
AQ: 'Antarctica',
AG: 'Antigua And Barbuda',
AR: 'Argentina',
AM: 'Armenia',
AW: 'Aruba',
AU: 'Australia',
AT: 'Austria',
AZ: 'Azerbaijan',
BS: 'Bahamas',
BH: 'Bahrain',
BD: 'Bangladesh',
BB: 'Barbados',
BY: 'Belarus',
BE: 'Belgium',
BZ: 'Belize',
BJ: 'Benin',
BM: 'Bermuda',
BT: 'Bhutan',
BO: 'Bolivia',
BA: 'Bosnia And Herzegovina',
BW: 'Botswana',
BV: 'Bouvet Island',
BR: 'Brazil',
IO: 'British Indian Ocean Territory',
BN: 'Brunei Darussalam',
BG: 'Bulgaria',
BF: 'Burkina Faso',
BI: 'Burundi',
KH: 'Cambodia',
CM: 'Cameroon',
CA: 'Canada',
CV: 'Cape Verde',
KY: 'Cayman Islands',
CF: 'Central African Republic',
TD: 'Chad',
CL: 'Chile',
CN: 'China',
CX: 'Christmas Island',
CC: 'Cocos (Keeling) Islands',
CO: 'Colombia',
KM: 'Comoros',
CG: 'Congo',
CD: 'Congo, Democratic Republic',
CK: 'Cook Islands',
CR: 'Costa Rica',
CI: 'Cote D\'Ivoire',
HR: 'Croatia',
CU: 'Cuba',
CY: 'Cyprus',
CZ: 'Czech Republic',
DK: 'Denmark',
DJ: 'Djibouti',
DM: 'Dominica',
DO: 'Dominican Republic',
EC: 'Ecuador',
EG: 'Egypt',
SV: 'El Salvador',
GQ: 'Equatorial Guinea',
ER: 'Eritrea',
EE: 'Estonia',
ET: 'Ethiopia',
FK: 'Falkland Islands (Malvinas)',
FO: 'Faroe Islands',
FJ: 'Fiji',
FI: 'Finland',
FR: 'France',
GF: 'French Guiana',
PF: 'French Polynesia',
TF: 'French Southern Territories',
GA: 'Gabon',
GM: 'Gambia',
GE: 'Georgia',
DE: 'Germany',
GH: 'Ghana',
GI: 'Gibraltar',
GR: 'Greece',
GL: 'Greenland',
GD: 'Grenada',
GP: 'Guadeloupe',
GU: 'Guam',
GT: 'Guatemala',
GG: 'Guernsey',
GN: 'Guinea',
GW: 'Guinea-Bissau',
GY: 'Guyana',
HT: 'Haiti',
HM: 'Heard Island & Mcdonald Islands',
VA: 'Holy See (Vatican City State)',
HN: 'Honduras',
HK: 'Hong Kong',
HU: 'Hungary',
IS: 'Iceland',
IN: 'India',
ID: 'Indonesia',
IR: 'Iran, Islamic Republic Of',
IQ: 'Iraq',
IE: 'Ireland',
IM: 'Isle Of Man',
IL: 'Israel',
IT: 'Italy',
JM: 'Jamaica',
JP: 'Japan',
JE: 'Jersey',
JO: 'Jordan',
KZ: 'Kazakhstan',
KE: 'Kenya',
KI: 'Kiribati',
KR: 'Korea',
KW: 'Kuwait',
KG: 'Kyrgyzstan',
LA: 'Lao People\'s Democratic Republic',
LV: 'Latvia',
LB: 'Lebanon',
LS: 'Lesotho',
LR: 'Liberia',
LY: 'Libyan Arab Jamahiriya',
LI: 'Liechtenstein',
LT: 'Lithuania',
LU: 'Luxembourg',
MO: 'Macao',
MK: 'Macedonia',
MG: 'Madagascar',
MW: 'Malawi',
MY: 'Malaysia',
MV: 'Maldives',
ML: 'Mali',
MT: 'Malta',
MH: 'Marshall Islands',
MQ: 'Martinique',
MR: 'Mauritania',
MU: 'Mauritius',
YT: 'Mayotte',
MX: 'Mexico',
FM: 'Micronesia, Federated States Of',
MD: 'Moldova',
MC: 'Monaco',
MN: 'Mongolia',
ME: 'Montenegro',
MS: 'Montserrat',
MA: 'Morocco',
MZ: 'Mozambique',
MM: 'Myanmar',
NA: 'Namibia',
NR: 'Nauru',
NP: 'Nepal',
NL: 'Netherlands',
AN: 'Netherlands Antilles',
NC: 'New Caledonia',
NZ: 'New Zealand',
NI: 'Nicaragua',
NE: 'Niger',
NG: 'Nigeria',
NU: 'Niue',
NF: 'Norfolk Island',
MP: 'Northern Mariana Islands',
NO: 'Norway',
OM: 'Oman',
PK: 'Pakistan',
PW: 'Palau',
PS: 'Palestinian Territory, Occupied',
PA: 'Panama',
PG: 'Papua New Guinea',
PY: 'Paraguay',
PE: 'Peru',
PH: 'Philippines',
PN: 'Pitcairn',
PL: 'Poland',
PT: 'Portugal',
PR: 'Puerto Rico',
QA: 'Qatar',
RE: 'Reunion',
RO: 'Romania',
RU: 'Russian Federation',
RW: 'Rwanda',
BL: 'Saint Barthelemy',
SH: 'Saint Helena',
KN: 'Saint Kitts And Nevis',
LC: 'Saint Lucia',
MF: 'Saint Martin',
PM: 'Saint Pierre And Miquelon',
VC: 'Saint Vincent And Grenadines',
WS: 'Samoa',
SM: 'San Marino',
ST: 'Sao Tome And Principe',
SA: 'Saudi Arabia',
SN: 'Senegal',
RS: 'Serbia',
SC: 'Seychelles',
SL: 'Sierra Leone',
SG: 'Singapore',
SK: 'Slovakia',
SI: 'Slovenia',
SB: 'Solomon Islands',
SO: 'Somalia',
ZA: 'South Africa',
GS: 'South Georgia And Sandwich Isl.',
ES: 'Spain',
LK: 'Sri Lanka',
SD: 'Sudan',
SR: 'Suriname',
SJ: 'Svalbard And Jan Mayen',
SZ: 'Swaziland',
SE: 'Sweden',
CH: 'Switzerland',
SY: 'Syrian Arab Republic',
TW: 'Taiwan',
TJ: 'Tajikistan',
TZ: 'Tanzania',
TH: 'Thailand',
TL: 'Timor-Leste',
TG: 'Togo',
TK: 'Tokelau',
TO: 'Tonga',
TT: 'Trinidad And Tobago',
TN: 'Tunisia',
TR: 'Turkey',
TM: 'Turkmenistan',
TC: 'Turks And Caicos Islands',
TV: 'Tuvalu',
UG: 'Uganda',
UA: 'Ukraine',
AE: 'United Arab Emirates',
GB: 'United Kingdom',
US: 'United States',
UM: 'United States Outlying Islands',
UY: 'Uruguay',
UZ: 'Uzbekistan',
VU: 'Vanuatu',
VE: 'Venezuela',
VN: 'Viet Nam',
VG: 'Virgin Islands, British',
VI: 'Virgin Islands, U.S.',
WF: 'Wallis And Futuna',
EH: 'Western Sahara',
YE: 'Yemen',
ZM: 'Zambia',
ZW: 'Zimbabwe'
}

View File

@@ -11,7 +11,7 @@ export function formatTimeHHMM(minutes?: number) {
}
export function formatMeters(meters?: number) {
if (!meters) {
if (meters === undefined) {
return "-";
}
if (meters % 1 === 0) {