Compare commits
3 Commits
074117869e
...
4b28e281c1
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b28e281c1 | |||
| de06056596 | |||
| 89d2066077 |
@@ -1,12 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState, type KeyboardEvent } from "react";
|
||||||
import { LoaderCircle, MapPin, Search, X } from "lucide-react";
|
import { LoaderCircle, MapPin, Search, X } from "lucide-react";
|
||||||
import { useLocationSearch } from "@/hooks/use-location-search";
|
import { useLocationSearch } from "@/hooks/use-location-search";
|
||||||
import { useWeatherStore } from "@/lib/store";
|
import { useWeatherStore } from "@/lib/store";
|
||||||
import { findNearestSynopStation, locateSynopStations } from "@/lib/location-utils";
|
import { findNearestSynopStation, locateSynopStations } from "@/lib/location-utils";
|
||||||
import { useI18n } from "@/lib/i18n";
|
import { useI18n } from "@/lib/i18n";
|
||||||
import type { MeteoStationPosition, SynopStation } from "@/types/imgw";
|
import type { MeteoStationPosition, SynopStation } from "@/types/imgw";
|
||||||
|
import type { SelectedLocation } from "@/types/location";
|
||||||
import { CurrentLocationControl } from "@/components/weather/current-location-control";
|
import { CurrentLocationControl } from "@/components/weather/current-location-control";
|
||||||
|
|
||||||
export function LocationSearch({ stations, positions }: { stations: SynopStation[]; positions: MeteoStationPosition[] }) {
|
export function LocationSearch({ stations, positions }: { stations: SynopStation[]; positions: MeteoStationPosition[] }) {
|
||||||
@@ -24,6 +25,19 @@ export function LocationSearch({ stations, positions }: { stations: SynopStation
|
|||||||
const showSuggestions = isFocused && query.trim().length >= 2;
|
const showSuggestions = isFocused && query.trim().length >= 2;
|
||||||
const isPreparingStations = positions.length === 0;
|
const isPreparingStations = positions.length === 0;
|
||||||
|
|
||||||
|
function chooseLocation(location: SelectedLocation) {
|
||||||
|
selectLocation(location);
|
||||||
|
setQuery("");
|
||||||
|
setIsFocused(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearchKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||||
|
const firstSuggestion = suggestions[0]?.nearest;
|
||||||
|
if (event.key !== "Enter" || !showSuggestions || !firstSuggestion) return;
|
||||||
|
event.preventDefault();
|
||||||
|
chooseLocation(firstSuggestion);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative z-30">
|
<section className="relative z-30">
|
||||||
<div className="glass rounded-panel p-3 sm:p-4">
|
<div className="glass rounded-panel p-3 sm:p-4">
|
||||||
@@ -31,12 +45,14 @@ export function LocationSearch({ stations, positions }: { stations: SynopStation
|
|||||||
<MapPin className="size-4 text-accent" />
|
<MapPin className="size-4 text-accent" />
|
||||||
<p className="section-kicker">{t("location.label")}</p>
|
<p className="section-kicker">{t("location.label")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="relative z-20">
|
||||||
<label className="relative block">
|
<label className="relative block">
|
||||||
<span className="sr-only">{t("location.searchLabel")}</span>
|
<span className="sr-only">{t("location.searchLabel")}</span>
|
||||||
{isFetching || isPreparingStations ? <LoaderCircle className="pointer-events-none absolute left-3.5 top-1/2 size-4 -translate-y-1/2 animate-spin text-accent" /> : <Search className="pointer-events-none absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-muted" />}
|
{isFetching || isPreparingStations ? <LoaderCircle className="pointer-events-none absolute left-3.5 top-1/2 size-4 -translate-y-1/2 animate-spin text-accent" /> : <Search className="pointer-events-none absolute left-3.5 top-1/2 size-4 -translate-y-1/2 text-muted" />}
|
||||||
<input
|
<input
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
onKeyDown={handleSearchKeyDown}
|
||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
onBlur={() => window.setTimeout(() => setIsFocused(false), 150)}
|
onBlur={() => window.setTimeout(() => setIsFocused(false), 150)}
|
||||||
placeholder={t("location.placeholder")}
|
placeholder={t("location.placeholder")}
|
||||||
@@ -45,16 +61,6 @@ export function LocationSearch({ stations, positions }: { stations: SynopStation
|
|||||||
/>
|
/>
|
||||||
{query && <button type="button" aria-label={t("location.clear")} onClick={() => setQuery("")} className="absolute right-3 top-1/2 -translate-y-1/2 rounded-control p-1 text-muted transition hover:bg-surface-muted/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"><X className="size-4" /></button>}
|
{query && <button type="button" aria-label={t("location.clear")} onClick={() => setQuery("")} className="absolute right-3 top-1/2 -translate-y-1/2 rounded-control p-1 text-muted transition hover:bg-surface-muted/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"><X className="size-4" /></button>}
|
||||||
</label>
|
</label>
|
||||||
<CurrentLocationControl stations={locatedStations} />
|
|
||||||
{selectedLocation && (
|
|
||||||
<p className="mt-3 px-1 text-xs text-muted">
|
|
||||||
{t("location.currentSource", { location: selectedLocation.name, station: selectedLocation.stationName, distance: selectedLocation.distanceKm })}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<p className="mt-3 px-1 text-[0.68rem] text-muted">
|
|
||||||
{t("location.attribution")} <a href="https://open-meteo.com/en/docs/geocoding-api" target="_blank" rel="noreferrer" className="underline decoration-muted/60 underline-offset-2 transition hover:text-accent">Open-Meteo / GeoNames</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{showSuggestions && (
|
{showSuggestions && (
|
||||||
<div className="glass absolute inset-x-0 top-[calc(100%+0.5rem)] overflow-hidden rounded-panel p-2 shadow-card">
|
<div className="glass absolute inset-x-0 top-[calc(100%+0.5rem)] overflow-hidden rounded-panel p-2 shadow-card">
|
||||||
{isPreparingStations ? <p className="px-3 py-4 text-sm text-muted">{t("location.preparing")}</p> :
|
{isPreparingStations ? <p className="px-3 py-4 text-sm text-muted">{t("location.preparing")}</p> :
|
||||||
@@ -64,11 +70,7 @@ export function LocationSearch({ stations, positions }: { stations: SynopStation
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={location.id}
|
key={location.id}
|
||||||
onClick={() => {
|
onClick={() => chooseLocation(nearest)}
|
||||||
selectLocation(nearest);
|
|
||||||
setQuery("");
|
|
||||||
setIsFocused(false);
|
|
||||||
}}
|
|
||||||
className="flex w-full items-start justify-between gap-3 rounded-card px-3 py-3 text-left transition hover:bg-surface-muted/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
className="flex w-full items-start justify-between gap-3 rounded-card px-3 py-3 text-left transition hover:bg-surface-muted/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@@ -80,6 +82,17 @@ export function LocationSearch({ stations, positions }: { stations: SynopStation
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
<CurrentLocationControl stations={locatedStations} />
|
||||||
|
{selectedLocation && (
|
||||||
|
<p className="mt-3 px-1 text-xs text-muted">
|
||||||
|
{t("location.currentSource", { location: selectedLocation.name, station: selectedLocation.stationName, distance: selectedLocation.distanceKm })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="mt-3 px-1 text-[0.68rem] text-muted">
|
||||||
|
{t("location.attribution")} <a href="https://open-meteo.com/en/docs/geocoding-api" target="_blank" rel="noreferrer" className="underline decoration-muted/60 underline-offset-2 transition hover:text-accent">Open-Meteo / GeoNames</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
Reference in New Issue
Block a user