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==