chore: add prettier formatting
All checks were successful
CI / Lint, typecheck and build (push) Successful in 9m56s
All checks were successful
CI / Lint, typecheck and build (push) Successful in 9m56s
This commit is contained in:
@@ -10,7 +10,13 @@ import { useWeatherStore } from "@/lib/store";
|
||||
import type { MeteoStationPosition, SynopStation } from "@/types/imgw";
|
||||
import type { SelectedLocation } from "@/types/location";
|
||||
|
||||
export function LocationSearch({ stations, positions }: { stations: SynopStation[]; positions: MeteoStationPosition[] }) {
|
||||
export function LocationSearch({
|
||||
stations,
|
||||
positions,
|
||||
}: {
|
||||
stations: SynopStation[];
|
||||
positions: MeteoStationPosition[];
|
||||
}) {
|
||||
const { language, t } = useI18n();
|
||||
const [query, setQuery] = useState("");
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
@@ -18,12 +24,17 @@ export function LocationSearch({ stations, positions }: { stations: SynopStation
|
||||
const selectLocation = useWeatherStore((state) => state.selectLocation);
|
||||
const { data: locations, isFetching, isError } = useLocationSearch(query, language);
|
||||
const locatedStations = useMemo(() => locateSynopStations(stations, positions), [positions, stations]);
|
||||
const suggestions = useMemo(() => (locations ?? []).map((location) => ({
|
||||
location,
|
||||
selected: createSelectedLocation(location, locatedStations),
|
||||
})), [locatedStations, locations]);
|
||||
const suggestions = useMemo(
|
||||
() =>
|
||||
(locations ?? []).map((location) => ({
|
||||
location,
|
||||
selected: createSelectedLocation(location, locatedStations),
|
||||
})),
|
||||
[locatedStations, locations],
|
||||
);
|
||||
const showSuggestions = isFocused && query.trim().length >= 2;
|
||||
const isPreparingStations = positions.length === 0 && suggestions.some(({ selected }) => selected.region === "PL" && !selected.stationName);
|
||||
const isPreparingStations =
|
||||
positions.length === 0 && suggestions.some(({ selected }) => selected.region === "PL" && !selected.stationName);
|
||||
|
||||
function chooseLocation(location: SelectedLocation) {
|
||||
selectLocation(location);
|
||||
@@ -48,7 +59,11 @@ export function LocationSearch({ stations, positions }: { stations: SynopStation
|
||||
<div className="relative z-20">
|
||||
<label className="relative block">
|
||||
<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
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
@@ -59,31 +74,57 @@ export function LocationSearch({ stations, positions }: { stations: SynopStation
|
||||
autoComplete="off"
|
||||
className="w-full rounded-card border border-border/70 bg-surface-raised/80 py-3.5 pl-10 pr-10 text-base placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent sm:text-sm"
|
||||
/>
|
||||
{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>
|
||||
{showSuggestions && (
|
||||
<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> :
|
||||
isError ? <p className="px-3 py-4 text-sm text-muted">{t("location.error")}</p> :
|
||||
!isFetching && !suggestions.length ? <p className="px-3 py-4 text-sm text-muted">{t("location.empty")}</p> :
|
||||
suggestions.map(({ location, selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
key={location.id}
|
||||
onClick={() => chooseLocation(selected)}
|
||||
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 className="block text-sm font-semibold">{location.name}</span>
|
||||
<span className="mt-0.5 block text-xs text-muted">{[location.admin2, location.admin1, location.country].filter(Boolean).join(" · ")}</span>
|
||||
{isPreparingStations ? (
|
||||
<p className="px-3 py-4 text-sm text-muted">{t("location.preparing")}</p>
|
||||
) : isError ? (
|
||||
<p className="px-3 py-4 text-sm text-muted">{t("location.error")}</p>
|
||||
) : !isFetching && !suggestions.length ? (
|
||||
<p className="px-3 py-4 text-sm text-muted">{t("location.empty")}</p>
|
||||
) : (
|
||||
suggestions.map(({ location, selected }) => (
|
||||
<button
|
||||
type="button"
|
||||
key={location.id}
|
||||
onClick={() => chooseLocation(selected)}
|
||||
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 className="block text-sm font-semibold">{location.name}</span>
|
||||
<span className="mt-0.5 block text-xs text-muted">
|
||||
{[location.admin2, location.admin1, location.country].filter(Boolean).join(" · ")}
|
||||
</span>
|
||||
{selected.stationName && selected.distanceKm !== null ? (
|
||||
<span className="shrink-0 text-right text-[0.68rem] leading-5 text-muted">{t("location.nearest")}<br /><strong className="font-semibold text-foreground">{selected.stationName} · {selected.distanceKm} km</strong></span>
|
||||
) : (
|
||||
<span className="shrink-0 text-right text-[0.68rem] leading-5 text-muted">{t("location.modelSource")}<br /><strong className="font-semibold text-foreground">Open-Meteo</strong></span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</span>
|
||||
{selected.stationName && selected.distanceKm !== null ? (
|
||||
<span className="shrink-0 text-right text-[0.68rem] leading-5 text-muted">
|
||||
{t("location.nearest")}
|
||||
<br />
|
||||
<strong className="font-semibold text-foreground">
|
||||
{selected.stationName} · {selected.distanceKm} km
|
||||
</strong>
|
||||
</span>
|
||||
) : (
|
||||
<span className="shrink-0 text-right text-[0.68rem] leading-5 text-muted">
|
||||
{t("location.modelSource")}
|
||||
<br />
|
||||
<strong className="font-semibold text-foreground">Open-Meteo</strong>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -91,12 +132,24 @@ export function LocationSearch({ stations, positions }: { stations: SynopStation
|
||||
{selectedLocation && (
|
||||
<p className="mt-3 px-1 text-xs text-muted">
|
||||
{selectedLocation.stationName && selectedLocation.distanceKm !== null
|
||||
? t("location.currentSource", { location: selectedLocation.name, station: selectedLocation.stationName, distance: selectedLocation.distanceKm })
|
||||
? t("location.currentSource", {
|
||||
location: selectedLocation.name,
|
||||
station: selectedLocation.stationName,
|
||||
distance: selectedLocation.distanceKm,
|
||||
})
|
||||
: t("location.currentSourceGlobal", { location: selectedLocation.name })}
|
||||
</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>
|
||||
{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>
|
||||
|
||||
Reference in New Issue
Block a user