feat(weather): 6-day forecast
FORECAST_DAYS=6 in lib/weather.ts is the single source for the fetch length and the UI labels (spot card + map popup). New count-interpolated labels spotUi.forecastDays / mapUi.forecastDays (en/es; other locales fall back to English). Legend colours moved to lib/skateabilityLevels.ts to keep the react-refresh lint at zero.
This commit is contained in:
parent
e47c4b9d81
commit
54882285b6
8 changed files with 34 additions and 18 deletions
|
|
@ -2411,7 +2411,11 @@ composed in `legacy/index.ts` and spread into `en`/`es` (same pattern as
|
|||
weather card, **sunrise/sunset** (`weather.ts` fetches Open-Meteo daily
|
||||
`sunrise,sunset`), and the score's reasons now render as **chips** instead of a
|
||||
bullet-joined run-on. Popup `text-gray-500` → `gray-300` for contrast. Units stay
|
||||
**Celsius / km/h** (deliberate — no unit toggle).
|
||||
**Celsius / km/h** (deliberate — no unit toggle). The forecast is now **6 days**
|
||||
(`FORECAST_DAYS` in `lib/weather.ts` is the single source for the fetch and the
|
||||
labels; the legend colours live in `lib/skateabilityLevels.ts`). Labels use
|
||||
`spotUi.forecastDays` / `mapUi.forecastDays` with a `{{count}}` param (the other
|
||||
locales fall back to English until translated).
|
||||
- `skaterLabelsUi` (21) — skater stance/style/level + sponsor tiers in `types.ts`
|
||||
- `notifyUi` (28) — OS notification titles/bodies (`OsNotificationBridge`)
|
||||
- `legacyMiscUi` (~160) — scattered component/page literals, image `alt`/`aria-label`,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import type { BtcMapPlace } from '@/hooks/useBtcMapPlaces';
|
|||
import { isRecentlyVerified, isBoosted } from '@/hooks/useBtcMapPlaces';
|
||||
import { sanitizeUrl } from '@/lib/sanitizeUrl';
|
||||
import { createCachedTileLayer } from '@/lib/cachedTileLayer';
|
||||
import { FORECAST_DAYS } from '@/lib/weather';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
|
||||
/** Translation function shape, so module-level popup builders can receive `t`. */
|
||||
|
|
@ -627,7 +628,7 @@ function createPopupContent(
|
|||
|
||||
const forecastTitle = document.createElement('div');
|
||||
forecastTitle.className = 'text-xs font-semibold mb-1 text-gray-700';
|
||||
forecastTitle.textContent = t('mapUi.threeDayForecast');
|
||||
forecastTitle.textContent = t('mapUi.forecastDays', { count: FORECAST_DAYS });
|
||||
forecastDiv.appendChild(forecastTitle);
|
||||
|
||||
const forecastGrid = document.createElement('div');
|
||||
|
|
|
|||
|
|
@ -1,17 +1,5 @@
|
|||
import { useTranslation } from '@/contexts/language';
|
||||
|
||||
/**
|
||||
* The skateability colour scale, mirroring `calculateSkateability` in lib/weather.
|
||||
* Kept here (not derived at runtime) so the legend and the scorer can't drift
|
||||
* silently — if the thresholds change there, change them here too.
|
||||
*/
|
||||
export const SKATEABILITY_LEVELS: { key: string; color: string }[] = [
|
||||
{ key: 'weatherUi.skateability.perfect', color: '#00e400' },
|
||||
{ key: 'weatherUi.skateability.good', color: '#92d050' },
|
||||
{ key: 'weatherUi.skateability.okay', color: '#ffff00' },
|
||||
{ key: 'weatherUi.skateability.poor', color: '#ff7e00' },
|
||||
{ key: 'weatherUi.skateability.bad', color: '#ff0000' },
|
||||
];
|
||||
import { SKATEABILITY_LEVELS } from '@/lib/skateabilityLevels';
|
||||
|
||||
/**
|
||||
* Small map overlay explaining the weather-layer marker colours. Rendered by
|
||||
|
|
|
|||
|
|
@ -934,6 +934,7 @@ mapUi: {
|
|||
humidity: 'Humidity',
|
||||
airQuality: 'Air quality',
|
||||
threeDayForecast: '3-Day Forecast',
|
||||
forecastDays: '{{count}}-Day Forecast',
|
||||
gearForSale: 'Gear for sale ({{count}})',
|
||||
viewItems: 'View Items →',
|
||||
viewAllItems: 'View All {{count}} Items →',
|
||||
|
|
@ -1366,6 +1367,7 @@ spotUi: {
|
|||
tagsSuggestedDesc: 'Your tag suggestions have been submitted.',
|
||||
takePhoto: 'Take Photo',
|
||||
threeDayForecast: '3-Day Forecast',
|
||||
forecastDays: '{{count}}-Day Forecast',
|
||||
tipTheMapper: 'Tip the Mapper',
|
||||
tipTheMapperDesc: 'Found this spot useful? Send a zap to thank the mapper!',
|
||||
titleRequired: 'Title required',
|
||||
|
|
|
|||
|
|
@ -934,6 +934,7 @@ mapUi: {
|
|||
humidity: 'Humedad',
|
||||
airQuality: 'Calidad del aire',
|
||||
threeDayForecast: 'Previsión a 3 días',
|
||||
forecastDays: 'Pronóstico de {{count}} días',
|
||||
gearForSale: 'Equipo en venta ({{count}})',
|
||||
viewItems: 'Ver artículos →',
|
||||
viewAllItems: 'Ver los {{count}} artículos →',
|
||||
|
|
@ -1366,6 +1367,7 @@ spotUi: {
|
|||
tagsSuggestedDesc: 'Tus sugerencias de etiquetas se han enviado.',
|
||||
takePhoto: 'Hacer foto',
|
||||
threeDayForecast: 'Previsión a 3 días',
|
||||
forecastDays: 'Pronóstico de {{count}} días',
|
||||
tipTheMapper: 'Invita al autor',
|
||||
tipTheMapperDesc: '¿Te ha servido este spot? ¡Envía un zap para agradecer al autor!',
|
||||
titleRequired: 'Título obligatorio',
|
||||
|
|
|
|||
15
src/lib/skateabilityLevels.ts
Normal file
15
src/lib/skateabilityLevels.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* The skateability colour scale, shown in the map's weather legend.
|
||||
*
|
||||
* Kept as a plain module (not exported from a component file — that trips the
|
||||
* `react-refresh` lint rule) and mirrors the colours emitted by
|
||||
* `calculateSkateability` in `lib/weather.ts`. If a threshold there changes,
|
||||
* change it here too.
|
||||
*/
|
||||
export const SKATEABILITY_LEVELS: { key: string; color: string }[] = [
|
||||
{ key: 'weatherUi.skateability.perfect', color: '#00e400' },
|
||||
{ key: 'weatherUi.skateability.good', color: '#92d050' },
|
||||
{ key: 'weatherUi.skateability.okay', color: '#ffff00' },
|
||||
{ key: 'weatherUi.skateability.poor', color: '#ff7e00' },
|
||||
{ key: 'weatherUi.skateability.bad', color: '#ff0000' },
|
||||
];
|
||||
|
|
@ -190,8 +190,11 @@ export async function fetchAirQuality(lat: number, lng: number): Promise<AirQual
|
|||
};
|
||||
}
|
||||
|
||||
/** Forecast length requested and displayed (Open-Meteo supports up to 16 days). */
|
||||
export const FORECAST_DAYS = 6;
|
||||
|
||||
/**
|
||||
* Fetch 3-day weather forecast for a location
|
||||
* Fetch a multi-day weather forecast for a location
|
||||
*/
|
||||
export async function fetchWeatherForecast(lat: number, lng: number): Promise<WeatherForecast> {
|
||||
const params = new URLSearchParams({
|
||||
|
|
@ -200,7 +203,7 @@ export async function fetchWeatherForecast(lat: number, lng: number): Promise<We
|
|||
daily: 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max,sunrise,sunset',
|
||||
temperature_unit: 'celsius',
|
||||
timezone: 'auto',
|
||||
forecast_days: '3',
|
||||
forecast_days: String(FORECAST_DAYS),
|
||||
});
|
||||
|
||||
const response = await fetch(`https://api.open-meteo.com/v1/forecast?${params}`);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { dateLocaleCode, formatDistanceToNow } from '@/lib/dateFnsI18n';
|
||||
import { FORECAST_DAYS } from '@/lib/weather';
|
||||
import { labProfilePath } from '@/lib/labRoutes';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
|
|
@ -1142,7 +1143,7 @@ export default function SpotDetailPage() {
|
|||
{/* 3-Day Forecast */}
|
||||
{weatherData.forecast?.daily && weatherData.forecast.daily.length > 0 && (
|
||||
<div className="pt-3 border-t border-border">
|
||||
<div className="text-sm font-semibold mb-3">{t('spotUi.threeDayForecast')}</div>
|
||||
<div className="text-sm font-semibold mb-3">{t('spotUi.forecastDays', { count: FORECAST_DAYS })}</div>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{weatherData.forecast.daily.map((day, idx) => {
|
||||
const date = new Date(day.date);
|
||||
|
|
|
|||
Loading…
Reference in a new issue