mirror of
https://github.com/gevera/hot-cold-cities.git
synced 2025-12-06 10:38:20 +00:00
Added OpenMeteo Service
This commit is contained in:
parent
77635600aa
commit
dde2580b99
57
src/app.css
57
src/app.css
@ -2,6 +2,61 @@
|
|||||||
@plugin '@tailwindcss/typography';
|
@plugin '@tailwindcss/typography';
|
||||||
@plugin '@tailwindcss/forms';
|
@plugin '@tailwindcss/forms';
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "hotcold";
|
||||||
|
default: true;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "light";
|
||||||
|
--color-base-100: oklch(98% 0.019 200.873);
|
||||||
|
--color-base-200: oklch(95% 0.045 203.388);
|
||||||
|
--color-base-300: oklch(91% 0.08 205.041);
|
||||||
|
--color-base-content: oklch(39% 0.07 227.392);
|
||||||
|
--color-primary: oklch(60% 0.25 292.717);
|
||||||
|
--color-primary-content: oklch(96% 0.016 293.756);
|
||||||
|
--color-secondary: oklch(62% 0.214 259.815);
|
||||||
|
--color-secondary-content: oklch(97% 0.014 254.604);
|
||||||
|
--color-accent: oklch(71% 0.143 215.221);
|
||||||
|
--color-accent-content: oklch(98% 0.019 200.873);
|
||||||
|
--color-neutral: oklch(52% 0.105 223.128);
|
||||||
|
--color-neutral-content: oklch(98% 0.019 200.873);
|
||||||
|
--color-info: oklch(62% 0.214 259.815);
|
||||||
|
--color-info-content: oklch(97% 0.014 254.604);
|
||||||
|
--color-success: oklch(70% 0.14 182.503);
|
||||||
|
--color-success-content: oklch(98% 0.014 180.72);
|
||||||
|
--color-warning: oklch(79% 0.184 86.047);
|
||||||
|
--color-warning-content: oklch(98% 0.026 102.212);
|
||||||
|
--color-error: oklch(65% 0.241 354.308);
|
||||||
|
--color-error-content: oklch(97% 0.014 343.198);
|
||||||
|
--radius-selector: 0.5rem;
|
||||||
|
--radius-field: 0.5rem;
|
||||||
|
--radius-box: 0.5rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 1;
|
||||||
|
--noise: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@plugin "daisyui" {
|
@plugin "daisyui" {
|
||||||
themes: winter --default;
|
themes: hotcold --default;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for IE, Edge and Firefox */
|
||||||
|
.no-scrollbar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
/* IE and Edge */
|
||||||
|
scrollbar-width: none;
|
||||||
|
/* Firefox */
|
||||||
}
|
}
|
||||||
@ -1,36 +1,38 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ComboOption, CountryBasic } from '$lib/types';
|
import type { CityBasic, ComboOption, CountryBasic } from '$lib/types';
|
||||||
import ChevronDown from './icons/ChevronDown.svelte';
|
import ChevronDown from './icons/ChevronDown.svelte';
|
||||||
import ChevronUp from './icons/ChevronUp.svelte';
|
import ChevronUp from './icons/ChevronUp.svelte';
|
||||||
import FuzzySearch from 'fuzzy-search';
|
|
||||||
import XMark from './icons/XMark.svelte';
|
import XMark from './icons/XMark.svelte';
|
||||||
|
import FuzzySearch from 'fuzzy-search';
|
||||||
|
// import Fuse from 'fuse.js'
|
||||||
|
|
||||||
type ComboBoxProps = {
|
type ComboBoxProps = {
|
||||||
options: ComboOption[] | CountryBasic[];
|
options: ComboOption[] | CountryBasic[] | CityBasic[];
|
||||||
onSelect: (data: ComboOption | CountryBasic) => void;
|
onSelect: (data: ComboOption | CountryBasic | CityBasic) => void;
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
|
disabledItems?: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
let { options, onSelect, onClear }: ComboBoxProps = $props();
|
let { options, onSelect, onClear, disabledItems }: ComboBoxProps = $props();
|
||||||
|
|
||||||
let focused = $state(false);
|
let focused = $state(false);
|
||||||
let selectSet = $state(false);
|
let selectSet = $state(false);
|
||||||
let inputElement: HTMLInputElement;
|
let inputElement: HTMLInputElement;
|
||||||
let dropdownElement: HTMLUListElement;
|
let dropdownElement: HTMLUListElement | null = $state(null);
|
||||||
let filterInput = $state('');
|
let filterInput = $state('');
|
||||||
|
let emoji: string = $state('');
|
||||||
|
|
||||||
const searcher = $state(new FuzzySearch(options, ['name'], {
|
let searcher = $state(new FuzzySearch(options, ['name'], { caseSensitive: false, sort: true }));
|
||||||
caseSensitive: false,
|
|
||||||
sort: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
searcher = new FuzzySearch(options, ['name'], { caseSensitive: false, sort: true });
|
||||||
|
});
|
||||||
|
|
||||||
let filteredOptions = $derived(filterInput ? searcher.search(filterInput) : options);
|
let filteredOptions = $derived(filterInput ? searcher.search(filterInput) : options);
|
||||||
|
|
||||||
|
const handleSelect = (option: ComboOption | CountryBasic | CityBasic) => {
|
||||||
const handleSelect = (option: ComboOption) => {
|
filterInput = option.name;
|
||||||
filterInput = option?.emoji ? option.name + ' ' + option?.emoji : option.name;
|
emoji = 'emoji' in option ? (option.emoji ?? '') : '';
|
||||||
focused = false;
|
focused = false;
|
||||||
selectSet = true;
|
selectSet = true;
|
||||||
onSelect(option);
|
onSelect(option);
|
||||||
@ -39,10 +41,10 @@
|
|||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
filterInput = '';
|
filterInput = '';
|
||||||
selectSet = false;
|
selectSet = false;
|
||||||
|
emoji = '';
|
||||||
onClear();
|
onClear();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
inputElement &&
|
inputElement &&
|
||||||
@ -68,10 +70,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="dropdown my-2 py-2">
|
<div class="dropdown my-2 py-2">
|
||||||
<label class="input">
|
<label class="input !outline-none focus:ring-0">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="focus:ring-0"
|
class="focus:ring-0 focus:outline-none"
|
||||||
onfocus={() => (focused = true)}
|
onfocus={() => (focused = true)}
|
||||||
bind:this={inputElement}
|
bind:this={inputElement}
|
||||||
bind:value={filterInput}
|
bind:value={filterInput}
|
||||||
@ -79,24 +81,28 @@
|
|||||||
placeholder="Type to search"
|
placeholder="Type to search"
|
||||||
/>
|
/>
|
||||||
{#if !selectSet}
|
{#if !selectSet}
|
||||||
<label class="swap swap-rotate" class:swap-active={focused}>
|
<label class="swap swap-rotate" class:swap-active={focused}>
|
||||||
<div class="swap-off">
|
<div class="swap-off">
|
||||||
<ChevronDown />
|
<ChevronDown />
|
||||||
</div>
|
</div>
|
||||||
<div class="swap-on">
|
<div class="swap-on">
|
||||||
<ChevronUp />
|
<ChevronUp />
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
{:else}
|
{:else}
|
||||||
<label class="">
|
<label class="relative">
|
||||||
<button class="cursor-pointer" onclick={handleClear}><XMark /></button>
|
{#if emoji}
|
||||||
</label>
|
<span class="absolute -top-0.5 right-5">{emoji}</span>
|
||||||
|
{/if}
|
||||||
|
<button class="cursor-pointer" onclick={handleClear}><XMark /></button>
|
||||||
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
{#if focused}
|
{#if focused}
|
||||||
<ul
|
<ul
|
||||||
bind:this={dropdownElement}
|
bind:this={dropdownElement}
|
||||||
class="dropdown-content rounded-box z-100 mt-1 w-full gap-1 bg-white p-2 shadow-sm"
|
class={`dropdown-content rounded-box relative z-100 mt-2 max-h-[50vh] w-full gap-1 overflow-y-auto border border-gray-100 bg-white p-2 shadow-md
|
||||||
|
${filteredOptions.length >= 5 ? '' : ''}`}
|
||||||
>
|
>
|
||||||
{#each filteredOptions as option (option.id)}
|
{#each filteredOptions as option (option.id)}
|
||||||
<li class="mb-1">
|
<li class="mb-1">
|
||||||
@ -104,20 +110,15 @@
|
|||||||
class="btn btn-soft w-full justify-between text-left"
|
class="btn btn-soft w-full justify-between text-left"
|
||||||
onclick={() => handleSelect(option)}
|
onclick={() => handleSelect(option)}
|
||||||
>
|
>
|
||||||
<div>
|
<div class="no-scrollbar overflow-x-auto text-ellipsis whitespace-nowrap">
|
||||||
|
{option.name}
|
||||||
|
</div>
|
||||||
{option.name}
|
<div>
|
||||||
</div>
|
{(option as CountryBasic)?.emoji ?? ''}
|
||||||
<div>
|
</div>
|
||||||
{option?.emoji ?? ''}
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
|
|||||||
58
src/lib/components/Footer.svelte
Normal file
58
src/lib/components/Footer.svelte
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<footer class="footer footer-horizontal footer-center bg-accent text-accent-content p-10 z-1">
|
||||||
|
<aside>
|
||||||
|
<svg
|
||||||
|
width="50"
|
||||||
|
height="50"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
class="inline-block fill-current">
|
||||||
|
<path
|
||||||
|
d="M22.672 15.226l-2.432.811.841 2.515c.33 1.019-.209 2.127-1.23 2.456-1.15.325-2.148-.321-2.463-1.226l-.84-2.518-5.013 1.677.84 2.517c.391 1.203-.434 2.542-1.831 2.542-.88 0-1.601-.564-1.86-1.314l-.842-2.516-2.431.809c-1.135.328-2.145-.317-2.463-1.229-.329-1.018.211-2.127 1.231-2.456l2.432-.809-1.621-4.823-2.432.808c-1.355.384-2.558-.59-2.558-1.839 0-.817.509-1.582 1.327-1.846l2.433-.809-.842-2.515c-.33-1.02.211-2.129 1.232-2.458 1.02-.329 2.13.209 2.461 1.229l.842 2.515 5.011-1.677-.839-2.517c-.403-1.238.484-2.553 1.843-2.553.819 0 1.585.509 1.85 1.326l.841 2.517 2.431-.81c1.02-.33 2.131.211 2.461 1.229.332 1.018-.21 2.126-1.23 2.456l-2.433.809 1.622 4.823 2.433-.809c1.242-.401 2.557.484 2.557 1.838 0 .819-.51 1.583-1.328 1.847m-8.992-6.428l-5.01 1.675 1.619 4.828 5.011-1.674-1.62-4.829z"></path>
|
||||||
|
</svg>
|
||||||
|
<p class="">
|
||||||
|
<span class="font-bold text-xl">HotColdCities</span>
|
||||||
|
<br />
|
||||||
|
Powered by <a href="https://open-meteo.com/" target="_blank">OpenMeteo</a>
|
||||||
|
</p>
|
||||||
|
<p>Copyright © {new Date().getFullYear()} - All right reserved</p>
|
||||||
|
</aside>
|
||||||
|
<!-- <nav>
|
||||||
|
<div class="grid grid-flow-col gap-4">
|
||||||
|
<a>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="fill-current">
|
||||||
|
<path
|
||||||
|
d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="fill-current">
|
||||||
|
<path
|
||||||
|
d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="fill-current">
|
||||||
|
<path
|
||||||
|
d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z"></path>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav> -->
|
||||||
|
</footer>
|
||||||
51
src/lib/components/TwoCities.svelte
Normal file
51
src/lib/components/TwoCities.svelte
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { CityBasic, ComboOption, CountryBasic } from '$lib/types';
|
||||||
|
import ComboBox from './ComboBox.svelte';
|
||||||
|
let selectedFirstCity: CityBasic | null = $state(null);
|
||||||
|
let selectedSecondCity: CityBasic | null = $state(null);
|
||||||
|
|
||||||
|
let { cities }: { cities: CityBasic[] } = $props();
|
||||||
|
|
||||||
|
const isCityBasic = (city: CityBasic | ComboOption | CountryBasic): city is CityBasic => {
|
||||||
|
return 'country_id' in city;
|
||||||
|
};
|
||||||
|
let comboFirstFilteredCities: CityBasic[] = $derived(
|
||||||
|
selectedSecondCity ?
|
||||||
|
cities.filter((c) => c.id !== selectedSecondCity?.id) :
|
||||||
|
cities
|
||||||
|
);
|
||||||
|
let comboSecondFilteredCities: CityBasic[] = $derived(
|
||||||
|
selectedFirstCity ?
|
||||||
|
cities.filter((c) => c.id !== selectedFirstCity?.id) :
|
||||||
|
cities
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFirstCitySelect = (city: CityBasic | ComboOption | CountryBasic) => {
|
||||||
|
if (isCityBasic(city)) {
|
||||||
|
selectedFirstCity = city;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSecondCitySelect = (city: CityBasic | ComboOption | CountryBasic) => {
|
||||||
|
if (isCityBasic(city)) {
|
||||||
|
selectedSecondCity = city;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<section class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
<div>
|
||||||
|
<ComboBox
|
||||||
|
options={comboFirstFilteredCities}
|
||||||
|
onSelect={onFirstCitySelect}
|
||||||
|
onClear={() => (selectedFirstCity = null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ComboBox
|
||||||
|
options={comboSecondFilteredCities}
|
||||||
|
onSelect={onSecondCitySelect}
|
||||||
|
onClear={() => selectedSecondCity = null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@ -10,4 +10,4 @@ export const allCountries = db.query(`
|
|||||||
WHERE capital IN('capital', 'admin') OR(population >= 100000 AND population IS NOT NULL)
|
WHERE capital IN('capital', 'admin') OR(population >= 100000 AND population IS NOT NULL)
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
export const allCities = db.query("SELECT id, city, country_id FROM cities WHERE capital IN ('capital', 'admin') OR (population >= 100000 AND population != '')");
|
export const allCities = db.query("SELECT id, city as name, country_id, lat as latitude, lng as longitude FROM cities WHERE capital IN ('capital', 'admin') OR (population >= 100000 AND population != '')");
|
||||||
|
|||||||
56
src/lib/server/meteoService.ts
Normal file
56
src/lib/server/meteoService.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// import { fetchWeatherApi } from 'openmeteo';
|
||||||
|
|
||||||
|
export const getLocationTemperature = async (latitude: string, longitude: string) => {
|
||||||
|
const params = {
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
daily: ["temperature_2m_max", "temperature_2m_min"],
|
||||||
|
forecast_days: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
const meteoEndpoint = "https://api.open-meteo.com/v1/forecast";
|
||||||
|
// https://api.open-meteo.com/v1/forecast?latitude=48.0356&longitude=27.8129&hourly=temperature_2m&daily=temperature_2m_max,temperature_2m_min&forecast_days=1&format=json&timeformat=unixtime
|
||||||
|
|
||||||
|
const url = new URL(meteoEndpoint);
|
||||||
|
url.searchParams.set('latitude', latitude);
|
||||||
|
url.searchParams.set('longitude', longitude);
|
||||||
|
url.searchParams.set('daily', 'temperature_2m_max,temperature_2m_min');
|
||||||
|
url.searchParams.set('format', 'json');
|
||||||
|
url.searchParams.set('forecast_days', '1');
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('DATA >>>>', data);
|
||||||
|
}
|
||||||
|
// const response = await fetchWeatherApi(url, params);
|
||||||
|
// Get the first location
|
||||||
|
// console.log('RESPONSES', responses)
|
||||||
|
|
||||||
|
// const response = responses[0];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// console.log('RESPONSE >>>>', response);
|
||||||
|
|
||||||
|
|
||||||
|
// Get daily data
|
||||||
|
// // const daily = response.daily();
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// max: daily.temperature_2m_max[0],
|
||||||
|
// min: daily.temperature_2m_min[0],
|
||||||
|
// unit: daily.temperature_2m_max_units
|
||||||
|
// };
|
||||||
|
return {
|
||||||
|
ok: true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return {
|
||||||
|
ok: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,12 +7,15 @@ export type CountryBasic = {
|
|||||||
|
|
||||||
export type CityBasic = {
|
export type CityBasic = {
|
||||||
id: number;
|
id: number;
|
||||||
city: string;
|
name: string;
|
||||||
country_id: number;
|
country_id: number;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ComboOption = {
|
export type ComboOption = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
|
country_id?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { allCities, allCountries } from '$lib/db/queries';
|
import { allCities, allCountries } from '$lib/db/queries';
|
||||||
import type { CityBasic, CountryBasic } from '$lib/types';
|
import type { CityBasic, CountryBasic } from '$lib/types';
|
||||||
|
import { error, type Actions } from '@sveltejs/kit';
|
||||||
|
import { getLocationTemperature } from '$lib/server/meteoService';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = () => {
|
export const load: PageServerLoad = () => {
|
||||||
@ -18,3 +20,28 @@ export const load: PageServerLoad = () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
fetchTemperature: async ({ request }) => {
|
||||||
|
try {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const latitude = formData.get('latitude') as string;
|
||||||
|
const longitude = formData.get('longitude') as string;
|
||||||
|
const response = await getLocationTemperature(latitude, longitude);
|
||||||
|
console.log('RESPONSE >>>>', response);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: response,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
data: null,
|
||||||
|
error: 'Failed to fetch temperature'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
import ComboBox from '$lib/components/ComboBox.svelte';
|
import ComboBox from '$lib/components/ComboBox.svelte';
|
||||||
import type { CityBasic, CountryBasic } from '$lib/types';
|
import Footer from '$lib/components/Footer.svelte';
|
||||||
|
import TwoCities from '$lib/components/TwoCities.svelte';
|
||||||
|
import type { CityBasic, CountryBasic, ComboOption } from '$lib/types';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
let filteredCities: CityBasic[] = $state([]);
|
|
||||||
|
|
||||||
// TODO:
|
let filteredCities: CityBasic[] = $state([]);
|
||||||
// - create a combo box
|
|
||||||
// - implement fuzzy-search
|
|
||||||
|
|
||||||
const setCountryCities = (id: number | null) => {
|
const setCountryCities = (id: number | null) => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@ -18,39 +18,51 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelect = (country: CountryBasic) => {
|
const onCountrySelect = (country: CountryBasic | ComboOption) => {
|
||||||
setCountryCities(country.id);
|
setCountryCities(country.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="container mx-auto my-3 max-w-4xl p-4">
|
<svelte:head>
|
||||||
<h1 class="my-5 text-center text-4xl font-light">HotColdCities</h1>
|
<title>HotColdCities</title>
|
||||||
<h2 class="mx-auto max-w-xl text-center text-xl leading-tight md:text-2xl">
|
</svelte:head>
|
||||||
Compare the hottest 🌡️ and coldest ❄️ cities in any region to plan 📅, move 🚚, or explore 🌍
|
|
||||||
with ease! 🎉✨
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<section>
|
<main class="flex h-full min-h-screen flex-col bg-white">
|
||||||
<ComboBox options={data.countries} {onSelect} onClear={() => setCountryCities(null)} />
|
<div class="container mx-auto my-3 max-w-4xl flex-grow p-4">
|
||||||
</section>
|
<h1 class="my-5 text-center text-4xl font-light">HotColdCities</h1>
|
||||||
|
<h2 class="mx-auto max-w-xl text-center text-xl leading-tight md:text-2xl">
|
||||||
|
Compare the hottest 🌡️ and coldest ❄️ cities in any region to plan 📅, move 🚚, or explore 🌍
|
||||||
|
with ease! 🎉✨
|
||||||
|
</h2>
|
||||||
|
|
||||||
<button class="btn btn-lg btn-soft btn-primary"> Hi </button>
|
<section>
|
||||||
<div class="grid grid-cols-2 gap-5">
|
<ComboBox
|
||||||
<!-- <ul>
|
options={data.countries}
|
||||||
{#each data.countries as country (country.id)}
|
onSelect={onCountrySelect}
|
||||||
<li>
|
onClear={() => setCountryCities(null)}
|
||||||
<button class="btn btn-ghost" onclick={() => setCountryCities(country.id)}>
|
/>
|
||||||
{country.name}
|
</section>
|
||||||
{country.emoji}
|
<!-- TODO: Show max min temperatures in the country and its coresponding cities -->
|
||||||
</button>
|
|
||||||
</li>
|
<!-- <TwoCities cities={data.cities} /> -->
|
||||||
{/each}
|
|
||||||
</ul> -->
|
<button class="btn btn-lg btn-soft btn-primary"> Hi </button>
|
||||||
<ul>
|
<div class="grid grid-cols-2 gap-5">
|
||||||
{#each filteredCities as city (city.id)}
|
<ul class="flex flex-col gap-1">
|
||||||
<li>{city.city}</li>
|
{#each filteredCities as city (city.id)}
|
||||||
{/each}
|
<li class="flex gap-2">
|
||||||
</ul>
|
{city.name}
|
||||||
<!-- <code>{JSON.stringify(filteredCities, null, 2)}</code> -->
|
<form action="?/fetchTemperature" method="POST" use:enhance>
|
||||||
|
<input type="text" class="hidden" name="latitude" value={city.latitude} />
|
||||||
|
<input type="text" class="hidden" name="longitude" value={city.longitude} />
|
||||||
|
<button type="submit" class="btn btn-soft btn-primary">Get</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
<!-- <code>{JSON.stringify(filteredCities, null, 2)}</code> -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user