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 @@ +