Skip to content

Commit 2ec13c5

Browse files
committed
Add NWS weather API examples with Scittle
Implements 8 progressive ClojureScript weather demos: - Simple location lookup - Current conditions with unit conversion - 7-day forecast viewer - Hourly timeline - Weather alerts - Complete dashboard with tabs - Reusable API client - Integration documentation Features zero-build Scittle setup, keyword arguments throughout, parallel data fetching, and responsive styling.
1 parent 90d47b8 commit 2ec13c5

File tree

8 files changed

+3988
-0
lines changed

8 files changed

+3988
-0
lines changed

src/scittle/weather/complete_dashboard.cljs

Lines changed: 591 additions & 0 deletions
Large diffs are not rendered by default.

src/scittle/weather/current_conditions.cljs

Lines changed: 497 additions & 0 deletions
Large diffs are not rendered by default.

src/scittle/weather/forecast_viewer.cljs

Lines changed: 469 additions & 0 deletions
Large diffs are not rendered by default.

src/scittle/weather/hourly_forecast.cljs

Lines changed: 569 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
(ns scittle.weather.simple-lookup
2+
"Simple weather lookup demo - minimal example showing basic API usage.
3+
4+
This is the simplest possible weather app:
5+
- Two input fields for coordinates
6+
- One button to fetch weather
7+
- Display location and temperature
8+
9+
Demonstrates:
10+
- Basic API call with keyword arguments
11+
- Loading state
12+
- Error handling
13+
- Minimal Reagent UI"
14+
(:require [reagent.core :as r]
15+
[reagent.dom :as rdom]
16+
[clojure.string :as str]))
17+
18+
;; ============================================================================
19+
;; Inline API Functions (simplified for this demo)
20+
;; ============================================================================
21+
22+
(defn fetch-json
23+
"Fetch JSON from URL with error handling."
24+
[{:keys [url on-success on-error]}]
25+
(-> (js/fetch url)
26+
(.then (fn [response]
27+
(if (.-ok response)
28+
(.then (.json response)
29+
(fn [data]
30+
(on-success (js->clj data :keywordize-keys true))))
31+
(on-error (str "HTTP Error: " (.-status response))))))
32+
(.catch (fn [error]
33+
(on-error (.-message error))))))
34+
35+
(defn fetch-weather-data
36+
"Fetch basic weather data for coordinates."
37+
[{:keys [lat lon on-success on-error]}]
38+
(let [points-url (str "https://api.weather.gov/points/" lat "," lon)]
39+
(fetch-json
40+
{:url points-url
41+
:on-success
42+
(fn [points-result]
43+
;; Got points, now get forecast
44+
(let [properties (get-in points-result [:properties])
45+
forecast-url (:forecast properties)
46+
city (:city (get-in points-result [:properties :relativeLocation :properties]))
47+
state (:state (get-in points-result [:properties :relativeLocation :properties]))]
48+
49+
;; Fetch the forecast
50+
(fetch-json
51+
{:url forecast-url
52+
:on-success
53+
(fn [forecast-result]
54+
(let [periods (get-in forecast-result [:properties :periods])
55+
first-period (first periods)]
56+
(on-success
57+
{:city city
58+
:state state
59+
:temperature (:temperature first-period)
60+
:temperatureUnit (:temperatureUnit first-period)
61+
:shortForecast (:shortForecast first-period)
62+
:detailedForecast (:detailedForecast first-period)})))
63+
:on-error on-error})))
64+
:on-error on-error})))
65+
66+
;; ============================================================================
67+
;; Styles
68+
;; ============================================================================
69+
70+
(def card-style
71+
{:background "#ffffff"
72+
:border "1px solid #e0e0e0"
73+
:border-radius "8px"
74+
:padding "20px"
75+
:box-shadow "0 2px 4px rgba(0,0,0,0.1)"
76+
:margin-bottom "20px"})
77+
78+
(def input-style
79+
{:width "100%"
80+
:padding "10px"
81+
:border "1px solid #ddd"
82+
:border-radius "4px"
83+
:font-size "14px"
84+
:margin-bottom "10px"})
85+
86+
(def button-style
87+
{:background "#2196f3"
88+
:color "white"
89+
:border "none"
90+
:padding "12px 24px"
91+
:border-radius "4px"
92+
:cursor "pointer"
93+
:font-size "16px"
94+
:width "100%"
95+
:transition "background 0.3s"})
96+
97+
(def button-hover-style
98+
(merge button-style
99+
{:background "#1976d2"}))
100+
101+
(def button-disabled-style
102+
(merge button-style
103+
{:background "#ccc"
104+
:cursor "not-allowed"}))
105+
106+
;; ============================================================================
107+
;; Components
108+
;; ============================================================================
109+
110+
(defn loading-spinner
111+
"Simple loading indicator."
112+
[]
113+
[:div {:style {:text-align "center"
114+
:padding "40px"}}
115+
[:div {:style {:display "inline-block"
116+
:width "40px"
117+
:height "40px"
118+
:border "4px solid #f3f3f3"
119+
:border-top "4px solid #2196f3"
120+
:border-radius "50%"
121+
:animation "spin 1s linear infinite"}}]
122+
[:style "@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }"]
123+
[:p {:style {:margin-top "15px"
124+
:color "#666"}}
125+
"Fetching weather data..."]])
126+
127+
(defn error-display
128+
"Display error message."
129+
[{:keys [error on-retry]}]
130+
[:div {:style (merge card-style
131+
{:background "#ffebee"
132+
:border "1px solid #ef5350"})}
133+
[:h4 {:style {:margin-top 0
134+
:color "#c62828"}}
135+
"⚠️ Error"]
136+
[:p {:style {:color "#666"}}
137+
error]
138+
(when on-retry
139+
[:button {:on-click on-retry
140+
:style {:background "#f44336"
141+
:color "white"
142+
:border "none"
143+
:padding "8px 16px"
144+
:border-radius "4px"
145+
:cursor "pointer"
146+
:margin-top "10px"}}
147+
"Try Again"])])
148+
149+
(defn weather-result
150+
"Display weather results."
151+
[{:keys [data]}]
152+
[:div {:style card-style}
153+
[:h2 {:style {:margin-top 0
154+
:color "#2196f3"}}
155+
"📍 " (:city data) ", " (:state data)]
156+
157+
[:div {:style {:text-align "center"
158+
:margin "30px 0"}}
159+
[:div {:style {:font-size "48px"
160+
:font-weight "bold"
161+
:color "#333"}}
162+
(:temperature data) "°" (:temperatureUnit data)]
163+
[:div {:style {:font-size "18px"
164+
:color "#666"
165+
:margin-top "10px"}}
166+
(:shortForecast data)]]
167+
168+
[:div {:style {:background "#f5f5f5"
169+
:padding "15px"
170+
:border-radius "4px"
171+
:margin-top "20px"}}
172+
[:p {:style {:margin 0
173+
:line-height 1.6
174+
:color "#555"}}
175+
(:detailedForecast data)]]])
176+
177+
(defn input-form
178+
"Input form for coordinates."
179+
[{:keys [lat lon on-lat-change on-lon-change on-submit loading? disabled?]}]
180+
[:div {:style card-style}
181+
[:h3 {:style {:margin-top 0}}
182+
"🌍 Enter Coordinates"]
183+
[:p {:style {:color "#666"
184+
:font-size "14px"
185+
:margin-bottom "15px"}}
186+
"Enter latitude and longitude to get weather data"]
187+
188+
[:div
189+
[:label {:style {:display "block"
190+
:margin-bottom "5px"
191+
:color "#555"
192+
:font-weight "500"}}
193+
"Latitude"]
194+
[:input {:type "number"
195+
:step "0.0001"
196+
:placeholder "e.g., 40.7128"
197+
:value @lat
198+
:on-change #(on-lat-change (.. % -target -value))
199+
:disabled loading?
200+
:style (merge input-style
201+
(when loading? {:opacity 0.6}))}]]
202+
203+
[:div
204+
[:label {:style {:display "block"
205+
:margin-bottom "5px"
206+
:color "#555"
207+
:font-weight "500"}}
208+
"Longitude"]
209+
[:input {:type "number"
210+
:step "0.0001"
211+
:placeholder "e.g., -74.0060"
212+
:value @lon
213+
:on-change #(on-lon-change (.. % -target -value))
214+
:disabled loading?
215+
:style (merge input-style
216+
(when loading? {:opacity 0.6}))}]]
217+
218+
[:button {:on-click on-submit
219+
:disabled (or loading? disabled?)
220+
:style (cond
221+
loading? button-disabled-style
222+
disabled? button-disabled-style
223+
:else button-style)}
224+
(if loading?
225+
"Loading..."
226+
"Get Weather")]])
227+
228+
(defn quick-locations
229+
"Quick access buttons for major cities."
230+
[{:keys [on-select loading?]}]
231+
[:div {:style {:margin-top "20px"}}
232+
[:p {:style {:color "#666"
233+
:font-size "14px"
234+
:margin-bottom "10px"}}
235+
"Or try these cities:"]
236+
[:div {:style {:display "flex"
237+
:flex-wrap "wrap"
238+
:gap "8px"}}
239+
(for [[city lat lon] [["Charlotte, NC" 35.2271 -80.8431]
240+
["Miami, FL" 25.7617 -80.1918]
241+
["Denver, CO" 39.7392 -104.9903]
242+
["New York, NY" 40.7128 -74.0060]
243+
["Los Angeles, CA" 34.0522 -118.2437]
244+
["Chicago, IL" 41.8781 -87.6298]]]
245+
^{:key city}
246+
[:button {:on-click #(on-select lat lon)
247+
:disabled loading?
248+
:style {:padding "6px 12px"
249+
:background (if loading? "#ccc" "transparent")
250+
:color (if loading? "#999" "#2196f3")
251+
:border "1px solid #2196f3"
252+
:border-radius "20px"
253+
:cursor (if loading? "not-allowed" "pointer")
254+
:font-size "13px"
255+
:transition "all 0.2s"}}
256+
city])]])
257+
258+
;; ============================================================================
259+
;; Main Component
260+
;; ============================================================================
261+
262+
(defn main-component
263+
"Main weather lookup component."
264+
[]
265+
(let [lat (r/atom "35.2271")
266+
lon (r/atom "-80.8431")
267+
loading? (r/atom false)
268+
error (r/atom nil)
269+
weather-data (r/atom nil)
270+
271+
fetch-weather (fn [latitude longitude]
272+
(reset! loading? true)
273+
(reset! error nil)
274+
(reset! weather-data nil)
275+
276+
(fetch-weather-data
277+
{:lat latitude
278+
:lon longitude
279+
:on-success (fn [data]
280+
(reset! loading? false)
281+
(reset! weather-data data))
282+
:on-error (fn [err]
283+
(reset! loading? false)
284+
(reset! error err))}))]
285+
286+
(fn []
287+
[:div {:style {:max-width "600px"
288+
:margin "0 auto"
289+
:padding "20px"
290+
:font-family "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"}}
291+
[:h1 {:style {:text-align "center"
292+
:color "#333"
293+
:margin-bottom "30px"}}
294+
"☀️ Simple Weather Lookup"]
295+
296+
;; Input Form
297+
[input-form
298+
{:lat lat
299+
:lon lon
300+
:on-lat-change #(reset! lat %)
301+
:on-lon-change #(reset! lon %)
302+
:on-submit #(when (and (not (str/blank? @lat))
303+
(not (str/blank? @lon)))
304+
(fetch-weather @lat @lon))
305+
:loading? @loading?
306+
:disabled? (or (str/blank? @lat)
307+
(str/blank? @lon))}]
308+
309+
;; Quick Locations
310+
[quick-locations
311+
{:on-select (fn [latitude longitude]
312+
(reset! lat (str latitude))
313+
(reset! lon (str longitude))
314+
(fetch-weather latitude longitude))
315+
:loading? @loading?}]
316+
317+
;; Loading State
318+
(when @loading?
319+
[loading-spinner])
320+
321+
;; Error Display
322+
(when @error
323+
[error-display
324+
{:error @error
325+
:on-retry #(when (and (not (str/blank? @lat))
326+
(not (str/blank? @lon)))
327+
(fetch-weather @lat @lon))}])
328+
329+
;; Weather Results
330+
(when @weather-data
331+
[weather-result {:data @weather-data}])
332+
333+
;; Instructions
334+
(when (and (not @loading?)
335+
(not @error)
336+
(not @weather-data))
337+
[:div {:style {:text-align "center"
338+
:margin-top "40px"
339+
:color "#999"
340+
:font-size "14px"}}
341+
[:p "Enter coordinates above or click a city to get started"]
342+
[:p {:style {:margin-top "10px"}}
343+
"Uses the free NWS API - no API key required!"]])])))
344+
345+
;; ============================================================================
346+
;; Mount
347+
;; ============================================================================
348+
349+
(defn ^:export init []
350+
(rdom/render [main-component]
351+
(js/document.getElementById "simple-lookup-demo")))
352+
353+
;; Auto-initialize when script loads
354+
(init)

0 commit comments

Comments
 (0)