From da4c12803f369be0a63e97c1578f5c4c772c2fd4 Mon Sep 17 00:00:00 2001 From: Oliver Hine Date: Mon, 13 Jan 2020 13:41:56 -0800 Subject: [PATCH] Snackbar to indicate that a tutorial is available, fixes #17 --- README.md | 11 +++++---- dev-resources/public/css/re-learn.css | 31 ++++++++++++++++++++++++++ example/checkout/checkout/app.cljs | 2 +- src/re_learn/model.cljs | 26 +++++++++++++++++----- src/re_learn/schema.cljs | 11 ++++----- src/re_learn/views.cljs | 32 +++++++++++++++++++-------- 6 files changed, 89 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 965c1c5..b6db1c8 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,13 @@ Combine lessons into tutorials and attach them to views: ```clojure (def checkout (re-learn/with-tutorial - {:id :checkout-tutorial - :name "The checkout" - :description "Review your basket, check the price and confirm your purchase" - :precedence 1 ;; optional, allows some tutorials to take precedence over others + {:id :checkout-tutorial + :name "The checkout" + :description "Review your basket, check the price and confirm your purchase" + :precedence 1 ;; optional, allows some tutorials to take precedence over others + :auto-accept? false ;; optional, defaults to false + ;; when true will start the tutorial immediately when this component is rendered + ;; when false will display a snackbar indicating that a tutorial is available :lessons [{:id :welcome-lesson ;; this is an inline lesson, not attached to anything :description "Welcome to the re-learn example"} basket diff --git a/dev-resources/public/css/re-learn.css b/dev-resources/public/css/re-learn.css index 9626675..4516cf7 100644 --- a/dev-resources/public/css/re-learn.css +++ b/dev-resources/public/css/re-learn.css @@ -99,6 +99,37 @@ border-bottom-color: rgba(0, 0, 0, 0.8); } +/* toast */ + +.toast { + position: fixed; + bottom: 0; + left: 50%; + transform: translateX(-50%); + background-color: rgba(0, 0, 0, 0.8); + color: white; + padding: 1em; + z-index: 10000; + border-top-right-radius: 4px; + border-top-left-radius: 4px; +} + +.toast button { + margin-left: 4px; + padding: 4px; + cursor: pointer; + border: none; + color: white; +} + +.toast button.accept { + background-color: rgb(255,64,129); +} + +.toast button.dismiss { + background-color: rgba(0, 0, 0, 0.4); +} + /* tutorial context */ .context-container { diff --git a/example/checkout/checkout/app.cljs b/example/checkout/checkout/app.cljs index bbcee2e..66e6569 100644 --- a/example/checkout/checkout/app.cljs +++ b/example/checkout/checkout/app.cljs @@ -130,7 +130,7 @@ app-root (js/document.getElementById "app")] (reagent/render [checkout app-db] app-root) - (reagent/render [re-learn-views/tutorial {:context? true}] tutorial-root))) + (reagent/render [re-learn-views/tutorial {:context? true :auto-accept? true}] tutorial-root))) (defn- on-figwheel-reload [] (mount-all)) diff --git a/src/re_learn/model.cljs b/src/re_learn/model.cljs index 22aef87..cd14767 100644 --- a/src/re_learn/model.cljs +++ b/src/re_learn/model.cljs @@ -1,6 +1,5 @@ (ns re-learn.model (:require [re-frame.core :as re-frame] - [re-frame.std-interceptors :refer [trim-v]] [re-learn.local-storage :as local-storage] [re-learn.schema :refer [ReLearnModel]] [re-learn.dom :refer [->absolute-bounds in-viewport? viewport-height viewport-width]] @@ -31,7 +30,7 @@ (re-frame/reg-event-fx ::hard-reset interceptors (fn [{:keys [db]}] - {:db (assoc db :lessons-learned {}) + {:db (assoc db :lessons-learned {} :accepted-tutorials #{}) ::local-storage/save [:re-learn/lessons-learned {}]})) (re-frame/reg-fx ::on-dom-event @@ -93,8 +92,11 @@ (re-frame/reg-event-fx ::lesson-learned interceptors - (fn [{:keys [db]} lesson-ids] - (let [lessons-learned (reduce (fn [learned lesson-id] + (fn [{:keys [db]} [lesson-id-or-ids]] + (let [lesson-ids (if (coll? lesson-id-or-ids) + lesson-id-or-ids + [lesson-id-or-ids]) + lessons-learned (reduce (fn [learned lesson-id] (assoc learned lesson-id (get-in db [:lessons lesson-id :version]))) (:lessons-learned db) @@ -109,6 +111,18 @@ {:db (assoc db :lessons-learned lessons-learned) ::local-storage/save [:re-learn/lessons-learned lessons-learned]}))) +(re-frame/reg-event-fx ::skip-tutorial + interceptors + (fn [{:keys [db]} [tutorial-id]] + (let [tutorial (get-in db [:tutorials tutorial-id])] + (js/console.log "tutorial" (pr-str tutorial-id) (pr-str tutorial)) + {:dispatch [::lesson-learned (:lessons tutorial)]}))) + +(re-frame/reg-event-db ::accept-tutorial + interceptors + (fn [db [tutorial-id]] + (update db :accepted-tutorials (fnil conj #{}) tutorial-id))) + (defn- ->lesson-id [lesson] (cond (keyword? lesson) @@ -179,10 +193,12 @@ (let [state (state db)] (first (for [tutorial (->> (:tutorials state) vals (sort-by :precedence)) :let [lessons (keep (:lessons state) (:lessons tutorial)) - [learned to-learn] (split-with #(already-learned? (:lessons-learned state) %) lessons)] + [learned to-learn] (split-with #(already-learned? (:lessons-learned state) %) lessons) + accepted? (contains? (:accepted-tutorials state) (:id tutorial))] lesson to-learn :let [total (count lessons)]] {:tutorial tutorial + :accepted? accepted? :learned learned :to-learn to-learn :completion {:ratio (/ (+ (count learned) 0.5) total) diff --git a/src/re_learn/schema.cljs b/src/re_learn/schema.cljs index e6b843f..c7a051d 100644 --- a/src/re_learn/schema.cljs +++ b/src/re_learn/schema.cljs @@ -26,8 +26,9 @@ :precedence s/Int}) (def ReLearnModel - {:help-mode? s/Bool - :highlighted-lesson-id (s/maybe LessonId) - (s/optional-key :lessons-learned) {LessonId s/Int} - (s/optional-key :lessons) {LessonId Lesson} - (s/optional-key :tutorials) {TutorialId Tutorial}}) + {:help-mode? s/Bool + :highlighted-lesson-id (s/maybe LessonId) + (s/optional-key :lessons-learned) {LessonId s/Int} + (s/optional-key :lessons) {LessonId Lesson} + (s/optional-key :tutorials) {TutorialId Tutorial} + (s/optional-key :accepted-tutorials) #{TutorialId}}) diff --git a/src/re_learn/views.cljs b/src/re_learn/views.cljs index c5727b0..a0ee672 100644 --- a/src/re_learn/views.cljs +++ b/src/re_learn/views.cljs @@ -112,7 +112,7 @@ (gstring/unescapeEntities "❱")])]) [:div.context-controls - [:a {:on-click #(re-frame/dispatch (into [::model/lesson-learned] (map :id (:to-learn @context))))} + [:a {:on-click #(re-frame/dispatch [::model/skip-tutorial (get-in @context [:tutorial :id])])} "SKIP " (gstring/unescapeEntities "⟫")]] (when (pos? (get-in @context [:completion :total])) @@ -168,14 +168,28 @@ (defn tutorial "Root view for displaying unlearned tutorials on the page. The :context? key allows you to turn on the context view which shows progress through the tutorial - at the bottom of the screen." - [{:keys [context?]}] + at the bottom of the screen. + The :auto-accept? key when false shows a notification that lessons are available allowing the user to choose to start one, + rather than starting the tutorial straight away when true (legacy behaviour)" + [{:keys [context? auto-accept?] + :or {context? false + auto-accept? false}}] (let [tutorial (re-frame/subscribe [::model/current-tutorial]) help-mode? (re-frame/subscribe [::model/help-mode?])] (fn [] - (if @help-mode? - [help-mode] - [:div - [lesson-view (:current-lesson @tutorial)] - (when context? - [lesson-context tutorial])])))) + (cond @help-mode? + [help-mode] + + (and (false? auto-accept?) (false? (:accepted? @tutorial))) + [:div.toast + "There is a tutorial available" + [:button.accept {:on-click #(re-frame/dispatch [::model/accept-tutorial (get-in @tutorial [:tutorial :id])])} + "Start"] + [:button.dismiss {:on-click #(re-frame/dispatch [::model/skip-tutorial (get-in @tutorial [:tutorial :id])])} + "Dismiss"]] + + (or auto-accept? (:accepted? @tutorial)) + [:div + [lesson-view (:current-lesson @tutorial)] + (when context? + [lesson-context tutorial])]))))