diff --git a/app/components/ModuleItem.vue b/app/components/ModuleItem.vue index 943b7a0..25f95a7 100644 --- a/app/components/ModuleItem.vue +++ b/app/components/ModuleItem.vue @@ -11,7 +11,7 @@ const moduleDef = computed(() => modules[props.item.moduleName]); // Create a render function component on the fly const RenderedModule = () => { if (moduleDef.value && typeof moduleDef.value.render === 'function') { - return moduleDef.value.render(); + return moduleDef.value.render(props.item.params || {}); } return null; }; diff --git a/app/dashboard.yml b/app/dashboard.yml index 595164f..43d59e9 100644 --- a/app/dashboard.yml +++ b/app/dashboard.yml @@ -3,6 +3,17 @@ moduleName: clock parentId: null order: -1 + params: + label: "Local System Time" +- id: weather-widget + type: module + moduleName: weather + parentId: null + order: 0 + params: + location: "Ninfield" + lat: 50.8765044 + lng: 0.3943669 - id: media-folder type: folder parentId: null diff --git a/app/modules/clock.ts b/app/modules/clock.ts index 752ec45..b786524 100644 --- a/app/modules/clock.ts +++ b/app/modules/clock.ts @@ -5,7 +5,9 @@ export const ClockModule: DashboardModule = { name: 'clock', width: 2, height: 1, - render() { + render(params) { + const label = params.label || 'Local Time'; + return h(defineComponent({ setup() { const time = ref(new Date().toLocaleTimeString()); @@ -25,7 +27,7 @@ export const ClockModule: DashboardModule = { 'div', { class: 'w-full h-full flex flex-col items-center justify-center p-5 rounded-2xl bg-white/10 backdrop-blur-md border border-white/10 shadow-lg text-white group cursor-default transition-all' }, [ - h('span', { class: 'text-sm text-white/50 mb-1 uppercase tracking-widest' }, 'Local Time'), + h('span', { class: 'text-sm text-white/50 mb-1 uppercase tracking-widest' }, label), h('span', { class: 'text-3xl font-light tracking-tight text-shadow' }, time.value) ] ); diff --git a/app/modules/index.ts b/app/modules/index.ts index d6fb24e..2de01a6 100644 --- a/app/modules/index.ts +++ b/app/modules/index.ts @@ -1,7 +1,9 @@ import type { DashboardModule } from '../types/module'; import { ClockModule } from './clock'; +import { WeatherModule } from './weather'; // Register all modules here export const modules: Record = { - [ClockModule.name]: ClockModule + [ClockModule.name]: ClockModule, + [WeatherModule.name]: WeatherModule }; diff --git a/app/modules/weather.ts b/app/modules/weather.ts new file mode 100644 index 0000000..db2649d --- /dev/null +++ b/app/modules/weather.ts @@ -0,0 +1,95 @@ +import { h, ref, onMounted, defineComponent } from 'vue'; +import { UIcon } from '#components'; +import type { DashboardModule } from '../types/module'; + +// Map WMO weather codes to simple descriptions and Lucide icons +const getWeatherInfo = (code: number) => { + if (code === 0) return { label: 'Clear', icon: 'i-wi-day-sunny', color: 'text-yellow-400' }; + if (code >= 1 && code < 4) return { label: 'Partly Cloudy', icon: 'i-wi-day-cloudy', color: 'text-gray-300' }; + if (code >= 4 && code < 45) return { label: 'Cloudy', icon: 'i-wi-cloud', color: 'text-gray-300' }; + if (code === 45 || code === 48) return { label: 'Fog', icon: 'i-wi-fog', color: 'text-gray-400' }; + if (code >= 51 && code <= 55) return { label: 'Drizzle', icon: 'i-wi-rain-mix', color: 'text-blue-300' }; + if (code >= 61 && code <= 65) return { label: 'Rain', icon: 'i-wi-rain', color: 'text-blue-400' }; + if (code >= 71 && code <= 75) return { label: 'Snow', icon: 'i-wi-snow', color: 'text-white' }; + if (code >= 80 && code <= 82) return { label: 'Showers', icon: 'i-wi-rain-wind', color: 'text-blue-500' }; + if (code >= 95) return { label: 'Storm', icon: 'i-wi-storm-showers', color: 'text-purple-400' }; + return { label: 'Unknown', icon: 'i-wi-na', color: 'text-gray-400' }; +}; + +export const WeatherModule: DashboardModule = { + name: 'weather', + width: 2, + height: 1, + render(params) { + const locationName = params?.location || (params?.lat && params?.lng ? `${params.lat}, ${params.lng}` : 'London'); + const inputLat = params?.lat; + const inputLng = params?.lng; + + return h(defineComponent({ + setup() { + const temp = ref(null); + const condition = ref<{label: string, icon: string, color: string} | null>(null); + const loading = ref(true); + const error = ref(false); + + onMounted(async () => { + try { + let latitude = inputLat; + let longitude = inputLng; + + // 1. Get coordinates for location if lat/lng are not provided + if (latitude === undefined || longitude === undefined) { + const geoRes = await $fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(locationName)}&count=1`); + + if (!geoRes.results || geoRes.results.length === 0) { + throw new Error('Location not found'); + } + + latitude = geoRes.results[0].latitude; + longitude = geoRes.results[0].longitude; + } + + // 2. Fetch current weather + const weatherRes = await $fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t_weather=true`); + + temp.value = Math.round(weatherRes.current_weather.temperature); + condition.value = getWeatherInfo(weatherRes.current_weather.weathercode); + } catch (err) { + console.error(err); + error.value = true; + } finally { + loading.value = false; + } + }); + + return () => { + let content; + + if (loading.value) { + content = h('div', { class: 'text-white/50 text-sm animate-pulse' }, 'Loading weather...'); + } else if (error.value || !condition.value) { + content = h('div', { class: 'text-red-400/80 text-sm' }, 'Failed to load weather'); + } else { + // The resolved weather info + content = h('div', { class: 'flex items-center gap-4 w-full justify-center' }, [ + h(UIcon, { name: condition.value.icon, class: `${condition.value.color} w-20 h-20 drop-shadow-lg` }), + h('div', { class: 'flex flex-col' }, [ + h('span', { class: 'text-3xl font-light tracking-tight text-shadow text-white' }, `${temp.value}°C`), + h('span', { class: 'text-sm text-white/50 font-medium tracking-wide' }, condition.value.label) + ]) + ]); + } + + return h( + 'div', + { class: 'w-full h-full flex flex-col items-center justify-center p-5 rounded-2xl bg-white/10 backdrop-blur-md border border-white/10 shadow-lg group transition-all relative overflow-hidden' }, + [ + h('div', { class: 'absolute top-3 left-4 text-[10px] text-white/40 uppercase tracking-widest font-bold' }, locationName), + content + ] + ); + }; + } + })); + } +}; diff --git a/app/types/module.ts b/app/types/module.ts index 0ef6680..55d1d04 100644 --- a/app/types/module.ts +++ b/app/types/module.ts @@ -4,5 +4,5 @@ export interface DashboardModule { name: string; width: number; // Number of grid columns to span height: number; // Number of grid rows to span - render: () => VNode | any; + render: (params: Record) => VNode | any; } diff --git a/nuxt.config.ts b/nuxt.config.ts index 059f8f6..459e7a9 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -16,6 +16,12 @@ export default defineNuxtConfig({ }, compatibilityDate: '2025-01-15', + + icon: { + serverBundle: { + collections: ['wi', 'lucide', 'simple-icons'] + } + }, eslint: { config: { diff --git a/package.json b/package.json index 2e192d1..2d5d203 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "vuedraggable": "^4.1.0" }, "devDependencies": { + "@iconify-json/wi": "^1.2.1", "@modyfi/vite-plugin-yaml": "^1.1.1", "@nuxt/eslint": "^1.15.2", "@types/js-yaml": "^4.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04fcf49..33b5c91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: specifier: ^4.1.0 version: 4.1.0(vue@3.5.31(typescript@6.0.2)) devDependencies: + '@iconify-json/wi': + specifier: ^1.2.1 + version: 1.2.1 '@modyfi/vite-plugin-yaml': specifier: ^1.1.1 version: 1.1.1(rollup@4.60.1)(vite@7.3.1(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -477,6 +480,9 @@ packages: '@iconify-json/simple-icons@1.2.78': resolution: {integrity: sha512-I3lkNp0Qu7q2iZWkdcf/I2hqGhzK6qxdILh9T7XqowQrnpmG/BayDsiCf6PktDoWlW0U971xA5g+panm+NFrfQ==} + '@iconify-json/wi@1.2.1': + resolution: {integrity: sha512-I4rLsMC0ta51SzapStkF253WMX7CwjwuAYSa8uWNDOHqM4kYXfkyMbATAQ/oRTOKXiqd/eHCL/RzI5sDvCh8lA==} + '@iconify/collections@1.0.667': resolution: {integrity: sha512-VJYrtbHyi7HAlSpGv9qsVZMi94bVbrOasCmHSRn0Deu/neOmGUywqO7Ufi0gG1Cmvh/amfZeoXlgj9ErGAhEIg==} @@ -5713,6 +5719,10 @@ snapshots: dependencies: '@iconify/types': 2.0.0 + '@iconify-json/wi@1.2.1': + dependencies: + '@iconify/types': 2.0.0 + '@iconify/collections@1.0.667': dependencies: '@iconify/types': 2.0.0