diff --git a/client/common/src/flybot/client/common/db/event.cljs b/client/common/src/flybot/client/common/db/event.cljs
index 95a6cbc3..a8714458 100644
--- a/client/common/src/flybot/client/common/db/event.cljs
+++ b/client/common/src/flybot/client/common/db/event.cljs
@@ -18,10 +18,14 @@
(rf/reg-event-db
:fx.http/failure
- [(rf/path :app/errors)]
- (fn [errors [_ result]]
+ (fn [db [_ result]]
;; result is a map containing details of the failure
- (assoc errors :failure-http-result result)))
+ (-> db
+ (assoc-in [:app/errors :failure-http-result] result)
+ (assoc :app/notification #:notification{:id (utils/mk-uuid)
+ :type :error
+ :title "HTTP failure"
+ :body result}))))
(rf/reg-event-fx
:fx.http/all-success
@@ -302,13 +306,25 @@
(fn [_ [_ new-options]]
(edn/read-string new-options)))
+;; ------- Notifications ------
+
+(rf/reg-event-fx
+ :evt.app/toast-notify
+ (fn [{:keys [db]} [_ notification]]
+ (let [theme (-> db :app/theme name)]
+ {:fx [[:fx.ui/toast-notify [notification {"theme" theme}]]]})))
+
;; ---------- Errors ----------
(rf/reg-event-db
:evt.error/set-validation-errors
- [(rf/path :app/errors)]
- (fn [errors [_ validation-err]]
- (assoc errors :validation-errors validation-err)))
+ (fn [db [_ validation-err]]
+ (-> db
+ (assoc-in [:app/errors :validation-errors] validation-err)
+ (assoc :app/notification #:notification{:id (utils/mk-uuid)
+ :type :error
+ :title "Validation error"
+ :body validation-err}))))
(rf/reg-event-db
:evt.error/clear-errors
diff --git a/client/common/src/flybot/client/common/db/fx.cljs b/client/common/src/flybot/client/common/db/fx.cljs
index 273de213..1b283cb3 100644
--- a/client/common/src/flybot/client/common/db/fx.cljs
+++ b/client/common/src/flybot/client/common/db/fx.cljs
@@ -1,6 +1,8 @@
(ns flybot.client.common.db.fx
- (:require [re-frame.core :as rf]
- [reitit.frontend.easy :as rfe]))
+ (:require [cljsjs.react-toastify]
+ [re-frame.core :as rf]
+ [reitit.frontend.easy :as rfe]
+ [reagent.core :as r]))
;; ---------- Routing ----------
@@ -13,4 +15,27 @@
(rf/reg-fx
:fx.log/message
(fn [messages]
- (.log js/console (apply str messages))))
\ No newline at end of file
+ (.log js/console (apply str messages))))
+
+;; ---- Toast notifications ----
+
+(rf/reg-fx
+ :fx.ui/toast-notify
+ (fn [[{:notification/keys [type title body]} options]]
+ (let [type-options (case type
+ :info {"type" "info"
+ "autoClose" 10000
+ "pauseOnHover" true}
+ :success {"type" "success"
+ "autoClose" 5000
+ "pauseOnHover" false}
+ :warning {"type" "warning"
+ "autoClose" 10000
+ "pauseOnHover" true}
+ :error {"type" "error"
+ "autoClose" 10000
+ "pauseOnHover" true}
+ {})]
+ (.toast js/ReactToastify
+ (r/as-element [:<> [:strong (str title)] [:p (str body)]])
+ (clj->js (merge type-options options))))))
diff --git a/client/web/src/flybot/client/web/core/dom.cljs b/client/web/src/flybot/client/web/core/dom.cljs
index c1d91e25..d13768f4 100644
--- a/client/web/src/flybot/client/web/core/dom.cljs
+++ b/client/web/src/flybot/client/web/core/dom.cljs
@@ -1,8 +1,9 @@
(ns flybot.client.web.core.dom
- (:require [flybot.client.web.core.dom.header :refer [header-comp]]
- [flybot.client.web.core.dom.page :refer [page]]
+ (:require [flybot.client.web.core.db]
[flybot.client.web.core.dom.footer :refer [footer-comp]]
- [flybot.client.web.core.db]
+ [flybot.client.web.core.dom.header :refer [header-comp]]
+ [flybot.client.web.core.dom.page :refer [page]]
+ [flybot.client.web.core.dom.page.notifications :as notifications]
[re-frame.core :as rf]))
(defn current-page []
@@ -16,4 +17,5 @@
[:div
[header-comp]
[current-page]
- [footer-comp]])
\ No newline at end of file
+ [footer-comp]
+ [notifications/toast-notification-comp]])
diff --git a/client/web/src/flybot/client/web/core/dom/common/error.cljs b/client/web/src/flybot/client/web/core/dom/common/error.cljs
deleted file mode 100644
index df9026e3..00000000
--- a/client/web/src/flybot/client/web/core/dom/common/error.cljs
+++ /dev/null
@@ -1,14 +0,0 @@
-(ns flybot.client.web.core.dom.common.error
- (:require [re-frame.core :as rf]))
-
-(defn errors
- [comp-id err-ids]
- (when @(rf/subscribe [:subs/pattern '{:app/errors ?x}])
- [:div.errors
- {:key comp-id}
- (doall
- (for [id err-ids]
- (when-let [error @(rf/subscribe [:subs/pattern {:app/errors {id '?x}}])]
- [:div.error
- {:key id}
- error])))]))
\ No newline at end of file
diff --git a/client/web/src/flybot/client/web/core/dom/page.cljs b/client/web/src/flybot/client/web/core/dom/page.cljs
index b303f63c..412f9bd9 100644
--- a/client/web/src/flybot/client/web/core/dom/page.cljs
+++ b/client/web/src/flybot/client/web/core/dom/page.cljs
@@ -1,8 +1,9 @@
(ns flybot.client.web.core.dom.page
- (:require [clojure.string :as str]
- [flybot.client.web.core.dom.page.post :as post :refer [blog-post-short page-post]]
+ (:require [cljsjs.react-toastify]
+ [clojure.string :as str]
[flybot.client.web.core.dom.page.admin :refer [admin-panel]]
[flybot.client.web.core.dom.page.options :as page.options]
+ [flybot.client.web.core.dom.page.post :as post :refer [blog-post-short page-post]]
[flybot.client.web.core.utils :as web.utils]
[re-frame.core :as rf]))
diff --git a/client/web/src/flybot/client/web/core/dom/page/admin.cljs b/client/web/src/flybot/client/web/core/dom/page/admin.cljs
index 6888ae1b..414d517b 100644
--- a/client/web/src/flybot/client/web/core/dom/page/admin.cljs
+++ b/client/web/src/flybot/client/web/core/dom/page/admin.cljs
@@ -1,11 +1,8 @@
(ns flybot.client.web.core.dom.page.admin
- (:require [flybot.client.web.core.dom.common.error :refer [errors]]
- [flybot.client.web.core.dom.common.role :as role]
+ (:require [flybot.client.web.core.dom.common.role :as role]
[flybot.client.web.core.dom.common.svg :as svg]
[re-frame.core :as rf]))
-
-
;;---------- Button ----------
(defn submit-role-button
[role]
@@ -42,7 +39,6 @@
[:h1 "Admin"]
(if (role/has-role? :owner)
[:<>
- [errors "admin-page" [:validation-errors :failure-http-result]]
[:div
[:form
[submit-role-button :admin]]
diff --git a/client/web/src/flybot/client/web/core/dom/page/notifications.cljs b/client/web/src/flybot/client/web/core/dom/page/notifications.cljs
new file mode 100644
index 00000000..bbf64c70
--- /dev/null
+++ b/client/web/src/flybot/client/web/core/dom/page/notifications.cljs
@@ -0,0 +1,32 @@
+(ns flybot.client.web.core.dom.page.notifications
+ "Rendering app notifications as pop-up (\"toast\") notifications in the DOM."
+ (:require [cljsjs.react-toastify]
+ [re-frame.core :as rf]))
+
+(def toast-notification-container
+ "A container for pop-up notifications, with default settings.
+
+ This container should be stacked above other HTML elements (either by
+ using the `z-index` property or by declaring it last) so that notifications
+ properly cover them."
+ [:> (-> (.-ToastContainer js/ReactToastify)
+ js->clj
+ (update "defaultProps" #(merge % {"position" "bottom-center"
+ "hideProgressBar" true
+ "newestOnTop" true
+ "pauseOnFocusLoss" false
+ "pauseOnHover" false
+ "closeButton" false}))
+ clj->js)])
+
+(defn toast-notification-subscription
+ []
+ (let [notification @(rf/subscribe [:subs/pattern {:app/notification '?x}])]
+ (when notification
+ (rf/dispatch [:evt.app/toast-notify notification]))))
+
+(defn toast-notification-comp
+ []
+ [:<>
+ [toast-notification-subscription]
+ toast-notification-container])
diff --git a/client/web/src/flybot/client/web/core/dom/page/post.cljs b/client/web/src/flybot/client/web/core/dom/page/post.cljs
index 022daa4f..fd6695d5 100644
--- a/client/web/src/flybot/client/web/core/dom/page/post.cljs
+++ b/client/web/src/flybot/client/web/core/dom/page/post.cljs
@@ -1,6 +1,5 @@
(ns flybot.client.web.core.dom.page.post
(:require [clojure.walk :as walk]
- [flybot.client.web.core.dom.common.error :refer [errors]]
[flybot.client.web.core.dom.common.link :as link]
[flybot.client.web.core.dom.common.role :as role]
[flybot.client.web.core.dom.common.svg :as svg]
@@ -270,8 +269,7 @@
[submit-button]
[edit-button id]
(when-not (utils/temporary-id? id)
- [trash-button])])
- [errors id [:validation-errors :failure-http-result]]]
+ [trash-button])])]
(when @(rf/subscribe [:subs/pattern '{:form/fields {:post/to-delete? ?x}}])
[delete-form id])
(if (= :preview @(rf/subscribe [:subs/pattern '{:form/fields {:post/view ?x}}]))
diff --git a/client/web/test/flybot/client/web/core/db_test.cljs b/client/web/test/flybot/client/web/core/db_test.cljs
index f47b1d4b..14f780e7 100644
--- a/client/web/test/flybot/client/web/core/db_test.cljs
+++ b/client/web/test/flybot/client/web/core/db_test.cljs
@@ -41,7 +41,9 @@
user (rf/subscribe [:subs/pattern '{:app/user ?x}])
navbar-open? (rf/subscribe [:subs/pattern '{:nav/navbar-open? ?x}])
posts (rf/subscribe [:subs.post/posts :home])
- http-error (rf/subscribe [:subs/pattern '{:app/errors {:failure-http-result ?x}}])]
+ http-error (rf/subscribe [:subs/pattern '{:app/errors {:failure-http-result ?x}}])
+ http-error-notification
+ (rf/subscribe [:subs/pattern {:app/notification '?x}])]
;;---------- SERVER ERROR
;; Mock failure http request
(rf/reg-fx :http-xhrio
@@ -50,7 +52,11 @@
;; Initialize db
(rf/dispatch [:evt.app/initialize])
(testing "Initital db state is accurate in case of server error."
- (is (= "ERROR-SERVER" @http-error)))
+ (is (= "ERROR-SERVER" @http-error))
+ (is (= #:notification{:type :error
+ :body "ERROR-SERVER"}
+ (select-keys @http-error-notification [:notification/type
+ :notification/body]))))
;;---------- SUCCESS
;; Mock success http request
@@ -125,6 +131,8 @@
posts (rf/subscribe [:subs.post/posts :home])
errors (rf/subscribe [:subs/pattern '{:app/errors ?x}])
validation-error (rf/subscribe [:subs/pattern '{:app/errors {:validation-errors ?x}}])
+ validation-error-notification
+ (rf/subscribe [:subs/pattern {:app/notification '?x}])
new-post-1 (assoc s/post-1
:post/md-content "#New Content 1"
:post/last-edit-date (utils/mk-date))]
@@ -156,7 +164,8 @@
;; Send post but validation error
(rf/dispatch [:evt.post.form/send-post])
(testing "Validation error added to db."
- (is @validation-error))
+ (is @validation-error)
+ (is @validation-error-notification))
;;---------- SEND POST SUCCESS
;; Mock success http request
@@ -260,4 +269,4 @@
{:posts {:removed-post {:post/id s/post-1-id}}}])))
(rf/dispatch [:evt.post/remove-post s/post-1-id])
(testing "Post got removed."
- (is (= [] @posts))))))
\ No newline at end of file
+ (is (= [] @posts))))))
diff --git a/deps.edn b/deps.edn
index d24f5ca1..783a6992 100644
--- a/deps.edn
+++ b/deps.edn
@@ -38,6 +38,7 @@
reagent/reagent {:mvn/version "1.2.0"}
cljsjs/react {:mvn/version "18.2.0-1"}
cljsjs/react-dom {:mvn/version "18.2.0-1"}
+ cljsjs/react-toastify {:mvn/version "9.1.0-0"}
cljs-ajax/cljs-ajax {:mvn/version "0.8.4"}
re-frame/re-frame {:mvn/version "1.3.0"}
day8.re-frame/http-fx {:mvn/version "0.2.4"}
diff --git a/resources/public/css/ReactToastify.css b/resources/public/css/ReactToastify.css
new file mode 100644
index 00000000..69232be6
--- /dev/null
+++ b/resources/public/css/ReactToastify.css
@@ -0,0 +1,621 @@
+/* From https://www.npmjs.com/package/react-toastify by fkhadra */
+
+:root {
+ --toastify-color-light: #fff;
+ --toastify-color-dark: #121212;
+ --toastify-color-info: #3498db;
+ --toastify-color-success: #07bc0c;
+ --toastify-color-warning: #f1c40f;
+ --toastify-color-error: #e74c3c;
+ --toastify-color-transparent: rgba(255, 255, 255, 0.7);
+ --toastify-icon-color-info: var(--toastify-color-info);
+ --toastify-icon-color-success: var(--toastify-color-success);
+ --toastify-icon-color-warning: var(--toastify-color-warning);
+ --toastify-icon-color-error: var(--toastify-color-error);
+ --toastify-toast-width: 480px;
+ --toastify-toast-background: #fff;
+ --toastify-toast-min-height: 64px;
+ --toastify-toast-max-height: 800px;
+ --toastify-font-family: inherit;
+ --toastify-z-index: 9999;
+ --toastify-text-color-light: #757575;
+ --toastify-text-color-dark: #fff;
+ --toastify-text-color-info: #fff;
+ --toastify-text-color-success: #fff;
+ --toastify-text-color-warning: #fff;
+ --toastify-text-color-error: #fff;
+ --toastify-spinner-color: #616161;
+ --toastify-spinner-color-empty-area: #e0e0e0;
+ --toastify-color-progress-light: linear-gradient(
+ to right,
+ #4cd964,
+ #5ac8fa,
+ #007aff,
+ #34aadc,
+ #5856d6,
+ #ff2d55
+ );
+ --toastify-color-progress-dark: #bb86fc;
+ --toastify-color-progress-info: var(--toastify-color-info);
+ --toastify-color-progress-success: var(--toastify-color-success);
+ --toastify-color-progress-warning: var(--toastify-color-warning);
+ --toastify-color-progress-error: var(--toastify-color-error);
+}
+
+.Toastify__toast-container {
+ z-index: var(--toastify-z-index);
+ -webkit-transform: translate3d(0, 0, var(--toastify-z-index));
+ position: fixed;
+ padding: 4px;
+ width: var(--toastify-toast-width);
+ box-sizing: border-box;
+ color: #fff;
+}
+.Toastify__toast-container--top-left {
+ top: 1em;
+ left: 1em;
+}
+.Toastify__toast-container--top-center {
+ top: 1em;
+ left: 50%;
+ transform: translateX(-50%);
+}
+.Toastify__toast-container--top-right {
+ top: 1em;
+ right: 1em;
+}
+.Toastify__toast-container--bottom-left {
+ bottom: 1em;
+ left: 1em;
+}
+.Toastify__toast-container--bottom-center {
+ bottom: 1em;
+ left: 50%;
+ transform: translateX(-50%);
+}
+.Toastify__toast-container--bottom-right {
+ bottom: 1em;
+ right: 1em;
+}
+
+@media only screen and (max-width : 480px) {
+ .Toastify__toast-container {
+ width: 100vw;
+ padding: 0;
+ left: 0;
+ margin: 0;
+ }
+ .Toastify__toast-container--top-left, .Toastify__toast-container--top-center, .Toastify__toast-container--top-right {
+ top: 0;
+ transform: translateX(0);
+ }
+ .Toastify__toast-container--bottom-left, .Toastify__toast-container--bottom-center, .Toastify__toast-container--bottom-right {
+ bottom: 0;
+ transform: translateX(0);
+ }
+ .Toastify__toast-container--rtl {
+ right: 0;
+ left: initial;
+ }
+}
+.Toastify__toast {
+ position: relative;
+ min-height: var(--toastify-toast-min-height);
+ box-sizing: border-box;
+ margin-bottom: 1rem;
+ padding: 8px;
+ border-radius: 4px;
+ box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 0.1), 0 2px 15px 0 rgba(0, 0, 0, 0.05);
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-pack: justify;
+ justify-content: space-between;
+ max-height: var(--toastify-toast-max-height);
+ overflow: hidden;
+ font-family: var(--toastify-font-family);
+ cursor: default;
+ direction: ltr;
+ /* webkit only issue #791 */
+ z-index: 0;
+}
+.Toastify__toast--rtl {
+ direction: rtl;
+}
+.Toastify__toast--close-on-click {
+ cursor: pointer;
+}
+.Toastify__toast-body {
+ margin: auto 0;
+ -ms-flex: 1 1 auto;
+ flex: 1 1 auto;
+ padding: 6px;
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-align: center;
+ align-items: center;
+}
+.Toastify__toast-body > div:last-child {
+ word-break: break-word;
+ -ms-flex: 1;
+ flex: 1;
+}
+.Toastify__toast-icon {
+ -webkit-margin-end: 10px;
+ margin-inline-end: 10px;
+ width: 32px;
+ -ms-flex-negative: 0;
+ flex-shrink: 0;
+ display: -ms-flexbox;
+ display: flex;
+}
+
+.Toastify--animate {
+ animation-fill-mode: both;
+ animation-duration: 0.7s;
+}
+
+.Toastify--animate-icon {
+ animation-fill-mode: both;
+ animation-duration: 0.3s;
+}
+
+@media only screen and (max-width : 480px) {
+ .Toastify__toast {
+ margin-bottom: 0;
+ border-radius: 0;
+ }
+}
+.Toastify__toast-theme--dark {
+ background: var(--toastify-color-dark);
+ color: var(--toastify-text-color-dark);
+}
+.Toastify__toast-theme--light {
+ background: var(--toastify-color-light);
+ color: var(--toastify-text-color-light);
+}
+.Toastify__toast-theme--colored.Toastify__toast--default {
+ background: var(--toastify-color-light);
+ color: var(--toastify-text-color-light);
+}
+.Toastify__toast-theme--colored.Toastify__toast--info {
+ color: var(--toastify-text-color-info);
+ background: var(--toastify-color-info);
+}
+.Toastify__toast-theme--colored.Toastify__toast--success {
+ color: var(--toastify-text-color-success);
+ background: var(--toastify-color-success);
+}
+.Toastify__toast-theme--colored.Toastify__toast--warning {
+ color: var(--toastify-text-color-warning);
+ background: var(--toastify-color-warning);
+}
+.Toastify__toast-theme--colored.Toastify__toast--error {
+ color: var(--toastify-text-color-error);
+ background: var(--toastify-color-error);
+}
+
+.Toastify__progress-bar-theme--light {
+ background: var(--toastify-color-progress-light);
+}
+.Toastify__progress-bar-theme--dark {
+ background: var(--toastify-color-progress-dark);
+}
+.Toastify__progress-bar--info {
+ background: var(--toastify-color-progress-info);
+}
+.Toastify__progress-bar--success {
+ background: var(--toastify-color-progress-success);
+}
+.Toastify__progress-bar--warning {
+ background: var(--toastify-color-progress-warning);
+}
+.Toastify__progress-bar--error {
+ background: var(--toastify-color-progress-error);
+}
+.Toastify__progress-bar-theme--colored.Toastify__progress-bar--info, .Toastify__progress-bar-theme--colored.Toastify__progress-bar--success, .Toastify__progress-bar-theme--colored.Toastify__progress-bar--warning, .Toastify__progress-bar-theme--colored.Toastify__progress-bar--error {
+ background: var(--toastify-color-transparent);
+}
+
+.Toastify__close-button {
+ color: #fff;
+ background: transparent;
+ outline: none;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+ opacity: 0.7;
+ transition: 0.3s ease;
+ -ms-flex-item-align: start;
+ align-self: flex-start;
+}
+.Toastify__close-button--light {
+ color: #000;
+ opacity: 0.3;
+}
+.Toastify__close-button > svg {
+ fill: currentColor;
+ height: 16px;
+ width: 14px;
+}
+.Toastify__close-button:hover, .Toastify__close-button:focus {
+ opacity: 1;
+}
+
+@keyframes Toastify__trackProgress {
+ 0% {
+ transform: scaleX(1);
+ }
+ 100% {
+ transform: scaleX(0);
+ }
+}
+.Toastify__progress-bar {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 5px;
+ z-index: var(--toastify-z-index);
+ opacity: 0.7;
+ transform-origin: left;
+}
+.Toastify__progress-bar--animated {
+ animation: Toastify__trackProgress linear 1 forwards;
+}
+.Toastify__progress-bar--controlled {
+ transition: transform 0.2s;
+}
+.Toastify__progress-bar--rtl {
+ right: 0;
+ left: initial;
+ transform-origin: right;
+}
+
+.Toastify__spinner {
+ width: 20px;
+ height: 20px;
+ box-sizing: border-box;
+ border: 2px solid;
+ border-radius: 100%;
+ border-color: var(--toastify-spinner-color-empty-area);
+ border-right-color: var(--toastify-spinner-color);
+ animation: Toastify__spin 0.65s linear infinite;
+}
+
+@keyframes Toastify__bounceInRight {
+ from, 60%, 75%, 90%, to {
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+ from {
+ opacity: 0;
+ transform: translate3d(3000px, 0, 0);
+ }
+ 60% {
+ opacity: 1;
+ transform: translate3d(-25px, 0, 0);
+ }
+ 75% {
+ transform: translate3d(10px, 0, 0);
+ }
+ 90% {
+ transform: translate3d(-5px, 0, 0);
+ }
+ to {
+ transform: none;
+ }
+}
+@keyframes Toastify__bounceOutRight {
+ 20% {
+ opacity: 1;
+ transform: translate3d(-20px, 0, 0);
+ }
+ to {
+ opacity: 0;
+ transform: translate3d(2000px, 0, 0);
+ }
+}
+@keyframes Toastify__bounceInLeft {
+ from, 60%, 75%, 90%, to {
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+ 0% {
+ opacity: 0;
+ transform: translate3d(-3000px, 0, 0);
+ }
+ 60% {
+ opacity: 1;
+ transform: translate3d(25px, 0, 0);
+ }
+ 75% {
+ transform: translate3d(-10px, 0, 0);
+ }
+ 90% {
+ transform: translate3d(5px, 0, 0);
+ }
+ to {
+ transform: none;
+ }
+}
+@keyframes Toastify__bounceOutLeft {
+ 20% {
+ opacity: 1;
+ transform: translate3d(20px, 0, 0);
+ }
+ to {
+ opacity: 0;
+ transform: translate3d(-2000px, 0, 0);
+ }
+}
+@keyframes Toastify__bounceInUp {
+ from, 60%, 75%, 90%, to {
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+ from {
+ opacity: 0;
+ transform: translate3d(0, 3000px, 0);
+ }
+ 60% {
+ opacity: 1;
+ transform: translate3d(0, -20px, 0);
+ }
+ 75% {
+ transform: translate3d(0, 10px, 0);
+ }
+ 90% {
+ transform: translate3d(0, -5px, 0);
+ }
+ to {
+ transform: translate3d(0, 0, 0);
+ }
+}
+@keyframes Toastify__bounceOutUp {
+ 20% {
+ transform: translate3d(0, -10px, 0);
+ }
+ 40%, 45% {
+ opacity: 1;
+ transform: translate3d(0, 20px, 0);
+ }
+ to {
+ opacity: 0;
+ transform: translate3d(0, -2000px, 0);
+ }
+}
+@keyframes Toastify__bounceInDown {
+ from, 60%, 75%, 90%, to {
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+ 0% {
+ opacity: 0;
+ transform: translate3d(0, -3000px, 0);
+ }
+ 60% {
+ opacity: 1;
+ transform: translate3d(0, 25px, 0);
+ }
+ 75% {
+ transform: translate3d(0, -10px, 0);
+ }
+ 90% {
+ transform: translate3d(0, 5px, 0);
+ }
+ to {
+ transform: none;
+ }
+}
+@keyframes Toastify__bounceOutDown {
+ 20% {
+ transform: translate3d(0, 10px, 0);
+ }
+ 40%, 45% {
+ opacity: 1;
+ transform: translate3d(0, -20px, 0);
+ }
+ to {
+ opacity: 0;
+ transform: translate3d(0, 2000px, 0);
+ }
+}
+.Toastify__bounce-enter--top-left, .Toastify__bounce-enter--bottom-left {
+ animation-name: Toastify__bounceInLeft;
+}
+.Toastify__bounce-enter--top-right, .Toastify__bounce-enter--bottom-right {
+ animation-name: Toastify__bounceInRight;
+}
+.Toastify__bounce-enter--top-center {
+ animation-name: Toastify__bounceInDown;
+}
+.Toastify__bounce-enter--bottom-center {
+ animation-name: Toastify__bounceInUp;
+}
+
+.Toastify__bounce-exit--top-left, .Toastify__bounce-exit--bottom-left {
+ animation-name: Toastify__bounceOutLeft;
+}
+.Toastify__bounce-exit--top-right, .Toastify__bounce-exit--bottom-right {
+ animation-name: Toastify__bounceOutRight;
+}
+.Toastify__bounce-exit--top-center {
+ animation-name: Toastify__bounceOutUp;
+}
+.Toastify__bounce-exit--bottom-center {
+ animation-name: Toastify__bounceOutDown;
+}
+
+@keyframes Toastify__zoomIn {
+ from {
+ opacity: 0;
+ transform: scale3d(0.3, 0.3, 0.3);
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+@keyframes Toastify__zoomOut {
+ from {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0;
+ transform: scale3d(0.3, 0.3, 0.3);
+ }
+ to {
+ opacity: 0;
+ }
+}
+.Toastify__zoom-enter {
+ animation-name: Toastify__zoomIn;
+}
+
+.Toastify__zoom-exit {
+ animation-name: Toastify__zoomOut;
+}
+
+@keyframes Toastify__flipIn {
+ from {
+ transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ animation-timing-function: ease-in;
+ opacity: 0;
+ }
+ 40% {
+ transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ animation-timing-function: ease-in;
+ }
+ 60% {
+ transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
+ opacity: 1;
+ }
+ 80% {
+ transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
+ }
+ to {
+ transform: perspective(400px);
+ }
+}
+@keyframes Toastify__flipOut {
+ from {
+ transform: perspective(400px);
+ }
+ 30% {
+ transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+ opacity: 1;
+ }
+ to {
+ transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+ opacity: 0;
+ }
+}
+.Toastify__flip-enter {
+ animation-name: Toastify__flipIn;
+}
+
+.Toastify__flip-exit {
+ animation-name: Toastify__flipOut;
+}
+
+@keyframes Toastify__slideInRight {
+ from {
+ transform: translate3d(110%, 0, 0);
+ visibility: visible;
+ }
+ to {
+ transform: translate3d(0, 0, 0);
+ }
+}
+@keyframes Toastify__slideInLeft {
+ from {
+ transform: translate3d(-110%, 0, 0);
+ visibility: visible;
+ }
+ to {
+ transform: translate3d(0, 0, 0);
+ }
+}
+@keyframes Toastify__slideInUp {
+ from {
+ transform: translate3d(0, 110%, 0);
+ visibility: visible;
+ }
+ to {
+ transform: translate3d(0, 0, 0);
+ }
+}
+@keyframes Toastify__slideInDown {
+ from {
+ transform: translate3d(0, -110%, 0);
+ visibility: visible;
+ }
+ to {
+ transform: translate3d(0, 0, 0);
+ }
+}
+@keyframes Toastify__slideOutRight {
+ from {
+ transform: translate3d(0, 0, 0);
+ }
+ to {
+ visibility: hidden;
+ transform: translate3d(110%, 0, 0);
+ }
+}
+@keyframes Toastify__slideOutLeft {
+ from {
+ transform: translate3d(0, 0, 0);
+ }
+ to {
+ visibility: hidden;
+ transform: translate3d(-110%, 0, 0);
+ }
+}
+@keyframes Toastify__slideOutDown {
+ from {
+ transform: translate3d(0, 0, 0);
+ }
+ to {
+ visibility: hidden;
+ transform: translate3d(0, 500px, 0);
+ }
+}
+@keyframes Toastify__slideOutUp {
+ from {
+ transform: translate3d(0, 0, 0);
+ }
+ to {
+ visibility: hidden;
+ transform: translate3d(0, -500px, 0);
+ }
+}
+.Toastify__slide-enter--top-left, .Toastify__slide-enter--bottom-left {
+ animation-name: Toastify__slideInLeft;
+}
+.Toastify__slide-enter--top-right, .Toastify__slide-enter--bottom-right {
+ animation-name: Toastify__slideInRight;
+}
+.Toastify__slide-enter--top-center {
+ animation-name: Toastify__slideInDown;
+}
+.Toastify__slide-enter--bottom-center {
+ animation-name: Toastify__slideInUp;
+}
+
+.Toastify__slide-exit--top-left, .Toastify__slide-exit--bottom-left {
+ animation-name: Toastify__slideOutLeft;
+}
+.Toastify__slide-exit--top-right, .Toastify__slide-exit--bottom-right {
+ animation-name: Toastify__slideOutRight;
+}
+.Toastify__slide-exit--top-center {
+ animation-name: Toastify__slideOutUp;
+}
+.Toastify__slide-exit--bottom-center {
+ animation-name: Toastify__slideOutDown;
+}
+
+@keyframes Toastify__spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/resources/public/css/style.css b/resources/public/css/style.css
index 62117044..682d5524 100644
--- a/resources/public/css/style.css
+++ b/resources/public/css/style.css
@@ -287,25 +287,31 @@ input[type=button] {
}
.burger {
- fill: var(--text-primary-color);
width: 32px;
height: 32px;
}
svg {
- fill: var(--link-primary-color);
width: 250px;
}
+svg:not(.Toastify *) {
+ fill: var(--link-primary-color);
+}
+
svg:hover {
fill: var(--text-secondary-color);
}
-.done, .plus {
+svg.burger {
+ fill: var(--text-primary-color);
+}
+
+svg.done, svg.plus {
fill: var(--border-primary-color);
}
-.close, .trash {
+svg.close, svg.trash {
fill: var(--error-primary-color);
}
diff --git a/resources/public/index.html b/resources/public/index.html
index 96c564ed..fd44bdb5 100644
--- a/resources/public/index.html
+++ b/resources/public/index.html
@@ -33,6 +33,7 @@
+