From 4cd69217cc34e8a960a83389d4a0ef7cde812819 Mon Sep 17 00:00:00 2001 From: Lukas Werner Date: Wed, 25 Jun 2025 18:58:43 -0700 Subject: [PATCH] version 1.0 --- components.json | 16 + national_weather_service_api.md | 324 ++++++++++++++++++ package.json | 5 + pnpm-lock.yaml | 50 +++ src/app.css | 162 ++++++++- src/lib/components/LocationButton.svelte | 16 + src/lib/components/WeatherCard.svelte | 84 +++++ src/lib/components/WeatherGrid.svelte | 16 + src/lib/components/WeatherIcon.svelte | 23 ++ src/lib/components/ui/badge/badge.svelte | 50 +++ src/lib/components/ui/badge/index.ts | 2 + src/lib/components/ui/button/button.svelte | 80 +++++ src/lib/components/ui/button/index.ts | 17 + src/lib/components/ui/card/card-action.svelte | 20 ++ .../components/ui/card/card-content.svelte | 15 + .../ui/card/card-description.svelte | 20 ++ src/lib/components/ui/card/card-footer.svelte | 20 ++ src/lib/components/ui/card/card-header.svelte | 23 ++ src/lib/components/ui/card/card-title.svelte | 20 ++ src/lib/components/ui/card/card.svelte | 23 ++ src/lib/components/ui/card/index.ts | 25 ++ src/lib/components/ui/input/index.ts | 7 + src/lib/components/ui/input/input.svelte | 51 +++ src/lib/services/weather.ts | 52 +++ src/lib/types/weather.ts | 46 +++ src/lib/utils.ts | 13 + src/lib/utils/weatherIcons.ts | 81 +++++ src/routes/+page.svelte | 91 ++++- 28 files changed, 1349 insertions(+), 3 deletions(-) create mode 100644 components.json create mode 100644 national_weather_service_api.md create mode 100644 src/lib/components/LocationButton.svelte create mode 100644 src/lib/components/WeatherCard.svelte create mode 100644 src/lib/components/WeatherGrid.svelte create mode 100644 src/lib/components/WeatherIcon.svelte create mode 100644 src/lib/components/ui/badge/badge.svelte create mode 100644 src/lib/components/ui/badge/index.ts create mode 100644 src/lib/components/ui/button/button.svelte create mode 100644 src/lib/components/ui/button/index.ts create mode 100644 src/lib/components/ui/card/card-action.svelte create mode 100644 src/lib/components/ui/card/card-content.svelte create mode 100644 src/lib/components/ui/card/card-description.svelte create mode 100644 src/lib/components/ui/card/card-footer.svelte create mode 100644 src/lib/components/ui/card/card-header.svelte create mode 100644 src/lib/components/ui/card/card-title.svelte create mode 100644 src/lib/components/ui/card/card.svelte create mode 100644 src/lib/components/ui/card/index.ts create mode 100644 src/lib/components/ui/input/index.ts create mode 100644 src/lib/components/ui/input/input.svelte create mode 100644 src/lib/services/weather.ts create mode 100644 src/lib/types/weather.ts create mode 100644 src/lib/utils.ts create mode 100644 src/lib/utils/weatherIcons.ts diff --git a/components.json b/components.json new file mode 100644 index 0000000..f258682 --- /dev/null +++ b/components.json @@ -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" +} diff --git a/national_weather_service_api.md b/national_weather_service_api.md new file mode 100644 index 0000000..393f14c --- /dev/null +++ b/national_weather_service_api.md @@ -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." + } + ] + } +} +``` diff --git a/package.json b/package.json index d88b3a3..f5e3428 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,18 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" }, "devDependencies": { + "@lucide/svelte": "^0.523.0", "@sveltejs/adapter-auto": "^6.0.0", "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", "@tailwindcss/vite": "^4.0.0", + "clsx": "^2.1.1", "svelte": "^5.0.0", "svelte-check": "^4.0.0", + "tailwind-merge": "^3.3.1", + "tailwind-variants": "^1.0.0", "tailwindcss": "^4.0.0", + "tw-animate-css": "^1.3.4", "typescript": "^5.0.0", "vite": "^6.2.6" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64a8ef6..fe04c9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@lucide/svelte': + specifier: ^0.523.0 + version: 0.523.0(svelte@5.34.8) '@sveltejs/adapter-auto': 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))) @@ -20,15 +23,27 @@ importers: '@tailwindcss/vite': specifier: ^4.0.0 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: specifier: ^5.0.0 version: 5.34.8 svelte-check: specifier: ^4.0.0 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: specifier: ^4.0.0 version: 4.1.10 + tw-animate-css: + specifier: ^1.3.4 + version: 1.3.4 typescript: specifier: ^5.0.0 version: 5.8.3 @@ -214,6 +229,11 @@ packages: '@jridgewell/trace-mapping@0.3.25': 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': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -684,6 +704,18 @@ packages: resolution: {integrity: sha512-TF+8irl7rpj3+fpaLuPRX5BqReTAqckp0Fumxa/mCeK3fo0/MnBb9W/Z2bLwtqj3C3r5Lm6NKIAw7YrgIv1Fwg==} 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: resolution: {integrity: sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==} @@ -703,6 +735,9 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tw-animate-css@1.3.4: + resolution: {integrity: sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==} + typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} @@ -866,6 +901,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@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': {} '@rollup/rollup-android-arm-eabi@4.44.0': @@ -1282,6 +1321,15 @@ snapshots: magic-string: 0.30.17 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: {} tapable@2.2.2: {} @@ -1302,6 +1350,8 @@ snapshots: totalist@3.0.1: {} + tw-animate-css@1.3.4: {} + typescript@5.8.3: {} vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1): diff --git a/src/app.css b/src/app.css index d4b5078..9067e16 100644 --- a/src/app.css +++ b/src/app.css @@ -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; + } +} diff --git a/src/lib/components/LocationButton.svelte b/src/lib/components/LocationButton.svelte new file mode 100644 index 0000000..370e6d7 --- /dev/null +++ b/src/lib/components/LocationButton.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/lib/components/WeatherCard.svelte b/src/lib/components/WeatherCard.svelte new file mode 100644 index 0000000..2b4fc58 --- /dev/null +++ b/src/lib/components/WeatherCard.svelte @@ -0,0 +1,84 @@ + + + + +
+ {period.name} + +
+ + {formatTime(period.startTime)} - {formatTime(period.endTime)} + +
+ +
+ + {period.temperature}°{period.temperatureUnit} + + + {period.isDaytime ? 'Day' : 'Night'} + +
+ +
+
+ Precipitation: + + {period.probabilityOfPrecipitation.value}% + +
+ +
+ Wind: + {period.windSpeed} {period.windDirection} +
+
+ +
+

{period.shortForecast}

+

+ {period.detailedForecast} +

+
+
+
\ No newline at end of file diff --git a/src/lib/components/WeatherGrid.svelte b/src/lib/components/WeatherGrid.svelte new file mode 100644 index 0000000..59b73a7 --- /dev/null +++ b/src/lib/components/WeatherGrid.svelte @@ -0,0 +1,16 @@ + + +
+ {#each periods as period (period.number)} + + {/each} +
\ No newline at end of file diff --git a/src/lib/components/WeatherIcon.svelte b/src/lib/components/WeatherIcon.svelte new file mode 100644 index 0000000..0462628 --- /dev/null +++ b/src/lib/components/WeatherIcon.svelte @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/src/lib/components/ui/badge/badge.svelte b/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000..5000457 --- /dev/null +++ b/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,50 @@ + + + + + + {@render children?.()} + diff --git a/src/lib/components/ui/badge/index.ts b/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000..64e0aa9 --- /dev/null +++ b/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/src/lib/components/ui/button/button.svelte b/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..4daf453 --- /dev/null +++ b/src/lib/components/ui/button/button.svelte @@ -0,0 +1,80 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/src/lib/components/ui/button/index.ts b/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..fb585d7 --- /dev/null +++ b/src/lib/components/ui/button/index.ts @@ -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, +}; diff --git a/src/lib/components/ui/card/card-action.svelte b/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000..cc36c56 --- /dev/null +++ b/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-content.svelte b/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..bc90b83 --- /dev/null +++ b/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-description.svelte b/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..9b20ac7 --- /dev/null +++ b/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

+ {@render children?.()} +

diff --git a/src/lib/components/ui/card/card-footer.svelte b/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..cf43353 --- /dev/null +++ b/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-header.svelte b/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..8a91abb --- /dev/null +++ b/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-title.svelte b/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..22586e6 --- /dev/null +++ b/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card.svelte b/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..99448cc --- /dev/null +++ b/src/lib/components/ui/card/card.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/index.ts b/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..4d3fce4 --- /dev/null +++ b/src/lib/components/ui/card/index.ts @@ -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, +}; diff --git a/src/lib/components/ui/input/index.ts b/src/lib/components/ui/input/index.ts new file mode 100644 index 0000000..f47b6d3 --- /dev/null +++ b/src/lib/components/ui/input/index.ts @@ -0,0 +1,7 @@ +import Root from "./input.svelte"; + +export { + Root, + // + Root as Input, +}; diff --git a/src/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte new file mode 100644 index 0000000..19c6dae --- /dev/null +++ b/src/lib/components/ui/input/input.svelte @@ -0,0 +1,51 @@ + + +{#if type === "file"} + +{:else} + +{/if} diff --git a/src/lib/services/weather.ts b/src/lib/services/weather.ts new file mode 100644 index 0000000..505e193 --- /dev/null +++ b/src/lib/services/weather.ts @@ -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 { + 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 { + const location = await this.getLocationFromCoords(lat, lon); + return await this.getForecast(location.office, location.gridX, location.gridY); + } + + static async getCurrentLocation(): Promise { + 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}`)); + } + ); + }); + } +} \ No newline at end of file diff --git a/src/lib/types/weather.ts b/src/lib/types/weather.ts new file mode 100644 index 0000000..3702f52 --- /dev/null +++ b/src/lib/types/weather.ts @@ -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; +} \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..55b3a91 --- /dev/null +++ b/src/lib/utils.ts @@ -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 extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; diff --git a/src/lib/utils/weatherIcons.ts b/src/lib/utils/weatherIcons.ts new file mode 100644 index 0000000..a68277f --- /dev/null +++ b/src/lib/utils/weatherIcons.ts @@ -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' }; +} \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index cc88df0..b6ab3c6 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,2 +1,89 @@ -

Welcome to SvelteKit

-

Visit svelte.dev/docs/kit to read the documentation

+ + +
+
+

Weather Forecast

+

+ Get current weather conditions and forecasts from the National Weather Service +

+ +
+ + {#if error} + + + Error + + +

{error}

+
+
+ {/if} + + {#if loading} +
+
+
+ {/if} + + {#if forecast && !loading} +
+ + + Forecast Information + + Generated: {new Date(forecast.properties.generatedAt).toLocaleString()} + {#if location} + • Location: {location.latitude.toFixed(4)}, {location.longitude.toFixed(4)} + {/if} + + + + + +
+ {/if} +
+