version 1.0
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"tailwind": {
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "zinc"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils",
|
||||||
|
"ui": "$lib/components/ui",
|
||||||
|
"hooks": "$lib/hooks",
|
||||||
|
"lib": "$lib"
|
||||||
|
},
|
||||||
|
"typescript": true,
|
||||||
|
"registry": "https://shadcn-svelte.com/registry"
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
|
||||||
|
## Get Forecast
|
||||||
|
`GET https://api.weather.gov/gridpoints/PQR/116,103/forecast`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://geojson.org/geojson-ld/geojson-context.jsonld",
|
||||||
|
{
|
||||||
|
"@version": "1.1",
|
||||||
|
"wx": "https://api.weather.gov/ontology#",
|
||||||
|
"geo": "http://www.opengis.net/ont/geosparql#",
|
||||||
|
"unit": "http://codes.wmo.int/common/unit/",
|
||||||
|
"@vocab": "https://api.weather.gov/ontology#"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-122.5566,
|
||||||
|
45.5102
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-122.56280000000001,
|
||||||
|
45.531
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-122.5926,
|
||||||
|
45.5267
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-122.5864,
|
||||||
|
45.505899899999996
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-122.5566,
|
||||||
|
45.5102
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"units": "us",
|
||||||
|
"forecastGenerator": "BaselineForecastGenerator",
|
||||||
|
"generatedAt": "2025-06-26T01:32:44+00:00",
|
||||||
|
"updateTime": "2025-06-26T00:40:50+00:00",
|
||||||
|
"validTimes": "2025-06-25T18:00:00+00:00/P7DT10H",
|
||||||
|
"elevation": {
|
||||||
|
"unitCode": "wmoUnit:m",
|
||||||
|
"value": 85.9536
|
||||||
|
},
|
||||||
|
"periods": [
|
||||||
|
{
|
||||||
|
"number": 1,
|
||||||
|
"name": "Tonight",
|
||||||
|
"startTime": "2025-06-25T18:00:00-07:00",
|
||||||
|
"endTime": "2025-06-26T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 58,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 31
|
||||||
|
},
|
||||||
|
"windSpeed": "2 to 6 mph",
|
||||||
|
"windDirection": "NNW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/ovc/rain,30?size=medium",
|
||||||
|
"shortForecast": "Cloudy then Chance Drizzle",
|
||||||
|
"detailedForecast": "A chance of drizzle after 5am. Cloudy, with a low around 58. North northwest wind 2 to 6 mph. Chance of precipitation is 30%."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 2,
|
||||||
|
"name": "Thursday",
|
||||||
|
"startTime": "2025-06-26T06:00:00-07:00",
|
||||||
|
"endTime": "2025-06-26T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 69,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 31
|
||||||
|
},
|
||||||
|
"windSpeed": "2 to 6 mph",
|
||||||
|
"windDirection": "SW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/rain,30/rain,20?size=medium",
|
||||||
|
"shortForecast": "Chance Drizzle",
|
||||||
|
"detailedForecast": "A chance of drizzle before 2pm. Mostly cloudy, with a high near 69. Southwest wind 2 to 6 mph. Chance of precipitation is 30%."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 3,
|
||||||
|
"name": "Thursday Night",
|
||||||
|
"startTime": "2025-06-26T18:00:00-07:00",
|
||||||
|
"endTime": "2025-06-27T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 56,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 8
|
||||||
|
},
|
||||||
|
"windSpeed": "2 to 6 mph",
|
||||||
|
"windDirection": "NW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/bkn?size=medium",
|
||||||
|
"shortForecast": "Mostly Cloudy",
|
||||||
|
"detailedForecast": "Mostly cloudy, with a low around 56. Northwest wind 2 to 6 mph."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 4,
|
||||||
|
"name": "Friday",
|
||||||
|
"startTime": "2025-06-27T06:00:00-07:00",
|
||||||
|
"endTime": "2025-06-27T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 73,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 3
|
||||||
|
},
|
||||||
|
"windSpeed": "2 to 6 mph",
|
||||||
|
"windDirection": "WNW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/bkn?size=medium",
|
||||||
|
"shortForecast": "Partly Sunny",
|
||||||
|
"detailedForecast": "Partly sunny, with a high near 73. West northwest wind 2 to 6 mph."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 5,
|
||||||
|
"name": "Friday Night",
|
||||||
|
"startTime": "2025-06-27T18:00:00-07:00",
|
||||||
|
"endTime": "2025-06-28T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 55,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 2
|
||||||
|
},
|
||||||
|
"windSpeed": "2 to 6 mph",
|
||||||
|
"windDirection": "NNW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/sct?size=medium",
|
||||||
|
"shortForecast": "Partly Cloudy",
|
||||||
|
"detailedForecast": "Partly cloudy, with a low around 55. North northwest wind 2 to 6 mph."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 6,
|
||||||
|
"name": "Saturday",
|
||||||
|
"startTime": "2025-06-28T06:00:00-07:00",
|
||||||
|
"endTime": "2025-06-28T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 80,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 to 9 mph",
|
||||||
|
"windDirection": "NNW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/few?size=medium",
|
||||||
|
"shortForecast": "Sunny",
|
||||||
|
"detailedForecast": "Sunny, with a high near 80."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 7,
|
||||||
|
"name": "Saturday Night",
|
||||||
|
"startTime": "2025-06-28T18:00:00-07:00",
|
||||||
|
"endTime": "2025-06-29T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 56,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "3 to 9 mph",
|
||||||
|
"windDirection": "NNW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/skc?size=medium",
|
||||||
|
"shortForecast": "Clear",
|
||||||
|
"detailedForecast": "Clear, with a low around 56."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 8,
|
||||||
|
"name": "Sunday",
|
||||||
|
"startTime": "2025-06-29T06:00:00-07:00",
|
||||||
|
"endTime": "2025-06-29T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 88,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "3 to 10 mph",
|
||||||
|
"windDirection": "NNW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/skc?size=medium",
|
||||||
|
"shortForecast": "Sunny",
|
||||||
|
"detailedForecast": "Sunny, with a high near 88."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 9,
|
||||||
|
"name": "Sunday Night",
|
||||||
|
"startTime": "2025-06-29T18:00:00-07:00",
|
||||||
|
"endTime": "2025-06-30T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 60,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
"windSpeed": "2 to 10 mph",
|
||||||
|
"windDirection": "NNW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/skc?size=medium",
|
||||||
|
"shortForecast": "Clear",
|
||||||
|
"detailedForecast": "Clear, with a low around 60."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 10,
|
||||||
|
"name": "Monday",
|
||||||
|
"startTime": "2025-06-30T06:00:00-07:00",
|
||||||
|
"endTime": "2025-06-30T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 90,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 7
|
||||||
|
},
|
||||||
|
"windSpeed": "2 to 10 mph",
|
||||||
|
"windDirection": "NNW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/few?size=medium",
|
||||||
|
"shortForecast": "Sunny",
|
||||||
|
"detailedForecast": "Sunny, with a high near 90."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 11,
|
||||||
|
"name": "Monday Night",
|
||||||
|
"startTime": "2025-06-30T18:00:00-07:00",
|
||||||
|
"endTime": "2025-07-01T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 61,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 7
|
||||||
|
},
|
||||||
|
"windSpeed": "2 to 10 mph",
|
||||||
|
"windDirection": "NW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/sct?size=medium",
|
||||||
|
"shortForecast": "Partly Cloudy",
|
||||||
|
"detailedForecast": "Partly cloudy, with a low around 61."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 12,
|
||||||
|
"name": "Tuesday",
|
||||||
|
"startTime": "2025-07-01T06:00:00-07:00",
|
||||||
|
"endTime": "2025-07-01T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 87,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 4
|
||||||
|
},
|
||||||
|
"windSpeed": "2 to 10 mph",
|
||||||
|
"windDirection": "NW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/sct?size=medium",
|
||||||
|
"shortForecast": "Mostly Sunny",
|
||||||
|
"detailedForecast": "Mostly sunny, with a high near 87."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 13,
|
||||||
|
"name": "Tuesday Night",
|
||||||
|
"startTime": "2025-07-01T18:00:00-07:00",
|
||||||
|
"endTime": "2025-07-02T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 59,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 4
|
||||||
|
},
|
||||||
|
"windSpeed": "2 to 10 mph",
|
||||||
|
"windDirection": "NW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/few?size=medium",
|
||||||
|
"shortForecast": "Mostly Clear",
|
||||||
|
"detailedForecast": "Mostly clear, with a low around 59."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 14,
|
||||||
|
"name": "Wednesday",
|
||||||
|
"startTime": "2025-07-02T06:00:00-07:00",
|
||||||
|
"endTime": "2025-07-02T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 83,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": "",
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 3
|
||||||
|
},
|
||||||
|
"windSpeed": "2 to 9 mph",
|
||||||
|
"windDirection": "NW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/few?size=medium",
|
||||||
|
"shortForecast": "Sunny",
|
||||||
|
"detailedForecast": "Sunny, with a high near 83."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -12,13 +12,18 @@
|
|||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@lucide/svelte": "^0.523.0",
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
"@sveltejs/adapter-auto": "^6.0.0",
|
||||||
"@sveltejs/kit": "^2.16.0",
|
"@sveltejs/kit": "^2.16.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwind-variants": "^1.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
|
"tw-animate-css": "^1.3.4",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^6.2.6"
|
"vite": "^6.2.6"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+50
@@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@lucide/svelte':
|
||||||
|
specifier: ^0.523.0
|
||||||
|
version: 0.523.0(svelte@5.34.8)
|
||||||
'@sveltejs/adapter-auto':
|
'@sveltejs/adapter-auto':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.1(@sveltejs/kit@2.22.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.34.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)))
|
version: 6.0.1(@sveltejs/kit@2.22.0(@sveltejs/vite-plugin-svelte@5.1.0(svelte@5.34.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.34.8)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)))
|
||||||
@@ -20,15 +23,27 @@ importers:
|
|||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.1.10(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1))
|
version: 4.1.10(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1))
|
||||||
|
clsx:
|
||||||
|
specifier: ^2.1.1
|
||||||
|
version: 2.1.1
|
||||||
svelte:
|
svelte:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.34.8
|
version: 5.34.8
|
||||||
svelte-check:
|
svelte-check:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.2.2(picomatch@4.0.2)(svelte@5.34.8)(typescript@5.8.3)
|
version: 4.2.2(picomatch@4.0.2)(svelte@5.34.8)(typescript@5.8.3)
|
||||||
|
tailwind-merge:
|
||||||
|
specifier: ^3.3.1
|
||||||
|
version: 3.3.1
|
||||||
|
tailwind-variants:
|
||||||
|
specifier: ^1.0.0
|
||||||
|
version: 1.0.0(tailwindcss@4.1.10)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.1.10
|
version: 4.1.10
|
||||||
|
tw-animate-css:
|
||||||
|
specifier: ^1.3.4
|
||||||
|
version: 1.3.4
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
@@ -214,6 +229,11 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.25':
|
'@jridgewell/trace-mapping@0.3.25':
|
||||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||||
|
|
||||||
|
'@lucide/svelte@0.523.0':
|
||||||
|
resolution: {integrity: sha512-t6CNxUAC7to9IPQv+yaJD8OlpXVeEL7wT1nw5+lx62HoBnkO6//xYZSTkV8Ia3bqtoF4deI3Pjff/8cPyQjiCw==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29':
|
'@polka/url@1.0.0-next.29':
|
||||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||||
|
|
||||||
@@ -684,6 +704,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-TF+8irl7rpj3+fpaLuPRX5BqReTAqckp0Fumxa/mCeK3fo0/MnBb9W/Z2bLwtqj3C3r5Lm6NKIAw7YrgIv1Fwg==}
|
resolution: {integrity: sha512-TF+8irl7rpj3+fpaLuPRX5BqReTAqckp0Fumxa/mCeK3fo0/MnBb9W/Z2bLwtqj3C3r5Lm6NKIAw7YrgIv1Fwg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
tailwind-merge@3.0.2:
|
||||||
|
resolution: {integrity: sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==}
|
||||||
|
|
||||||
|
tailwind-merge@3.3.1:
|
||||||
|
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
|
||||||
|
|
||||||
|
tailwind-variants@1.0.0:
|
||||||
|
resolution: {integrity: sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==}
|
||||||
|
engines: {node: '>=16.x', pnpm: '>=7.x'}
|
||||||
|
peerDependencies:
|
||||||
|
tailwindcss: '*'
|
||||||
|
|
||||||
tailwindcss@4.1.10:
|
tailwindcss@4.1.10:
|
||||||
resolution: {integrity: sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==}
|
resolution: {integrity: sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==}
|
||||||
|
|
||||||
@@ -703,6 +735,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
tw-animate-css@1.3.4:
|
||||||
|
resolution: {integrity: sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==}
|
||||||
|
|
||||||
typescript@5.8.3:
|
typescript@5.8.3:
|
||||||
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
|
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
@@ -866,6 +901,10 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
|
'@lucide/svelte@0.523.0(svelte@5.34.8)':
|
||||||
|
dependencies:
|
||||||
|
svelte: 5.34.8
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29': {}
|
'@polka/url@1.0.0-next.29': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.44.0':
|
'@rollup/rollup-android-arm-eabi@4.44.0':
|
||||||
@@ -1282,6 +1321,15 @@ snapshots:
|
|||||||
magic-string: 0.30.17
|
magic-string: 0.30.17
|
||||||
zimmerframe: 1.1.2
|
zimmerframe: 1.1.2
|
||||||
|
|
||||||
|
tailwind-merge@3.0.2: {}
|
||||||
|
|
||||||
|
tailwind-merge@3.3.1: {}
|
||||||
|
|
||||||
|
tailwind-variants@1.0.0(tailwindcss@4.1.10):
|
||||||
|
dependencies:
|
||||||
|
tailwind-merge: 3.0.2
|
||||||
|
tailwindcss: 4.1.10
|
||||||
|
|
||||||
tailwindcss@4.1.10: {}
|
tailwindcss@4.1.10: {}
|
||||||
|
|
||||||
tapable@2.2.2: {}
|
tapable@2.2.2: {}
|
||||||
@@ -1302,6 +1350,8 @@ snapshots:
|
|||||||
|
|
||||||
totalist@3.0.1: {}
|
totalist@3.0.1: {}
|
||||||
|
|
||||||
|
tw-animate-css@1.3.4: {}
|
||||||
|
|
||||||
typescript@5.8.3: {}
|
typescript@5.8.3: {}
|
||||||
|
|
||||||
vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1):
|
vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1):
|
||||||
|
|||||||
+161
-1
@@ -1 +1,161 @@
|
|||||||
@import 'tailwindcss';
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.129 0.042 264.695);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.129 0.042 264.695);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.129 0.042 264.695);
|
||||||
|
--primary: oklch(0.208 0.042 265.755);
|
||||||
|
--primary-foreground: oklch(0.984 0.003 247.858);
|
||||||
|
--secondary: oklch(0.968 0.007 247.896);
|
||||||
|
--secondary-foreground: oklch(0.208 0.042 265.755);
|
||||||
|
--muted: oklch(0.968 0.007 247.896);
|
||||||
|
--muted-foreground: oklch(0.554 0.046 257.417);
|
||||||
|
--accent: oklch(0.968 0.007 247.896);
|
||||||
|
--accent-foreground: oklch(0.208 0.042 265.755);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.929 0.013 255.508);
|
||||||
|
--input: oklch(0.929 0.013 255.508);
|
||||||
|
--ring: oklch(0.704 0.04 256.788);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.984 0.003 247.858);
|
||||||
|
--sidebar-foreground: oklch(0.129 0.042 264.695);
|
||||||
|
--sidebar-primary: oklch(0.208 0.042 265.755);
|
||||||
|
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
||||||
|
--sidebar-accent: oklch(0.968 0.007 247.896);
|
||||||
|
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
|
||||||
|
--sidebar-border: oklch(0.929 0.013 255.508);
|
||||||
|
--sidebar-ring: oklch(0.704 0.04 256.788);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.129 0.042 264.695);
|
||||||
|
--foreground: oklch(0.984 0.003 247.858);
|
||||||
|
--card: oklch(0.208 0.042 265.755);
|
||||||
|
--card-foreground: oklch(0.984 0.003 247.858);
|
||||||
|
--popover: oklch(0.208 0.042 265.755);
|
||||||
|
--popover-foreground: oklch(0.984 0.003 247.858);
|
||||||
|
--primary: oklch(0.929 0.013 255.508);
|
||||||
|
--primary-foreground: oklch(0.208 0.042 265.755);
|
||||||
|
--secondary: oklch(0.279 0.041 260.031);
|
||||||
|
--secondary-foreground: oklch(0.984 0.003 247.858);
|
||||||
|
--muted: oklch(0.279 0.041 260.031);
|
||||||
|
--muted-foreground: oklch(0.704 0.04 256.788);
|
||||||
|
--accent: oklch(0.279 0.041 260.031);
|
||||||
|
--accent-foreground: oklch(0.984 0.003 247.858);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.551 0.027 264.364);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.208 0.042 265.755);
|
||||||
|
--sidebar-foreground: oklch(0.984 0.003 247.858);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
||||||
|
--sidebar-accent: oklch(0.279 0.041 260.031);
|
||||||
|
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.551 0.027 264.364);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.21 0.006 285.885);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.92 0.004 286.32);
|
||||||
|
--primary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.274 0.006 286.033);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.552 0.016 285.938);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground min-h-screen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.container {
|
||||||
|
@apply max-w-7xl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
|
import { MapPin } from "@lucide/svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onclick: () => void;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { onclick, loading = false }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button {onclick} disabled={loading} class="flex items-center gap-2">
|
||||||
|
<MapPin class="w-4 h-4" />
|
||||||
|
{loading ? "Getting location..." : "Use current location"}
|
||||||
|
</Button>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card/index.js';
|
||||||
|
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||||
|
import WeatherIcon from './WeatherIcon.svelte';
|
||||||
|
import type { WeatherPeriod } from '$lib/types/weather.js';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
period: WeatherPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { period }: Props = $props();
|
||||||
|
|
||||||
|
function formatTime(timeString: string): string {
|
||||||
|
return new Date(timeString).toLocaleTimeString('en-US', {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTemperatureColor(temp: number): string {
|
||||||
|
if (temp >= 80) return 'bg-red-100 text-red-800';
|
||||||
|
if (temp >= 70) return 'bg-orange-100 text-orange-800';
|
||||||
|
if (temp >= 60) return 'bg-yellow-100 text-yellow-800';
|
||||||
|
if (temp >= 50) return 'bg-blue-100 text-blue-800';
|
||||||
|
return 'bg-slate-100 text-slate-800';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrecipitationColor(chance: number): string {
|
||||||
|
if (chance >= 70) return 'bg-blue-100 text-blue-800';
|
||||||
|
if (chance >= 40) return 'bg-sky-100 text-sky-800';
|
||||||
|
if (chance >= 20) return 'bg-slate-100 text-slate-800';
|
||||||
|
return 'bg-gray-50 text-gray-600';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card class="w-full">
|
||||||
|
<CardHeader class="pb-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<CardTitle class="text-lg">{period.name}</CardTitle>
|
||||||
|
<WeatherIcon
|
||||||
|
iconUrl={period.icon}
|
||||||
|
shortForecast={period.shortForecast}
|
||||||
|
isDaytime={period.isDaytime}
|
||||||
|
size={48}
|
||||||
|
class="text-muted-foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CardDescription class="text-sm text-muted-foreground">
|
||||||
|
{formatTime(period.startTime)} - {formatTime(period.endTime)}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-2xl font-bold">
|
||||||
|
{period.temperature}°{period.temperatureUnit}
|
||||||
|
</span>
|
||||||
|
<Badge class={getTemperatureColor(period.temperature)}>
|
||||||
|
{period.isDaytime ? 'Day' : 'Night'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-muted-foreground">Precipitation:</span>
|
||||||
|
<Badge class={getPrecipitationColor(period.probabilityOfPrecipitation.value)}>
|
||||||
|
{period.probabilityOfPrecipitation.value}%
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between text-sm">
|
||||||
|
<span class="text-muted-foreground">Wind:</span>
|
||||||
|
<span class="font-medium">{period.windSpeed} {period.windDirection}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p class="font-medium text-sm">{period.shortForecast}</p>
|
||||||
|
<p class="text-xs text-muted-foreground leading-relaxed">
|
||||||
|
{period.detailedForecast}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import WeatherCard from './WeatherCard.svelte';
|
||||||
|
import type { WeatherPeriod } from '$lib/types/weather.js';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
periods: WeatherPeriod[];
|
||||||
|
}
|
||||||
|
|
||||||
|
let { periods }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||||
|
{#each periods as period (period.number)}
|
||||||
|
<WeatherCard {period} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getWeatherIcon } from '$lib/utils/weatherIcons.js';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
iconUrl: string;
|
||||||
|
shortForecast: string;
|
||||||
|
isDaytime: boolean;
|
||||||
|
size?: number;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { iconUrl, shortForecast, isDaytime, size = 24, class: className = '' }: Props = $props();
|
||||||
|
|
||||||
|
const weatherIcon = $derived(getWeatherIcon(iconUrl, shortForecast, isDaytime));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:component
|
||||||
|
this={weatherIcon.icon}
|
||||||
|
{size}
|
||||||
|
class={className}
|
||||||
|
aria-label={weatherIcon.description}
|
||||||
|
title={weatherIcon.description}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const badgeVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-md border px-2 py-0.5 text-xs font-medium transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
||||||
|
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAnchorAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
href,
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: BadgeVariant;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:element
|
||||||
|
this={href ? "a" : "span"}
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="badge"
|
||||||
|
{href}
|
||||||
|
class={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</svelte:element>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Badge } from "./badge.svelte";
|
||||||
|
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
||||||
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const buttonVariants = tv({
|
||||||
|
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium outline-none transition-all focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white",
|
||||||
|
outline:
|
||||||
|
"bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||||
|
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
||||||
|
|
||||||
|
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
||||||
|
WithElementRef<HTMLAnchorAttributes> & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
ref = $bindable(null),
|
||||||
|
href = undefined,
|
||||||
|
type = "button",
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ButtonProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if href}
|
||||||
|
<a
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
href={disabled ? undefined : href}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
role={disabled ? "link" : undefined}
|
||||||
|
tabindex={disabled ? -1 : undefined}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="button"
|
||||||
|
class={cn(buttonVariants({ variant, size }), className)}
|
||||||
|
{type}
|
||||||
|
{disabled}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import Root, {
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
buttonVariants,
|
||||||
|
} from "./button.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
type ButtonProps as Props,
|
||||||
|
//
|
||||||
|
Root as Button,
|
||||||
|
buttonVariants,
|
||||||
|
type ButtonProps,
|
||||||
|
type ButtonSize,
|
||||||
|
type ButtonVariant,
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-action"
|
||||||
|
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-description"
|
||||||
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-footer"
|
||||||
|
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-header"
|
||||||
|
class={cn(
|
||||||
|
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card-title"
|
||||||
|
class={cn("font-semibold leading-none", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="card"
|
||||||
|
class={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import Root from "./card.svelte";
|
||||||
|
import Content from "./card-content.svelte";
|
||||||
|
import Description from "./card-description.svelte";
|
||||||
|
import Footer from "./card-footer.svelte";
|
||||||
|
import Header from "./card-header.svelte";
|
||||||
|
import Title from "./card-title.svelte";
|
||||||
|
import Action from "./card-action.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Description,
|
||||||
|
Footer,
|
||||||
|
Header,
|
||||||
|
Title,
|
||||||
|
Action,
|
||||||
|
//
|
||||||
|
Root as Card,
|
||||||
|
Content as CardContent,
|
||||||
|
Description as CardDescription,
|
||||||
|
Footer as CardFooter,
|
||||||
|
Header as CardHeader,
|
||||||
|
Title as CardTitle,
|
||||||
|
Action as CardAction,
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Root from "./input.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Input,
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
|
||||||
|
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||||
|
|
||||||
|
type Props = WithElementRef<
|
||||||
|
Omit<HTMLInputAttributes, "type"> &
|
||||||
|
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||||
|
>;
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
type,
|
||||||
|
files = $bindable(),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if type === "file"}
|
||||||
|
<input
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="input"
|
||||||
|
class={cn(
|
||||||
|
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
type="file"
|
||||||
|
bind:files
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="input"
|
||||||
|
class={cn(
|
||||||
|
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{type}
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { WeatherForecast, LocationInfo } from '$lib/types/weather.js';
|
||||||
|
|
||||||
|
export class WeatherService {
|
||||||
|
private static readonly BASE_URL = 'https://api.weather.gov';
|
||||||
|
|
||||||
|
static async getLocationFromCoords(lat: number, lon: number): Promise<{ gridX: number; gridY: number; office: string }> {
|
||||||
|
const response = await fetch(`${this.BASE_URL}/points/${lat},${lon}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to get location data: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
gridX: data.properties.gridX,
|
||||||
|
gridY: data.properties.gridY,
|
||||||
|
office: data.properties.gridId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getForecast(office: string, gridX: number, gridY: number): Promise<WeatherForecast> {
|
||||||
|
const response = await fetch(`${this.BASE_URL}/gridpoints/${office}/${gridX},${gridY}/forecast`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to get forecast: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getForecastByCoords(lat: number, lon: number): Promise<WeatherForecast> {
|
||||||
|
const location = await this.getLocationFromCoords(lat, lon);
|
||||||
|
return await this.getForecast(location.office, location.gridX, location.gridY);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getCurrentLocation(): Promise<LocationInfo> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
reject(new Error('Geolocation is not supported by this browser'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(position) => {
|
||||||
|
resolve({
|
||||||
|
latitude: position.coords.latitude,
|
||||||
|
longitude: position.coords.longitude
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
reject(new Error(`Geolocation error: ${error.message}`));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
export interface WeatherPeriod {
|
||||||
|
number: number;
|
||||||
|
name: string;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
isDaytime: boolean;
|
||||||
|
temperature: number;
|
||||||
|
temperatureUnit: string;
|
||||||
|
temperatureTrend: string;
|
||||||
|
probabilityOfPrecipitation: {
|
||||||
|
unitCode: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
windSpeed: string;
|
||||||
|
windDirection: string;
|
||||||
|
icon: string;
|
||||||
|
shortForecast: string;
|
||||||
|
detailedForecast: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeatherForecast {
|
||||||
|
type: string;
|
||||||
|
geometry: {
|
||||||
|
type: string;
|
||||||
|
coordinates: number[][][];
|
||||||
|
};
|
||||||
|
properties: {
|
||||||
|
units: string;
|
||||||
|
forecastGenerator: string;
|
||||||
|
generatedAt: string;
|
||||||
|
updateTime: string;
|
||||||
|
validTimes: string;
|
||||||
|
elevation: {
|
||||||
|
unitCode: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
periods: WeatherPeriod[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocationInfo {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
city?: string;
|
||||||
|
state?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||||
|
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||||
|
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
Sun,
|
||||||
|
Moon,
|
||||||
|
Cloud,
|
||||||
|
CloudRain,
|
||||||
|
CloudSnow,
|
||||||
|
CloudLightning,
|
||||||
|
CloudDrizzle,
|
||||||
|
Cloudy,
|
||||||
|
Wind,
|
||||||
|
Eye,
|
||||||
|
type Icon
|
||||||
|
} from '@lucide/svelte';
|
||||||
|
|
||||||
|
export interface WeatherIconMapping {
|
||||||
|
icon: typeof Icon;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeatherIcon(iconUrl: string, shortForecast: string, isDaytime: boolean): WeatherIconMapping {
|
||||||
|
const forecast = shortForecast.toLowerCase();
|
||||||
|
const url = iconUrl.toLowerCase();
|
||||||
|
|
||||||
|
// Check for specific weather conditions in forecast text and icon URL
|
||||||
|
if (forecast.includes('thunderstorm') || forecast.includes('storm') || url.includes('tsra')) {
|
||||||
|
return { icon: CloudLightning, description: 'Thunderstorm' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forecast.includes('snow') || url.includes('snow')) {
|
||||||
|
return { icon: CloudSnow, description: 'Snow' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forecast.includes('rain') || forecast.includes('shower') || url.includes('rain') || url.includes('shra')) {
|
||||||
|
return { icon: CloudRain, description: 'Rain' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forecast.includes('drizzle') || url.includes('drizzle')) {
|
||||||
|
return { icon: CloudDrizzle, description: 'Drizzle' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forecast.includes('fog') || forecast.includes('mist') || url.includes('fog')) {
|
||||||
|
return { icon: Eye, description: 'Fog' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forecast.includes('wind') || forecast.includes('breezy') || forecast.includes('gusty')) {
|
||||||
|
return { icon: Wind, description: 'Windy' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cloud conditions
|
||||||
|
if (forecast.includes('overcast') || url.includes('ovc')) {
|
||||||
|
return { icon: Cloudy, description: 'Overcast' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forecast.includes('mostly cloudy') || forecast.includes('considerable cloudiness') || url.includes('bkn')) {
|
||||||
|
return { icon: Cloud, description: 'Mostly Cloudy' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forecast.includes('partly cloudy') || forecast.includes('partly sunny') || url.includes('sct')) {
|
||||||
|
return isDaytime
|
||||||
|
? { icon: Sun, description: 'Partly Cloudy' }
|
||||||
|
: { icon: Moon, description: 'Partly Cloudy' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forecast.includes('mostly clear') || forecast.includes('mostly sunny') || url.includes('few')) {
|
||||||
|
return isDaytime
|
||||||
|
? { icon: Sun, description: 'Mostly Clear' }
|
||||||
|
: { icon: Moon, description: 'Mostly Clear' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear conditions (default)
|
||||||
|
if (forecast.includes('clear') || forecast.includes('sunny') || url.includes('skc') || url.includes('clr')) {
|
||||||
|
return isDaytime
|
||||||
|
? { icon: Sun, description: 'Clear' }
|
||||||
|
: { icon: Moon, description: 'Clear' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default fallback based on time of day
|
||||||
|
return isDaytime
|
||||||
|
? { icon: Sun, description: 'Clear' }
|
||||||
|
: { icon: Moon, description: 'Clear' };
|
||||||
|
}
|
||||||
+89
-2
@@ -1,2 +1,89 @@
|
|||||||
<h1>Welcome to SvelteKit</h1>
|
<script lang="ts">
|
||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
import { onMount } from 'svelte';
|
||||||
|
import WeatherGrid from '$lib/components/WeatherGrid.svelte';
|
||||||
|
import LocationButton from '$lib/components/LocationButton.svelte';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card/index.js';
|
||||||
|
import { WeatherService } from '$lib/services/weather.js';
|
||||||
|
import type { WeatherForecast, LocationInfo } from '$lib/types/weather.js';
|
||||||
|
|
||||||
|
let forecast: WeatherForecast | null = $state(null);
|
||||||
|
let loading = $state(false);
|
||||||
|
let error = $state<string | null>(null);
|
||||||
|
let location: LocationInfo | null = $state(null);
|
||||||
|
|
||||||
|
async function loadWeatherForLocation(lat: number, lon: number) {
|
||||||
|
try {
|
||||||
|
loading = true;
|
||||||
|
error = null;
|
||||||
|
forecast = await WeatherService.getForecastByCoords(lat, lon);
|
||||||
|
location = { latitude: lat, longitude: lon };
|
||||||
|
} catch (err) {
|
||||||
|
error = err instanceof Error ? err.message : 'Failed to load weather data';
|
||||||
|
forecast = null;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLocationClick() {
|
||||||
|
try {
|
||||||
|
loading = true;
|
||||||
|
error = null;
|
||||||
|
const currentLocation = await WeatherService.getCurrentLocation();
|
||||||
|
await loadWeatherForLocation(currentLocation.latitude, currentLocation.longitude);
|
||||||
|
} catch (err) {
|
||||||
|
error = err instanceof Error ? err.message : 'Failed to get location';
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loadWeatherForLocation(45.5152, -122.6784);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 py-8 space-y-6">
|
||||||
|
<div class="text-center space-y-4">
|
||||||
|
<h1 class="text-4xl font-bold tracking-tight">Weather Forecast</h1>
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
Get current weather conditions and forecasts from the National Weather Service
|
||||||
|
</p>
|
||||||
|
<LocationButton onclick={handleLocationClick} {loading} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<Card class="border-destructive">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="text-destructive">Error</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p class="text-destructive">{error}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<div class="flex justify-center items-center py-12">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if forecast && !loading}
|
||||||
|
<div class="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Forecast Information</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Generated: {new Date(forecast.properties.generatedAt).toLocaleString()}
|
||||||
|
{#if location}
|
||||||
|
• Location: {location.latitude.toFixed(4)}, {location.longitude.toFixed(4)}
|
||||||
|
{/if}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<WeatherGrid periods={forecast.properties.periods} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user