mirror of
https://github.com/penpot/penpot.git
synced 2025-06-06 09:31:38 +02:00
🎉 New oops page with login and request access
This commit is contained in:
parent
d2311f066a
commit
6169f5c2e8
46 changed files with 4117 additions and 134 deletions
|
@ -155,3 +155,18 @@
|
|||
:files files
|
||||
:binary? binary?}))))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Team Request
|
||||
;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn create-team-access-request
|
||||
[params]
|
||||
(ptk/reify ::create-team-access-request
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)]
|
||||
(->> (rp/cmd! :create-team-access-request params)
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
|
|
@ -153,13 +153,8 @@
|
|||
accepting invitation, or third party auth signup or singin."
|
||||
[profile]
|
||||
(letfn [(get-redirect-event []
|
||||
(let [team-id (get-current-team-id profile)
|
||||
redirect-url (:redirect-url @storage)]
|
||||
(if (some? redirect-url)
|
||||
(do
|
||||
(swap! storage dissoc :redirect-url)
|
||||
(.replace js/location redirect-url))
|
||||
(rt/nav' :dashboard-projects {:team-id team-id}))))]
|
||||
(let [team-id (get-current-team-id profile)]
|
||||
(rt/nav' :dashboard-projects {:team-id team-id})))]
|
||||
|
||||
(ptk/reify ::logged-in
|
||||
ev/Event
|
||||
|
@ -316,7 +311,6 @@
|
|||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
;; We prefer to keek some stuff in the storage like the current-team-id and the profile
|
||||
(swap! storage dissoc :redirect-url)
|
||||
(set-current-team! nil)))))
|
||||
|
||||
(defn logout
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
[app.util.globals :as glob]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.timers :as ts]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
@ -96,16 +95,23 @@
|
|||
(print-trace! error)
|
||||
(print-data! error))))
|
||||
|
||||
;; We receive a explicit authentication error; this explicitly clears
|
||||
;; We receive a explicit authentication error;
|
||||
;; If the uri is for workspace, dashboard or view assign the
|
||||
;; exception for the 'Oops' page. Otherwise this explicitly clears
|
||||
;; all profile data and redirect the user to the login page. This is
|
||||
;; here and not in app.main.errors because of circular dependency.
|
||||
(defmethod ptk/handle-error :authentication
|
||||
[_]
|
||||
(let [msg (tr "errors.auth.unable-to-login")
|
||||
uri (. (. js/document -location) -href)]
|
||||
(st/emit! (du/logout {:capture-redirect true}))
|
||||
(ts/schedule 500 #(st/emit! (ntf/warn msg)))
|
||||
(ts/schedule 1000 #(swap! storage assoc :redirect-url uri))))
|
||||
[e]
|
||||
(let [msg (tr "errors.auth.unable-to-login")
|
||||
uri (.-href glob/location)
|
||||
show-oops? (or (str/includes? uri "workspace")
|
||||
(str/includes? uri "dashboard")
|
||||
(str/includes? uri "view"))]
|
||||
(if show-oops?
|
||||
(st/async-emit! (rt/assign-exception e))
|
||||
(do
|
||||
(st/emit! (du/logout {:capture-redirect true}))
|
||||
(ts/schedule 500 #(st/emit! (ntf/warn msg)))))))
|
||||
|
||||
;; Error that happens on an active business model validation does not
|
||||
;; passes an validation (example: profile can't leave a team). From
|
||||
|
|
|
@ -133,7 +133,7 @@
|
|||
[:& dashboard-page {:route route :profile profile}]]
|
||||
:viewer
|
||||
(let [{:keys [query-params path-params]} route
|
||||
{:keys [index share-id section page-id interactions-mode frame-id]
|
||||
{:keys [index share-id section page-id interactions-mode frame-id share]
|
||||
:or {section :interactions interactions-mode :show-on-click}} query-params
|
||||
{:keys [file-id]} path-params]
|
||||
[:? {}
|
||||
|
@ -154,7 +154,8 @@
|
|||
:hide false
|
||||
:show true
|
||||
:show-on-click false)
|
||||
:frame-id frame-id}])])
|
||||
:frame-id frame-id
|
||||
:share share}])])
|
||||
|
||||
:workspace
|
||||
(let [project-id (some-> params :path :project-id uuid)
|
||||
|
|
|
@ -14,31 +14,12 @@
|
|||
[app.main.ui.auth.login :refer [login-page]]
|
||||
[app.main.ui.auth.recovery :refer [recovery-page]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page]]
|
||||
[app.main.ui.auth.register :refer [register-page register-success-page register-validate-page]]
|
||||
[app.main.ui.auth.register :refer [register-page register-success-page register-validate-page terms-register]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc terms-login
|
||||
[]
|
||||
(let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri)
|
||||
show-terms? (some? cf/terms-of-service-uri)
|
||||
show-privacy? (some? cf/privacy-policy-uri)]
|
||||
|
||||
(when show-all?
|
||||
[:div {:class (stl/css :terms-login)}
|
||||
(when show-terms?
|
||||
[:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :auth-link)}
|
||||
(tr "auth.terms-of-service")])
|
||||
|
||||
(when show-all?
|
||||
[:span {:class (stl/css :and-text)}
|
||||
(dm/str " " (tr "labels.and") " ")])
|
||||
|
||||
(when show-privacy?
|
||||
[:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :auth-link)}
|
||||
(tr "auth.privacy-policy")])])))
|
||||
|
||||
(mf/defc auth
|
||||
{::mf/props :obj}
|
||||
|
@ -90,4 +71,4 @@
|
|||
[:& recovery-page {:params params}])
|
||||
|
||||
(when (= section :auth-register)
|
||||
[:& terms-login])]]))
|
||||
[:& terms-register])]]))
|
||||
|
|
|
@ -103,23 +103,3 @@
|
|||
fill: var(--main-icon-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.terms-login {
|
||||
@include bodySmallTypography;
|
||||
display: flex;
|
||||
gap: $s-4;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.and-text {
|
||||
border-bottom: $s-1 solid transparent;
|
||||
color: var(--title-foreground-color);
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
color: var(--link-foreground-color);
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
[:string {:min 1}]]])
|
||||
|
||||
(mf/defc login-form
|
||||
[{:keys [params on-success-callback origin] :as props}]
|
||||
[{:keys [params on-success-callback on-recovery-request origin] :as props}]
|
||||
(let [initial (mf/with-memo [params] params)
|
||||
error (mf/use-state false)
|
||||
form (fm/use-form :schema schema:login-form
|
||||
|
@ -139,9 +139,12 @@
|
|||
:on-success on-success})]
|
||||
(st/emit! (du/login-with-ldap params)))))
|
||||
|
||||
on-recovery-request
|
||||
default-recovery-req
|
||||
(mf/use-fn
|
||||
#(st/emit! (rt/nav :auth-recovery-request)))]
|
||||
#(st/emit! (rt/nav :auth-recovery-request)))
|
||||
|
||||
on-recovery-request (or on-recovery-request
|
||||
default-recovery-req)]
|
||||
|
||||
[:*
|
||||
(when-let [message @error]
|
||||
|
@ -243,7 +246,7 @@
|
|||
(tr "auth.login-with-oidc-submit")])))
|
||||
|
||||
(mf/defc login-methods
|
||||
[{:keys [params on-success-callback origin] :as props}]
|
||||
[{:keys [params on-success-callback on-recovery-request origin] :as props}]
|
||||
[:*
|
||||
(when show-alt-login-buttons?
|
||||
[:*
|
||||
|
@ -257,7 +260,7 @@
|
|||
(when (or (contains? cf/flags :login)
|
||||
(contains? cf/flags :login-with-password)
|
||||
(contains? cf/flags :login-with-ldap))
|
||||
[:& login-form {:params params :on-success-callback on-success-callback :origin origin}])])
|
||||
[:& login-form {:params params :on-success-callback on-success-callback :on-recovery-request on-recovery-request :origin origin}])])
|
||||
|
||||
(mf/defc login-page
|
||||
[{:keys [params] :as props}]
|
||||
|
|
|
@ -102,3 +102,16 @@
|
|||
:class (stl/css :go-back-link)
|
||||
:data-testid "go-back-link"}
|
||||
(tr "labels.go-back")]]]))
|
||||
|
||||
|
||||
(mf/defc recovery-sent-page
|
||||
{::mf/props :obj}
|
||||
[{:keys [email]}]
|
||||
[:div {:class (stl/css :auth-form-wrapper :register-success)}
|
||||
[:div {:class (stl/css :auth-title-wrapper)}
|
||||
[:h2 {:class (stl/css :auth-title)}
|
||||
(tr "auth.check-mail")]
|
||||
[:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery")]]
|
||||
[:div {:class (stl/css :notification-text-email)} email]
|
||||
[:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery-check")]])
|
||||
|
||||
|
|
|
@ -10,3 +10,10 @@
|
|||
.fields-row {
|
||||
margin-bottom: $s-8;
|
||||
}
|
||||
|
||||
.notification-text-email {
|
||||
@include medTitleTipography;
|
||||
font-size: $fs-20;
|
||||
color: var(--register-confirmation-color);
|
||||
margin-inline: $s-36;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.main.ui.auth.register
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.notifications :as ntf]
|
||||
|
@ -103,11 +104,12 @@
|
|||
|
||||
(mf/defc register-methods
|
||||
{::mf/props :obj}
|
||||
[{:keys [params on-success-callback]}]
|
||||
[{:keys [params hide-separator on-success-callback]}]
|
||||
[:*
|
||||
(when login/show-alt-login-buttons?
|
||||
[:& login/login-buttons {:params params}])
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
(when (or login/show-alt-login-buttons? (false? hide-separator))
|
||||
[:hr {:class (stl/css :separator)}])
|
||||
[:& register-form {:params params :on-success-callback on-success-callback}]])
|
||||
|
||||
(mf/defc register-page
|
||||
|
@ -251,14 +253,37 @@
|
|||
|
||||
(mf/defc register-success-page
|
||||
{::mf/props :obj}
|
||||
[]
|
||||
(let [email (::email @sto/storage)]
|
||||
[{:keys [params]}]
|
||||
(let [email (or (:email params) (::email @sto/storage))]
|
||||
[:div {:class (stl/css :auth-form-wrapper :register-success)}
|
||||
[:h1 {:class (stl/css :logo-container)}
|
||||
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
|
||||
(when-not (:hide-logo params)
|
||||
[:h1 {:class (stl/css :logo-container)}
|
||||
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]])
|
||||
[:div {:class (stl/css :auth-title-wrapper)}
|
||||
[:h2 {:class (stl/css :auth-title)}
|
||||
(tr "auth.check-mail")]
|
||||
[:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]]
|
||||
[:div {:class (stl/css :notification-text-email)} email]
|
||||
[:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]]))
|
||||
|
||||
|
||||
(mf/defc terms-register
|
||||
[]
|
||||
(let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri)
|
||||
show-terms? (some? cf/terms-of-service-uri)
|
||||
show-privacy? (some? cf/privacy-policy-uri)]
|
||||
|
||||
(when show-all?
|
||||
[:div {:class (stl/css :terms-register)}
|
||||
(when show-terms?
|
||||
[:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :auth-link)}
|
||||
(tr "auth.terms-of-service")])
|
||||
|
||||
(when show-all?
|
||||
[:span {:class (stl/css :and-text)}
|
||||
(dm/str " " (tr "labels.and") " ")])
|
||||
|
||||
(when show-privacy?
|
||||
[:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :auth-link)}
|
||||
(tr "auth.privacy-policy")])])))
|
||||
|
||||
|
|
|
@ -66,3 +66,23 @@
|
|||
width: $s-120;
|
||||
margin-block-end: $s-24;
|
||||
}
|
||||
|
||||
.terms-register {
|
||||
@include bodySmallTypography;
|
||||
display: flex;
|
||||
gap: $s-4;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.and-text {
|
||||
border-bottom: $s-1 solid transparent;
|
||||
color: var(--title-foreground-color);
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
color: var(--link-foreground-color);
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -420,7 +420,7 @@
|
|||
(into [] (distinct) (conj coll item)))
|
||||
|
||||
(mf/defc multi-input
|
||||
[{:keys [form label class name trim valid-item-fn caution-item-fn on-submit] :as props}]
|
||||
[{:keys [form label class name trim valid-item-fn caution-item-fn on-submit invite-email] :as props}]
|
||||
(let [form (or form (mf/use-ctx form-ctx))
|
||||
input-name (get props :name)
|
||||
touched? (get-in @form [:touched input-name])
|
||||
|
@ -528,6 +528,12 @@
|
|||
values (filterv #(:valid %) values)]
|
||||
(update-form! values)))
|
||||
|
||||
(mf/with-effect []
|
||||
(when invite-email
|
||||
(swap! items conj-dedup {:text (str/trim invite-email)
|
||||
:valid (valid-item-fn invite-email)
|
||||
:caution (caution-item-fn invite-email)})))
|
||||
|
||||
[:div {:class klass}
|
||||
[:input {:id (name input-name)
|
||||
:class in-klass
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
(assoc :project-id (uuid project-id)))))
|
||||
|
||||
(mf/defc dashboard-content
|
||||
[{:keys [team projects project section search-term profile] :as props}]
|
||||
[{:keys [team projects project section search-term profile invite-email] :as props}]
|
||||
(let [container (mf/use-ref)
|
||||
content-width (mf/use-state 0)
|
||||
project-id (:id project)
|
||||
|
@ -129,7 +129,7 @@
|
|||
[:& libraries-page {:team team}]
|
||||
|
||||
:dashboard-team-members
|
||||
[:& team-members-page {:team team :profile profile}]
|
||||
[:& team-members-page {:team team :profile profile :invite-email invite-email}]
|
||||
|
||||
:dashboard-team-invitations
|
||||
[:& team-invitations-page {:team team}]
|
||||
|
@ -153,6 +153,7 @@
|
|||
project-id (:project-id params)
|
||||
team-id (:team-id params)
|
||||
search-term (:search-term params)
|
||||
invite-email (-> route :query-params :invite-email)
|
||||
|
||||
teams (mf/deref refs/teams)
|
||||
team (get teams team-id)
|
||||
|
@ -204,5 +205,6 @@
|
|||
:project project
|
||||
:section section
|
||||
:search-term search-term
|
||||
:team team}])])]]))
|
||||
:team team
|
||||
:invite-email invite-email}])])]]))
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
(mf/defc header
|
||||
{::mf/wrap [mf/memo]
|
||||
::mf/wrap-props false}
|
||||
[{:keys [section team]}]
|
||||
[{:keys [section team invite-email]}]
|
||||
(let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members)))
|
||||
on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings)))
|
||||
on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations)))
|
||||
|
@ -79,7 +79,12 @@
|
|||
(fn []
|
||||
(st/emit! (modal/show {:type :invite-members
|
||||
:team team
|
||||
:origin :team}))))]
|
||||
:origin :team
|
||||
:invite-email invite-email}))))]
|
||||
|
||||
(mf/with-effect []
|
||||
(when invite-email
|
||||
(on-invite-member)))
|
||||
|
||||
[:header {:class (stl/css :dashboard-header :team) :data-testid "dashboard-header"}
|
||||
[:div {:class (stl/css :dashboard-title)}
|
||||
|
@ -141,7 +146,7 @@
|
|||
{::mf/register modal/components
|
||||
::mf/register-as :invite-members
|
||||
::mf/wrap-props false}
|
||||
[{:keys [team origin]}]
|
||||
[{:keys [team origin invite-email]}]
|
||||
(let [members-map (mf/deref refs/dashboard-team-members)
|
||||
perms (:permissions team)
|
||||
|
||||
|
@ -192,7 +197,8 @@
|
|||
:on-error (partial on-error form)}]
|
||||
(st/emit! (-> (dd/invite-team-members (with-meta params mdata))
|
||||
(with-meta {::ev/origin origin}))
|
||||
(dd/fetch-team-invitations))))]
|
||||
(dd/fetch-team-invitations)
|
||||
(dd/fetch-team-members (:id team)))))]
|
||||
|
||||
|
||||
[:div {:class (stl/css-case :modal-team-container true
|
||||
|
@ -223,7 +229,8 @@
|
|||
:valid-item-fn us/parse-email
|
||||
:caution-item-fn current-members-emails
|
||||
:label (tr "modals.invite-member.emails")
|
||||
:on-submit on-submit}]]
|
||||
:on-submit on-submit
|
||||
:invite-email invite-email}]]
|
||||
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:> fm/submit-button*
|
||||
|
@ -497,7 +504,7 @@
|
|||
|
||||
(mf/defc team-members-page
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [team profile]}]
|
||||
[{:keys [team profile invite-email]}]
|
||||
(let [members-map (mf/deref refs/dashboard-team-members)]
|
||||
|
||||
(mf/with-effect [team]
|
||||
|
@ -511,7 +518,7 @@
|
|||
(st/emit! (dd/fetch-team-members (:id team))))
|
||||
|
||||
[:*
|
||||
[:& header {:section :dashboard-team-members :team team}]
|
||||
[:& header {:section :dashboard-team-members :team team :invite-email invite-email}]
|
||||
[:section {:class (stl/css :dashboard-container :dashboard-team-members)}
|
||||
[:& team-members
|
||||
{:profile profile
|
||||
|
|
|
@ -10,32 +10,50 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.uri :as u]
|
||||
[app.main.data.common :as dc]
|
||||
[app.main.data.events :as ev]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.auth.login :refer [login-methods]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]]
|
||||
[app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page terms-register]]
|
||||
[app.main.ui.dashboard.sidebar :refer [sidebar]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.viewer.header :as header]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
[app.util.webapi :as wapi]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc error-container
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [children]}]
|
||||
(let [on-click (mf/use-callback #(set! (.-href globals/location) "/"))]
|
||||
(let [profile-id (:profile-id @st/state)]
|
||||
[:section {:class (stl/css :exception-layout)}
|
||||
[:button
|
||||
{:class (stl/css :exception-header)
|
||||
:on-click on-click}
|
||||
i/logo-icon]
|
||||
:on-click rt/nav-root}
|
||||
i/logo-icon
|
||||
(when profile-id
|
||||
(str "< "
|
||||
(tr "not-found.no-permission.go-dashboard")))]
|
||||
[:div {:class (stl/css :deco-before)} i/logo-error-screen]
|
||||
(when-not profile-id
|
||||
[:button {:class (stl/css :login-header)
|
||||
:on-click rt/nav-root}
|
||||
(tr "labels.login")])
|
||||
|
||||
[:div {:class (stl/css :exception-content)}
|
||||
[:div {:class (stl/css :container)} children]]
|
||||
|
||||
[:div {:class (stl/css :deco-after)} i/logo-error-screen]]))
|
||||
[:div {:class (stl/css :deco-after2)}
|
||||
[:span (tr "labels.copyright")]
|
||||
i/logo-error-screen
|
||||
[:span (tr "not-found.made-with-love")]]]))
|
||||
|
||||
(mf/defc invalid-token
|
||||
[]
|
||||
|
@ -43,16 +61,221 @@
|
|||
[:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")]
|
||||
[:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]])
|
||||
|
||||
|
||||
|
||||
(mf/defc login-dialog
|
||||
{::mf/props :obj}
|
||||
[{:keys [show-dialog]}]
|
||||
(let [current-section (mf/use-state :login)
|
||||
user-email (mf/use-state "")
|
||||
register-token (mf/use-state "")
|
||||
|
||||
set-section
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [section (-> (dom/get-current-target event)
|
||||
(dom/get-data "section")
|
||||
(keyword))]
|
||||
(reset! current-section section))))
|
||||
|
||||
set-section-recovery
|
||||
(mf/use-fn
|
||||
#(reset! current-section :recovery-request))
|
||||
|
||||
set-section-login
|
||||
(mf/use-fn
|
||||
#(reset! current-section :login))
|
||||
|
||||
success-login
|
||||
(fn []
|
||||
(reset! show-dialog false)
|
||||
(.reload js/window.location true))
|
||||
|
||||
success-register
|
||||
(fn [data]
|
||||
(reset! register-token (:token data))
|
||||
(reset! current-section :register-validate))
|
||||
|
||||
register-email-sent
|
||||
(fn [email]
|
||||
(reset! user-email email)
|
||||
(reset! current-section :register-email-sent))
|
||||
|
||||
recovery-email-sent
|
||||
(fn [email]
|
||||
(reset! user-email email)
|
||||
(reset! current-section :recovery-email-sent))]
|
||||
|
||||
[:div {:class (stl/css :overlay)}
|
||||
[:div {:class (stl/css :dialog-login)}
|
||||
[:div {:class (stl/css :modal-close)}
|
||||
[:button {:class (stl/css :modal-close-button) :on-click rt/nav-root}
|
||||
i/close]]
|
||||
[:div {:class (stl/css :login)}
|
||||
[:div {:class (stl/css :logo)} i/logo]
|
||||
|
||||
(case @current-section
|
||||
:login
|
||||
[:*
|
||||
[:div {:class (stl/css :logo-title)} (tr "labels.login")]
|
||||
[:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.free")]
|
||||
[:& login-methods {:on-recovery-request set-section-recovery
|
||||
:on-success-callback success-login}]
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :change-section)}
|
||||
(tr "auth.register")
|
||||
" "
|
||||
[:a {:data-section "register"
|
||||
:on-click set-section} (tr "auth.register-submit")]]]
|
||||
|
||||
:register
|
||||
[:*
|
||||
[:div {:class (stl/css :logo-title)} (tr "not-found.login.signup-free")]
|
||||
[:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.start-using")]
|
||||
[:& register-methods {:on-success-callback success-register :hide-separator true}]
|
||||
#_[:hr {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :separator)}]
|
||||
[:div {:class (stl/css :change-section)}
|
||||
(tr "auth.already-have-account")
|
||||
" "
|
||||
[:a {:data-section "login"
|
||||
:on-click set-section} (tr "auth.login-here")]]
|
||||
[:div {:class (stl/css :links)}
|
||||
[:hr {:class (stl/css :separator)}]
|
||||
[:& terms-register]]]
|
||||
|
||||
:register-validate
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:& register-validate-form {:params {:token @register-token}
|
||||
:on-success-callback register-email-sent}]
|
||||
[:div {:class (stl/css :links)}
|
||||
[:div {:class (stl/css :register)}
|
||||
[:a {:data-section "register"
|
||||
:on-click set-section}
|
||||
(tr "labels.go-back")]]]]
|
||||
|
||||
:register-email-sent
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:& register-success-page {:params {:email @user-email :hide-logo true}}]]
|
||||
|
||||
:recovery-request
|
||||
[:& recovery-request-page {:go-back-callback set-section-login
|
||||
:on-success-callback recovery-email-sent}]
|
||||
|
||||
:recovery-email-sent
|
||||
[:div {:class (stl/css :form-container)}
|
||||
[:& recovery-sent-page {:email @user-email}]])]]]))
|
||||
|
||||
(mf/defc request-dialog
|
||||
{::mf/props :obj}
|
||||
[{:keys [title content button-text on-button-click cancel-text]}]
|
||||
(let [on-click (or on-button-click rt/nav-root)]
|
||||
[:div {:class (stl/css :overlay)}
|
||||
[:div {:class (stl/css :dialog)}
|
||||
[:div {:class (stl/css :modal-close)}
|
||||
[:button {:class (stl/css :modal-close-button) :on-click rt/nav-root}
|
||||
i/close]]
|
||||
[:div {:class (stl/css :dialog-title)} title]
|
||||
(for [txt content]
|
||||
[:div txt])
|
||||
[:div {:class (stl/css :sign-info)}
|
||||
(when cancel-text
|
||||
[:button {:class (stl/css :cancel-button) :on-click rt/nav-root} cancel-text])
|
||||
[:button {:on-click on-click} button-text]]]]))
|
||||
|
||||
|
||||
(mf/defc request-access
|
||||
{::mf/props :obj}
|
||||
[{:keys [file-id team-id is-default workspace?]}]
|
||||
(let [profile (:profile @st/state)
|
||||
requested* (mf/use-state {:sent false :already-requested false})
|
||||
requested (deref requested*)
|
||||
show-dialog (mf/use-state true)
|
||||
on-success
|
||||
(mf/use-fn
|
||||
#(reset! requested* {:sent true :already-requested false}))
|
||||
on-error
|
||||
(mf/use-fn
|
||||
#(reset! requested* {:sent true :already-requested true}))
|
||||
on-request-access
|
||||
(mf/use-fn
|
||||
(mf/deps file-id team-id workspace?)
|
||||
(fn []
|
||||
(let [params (if (some? file-id) {:file-id file-id :is-viewer (not workspace?)} {:team-id team-id})
|
||||
mdata {:on-success on-success :on-error on-error}]
|
||||
(st/emit! (dc/create-team-access-request (with-meta params mdata))))))]
|
||||
|
||||
|
||||
[:*
|
||||
(if (some? file-id)
|
||||
(if workspace?
|
||||
[:div {:class (stl/css :workspace)}
|
||||
[:div {:class (stl/css :workspace-left)}
|
||||
i/logo-icon
|
||||
[:div
|
||||
[:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")]
|
||||
[:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]]
|
||||
[:div {:class (stl/css :workspace-right)}]]
|
||||
[:div {:class (stl/css :viewer)}
|
||||
[:& header/header {:project {:name (tr "not-found.no-permission.project-name")}
|
||||
:index 0
|
||||
:file {:name (tr "not-found.no-permission.penpot-file")}
|
||||
:page nil
|
||||
:frame nil
|
||||
:permissions {:is-logged true}
|
||||
:zoom 1
|
||||
:section :interactions
|
||||
:shown-thumbnails false
|
||||
:interactions-mode nil}]])
|
||||
|
||||
[:div {:class (stl/css :dashboard)}
|
||||
[:div {:class (stl/css :dashboard-sidebar)}
|
||||
[:& sidebar
|
||||
{:team nil
|
||||
:projects []
|
||||
:project (:default-project-id profile)
|
||||
:profile profile
|
||||
:section :dashboard-projects
|
||||
:search-term ""}]]])
|
||||
|
||||
(when @show-dialog
|
||||
(cond
|
||||
(nil? profile)
|
||||
[:& login-dialog {:show-dialog show-dialog}]
|
||||
|
||||
is-default
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.project") :button-text (tr "not-found.no-permission.go-dashboard")}]
|
||||
|
||||
(and (some? file-id) (:already-requested requested))
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.file") :content [(tr "not-found.no-permission.already-requested.or-others.file")] :button-text (tr "not-found.no-permission.go-dashboard")}]
|
||||
|
||||
(:already-requested requested)
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.already-requested.project") :content [(tr "not-found.no-permission.already-requested.or-others.project")] :button-text (tr "not-found.no-permission.go-dashboard")}]
|
||||
|
||||
(:sent requested)
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.done.success") :content [(tr "not-found.no-permission.done.remember")] :button-text (tr "not-found.no-permission.go-dashboard")}]
|
||||
|
||||
(some? file-id)
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.file") :content [(tr "not-found.no-permission.you-can-ask.file") (tr "not-found.no-permission.if-approves")] :button-text (tr "not-found.no-permission.ask") :on-button-click on-request-access :cancel-text (tr "not-found.no-permission.go-dashboard")}]
|
||||
|
||||
(some? team-id)
|
||||
[:& request-dialog {:title (tr "not-found.no-permission.project") :content [(tr "not-found.no-permission.you-can-ask.project") (tr "not-found.no-permission.if-approves")] :button-text (tr "not-found.no-permission.ask") :on-button-click on-request-access :cancel-text (tr "not-found.no-permission.go-dashboard")}]))]))
|
||||
|
||||
|
||||
|
||||
(mf/defc not-found
|
||||
[]
|
||||
[:> error-container {}
|
||||
[:div {:class (stl/css :main-message)} (tr "labels.not-found.main-message")]
|
||||
[:div {:class (stl/css :desc-message)} (tr "labels.not-found.desc-message")]])
|
||||
[:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.error")]
|
||||
[:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.doesnt-exist")]])
|
||||
|
||||
|
||||
|
||||
(mf/defc bad-gateway
|
||||
[]
|
||||
(let [handle-retry
|
||||
(mf/use-callback
|
||||
(mf/use-fn
|
||||
(fn [] (st/emit! (rt/assign-exception nil))))]
|
||||
[:> error-container {}
|
||||
[:div {:class (stl/css :main-message)} (tr "labels.bad-gateway.main-message")]
|
||||
|
@ -150,13 +373,49 @@
|
|||
(mf/defc exception-page
|
||||
{::mf/props :obj}
|
||||
[{:keys [data route] :as props}]
|
||||
(let [type (:type data)
|
||||
path (:path route)
|
||||
query-params (u/map->query-string (:query-params route))]
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "exception-page" :type type :path path :query-params query-params}))
|
||||
(let [file-info (mf/use-state {:pending true})
|
||||
team-info (mf/use-state {:pending true})
|
||||
type (:type data)
|
||||
path (:path route)
|
||||
|
||||
workspace? (str/includes? path "workspace")
|
||||
dashboard? (str/includes? path "dashboard")
|
||||
view? (str/includes? path "view")
|
||||
|
||||
request-access? (and
|
||||
(or workspace? dashboard? view?)
|
||||
(or (not (str/empty? (:file-id @file-info))) (not (str/empty? (:team-id @team-info)))))
|
||||
|
||||
query-params (u/map->query-string (:query-params route))
|
||||
pparams (:path-params route)
|
||||
on-file-info (mf/use-fn
|
||||
(fn [info]
|
||||
(reset! file-info {:file-id (:id info)})))
|
||||
on-team-info (mf/use-fn
|
||||
(fn [info]
|
||||
(reset! team-info {:team-id (:id info) :is-default (:is-default info)})))]
|
||||
|
||||
(mf/with-effect [type path query-params pparams @file-info @team-info]
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "exception-page" :type type :path path :query-params query-params}))
|
||||
|
||||
(when (and (:file-id pparams) (:pending @file-info))
|
||||
(->> (rp/cmd! :get-file-info {:id (:file-id pparams)})
|
||||
(rx/subs! on-file-info)))
|
||||
|
||||
(when (and (:team-id pparams) (:pending @team-info))
|
||||
(->> (rp/cmd! :get-team-info {:id (:team-id pparams)})
|
||||
(rx/subs! on-team-info))))
|
||||
|
||||
(case (:type data)
|
||||
:not-found
|
||||
[:& not-found]
|
||||
(if request-access?
|
||||
[:& request-access {:file-id (:file-id @file-info) :team-id (:team-id @team-info) :is-default (:is-default @team-info) :workspace? workspace?}]
|
||||
[:& not-found])
|
||||
|
||||
:authentication
|
||||
(if request-access?
|
||||
[:& request-access {:file-id (:file-id @file-info) :team-id (:team-id @team-info) :is-default (:is-default @team-info) :workspace? workspace?}]
|
||||
[:& not-found])
|
||||
|
||||
:bad-gateway
|
||||
[:& bad-gateway]
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
.deco-before {
|
||||
height: 34vh;
|
||||
top: 0;
|
||||
|
||||
svg {
|
||||
bottom: 0;
|
||||
}
|
||||
|
@ -36,17 +37,52 @@
|
|||
.deco-after {
|
||||
height: 34vh;
|
||||
bottom: 0;
|
||||
|
||||
svg {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.deco-after2 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: $s-8;
|
||||
width: 100%;
|
||||
height: 34vh;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
color: var(--color-foreground-primary);
|
||||
|
||||
svg {
|
||||
fill: var(--color-foreground-secondary);
|
||||
height: 1537px;
|
||||
width: $s-80;
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
width: 25%;
|
||||
|
||||
&:first-child {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.exception-header {
|
||||
color: var(--color-foreground-secondary);
|
||||
padding: $s-24 $s-32;
|
||||
position: fixed;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
fill: var(--color-foreground-primary);
|
||||
width: $s-48;
|
||||
|
@ -54,6 +90,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.login-header {
|
||||
@extend .button-primary;
|
||||
padding: $s-8 $s-16;
|
||||
font-size: $fs-11;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
margin: $s-40 $s-32;
|
||||
}
|
||||
|
||||
.exception-content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
@ -85,6 +130,7 @@
|
|||
|
||||
.sign-info {
|
||||
text-align: center;
|
||||
|
||||
button {
|
||||
@extend .button-primary;
|
||||
text-transform: uppercase;
|
||||
|
@ -98,3 +144,180 @@
|
|||
fill: var(--color-foreground-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.workspace {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-canvas);
|
||||
position: relative;
|
||||
|
||||
.workspace-left,
|
||||
.workspace-right {
|
||||
padding: $s-12;
|
||||
width: $s-276;
|
||||
height: 100%;
|
||||
background-color: var(--color-background-primary);
|
||||
display: flex;
|
||||
gap: $s-4;
|
||||
|
||||
svg {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
fill: var(--icon-foreground-hover);
|
||||
}
|
||||
|
||||
.project-name {
|
||||
@include uppercaseTitleTipography;
|
||||
color: var(--title-foreground-color);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
@include smallTitleTipography;
|
||||
text-transform: none;
|
||||
color: var(--title-foreground-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.dashboard-sidebar {
|
||||
width: $s-300;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.viewer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
background-color: rgba(0, 0, 0, 0.65);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.dialog,
|
||||
.dialog-login {
|
||||
width: 556px;
|
||||
background-color: var(--color-background-primary);
|
||||
border-radius: $s-8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: stretch;
|
||||
padding: $s-36;
|
||||
color: var(--modal-text-foreground-color);
|
||||
|
||||
.modal-close {
|
||||
text-align: right;
|
||||
|
||||
.modal-close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
cursor: pointer;
|
||||
width: $s-24;
|
||||
height: $s-24;
|
||||
fill: var(--modal-text-foreground-color);
|
||||
stroke: var(--modal-text-foreground-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: $fs-20;
|
||||
}
|
||||
|
||||
.sign-info {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: $s-32;
|
||||
|
||||
button {
|
||||
@extend .button-primary;
|
||||
text-transform: uppercase;
|
||||
padding: $s-8 $s-16;
|
||||
font-size: $fs-11;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
@extend .button-secondary;
|
||||
text-transform: uppercase;
|
||||
padding: $s-8 $s-16;
|
||||
font-size: $fs-11;
|
||||
margin-right: $s-16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
gap: $s-12;
|
||||
}
|
||||
|
||||
.login {
|
||||
gap: 0;
|
||||
padding: 0 $s-36 $s-72 $s-36;
|
||||
|
||||
.logo {
|
||||
margin-bottom: $s-40;
|
||||
|
||||
svg {
|
||||
fill: var(--color-foreground-primary);
|
||||
width: $s-120;
|
||||
height: $s-40;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
font-size: $fs-20;
|
||||
color: var(--title-foreground-color-hover);
|
||||
margin-bottom: $s-4;
|
||||
}
|
||||
|
||||
.logo-subtitle {
|
||||
font-size: $fs-14;
|
||||
color: var(--title-foreground-color-hover);
|
||||
margin-bottom: $s-24;
|
||||
}
|
||||
|
||||
.change-section {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
color: var(--link-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: $s-20 0;
|
||||
border-top: solid 1px var(--modal-separator-backogrund-color);
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: $s-20 0;
|
||||
}
|
||||
|
||||
form div {
|
||||
margin-bottom: $s-8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
background-color: red;
|
||||
}
|
||||
|
|
|
@ -276,7 +276,7 @@
|
|||
|
||||
(mf/defc viewer-content
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [data page-id share-id section index interactions-mode] :as props}]
|
||||
[{:keys [data page-id share-id section index interactions-mode share] :as props}]
|
||||
(let [{:keys [file users project permissions]} data
|
||||
allowed (or
|
||||
(= section :interactions)
|
||||
|
@ -615,7 +615,8 @@
|
|||
:zoom zoom
|
||||
:section section
|
||||
:shown-thumbnails (:show-thumbnails local)
|
||||
:interactions-mode interactions-mode}]]))
|
||||
:interactions-mode interactions-mode
|
||||
:share share}]]))
|
||||
|
||||
;; --- Component: Viewer
|
||||
|
||||
|
|
|
@ -120,7 +120,7 @@
|
|||
:key (dm/str "zoom-fullscreen-" sc)} sc])]]]]]))
|
||||
|
||||
(mf/defc header-options
|
||||
[{:keys [section zoom page file index permissions interactions-mode]}]
|
||||
[{:keys [section zoom page file index permissions interactions-mode share]}]
|
||||
(let [fullscreen? (mf/deref fullscreen-ref)
|
||||
|
||||
toggle-fullscreen
|
||||
|
@ -159,6 +159,12 @@
|
|||
handle-zoom-fit
|
||||
(mf/use-fn
|
||||
#(st/emit! dv/zoom-to-fit))]
|
||||
(mf/with-effect [permissions share]
|
||||
(when (and
|
||||
(:in-team permissions)
|
||||
(:is-admin permissions)
|
||||
share)
|
||||
(open-share-dialog)))
|
||||
|
||||
[:div {:class (stl/css :options-zone)}
|
||||
[:& export-progress-widget]
|
||||
|
@ -261,7 +267,7 @@
|
|||
|
||||
|
||||
(mf/defc header
|
||||
[{:keys [project file page frame zoom section permissions index interactions-mode shown-thumbnails]}]
|
||||
[{:keys [project file page frame zoom section permissions index interactions-mode shown-thumbnails share]}]
|
||||
(let [go-to-dashboard
|
||||
(mf/use-fn
|
||||
#(st/emit! (dv/go-to-dashboard)))
|
||||
|
@ -351,4 +357,5 @@
|
|||
:file file
|
||||
:index index
|
||||
:zoom zoom
|
||||
:interactions-mode interactions-mode}]]))
|
||||
:interactions-mode interactions-mode
|
||||
:share share}]]))
|
||||
|
|
|
@ -10,14 +10,12 @@
|
|||
[app.common.logging :as log]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.auth :refer [terms-login]]
|
||||
[app.main.ui.auth.login :refer [login-methods]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page]]
|
||||
[app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page]]
|
||||
[app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page terms-register]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.storage :refer [storage]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(log/set-level! :warn)
|
||||
|
@ -26,8 +24,7 @@
|
|||
{::mf/register modal/components
|
||||
::mf/register-as :login-register}
|
||||
[_]
|
||||
(let [uri (. (. js/document -location) -href)
|
||||
user-email (mf/use-state "")
|
||||
(let [user-email (mf/use-state "")
|
||||
register-token (mf/use-state "")
|
||||
|
||||
current-section* (mf/use-state :login)
|
||||
|
@ -66,9 +63,6 @@
|
|||
(reset! register-token (:token data))
|
||||
(set-current-section :register-validate))]
|
||||
|
||||
(mf/with-effect []
|
||||
(swap! storage assoc :redirect-url uri))
|
||||
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
|
@ -125,4 +119,4 @@
|
|||
|
||||
(when main-section
|
||||
[:div {:class (stl/css :links)}
|
||||
[:& terms-login]])]]]))
|
||||
[:& terms-register]])]]]))
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
.modal-container {
|
||||
@extend .modal-container-base;
|
||||
width: $s-368;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
|
@ -32,8 +33,8 @@
|
|||
@include bodySmallTypography;
|
||||
gap: $s-24;
|
||||
max-height: $s-400;
|
||||
width: $s-368;
|
||||
overflow: hidden auto;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -46,7 +47,6 @@
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
max-width: $s-368;
|
||||
}
|
||||
|
||||
.links {
|
||||
|
@ -64,6 +64,7 @@
|
|||
color: var(--modal-text-foreground-color);
|
||||
margin-top: $s-12;
|
||||
}
|
||||
|
||||
a {
|
||||
@extend .button-secondary;
|
||||
height: $s-40;
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
[app.main.data.events :as ev]
|
||||
[app.util.browser-history :as bhistory]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.timers :as ts]
|
||||
[beicon.v2.core :as rx]
|
||||
[goog.events :as e]
|
||||
|
@ -143,6 +144,11 @@
|
|||
(= (.-hostname location) (:host referrer)))
|
||||
(nav-back))))
|
||||
|
||||
(defn nav-root
|
||||
"Navigate to the root page."
|
||||
[]
|
||||
(set! (.-href globals/location) "/"))
|
||||
|
||||
;; --- History API
|
||||
|
||||
(defn initialize-history
|
||||
|
|
|
@ -44,3 +44,4 @@
|
|||
(defonce storage (atom (load (ex/ignoring (unchecked-get g/global "localStorage")))))
|
||||
|
||||
(add-watch storage :persistence #(persist js/localStorage %3 %4))
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue