|
| 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