feat: add weather widget
All checks were successful
renovate / renovate (push) Successful in 1m3s
test-build / build (push) Successful in 1m47s

This commit is contained in:
2026-02-10 21:42:04 -06:00
parent 63cbcdf39b
commit b6dfc738f1
15 changed files with 164 additions and 25 deletions

View File

@@ -1,6 +1,6 @@
import fs from 'node:fs/promises';
export interface BlurImageMetadata {
interface BlurImageMetadata {
/**
* The width of the origin image
*/
@@ -23,7 +23,7 @@ export interface BlurImageMetadata {
blurHeight: number;
}
export async function blurStyle(filePath: string) {
async function blurStyle(filePath: string) {
const image = await blurImageMetadata(filePath);
const svg = blurImageSVG(image);
return {
@@ -64,3 +64,5 @@ async function blurImageMetadata(filepath: string): Promise<BlurImageMetadata> {
return { blurDataURL, blurHeight, blurWidth, width, height };
}
export { blurStyle };

View File

@@ -1,6 +1,6 @@
import { join } from 'node:path';
export function resolveFilePath(path: string) {
function resolveFilePath(path: string) {
if (path.startsWith('/')) {
return resolveFilePathPublic(path);
}
@@ -8,12 +8,14 @@ export function resolveFilePath(path: string) {
return resolveFilePathInternal(path);
}
export function resolveFilePathPublic(path: string) {
function resolveFilePathPublic(path: string) {
return join(process.cwd(), path);
}
export function resolveFilePathInternal(path: string) {
function resolveFilePathInternal(path: string) {
const normalizePath = path.startsWith('@') ? path.replace('@', '') : path;
return join(process.cwd(), 'src/', normalizePath);
}
export { resolveFilePath, resolveFilePathPublic, resolveFilePathInternal };

67
src/support/weather.ts Normal file
View File

@@ -0,0 +1,67 @@
interface DayForecast {
date: string;
temp: number;
code: number;
label: string;
icon: string;
dayName: string;
}
interface WeatherResult {
forecastDays: DayForecast[];
error: string | null;
}
const getWeatherInfo = (code: number) => {
if (code === 0) return { label: 'Clear', icon: '01d' };
if (code >= 1 && code <= 3) return { label: 'Partly Cloudy', icon: '02d' };
if (code === 45 || code === 48) return { label: 'Foggy', icon: '50d' };
if (code >= 51 && code <= 55) return { label: 'Drizzle', icon: '09d' };
if (code >= 61 && code <= 65) return { label: 'Rainy', icon: '10d' };
if (code === 66 || code === 67) return { label: 'Freezing Rain', icon: '13d' };
if (code >= 71 && code <= 75) return { label: 'Snowy', icon: '13d' };
if (code === 77) return { label: 'Snow Grains', icon: '13d' };
if (code >= 80 && code <= 82) return { label: 'Showers', icon: '09d' };
if (code === 85 || code === 86) return { label: 'Snow Showers', icon: '13d' };
if (code >= 95 && code <= 99) return { label: 'Stormy', icon: '11d' };
return { label: 'Unknown', icon: '03d' };
};
const getDayName = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('en-US', { weekday: 'short' });
};
async function getFiveDayForecast(latitude: string, longitude: string): Promise<WeatherResult> {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=weather_code,temperature_2m_max&timezone=auto&temperature_unit=fahrenheit`;
try {
const response = await fetch(url);
if (!response.ok) throw new Error("Weather service unavailable");
const data = await response.json();
const forecastDays = data.daily.time.map((date: string, index: number): DayForecast => {
const code = data.daily.weather_code[index];
const info = getWeatherInfo(code);
return {
date,
temp: Math.round(data.daily.temperature_2m_max[index]),
code,
label: info.label,
icon: info.icon,
dayName: getDayName(date)
};
}).slice(0, 5);
return { forecastDays, error: null };
} catch (e: unknown) {
return {
forecastDays: [],
error: e instanceof Error ? e.message : "An unexpected error occurred"
};
}
}
export { getFiveDayForecast };