diff --git a/next.config.js b/next.config.js new file mode 100644 index 0000000..82a53e0 --- /dev/null +++ b/next.config.js @@ -0,0 +1,14 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'cdn.weatherapi.com', + pathname: '/weather/**', + }, + ], + }, +} + +module.exports = nextConfig \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8765c39..124fa5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@react-pdf/renderer": "^4.3.0", "framer-motion": "^12.12.1", "mongodb": "^6.3.0", + "mongoose": "^8.15.0", "next": "14.1.0", "pdf-lib": "^1.17.1", "pdfkit": "^0.17.1", @@ -1171,6 +1172,23 @@ "node": ">=12" } }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -1663,6 +1681,15 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -1845,6 +1872,28 @@ "whatwg-url": "^14.1.0 || ^13.0.0" } }, + "node_modules/mongoose": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.15.0.tgz", + "integrity": "sha512-WFKsY1q12ScGabnZWUB9c/QzZmz/ESorrV27OembB7Gz6rrh9m3GA4Srsv1uvW1s9AHO5DeZ6DdUTyF9zyNERQ==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.3", + "kareem": "2.6.3", + "mongodb": "~6.16.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, "node_modules/motion-dom": { "version": "12.12.1", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.12.1.tgz", @@ -1860,6 +1909,33 @@ "integrity": "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==", "license": "MIT" }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -2614,6 +2690,12 @@ "node": ">=8" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", diff --git a/package.json b/package.json index c585e0a..8bb0d1a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@react-pdf/renderer": "^4.3.0", "framer-motion": "^12.12.1", "mongodb": "^6.3.0", + "mongoose": "^8.15.0", "next": "14.1.0", "pdf-lib": "^1.17.1", "pdfkit": "^0.17.1", diff --git a/src/app/api/weather/route.js b/src/app/api/weather/route.js new file mode 100644 index 0000000..50d4ce7 --- /dev/null +++ b/src/app/api/weather/route.js @@ -0,0 +1,74 @@ +import { NextResponse } from 'next/server' +import { connectToDatabase } from '@/lib/mongodb' + +const CACHE_DURATION = 2 * 60 * 60 * 1000 // 2 hours in milliseconds +const COORDS = { + lat: 47.06149235737582, + lon: 28.86683069635403 +} + +async function fetchWeatherFromAPI() { + const response = await fetch( + `http://api.weatherapi.com/v1/forecast.json?key=${process.env.WEATHER_API_KEY}&q=${COORDS.lat},${COORDS.lon}&days=7&aqi=no` + ) + + if (!response.ok) { + throw new Error('Failed to fetch weather data') + } + + return response.json() +} + +export async function GET() { + try { + // Ensure database connection + const { db } = await connectToDatabase() + + // Check if we have cached data + const cachedWeather = await db.collection('weather').findOne({ + 'location.lat': COORDS.lat, + 'location.lon': COORDS.lon + }) + + const now = new Date() + const isCacheValid = cachedWeather && + (now - new Date(cachedWeather.lastUpdated)) < CACHE_DURATION + + // If cache is valid, return cached data + if (isCacheValid) { + return NextResponse.json(cachedWeather) + } + + // If no cache or cache is invalid, fetch fresh data + const weatherData = await fetchWeatherFromAPI() + + // Add metadata to the weather data + const weatherWithMetadata = { + ...weatherData, + location: COORDS, + lastUpdated: now + } + + // Update or create weather document + await db.collection('weather').updateOne( + { + 'location.lat': COORDS.lat, + 'location.lon': COORDS.lon + }, + { $set: weatherWithMetadata }, + { upsert: true } + ) + + return NextResponse.json(weatherWithMetadata) + } catch (error) { + console.error('Weather API Error:', error) + // If we have cached data but failed to fetch new data, return cached data + if (cachedWeather) { + return NextResponse.json(cachedWeather) + } + return NextResponse.json( + { error: 'Failed to fetch weather data' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/src/app/page.js b/src/app/page.js index e889386..c97ddac 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -10,6 +10,7 @@ import { LastRefresh } from '@/components/LastRefresh' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { motion, AnimatePresence } from 'framer-motion' import { Navbar } from '@/components/Navbar' +import WeatherForecast from '@/components/WeatherForecast' export default function Dashboard() { const [data, setData] = useState([]) @@ -153,6 +154,15 @@ export default function Dashboard() {
+ + + + { + const fetchWeather = async () => { + try { + const response = await fetch('/api/weather'); + if (!response.ok) throw new Error('Failed to fetch weather data'); + const data = await response.json(); + setWeatherData(data); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + fetchWeather(); + }, []); + + if (loading) { + return ( +
+
Loading weather data...
+
+ ); + } + + if (error) { + return ( +
+
Error loading weather data
+
+ ); + } + + if (!weatherData?.forecast?.forecastday) { + return null; + } + + return ( + +
+
+

7-Day Forecast

+
+ Chisinau, UTM +
+
+
+ Last updated: {new Date(weatherData.lastUpdated).toLocaleTimeString()} +
+
+
+ {weatherData.forecast.forecastday.map((day, index) => ( + +
+ {index === 0 ? 'Today' : new Date(day.date).toLocaleDateString('en-US', { weekday: 'short' })} +
+
+
+ {day.day.condition.text} +
+
+ + {Math.round(day.day.maxtemp_c)}° + + + {Math.round(day.day.mintemp_c)}° + +
+
+
+ {day.day.condition.text} +
+
+ {day.day.daily_chance_of_rain}% +
+
+ ))} +
+
+ ) +} \ No newline at end of file diff --git a/src/lib/weather.js b/src/lib/weather.js new file mode 100644 index 0000000..8ed1191 --- /dev/null +++ b/src/lib/weather.js @@ -0,0 +1,88 @@ +import { connectToDatabase } from './mongodb' + +const WEATHER_API_KEY = process.env.OPENWEATHER_API_KEY +const LAT = 47.06149235737582 +const LON = 28.86683069635403 + +const WEATHER_ICONS = { + '01d': '☀️', // clear sky + '01n': '🌙', // clear sky night + '02d': '⛅', // few clouds + '02n': '☁️', // few clouds night + '03d': '☁️', // scattered clouds + '03n': '☁️', // scattered clouds night + '04d': '☁️', // broken clouds + '04n': '☁️', // broken clouds night + '09d': '🌧️', // shower rain + '09n': '🌧️', // shower rain night + '10d': '🌦️', // rain + '10n': '🌧️', // rain night + '11d': '⛈️', // thunderstorm + '11n': '⛈️', // thunderstorm night + '13d': '🌨️', // snow + '13n': '🌨️', // snow night + '50d': '🌫️', // mist + '50n': '🌫️', // mist night +} + +export async function fetchWeatherData() { + try { + const response = await fetch( + `https://api.openweathermap.org/data/2.5/forecast?lat=${LAT}&lon=${LON}&appid=${WEATHER_API_KEY}&units=metric` + ) + const data = await response.json() + + // Process the data to get daily forecasts + const dailyForecasts = data.list.reduce((acc, item) => { + const date = new Date(item.dt * 1000) + const day = date.toISOString().split('T')[0] + + if (!acc[day]) { + acc[day] = { + date: day, + temp_min: item.main.temp_min, + temp_max: item.main.temp_max, + humidity: item.main.humidity, + wind_speed: item.wind.speed, + icon: item.weather[0].icon, + description: item.weather[0].description, + timestamp: date + } + } else { + acc[day].temp_min = Math.min(acc[day].temp_min, item.main.temp_min) + acc[day].temp_max = Math.max(acc[day].temp_max, item.main.temp_max) + } + + return acc + }, {}) + + return Object.values(dailyForecasts) + } catch (error) { + console.error('Error fetching weather data:', error) + throw error + } +} + +export async function getWeatherData() { + const { db } = await connectToDatabase() + const collection = db.collection('weather_data') + + // Get the latest weather data + const latestData = await collection.findOne({}, { sort: { timestamp: -1 } }) + + // If no data exists or data is older than 2 hours, fetch new data + if (!latestData || (Date.now() - latestData.timestamp.getTime() > 2 * 60 * 60 * 1000)) { + const newData = await fetchWeatherData() + await collection.insertOne({ + forecasts: newData, + timestamp: new Date() + }) + return newData + } + + return latestData.forecasts +} + +export function getWeatherIcon(iconCode) { + return WEATHER_ICONS[iconCode] || '❓' +} \ No newline at end of file