diff --git a/ext/css/sidebar.css b/ext/css/sidebar.css index b317f0a..2fbf964 100644 --- a/ext/css/sidebar.css +++ b/ext/css/sidebar.css @@ -222,6 +222,7 @@ * { font-family: 'Fira Sans', Arial, Helvetica, sans-serif; color: #0c0c0d; + box-sizing: border-box; } html, body { @@ -236,7 +237,13 @@ html, body { box-sizing: border-box; } +#sidebarContent { + height: 96%; + overflow: scroll; +} + .sidebarHeader { + height: 4%; display: flex; align-items: center; padding: 8px; @@ -253,7 +260,7 @@ html, body { .card { background-color: #f9f9fa; - width: 90%; + width: 284px; margin: auto; margin-top: 8px; padding: 8px; @@ -261,12 +268,17 @@ html, body { flex-direction: column; justify-content: center; box-shadow: 0 1px 4px rgba(12, 12, 13, 0.1); + box-sizing: border-box; } .card.clickable:hover { box-shadow: 0 2px 8px rgba(12, 12, 13, 0.1); } +.card.child { + border-left: 4px solid #b1b1b3; +} + .commentsIndicator { margin-top: 8px; display: flex; @@ -283,6 +295,11 @@ html, body { font-weight: 500; } +.body10 { + font-size: 13px; + font-weight: 400; +} + .body20 { font-size: 15px; font-weight: 400; diff --git a/src/looped_in/components.cljs b/src/looped_in/components.cljs index 7a70ad1..836747c 100644 --- a/src/looped_in/components.cljs +++ b/src/looped_in/components.cljs @@ -3,6 +3,7 @@ [goog.dom.classlist :as classes] [goog.events :as events] [goog.object :as gobject] + [goog.html.sanitizer.HtmlSanitizer :as Sanitizer] [clojure.string :as string] [looped-in.logging :as log]) (:import (goog.date DateTime))) @@ -24,6 +25,11 @@ "commentsIndicator" (caption30 (str num-comments " comment" (when (not= num-comments 1) "s"))))) +(defn replies-indicator [num-replies] + (dom/createDom "div" + "commentsIndicator" + (caption30 (str num-replies " " (if (not= num-replies 1) "replies" "reply"))))) + (defn get-time-ago-str "Returns the string ' ' based on how long ago `timestamp` was, for example '3 days' or '5 hours'" @@ -38,11 +44,19 @@ (let [hours (.diff range "hours")] (str hours " hour" (when (not= days 1) "s")))))))) +(defn comment-text [text] + (dom/createDom "div" "body10" (dom/safeHtmlToNode (Sanitizer/sanitize text)))) + (defn story-caption [points author timestamp] (dom/createDom "div" "storyCaption caption10" (str points " points by " author " " (get-time-ago-str timestamp) " ago"))) +(defn comment-caption [author timestamp] + (dom/createDom "div" + "caption10" + (str author " " (get-time-ago-str timestamp) " ago"))) + (defn loader [] (apply dom/createDom "div" "spinner" (for [i (range 1 6)] diff --git a/src/looped_in/macros.clj b/src/looped_in/macros.clj index 0becc80..ffe2d6f 100644 --- a/src/looped_in/macros.clj +++ b/src/looped_in/macros.clj @@ -1,3 +1,3 @@ (ns looped-in.macros) -(defmacro get-in-items [m ks] `(get-in ~m (vec (interpose :children ~ks)))) +(defmacro get-in-item [m ks] `(get-in ~m (vec (interpose :children ~ks)))) diff --git a/src/looped_in/sidebar.cljs b/src/looped_in/sidebar.cljs index 3325280..75b5b40 100644 --- a/src/looped_in/sidebar.cljs +++ b/src/looped_in/sidebar.cljs @@ -7,65 +7,36 @@ [looped-in.components :as components] [looped-in.promises :refer [promise->channel]] [looped-in.logging :as log]) - (:require-macros [looped-in.macros :refer [get-in-items]]) + (:require-macros [looped-in.macros :refer [get-in-item]]) (:import (goog.ui Zippy))) (enable-console-print!) -(defn comment-dom [comment] - (let [text (.-text comment) - author (.-author comment) - children (array-seq (.-children comment)) - $text (dom/createDom "div" - #js {:class "commentText body20"} - (dom/safeHtmlToNode (Sanitizer/sanitize text))) - $author (dom/createDom "div" - #js {:class "commentAuthor"} - author) - $card (dom/createDom "div" #js {:class "card"} $text $author)] - (if (> (count children) 0) - (let [$toggle (dom/createDom "img" - #js {:class "commentToggle" - :src "icons/arrowhead-down-16.svg" - :width "16px" - :height "16px"}) - $children (apply dom/createDom - "div" - #js {:class "commentChildren"} - (clj->js (map comment-dom children)))] - (Zippy. $toggle $children) - (dom/appendChild $card $toggle) - (dom/createDom "div" - #js {:class "comment"} - $card - $children)) - (dom/createDom "div" - #js {:class "comment"} - $card)))) +(defn obj->clj [obj] + (into {} (for [k (.keys js/Object obj)] + [(keyword k) + (let [v (aget obj k)] + (cond + (object? v) (obj->clj v) + (.isArray js/Array v) (map obj->clj (array-seq v)) + :default (js->clj v)))]))) -(defn comments-dom [comments] - (clj->js - (apply dom/createDom - "div" - #js {:class "comments"} - (map comment-dom comments)))) - -(defn story-dom [story] - (let [$title (dom/createDom "div" - #js {:class "storyTitle title20 card"} - (.-title story)) - $comments (comments-dom (filter #(= "comment" (.-type %)) (array-seq (.-children story))))] - (Zippy. $title $comments) - (dom/createDom "div" - #js {:class "story"} - $title - $comments))) +(defn fetch-item + "Fetch the item with id `id`" + [id] + (go (-> js/browser + (.-runtime) + (.sendMessage (clj->js {:type "fetchItem" + :id id})) + (promise->channel) + (clj)))) (defn model "Returns initial sidebar state" [] - {:item () - :hits () + {:item nil + :hits nil :depth [] :loading false}) @@ -73,8 +44,12 @@ "Given a message and the old state, returns the new state" [msg state] (case (:type msg) - :item (assoc state :item (:item msg)) - :hits (assoc state :hits (:hits msg)) + :got-item (-> state + (assoc :item (:item msg)) + (assoc :loading false)) + :got-hits (-> state + (assoc :hits (:hits msg)) + (assoc :loading false)) :loading (assoc state :loading (:loading msg)) state)) @@ -82,22 +57,48 @@ "Given a callback to dispatch an update message and the sidebar state, returns the sidebar DOM" [dispatch-message state] (log/debug state) - (if (:loading state) - (components/loader) - (map #(-> (components/card - (components/body30 (:title %)) - (components/story-caption (:points %) - (:author %) - (* (:created_at_i %) 1000)) - (components/comments-indicator (:num_comments %))) - ((fn [card] - (if (> (:num_comments %) 0) - (components/with-classes card "clickable") - card))) - (components/with-listener - "click" - (fn [e] (log/debug %)))) - (:hits state)) + (cond + (:loading state) (components/loader) + (:item state) (let [current-item (get-in-item (:item state) (:depth state))] + (cons + (case (:type current-item) + "story" (components/card + (components/body30 (:title current-item)) + (components/story-caption (:points current-item) + (:author current-item) + (* (:created_at_i current-item) 1000))) + "comment" ()) + (map (fn [child] + (-> (components/card + (components/comment-caption (:author child) + (* (:created_at_i child) 1000)) + (components/comment-text (:text child)) + (components/replies-indicator (count (:children child)))) + (components/with-classes "child"))) + (->> (:children current-item) + (filter #(contains? % :text)) + (sort-by #(count (:children %)) #(compare %2 %1)))))) + (:hits state) (map (fn [hit] + (-> (components/card + (components/body30 (:title hit)) + (components/story-caption (:points hit) + (:author hit) + (* (:created_at_i hit) 1000)) + (components/comments-indicator (:num_comments hit))) + ((fn [card] + (if (> (:num_comments hit) 0) + (components/with-classes card "clickable") + card))) + (components/with-listener + "click" + (fn [e] + (dispatch-message {:type :loading :loading true}) + (go + (-> (fetch-item (:objectID hit)) + ( (count current-item) 1) (map #(components/card (:title %)) current-item) @@ -122,15 +123,6 @@ (run-render-loop new-state)))] (render (view dispatch-message state)))) -(defn obj->clj [obj] - (into {} (for [k (.keys js/Object obj)] - [(keyword k) - (let [v (aget obj k)] - (cond - (object? v) (obj->clj v) - (.isArray js/Array v) (map obj->clj (array-seq v)) - :default (js->clj v)))]))) - (defn handle-close-button [e] (.postMessage js/window.parent (clj->js {:type "closeSidebar"}) "*")) @@ -145,17 +137,6 @@ (array-seq) ((fn [hits] (map obj->clj hits)))))) -(defn fetch-item - "Fetch the item with id `id`" - [id] - (go (-> js/browser - (.-runtime) - (.sendMessage (clj->js {:type "fetchItem" - :id id})) - (promise->channel) - (clj)))) - (defn init "Initializes the sidebar" [] @@ -164,10 +145,8 @@ (run-render-loop initial-state) (go (-> (fetch-hits) (