├── LICENSE ├── README.md ├── env ├── dev │ └── cljs │ │ └── reddit_viewer │ │ └── dev.cljs └── prod │ └── cljs │ └── reddit_viewer │ └── prod.cljs ├── project.clj ├── public └── index.html └── src └── reddit_viewer ├── chart.cljs ├── core.cljs └── sample_data.cljs /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 reagent-project 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [JS-Workshop](https://github.com/ClojureTO/JS-Workshop) 2 | 3 | # Part 1: Reagent 4 | 5 | ## Before the Workshop 6 | 7 | Please make sure that you have a copy of the JDK and Leiningen build tool setup to follow along with the workshop. 8 | You can follow installation instructions in the links below: 9 | 10 | * [JDK 1.8+](http://www.azul.com/downloads/zulu/) 11 | * [Leiningen](https://leiningen.org/) 12 | 13 | To compile our ClojureScript code we will use [figwheel](https://figwheel.org/). If you want to use [shadow-cljs](https://github.com/thheller/shadow-cljs) you should follow along with [this](https://github.com/ClojureTO/JS-Workshop/tree/shadow-cljs) version of the workshop. 14 | 15 | ### Creating and running the project 16 | 17 | Run the following commands to create a new project and run it to ensure that the setup was completed successfully: 18 | 19 | lein new reagent-frontend reddit-viewer +figwheel 20 | cd reddit-viewer 21 | lein figwheel 22 | 23 | If the project starts up successfully, then you should have a browser window open at `localhost:3449/index.html`. 24 | 25 | ## During the Workshop 26 | 27 | This is a comprehensive guide to the workshop itself, for those playing along from home! 28 | 29 | We'll update project dependencies in `project.clj` to look as follows: 30 | 31 | ```clojure 32 | :dependencies [[org.clojure/clojure "1.8.0" :scope "provided"] 33 | [org.clojure/clojurescript "1.9.671" :scope "provided"] 34 | [reagent "0.7.0"] 35 | [cljsjs/chartjs "2.5.0-0"] 36 | [cljs-ajax "0.6.0"]] 37 | ``` 38 | 39 | Next, let's replace the generated CSS link with the Bootstrap CSS in the `public/index.html` file: 40 | 41 | ```xml 42 | 43 | 44 | 45 | 46 | 47 | ``` 48 | 49 | start the project in development mode: 50 | 51 | lein figwheel 52 | 53 | Leiningen will download the dependencies and start compiling the project, this can take a minute first time around. 54 | Once the project compilation finishes, a browser window will open at [http://localhost:3449/index.html](http://localhost:3449/index.html). 55 | 56 | ## Editing the project 57 | 58 | Now that we have the project running, let's see how we can add some functionality to it. 59 | We'll open up the `reddit_viewer/core.cljs` file that has some initial boilerplate in it and see what it's doing. 60 | 61 | ```clojure 62 | (ns reddit-viewer.core 63 | (:require 64 | [reagent.core :as r])) 65 | 66 | ;; ------------------------- 67 | ;; Views 68 | 69 | (defn home-page [] 70 | [:div [:h2 "Welcome to Reagent"]]) 71 | 72 | ;; ------------------------- 73 | ;; Initialize app 74 | 75 | (defn mount-root [] 76 | (r/render [home-page] (.getElementById js/document "app"))) 77 | 78 | (defn init! [] 79 | (mount-root)) 80 | ``` 81 | 82 | The top section of the file contains a namespace declaration. The namespace requires the `reagent.core` namespace that's 83 | used to create the UI. 84 | 85 | The `home-page` function creates a Reagent component. The component contains a `div` with an `h2` tag inside it. 86 | 87 | Reagent uses Clojure literal notation for vectors and maps to represent HTML. The tag is defined using a vector, where the first element is the keyword representing the tag name, followed by an optional map of attributes, and the tag content. 88 | 89 | For example, `[:div [:h2 "Welcome to Reagent"]` maps to `

Welcome to Reagent

`. If we wanted to add `id` and `class` to the `div`, we could do that as follows: `[:div {:id "foo" :class "bar baz"} ...]`. 90 | 91 | Since setting the `id` and `class` attributes is a very common operation, Reagent provides a shortcut for doing that using syntax similar to CSS selectors: `[:div#foo.bar.baz ...]`. 92 | 93 | This component is rendered inside the DOM element with the ID `app`. This element is defined in the `public/index.html` file 94 | by the `mount-root` function. 95 | 96 | Finally, we have the `init!` function that serves as the entry point for the application. 97 | 98 | ### Task 1: Loading data using Ajax and viewing it. 99 | 100 | Let's start by creating a container to hold the results: 101 | 102 | ```clojure 103 | (defonce posts (r/atom nil)) 104 | ``` 105 | 106 | The `atom` is a container for mutable data. We'll initialize it with a `nil` value. 107 | 108 | Next, we'll require the `ajax.core` namespace and add a couple of functions that will load posts from the `http://www.reddit.com/r/Catloaf.json?sort=new&limit=9` URL, filter out the ones with images, 109 | and save them in the `posts` atom: 110 | 111 | ```clojure 112 | (ns reddit-viewer.core 113 | (:require 114 | [ajax.core :as ajax] 115 | [reagent.core :as r])) 116 | 117 | (defonce posts (r/atom nil)) 118 | 119 | (defn find-posts-with-preview [posts] 120 | (filter #(= (:post_hint %) "image") posts)) 121 | 122 | (defn load-posts [] 123 | (ajax/GET "http://www.reddit.com/r/Catloaf.json?sort=new&limit=10" 124 | {:handler #(->> (get-in % [:data :children]) 125 | (map :data) 126 | (find-posts-with-preview) 127 | (reset! posts)) 128 | :response-format :json 129 | :keywords? true})) 130 | ``` 131 | 132 | The `load-posts` function loads the JSON data and converts it to a Clojure data structure. We pass the `ajax/GET` function the URL and 133 | a map of options. The options contain the `:handler` key pointing to the function that should be called to handle the successful response, 134 | the `:response-format` key that hints that the response type is JSON, and `:keywords?` hint indicating that we would like to convert JSON 135 | string keys into Clojure keywords for maps. 136 | 137 | The original data has the following structure: 138 | 139 | ```clojure 140 | {:data {:children [{:data {...}} ...]}} 141 | ``` 142 | 143 | The top level data structure is a map that contains a key called `:data`, this key points to a map that contains a key called 144 | `:children`. Finally, the `:children` key points to a collection of maps representing the posts. Each map, in turn, has a key 145 | called `:data` that contains the data for the post. 146 | 147 | Our `:handler` function grabs the collection of posts, and maps across them to get the `:data` key containing the information about 148 | each post. It then calls the `find-posts-with-preview` function to filter out posts without images. After we process the original response data, we reset the `posts` atom with the result. 149 | 150 | We can test our function in the Figwheel REPL by running the following commands: 151 | 152 | ```clojure 153 | (in-ns 'reddit-viewer.core) 154 | (load-posts) 155 | (first @posts) 156 | ``` 157 | 158 | We should see the data contained in the first item in the collection of posts that was loaded. 159 | 160 | ### Task 2: Rendering the data 161 | 162 | Each post map contains a `:url` key that points to an image. Let's write a component function to render the image from the first post that looks as follows: 163 | 164 | ```clojure 165 | (defn display-post [{:keys [url]}] 166 | (when url [:img {:src url}])) 167 | ``` 168 | 169 | When a Reagent component function returns `nil` it is omitted in the DOM, so the `display-posts` component will only be rendered when provided with a map containing a `:url` key that has a value. 170 | 171 | We can now parent this component under the `home-page` component: 172 | 173 | ```clojure 174 | (defn home-page [] 175 | [:div [:h2 "Welcome to Reagent"] 176 | [display-post (first @posts)]]) 177 | ``` 178 | 179 | Note that we're putting the `display-post` component in a vector `[display-post]` as opposed to calling it as a function with `(display-post)`. 180 | 181 | This is a property of how the Reagent library works. The templates specify the structure of the page. Reagent 182 | then manages the lifecycle of the component functions, and decides when they need to be called based on the state of the data. 183 | 184 | If we called the function directly by writing `(display-post)`, then it would be executed a single time when the code is initialized, and 185 | it would not be repainted when the contents of `posts` atom change. 186 | 187 | By using the vector notation and writing `[display-post]`, we're telling Reagent where we would like to render the `display-post` component, and let it manage when to call it based on the state of the data. 188 | 189 | Reagent atoms are reactive meaning that any time the atom is dereferenced using the `@` notation, a listener is created. When the atom value changes, all the listeners are notified of the change, and the components are repainted. 190 | 191 | We can tests this by going to the REPL and clearing the `posts` atom: 192 | 193 | ```clojure 194 | (reset! posts nil) 195 | ``` 196 | 197 | We can see that the image disappears on the page once the contents of the atom have been cleared. Let's run the `(load-posts)` function again: 198 | 199 | ```clojure 200 | (load-posts) 201 | ``` 202 | 203 | We should be seeing the cat picture once again as the `display-post` component is repainted with new data. 204 | 205 | #### Working with HTML 206 | 207 | We've now seen that the data is being loaded, but it's not terribly nice to look at. Let's render it in a better way using Bootstrap CSS. 208 | We'll update the `display-post` component function as follows: 209 | 210 | ```clojure 211 | (defn display-post [{:keys [permalink subreddit title score url]}] 212 | [:div.card.m-2 213 | [:div.card-block 214 | [:h4.card-title 215 | [:a {:href (str "http://reddit.com" permalink)} title " "]] 216 | [:div [:span.badge.badge-info {:color "info"} subreddit " score " score]] 217 | [:img {:width "300px" :src url}]]]) 218 | ``` 219 | 220 | Now that we can render a single post nicely, let's write a function that will render a multiple posts: 221 | 222 | ```clojure 223 | (defn display-posts [posts] 224 | (when-not (empty? posts) 225 | [:div 226 | (for [posts-row (partition-all 3 posts)] 227 | ^{:key posts-row} 228 | [:div.row 229 | (for [post posts-row] 230 | ^{:key post} 231 | [:div.col-4 [display-post post]])])])) 232 | ``` 233 | 234 | The function will accept a collection of posts as its parameter. It will then check whether the collection is empty. 235 | 236 | When the `posts` are not empty, we'll partition them into groups of three. 237 | We'll create a Bootstrap row for each group and pass the posts in the row to the `display-post` function we wrote earlier. 238 | 239 | Note that we're using the `^{:key posts-row}` notation for dynamic collections elements. This provides Reagent with a unique identifier for each element to decide when to repaint it efficiently. If the key was omitted, then Reagent would repaint all elements whenever any of the elements need repainting. 240 | 241 | With that in place, we can update the `home-page` component to render the posts: 242 | 243 | ```clojure 244 | (defn home-page [] 245 | [:div.card>div.card-block 246 | [display-posts @posts]]) 247 | ``` 248 | 249 | ### Task 3: Manipulating the data 250 | 251 | We're able to load the posts, and have a UI for render them. Let's take a look at adding the ability to sort the posts, and see how the UI will track the changes for us. 252 | 253 | We'll add a `sort-posts` component function that looks as follows: 254 | 255 | ```clojure 256 | (defn sort-posts [title sort-key] 257 | (when-not (empty? @posts) 258 | [:button.btn.btn-secondary 259 | {:on-click #(swap! posts (partial sort-by sort-key))} 260 | (str "sort posts by " title)])) 261 | ``` 262 | 263 | This function will check that the posts are not empty, and add a button to sort the posts by the specified key. 264 | 265 | Let's add a couple of buttons to the `home-page` that will allow us to sort posts by their score and comments: 266 | 267 | ```clojure 268 | (defn home-page [] 269 | [:div.card>div.card-block 270 | [:div.btn-group 271 | [sort-posts "score" :score] 272 | [sort-posts "comments" :num_comments]] 273 | [display-posts @posts]]) 274 | ``` 275 | 276 | Note that as we're updating the UI, we're retaining the state of the application. As new components are added, the `posts` atom state is retained. We can modify the way the UI looks without having to reload the application to see the changes. 277 | 278 | ### Task 4: JavaScript interop 279 | 280 | So far we've been working exclusively with Reagent components. Now, let's take a look at using a plain JavaScript library that expects to manipulate the DOM directly. 281 | 282 | Let's create a new namespace called `reddit-viewer.chart` in the `src/reddit_viewer/chart.cljs` file to handle charting our data using the Chart.js library. The namespace declaration will look as follows: 283 | 284 | 285 | ```clojure 286 | (ns reddit-viewer.chart 287 | (:require 288 | [cljsjs.chartjs] 289 | [reagent.core :as r])) 290 | ``` 291 | 292 | Next, we'll write a function that calls Chart.js to render given data in a DOM node as a bar chart: 293 | 294 | ```clojure 295 | (defn render-data [node data] 296 | (js/Chart. 297 | node 298 | (clj->js 299 | {:type "bar" 300 | :data {:labels (map :title data) 301 | :datasets [{:label "votes" 302 | :data (map :score data)}]} 303 | :options {:scales {:xAxes [{:display false}]}}}))) 304 | ``` 305 | 306 | The above code is equivalent to writing the following JavaScript: 307 | 308 | ``` 309 | new Chart(node 310 | {type: "bar", 311 | data: { 312 | labels: data.map(function(x) {return x.title}), 313 | datasets: 314 | [{ 315 | label: "votes", 316 | data: data.map(function(x) {return x.ups}) 317 | }] 318 | }, 319 | options: { 320 | scales: {xAxes: [{display: false}]} 321 | } 322 | }); 323 | ``` 324 | 325 | Now that we have the code to render the chart, we need to have access to a DOM node. Since Reagent is based on React, it uses a virtual DOM and renders components in the browser DOM as needed. 326 | 327 | So far we've been writing components as functions that return HTML elements. However, these functions only represent the render method of a React class. 328 | 329 | In order to get access to the DOM we have to implement other lifecycle functions that get called when the component is mounted, updated, and unmounted. This is achieved by calling the `create-class` function: 330 | 331 | ```clojure 332 | (defn chart-posts-by-votes [data] 333 | (let [chart (r/atom nil)] 334 | (r/create-class 335 | {:component-did-mount (render-chart chart data) 336 | :component-did-update (render-chart chart data) 337 | :component-will-unmount (fn [_] (destroy-chart chart)) 338 | :render (fn [] (when @data [:canvas]))}))) 339 | ``` 340 | 341 | The function accepts a map keyed on the lifecycle events. Whenever each event occurs, the associated function will be called. 342 | 343 | We'll track the state of the chart using an atom. This will be necessary because we have to destroy the existing chart when component is unmounted. 344 | 345 | You can see that the `:render` key points to a function that will return the `:canvas` element when data is available. 346 | 347 | The `:component-did-mount` and `:component-did-update` keys point to the `render-chart` function that w'll write next: 348 | 349 | ```clojure 350 | (defn render-chart [chart data] 351 | (fn [component] 352 | (when (not-empty @data) 353 | (let [node (r/dom-node component)] 354 | (destroy-chart chart) 355 | (reset! chart (render-data node @data)))))) 356 | ``` 357 | 358 | This function is a closure that returns a function that will receive the React component. The inner function will check if there's any data available, and if so, then it will grab the mounted DOM node by calling `r/dom-node` on the `component`. It will attempt to clear the existing chart by calling the `destroy-chart` function, and then create a new chart by calling the `render-data` function we wrote earlier. 359 | 360 | Finally, we'll implement the `destroy-chart` function as follows: 361 | 362 | ```clojure 363 | (defn destroy-chart [chart] 364 | (when @chart 365 | (.destroy @chart) 366 | (reset! chart nil))) 367 | ``` 368 | 369 | This function will check whether there's an existing chart present and call its `destroy` method. It will then reset the `chart` atom to a `nil` value. 370 | 371 | With that in place, we can navigate back to the `reddit-viewer.core` namespace, and require the `reddit-viewer.chart` namespace there: 372 | 373 | ```clojure 374 | (ns reddit-viewer.core 375 | (:require 376 | [ajax.core :as ajax] 377 | [reagent.core :as r] 378 | [reddit-viewer.chart :as chart])) 379 | ``` 380 | 381 | We'll now update the `home-page` component to display the chart: 382 | 383 | ```clojure 384 | (defn home-page [] 385 | [:div.card>div.card-block 386 | [:div.btn-group 387 | [sort-posts "score" :score] 388 | [sort-posts "comments" :num_comments]] 389 | [chart/chart-posts-by-votes posts] 390 | [display-posts @posts]]) 391 | ``` 392 | 393 | We should now see the chart rendered, and it should update when we change the sort order of our data using the `score` and `comment` sorting buttons. 394 | 395 | ### Task 5: Managing local state within components 396 | 397 | As a final touch, let's add a navbar to separate the posts and the chart into separate views. We'll start by adding a `navitem` function that creates a navigation link given a title, an atom containing the currently selected view, and the id of the nav item: 398 | 399 | ```clojure 400 | (defn navitem [title view id] 401 | [:li.nav-item 402 | {:class-name (when (= id @view) "active")} 403 | [:a.nav-link 404 | {:href "#" 405 | :on-click #(reset! view id)} 406 | title]]) 407 | ``` 408 | 409 | The component checks whether the current id in the view matches the item id in order to decide whether its class should be set to active. When it's clicked, the component will reset the `view` atom to its id. 410 | 411 | We can now create a Bootstrap navbar with links to posts and the chart: 412 | 413 | ```clojure 414 | (defn navbar [view] 415 | [:nav.navbar.navbar-toggleable-md.navbar-light.bg-faded 416 | [:ul.navbar-nav.mr-auto.nav 417 | {:className "navbar-nav mr-auto"} 418 | [navitem "Posts" view :posts] 419 | [navitem "Chart" view :chart]]]) 420 | ``` 421 | 422 | Finally, we'll update the home page to use the `navbar` component. The home page will now need to track a local state to know what view it needs to display. 423 | This is accomplished by creating a local atom called `view`: 424 | 425 | ```clojure 426 | (defn home-page [] 427 | (let [view (r/atom :posts)] 428 | (fn [] 429 | [:div 430 | [navbar view] 431 | [:div.card>div.card-block 432 | [:div.btn-group 433 | [sort-posts "score" :score] 434 | [sort-posts "comments" :num_comments]] 435 | (case @view 436 | :chart [chart/chart-posts-by-votes posts] 437 | :posts [display-posts @posts])]]))) 438 | ``` 439 | 440 | Notice that we return an anonymous function from inside the `let` statement. This is a Reagent mechanic for creating local state within components. 441 | 442 | If the inner function was not present, then the top level function would be called each time the component was repainted and the `let` statement would be reinitialized. 443 | 444 | When a component returns a function as the result, Reagent knows to call that function when subsequent calls to that component occur. 445 | 446 | Since this is a common operation, Reagent provides a helper macro called `with-let`. We can rewrite the above function using it as follows: 447 | 448 | ```clojure 449 | (defn home-page [] 450 | (r/with-let [view (r/atom :posts)] 451 | [:div 452 | [navbar view] 453 | [:div.card>div.card-block 454 | [:div.btn-group 455 | [sort-posts "score" :score] 456 | [sort-posts "comments" :num_comments]] 457 | (case @view 458 | :chart [chart/chart-posts-by-votes posts] 459 | :posts [display-posts @posts])]])) 460 | ``` 461 | 462 | That completes all the functionality we set out to add to our application. The only thing left to do is to compile it for production use. 463 | 464 | ## Excercises 465 | 466 | * Add a loading dialog that will be displayed when images are being loaded 467 | * Add a button to select the number of posts to fetch 468 | * Add the ability to select what subreddit the images are loaded from 469 | * Add the ability to load posts from multiple subreddits 470 | * Add tabs to show posts by subreddit 471 | 472 | ## Compiling for release 473 | 474 | So far we've been working with ClojureScript in development mode. This compilation method allows for fast incremental compilation and reloading. However, it generates very large JavaScript files. 475 | 476 | To use our app in production we'll want to use the advanced compilation method that will produce optimized JavaScript. This is accomplished by running the following command: 477 | 478 | lein package 479 | 480 | This will produce a single minified JavaScript file called `public/js/app.js` that's ready for production use. 481 | 482 | ## [Part 2: re-frame integration](https://github.com/ClojureTO/JS-Workshop/tree/re-frame) 483 | 484 | ## Libraries used in the project 485 | 486 | * [Chart.js](http://www.chartjs.org/) - used to generate the bar chart 487 | * [cljs-ajax](https://github.com/JulianBirch/cljs-ajax) - used to fetch data from Reddit 488 | * [Reagent](https://reagent-project.github.io/) - ClojureScript interface for React -------------------------------------------------------------------------------- /env/dev/cljs/reddit_viewer/dev.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-no-load reddit-viewer.dev 2 | (:require [reddit-viewer.core :as core] 3 | [devtools.core :as devtools] 4 | [figwheel.client :as figwheel :include-macros true])) 5 | 6 | (devtools/install!) 7 | 8 | (enable-console-print!) 9 | 10 | (core/init!) 11 | -------------------------------------------------------------------------------- /env/prod/cljs/reddit_viewer/prod.cljs: -------------------------------------------------------------------------------- 1 | (ns reddit-viewer.prod 2 | (:require [reddit-viewer.core :as core])) 3 | 4 | ;;ignore println statements in prod 5 | (set! *print-fn* (fn [& _])) 6 | 7 | (core/init!) 8 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject reddit-viewer "0.1.0-SNAPSHOT" 2 | :description "FIXME: write description" 3 | :url "http://example.com/FIXME" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :dependencies [[org.clojure/clojure "1.10.0" :scope "provided"] 8 | [org.clojure/clojurescript "1.10.520" :scope "provided"] 9 | [reagent "0.8.1"] 10 | [cljsjs/chartjs "2.7.3-0"] 11 | [cljs-ajax "0.8.0"]] 12 | 13 | :plugins [[lein-cljsbuild "1.1.7"] 14 | [lein-figwheel "0.5.18"]] 15 | 16 | :min-lein-version "2.5.0" 17 | :source-paths ["src"] 18 | :clean-targets ^{:protect false} 19 | [:target-path 20 | [:cljsbuild :builds :app :compiler :output-dir] 21 | [:cljsbuild :builds :app :compiler :output-to]] 22 | 23 | :resource-paths ["public"] 24 | 25 | :figwheel {:http-server-root "." 26 | :nrepl-port 7002 27 | :nrepl-middleware ["cemerick.piggieback/wrap-cljs-repl"] 28 | :css-dirs ["public/css"]} 29 | 30 | :cljsbuild {:builds {:app 31 | {:source-paths ["src" "env/dev/cljs"] 32 | :compiler 33 | {:main "reddit-viewer.dev" 34 | :output-to "public/js/app.js" 35 | :output-dir "public/js/out" 36 | :asset-path "js/out" 37 | :source-map true 38 | :optimizations :none 39 | :pretty-print true} 40 | :figwheel 41 | {:open-urls ["http://localhost:3449/index.html"] 42 | :on-jsload "reddit-viewer.core/mount-root"}} 43 | :release 44 | {:source-paths ["src" "env/prod/cljs"] 45 | :compiler 46 | {:output-to "public/js/app.js" 47 | :output-dir "public/js/release" 48 | :asset-path "js/out" 49 | :optimizations :advanced 50 | :pretty-print false}}}} 51 | 52 | :aliases {"package" ["do" "clean" ["cljsbuild" "once" "release"]]} 53 | 54 | :profiles {:dev {:dependencies [[binaryage/devtools "0.9.10"] 55 | [figwheel-sidecar "0.5.18"] 56 | [nrepl "0.6.0"] 57 | [cider/piggieback "0.4.0"]]}}) 58 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |

ClojureScript has not been compiled!

12 |

please run lein figwheel in order to start the compiler

13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/reddit_viewer/chart.cljs: -------------------------------------------------------------------------------- 1 | (ns reddit-viewer.chart 2 | (:require 3 | [cljsjs.chartjs] 4 | [reagent.core :as r])) 5 | 6 | (defn render-data [node data] 7 | (js/Chart. 8 | node 9 | (clj->js 10 | {:type "bar" 11 | :data {:labels (map :title data) 12 | :datasets [{:label "votes" 13 | :data (map :score data)}]} 14 | :options {:scales {:xAxes [{:display false}]}}}))) 15 | 16 | (defn destroy-chart [chart] 17 | (when @chart 18 | (.destroy @chart) 19 | (reset! chart nil))) 20 | 21 | (defn render-chart [chart data] 22 | (fn [component] 23 | (when-not (empty? @data) 24 | (let [node (r/dom-node component)] 25 | (destroy-chart chart) 26 | (reset! chart (render-data node @data)))))) 27 | 28 | (defn chart-posts-by-votes [data] 29 | (let [chart (r/atom nil)] 30 | (r/create-class 31 | {:component-did-mount (render-chart chart data) 32 | :component-did-update (render-chart chart data) 33 | :component-will-unmount (fn [_] (destroy-chart chart)) 34 | :render (fn [] (when @data [:canvas]))}))) 35 | -------------------------------------------------------------------------------- /src/reddit_viewer/core.cljs: -------------------------------------------------------------------------------- 1 | (ns reddit-viewer.core 2 | (:require 3 | [ajax.core :as ajax] 4 | [reagent.core :as r] 5 | [reddit-viewer.chart :as chart])) 6 | 7 | (defonce posts (r/atom nil)) 8 | 9 | (defn find-posts-with-preview [posts] 10 | (filter #(= (:post_hint %) "image") posts)) 11 | 12 | (defn load-posts [] 13 | (ajax/GET "http://www.reddit.com/r/Catloaf.json?sort=new&limit=10" 14 | {:handler #(->> (get-in % [:data :children]) 15 | (map :data) 16 | (find-posts-with-preview) 17 | (reset! posts)) 18 | :response-format :json 19 | :keywords? true})) 20 | 21 | 22 | ;; ------------------------- 23 | ;; Views 24 | 25 | (defn display-post [{:keys [permalink subreddit title score url]}] 26 | [:div.card.m-2 27 | [:div.card-block 28 | [:h4.card-title 29 | [:a {:href (str "http://reddit.com" permalink)} title " "]] 30 | [:div [:span.badge.badge-info {:color "info"} subreddit " score " score]] 31 | [:img {:width "300px" :src url}]]]) 32 | 33 | (defn display-posts [posts] 34 | (when-not (empty? posts) 35 | [:div 36 | (for [posts-row (partition-all 3 posts)] 37 | ^{:key posts-row} 38 | [:div.row 39 | (for [post posts-row] 40 | ^{:key post} 41 | [:div.col-4 [display-post post]])])])) 42 | 43 | (defn sort-posts [title sort-key] 44 | (when-not (empty? @posts) 45 | [:button.btn.btn-secondary 46 | {:on-click #(swap! posts (partial sort-by sort-key))} 47 | (str "sort posts by " title)])) 48 | 49 | (defn navitem [title view id] 50 | [:li.nav-item 51 | {:class-name (when (= id @view) "active")} 52 | [:a.nav-link 53 | {:href "#" 54 | :on-click #(reset! view id)} 55 | title]]) 56 | 57 | (defn navbar [view] 58 | [:nav.navbar.navbar-toggleable-md.navbar-light.bg-faded 59 | [:ul.navbar-nav.mr-auto.nav 60 | {:className "navbar-nav mr-auto"} 61 | [navitem "Posts" view :posts] 62 | [navitem "Chart" view :chart]]]) 63 | 64 | (defn home-page [] 65 | (r/with-let [view (r/atom :posts)] 66 | [:div 67 | [navbar view] 68 | [:div.card>div.card-block 69 | [:div.btn-group 70 | [sort-posts "score" :score] 71 | [sort-posts "comments" :num_comments]] 72 | (case @view 73 | :chart [chart/chart-posts-by-votes posts] 74 | :posts [display-posts @posts])]])) 75 | 76 | ;; ------------------------- 77 | ;; Initialize app 78 | 79 | (defn mount-root [] 80 | (r/render [home-page] (.getElementById js/document "app"))) 81 | 82 | (defn init! [] 83 | (mount-root) 84 | (load-posts)) 85 | -------------------------------------------------------------------------------- /src/reddit_viewer/sample_data.cljs: -------------------------------------------------------------------------------- 1 | (ns reddit-viewer.sample-data) 2 | 3 | (def posts 4 | [{:permalink "/r/Catloaf/comments/6lgj64/anytime_i_leave_clothes_on_the_floor/", 5 | :subreddit "Catloaf", 6 | :title "Anytime I leave clothes on the floor", 7 | :score 2517, 8 | :url "https://i.redd.it/wp3soyqtxt7z.jpg"} 9 | {:permalink "/r/Catloaf/comments/6llznm/piano_loaf/", 10 | :subreddit "Catloaf", 11 | :title "Piano loaf", 12 | :score 31, 13 | :url "https://i.redd.it/mtrgzmqtcz7z.jpg"} 14 | {:permalink 15 | "/r/Catloaf/comments/6lmrk6/pumpernickel_loaf_on_the_coffee_table/", 16 | :subreddit "Catloaf", 17 | :title "Pumpernickel loaf on the coffee table.", 18 | :score 15, 19 | :url "http://m.imgur.com/vbTNI9y"} 20 | {:permalink "/r/Catloaf/comments/6llhaf/dress_loaf/", 21 | :subreddit "Catloaf", 22 | :title "Dress loaf", 23 | :score 19, 24 | :url "https://i.redd.it/c6g4hr6iwy7z.jpg"} 25 | {:permalink 26 | "/r/Catloaf/comments/6ln0kh/my_other_loaf_shes_11_this_month/", 27 | :subreddit "Catloaf", 28 | :title "My other loaf. She's 11 this month.", 29 | :score 8, 30 | :url "https://i.redd.it/u3dnwgi4408z.jpg"} 31 | {:permalink "/r/Catloaf/comments/6lmx5t/ella_fitzgerald_loaf/", 32 | :subreddit "Catloaf", 33 | :title "Ella Fitzgerald Loaf", 34 | :score 10, 35 | :url "https://i.redd.it/xswecf0t108z.jpg"} 36 | {:permalink "/r/Catloaf/comments/6lekoj/unevenly_baked_loaf/", 37 | :subreddit "Catloaf", 38 | :title "Unevenly baked loaf", 39 | :score 218, 40 | :url "https://i.redd.it/xuzq77hehs7z.jpg"} 41 | {:permalink "/r/Catloaf/comments/6lbo4u/this_face/", 42 | :subreddit "Catloaf", 43 | :title "This face!", 44 | :score 1488, 45 | :url "https://i.redd.it/knr8rmvx4p7z.jpg"} 46 | {:permalink "/r/Catloaf/comments/6liiiq/my_cheeseloaf/", 47 | :subreddit "Catloaf", 48 | :title "My cheeseloaf", 49 | :score 22, 50 | :url "https://i.redd.it/98gmjyizkv7z.jpg"} 51 | {:permalink 52 | "/r/Catloaf/comments/6lg39v/triple_loaf_baking_in_front_of_cat_tv/", 53 | :subreddit "Catloaf", 54 | :title "Triple loaf baking in front of Cat TV", 55 | :score 61, 56 | :url "http://i.imgur.com/ZNCwbNH.jpg"} 57 | {:permalink "/r/Catloaf/comments/6lhszc/loaf_life_is_rough/", 58 | :subreddit "Catloaf", 59 | :title "Loaf life is rough.", 60 | :score 28, 61 | :url "https://i.redd.it/pjmwv3r0yu7z.jpg"} 62 | {:permalink "/r/Catloaf/comments/6ldmz2/mid_meow/", 63 | :subreddit "Catloaf", 64 | :title "Mid Meow", 65 | :score 121, 66 | :url "https://i.redd.it/ys9mudf0nr7z.jpg"} 67 | {:permalink "/r/Catloaf/comments/6lk18h/thicc_loaf/", 68 | :subreddit "Catloaf", 69 | :title "Thicc loaf", 70 | :score 3, 71 | :url "https://i.redd.it/y9l74sx84x7z.jpg"} 72 | {:permalink "/r/Catloaf/comments/6li8o3/mittens_all_tucked/", 73 | :subreddit "Catloaf", 74 | :title "Mittens all tucked!", 75 | :score 12, 76 | :url "https://i.redd.it/u7hy6ue9cv7z.jpg"} 77 | {:permalink "/r/Catloaf/comments/6ljeqm/grumpy_catloaf/", 78 | :subreddit "Catloaf", 79 | :title "grumpy catloaf", 80 | :score 6, 81 | :url "http://imgur.com/DcIBSrF"} 82 | {:permalink "/r/Catloaf/comments/6lhnlj/wide_loaf/", 83 | :subreddit "Catloaf", 84 | :title "Wide Loaf", 85 | :score 15, 86 | :url "https://i.redd.it/0fkg6kkgtu7z.jpg"} 87 | {:permalink 88 | "/r/Catloaf/comments/6li6qb/a_big_ole_loaf_of_white_bread/", 89 | :subreddit "Catloaf", 90 | :title "A big ole loaf of white bread", 91 | :score 8, 92 | :url "https://i.redd.it/etrftophav7z.jpg"} 93 | {:permalink 94 | "/r/Catloaf/comments/6libdc/fresh_catloaf_warming_up_the_clean_washing/", 95 | :subreddit "Catloaf", 96 | :title "Fresh catloaf warming up the clean washing.", 97 | :score 7, 98 | :url "https://i.redd.it/z9wjdg1nev7z.jpg"} 99 | {:permalink "/r/Catloaf/comments/6leu1w/one_burnt_loaf/", 100 | :subreddit "Catloaf", 101 | :title "One Burnt Loaf", 102 | :score 27, 103 | :url "http://imgur.com/zh90LZW"} 104 | {:permalink "/r/Catloaf/comments/6ljk24/on_the_prowl_loaf/", 105 | :subreddit "Catloaf", 106 | :title "On the prowl loaf.", 107 | :score 3, 108 | :url "https://i.redd.it/hmprek4gkw7z.jpg"} 109 | {:permalink 110 | "/r/Catloaf/comments/6ljk1b/pakas_first_loaf_she_is_all_sass_about_this/", 111 | :subreddit "Catloaf", 112 | :title "Pakas first loaf. She is all sass about this.", 113 | :score 2, 114 | :url "https://i.redd.it/9g77ckufkw7z.jpg"} 115 | {:permalink 116 | "/r/Catloaf/comments/6liuqt/unhappily_under_the_bed_loaf/", 117 | :subreddit "Catloaf", 118 | :title "unhappily under the bed loaf", 119 | :score 3, 120 | :url "https://i.redd.it/abt6qkx0wv7z.jpg"} 121 | {:permalink "/r/Catloaf/comments/6ljk30/swirled_bun/", 122 | :subreddit "Catloaf", 123 | :title "Swirled bun.", 124 | :score 1, 125 | :url "https://i.redd.it/wjx3fm1gkw7z.jpg"} 126 | {:permalink "/r/Catloaf/comments/6l6zzw/cat_loaf_is_watching_you/", 127 | :subreddit "Catloaf", 128 | :title "Cat loaf is watching you", 129 | :score 1249, 130 | :url "https://i.redd.it/wekkdin6tk7z.jpg"} 131 | {:permalink "/r/Catloaf/comments/6lew8k/table_loaf/", 132 | :subreddit "Catloaf", 133 | :title "Table loaf", 134 | :score 9, 135 | :url "https://i.redd.it/sdwrln8fqs7z.jpg"}]) --------------------------------------------------------------------------------