This commit is contained in:
@@ -11,7 +11,7 @@ const moduleDef = computed(() => modules[props.item.moduleName]);
|
|||||||
// Create a render function component on the fly
|
// Create a render function component on the fly
|
||||||
const RenderedModule = () => {
|
const RenderedModule = () => {
|
||||||
if (moduleDef.value && typeof moduleDef.value.render === 'function') {
|
if (moduleDef.value && typeof moduleDef.value.render === 'function') {
|
||||||
return moduleDef.value.render();
|
return moduleDef.value.render(props.item.params || {});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,17 @@
|
|||||||
moduleName: clock
|
moduleName: clock
|
||||||
parentId: null
|
parentId: null
|
||||||
order: -1
|
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
|
- id: media-folder
|
||||||
type: folder
|
type: folder
|
||||||
parentId: null
|
parentId: null
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ export const ClockModule: DashboardModule = {
|
|||||||
name: 'clock',
|
name: 'clock',
|
||||||
width: 2,
|
width: 2,
|
||||||
height: 1,
|
height: 1,
|
||||||
render() {
|
render(params) {
|
||||||
|
const label = params.label || 'Local Time';
|
||||||
|
|
||||||
return h(defineComponent({
|
return h(defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const time = ref(new Date().toLocaleTimeString());
|
const time = ref(new Date().toLocaleTimeString());
|
||||||
@@ -25,7 +27,7 @@ export const ClockModule: DashboardModule = {
|
|||||||
'div',
|
'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' },
|
{ 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)
|
h('span', { class: 'text-3xl font-light tracking-tight text-shadow' }, time.value)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { DashboardModule } from '../types/module';
|
import type { DashboardModule } from '../types/module';
|
||||||
import { ClockModule } from './clock';
|
import { ClockModule } from './clock';
|
||||||
|
import { WeatherModule } from './weather';
|
||||||
|
|
||||||
// Register all modules here
|
// Register all modules here
|
||||||
export const modules: Record<string, DashboardModule> = {
|
export const modules: Record<string, DashboardModule> = {
|
||||||
[ClockModule.name]: ClockModule
|
[ClockModule.name]: ClockModule,
|
||||||
|
[WeatherModule.name]: WeatherModule
|
||||||
};
|
};
|
||||||
|
|||||||
95
app/modules/weather.ts
Normal file
95
app/modules/weather.ts
Normal file
@@ -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<number | null>(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<any>(`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<any>(`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
|
||||||
|
]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,5 +4,5 @@ export interface DashboardModule {
|
|||||||
name: string;
|
name: string;
|
||||||
width: number; // Number of grid columns to span
|
width: number; // Number of grid columns to span
|
||||||
height: number; // Number of grid rows to span
|
height: number; // Number of grid rows to span
|
||||||
render: () => VNode | any;
|
render: (params: Record<string, any>) => VNode | any;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
compatibilityDate: '2025-01-15',
|
compatibilityDate: '2025-01-15',
|
||||||
|
|
||||||
|
icon: {
|
||||||
|
serverBundle: {
|
||||||
|
collections: ['wi', 'lucide', 'simple-icons']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
eslint: {
|
eslint: {
|
||||||
config: {
|
config: {
|
||||||
stylistic: {
|
stylistic: {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/wi": "^1.2.1",
|
||||||
"@modyfi/vite-plugin-yaml": "^1.1.1",
|
"@modyfi/vite-plugin-yaml": "^1.1.1",
|
||||||
"@nuxt/eslint": "^1.15.2",
|
"@nuxt/eslint": "^1.15.2",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -30,6 +30,9 @@ importers:
|
|||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0(vue@3.5.31(typescript@6.0.2))
|
version: 4.1.0(vue@3.5.31(typescript@6.0.2))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@iconify-json/wi':
|
||||||
|
specifier: ^1.2.1
|
||||||
|
version: 1.2.1
|
||||||
'@modyfi/vite-plugin-yaml':
|
'@modyfi/vite-plugin-yaml':
|
||||||
specifier: ^1.1.1
|
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))
|
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':
|
'@iconify-json/simple-icons@1.2.78':
|
||||||
resolution: {integrity: sha512-I3lkNp0Qu7q2iZWkdcf/I2hqGhzK6qxdILh9T7XqowQrnpmG/BayDsiCf6PktDoWlW0U971xA5g+panm+NFrfQ==}
|
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':
|
'@iconify/collections@1.0.667':
|
||||||
resolution: {integrity: sha512-VJYrtbHyi7HAlSpGv9qsVZMi94bVbrOasCmHSRn0Deu/neOmGUywqO7Ufi0gG1Cmvh/amfZeoXlgj9ErGAhEIg==}
|
resolution: {integrity: sha512-VJYrtbHyi7HAlSpGv9qsVZMi94bVbrOasCmHSRn0Deu/neOmGUywqO7Ufi0gG1Cmvh/amfZeoXlgj9ErGAhEIg==}
|
||||||
|
|
||||||
@@ -5713,6 +5719,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
|
'@iconify-json/wi@1.2.1':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
'@iconify/collections@1.0.667':
|
'@iconify/collections@1.0.667':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user