diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..4a4726a
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use_nix
diff --git a/.gitignore b/.gitignore
index b8549d4..295a9c8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,14 +1,20 @@
+node_modules/
+public/js
+
+/target
+/checkouts
+/src/gen
+
pom.xml
-*jar
-/lib/
-/classes/
-/out/
-/target/
-.lein-deps-sum
-.lein-repl-history
-.lein-plugins/
-.repl
-.nrepl-port
-.cpcache/
-.rebel_readline_history
-resources/public/cljs-out/
+pom.xml.asc
+*.iml
+*.jar
+*.log
+.shadow-cljs
+.idea
+.lein-*
+.nrepl-*
+.DS_Store
+
+.hgignore
+.hg/
diff --git a/LICENSE.md b/LICENSE.md
index 5bf5beb..b9f1610 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2020 Rameez Khan
+Copyright (c) 2021 Rameez Khan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Makefile b/Makefile
index ef45b3a..9c4d722 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,10 @@
-.PHONY: build test
+.PHONY: build
-build:
- lein fig:build
+dev:
+ yarn dev
-production:
- lein fig:min
+release:
+ yarn release
+
+nrepl:
+ clj -M:nrepl
diff --git a/README.md b/README.md
index f750502..248a214 100644
--- a/README.md
+++ b/README.md
@@ -10,43 +10,11 @@ Live version can be accessed [here](https://financialhealth.app) 👈🏽.
Assess your financial health with a high level dashboard.
-### Features
-- [x] Chart to view your salary over time
-- [ ] View your current net worth
-
-### Future work
-- [ ] View your net worth over time
-- [ ] Track TFSA contributions
-
-## Development
-
-To get an interactive development environment run:
-
- lein fig:build
-
-This will auto compile and send all changes to the browser without the
-need to reload. After the compilation process is complete, you will
-get a Browser Connected REPL. An easy way to try it is:
-
- (js/alert "Am I connected?")
-
-and you should see an alert in the browser window.
-
-To clean all compiled files:
-
- lein clean
-
-To create a production build run:
-
- lein clean
- lein fig:min
-
-
## License
MIT License
-Copyright (c) 2020 Rameez Khan
+Copyright (c) 2021 Rameez Khan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/dev.cljs.edn b/dev.cljs.edn
deleted file mode 100644
index 17f64af..0000000
--- a/dev.cljs.edn
+++ /dev/null
@@ -1,4 +0,0 @@
-^{:watch-dirs ["test" "src"]
- :css-dirs ["resources/public/css"]
- :auto-testing true}
-{:main financial-health-dashboard.core}
diff --git a/example-data.csv b/example-data.csv
deleted file mode 100644
index 120ada1..0000000
--- a/example-data.csv
+++ /dev/null
@@ -1,93 +0,0 @@
-fi-expense|15000
-emergency-monthly-expense|2020|5|25000
-
-comment|July 2020
-
-income|salary|2020|7|55000
-expense|day-to-day|2020|7|7000
-expense|recurring|2020|7|12000
-expense|home-loan-payment|2020|7|3000
-
-asset|home|2020|7|1000000
-liability|home-loan|2020|7|700000
-
-asset|car|2020|7|98887
-liability|car-loan|2020|7|20000
-
-asset|cheque-account|2020|7|6636
-asset|credit-card|2020|7|3198
-liability|credit-card|2020|7|0
-asset|tyme-goalsave|2020|7|100000
-
-asset|ra-1|2020|7|120000
-asset|preservation-1|2020|7|170000
-asset|ra-2|2020|7|1500
-asset|tfsa|2020|7|45000
-asset|education-fund|2020|7|5000
-
-emergency-fund|2020|7|140000
-
-comment|Aug 2020
-
-income|salary|2020|8|55000
-expense|day-to-day|2020|8|8000
-expense|recurring|2020|8|12000
-expense|home-loan-payment|2020|8|3000
-
-asset|home|2020|8|1000000
-liability|home-loan|2020|8|790000
-
-asset|car|2020|8|98887
-liability|car-loan|2020|8|19500
-
-asset|cheque-account|2020|8|6636
-asset|credit-card|2020|8|3198
-liability|credit-card|2020|8|0
-asset|tyme-goalsave|2020|8|100000
-
-asset|ra-1|2020|8|125000
-asset|preservation-1|2020|8|17000
-asset|ra-2|2020|8|1600
-asset|tfsa|2020|8|80000
-asset|education-fund|2020|8|5000
-
-emergency-fund|2020|8|150000
-
-comment|Sep 2020
-
-income|salary|2020|9|50000
-expense|day-to-day|2020|9|8500
-expense|recurring|2020|9|11000
-expense|home-loan-payment|2020|9|3000
-
-asset|home|2020|9|1000000
-liability|home-loan|2020|9|780000
-
-asset|car|2020|9|98887
-liability|car-loan|2020|9|19000
-
-asset|cheque-account|2020|9|6636
-asset|credit-card|2020|9|3198
-liability|credit-card|2020|9|0
-asset|tyme-goalsave|2020|9|100000
-
-asset|ra-1|2020|9|125000
-asset|preservation-1|2020|9|210000
-asset|ra-2|2020|9|1800
-asset|tfsa|2020|9|90000
-asset|education-fund|2020|9|5000
-
-emergency-fund|2020|9|151000
-
-comment|tfsa
-tfsa-contribution|2019|6|4100
-tfsa-contribution|2019|8|2750
-tfsa-contribution|2019|11|2750
-tfsa-contribution|2020|1|500
-tfsa-contribution|2020|2|500
-tfsa-contribution|2020|3|6000
-tfsa-contribution|2020|4|6000
-tfsa-contribution|2020|5|6000
-tfsa-contribution|2020|6|6000
-tfsa-contribution|2020|7|6000
-tfsa-contribution|2020|8|6000
diff --git a/figwheel-main.edn b/figwheel-main.edn
deleted file mode 100644
index 63dc990..0000000
--- a/figwheel-main.edn
+++ /dev/null
@@ -1,31 +0,0 @@
-;; Figwheel-main configuration options see: https://figwheel.org/config-options
-;; these will be overriden by the metadata config options in dev.cljs.edn build file
-{
- ;; Set the server port https://figwheel.org/config-options#ring-server-options
- ;; :ring-server-options {:port 9500}
-
- ;; Change the target directory from the "target" to "resources"
- ;; https://figwheel.org/config-options#target-dir
- :target-dir "resources"
-
- ;; Server Ring Handler (optional) https://figwheel.org/docs/ring-handler.html
- ;; If you want to embed a ring handler into the figwheel server, this
- ;; is for simple ring servers
- ;; :ring-handler hello_world.server/handler
-
- ;; To be able to open files in your editor from the heads up display
- ;; you will need to put a script on your path. This script will have
- ;; to take a file path and a line number ie.
- ;; in ~/bin/myfile-opener:
- ;;
- ;; #! /bin/sh
- ;; emacsclient -n +$2:$3 $1
- ;;
- ;; :open-file-command "myfile-opener"
-
- ;; if you are using emacsclient you can just use
- ;; :open-file-command "emacsclient"
-
- ;; Logging output gets printed to the REPL, if you want to redirect it to a file:
- ;; :log-file "figwheel-main.log"
-}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6cdb03a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "financial-health-dashboard",
+ "version": "2.0.0",
+ "main": "index.js",
+ "repository": "git@github.com:rameezk/financial-health-dashboard.git",
+ "author": "Rameez Khan",
+ "license": "MIT",
+ "dependencies": {
+ "react": "17.0.1",
+ "react-dom": "17.0.1"
+ },
+ "devDependencies": {
+ "shadow-cljs": "^2.11.18"
+ },
+ "scripts": {
+ "dev": "shadow-cljs watch app",
+ "release": "shadow-cljs release app"
+ }
+}
diff --git a/project.clj b/project.clj
deleted file mode 100644
index 2276811..0000000
--- a/project.clj
+++ /dev/null
@@ -1,25 +0,0 @@
-(defproject financial-health-dashboard "0.1.0-SNAPSHOT"
- :description "FIXME: write this!"
- :url "http://example.com/FIXME"
- :license {:name "Eclipse Public License"
- :url "http://www.eclipse.org/legal/epl-v10.html"}
-
- :min-lein-version "2.7.1"
-
- :dependencies [[org.clojure/clojure "1.9.0"]
- [org.clojure/clojurescript "1.10.520"]
- [testdouble/clojurescript.csv "0.4.5"]
- [com.andrewmcveigh/cljs-time "0.5.2"]
- [reagent "0.8.1"]]
-
- :source-paths ["src"]
-
- :aliases {"fig" ["trampoline" "run" "-m" "figwheel.main"]
- "fig:build" ["trampoline" "run" "-m" "figwheel.main" "-b" "dev" "-r"]
- "fig:min" ["run" "-m" "figwheel.main" "-O" "advanced" "-bo" "dev"]
- "fig:test" ["run" "-m" "figwheel.main" "-co" "test.cljs.edn" "-m" "financial-health-dashboard.test-runner"]}
-
- :profiles {:dev {:dependencies [[com.bhauman/figwheel-main "0.2.3"]
- [com.bhauman/rebel-readline-cljs "0.1.4"]]
- }})
-
diff --git a/resources/public/android-chrome-192x192.png b/public/android-chrome-192x192.png
similarity index 100%
rename from resources/public/android-chrome-192x192.png
rename to public/android-chrome-192x192.png
diff --git a/resources/public/android-chrome-512x512.png b/public/android-chrome-512x512.png
similarity index 100%
rename from resources/public/android-chrome-512x512.png
rename to public/android-chrome-512x512.png
diff --git a/resources/public/apple-touch-icon.png b/public/apple-touch-icon.png
similarity index 100%
rename from resources/public/apple-touch-icon.png
rename to public/apple-touch-icon.png
diff --git a/resources/public/css/style.css b/public/css/style.css
similarity index 100%
rename from resources/public/css/style.css
rename to public/css/style.css
diff --git a/resources/public/favicon-16x16.png b/public/favicon-16x16.png
similarity index 100%
rename from resources/public/favicon-16x16.png
rename to public/favicon-16x16.png
diff --git a/resources/public/favicon-32x32.png b/public/favicon-32x32.png
similarity index 100%
rename from resources/public/favicon-32x32.png
rename to public/favicon-32x32.png
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..47f8495
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Financial Health Dashboard
+
+
+
+
+
+
+
+
+
diff --git a/resources/public/site.webmanifest b/public/site.webmanifest
similarity index 100%
rename from resources/public/site.webmanifest
rename to public/site.webmanifest
diff --git a/resources/public/index.html b/resources/public/index.html
deleted file mode 100644
index 19fa812..0000000
--- a/resources/public/index.html
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Financial Health Dashboard
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/resources/public/test.html b/resources/public/test.html
deleted file mode 100644
index b08b182..0000000
--- a/resources/public/test.html
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
- Test host page
-
-
-
diff --git a/shadow-cljs.edn b/shadow-cljs.edn
new file mode 100644
index 0000000..32e53d0
--- /dev/null
+++ b/shadow-cljs.edn
@@ -0,0 +1,24 @@
+{:source-paths
+ ["src"]
+
+ :nrepl {:port 9000}
+
+ :dependencies
+ [[cider/cider-nrepl "0.25.8"]
+ [reagent "1.0.0"]
+ [re-frame "1.1.2"]]
+
+ :builds
+ {:app {:target :browser
+ :output-dir "public/js"
+ :asset-path "/js"
+
+ :modules
+ {:main
+ {:entries [financial_health_dashboard.core]}}
+
+ :devtools
+ {:http-root "public"
+ :http-port 3001}
+
+ :release {:compiler-options {:optimizations :simple}}}}}
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..2c78ccd
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,11 @@
+let pkgs = import { };
+in pkgs.mkShell rec {
+ name = "financial-health-dashboard";
+
+ buildInputs = with pkgs; [
+ nodejs-14_x
+ (yarn.override { nodejs = nodejs-14_x; })
+ clojure
+ jdk
+ ];
+}
diff --git a/src/financial_health_dashboard/changelog.cljs b/src/financial_health_dashboard/changelog.cljs
deleted file mode 100644
index 5c0717e..0000000
--- a/src/financial_health_dashboard/changelog.cljs
+++ /dev/null
@@ -1,7 +0,0 @@
-(ns financial-health-dashboard.changelog)
-
-(defn render []
- [:div
- [:ul
- [:li "thing 2"]
- [:li "thing 1"]]])
diff --git a/src/financial_health_dashboard/components.cljs b/src/financial_health_dashboard/components.cljs
new file mode 100644
index 0000000..159acc6
--- /dev/null
+++ b/src/financial_health_dashboard/components.cljs
@@ -0,0 +1,8 @@
+(ns financial-health-dashboard.components)
+
+(defn nav []
+ [:nav.bg-purple-700.text-white.flex.items-center.justify-between.flex-wrap.p-6
+ [:div.flex.items-center.flex-shrink-0.mr-6
+ [:span "💰 Financial Health Dashboard"]]
+ [:div
+ [:a.inline-block.text-sm.px-4.py-2.leading-none.border.rounded.border-white.hover:border-transparent.hover:text-teal-500.hover:bg-purple-300.mt-4.lg:mt-0 {:href "https://www.google.com"} "Upload Data File"]]])
diff --git a/src/financial_health_dashboard/core.cljs b/src/financial_health_dashboard/core.cljs
index ee3e69c..a357d2e 100644
--- a/src/financial_health_dashboard/core.cljs
+++ b/src/financial_health_dashboard/core.cljs
@@ -1,709 +1,25 @@
-(ns ^:figwheel-hooks financial-health-dashboard.core
- (:require
- [financial-health-dashboard.changelog :as changelog]
- [financial-health-dashboard.parse :as parse]
- [financial-health-dashboard.domain :as domain]
- [financial-health-dashboard.localstorage :as localstorage]
- [financial-health-dashboard.example :as example]
- [clojure.edn :as edn]
- [cljs-time.format :as tf]
- [goog.dom :as gdom]
- [goog.dom.classlist :as gc]
- [reagent.core :as reagent :refer [atom]]))
-
-(defn multiply [a b] (* a b))
-
-(def number-formatter (js/Intl.NumberFormat.))
-
-(defn format-number [number] (.format number-formatter number))
-
-;; define your app data so that it doesn't get over-written on reload
-(defonce state (reagent/atom {:page :loading
- :delimiter parse/pipe
- :modal {:key :hidden :data nil}
- :data nil}))
-
-(defmulti render-page :page)
-
-(defn page [page] (swap! state #(assoc % :page page)))
-
-(defmulti render-modal (fn [state] (get-in @state [:modal :key])))
-
-(defn show-modal [key data]
- (swap! state #(assoc % :modal {:key key :data data})))
-
-(defn hide-modal []
- (swap! state #(assoc % :modal {:key :hidden})))
-
-(defn toggle-burger-menu []
- (gc/toggle (js/document.getElementById "nav-menu") "is-active")
- (gc/toggle (js/document.getElementById "nav-menu-burger") "is-active"))
-
-
-(defn set-file-data [data]
- (swap! state #(assoc-in % [:modal :data] data)))
-
-(defn set-app-data [data]
- (swap! state #(assoc % :data data)))
-
-(defn save-data-to-localstorage [data]
- (localstorage/set-item! "data" (prn-str data)))
-
-(defn build-app-data-from-uploaded-data [parsed-data]
- (save-data-to-localstorage parsed-data)
- (set-app-data (-> parsed-data parse/as-domain-values domain/all-your-bucks)))
-
-(defn build-app-data-from-localstorage-data [localstorage-data]
- (when-not (nil? localstorage-data)
- (set-app-data (-> localstorage-data
- parse/as-domain-values
- domain/all-your-bucks))))
-
-(defn get-data-from-localstorage []
- (or (->> (localstorage/get-item "data") (edn/read-string)) nil))
-
-(defn get-sample-data []
- (parse/parse parse/pipe example/data-piped))
-
-(defmethod render-modal :upload [{:keys [modal delimiter]}]
- [:div.has-text-dark
- [:h1.heading.has-text-centered "Choose file"]
- [:form
- [:div.file.is-centered
- [:label.file-label
- [:input.file-input {:type "file" :name "storage"
- :on-change (fn [e]
- (let [file (aget (.. e -target -files) 0)
- reader (js/FileReader.)]
- (set! (.-onload reader)
- #(set-file-data (.. % -target -result)))
- (.readAsText reader file)))}]
- [:span.file-cta
- [:span.file-icon
- [:i.fa.fa-upload]]
- [:span.file-label "Upload"]]]]]
- (when-let [content (get-in @state [:modal :data])]
- (let [result
- (->> content
- (parse/parse parse/pipe))
- errors
- (->> result
- (filter (comp not :valid?))
- (map (juxt :i :error)))]
- (if-not (empty? errors)
- [:div.content
- [:hr]
- [:p.heading.has-text-centered.has-text-danger "Oops. You have some errors"]
- [:ul
- (map-indexed
- (fn [i [row e]]
- [:li {:key i} "row: " row ": " e])
- errors)]]
- [:div.content
- [:hr]
- [:p.heading.has-text-centered.has-text-primary "Awesome! No errors!"]
- [:div.buttons.is-centered
- [:button.button.is-primary
- {:on-click (fn [_] (build-app-data-from-uploaded-data result)
- (hide-modal)
- (toggle-burger-menu)
- (page :loading)
- (js/setTimeout #(page :main)))}
- "GO"]]])))])
-
-(defmethod render-modal :help [{:keys [modal delimiter]}]
- (println "help modal"))
-
-(defmethod render-modal :save [{:keys [modal delimiter]}]
- (println "save modal"))
-
-(defmethod render-modal :changelog []
- (changelog/render))
-
-(defn bar-chart
- [id]
- (let [context (.getContext (.getElementById js/document id) "2d")
- chart-data {:type "bar"
- :options {:legend {:labels {:fontColor "white"}}
- :scales {:xAxes [{:ticks {:fontColor "white"}}]
- :yAxes [{:ticks {:fontColor "white"}}]}}
- :data {:labels ["2012" "2013" "2014" "2015" "2016"]
- :datasets [{:data [5 10 15 20 25]
- :label "Rev in MM"
- :backgroundColor "#90EE90"}
- {:data [3 6 9 12 15]
- :label "Cost in MM"
- :backgroundColor "#F08080"}]}}]
- (js/Chart. context (clj->js chart-data))))
-
-(defn cash-flow-chart
- [id labels incomes expenses]
- (let [context (.getContext (.getElementById js/document id) "2d")
- chart-data {:type "line"
- :options {:legend {:labels {:fontColor "white"}}
- :scales {:xAxes [{:ticks {:fontColor "white" :maxTicksLimit 12}}]
- :yAxes [{:ticks {:fontColor "white"}}]}}
- :data {:labels labels
- :datasets [{:data incomes
- :label "Income"
- :lineTension 0
- :fill false
- :borderColor "#90EE90"
- :cubicInterpolationMode "linear"
- :backgroundColor "#90EE90"}
- {:data expenses
- :label "Expense"
- :lineTension 0
- :fill false
- :borderColor "#EA3C53"
- :cubicInterpolationMode "linear"
- :backgroundColor "#EA3C53"}]}}]
-
- (js/Chart. context (clj->js chart-data))))
-
-(defn savings-rate-chart
- [id labels savings-rate]
- (let [context (.getContext (.getElementById js/document id) "2d")
- chart-data {:type "line"
- :options {:legend {:labels {:fontColor "white"}}
- :scales {:xAxes [{:ticks {:fontColor "white" :maxTicksLimit 12}}]
- :yAxes [{:ticks {:fontColor "white"}}]}}
- :data {:labels labels
- :datasets [{:data savings-rate
- :label "Savings Rate"
- :lineTension 0
- :fill false
- :borderColor "#8e44ad"
- :cubicInterpolationMode "linear"
- :backgroundColor "#8e44ad"}]}}]
-
- (js/Chart. context (clj->js chart-data))))
-
-(defn net-worth-line-chart
- [id labels net-worth]
- (let [context (.getContext (.getElementById js/document id) "2d")
- chart-data {:type "line"
- :options {:legend {:labels {:fontColor "white"}}
- :scales {:xAxes [{:ticks {:fontColor "white" :maxTicksLimit 12}}]
- :yAxes [{:ticks {:fontColor "white"}}]}}
- :data {:labels labels
- :datasets [{:data net-worth
- :label "Net Worth"
- :lineTension 0
- :fill false
- :borderColor "#90EE90"
- :cubicInterpolationMode "linear"
- :backgroundColor "#90EE90"}]}}]
-
- (js/Chart. context (clj->js chart-data))))
-
-(defn assets-line-chart
- [id labels assets]
- (let [context (.getContext (.getElementById js/document id) "2d")
- chart-data {:type "line"
- :options {:legend {:labels {:fontColor "white"}}
- :scales {:xAxes [{:ticks {:fontColor "white" :maxTicksLimit 12}}]
- :yAxes [{:ticks {:fontColor "white"}}]}}
- :data {:labels labels
- :datasets [{:data assets
- :label "Assets"
- :lineTension 0
- :fill false
- :borderColor "#EA3C53"
- :cubicInterpolationMode "linear"
- :backgroundColor "#EA3C53"}]}}]
-
- (js/Chart. context (clj->js chart-data))))
-
-(defn liabilities-line-chart
- [id labels liabilities]
- (let [context (.getContext (.getElementById js/document id) "2d")
- chart-data {:type "line"
- :options {:legend {:labels {:fontColor "white"}}
- :scales {:xAxes [{:ticks {:fontColor "white" :maxTicksLimit 12}}]
- :yAxes [{:ticks {:fontColor "white"}}]}}
- :data {:labels labels
- :datasets [{:data liabilities
- :label "Liabilities"
- :lineTension 0
- :fill false
- :borderColor "#e67e22"
- :cubicInterpolationMode "linear"
- :backgroundColor "#e67e22"}]}}]
-
- (js/Chart. context (clj->js chart-data))))
-
-(defn tfsa-yearly-chart
- [id labels contributions limits]
- (let [context (.getContext (.getElementById js/document id) "2d")
- chart-data {:type "horizontalBar"
- :options {:legend {:labels {:fontColor "white"}}
- :scales {:xAxes [{:ticks {:fontColor "white" :beginAtZero true}}]
- :yAxes [{:ticks {:fontColor "white"}}]}}
- :data {:labels labels
- :datasets [{:data limits
- :label "Limit"
- :backgroundColor "#EA3C53"}
- {:data contributions
- :label "Contribution"
- :backgroundColor "#90EE90"}]}}]
- (js/Chart. context (clj->js chart-data))))
-
-(defn tfsa-lifetime-chart
- [id contribution limit]
- (let [context (.getContext (.getElementById js/document id) "2d")
- chart-data {:type "horizontalBar"
- :options {:legend {:labels {:fontColor "white"}}
- :scales {:xAxes [{:ticks {:fontColor "white" :beginAtZero true}}]
- :yAxes [{:ticks {:fontColor "white"}}]}}
- :data {:labels ["Lifetime"]
- :datasets [{:data limit
- :label "Limit"
- :backgroundColor "#EA3C53"}
- {:data contribution
- :label "Contribution"
- :backgroundColor "#90EE90"}]}}]
- (js/Chart. context (clj->js chart-data))))
-
-(defn pie-chart-1
- [id]
- (let [context (.getContext (.getElementById js/document id) "2d")
- chart-data {:type "pie"
- :options {:legend {:labels {:fontColor "white"}}
- :scales {}}
- :data {:labels ["Retirement Annuity"
- "Preservation Fund"
- "Emergency Fund"
- "TFSA"
- "Discretionary Investments"]
- :datasets [{:data [20 20 20 20 20]
- :backgroundColor ["#2ecc71"
- "#3498db"
- "#e67e22"
- "#9b59b6"
- "#1abc9c"]}]}}]
- (js/Chart. context (clj->js chart-data))))
-
-(defn pie-chart-2
- [id]
- (let [context (.getContext (.getElementById js/document id) "2d")
- chart-data {:type "pie"
- :options {:legend {:labels {:fontColor "white"}}
- :scales {}}
- :data {:labels ["Local"
- "Offshore"]
- :datasets [{:data [90 10]
- :backgroundColor ["#2ecc71"
- "#3498db"]}]}}]
- (js/Chart. context (clj->js chart-data))))
-
-(defn pie-chart-3
- [id]
- (let [context (.getContext (.getElementById js/document id) "2d")
- chart-data {:type "pie"
- :options {:legend {:labels {:fontColor "white"}}
- :scales {}}
- :data {:labels ["Property"
- "Bonds"
- "Equity"
- "Cash"
- "Vehicle"
- "Offshore"]
- :datasets [{:data [2 20 29 35 8 6]
- :backgroundColor ["#2ecc71"
- "#3498db"
- "#e67e22"
- "#9b59b6"
- "#f1c40f"
- "#c0392b"]}]}}]
- (js/Chart. context (clj->js chart-data))))
-
-(defn draw-chart [id chart x y1 y2 y3]
- (reagent/create-class
- {:component-did-mount #(chart id x y1 y2 y3)
- :display-name "chart"
- :reagent-render (fn [] [:canvas {:id id}])}))
-
-(defn nav []
- [:div
- [:nav.navbar.is-dark
- [:div.navbar-brand
- [:a.navbar-item {:href "#"} "💰 Financial Health Dashboard"]
- [:a.navbar-burger.burger {:id "nav-menu-burger"
- :on-click (fn []
- (toggle-burger-menu))}
- [:span {:aria-hidden "true"}]
- [:span {:aria-hidden "true"}]
- [:span {:aria-hidden "true"}]]]
- [:div.navbar-menu {:id "nav-menu"}
- [:div.navbar-end
- ;; [:a.navbar-item {:on-click #(show-modal :help nil)}
- ;; [:span.icon [:i.fa.fa-question-circle]]]
- [:a.navbar-item {:on-click #(show-modal :upload nil)}
- [:span.icon [:i.fa.fa-upload]]
- [:span.is-hidden-desktop "Upload Data"]]
- ;; [:a.navbar-item {:on-click #(show-modal :save nil)}
- ;; [:span.icon [:i.fa.fa-save]]]
- ;; [:a.navbar-item {:on-click #(show-modal :changelog nil)}
- ;; [:span.icon [:i.fa.fa-history]]]
- ]]]])
-
-(defn modal [model-state]
- [:div.modal.is-active
- [:div.modal-background {:on-click hide-modal}]
- [:div.modal-content
- [:div.box.has-background-light
- (render-modal model-state)]
- [:button.modal-close.is-large {:on-click hide-modal}]]])
-
-(defn col-real-data [size-desktop size-mobile & children]
- [:div.column {:class (str "is-" size-desktop "-desktop " "is-" size-mobile "-mobile " "is-" size-mobile "-tablet")}
- [:div.box.is-shadowless.has-text-grey-lighter
- children]])
-
-(defn col-sample-data [size-desktop size-mobile & children]
- [:div.column {:class (str "is-" size-desktop "-desktop " "is-" size-mobile "-mobile " "is-" size-mobile "-tablet")}
- [:div.box.is-shadowless.has-text-grey-lighter
- [:span.tag.is-warning.is-size-7.sample-tag "sample"]
- children]])
-
-(defn info-box [title info change & [class]]
- [:div.has-text-centered.info-box
- [:p.heading title]
- [:p.title {:class (or class "has-text-light")} info]
- (if (> change 0)
- [:p.subtitle.is-size-7.has-text-light.has-text-success
- [:i.fa.fa-arrow-up] (str " " change)]
- [:p.subtitle.is-size-7.has-text-light.has-text-danger
- [:i.fa.fa-arrow-down] (str " " change)])])
-
-(defn chart-box [title content & [class]]
- [:div.has-text-centered.info-box
- [:p.heading title]
- [:div.is-centered
- [content]]])
-
-(def custom-month-year
- (tf/formatter "MMM-yy"))
-
-(defn cash-flow-over-time-chart [{:keys [income expenses]}]
- (let [labels (->> income (map :cljs-date) (map #(tf/unparse custom-month-year %)) (take-last 13))
- income (->> income (map :amount) (take-last 13))
- expenses (->> expenses (map :amount) (take-last 13))]
- (chart-box "CASH FLOW OVER TIME" (draw-chart
- "cash-flow-over-time"
- cash-flow-chart labels income expenses nil))))
-
-(defn savings-rate-over-time-chart [{:keys [income expenses]}]
- (let [labels (->> income (map :cljs-date) (map #(tf/unparse custom-month-year %)) (take-last 13))
- income (->> income (map :amount) (take-last 13))
- expenses (->> expenses (map :amount) (take-last 13))
- savings (map - income expenses)
- savings-rate (map #(* % 100) (map / savings income))]
- (chart-box "SAVINGS RATE OVER TIME" (draw-chart
- "savings-rate-over-time"
- savings-rate-chart labels savings-rate nil nil))))
-
-(defn calculate-growth [amount rate]
- (* amount (+ 1 rate)))
-
-(defn monthly-rate-from-annual [annual-rate]
- (- (Math/pow (+ 1 annual-rate) (/ 1 12)) 1))
-
-(defn is-new-year? [month]
- (zero? (mod month 12)))
-
-(defn apply-inflation [amount inflation-rate years]
- (/ amount (Math/pow (+ 1 inflation-rate) years)))
-
-
-(defn calculate-investment-value [{:keys [monthly-amount
- annual-growth-rate
- annual-monthly-amount-escalation
- annual-inflation
- years-invested]}]
- (apply-inflation
- (let [monthly-growth (monthly-rate-from-annual annual-growth-rate)
- months (* years-invested 12)]
- (loop [month 1
- monthly-amount monthly-amount
- total (calculate-growth monthly-amount monthly-growth)]
- (if (>= month months)
- total
- (recur (inc month)
- (if (is-new-year? (inc month))
- (calculate-growth monthly-amount annual-monthly-amount-escalation)
- monthly-amount)
- (calculate-growth (+ total monthly-amount) monthly-growth)))))
- annual-inflation
- years-invested))
-
-(defn investment-benchmark-chart
- [id labels investment investment2 investment3]
- (let [context (.getContext (.getElementById js/document id) "2d")
- chart-data {:type "line"
- :options {:legend {:labels {:fontColor "white"}}
- :scales {:xAxes [{:scaleLabel {:display true :labelString "years" :fontColor "white"} :ticks {:fontColor "white" :maxTicksLimit 12}}]
- :yAxes [{:ticks {:callback (fn [value, index, values] (format-number value)) :fontColor "white"}}]}
- :tooltips {:callbacks {:label (fn [tooltip-item data] (format-number (aget tooltip-item "yLabel")))}}}
- :data {:labels labels
- :datasets [{:data investment
- :label "Current Investment"
- :lineTension 0
- :fill false
- :borderColor "#8e44ad"
- :cubicInterpolationMode "linear"
- :backgroundColor "#8e44ad"}
- {:data investment2
- :label "Invest 25,000 Monthly"
- :lineTension 0
- :fill false
- :borderColor "#e67e22"
- :cubicInterpolationMode "linear"
- :backgroundColor "#e67e22"}
- {:data investment3
- :label "Invest 10,000 Monthly"
- :lineTension 0
- :fill false
- :borderColor "#2ecc71"
- :cubicInterpolationMode "linear"
- :backgroundColor "#2ecc71"}]}}]
-
- (js/Chart. context (clj->js chart-data))))
-
-(defn investment-benchmark-over-time-chart []
- (let [labels (range 36)
- investment (->> labels (map #(calculate-investment-value {:monthly-amount 6000
- :annual-growth-rate 0.15
- :annual-monthly-amount-escalation 0.06
- :annual-inflation 0.06
- :years-invested %})))
- invest-2 (->> labels (map #(calculate-investment-value {:monthly-amount 25000
- :annual-growth-rate 0.15
- :annual-monthly-amount-escalation 0.06
- :annual-inflation 0.06
- :years-invested %})))
- invest-3 (->> labels (map #(calculate-investment-value {:monthly-amount 10000
- :annual-growth-rate 0.15
- :annual-monthly-amount-escalation 0.06
- :annual-inflation 0.06
- :years-invested %})))]
- (chart-box "INVESTMENT BENCHMARK" (draw-chart
- "investment-benchmark-chart"
- investment-benchmark-chart labels investment invest-2 invest-3))))
-
-(defn net-worth-over-time-chart [{:keys [net-worths]}]
- (let [labels (->> net-worths (map :cljs-date) (map #(tf/unparse custom-month-year %)))
- net-worth (->> net-worths (map :amount))]
- (chart-box "NET WORTH OVER TIME" (draw-chart
- "net-worth-over-time"
- net-worth-line-chart labels net-worth nil nil))))
-
-(defn assets-over-time-chart [{:keys [net-assets]}]
- (let [labels (->> net-assets (map :cljs-date) (map #(tf/unparse custom-month-year %)))
- assets (->> net-assets (map :amount))]
- (chart-box "ASSETS OVER TIME" (draw-chart
- "assets-over-time"
- assets-line-chart labels assets nil nil))))
-
-(defn liabilities-over-time-chart [{:keys [net-liabilities]}]
- (let [labels (->> net-liabilities (map :cljs-date) (map #(tf/unparse custom-month-year %)))
- liabilities (->> net-liabilities (map :amount))]
- (chart-box "LIABILITIES OVER TIME" (draw-chart
- "liabilities-over-time"
- liabilities-line-chart labels liabilities nil nil))))
-
-(defn emergency-fund-months-info-box [{:keys [emergency-fund-months emergency-fund-months-change]}]
- [:div.has-text-centered.info-box
- [:p.heading "EMERGENCY FUND MONTHS"]
- [:p.title {:class "has-text-light"} (format-number emergency-fund-months)]
- (if (= (get emergency-fund-months-change :direction) :up)
- [:p.subtitle.is-size-7.has-text-light.has-text-success
- [:i.fa.fa-arrow-up] (str " " (format-number (get emergency-fund-months-change :delta)) " (" (format-number (get emergency-fund-months-change :percentage)) "%)")]
- (if (= (get emergency-fund-months-change :direction) :down)
- [:p.subtitle.is-size-7.has-text-light.has-text-danger
- [:i.fa.fa-arrow-down] (str " " (format-number (get emergency-fund-months-change :delta)) " (" (format-number (get emergency-fund-months-change :percentage)) "%)")]
- [:p.subtitle.is-size-7.has-text-light.has-text-warning
- [:i.fa.fa-arrow-right] (str " " (format-number (get emergency-fund-months-change :delta)) " (" (format-number (get emergency-fund-months-change :percentage)) "%)")]))])
-
-(defn net-worth-info-box [{:keys [net-worth net-worth-change]}]
- [:div.has-text-centered.info-box
- [:p.heading "NET WORTH"]
- [:p.title {:class "has-text-light"} (format-number net-worth)]
- (if (= (get net-worth-change :direction) :up)
- [:p.subtitle.is-size-7.has-text-light.has-text-success
- [:i.fa.fa-arrow-up] (str " " (format-number (get net-worth-change :delta)) " (" (format-number (get net-worth-change :percentage)) "%)")]
- (if (= (get net-worth-change :direction) :down)
- [:p.subtitle.is-size-7.has-text-light.has-text-danger
- [:i.fa.fa-arrow-down] (str " " (format-number (get net-worth-change :delta)) " (" (format-number (get net-worth-change :percentage)) "%)")]
- [:p.subtitle.is-size-7.has-text-light.has-text-warning
- [:i.fa.fa-arrow-right] (str " " (format-number (get net-worth-change :delta)) " (" (format-number (get net-worth-change :percentage)) "%)")]))])
-
-
-(defn fi-investments-info-box [{:keys [fi-investments fi-investments-change]}]
- [:div.has-text-centered.info-box
- [:p.heading "FI INVESTMENTS"]
- [:p.title {:class "has-text-light"} (format-number (:amount (last fi-investments)))]
- (if (= (get fi-investments-change :direction) :up)
- [:p.subtitle.is-size-7.has-text-light.has-text-success
- [:i.fa.fa-arrow-up] (str " " (format-number (get fi-investments-change :delta)) " (" (format-number (get fi-investments-change :percentage)) "%)")]
- (if (= (get fi-investments-change :direction) :down)
- [:p.subtitle.is-size-7.has-text-light.has-text-danger
- [:i.fa.fa-arrow-down] (str " " (format-number (get fi-investments-change :delta)) " (" (format-number (get fi-investments-change :percentage)) "%)")]
- [:p.subtitle.is-size-7.has-text-light.has-text-warning
- [:i.fa.fa-arrow-right] (str " " (format-number (get fi-investments-change :delta)) " (" (format-number (get fi-investments-change :percentage)) "%)")]))])
-
-(defn change-str [change change-percentage]
- (if (nil? change-percentage)
- (str " " (format-number change))
- (str " " (format-number change) " (" (format-number change-percentage) "%)")))
-
-(defn info-box-with-amount-and-change [title amount change change-percentage change-direction]
- [:div.has-text-centered.info-box
- [:p.heading title]
- [:p.title {:class "has-text-light"} (format-number amount)]
- (if (= change-direction :up)
- [:p.subtitle.is-size-7.has-text-light.has-text-success
- [:i.fa.fa-arrow-up] (change-str change change-percentage)]
- (if (= change-direction :down)
- [:p.subtitle.is-size-7.has-text-light.has-text-danger
- [:i.fa.fa-arrow-down] (change-str change change-percentage)]
- [:p.subtitle.is-size-7.has-text-light.has-text-warning
- [:i.fa.fa-arrow-right] (change-str change change-percentage)]))])
-
-(defn fi-monthly-withdrawal-info-box [{:keys [fi-investments fi-monthly-withdrawal-change]}]
- (let [title "FI MONTHLY WITHDRAWAL"
- amount (:fi-monthly-withdrawal (last fi-investments))
- change (:delta fi-monthly-withdrawal-change)
- change-percentage (:percentage fi-monthly-withdrawal-change)
- change-direction (:direction fi-monthly-withdrawal-change)]
- (info-box-with-amount-and-change title amount change change-percentage change-direction)))
-
-(defn fi-percentage-info-box [{:keys [fi-investments]}]
- (let [title "FI PERCENTAGE"
- amount (:fi-percentage (last fi-investments))
- change (- amount (:fi-percentage (first (take-last 2 fi-investments))))
- change-percentage nil
- change-direction (if (pos? change)
- :up
- (if (neg? change)
- :down
- :same))]
- (info-box-with-amount-and-change title amount change change-percentage change-direction)))
-
-(defn investments-info-box [{:keys [investments investments-change]}]
- (let [title "INVESTMENTS"
- amount (:amount (last investments))
- change (:delta investments-change)
- change-percentage (:percentage investments-change)
- change-direction (:direction investments-change)]
- (info-box-with-amount-and-change title amount change change-percentage change-direction)))
-
-
-(defn tfsa-yearly-contributions-chart [{:keys [tfsa-contributions-per-year]}]
- (let [labels (->> tfsa-contributions-per-year (map :year))
- contributions (->> tfsa-contributions-per-year (map :amount))
- limits (->> tfsa-contributions-per-year (map :limit))]
- (chart-box "TFSA YEARLY CONTRIBUTIONS"
- (draw-chart
- "tfsa-yearly-contributions"
- tfsa-yearly-chart labels contributions limits nil))))
-
-(defn tfsa-lifetime-contribution-chart [{:keys [tfsa-contributions-over-lifetime]}]
-(let [contribution [(:amount tfsa-contributions-over-lifetime)]
- limit [(:limit tfsa-contributions-over-lifetime)]]
- (chart-box "TFSA LIFETIME CONTRIBUTION"
- (draw-chart
- "tfsa-lifetime-contribution"
- tfsa-lifetime-chart contribution limit nil nil))))
-
-(defn asset-distribution-chart []
-(chart-box "ASSET TYPE DISTRIBUTION"
- (draw-chart "asset-distribution" pie-chart-1 nil nil nil nil)))
-
-(defn asset-geographic-distribution-chart []
-(chart-box "ASSET GEOGRAPHIC DISTRIBUTION"
- (draw-chart "asset-geographic-distribution" pie-chart-2 nil nil nil nil)))
-
-(defn asset-allocation-chart []
-(chart-box "ASSET ALLOCATION"
- (draw-chart "asset-allocation" pie-chart-3 nil nil nil nil)))
-
-(defmethod render-page :main [{:keys [data]}]
- (let [col
- (if (= (->
- data
- (get :sample)
- (first)
- (get :is-sample))
- "yes")
- col-sample-data
- col-real-data)]
- [:div.columns.is-multiline.is-centered
- [col 2 12 (net-worth-info-box data)]
- [col 2 12 (emergency-fund-months-info-box data)]
- [col 2 12 (investments-info-box data)]
- [col 2 12 (fi-investments-info-box data)]
- [col 2 12 (fi-monthly-withdrawal-info-box data)]
- [col 2 12 (fi-percentage-info-box data)]
- [col 4 12 (net-worth-over-time-chart data)]
- [col 4 12 (assets-over-time-chart data)]
- [col 4 12 (liabilities-over-time-chart data)]
- [col 6 12 (cash-flow-over-time-chart data)]
- [col 6 12 (savings-rate-over-time-chart data)]
- [col 6 12 (tfsa-yearly-contributions-chart data)]
- [col 6 12 (tfsa-lifetime-contribution-chart data)]
- [col 12 12 (investment-benchmark-over-time-chart)]
- ;; [col 4 12 (asset-distribution-chart)]
- ;; [col 4 12 (asset-geographic-distribution-chart)]
- ;; [col 4 12 (asset-allocation-chart)]
- ]))
-
-(defn footer []
- [:footer.footer
- [:div.content.has-text-centered
- [:p "Built with ❤️ by "
- [:a {:href "https://rameezkhan.me"} "Rameez"]]]])
-
-(defn app []
- [:div
- [nav]
- (when-not (= :hidden (get-in @state [:modal :key]))
- [modal state])
- [:div.section.has-background-light
- (render-page @state)]
- (footer)])
-
-(defn sleep [f ms]
-(js/setTimeout f ms))
-
-(defmethod render-page :loading [state]
-[:div "loading"])
-
-(defn get-app-element []
-(gdom/getElement "app"))
-
-(defn mount [el]
-(reagent/render-component [app] el))
-
-(defn mount-app-element []
-(when-let [el (get-app-element)]
- (mount el)))
-
-(when (= :loading (:page @state))
-(build-app-data-from-localstorage-data (or
- (get-data-from-localstorage)
- (get-sample-data)))
-(js/setTimeout #(page :main)))
-
-;; conditionally start your application based on the presence of an "app" element
-;; this is particularly helpful for testing this ns without launching the app
-(mount-app-element)
-
-;; specify reload hook with ^;after-load metadata
-(defn ^:after-load on-reload []
-(mount-app-element)
-;; optionally touch your app-state to force rerendering depending on
-;; your application
-;; (swap! app-state update-in [:__figwheel_counter] inc)
-)
+(ns financial-health-dashboard.core
+ (:require [reagent.dom :as rd]
+ [financial-health-dashboard.components :as c]))
+
+(defn app
+ "DOM entrypoint"
+ []
+ [:div.section
+ [c/nav]])
+
+(defn mount-reagent []
+ (rd/render
+ app
+ (js/document.getElementById "app")))
+
+(defn ^:dev/after-load start []
+ (js/console.log "start")
+ (mount-reagent))
+
+(defn ^:export init []
+ (js/console.log "init")
+ (start))
+
+(defn ^:dev/before-load stop []
+ (js/console.log "stop"))
diff --git a/src/financial_health_dashboard/domain.cljs b/src/financial_health_dashboard/domain.cljs
index 752f1f1..9b8107e 100644
--- a/src/financial_health_dashboard/domain.cljs
+++ b/src/financial_health_dashboard/domain.cljs
@@ -1,337 +1,83 @@
-(ns financial-health-dashboard.domain
- (:require
- [clojure.spec.alpha :as s]
- [cljs-time.core :as time]))
-
-(defn not-empty-string? [s] (and (string? s)
- (not-empty s)))
-(defn year? [n] (boolean (and (number? n) (>= n 1900) (> 3000 n))))
-(defn month? [n] (boolean (and (number? n) (>= n 1) (> 13 n))))
-(def yes "yes")
-(def no "no")
-
-(def tfsa-lifetime-limit 500000)
-
-(def tfsa-limits
- {:2016 30000
- :2017 30000
- :2018 33000
- :2019 33000
- :2020 33000
- :2021 36000})
-
-(def fi-yearly-withdrawal-rate 0.04)
-(def fi-monthly-withdrawal-rate (/ fi-yearly-withdrawal-rate 12))
-
-
-;; SPECS
-
-
-(s/def :d/year year?)
-(s/def :d/month month?)
-(s/def :d/amount number?)
-(s/def :d/name not-empty-string?)
-(s/def :d/is-sample #(contains? #{yes no} %))
-
-
-;; DATA TYPES
-
-
-(def data-types-config
- [["sample"
- [:d/is-sample]
- "Indicate sample data"]
- ["comment"
- []
- "A row used for any kind of comment"]
- ["income"
- [:d/name :d/year :d/month :d/amount]
- "Income"]
- ["expense"
- [:d/name :d/year :d/month :d/amount]
- "Expense"]
- ["emergency-monthly-expense"
- [:d/year :d/month :d/amount]
- "Monthly expense. This doesn't include contributions to RA's, investments or savings accounts."]
- ["emergency-fund"
- [:d/year :d/month :d/amount]
- "Emergency fund balance."]
- ["asset"
- [:d/name :d/year :d/month :d/amount]]
- ["liability"
- [:d/name :d/year :d/month :d/amount]]
- ["tfsa-contribution"
- [:d/year :d/month :d/amount]]
- ["fi-expense"
- [:d/amount]]])
-
-(def data-types (->> data-types-config
- (map (juxt first second))
- (into {})))
-
-(defn type-of? [data-type m]
- (= data-type (:data-type m)))
-
-(defn type-of-f? [data-type] (partial type-of? data-type))
-
-(defn types-of-f? [& types]
- (fn [m] (->> types
- (map #(type-of? % m))
- (filter true?)
- not-empty)))
-
-(defn timestamped [{:keys [year month day] :as m}]
- (let [date (js/Date. year (dec month) day)] ;;js months start at 0
- (assoc m
- :date date
- :cljs-date (time/date-time year month day)
- :timestamp (.getTime date))))
-
-
-;; DATA EXTRACTORS
-(defn income [data]
- (->> data (filter (type-of-f? :income))
- (map timestamped)
- (sort-by :timestamp)))
-
-(defn emergency-monthly-expense [data]
- (->> data (filter (type-of-f? :emergency-monthly-expense))
- (map timestamped)
- (sort-by :timestamp)))
-
-(defn emergency-fund [data]
- (->> data (filter (type-of-f? :emergency-fund))
- (map timestamped)
- (sort-by :timestamp)))
-
-(defn emergency-fund-months [emergency-fund emergency-monthly-expense]
- (let [latest-emergency-monthly-expense (get (last emergency-monthly-expense) :amount)
- latest-emergency-fund-balance (get (last emergency-fund) :amount)]
- (/ latest-emergency-fund-balance latest-emergency-monthly-expense)))
-
-(defn emergency-fund-months-change [emergency-fund emergency-monthly-expense]
- (if (> (count emergency-fund) 1)
- (let [latest-emergency-monthly-expense (get (last emergency-monthly-expense) :amount)
- latest-em-fund-balance (get (last emergency-fund) :amount)
- second-last-em-fund-balance (get (-> emergency-fund (reverse) (nth 1 nil)) :amount)
- change-in-amount (- latest-em-fund-balance second-last-em-fund-balance)
- delta (/ change-in-amount latest-emergency-monthly-expense)]
- (if (= delta 0.0)
- {:direction :same :delta delta :percentage 0}
- (if (< delta 0)
- {:direction :down :delta (* -1 delta) :percentage (* (/ change-in-amount second-last-em-fund-balance) -100)}
- {:direction :up :delta delta :percentage (* (/ change-in-amount second-last-em-fund-balance) 100)})))
- nil))
-
-(defn sample [data]
- (->> data (filter (type-of-f? :sample))))
-
-(defn assets [data]
- (->> data (filter (type-of-f? :asset))
- (map timestamped)
- (sort-by :timestamp)))
-
-(defn net-per-month [data]
- (->> data (map #(assoc % :grouping [(:year %) (:month %)]))
- (group-by :grouping)
- (map (fn [[g t]]
- [g (->> t (map :amount) (reduce +))]))
- (into (sorted-map))))
-
-(defn liabilities [data]
- (->> data (filter (type-of-f? :liability))
- (map timestamped)
- (sort-by :timestamp)))
+(ns financial-health-dashboard.domain)
+
+(def data {:user/name "Bob"
+ :user/age 28
+ :user/accounts [{:account/name "FNB Cheque Account"
+ :account/type :bank
+ :account/id 1
+ :account/active true}
+ {:account/name "Easy Equities TFSA"
+ :account/type :investment
+ :account/id 2
+ :account/active true}]
+ :user/account-balances [{:account/id 1
+ :account-balance/year 2021
+ :account-balance/month 6
+ :account-balance/amount 100}
+ {:account/id 2
+ :account-balance/year 2021
+ :account-balance/month 6
+ :account-balance/amount 200}
+ {:account/id 2
+ :account-balance/year 2021
+ :account-balance/month 1
+ :account-balance/amount 400}]})
+
+
+(defn user-name
+ "Return a user's name"
+ [{:user/keys [name]}]
+ name)
+
+(defn user-age
+ "Return a user's age"
+ [{:user/keys [age]}]
+ age)
+
+(defn user-accounts
+ "Return a user's active accounts, filtered if account-type is provided"
+ ([{:user/keys [accounts]}]
+ (filter #(:account/active %) accounts))
+ ([data account-type]
+ (filter
+ #(= (:account/type %) account-type)
+ (user-accounts data))))
+
+(defn account
+ "Return an account by provided account-id"
+ [data account-id]
+ (let [accounts (user-accounts data)]
+ (first (filter #(= (:account/id %) account-id) accounts))))
+
+(defn account-balances
+ "Returns all account balances, filtered if year or month are provided"
+ ([data]
+ (:user/account-balances data))
+
+ ([data year]
+ (filter #(= (:account-balance/year %) year)
+ (account-balances data)))
+
+ ([data year month]
+ (filter #(and
+ (= (:account-balance/year %) year)
+ (= (:account-balance/month %) month))
+ (account-balances data))))
+
+(defn balance-exists-for-account?
+ "Check if balance exists for account"
+ [account-id year month]
+ (let [balances (account-balances data year month)
+ balances-for-account (filter #(= (:account/id %) account-id) balances)]
+ (pos? (count balances-for-account))))
(defn net-worth
- ([net-assets net-liabilities] (net-worth net-assets net-liabilities 1))
- ([net-assets net-liabilities n-last]
- (let [latest-monthly-net-assets (->> net-assets (take-last n-last) (first) (second))
- latest-monthly-net-liabilities (->> net-liabilities (take-last n-last) (first) (second))]
- (- latest-monthly-net-assets latest-monthly-net-liabilities))))
-
-(defn net-worth-change [net-assets-per-month net-liabilities-per-month]
- (if (or (> (count net-assets-per-month) 1) (> (count net-liabilities-per-month) 1))
- (let [latest-net-worth (net-worth net-assets-per-month net-liabilities-per-month)
- second-last-net-worth (net-worth net-assets-per-month net-liabilities-per-month 2)
- delta (- latest-net-worth second-last-net-worth)]
- (if (zero? delta)
- {:direction :same :delta delta :percentage 0}
- (if (neg? delta)
- {:direction :down :delta (* -1 delta) :percentage (* (/ delta second-last-net-worth) -100)}
- {:direction :up :delta delta :percentage (* (/ delta second-last-net-worth) 100)})))
- nil))
-
-(defn flatten-grouped-months [grouped-months]
- (map (fn [[[y m] a]] {:year y :month m :amount a}) grouped-months))
-
-(defn net-worths [assets liabilities]
- (let [l (->> liabilities (map #(assoc % :amount (* -1 (:amount %)))))
- al (concat assets l)]
- (->> al (map #(assoc % :grouping [(:year %) (:month %)]))
- (group-by :grouping)
- (map (fn [[g t]]
- [g (->> t (map :amount) (reduce +))]))
- (into {}))))
-
-(defn make-series-from-grouped-data [grouped-data]
- (->> grouped-data (flatten-grouped-months)
- (map timestamped)
- (sort-by :timestamp)))
-
-(defn tfsa-contributions [data]
- (->> data (filter (type-of-f? :tfsa-contribution))
- (map timestamped)
- (sort-by :timestamped)))
-
-(defn tax-year [date]
- (let [year (time/year date)
- month (time/month date)]
- (if (> month 2) (inc year) year)))
-
-(defn tfsa-contributions-per-year [contributions]
- (->> contributions (map #(assoc % :tax-year [(tax-year (:cljs-date %))]))
- (group-by :tax-year)
- (map (fn [[g t]]
- [g (->> t (map :amount) (reduce +))]))
- (into {})))
-
-(defn tfsa-contributions-over-lifetime [contributions]
- (let [total (->> contributions (map :amount) (reduce +))]
- {:amount total :limit tfsa-lifetime-limit}))
-
-(defn map-tfsa-yearly-limits [contributions]
- (->> contributions (map #(assoc %
- :limit (get tfsa-limits
- (keyword (str (:year %))))))))
-
-(defn expenses [data]
- (->> data (filter (type-of-f? :expense))
- (map timestamped)
- (sort-by :timestamp)))
-
-(defn fi-expense [data]
- (last (->> data (filter (type-of-f? :fi-expense)))))
-
-
-(defn investments [assets]
- (->> assets
- (filter #(or
- (= (:name %) "tfsa")
- (= (:name %) "liberty-ra")
- (= (:name %) "liberty-preservation-fund")
- (= (:name %) "sygnia-ra")
- (= (:name %) "td-ameritrade")
- (= (:name %) "education-fund")))
- (map #(assoc % :grouping [(:year %) (:month %)]))
- (group-by :grouping)
- (map (fn [[g t]]
- [g (->> t (map :amount) (reduce +))]))
- (into {})))
-
-(defn investments-change [investments]
- (if (> (count investments) 1)
- (let [latest-amount (:amount (last investments ))
- second-last-amount (:amount (first (take-last 2 investments)))
- delta (- latest-amount second-last-amount)]
- (if (zero? delta)
- {:direction :same :delta delta :percentage 0}
- (if (neg? delta)
- {:direction :down :delta (* -1 delta) :percentage (* (/ delta second-last-amount) -100)}
- {:direction :up :delta delta :percentage (* (/ delta second-last-amount) 100)})))
- nil))
-
-(defn fi-investments [assets]
- (->> assets
- (filter #(or
- (= (:name %) "tfsa")
- (= (:name %) "td-ameritrade")))
- (map #(assoc % :grouping [(:year %) (:month %)]))
- (group-by :grouping)
- (map (fn [[g t]]
- [g (->> t (map :amount) (reduce +))]))
- (into {})))
-
-(defn fi-investments-change [fi-investments]
- (if (> (count fi-investments) 1)
- (let [latest-amount (:amount (last fi-investments ))
- second-last-amount (:amount (first (take-last 2 fi-investments)))
- delta (- latest-amount second-last-amount)]
- (if (zero? delta)
- {:direction :same :delta delta :percentage 0}
- (if (neg? delta)
- {:direction :down :delta (* -1 delta) :percentage (* (/ delta second-last-amount) -100)}
- {:direction :up :delta delta :percentage (* (/ delta second-last-amount) 100)})))
- nil))
-
-(defn map-fi-withdrawals [investments]
- (->> investments (map #(assoc %
- :fi-monthly-withdrawal (Math/ceil ( * fi-monthly-withdrawal-rate (:amount %) ))))))
-
-(defn map-fi-percentage [fi-expense investments]
- (->> investments (map #(assoc %
- :fi-percentage (* (/ (:amount %) (* (:amount fi-expense) 300)) 100)))))
-
-
-
-(defn fi-monthly-withdrawal-change [fi-investments]
- (if (> (count fi-investments) 1)
- (let [latest-amount (:fi-monthly-withdrawal (last fi-investments ))
- second-last-amount (:fi-monthly-withdrawal (first (take-last 2 fi-investments)))
- delta (- latest-amount second-last-amount)]
- (if (zero? delta)
- {:direction :same :delta delta :percentage 0}
- (if (neg? delta)
- {:direction :down :delta (* -1 delta) :percentage (* (/ delta second-last-amount) -100)}
- {:direction :up :delta delta :percentage (* (/ delta second-last-amount) 100)})))
- nil))
-
-
-(defn all-your-bucks[data]
- (let [sample (sample data)
- income (income data)
- expenses (->> data (expenses) (net-per-month) (make-series-from-grouped-data))
- emergency-fund (emergency-fund data)
- emergency-monthly-expense (emergency-monthly-expense data)
- emergency-fund-months (emergency-fund-months emergency-fund emergency-monthly-expense)
- emergency-fund-months-change (emergency-fund-months-change emergency-fund emergency-monthly-expense)
- assets (assets data)
- net-assets-per-month (net-per-month assets)
- net-assets-series (make-series-from-grouped-data net-assets-per-month)
- liabilities (liabilities data)
- net-liabilities-per-month (net-per-month liabilities)
- net-liabilities-series (make-series-from-grouped-data net-liabilities-per-month)
- net-worth (net-worth net-assets-per-month net-liabilities-per-month)
- net-worths (make-series-from-grouped-data (net-worths assets liabilities))
- net-worth-change (net-worth-change net-assets-per-month net-liabilities-per-month)
- tfsa-contributions (tfsa-contributions data)
- tfsa-contributions-per-year (->> tfsa-contributions
- (tfsa-contributions-per-year)
- (make-series-from-grouped-data)
- (map-tfsa-yearly-limits))
- tfsa-contributions-over-lifetime (tfsa-contributions-over-lifetime tfsa-contributions)
- fi-expense (fi-expense data)
- fi-investments (map-fi-percentage fi-expense ( map-fi-withdrawals (make-series-from-grouped-data (fi-investments assets)) ))
- investments (make-series-from-grouped-data (investments assets))
- investments-change (investments-change investments)
- fi-investments-change (fi-investments-change fi-investments)
- fi-monthly-withdrawal-change (fi-monthly-withdrawal-change fi-investments)
- ]
- {:sample sample
- :income income
- :expenses expenses
- :emergency-fund-months emergency-fund-months
- :emergency-fund-months-change emergency-fund-months-change
- :net-worths net-worths
- :net-worth net-worth
- :net-worth-change net-worth-change
- :net-assets net-assets-series
- :net-liabilities net-liabilities-series
- :tfsa-contributions-per-year tfsa-contributions-per-year
- :tfsa-contributions-over-lifetime tfsa-contributions-over-lifetime
- :fi-investments fi-investments
- :fi-investments-change fi-investments-change
- :investments investments
- :investments-change investments-change
- :fi-monthly-withdrawal-change fi-monthly-withdrawal-change}))
+ "Calculate net worth for year and month"
+ [data year month]
+ (reduce
+ (fn [acc
+ {:account-balance/keys [amount]}]
+ (+ acc amount))
+ 0
+ (account-balances data year month)))
diff --git a/src/financial_health_dashboard/example.cljs b/src/financial_health_dashboard/example.cljs
deleted file mode 100644
index 43d5427..0000000
--- a/src/financial_health_dashboard/example.cljs
+++ /dev/null
@@ -1,83 +0,0 @@
-(ns financial-health-dashboard.example
- (:require [cljs-time.core :as time]
- [clojure.string :as str]))
-
-(def now (time/now))
-
-(def this-year (time/year now))
-
-(def last-year (dec this-year))
-
-(def date-of-birth
- [["date-of-birth" 1985 11 6]])
-
-(def year-goals
- [["year-goal" this-year 15]
- ["year-goal" this-year 13]
- ["year-goal" this-year 11]])
-
-(def income
- [["income" "salary" this-year 1 20000]
- ["income" "salary" this-year 2 18000]
- ["income" "salary" this-year 3 22000]
- ["income" "salary" this-year 4 19000]
- ["income" "salary" this-year 5 28000]])
-
-(def expense
- [["expense" "day-to-day" this-year 1 18000]
- ["expense" "recurring" this-year 1 2000]
- ["expense" "day-to-day" this-year 2 19000]
- ["expense" "day-to-day" this-year 3 21000]
- ["expense" "day-to-day" this-year 4 19000]
- ["expense" "day-to-day" this-year 5 21000]])
-
-(def emergency-monthly-expense
- [["emergency-monthly-expense" this-year 1 20000]])
-
-(def emergency-fund
- [["emergency-fund" this-year 3 50000]
- ["emergency-fund" this-year 4 60000]])
-
-(def example-comment
- [["comment" "some random comment"]])
-
-(def sample
- [["sample" "yes"]])
-
-(def assets
- [["asset" "emergency-fund" this-year 1 10000]
- ["asset" "emergency-fund" this-year 2 20000]
- ["asset" "emergency-fund" this-year 3 30000]
- ["asset" "TFSA" this-year 1 30000]
- ["asset" "TFSA" this-year 2 40000]
- ["asset" "TFSA" this-year 3 50000]])
-
-(def liabilities
- [["liability" "home-loan" this-year 1 8000]
- ["liability" "home-loan" this-year 2 6000]])
-
-(def tfsa-contributions
- [["tfsa-contribution" 2016 3 0]
- ["tfsa-contribution" this-year 2 6000]
- ["tfsa-contribution" this-year 3 6000]
- ["tfsa-contribution" this-year 4 6000]])
-
-(def fi-expense
- [["fi-expense" 10000]])
-
-(def data-piped
- (->> [sample
- date-of-birth
- year-goals
- income
- expense
- emergency-monthly-expense
- emergency-fund
- assets
- liabilities
- tfsa-contributions
- fi-expense]
- (reduce into)
- (map #(->> % (map str) (str/join "|")))
- (str/join "\n")))
-
diff --git a/src/financial_health_dashboard/localstorage.cljs b/src/financial_health_dashboard/localstorage.cljs
deleted file mode 100644
index 2f71fee..0000000
--- a/src/financial_health_dashboard/localstorage.cljs
+++ /dev/null
@@ -1,11 +0,0 @@
-(ns financial-health-dashboard.localstorage)
-
-(defn set-item!
- "Set `key' in browser's localStorage to `val`."
- [key val]
- (.setItem (.-localStorage js/window) key val))
-
-(defn get-item
- "Returns value of `key' from browser's localStorage."
- [key]
- (.getItem (.-localStorage js/window) key))
diff --git a/src/financial_health_dashboard/parse.cljs b/src/financial_health_dashboard/parse.cljs
deleted file mode 100644
index 826e940..0000000
--- a/src/financial_health_dashboard/parse.cljs
+++ /dev/null
@@ -1,78 +0,0 @@
-(ns financial-health-dashboard.parse
- (:require
- [financial-health-dashboard.domain :as d]
- [cljs.spec.alpha :as s]
- [clojure.string :as str]
- [cljs.reader :as reader]
- [testdouble.cljs.csv :as csv]))
-
-(def pipe "|")
-
-(defn- not-empty-row? [row]
- (->> row
- (filter (comp not empty?))
- count
- (not= 0)))
-
-(defn- parse-field
- [s]
- (let [value (reader/read-string s)]
- (if (symbol? value) s value)))
-
-(defn- parse-row [row]
- (map
- (fn [field]
- (-> field
- str/trim
- parse-field))
- row))
-
-(defn- validate-row-data [key data]
- (let [specs (get d/data-types key)]
- (->> specs
- (map-indexed
- (fn [i spec]
- (let [field (nth data i)]
- {:valid? (s/valid? spec field)
- :spec spec
- :field field}))))))
-
-(defn- validate-row [[key & data]]
- (if-not (contains? d/data-types key)
- {:valid? false :error (str key " is an invalid data type") :data data}
- (let [validation (validate-row-data key data)
- specs (->> (get d/data-types key))]
- (if (empty? (->> validation (map :valid?) (filter false?)))
- {:valid? true :specs specs :data data :key (keyword key)}
- (let [failed-specs (->> validation
- (filter (comp false? :valid?))
- (map :spec))]
- {:valid false :data data
- :specs specs
- :key (keyword key)
- :failed-specs failed-specs
- :error (str key " has invalid values for "
- (map name failed-specs)
- ". Expected " (map name specs))})))))
-
-(defn parse
- [seperator data]
- (->> (csv/read-csv data :separator seperator)
- (filter not-empty-row?)
- (map-indexed (fn [i r]
- (-> r parse-row validate-row (assoc :i (inc i)))))))
-
-
-(defn- as-domain-value [{:keys [specs key data]}]
- (-> (zipmap
- (map (comp keyword name) specs)
- (take (count specs) data))
- (assoc :data-type key)))
-
-(defn as-domain-values
- "Converts a collection of parsed and valid data rows into domain data."
- [rows]
- (->> rows
- (filter :valid?)
- (map as-domain-value)))
-
diff --git a/src/financial_health_dashboard/scratch.cljs b/src/financial_health_dashboard/scratch.cljs
deleted file mode 100644
index c6f80cd..0000000
--- a/src/financial_health_dashboard/scratch.cljs
+++ /dev/null
@@ -1,7 +0,0 @@
-(ns financial-health-dashboard.scratch
- (:require
- [financial-health-dashboard.parse :as parse]
- [financial-health-dashboard.example :as example]))
-
-(defn load-example-data []
- (parse/as-domain-values (parse/parse "|" example/data-piped)))
diff --git a/test.cljs.edn b/test.cljs.edn
deleted file mode 100644
index 6d2fbbf..0000000
--- a/test.cljs.edn
+++ /dev/null
@@ -1,10 +0,0 @@
-^{
- ;; use an alternative landing page for the tests so that we don't
- ;; launch the application
- :open-url "http://[[server-hostname]]:[[server-port]]/test.html"
-
- ;; uncomment to launch tests in a headless environment
- ;; you will have to figure out the path to chrome on your system
- ;; :launch-js ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" "--headless" "--disable-gpu" "--repl" :open-url]
- }
-{:main financial-health-dashboard.test-runner}
diff --git a/test/financial_health_dashboard/core_test.cljs b/test/financial_health_dashboard/core_test.cljs
deleted file mode 100644
index 10ec82d..0000000
--- a/test/financial_health_dashboard/core_test.cljs
+++ /dev/null
@@ -1,10 +0,0 @@
-(ns financial-health-dashboard.core-test
- (:require
- [cljs.test :refer-macros [deftest is testing]]
- [financial-health-dashboard.core :refer [multiply]]))
-
-(deftest multiply-test
- (is (= (* 1 2) (multiply 1 2))))
-
-(deftest multiply-test-2
- (is (= (* 75 10) (multiply 10 75))))
diff --git a/test/financial_health_dashboard/test_runner.cljs b/test/financial_health_dashboard/test_runner.cljs
deleted file mode 100644
index 33f23bf..0000000
--- a/test/financial_health_dashboard/test_runner.cljs
+++ /dev/null
@@ -1,9 +0,0 @@
-;; This test runner is intended to be run from the command line
-(ns financial-health-dashboard.test-runner
- (:require
- ;; require all the namespaces that you want to test
- [financial-health-dashboard.core-test]
- [figwheel.main.testing :refer [run-tests-async]]))
-
-(defn -main [& args]
- (run-tests-async 5000))
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 0000000..4a24e92
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,698 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+asn1.js@^5.2.0:
+ version "5.4.1"
+ resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
+ integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
+ dependencies:
+ bn.js "^4.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+ safer-buffer "^2.1.0"
+
+assert@^1.1.1:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb"
+ integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==
+ dependencies:
+ object-assign "^4.1.1"
+ util "0.10.3"
+
+async-limiter@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
+ integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+
+base64-js@^1.0.2:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+ integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
+ version "4.11.9"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
+ integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
+
+bn.js@^5.0.0, bn.js@^5.1.1:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b"
+ integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==
+
+brorand@^1.0.1, brorand@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+ integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
+ integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==
+ dependencies:
+ buffer-xor "^1.0.3"
+ cipher-base "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.3"
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+browserify-cipher@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0"
+ integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==
+ dependencies:
+ browserify-aes "^1.0.4"
+ browserify-des "^1.0.0"
+ evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c"
+ integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==
+ dependencies:
+ cipher-base "^1.0.1"
+ des.js "^1.0.0"
+ inherits "^2.0.1"
+ safe-buffer "^5.1.2"
+
+browserify-rsa@^4.0.0, browserify-rsa@^4.0.1:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d"
+ integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==
+ dependencies:
+ bn.js "^5.0.0"
+ randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3"
+ integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==
+ dependencies:
+ bn.js "^5.1.1"
+ browserify-rsa "^4.0.1"
+ create-hash "^1.2.0"
+ create-hmac "^1.1.7"
+ elliptic "^6.5.3"
+ inherits "^2.0.4"
+ parse-asn1 "^5.1.5"
+ readable-stream "^3.6.0"
+ safe-buffer "^5.2.0"
+
+browserify-zlib@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+ integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==
+ dependencies:
+ pako "~1.0.5"
+
+buffer-xor@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+ integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
+
+buffer@^4.3.0:
+ version "4.9.2"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
+ integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+ isarray "^1.0.0"
+
+builtin-status-codes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+ integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+ integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+console-browserify@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
+ integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
+
+constants-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+ integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=
+
+core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+ integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+create-ecdh@^4.0.0:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
+ integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==
+ dependencies:
+ bn.js "^4.1.0"
+ elliptic "^6.5.3"
+
+create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
+ integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
+ dependencies:
+ cipher-base "^1.0.1"
+ inherits "^2.0.1"
+ md5.js "^1.3.4"
+ ripemd160 "^2.0.1"
+ sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
+ integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
+ dependencies:
+ cipher-base "^1.0.3"
+ create-hash "^1.1.0"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+crypto-browserify@^3.11.0:
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
+ integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==
+ dependencies:
+ browserify-cipher "^1.0.0"
+ browserify-sign "^4.0.0"
+ create-ecdh "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.0"
+ diffie-hellman "^5.0.0"
+ inherits "^2.0.1"
+ pbkdf2 "^3.0.3"
+ public-encrypt "^4.0.0"
+ randombytes "^2.0.0"
+ randomfill "^1.0.3"
+
+des.js@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
+ integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==
+ dependencies:
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+diffie-hellman@^5.0.0:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
+ integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==
+ dependencies:
+ bn.js "^4.1.0"
+ miller-rabin "^4.0.0"
+ randombytes "^2.0.0"
+
+domain-browser@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
+ integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
+
+elliptic@^6.5.3:
+ version "6.5.4"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
+ integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
+ dependencies:
+ bn.js "^4.11.9"
+ brorand "^1.1.0"
+ hash.js "^1.0.0"
+ hmac-drbg "^1.0.1"
+ inherits "^2.0.4"
+ minimalistic-assert "^1.0.1"
+ minimalistic-crypto-utils "^1.0.1"
+
+events@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379"
+ integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==
+
+evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+ integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==
+ dependencies:
+ md5.js "^1.3.4"
+ safe-buffer "^5.1.1"
+
+hash-base@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33"
+ integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==
+ dependencies:
+ inherits "^2.0.4"
+ readable-stream "^3.6.0"
+ safe-buffer "^5.2.0"
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
+ integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.1"
+
+hmac-drbg@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+ integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
+ dependencies:
+ hash.js "^1.0.3"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.1"
+
+https-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
+ integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
+
+ieee754@^1.1.4:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+ integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
+inherits@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+ integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
+
+inherits@2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+ integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
+inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+isarray@^1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+ integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+"js-tokens@^3.0.0 || ^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+loose-envify@^1.1.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+ integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+md5.js@^1.3.4:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
+ integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+ safe-buffer "^5.1.2"
+
+miller-rabin@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+ integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==
+ dependencies:
+ bn.js "^4.0.0"
+ brorand "^1.0.1"
+
+minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+ integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+minimalistic-crypto-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+ integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
+
+node-libs-browser@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
+ integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==
+ dependencies:
+ assert "^1.1.1"
+ browserify-zlib "^0.2.0"
+ buffer "^4.3.0"
+ console-browserify "^1.1.0"
+ constants-browserify "^1.0.0"
+ crypto-browserify "^3.11.0"
+ domain-browser "^1.1.1"
+ events "^3.0.0"
+ https-browserify "^1.0.0"
+ os-browserify "^0.3.0"
+ path-browserify "0.0.1"
+ process "^0.11.10"
+ punycode "^1.2.4"
+ querystring-es3 "^0.2.0"
+ readable-stream "^2.3.3"
+ stream-browserify "^2.0.1"
+ stream-http "^2.7.2"
+ string_decoder "^1.0.0"
+ timers-browserify "^2.0.4"
+ tty-browserify "0.0.0"
+ url "^0.11.0"
+ util "^0.11.0"
+ vm-browserify "^1.0.1"
+
+object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+ integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+os-browserify@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
+ integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
+
+pako@~1.0.5:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
+ integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
+parse-asn1@^5.0.0, parse-asn1@^5.1.5:
+ version "5.1.6"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4"
+ integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==
+ dependencies:
+ asn1.js "^5.2.0"
+ browserify-aes "^1.0.0"
+ evp_bytestokey "^1.0.0"
+ pbkdf2 "^3.0.3"
+ safe-buffer "^5.1.1"
+
+path-browserify@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a"
+ integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==
+
+pbkdf2@^3.0.3:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94"
+ integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==
+ dependencies:
+ create-hash "^1.1.2"
+ create-hmac "^1.1.4"
+ ripemd160 "^2.0.1"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+process-nextick-args@~2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+ integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
+process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+ integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
+
+public-encrypt@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
+ integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==
+ dependencies:
+ bn.js "^4.1.0"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ parse-asn1 "^5.0.0"
+ randombytes "^2.0.1"
+ safe-buffer "^5.1.2"
+
+punycode@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+ integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
+
+punycode@^1.2.4:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+ integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+
+querystring-es3@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+ integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
+
+querystring@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+ integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
+
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+ integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+ dependencies:
+ safe-buffer "^5.1.0"
+
+randomfill@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458"
+ integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==
+ dependencies:
+ randombytes "^2.0.5"
+ safe-buffer "^5.1.0"
+
+react-dom@17.0.1:
+ version "17.0.1"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6"
+ integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ scheduler "^0.20.1"
+
+react@17.0.1:
+ version "17.0.1"
+ resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
+ integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
+readable-stream@^2.0.2, readable-stream@^2.3.3, readable-stream@^2.3.6:
+ version "2.3.7"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
+ integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
+readable-stream@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+ integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+readline-sync@^1.4.7:
+ version "1.4.10"
+ resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b"
+ integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
+ integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safer-buffer@^2.1.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+scheduler@^0.20.1:
+ version "0.20.1"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c"
+ integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
+setimmediate@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+ integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+ version "2.4.11"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
+ integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+shadow-cljs-jar@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b"
+ integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
+
+shadow-cljs@^2.11.18:
+ version "2.11.18"
+ resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.11.18.tgz#f83fe776c9001afdf92611e6bef6da8aed926aab"
+ integrity sha512-7EAXl1xk2GjhViUeexn7cqAPx0lkEl2J40nAbPPCrAbfLfOWn5tjV4P3Be6IqTInSHMx04tFxDRV+0xFdIhl5A==
+ dependencies:
+ node-libs-browser "^2.2.1"
+ readline-sync "^1.4.7"
+ shadow-cljs-jar "1.3.2"
+ source-map-support "^0.4.15"
+ which "^1.3.1"
+ ws "^3.0.0"
+
+source-map-support@^0.4.15:
+ version "0.4.18"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
+ integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==
+ dependencies:
+ source-map "^0.5.6"
+
+source-map@^0.5.6:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+ integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+
+stream-browserify@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
+ integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "^2.0.2"
+
+stream-http@^2.7.2:
+ version "2.8.3"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc"
+ integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==
+ dependencies:
+ builtin-status-codes "^3.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.3.6"
+ to-arraybuffer "^1.0.0"
+ xtend "^4.0.0"
+
+string_decoder@^1.0.0, string_decoder@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
+string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+ integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+timers-browserify@^2.0.4:
+ version "2.0.12"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee"
+ integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==
+ dependencies:
+ setimmediate "^1.0.4"
+
+to-arraybuffer@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+ integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
+
+tty-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+ integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=
+
+ultron@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
+ integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
+
+url@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+ integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
+ dependencies:
+ punycode "1.3.2"
+ querystring "0.2.0"
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util@0.10.3:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+ integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk=
+ dependencies:
+ inherits "2.0.1"
+
+util@^0.11.0:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"
+ integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==
+ dependencies:
+ inherits "2.0.3"
+
+vm-browserify@^1.0.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
+ integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
+
+which@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
+ws@^3.0.0:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
+ integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==
+ dependencies:
+ async-limiter "~1.0.0"
+ safe-buffer "~5.1.0"
+ ultron "~1.1.0"
+
+xtend@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
+ integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==