From c5c9ae4ae58773c5a1a4608a4187d26c25c8d8e0 Mon Sep 17 00:00:00 2001 From: Daniel Leong Date: Sat, 9 Feb 2019 22:51:19 -0500 Subject: [PATCH] Initial groundwork for DM Screen / Campaign UI (#103) * Add routing for DM screens ("campaigns") Refs #69 * Scaffold out Campaign pages Refs #69 * Add initial campaign creation UI/UX * Include campaigns in "query-sheets" results; implement :known-campaigns * Add preliminary chars carousel widget Refs #69 Mostly just ensures sheets are loaded; styling to come... eventually * Persist sheet type properly from `:put-sheet!` * Introduce reg-id-sub that supports passing the sheet-id of interest Refs #69 This is not 100% bullet-proof, but it's relatively painless to use in place of an existing subscription, and it allows those subscriptions to *optionally* be used with an arbitrary sheet-id, for use on character cards for a campaign page. * Fix merge error * Implement some core styling for chars carousel, hp bar, etc. Refs #69 The character card is starting to look useful! * Compute smooth color transitions for the HP bar * Add a nice CSS transition on the HP bar * Scaffold out campaign invitation Refs #69 Basically the DM has to add them to the DM sheet, then the player has to open a special "invite" link to "accept" (IE: save the "campaign" field in their own sheet). Sort of an extra step, but reasonable, I think. * Fix ::limited-use being an id-sub * Support including campaign name in invite URLs Also makes it possible to hot-swap routes, which is just fantastic * Sketch out UI for joining a campaign * Implement event flow for joining a campaign We'd probably like to expand the "update-notifier" functionality to be more general so we can show "Campaign joined!" or something as we enter the sheet page * Refactor to have a more general notifiers system * Fix typo * Show notification when joining/leaving a campaign * Move Campaign events/subs to own file; support add/remove members Refs #69 * Add campaign invite instructions + some minor style improvements * Sort others' characters before your own in "add" screen * Show notifiers above overlays * Move "add character" button to the end of the carousel * Add some placeholders and a link to the Campaign UI ticket It's about time for this branch to get merged; there's a ton of refactoring in it already; all the remaining features can be done in one or more seperate branches. The only thing we really need to do before merging is allow players to detach the campaign from their character. * Add a widget in the Builder for leaving the current campaign --- dev/cljs/wish/config.cljs | 1 + less/site.less | 119 ++++++++++++++++-- prod/cljs/wish/config.cljs | 3 + src/cljs/wish/db.cljs | 4 +- src/cljs/wish/events.cljs | 43 ++++++- src/cljs/wish/fx.cljs | 4 + src/cljs/wish/providers.cljs | 19 +-- src/cljs/wish/providers/caching.cljs | 4 +- src/cljs/wish/providers/core.cljs | 9 +- src/cljs/wish/providers/gdrive.cljs | 26 ++-- src/cljs/wish/providers/gdrive/api.cljs | 12 +- src/cljs/wish/providers/wish.cljs | 2 +- src/cljs/wish/routes.cljs | 34 ++++- src/cljs/wish/sheets.cljs | 50 ++++++-- src/cljs/wish/sheets/dnd5e.cljs | 51 ++++---- src/cljs/wish/sheets/dnd5e/builder.cljs | 28 +++-- src/cljs/wish/sheets/dnd5e/campaign.cljs | 32 +++++ .../wish/sheets/dnd5e/campaign/style.cljs | 19 +++ src/cljs/wish/sheets/dnd5e/subs.cljs | 68 +++++----- src/cljs/wish/subs.cljs | 85 +++++++++---- src/cljs/wish/subs_util.cljs | 83 +++++++++++- src/cljs/wish/util/nav.cljs | 47 +++++-- src/cljs/wish/views.cljs | 14 ++- src/cljs/wish/views/campaign/base.cljs | 24 ++++ .../wish/views/campaign/chars_carousel.cljs | 108 ++++++++++++++++ src/cljs/wish/views/campaign/events.cljs | 54 ++++++++ src/cljs/wish/views/campaign/hp_bar.cljs | 56 +++++++++ src/cljs/wish/views/campaign/join.cljs | 29 +++++ src/cljs/wish/views/campaign/subs.cljs | 28 +++++ src/cljs/wish/views/campaign_browser.cljs | 29 +++++ src/cljs/wish/views/home.cljs | 38 ++++-- src/cljs/wish/views/new_campaign.cljs | 98 +++++++++++++++ src/cljs/wish/views/notifiers.cljs | 43 +++++++ src/cljs/wish/views/router.cljs | 2 +- src/cljs/wish/views/sheet_builder_util.cljs | 38 +++++- src/cljs/wish/views/update_notifier.cljs | 17 --- src/cljs/wish/views/widgets.cljs | 2 +- 37 files changed, 1143 insertions(+), 180 deletions(-) create mode 100644 src/cljs/wish/sheets/dnd5e/campaign.cljs create mode 100644 src/cljs/wish/sheets/dnd5e/campaign/style.cljs create mode 100644 src/cljs/wish/views/campaign/base.cljs create mode 100644 src/cljs/wish/views/campaign/chars_carousel.cljs create mode 100644 src/cljs/wish/views/campaign/events.cljs create mode 100644 src/cljs/wish/views/campaign/hp_bar.cljs create mode 100644 src/cljs/wish/views/campaign/join.cljs create mode 100644 src/cljs/wish/views/campaign/subs.cljs create mode 100644 src/cljs/wish/views/campaign_browser.cljs create mode 100644 src/cljs/wish/views/new_campaign.cljs create mode 100644 src/cljs/wish/views/notifiers.cljs delete mode 100644 src/cljs/wish/views/update_notifier.cljs diff --git a/dev/cljs/wish/config.cljs b/dev/cljs/wish/config.cljs index 090f5424..ed61b5f3 100644 --- a/dev/cljs/wish/config.cljs +++ b/dev/cljs/wish/config.cljs @@ -8,6 +8,7 @@ ^boolean goog.DEBUG) (def server-root "") +(def full-url-root (str "http://localhost:3450" server-root)) (def gdrive-client-id "661182319990-1uerkr0pue6k60a83atj2f58md95fb1b.apps.googleusercontent.com") diff --git a/less/site.less b/less/site.less index c09780ac..8449be9b 100644 --- a/less/site.less +++ b/less/site.less @@ -86,6 +86,10 @@ a { cursor: pointer; } +.metadata() { + font-size: 80%; +} + .scrollable() { overflow-y: scroll; .box-shadow(inset 0 -8px 12px -4px fade(#333, 50%)); @@ -189,10 +193,14 @@ a { } .metadata { - font-size: 80%; + .metadata(); } } +.group { + margin: 8px 0; +} + .sections { .flex(wrap); .justify-content(center); @@ -259,13 +267,20 @@ a { margin-top: 12px; } -.update-notifier { +.notifiers { @media @smartphones { left: 0; right: 0; bottom: 0; } + position: fixed; + right: 24px; + bottom: 24px; + z-index: 2; +} + +.notifier { @keyframes slide-in { from { transform: translateY(100%); @@ -282,13 +297,11 @@ a { background-color: #666666; color: #fff; - position: fixed; - right: 24px; - bottom: 24px; - padding: 4px; vertical-align: center; + margin-top: 8px; + .ignore { .justify-content(center); @@ -307,12 +320,104 @@ a { .flex-grow(); } - .update { + .action { margin-right: 8px; padding: 8px; } } +// +// Campaign screen widgets + +.add-chars-overlay { + padding: 32px; + + .desc { + .metadata(); + padding: 8px; + } + + .character { + .flex(); + .align-items(center); + + width: 100%; + margin: 8px 0; + + .name { + width: 45%; + } + } +} + +.carousel-container { + width: 100%; + overflow-x: auto; + + .carousel { + .flex(); + + a.add-button { + .flex(); + .flex-direction(row); + .align-items(center); + + padding: 8px; + } + + a.card { + color: inherit; + margin: 8px 4px; + padding: 0px; + } + div.card { + background-color: #eee; + border-radius: 4px; + padding: 8px; + text-align: center; + width: 220px; + + &:hover { + background-color: #ddd; + } + + &:active { + background-color: #ccc; + } + } + } +} + +.hp-bar { + @height: 32pt; + + position: relative; + background-color: #fcfcfc; + border-radius: 4px; + height: @height; + margin: 8px 0; + + .bar { + position: absolute; + left: 0; + top: 0; + bottom: 0; + border-radius: 4px; + + transition: all 450ms cubic-bezier(0, 0, 0.2, 1); + } + + .label { + font-size: 16pt; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + line-height: @height; + } +} + // // shared widgets diff --git a/prod/cljs/wish/config.cljs b/prod/cljs/wish/config.cljs index bd4d5353..5470a037 100644 --- a/prod/cljs/wish/config.cljs +++ b/prod/cljs/wish/config.cljs @@ -9,6 +9,9 @@ ; IE: https://dhleong.github.io/wish (def server-root "/wish") +; TODO good-define? +(def full-url-root (str "https://dhleong.github.io" server-root)) + (def gdrive-client-id "661182319990-3aa8akj9fh8eva9lf7bt02621q2i18s6.apps.googleusercontent.com") (def push-server "https://wish-server.now.sh") diff --git a/src/cljs/wish/db.cljs b/src/cljs/wish/db.cljs index d59eff4f..83240887 100644 --- a/src/cljs/wish/db.cljs +++ b/src/cljs/wish/db.cljs @@ -39,4 +39,6 @@ ::save-errors #{} ::pending-saves #{} - ::processing-saves #{}}) + ::processing-saves #{} + + :notifications {}}) diff --git a/src/cljs/wish/events.cljs b/src/cljs/wish/events.cljs index eda0e2f2..7ad87c5d 100644 --- a/src/cljs/wish/events.cljs +++ b/src/cljs/wish/events.cljs @@ -30,6 +30,12 @@ :dispatch-n [[::update-keymap page-spec] [:push/check]]})) +(reg-event-fx + :nav/replace! + [trim-v] + (fn-traced [_ [new-location]] + {:nav/replace! new-location})) + (reg-event-db :set-device [trim-v] @@ -135,6 +141,36 @@ :ready)})) +; ======= notifications =================================== + +(reg-event-fx + :notify! + [trim-v] + (fn [{:keys [db]} [{:keys [duration duration-ms content + dismissable?] + :or {dismissable? true}}]] + (let [created (js/Date.now) + id (keyword (str created))] + {:db (assoc-in db [:notifications id] + {:id id + :created created + :content content + :dismiss-event (when (or (nil? duration) + dismissable?) + [::remove-notify! id])}) + :dispatch-later [(when (or duration duration-ms) + {:ms (case duration + :short 3000 + :long 7500 + duration-ms) + :dispatch [::remove-notify! id]})]}))) + +(reg-event-db + ::remove-notify! + [trim-v (path :notifications)] + (fn-traced [notifications [id]] + (dissoc notifications id))) + ; ======= Provider management ============================== @@ -259,7 +295,11 @@ [trim-v] (fn-traced [{:keys [db]} [sheet-id sheet]] {:db (-> db - (assoc-in [:sheets sheet-id] sheet) + (update-in [:sheets sheet-id] + (fn [old-value] + (merge + (select-keys old-value [:type]) + sheet))) (reset-sheet-err sheet-id)) ; NOTE: we're probably on a :sheet page, and we might not have @@ -757,4 +797,3 @@ (when (contains? changed-ids active-sheet-id) ; trigger sheet data reload {:load-sheet! active-sheet-id}))) - diff --git a/src/cljs/wish/fx.cljs b/src/cljs/wish/fx.cljs index b8769a09..0ba48f71 100644 --- a/src/cljs/wish/fx.cljs +++ b/src/cljs/wish/fx.cljs @@ -22,6 +22,10 @@ (log "document.title <-" title) (set! js/document.title title))) +(reg-fx + :nav/replace! + nav/replace!) + ; ======= keymaps ========================================= diff --git a/src/cljs/wish/providers.cljs b/src/cljs/wish/providers.cljs index a481254b..7c6fe7fa 100644 --- a/src/cljs/wish/providers.cljs +++ b/src/cljs/wish/providers.cljs @@ -111,15 +111,20 @@ (let [[provider-id _] (unpack-id sheet-id)] (provider-key provider-id :share!))) -(defn create-sheet-with-data - "Returns a channel that emits [err sheet-id] on success" - [sheet-name provider-id data] +(defn create-file-with-data + "Returns a channel that emits [err sheet-id] on success. + `kind` may be one of: + - :campaign + - :sheet" + [kind sheet-name provider-id data] {:pre [(not (nil? provider-id)) - (not (nil? data))]} + (not (nil? data)) + (contains? #{:campaign :sheet} kind)]} (if-let [inst (provider-key provider-id :inst)] - (provider/create-sheet inst - sheet-name - data) + (provider/create-file inst + kind + sheet-name + data) (throw (js/Error. (str "No provider instance for " provider-id))))) diff --git a/src/cljs/wish/providers/caching.cljs b/src/cljs/wish/providers/caching.cljs index 2ec5e73e..8fe955cf 100644 --- a/src/cljs/wish/providers/caching.cljs +++ b/src/cljs/wish/providers/caching.cljs @@ -49,8 +49,8 @@ (deftype CachingProvider [base my-id storage dirty?-storage] IProvider (id [this] my-id) - (create-sheet [this file-name data] - (provider/create-sheet base file-name data)) + (create-file [this kind file-name data] + (provider/create-file base kind file-name data)) (init! [this] (go (let [base-state (or ( raw-file - (select-keys [:name]) - (assoc :mine? (:ownedByMe raw-file)))]))))] + {:name (:name raw-file) + :mine? (:ownedByMe raw-file) + :type (-> raw-file + :appProperties + :wish-type + (subs (count "wish-")) + keyword)}]))))] (-> js/gapi.client.drive.files (.list #js {:q q :pageSize page-size :spaces "drive" - :fields "nextPageToken, files(id, name, ownedByMe)"}) + :fields "nextPageToken, files(id, name, ownedByMe, appProperties)"}) (promise->chan 1 (map (fn [[err resp :as r]] (if resp ; success! diff --git a/src/cljs/wish/providers/wish.cljs b/src/cljs/wish/providers/wish.cljs index 0110e992..cb365fb5 100644 --- a/src/cljs/wish/providers/wish.cljs +++ b/src/cljs/wish/providers/wish.cljs @@ -21,7 +21,7 @@ (deftype WishProvider [] IProvider (id [this] :wish) - (create-sheet [this file-name data] + (create-file [this kind file-name data] (to-chan [[(js/Error. "Not implemented") nil]])) (init! [this] diff --git a/src/cljs/wish/routes.cljs b/src/cljs/wish/routes.cljs index a7d8b93c..3eea670e 100644 --- a/src/cljs/wish/routes.cljs +++ b/src/cljs/wish/routes.cljs @@ -1,17 +1,36 @@ (ns wish.routes (:require-macros [secretary.core :refer [defroute]]) (:require [pushy.core :as pushy] + [secretary.core :as secretary] [wish.util.nav :as nav :refer [hook-browser-navigation! navigate!]])) -(defn app-routes [] - (nav/init!) +(defn- def-routes [] + (secretary/reset-routes!) + + ;; + ;; app routes declared here: - ;; -------------------- - ;; define routes here (defroute "/" [] (navigate! :home)) + (defroute "/campaigns" [] + (navigate! :campaign-browser)) + + (defroute "/campaigns/new" [] + (navigate! :new-campaign)) + + (defroute #"/campaigns/([a-z0-9-]+/[^/]+)" [id] + (navigate! :campaign [(keyword id)])) + + (defroute #"/join-campaign/([a-z0-9-]+/[^/]+)(/n/[^/]+)?/as/(.*)" [campaign-id label sheet-id] + (navigate! :join-campaign [(keyword campaign-id) + (keyword sheet-id) + (when-not (empty? label) + (js/decodeURIComponent + ; trim off /n/ + (subs label 3)))])) + (defroute "/sheets" [] (navigate! :sheet-browser)) @@ -29,7 +48,12 @@ (defroute "/providers/:provider-id/config" [provider-id] (navigate! :provider-config (keyword provider-id))) + ) + +(defn app-routes [] + (nav/init!) + + (def-routes) - ;; -------------------- (hook-browser-navigation!)) diff --git a/src/cljs/wish/sheets.cljs b/src/cljs/wish/sheets.cljs index faaa8d30..4198819e 100644 --- a/src/cljs/wish/sheets.cljs +++ b/src/cljs/wish/sheets.cljs @@ -3,10 +3,11 @@ wish.sheets (:require [wish.sheets.dnd5e :as dnd5e] [wish.sheets.dnd5e.builder :as dnd5e-builder] + [wish.sheets.dnd5e.campaign :as dnd5e-campaign] [wish.sheets.dnd5e.keymaps :as dnd5e-key] [wish.sheets.dnd5e.util :as dnd5e-util] [wish.sources.compiler :refer [compiler-version]] - [wish.providers :refer [create-sheet-with-data + [wish.providers :refer [create-file-with-data error-resolver-view]] [wish.util :refer [click>evt evt]] [wish.views.error-boundary :refer [error-boundary]] @@ -20,6 +21,7 @@ {:dnd5e {:name "D&D 5E" :fn #'dnd5e/sheet :builder #'dnd5e-builder/view + :campaign #'dnd5e-campaign/view :v 1 :default-sources [:wish/wdnd5e-srd] @@ -63,6 +65,25 @@ ; no processor for this sheet; pass through data)) +(defn stub-campaign + "Create the initial data for a new campaign" + [kind campaign-name] + (let [kind-meta (get sheets kind)] + (when-not kind-meta + (throw (js/Error. + (str "Unable to get sheet meta for kind: " kind)))) + + {:v [compiler-version (:v kind-meta)] ; wish + sheet version numbers + :updated (.getTime (js/Date.)) ; date + :kind kind + + :name campaign-name + + :sources (:default-sources kind-meta) + + :players #{} + })) + (defn stub-sheet "Create the initial data for a new sheet" [kind sheet-name] @@ -87,13 +108,21 @@ :equipped #{} })) +(defn create-campaign! + "Returns a channel that emits [err sheet-id] on success" + [campaign-name provider-id sheet-kind] + {:pre [(not (nil? provider-id)) + (not (nil? sheet-kind))]} + (create-file-with-data :campaign campaign-name provider-id + (stub-campaign sheet-kind campaign-name))) + (defn create-sheet! "Returns a channel that emits [err sheet-id] on success" [sheet-name provider-id sheet-kind] {:pre [(not (nil? provider-id)) (not (nil? sheet-kind))]} - (create-sheet-with-data sheet-name provider-id - (stub-sheet sheet-kind sheet-name))) + (create-file-with-data :sheet sheet-name provider-id + (stub-sheet sheet-kind sheet-name))) ; ======= Views ============================================ @@ -169,12 +198,19 @@ [[sheet-id section]] (ensuring-loaded sheet-id - (fn [sheet-info] - [(:builder sheet-info) section]))) + (fn [{view :builder}] + [view section]))) + +(defn campaign + [[campaign-id section]] + (ensuring-loaded + campaign-id + (fn [{view :campaign}] + [view section]))) (defn viewer [sheet-id] (ensuring-loaded sheet-id - (fn [sheet-info] - [(:fn sheet-info)]))) + (fn [{view :fn}] + [view]))) diff --git a/src/cljs/wish/sheets/dnd5e.cljs b/src/cljs/wish/sheets/dnd5e.cljs index 976f31ea..9894b926 100644 --- a/src/cljs/wish/sheets/dnd5e.cljs +++ b/src/cljs/wish/sheets/dnd5e.cljs @@ -58,14 +58,16 @@ (icon :radio-button-unchecked.icon {:class icon-class})) {:key i}))]) -(defn- hp-death-saving-throws [] - (let [{:keys [saves fails]} ( - [save-indicators "😇" :save saves] - [save-indicators "☠️" :fail fails]])) +(defn hp-death-saving-throws + ([] (hp-death-saving-throws nil)) + ([sheet-id] + (let [{:keys [saves fails]} ( + [save-indicators "😇" :save saves] + [save-indicators "☠️" :fail fails]]))) (defn hp [] - (let [[hp max-hp] (evt [:toggle-overlay [#'overlays/hp-overlay]])} @@ -154,26 +156,33 @@ [:wis "WIS"] [:cha "CHA"]]) +(defn abilities-display + ([abilities] (abilities-display abilities false)) + ([abilities clickable?] + [:<> + (for [[id label] labeled-abilities] + (let [{:keys [score modifier mod]} (get abilities id)] + ^{:key id} + [:div.ability {:class (when mod + (case mod + :buff "buffed" + :nerf "nerfed")) + :on-click (when clickable? + (click>evt [:toggle-overlay + [#'overlays/ability-tmp + id + label]]))} + [:div.label label] + [:div.mod modifier] + [:div.score "(" score ")"] + ]))])) + (defn abilities-section [] (let [abilities (evt [:toggle-overlay - [#'overlays/ability-tmp - id - label]])} - [:div.label label] - [:div.mod modifier] - [:div.score "(" score ")"] - ]))] + [abilities-display abilities :clickable]] [:div.info "Saves"] diff --git a/src/cljs/wish/sheets/dnd5e/builder.cljs b/src/cljs/wish/sheets/dnd5e/builder.cljs index 66a48bef..2031a572 100644 --- a/src/cljs/wish/sheets/dnd5e/builder.cljs +++ b/src/cljs/wish/sheets/dnd5e/builder.cljs @@ -15,7 +15,9 @@ [wish.style :refer-macros [defclass defstyled]] [wish.style.flex :as flex :refer [flex]] [wish.style.shared :as style] - [wish.views.sheet-builder-util :refer [data-source-manager router + [wish.views.sheet-builder-util :refer [campaign-manager + data-source-manager + router count-max-options]] [wish.views.widgets :refer [formatted-text]] [wish.views.widgets.dynamic-list] @@ -82,18 +84,22 @@ (defn home-page [] [:div - [:h3 "Home" - [bind-fields - [:div - [:input {:field :text - :id :name}] ] + [:h3 "Home"] + [bind-fields + [:div + [:input {:field :text + :id :name}] ] - {:get #(get-in (evt [:update-meta path (constantly v)]))}] + {:get #(get-in (evt [:update-meta path (constantly v)]))}] - ; data source mgmt - [data-source-manager]]]) + ; campaign mgmt + [campaign-manager] + + ; data source mgmt + [data-source-manager] + ]) (defn- feature-option [option selected?] diff --git a/src/cljs/wish/sheets/dnd5e/campaign.cljs b/src/cljs/wish/sheets/dnd5e/campaign.cljs new file mode 100644 index 00000000..9a3955ba --- /dev/null +++ b/src/cljs/wish/sheets/dnd5e/campaign.cljs @@ -0,0 +1,32 @@ +(ns ^{:author "Daniel Leong" + :doc "Campaign-viewer for D&D 5e"} + wish.sheets.dnd5e.campaign + (:require [wish.sheets.dnd5e.subs :as subs] + [wish.sheets.dnd5e :as dnd5e] + [wish.sheets.dnd5e.campaign.style :as style] + [wish.views.campaign.base :as base] + [wish.views.campaign.hp-bar :refer [hp-bar]] + [wish.views.widgets :as widgets + :refer-macros [icon] + :refer [link link>evt]] + [wish.util :refer [evt]])) + +(defn char-card [{:keys [id] :as c}] + [:div style/char-card + [:div.name (:name c)] + + [:div.hp + (let [[hp max-hp] ( hp 0) + [hp-bar hp max-hp] + [dnd5e/hp-death-saving-throws id]))] + + [:div.abilities + (let [info (str]] [wish.sheets.dnd5e.builder.data :refer [point-buy-max score-point-cost]] + [wish.subs-util :refer [reg-id-sub query-vec->preferred-id]] [wish.util :refer [map]] [wish.util.string :as wstr])) @@ -91,7 +92,7 @@ "Convenience for creating a sub that just gets a specific field from the :sheet key of the sheet-meta" [id getter] - (reg-sub + (reg-id-sub id :<- [:meta/sheet] (fn [sheet _] @@ -143,7 +144,7 @@ ; ======= class and level ================================== -(reg-sub +(reg-id-sub ::class->level :<- [:classes] (fn [classes _] @@ -153,19 +154,19 @@ {} classes))) -(reg-sub +(reg-id-sub ::class-level :<- [::class->level] (fn [classes [_ class-id]] (get classes class-id))) -(reg-sub +(reg-id-sub ::abilities-raw :<- [:meta/sheet] (fn [sheet] (:abilities sheet))) -(reg-sub +(reg-id-sub ::abilities-improvements :<- [:classes] :<- [:races] @@ -175,7 +176,7 @@ (map (comp :buffs :attrs)) (apply merge-with +)))) -(reg-sub +(reg-id-sub ::abilities-racial :<- [:race] (fn [race] @@ -188,7 +189,7 @@ ; TODO when we do handle equippable item buffs here, we need ; to make sure ::available-classes doesn't use it (only ability ; score improvements and racial bonuses ...) -(reg-sub +(reg-id-sub ::abilities-base :<- [::abilities-raw] :<- [::abilities-racial] @@ -199,7 +200,7 @@ race improvements))) -(reg-sub +(reg-id-sub ::abilities :<- [::abilities-base] :<- [:meta/sheet] @@ -208,7 +209,7 @@ base (:ability-tmp sheet)))) -(reg-sub +(reg-id-sub ::ability-modifiers :<- [::abilities] (fn [abilities] @@ -218,7 +219,7 @@ {} abilities))) -(reg-sub +(reg-id-sub ::ability-saves :<- [::ability-modifiers] :<- [::proficiency-bonus] @@ -238,7 +239,7 @@ {} modifiers))) -(reg-sub +(reg-id-sub ::ability-info :<- [::abilities] :<- [::abilities-base] @@ -262,7 +263,7 @@ {} abilities))) -(reg-sub +(reg-id-sub ::skill-info :<- [::ability-modifiers] :<- [::skill-expertise] @@ -294,7 +295,7 @@ {} data/skill-id->ability))) -(reg-sub +(reg-id-sub ::limited-uses :<- [:limited-uses] :<- [:total-level] @@ -355,13 +356,17 @@ :max-slots (:total input)} ))) -(reg-sub +(reg-id-sub ::rolled-hp :<- [:meta/sheet] (fn [sheet [_ ?path]] (get-in sheet (concat [:hp-rolled] - ?path)))) + + ; NOTE: as an id-sub, we can also be called + ; where the var at this position is the sheet id + (when (coll? ?path) + ?path))))) (reg-sheet-sub ::temp-hp @@ -372,7 +377,7 @@ :temp-max-hp) (def ^:private compile-hp-buff (memoize ->callable)) -(reg-sub +(reg-id-sub ::max-hp-buffs :<- [:race] :<- [:classes] @@ -391,7 +396,7 @@ entity))) (apply +)))) -(reg-sub +(reg-id-sub ::max-hp-mode :<- [:meta/sheet] (fn [sheet] @@ -404,7 +409,7 @@ ; default to :average for new users :average))) -(reg-sub +(reg-id-sub ::max-hp-rolled :<- [::rolled-hp] :<- [::class->level] @@ -431,7 +436,7 @@ (apply +)))) -(reg-sub +(reg-id-sub ::max-hp-average :<- [:classes] (fn [classes] @@ -456,24 +461,23 @@ 0 ; start at 0 classes))) -; NOTE: we use reg-sub-raw here since it feels wrong to -(reg-sub +(reg-id-sub ::max-hp - (fn [] + (fn [query-vec] [; NOTE: this preferred-id query-vec)]) + :manual [::max-hp-rolled] + :average [::max-hp-average]) + + [::temp-max-hp] + [::abilities] + [:total-level] + [::max-hp-buffs] ]) - (fn [[base-max temp-max abilities total-level buffs]] + (fn [[base-max temp-max abilities total-level buffs :as in]] (+ base-max temp-max @@ -485,7 +489,7 @@ buffs))) -(reg-sub +(reg-id-sub ::hp :<- [::temp-hp] :<- [::max-hp] diff --git a/src/cljs/wish/subs.cljs b/src/cljs/wish/subs.cljs index 4de50c7e..1bef5115 100644 --- a/src/cljs/wish/subs.cljs +++ b/src/cljs/wish/subs.cljs @@ -6,7 +6,7 @@ [wish.db :as db] [wish.inventory :as inv] [wish.providers :as providers] - [wish.subs-util :refer [active-sheet-id]] + [wish.subs-util :refer [active-sheet-id reg-id-sub]] [wish.sheets :as sheets] [wish.sources.compiler :refer [apply-directives inflate]] [wish.sources.compiler.lists :as lists] @@ -16,6 +16,14 @@ (reg-sub :device-type :device-type) (reg-sub :showing-overlay :showing-overlay) +(reg-sub + :notifications + (fn [db] + (->> db + :notifications + vals + (sort-by :created)))) + (reg-sub :update-available? (fn [{{:keys [latest ignored]} :updates} _] @@ -87,7 +95,7 @@ ; each part of the sheet-meta, to avoid a small edit to HP, ; for example, causing all of the spell lists and features ; (which rely on classes, etc.) to be re-calculated - (reg-sub + (reg-id-sub id :<- [:sheet-meta] (fn [sheet _] @@ -99,6 +107,8 @@ (reg-sub :sheets-filters :sheets-filters) (reg-sub :sheet-sources :sheet-sources) +; sheets +(reg-meta-sub :meta/name :name) (reg-meta-sub :meta/sheet :sheet) (reg-meta-sub :meta/sources :sources) (reg-meta-sub :meta/kind :kind) @@ -109,6 +119,10 @@ (reg-meta-sub :meta/inventory :inventory) (reg-meta-sub :meta/items :items) (reg-meta-sub :meta/equipped :equipped) +(reg-meta-sub :meta/campaign :campaign) + +; campaigns +(reg-meta-sub :meta/players :players) (reg-sub :active-sheet-source-ids @@ -118,8 +132,14 @@ (reg-sub :active-sheet-id :<- [:page] - (fn [page-vec _] - (active-sheet-id nil page-vec))) + (fn [page-vec [_ ?requested-id]] + ; NOTE: subscriptions created with wish.sub-util/reg-id-sub + ; can accept an extra param in their query vector that will + ; get passed down to us as ?requested-id, if provided; if + ; not, we just do the normal thing and extract the + ; active-sheet-id from the page vector + (or ?requested-id + (active-sheet-id nil page-vec)))) (reg-sub :sharable-sheet-id @@ -141,7 +161,7 @@ :id sheet-id))) (reg-sub - :known-sheets + ::known-files :<- [:sheets] :<- [:my-sheets] (fn [[sheets my-sheets] _] @@ -164,6 +184,15 @@ (filter :name) (sort-by :name)))) +(reg-sub + :known-sheets + :<- [::known-files] + (fn [all-files _] + (->> all-files + (filter (comp (partial = :sheet) + :type)) + (sort-by :name)))) + (reg-sub :filtered-known-sheets :<- [:known-sheets] @@ -180,9 +209,19 @@ (:shared? filters) (remove :mine? sheets)))) +(reg-sub + :known-campaigns + :<- [::known-files] + (fn [all-files _] + (->> all-files + (filter (comp (partial = :campaign) + :type)) + (sort-by :name)))) + + ; if a specific sheet-id is not provided, loads ; for the active sheet id -(reg-sub +(reg-id-sub :sheet-source :<- [:sheet-sources] :<- [:active-sheet-id] @@ -205,14 +244,14 @@ ; ======= Accessors for the active sheet =================== -(reg-sub +(reg-id-sub :sheet-meta :<- [:sheets] :<- [:active-sheet-id] (fn [[sheets id]] (get sheets id))) -(reg-sub +(reg-id-sub :classes :<- [:meta/kind] :<- [:sheet-source] @@ -235,7 +274,7 @@ ; A single class instance, or nil if none at all; if any ; class is marked primary, that class is returned. If none ; are so marked, then nil is returned -(reg-sub +(reg-id-sub :primary-class :<- [:classes] (fn [classes] @@ -244,13 +283,13 @@ first))) ; sum of levels from all classes -(reg-sub +(reg-id-sub :total-level :<- [:classes] (fn [classes _] (apply + (map :level classes)))) -(reg-sub +(reg-id-sub :races :<- [:sheet-meta] :<- [:sheet-source] @@ -277,7 +316,7 @@ :race)))))))) ; combines :attrs from all classes and races into a single map -(reg-sub +(reg-id-sub :all-attrs :<- [:classes] :<- [:races] @@ -447,7 +486,7 @@ sheet data-source])) -(reg-sub +(reg-id-sub :class-features :<- [:classes] get-features) @@ -474,12 +513,12 @@ (subscribe [:sheet-source])]) only-feature-options) -(reg-sub +(reg-id-sub :race-features :<- [:races] get-features) -(reg-sub +(reg-id-sub :inflated-race-features :<- [:race-features] :<- [:meta/options] @@ -488,7 +527,7 @@ :<- [:sheet-source] inflate-feature-options) -(reg-sub +(reg-id-sub :race-features-with-options :<- [:race-features] :<- [:meta/options] @@ -498,7 +537,7 @@ only-feature-options) ; semantic convenience for single-race systems -(reg-sub +(reg-id-sub :race :<- [:races] (fn [races _] @@ -514,7 +553,7 @@ :wish/context-type kind :wish/context entity))))) -(reg-sub +(reg-id-sub :limited-uses :<- [:classes] :<- [:races] @@ -530,7 +569,7 @@ vals (map (partial uses-with-context :item))))))) -(reg-sub +(reg-id-sub :limited-uses-map :<- [:limited-uses] (fn [limited-uses] @@ -566,7 +605,7 @@ ; will always be the (surprise) item-id. ; In addition, every item in :equipped will have the :wish/equipped? ; set to true -(reg-sub +(reg-id-sub :inventory-map :<- [:meta/kind] :<- [:meta/inventory] @@ -599,7 +638,7 @@ raw-inventory))) ; sorted list of inflated inventory items -(reg-sub +(reg-id-sub :inventory-sorted :<- [:inventory-map] (fn [inventory-map] @@ -608,7 +647,7 @@ (sort-by :name)))) ; sorted list of inflated + equipped inventory items -(reg-sub +(reg-id-sub :equipped-sorted :<- [:inventory-sorted] (fn [inventory-sorted] @@ -639,7 +678,6 @@ (fn [source [_ entity-kind]] (src/list-entities source entity-kind))) - (reg-sub :options-> :<- [:meta/options] @@ -651,6 +689,7 @@ instanced-value v)))) + ; ======= Save state ======================================= (reg-sub diff --git a/src/cljs/wish/subs_util.cljs b/src/cljs/wish/subs_util.cljs index 41e558f1..c3dba01a 100644 --- a/src/cljs/wish/subs_util.cljs +++ b/src/cljs/wish/subs_util.cljs @@ -1,6 +1,8 @@ (ns ^{:author "Daniel Leong" :doc "subs-util"} - wish.subs-util) + wish.subs-util + (:require-macros [wish.util.log :as log :refer [log]]) + (:require [re-frame.core :refer [reg-sub subscribe]])) (defn active-sheet-id [db & [page-vec]] @@ -8,9 +10,88 @@ (:page db))] (let [[page args] page-vec] (case page + :campaign (first args) + :join-campaign (second args) :sheet args :sheet-builder (first args) ; else, no sheet nil)))) +(defonce ^:private id-subs (atom #{})) +(defn- id-sub? [query-vec] + (let [query-id (first query-vec)] + (or (= :active-sheet-id query-id) + (contains? @id-subs query-id)))) + +(defn query-vec->preferred-id [query-vec] + ; TODO handle normal arguments? We can't + ; just use (last), since if no args are passed + ; that will just return the query-id + (second query-vec)) + +(defn inject-preferred-id [vec preferred-sheet-id] + (if preferred-sheet-id + (conj vec preferred-sheet-id) + vec)) + +(defn reg-id-sub + "This is a drop-in replacement for a subscription which + ultimately depends on [:active-sheet-id], which optionally + takes a single parameter in the query vector indicating + the sheet-id that should be used instead of the result of + [:active-sheet-id]" + [query-id & args] + (let [computation-fn (last args) + input-args (butlast args) + err-header (str "(reg-id-sub " query-id ")") + inputs-fn (case (count input-args) + ; error case + 0 nil + + ; single function; just pass through + 1 (let [base-fn (first input-args)] + (fn inp-fn [query-vec] + (let [base-subs (base-fn query-vec) + actual-sheet-id (query-vec->preferred-id query-vec)] + ; verify sanity + (if-not (every? vector? base-subs) + (do (log/err err-header "should return query vectors, not subscriptions") + ; use them, I guess + base-subs) + + (->> base-subs + (map #(inject-preferred-id % actual-sheet-id)) + (map subscribe)))))) + + ; single sugar pair + 2 (let [[marker vec] input-args] + (when-not (= :<- marker) + (log/err err-header "expected :<-, got:" marker)) + (fn inp-fn [query-vec] + (let [actual-sheet-id (query-vec->preferred-id query-vec)] + (subscribe (inject-preferred-id vec actual-sheet-id))))) + + ; multiple sugar pairs + (let [pairs (partition 2 input-args) + markers (map first pairs) + vecs (map last pairs) + any-id-subs? (some id-sub? vecs)] + (when-not (and (every? #{:<-} markers) (every? vector? vecs)) + (log/err err-header "expected pairs of :<- and vectors, got:" pairs)) + + (fn inp-fn [query-vec] + (let [actual-sheet-id (query-vec->preferred-id query-vec)] + (->> vecs + (map #(inject-preferred-id % actual-sheet-id)) + (map subscribe))))) + )] + (if-not inputs-fn + (log/warn err-header "must have input args") + + (do + (swap! id-subs conj query-id) + (reg-sub + query-id + inputs-fn + computation-fn))))) diff --git a/src/cljs/wish/util/nav.cljs b/src/cljs/wish/util/nav.cljs index 81142f6b..3710c2cf 100644 --- a/src/cljs/wish/util/nav.cljs +++ b/src/cljs/wish/util/nav.cljs @@ -8,6 +8,7 @@ [goog.events :as gevents] [goog.history.EventType :as HistoryEventType] [pushy.core :as pushy] + [wish.config :as config] [wish.util :refer [is-ios? >evt]]) (:import goog.History)) @@ -86,19 +87,46 @@ (js/window.location.replace (prefix new-location))) + +(defn- base-sheet-url + "Generate the url to a sheet, optionally with + extra path sections after it" + [kind id & extra-sections] + (apply str "/" kind + "/" (namespace id) + "/" (name id) + (when extra-sections + (interleave (repeat "/") + (map + #(if (keyword? %) + (name %) + (str %)) + extra-sections))))) + +(defn campaign-url + "Generate the url to a campaign, optionally with + extra path sections after it" + [id & extra-sections] + (apply base-sheet-url "campaigns" id extra-sections)) + +(defn campaign-invite-url + "Generate the url to join a campaign" + ([campaign-id invited-sheet-url] + (campaign-invite-url campaign-id invited-sheet-url nil)) + ([campaign-id invited-sheet-url campaign-name] + (str + config/full-url-root + (prefix + (base-sheet-url "join-campaign" campaign-id + "n" (js/encodeURIComponent campaign-name) + "as" + (namespace invited-sheet-url) invited-sheet-url))))) + (defn sheet-url "Generate the url to a sheet, optionally with extra path sections after it" [id & extra-sections] - (apply str "/sheets/" (namespace id) - "/" (name id) - (when extra-sections - (interleave (repeat "/") - (map - #(if (keyword? %) - (name %) - (str %)) - extra-sections))))) + (apply base-sheet-url "sheets" id extra-sections)) ; ======= support back button to close overlays =========== @@ -129,4 +157,3 @@ ; stop listening to onpopstate (set! js/window.onpopstate nil) (js/history.go -1)))) - diff --git a/src/cljs/wish/views.cljs b/src/cljs/wish/views.cljs index fb2f98eb..2d6a28b2 100644 --- a/src/cljs/wish/views.cljs +++ b/src/cljs/wish/views.cljs @@ -7,18 +7,26 @@ [wish.subs :as subs] [wish.util :refer [evt]] [wish.views.error-boundary :refer [error-boundary]] + [wish.views.campaign-browser :as campaign-browser] + [wish.views.campaign.join :as join-campaign] + [wish.views.error-boundary :refer [error-boundary]] [wish.views.home :refer [home]] + [wish.views.new-campaign :as new-campaign] [wish.views.new-sheet :refer [new-sheet-page]] [wish.views.router :refer [router]] [wish.views.sheet-browser :as sheet-browser] [wish.views.splash :as splash] - [wish.views.update-notifier :refer [update-notifier]] + [wish.views.notifiers :refer [notifiers]] [wish.views.widgets :refer [link] :refer-macros [icon]] [wish.views.widgets.media-tracker :refer [media-tracker]] )) (def pages - {:home #'home + {:campaign #'sheets/campaign + :campaign-browser #'campaign-browser/page + :home #'home + :join-campaign #'join-campaign/page + :new-campaign #'new-campaign/page :new-sheet #'new-sheet-page :sheet #'sheets/viewer :sheet-browser #'sheet-browser/page @@ -61,4 +69,4 @@ [overlay] - [update-notifier]]) + [notifiers]]) diff --git a/src/cljs/wish/views/campaign/base.cljs b/src/cljs/wish/views/campaign/base.cljs new file mode 100644 index 00000000..052cadf4 --- /dev/null +++ b/src/cljs/wish/views/campaign/base.cljs @@ -0,0 +1,24 @@ +(ns ^{:author "Daniel Leong" + :doc "base"} + wish.views.campaign.base + (:require [wish.views.campaign.chars-carousel :refer [chars-carousel]] + [wish.views.error-boundary :refer [error-boundary]])) + +(defn campaign-page + [section & {:keys [char-card]}] + [error-boundary + [:div.campaign + [chars-carousel char-card] + + [:div.info + "This is the campaign page." + + [:div.group + [:a {:href "https://github.com/dhleong/wish/issues/69" + :target '_blank} + "More will be coming here"] + "... eventually."] + + [:div.group + "In the meantime, use the + button above to add characters to this campaign."] + ]]]) diff --git a/src/cljs/wish/views/campaign/chars_carousel.cljs b/src/cljs/wish/views/campaign/chars_carousel.cljs new file mode 100644 index 00000000..66aa4fb8 --- /dev/null +++ b/src/cljs/wish/views/campaign/chars_carousel.cljs @@ -0,0 +1,108 @@ +(ns ^{:author "Daniel Leong" + :doc "campaign.chars-carousel"} + wish.views.campaign.chars-carousel + (:require [wish.util :refer [evt]] + [wish.util.nav :as nav :refer [sheet-url]] + [wish.views.error-boundary :refer [error-boundary]] + [wish.views.widgets :refer [icon link link>evt]] + [wish.views.campaign.events :as events] + [wish.views.campaign.subs :as subs])) + +(defn- sheet-loader [sheet] + [:div "Loading " (:name sheet) "..."]) + +(defn- sources-loader [sheet] + [:div "Loading " (:name sheet) "..."]) + +(defn- char-sheet-loader + [sheet-id content-fn] + (let [sheet (evt [:load-sheet-source! sheet (:sources sheet)]) + [sources-loader sheet])) + + ; either we don't have the sheet at all, or it's just + ; a stub with no actual data; either way, load it! + (do + (>evt [:load-sheet! sheet-id]) + [sheet-loader sheet])))) + +(defn add-chars-overlay [] + (let [campaign-id (evt [:notify! {:duration :short + :content "Copied to clipboard!"}]) + (catch :default e + (println "Unable to copy to clipboard")))) + :value (nav/campaign-invite-url + campaign-id + (:id c) + campaign-name)}] + + [:div.remove + [link>evt [::events/remove-player (:id c)] + (icon :close)]] + ]) + + (for [c (evt [::events/add-player (:id c)] + "Add to campaign"]] + ])] + ])) + +(defn chars-carousel [chars-card-view] + (if-let [members (seq (evt {:> [:toggle-overlay [#'add-chars-overlay]] + :class "add-button"} + (icon :add)] + + ]] + + [:div.empty-carousel + "No characters in this campaign... yet!"])) diff --git a/src/cljs/wish/views/campaign/events.cljs b/src/cljs/wish/views/campaign/events.cljs new file mode 100644 index 00000000..6a1f45d5 --- /dev/null +++ b/src/cljs/wish/views/campaign/events.cljs @@ -0,0 +1,54 @@ +(ns ^{:author "Daniel Leong" + :doc "Campaign-specific events"} + wish.views.campaign.events + (:require-macros [wish.util.log :as log :refer [log]]) + (:require [clojure.string :as str] + [re-frame.core :refer [dispatch reg-event-db reg-event-fx + path + inject-cofx trim-v]] + [day8.re-frame.tracing :refer-macros [fn-traced defn-traced]] + [vimsical.re-frame.cofx.inject :as inject] + [wish.sheets.util :refer [update-sheet-path]])) + +;; can also be passed nil to leave a campaign +(reg-event-fx + ::join-campaign + [trim-v (inject-cofx ::inject/sub [:active-sheet-id])] + (fn [{:keys [active-sheet-id] :as cofx} [campaign-id ?campaign-name]] + (if campaign-id + (let [info {:id campaign-id + :name ?campaign-name}] + (log/info "Joining " campaign-id) + + (-> cofx + (update-sheet-path [] assoc :campaign info) + + ; raise a notifier + (assoc :dispatch [:notify! {:duration :short + :content (str "Joined " + (or (:name info) + "the campaign") + " successfully!")}]))) + + (do + (log/info "Leaving campaign") + + (-> cofx + (update-sheet-path [] dissoc :campaign) + + ; raise a notifier + (assoc :dispatch [:notify! {:duration :short + :content "Left the campaign successfully."}])))))) + +(reg-event-fx + ::add-player + [trim-v] + (fn-traced [cofx [character-id]] + (update-sheet-path cofx [:players] conj character-id))) + +(reg-event-fx + ::remove-player + [trim-v] + (fn-traced [cofx [character-id]] + (update-sheet-path cofx [:players] disj character-id))) + diff --git a/src/cljs/wish/views/campaign/hp_bar.cljs b/src/cljs/wish/views/campaign/hp_bar.cljs new file mode 100644 index 00000000..a2f4b127 --- /dev/null +++ b/src/cljs/wish/views/campaign/hp_bar.cljs @@ -0,0 +1,56 @@ +(ns ^{:author "Daniel Leong" + :doc "hp-bar"} + wish.views.campaign.hp-bar + (:require [garden.color :as color])) + +(def ^:private health-colors + ["#cc4433" + "#cc6633" + "#aaaa33" + "#00cc33"]) + +(defn- css% [fraction] + (str (int (* 100 fraction)) "%")) + +; NOTE: garden's weighted-mix fn is borked, so let's +; make the real deal: +(defn weighted-mix + "Returns a hex color that is `weight` % from + color1 to color2; weight must be a decimal + in the range `[0, 1]`. + A weight of 0 means color1 is returned, while + a weight of 1.0 means color2 is returned." + [color1 color2 weight] + (letfn [(mix [a b] + (int (+ (* a (- 1.0 weight)) + (* b weight))))] + (let [rgb1 (color/hex->rgb color1) + rgb2 (color/hex->rgb color2)] + (->> [:red :green :blue] + (reduce + (fn [m channel] + (assoc m channel (mix (channel rgb1) + (channel rgb2)))) + {}) + (color/rgb->hex))))) + +(defn- perc->color [fraction] + (let [fractional-index (* (dec (count health-colors)) + fraction) + int-val (int fractional-index) + weight (- fractional-index int-val)] + (if (<= weight 0.0001) + (nth health-colors int-val) + + (let [start (nth health-colors int-val) + end (nth health-colors (inc int-val))] + (weighted-mix start end weight))))) + +(defn hp-bar [hp max-hp] + (let [perc (/ hp max-hp)] + [:div.hp-bar + [:div.bar {:style {:background-color (perc->color perc) + :width (css% perc)}}] + [:div.label + hp " / " max-hp] + ])) diff --git a/src/cljs/wish/views/campaign/join.cljs b/src/cljs/wish/views/campaign/join.cljs new file mode 100644 index 00000000..7aae5cb0 --- /dev/null +++ b/src/cljs/wish/views/campaign/join.cljs @@ -0,0 +1,29 @@ +(ns ^{:author "Daniel Leong" + :doc "Join a campaign as a player"} + wish.views.campaign.join + (:require [wish.util :refer [evt click>evts]] + [wish.util.nav :refer [sheet-url]] + [wish.views.campaign.events :as events])) + +(defn page [[campaign-id sheet-id ?campaign-name]] + [:div + [:h3 "You've been invited to " (or ?campaign-name + "a campaign")] + + [:div.explanation + "This does not automatically grant the DM permission to modify your sheet, + nor does it grant them permission to read it. You will need to use the + sharing settings at the top of your sheet to grant whatever permissions + you feel necessary. They must be able to at least read your sheet for + there to be any benefit in joining the campaign, however!"] + + [:div + [:h4 "Would you like to join?"] + + [:div.button {:on-click (click>evts + [::events/join-campaign campaign-id ?campaign-name] + [:nav/replace! (sheet-url sheet-id)])} + "Yes! Join " (or ?campaign-name + "the campaign")] + ] + ]) diff --git a/src/cljs/wish/views/campaign/subs.cljs b/src/cljs/wish/views/campaign/subs.cljs new file mode 100644 index 00000000..63ed452f --- /dev/null +++ b/src/cljs/wish/views/campaign/subs.cljs @@ -0,0 +1,28 @@ +(ns ^{:author "Daniel Leong" + :doc "Campaign-specific subs"} + wish.views.campaign.subs + (:require [clojure.string :as str] + [re-frame.core :refer [reg-sub subscribe]])) + +(reg-sub + ::add-char-candidates + :<- [:meta/players] + :<- [:known-sheets] + (fn [[current-members sheets]] + (let [by-mine (->> sheets + (remove (comp current-members :id)) + (group-by :mine?))] + (concat (get by-mine false) + (get by-mine true))))) + +(reg-sub + ::campaign-members + :<- [:meta/players] + :<- [:sheets] + (fn [[char-sheet-ids sheets] qv] + (->> char-sheet-ids + (map (fn [id] + (or (assoc (get sheets id) + :id id) + {:id id}))) + (sort-by :name)))) diff --git a/src/cljs/wish/views/campaign_browser.cljs b/src/cljs/wish/views/campaign_browser.cljs new file mode 100644 index 00000000..88f660a0 --- /dev/null +++ b/src/cljs/wish/views/campaign_browser.cljs @@ -0,0 +1,29 @@ +(ns ^{:author "Daniel Leong" + :doc "campaign-browser"} + wish.views.campaign-browser + (:require [reagent.core :as r] + [reagent-forms.core :refer [bind-fields]] + [wish.util :refer [>evt + [:div + [:p "Pick a name (you can change it later)"] + [:input {:field :text + :id :name}]] + + [:div + [:p "Pick a sheet type (you " [:i "can't"] " can't change this one!)"] + (into + [:select {:field :list + :id :sheet} + [:option {:key :-none} "—Pick a sheet type—"]] + + (for [[sheet-id info] sheets/sheets] + ^{:key sheet-id} + [:option {:key sheet-id} + (:name info)]))]] + + form-data] + + [:div + [:p "Where do you want to store it?"] + + (for [[provider-id state] (evt evt]])) + +(defn notifier + [& {:keys [ignore-event + content + action-label + action-event]}] + [:div.notifier + (when ignore-event + [:div.ignore + [link>evt {:class "link" + :> ignore-event} + (icon :close)]]) + + [:div.content content] + + (when action-event + [:div.action + [link>evt action-event + action-label]])]) + +(defn update-notifier [] + (when (evt click>evt]] [wish.util.nav :refer [sheet-url]] [wish.providers :as providers] - [wish.views.widgets :refer [link save-state] :refer-macros [icon]])) + [wish.views.widgets :refer [link link>evt save-state] :refer-macros [icon]])) (defn- find-section [candidates target-id] @@ -88,6 +88,40 @@ [link {:href (sheet-url sheet-id)} "Let's play!"])]]])) +(defn campaign-manager [] + (when-let [campaign-info ( + [:h3 "Campaign"] + + [:div.group + ( + [link>evt [:share-sheet! sheet-id] + "Click here"] + " to do this now."]) ] + + [:div.group + [:input {:type 'button + :on-click (click>evt [:join-campaign]) + :value "I understand; leave the campaign"}]]] + + [:div.group + [link>evt {:on-click (fn-click + (swap! wants-to-leave? not))} + "Click here if you want to leave this campaign."] + ])]))) + (defn data-source-manager [] (let [original-ids (evt [:query-data-sources]) (let [sheet-id ( [:h3 "Data Sources"] [:div (if-let [sources (evt evt]])) - -(defn update-notifier [] - (when (evt {:class "link" - :> [:ignore-latest-update]} - (icon :close)]] - [:div.content "New version of WISH available!"] - [:div.update - [link>evt [:update-app] - "Update"]]])) diff --git a/src/cljs/wish/views/widgets.cljs b/src/cljs/wish/views/widgets.cljs index 00823911..0b286691 100644 --- a/src/cljs/wish/views/widgets.cljs +++ b/src/cljs/wish/views/widgets.cljs @@ -54,7 +54,7 @@ (vector? evt-or-opts) (assoc base :on-click (click>evt evt-or-opts)) ; map with :on-click, the easy case - (:on-click evt-or-opts) evt-or-opts + (:on-click evt-or-opts) (merge base evt-or-opts) ; fancy case; merge in all the provided opts, in case they ; provided, eg :class