114 lines
4.5 KiB
TypeScript
114 lines
4.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { pixelLabel } from "@/lib/styles";
|
|
import type { ProviderId, ProviderInfo } from "@/lib/weather/types";
|
|
import { Slot } from "./Slot";
|
|
import { ChoiceChips } from "./ui/ChoiceChips";
|
|
|
|
const PROVIDER_KEY_HINTS: Record<ProviderId, { envVar: string | null; summary: string }> = {
|
|
openweather: {
|
|
envVar: "OPENWEATHER_API_KEY",
|
|
summary: "5-day forecast, reverse location naming, and 5 extra map layers (precip, clouds, temp, wind, pressure).",
|
|
},
|
|
weatherapi: {
|
|
envVar: "WEATHERAPI_API_KEY",
|
|
summary: "3-day hourly & daily forecast plus worldwide weather alerts (instant key activation).",
|
|
},
|
|
tomorrow: {
|
|
envVar: "TOMORROW_API_KEY",
|
|
summary: "5-day hourly & 6-day daily timelines (free tier: 25 calls/hr, 500/day).",
|
|
},
|
|
openmeteo: {
|
|
envVar: null,
|
|
summary: "14-day daily & 7-day hourly forecast — free out of the box, no API key required.",
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Provider choices. Each shows how many forecast days it delivers on the
|
|
* configured plan. Providers without an API key stay visible with an optional
|
|
* setup guide so adding free keys to `.env.local` is self-explanatory.
|
|
*/
|
|
export function ProviderPicker({ providers, selected, onChange }: {
|
|
providers: ProviderInfo[];
|
|
selected: ProviderId;
|
|
onChange: (id: ProviderId) => void;
|
|
}) {
|
|
const [showKeyGuide, setShowKeyGuide] = useState(false);
|
|
const missingProviders = providers.filter((p) => !p.ready);
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-1.5">
|
|
<span className={pixelLabel}>Source</span>
|
|
<ChoiceChips
|
|
legend="Weather data source"
|
|
size="sm"
|
|
selected={selected}
|
|
onChange={onChange}
|
|
choices={providers.map((p) => ({
|
|
value: p.id,
|
|
label: p.label,
|
|
disabled: !p.ready,
|
|
note: p.ready ? `${p.forecastDays}d` : "Locked",
|
|
noteTone: p.ready ? "muted" : "subtle",
|
|
}))}
|
|
/>
|
|
</div>
|
|
|
|
{missingProviders.length > 0 && (
|
|
<button
|
|
type="button"
|
|
aria-expanded={showKeyGuide}
|
|
onClick={() => setShowKeyGuide((open) => !open)}
|
|
className="cursor-pointer font-pixel text-[0.7rem] text-gold hover:text-gold-bright hover:underline"
|
|
>
|
|
{showKeyGuide ? "Hide key guide" : `+ Unlock ${missingProviders.length} ${missingProviders.length === 1 ? "source" : "sources"}`}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{showKeyGuide && missingProviders.length > 0 && (
|
|
<Slot className="[--cut:10px]" innerClassName="flex flex-col gap-3 p-4 text-sm">
|
|
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
<p className="font-display text-sm font-medium tracking-[0.06em] text-gold-bright">
|
|
Optional free API keys
|
|
</p>
|
|
<p className="text-xs text-stone">
|
|
Add keys to <code className="border border-gold-dim/60 bg-black px-1.5 py-0.5 font-pixel text-[0.68rem] text-ink">.env.local</code> and reload.
|
|
</p>
|
|
</div>
|
|
|
|
<ul className="grid gap-2.5 sm:grid-cols-3">
|
|
{missingProviders.map((p) => {
|
|
const info = PROVIDER_KEY_HINTS[p.id];
|
|
return (
|
|
<li key={p.id} className="flex flex-col justify-between gap-2 border border-gold-dim/60 bg-black/70 p-3">
|
|
<div className="flex flex-col gap-1">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<span className="font-medium text-ink">{p.label}</span>
|
|
{info.envVar && (
|
|
<code className="font-pixel text-[0.62rem] text-teal">{info.envVar}</code>
|
|
)}
|
|
</div>
|
|
<p className="text-xs leading-relaxed text-stone">{info.summary}</p>
|
|
</div>
|
|
<a
|
|
href={p.signupUrl}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="inline-flex items-center gap-1 self-start font-pixel text-[0.7rem] text-gold hover:text-gold-bright hover:underline"
|
|
>
|
|
Get free key <span aria-hidden="true">↗</span>
|
|
</a>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</Slot>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|