adds trails page
This commit is contained in:
54
web/src/lib/components/base/double_slider.svelte
Normal file
54
web/src/lib/components/base/double_slider.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
41
web/src/lib/components/base/radio_group.svelte
Normal file
41
web/src/lib/components/base/radio_group.svelte
Normal 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}
|
||||
110
web/src/lib/components/base/search.svelte
Normal file
110
web/src/lib/components/base/search.svelte
Normal 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>
|
||||
52
web/src/lib/components/base/slider.svelte
Normal file
52
web/src/lib/components/base/slider.svelte
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user