remove sign up prompts
This commit is contained in:
parent
77d33c185b
commit
70182d8bb7
10 changed files with 31 additions and 138 deletions
|
|
@ -1,114 +1,34 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { pixelLabel } from "@/lib/styles";
|
import { pixelLabel } from "@/lib/styles";
|
||||||
import type { ProviderId, ProviderInfo } from "@/lib/weather/types";
|
import type { ProviderId, ProviderInfo } from "@/lib/weather/types";
|
||||||
import { Slot } from "./Slot";
|
|
||||||
import { ChoiceChips } from "./ui/ChoiceChips";
|
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
|
* Switcher between active weather data sources.
|
||||||
* configured plan. Providers without an API key stay visible with an optional
|
* Only configured providers are shown; hidden automatically if only one source is active.
|
||||||
* setup guide so adding free keys to `.env.local` is self-explanatory.
|
|
||||||
*/
|
*/
|
||||||
export function ProviderPicker({ providers, selected, onChange }: {
|
export function ProviderPicker({ providers, selected, onChange }: {
|
||||||
providers: ProviderInfo[];
|
providers: ProviderInfo[];
|
||||||
selected: ProviderId;
|
selected: ProviderId;
|
||||||
onChange: (id: ProviderId) => void;
|
onChange: (id: ProviderId) => void;
|
||||||
}) {
|
}) {
|
||||||
const [showKeyGuide, setShowKeyGuide] = useState(false);
|
if (providers.length <= 1) return null;
|
||||||
const missingProviders = providers.filter((p) => !p.ready);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1.5">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<span className={pixelLabel}>Source</span>
|
||||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-1.5">
|
<ChoiceChips
|
||||||
<span className={pixelLabel}>Source</span>
|
legend="Weather data source"
|
||||||
<ChoiceChips
|
size="sm"
|
||||||
legend="Weather data source"
|
selected={selected}
|
||||||
size="sm"
|
onChange={onChange}
|
||||||
selected={selected}
|
choices={providers.map((p) => ({
|
||||||
onChange={onChange}
|
value: p.id,
|
||||||
choices={providers.map((p) => ({
|
label: p.label,
|
||||||
value: p.id,
|
note: `${p.forecastDays}d`,
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,11 +106,9 @@ export default function WeatherApp() {
|
||||||
fetchProviders()
|
fetchProviders()
|
||||||
.then((list) => {
|
.then((list) => {
|
||||||
setProviders(list);
|
setProviders(list);
|
||||||
// Prefer OpenWeather if its key was just added and user hasn't explicitly saved a choice;
|
const savedIsAvailable = list.some((p) => p.id === saved.provider);
|
||||||
// otherwise keep saved provider if ready, or fall back to the first ready source.
|
const fallback = list[0]?.id ?? saved.provider;
|
||||||
const savedIsReady = list.some((p) => p.id === saved.provider && p.ready);
|
setSettings({ ...saved, provider: savedIsAvailable ? saved.provider : fallback });
|
||||||
const fallback = list.find((p) => p.ready)?.id ?? saved.provider;
|
|
||||||
setSettings({ ...saved, provider: savedIsReady ? saved.provider : fallback });
|
|
||||||
})
|
})
|
||||||
.catch(() => setSettings(saved))
|
.catch(() => setSettings(saved))
|
||||||
.finally(() => setLoaded(true));
|
.finally(() => setLoaded(true));
|
||||||
|
|
@ -146,7 +144,7 @@ export default function WeatherApp() {
|
||||||
|
|
||||||
const activeProvider = providers.find((p) => p.id === settings.provider);
|
const activeProvider = providers.find((p) => p.id === settings.provider);
|
||||||
const reportProvider = providers.find((p) => p.id === report?.provider);
|
const reportProvider = providers.find((p) => p.id === report?.provider);
|
||||||
const hasOpenWeather = providers.some((p) => p.id === "openweather" && p.ready);
|
const hasOpenWeather = providers.some((p) => p.id === "openweather");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ export function DailyView({ daily, units, timeZone, providerLabel, maxDaysOnPlan
|
||||||
|
|
||||||
{daily.length < 14 && (
|
{daily.length < 14 && (
|
||||||
<p className={pixelLabel}>
|
<p className={pixelLabel}>
|
||||||
{providerLabel} gives {maxDaysOnPlan} days on this plan. Choose Open-Meteo for 14.
|
{providerLabel} provides a {maxDaysOnPlan}-day outlook. Switch to Open-Meteo above for 14 days.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ import { useId } from "react";
|
||||||
export interface Choice<T extends string> {
|
export interface Choice<T extends string> {
|
||||||
value: T;
|
value: T;
|
||||||
label: string;
|
label: string;
|
||||||
note?: string; // small extra text, e.g. "14d" or "No key"
|
note?: string; // small extra text, e.g. "14d"
|
||||||
noteTone?: "muted" | "subtle" | "warning";
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,19 +53,7 @@ export function ChoiceChips<T extends string>({ legend, choices, selected, onCha
|
||||||
onChange={() => onChange(choice.value)}
|
onChange={() => onChange(choice.value)}
|
||||||
/>
|
/>
|
||||||
{choice.label}
|
{choice.label}
|
||||||
{choice.note && (
|
{choice.note && <span className="text-teal/90">{choice.note}</span>}
|
||||||
<span
|
|
||||||
className={
|
|
||||||
choice.noteTone === "warning"
|
|
||||||
? "text-heart"
|
|
||||||
: choice.noteTone === "subtle"
|
|
||||||
? "text-[0.65rem] text-stone/80"
|
|
||||||
: "text-teal/90"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{choice.note}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
|
||||||
|
|
@ -28,18 +28,13 @@ export function isReady(provider: WeatherProvider): boolean {
|
||||||
return provider.envKey === null || Boolean(process.env[provider.envKey]?.trim());
|
return provider.envKey === null || Boolean(process.env[provider.envKey]?.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provider list for the browser — readiness and limits only, never the keys.
|
/** Configured providers available to the browser — never exposes keys or unconfigured sources. */
|
||||||
* Ready sources lead so the picker starts with working options out of the box. */
|
|
||||||
export function listProviders(): ProviderInfo[] {
|
export function listProviders(): ProviderInfo[] {
|
||||||
const all = DISPLAY_ORDER.map((id) => {
|
return DISPLAY_ORDER.map((id) => PROVIDERS[id])
|
||||||
const provider = PROVIDERS[id];
|
.filter(isReady)
|
||||||
return {
|
.map((provider) => ({
|
||||||
id,
|
id: provider.id,
|
||||||
label: provider.label,
|
label: provider.label,
|
||||||
ready: isReady(provider),
|
|
||||||
signupUrl: provider.signupUrl,
|
|
||||||
...provider.limits(),
|
...provider.limits(),
|
||||||
};
|
}));
|
||||||
});
|
|
||||||
return [...all.filter((p) => p.ready), ...all.filter((p) => !p.ready)];
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ export const openMeteo: WeatherProvider = {
|
||||||
id: "openmeteo",
|
id: "openmeteo",
|
||||||
label: "Open-Meteo",
|
label: "Open-Meteo",
|
||||||
envKey: null,
|
envKey: null,
|
||||||
signupUrl: "https://open-meteo.com/",
|
|
||||||
limits: () => ({ forecastDays: 14, hourlyStepHours: 1 }),
|
limits: () => ({ forecastDays: 14, hourlyStepHours: 1 }),
|
||||||
|
|
||||||
async getForecast(lat, lon) {
|
async getForecast(lat, lon) {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ export const openWeather: WeatherProvider = {
|
||||||
id: "openweather",
|
id: "openweather",
|
||||||
label: "OpenWeather",
|
label: "OpenWeather",
|
||||||
envKey: "OPENWEATHER_API_KEY",
|
envKey: "OPENWEATHER_API_KEY",
|
||||||
signupUrl: "https://home.openweathermap.org/users/sign_up",
|
|
||||||
limits: () => (usesOneCall() ? { forecastDays: 8, hourlyStepHours: 1 } : { forecastDays: 5, hourlyStepHours: 3 }),
|
limits: () => (usesOneCall() ? { forecastDays: 8, hourlyStepHours: 1 } : { forecastDays: 5, hourlyStepHours: 3 }),
|
||||||
|
|
||||||
getForecast(lat, lon) {
|
getForecast(lat, lon) {
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,6 @@ export const tomorrowIo: WeatherProvider = {
|
||||||
id: "tomorrow",
|
id: "tomorrow",
|
||||||
label: "Tomorrow.io",
|
label: "Tomorrow.io",
|
||||||
envKey: "TOMORROW_API_KEY",
|
envKey: "TOMORROW_API_KEY",
|
||||||
signupUrl: "https://app.tomorrow.io/signup",
|
|
||||||
limits: () => ({ forecastDays: 6, hourlyStepHours: 1 }),
|
limits: () => ({ forecastDays: 6, hourlyStepHours: 1 }),
|
||||||
|
|
||||||
async getForecast(lat, lon) {
|
async getForecast(lat, lon) {
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,6 @@ export const weatherApi: WeatherProvider = {
|
||||||
id: "weatherapi",
|
id: "weatherapi",
|
||||||
label: "WeatherAPI",
|
label: "WeatherAPI",
|
||||||
envKey: "WEATHERAPI_API_KEY",
|
envKey: "WEATHERAPI_API_KEY",
|
||||||
signupUrl: "https://www.weatherapi.com/signup.aspx",
|
|
||||||
// Set WEATHERAPI_PLAN=paid in .env.local if your plan includes 14 days.
|
// Set WEATHERAPI_PLAN=paid in .env.local if your plan includes 14 days.
|
||||||
limits: () => ({ forecastDays: process.env.WEATHERAPI_PLAN === "paid" ? 14 : 3, hourlyStepHours: 1 }),
|
limits: () => ({ forecastDays: process.env.WEATHERAPI_PLAN === "paid" ? 14 : 3, hourlyStepHours: 1 }),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,18 +121,15 @@ export interface WeatherProvider {
|
||||||
label: string;
|
label: string;
|
||||||
/** Name of the env var holding its key, or null if no key is needed. */
|
/** Name of the env var holding its key, or null if no key is needed. */
|
||||||
envKey: string | null;
|
envKey: string | null;
|
||||||
signupUrl: string;
|
/** Forecast horizon available on the configured tier. */
|
||||||
/** What this provider can deliver on the plan configured in .env.local. */
|
|
||||||
limits(): { forecastDays: number; hourlyStepHours: 1 | 3 };
|
limits(): { forecastDays: number; hourlyStepHours: 1 | 3 };
|
||||||
getForecast(lat: number, lon: number): Promise<ProviderForecast>;
|
getForecast(lat: number, lon: number): Promise<ProviderForecast>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Safe-to-share provider info for the browser (never includes keys). */
|
/** Configured provider metadata exposed to the browser (never includes keys). */
|
||||||
export interface ProviderInfo {
|
export interface ProviderInfo {
|
||||||
id: ProviderId;
|
id: ProviderId;
|
||||||
label: string;
|
label: string;
|
||||||
ready: boolean;
|
|
||||||
signupUrl: string;
|
|
||||||
forecastDays: number;
|
forecastDays: number;
|
||||||
hourlyStepHours: 1 | 3;
|
hourlyStepHours: 1 | 3;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue