Optimize profile setup flow for better user experience (#6223)

*  Optimize profile setup flow for better user experience

* 📎 Remove extra onboarding step

* 📎 Code review

* 📎 Update changelog

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
María Valderrama 2025-05-05 10:42:08 +02:00 committed by GitHub
parent aae81b8a04
commit 86a498fc29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 364 additions and 422 deletions

View file

@ -167,7 +167,6 @@
(and (contains? cf/flags :onboarding)
(not (:onboarding-viewed props))
(not (contains? props :onboarding-team-id))
(contains? props :newsletter-updates)
(:is-default team))
show-release-modal?
@ -233,7 +232,7 @@
[:& onboarding-newsletter]
show-team-modal?
[:& onboarding-team-modal {:go-to-team? true}]
[:& onboarding-team-modal {:go-to-team true}]
show-release-modal?
[:& release-notes-modal {:version (:main cf/version)}])
@ -259,11 +258,8 @@
show-question-modal?
[:& questions-modal]
show-newsletter-modal?
[:& onboarding-newsletter]
show-team-modal?
[:& onboarding-team-modal {:go-to-team? false}]
[:& onboarding-team-modal {:go-to-team false}]
show-release-modal?
[:& release-notes-modal {:version (:main cf/version)}]))

View file

@ -23,9 +23,11 @@
{::mf/props :obj}
[{:keys [route]}]
(let [section (dm/get-in route [:data :name])
show-login-icon (and
(not= section :auth-register-validate)
(not= section :auth-register-success))
is-register (or
(= section :auth-register)
(= section :auth-register-validate)
(= section :register-validate-page)
(= section :auth-register-success))
params (:query-params route)
error (:error params)]
@ -36,10 +38,11 @@
(when error
(st/emit! (da/show-redirect-error error))))
[:main {:class (stl/css :auth-section)}
(when show-login-icon
[:h1 {:class (stl/css :logo-container)}
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]])
[:main {:class (stl/css-case
:auth-section true
:register is-register)}
[:h1 {:class (stl/css :logo-container)}
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
[:div {:class (stl/css :login-illustration)}
i/login-illustration]
@ -49,12 +52,12 @@
:auth-register
[:& register-page {:params params}]
:auth-register-validate
[:& register-validate-page {:params params}]
:auth-register-success
[:& register-success-page {:params params}]
:auth-register-validate
[:& register-validate-page {:params params}]
:auth-login
[:& login-page {:params params}]

View file

@ -22,6 +22,16 @@
display: flex;
justify-content: center;
}
&.register {
display: flex;
justify-content: center;
align-items: center;
.login-illustration {
display: none;
}
}
}
.logo-container {

View file

@ -18,7 +18,6 @@
[app.main.ui.auth.login :as login]
[app.main.ui.components.forms :as fm]
[app.main.ui.components.link :as lk]
[app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [tr]]
[app.util.storage :as storage]
[beicon.v2.core :as rx]
@ -26,11 +25,48 @@
;; --- PAGE: Register
(mf/defc newsletter-options*
{::mf/private true}
[]
(let [updates-label
(mf/html
[:> i18n/tr-html*
{:tag-name "div"
:content (tr "onboarding-v2.newsletter.updates")}])]
[:div {:class (stl/css :fields-row :input-visible :newsletter-option-wrapper)}
[:& fm/input {:name :accept-newsletter-updates
:type "checkbox"
:default-checked false
:label updates-label}]]))
(mf/defc terms-and-privacy
{::mf/props :obj
::mf/private true}
[]
(let [terms-label
(mf/html
[:> i18n/tr-html*
{:tag-name "div"
:content (tr "auth.terms-and-privacy-agreement"
cf/terms-of-service-uri
cf/privacy-policy-uri)}])]
[:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)}
[:& fm/input {:name :accept-terms-and-privacy
:class (stl/css :checkbox-terms-and-privacy)
:type "checkbox"
:default-checked false
:label terms-label}]]))
(def ^:private schema:register-form
[:map {:title "RegisterForm"}
[:password ::sm/password]
[:fullname [::sm/text {:max 250}]]
[:email ::sm/email]
[:invitation-token {:optional true} ::sm/text]])
[:accept-terms-and-privacy {:optional (not (contains? cf/flags :terms-and-privacy-checkbox))}
[:and :boolean [:= true]]]
[:accept-newsletter-updates {:optional true} :boolean]
[:token {:optional true} ::sm/text]])
(mf/defc register-form
{::mf/props :obj}
@ -65,23 +101,59 @@
(st/emit! (ntf/error (tr "errors.generic")))))))
on-success
(mf/use-fn
(mf/deps on-success-callback)
(fn [params]
(if (fn? on-success-callback)
(on-success-callback (:email params))
(cond
(some? (:token params))
(let [token (:token params)]
(st/emit! (rt/nav :auth-verify-token {:token token})))
(:is-active params)
(st/emit! (da/login-from-register))
:else
(do
(swap! storage/user assoc ::email (:email params))
(st/emit! (rt/nav :auth-register-success)))))))
on-register-profile
(mf/use-fn
(mf/deps on-success on-error)
(fn [form]
(reset! submitted? true)
(let [create-welcome-file?
(cf/external-feature-flag "onboarding-03" "test")
params
(cond-> form
create-welcome-file? (assoc :create-welcome-file true))]
(->> (rp/cmd! :register-profile params)
(rx/subs! on-success on-error #(reset! submitted? false))))))
on-submit
(mf/use-fn
(mf/deps on-success-callback)
(fn [form _event]
(reset! submitted? true)
(let [cdata (:clean-data @form)
on-success (fn [data]
(if (fn? on-success-callback)
(on-success-callback data)
(st/emit! (rt/nav :auth-register-validate data))))]
(let [cdata (:clean-data @form)]
(->> (rp/cmd! :prepare-register-profile cdata)
(rx/map #(merge % params))
(rx/map #(merge % cdata))
(rx/finalize #(reset! submitted? false))
(rx/subs! on-success (partial on-error form))))))]
(rx/subs! on-register-profile)))))]
[:& fm/form {:on-submit on-submit :form form}
[:div {:class (stl/css :fields-row)}
[:& fm/input {:name :fullname
:label (tr "auth.fullname")
:type "text"
:show-success? true
:class (stl/css :form-field)}]]
[:div {:class (stl/css :fields-row)}
[:& fm/input {:type "text"
:name :email
@ -97,6 +169,11 @@
:type "password"
:class (stl/css :form-field)}]]
(when (contains? cf/flags :terms-and-privacy-checkbox)
[:& terms-and-privacy])
[:> newsletter-options*]
[:> fm/submit-button*
{:label (tr "auth.register-submit")
:disabled @submitted?
@ -120,8 +197,6 @@
[:div {:class (stl/css :auth-form-wrapper :register-form)}
[:h1 {:class (stl/css :auth-title)
:data-testid "registration-title"} (tr "auth.register-title")]
[:p {:class (stl/css :auth-tagline)}
(tr "auth.register-tagline")]
(when (contains? cf/flags :demo-warning)
[:& login/demo-warning])
@ -144,26 +219,41 @@
:class (stl/css :demo-account-link)}
(tr "auth.create-demo-account")]]])]])
;; --- PAGE: register validation
;; --- PAGE: register success page
(mf/defc terms-and-privacy
{::mf/props :obj
::mf/private true}
(mf/defc register-success-page
{::mf/props :obj}
[{:keys [params]}]
(let [email (or (:email params) (::email storage/user))]
[: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 "auth.verification-email-sent")]]
[:div {:class (stl/css :notification-text-email)} email]]))
(mf/defc terms-register
[]
(let [terms-label
(mf/html
[:> i18n/tr-html*
{:tag-name "div"
:content (tr "auth.terms-and-privacy-agreement"
cf/terms-of-service-uri
cf/privacy-policy-uri)}])]
(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)]
[:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)}
[:& fm/input {:name :accept-terms-and-privacy
:class (stl/css :checkbox-terms-and-privacy)
:type "checkbox"
:default-checked false
:label terms-label}]]))
(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")])])))
;; --- PAGE: register validation
(def ^:private schema:register-validate-form
[:map {:title "RegisterValidateForm"}
@ -245,9 +335,9 @@
(mf/defc register-validate-page
{::mf/props :obj}
[{:keys [params]}]
[:div {:class (stl/css :auth-form-wrapper)}
[:h1 {:class (stl/css :logo-container)}
[:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]
[:div {:class (stl/css :auth-form-wrapper :register-form)}
[:div {:class (stl/css :auth-title-wrapper)}
[:h2 {:class (stl/css :auth-title)
:data-testid "register-title"} (tr "auth.register-account-title")]
@ -260,40 +350,3 @@
[:& lk/link {:action #(st/emit! (rt/nav :auth-register {}))
:class (stl/css :go-back-link)}
(tr "labels.go-back")]]]])
(mf/defc register-success-page
{::mf/props :obj}
[{:keys [params]}]
(let [email (or (:email params) (::email storage/user))]
[:div {:class (stl/css :auth-form-wrapper :register-success)}
(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")])])))

View file

@ -444,6 +444,8 @@
error (get-in @form [:errors input-name])
focus? (mf/use-state false)
auto-focus? (get props :auto-focus? false)
items (mf/use-state
(fn []
(let [initial (get-in @form [:data input-name])]
@ -562,7 +564,7 @@
[:input {:id (name input-name)
:class in-klass
:type "text"
:auto-focus true
:auto-focus auto-focus?
:on-focus on-focus
:on-blur on-blur
:on-key-down on-key-down

View file

@ -293,7 +293,6 @@
border-radius: $br-8;
color: var(--input-foreground-color-active);
background-color: var(--input-background-color);
border: $s-1 solid var(--input-border-color-active);
&:focus {
outline: none;
border: $s-1 solid var(--input-border-color-focus);

View file

@ -39,7 +39,6 @@
[:& fm/form {:form form
:on-submit on-next*
:class (dm/str class " " (stl/css :form-wrapper))}
[:div {:class (stl/css :paginator)} (str/ffmt "%/5" step)]
children
@ -51,7 +50,7 @@
(tr "labels.previous")])
[:> fm/submit-button*
{:label (if (< step 5)
{:label (if (< step 4)
(tr "labels.next")
(tr "labels.start"))
:class (stl/css :next-button)}]]]))
@ -60,47 +59,41 @@
[:and
[:map {:title "QuestionsFormStep1"}
[:planning ::sm/text]
[:expected-use [:enum "work" "education" "personal"]]
[:planning-other {:optional true}
[::sm/text {:max 512}]]]
[:role
[:enum "ux" "developer" "student-teacher" "designer" "marketing" "manager" "other"]]
[:role-other {:optional true} [::sm/text {:max 512}]]]
[:fn {:error/field :planning-other}
(fn [{:keys [planning planning-other]}]
(or (not= planning "other")
(and (= planning "other")
(not (str/blank? planning-other)))))]])
[:fn {:error/field :role-other}
(fn [{:keys [role role-other]}]
(or (not= role "other")
(and (= role "other")
(not (str/blank? role-other)))))]])
(mf/defc step-1
{::mf/props :obj}
[{:keys [on-next form]}]
[{:keys [on-next form show-step-3]}]
(let [use-options
(mf/with-memo []
(shuffle [{:label (tr "onboarding.questions.use.work") :value "work"}
{:label (tr "onboarding.questions.use.education") :value "education"}
{:label (tr "onboarding.questions.use.personal") :value "personal"}]))
planning-options
role-options
(mf/with-memo []
(-> (shuffle [{:label (tr "labels.select-option")
:value "" :key "questions:what-brings-you-here"
:disabled true}
{:label (tr "onboarding.questions.reasons.exploring")
:value "discover-more-about-penpot"
:key "discover-more-about-penpot"}
{:label (tr "onboarding.questions.reasons.fit")
:value "test-penpot-to-see-if-its-a-fit-for-team"
:key "test-penpot-to-see-if-its-a-fit-for-team"}
{:label (tr "onboarding.questions.reasons.alternative")
:value "alternative-to-figma"
:key "alternative-to-figma"}
{:label (tr "onboarding.questions.reasons.testing")
:value "try-out-before-using-penpot-on-premise"
:key "try-out-before-using-penpot-on-premise"}])
(-> (shuffle [{:label (tr "labels.select-option") :value "" :key "role" :disabled true}
{:label (tr "labels.product-design") :value "ux" :key "ux"}
{:label (tr "labels.developer") :value "developer" :key "developer"}
{:label (tr "labels.student-teacher") :value "student-teacher" :key "student"}
{:label (tr "labels.graphic-design") :value "designer" :key "design"}
{:label (tr "labels.marketing") :value "marketing" :key "marketing"}
{:label (tr "labels.product-management") :value "manager" :key "manager"}])
(conj {:label (tr "labels.other-short") :value "other"})))
current-planning
(dm/get-in @form [:data :planning])]
current-role
(dm/get-in @form [:data :role])]
[:& step-container {:form form
:step 1
@ -108,6 +101,8 @@
:on-next on-next
:class (stl/css :step-1)}
[:div {:class (stl/css :paginator)} (str/ffmt "1/%" (if @show-step-3 4 3))]
[:img {:class (stl/css :header-image)
:src "images/form/use-for-1.png"
:alt (tr "onboarding.questions.lets-get-started")}]
@ -124,18 +119,14 @@
:name :expected-use
:class (stl/css :radio-btns)}]
[:h3 {:class (stl/css :modal-subtitle)}
(tr "onboarding.questions.step1.question2")]
[:h3 {:class (stl/css :modal-subtitle)} (tr "onboarding.questions.step3.question1")]
[:& fm/select {:options role-options
:select-class (stl/css :select-class)
:default ""
:name :role}]
[:& fm/select
{:options planning-options
:select-class (stl/css :select-class)
:default ""
:name :planning
:dropdown-class (stl/css :question-dropdown)}]
(when (= current-planning "other")
[:& fm/input {:name :planning-other
(when (= current-role "other")
[:& fm/input {:name :role-other
:class (stl/css :input-spacing)
:placeholder (tr "labels.other")
:show-error false
@ -159,7 +150,7 @@
(mf/defc step-2
{::mf/props :obj}
[{:keys [on-next on-prev form]}]
[{:keys [on-next on-prev form show-step-3]}]
(let [design-tool-options
(mf/with-memo []
(-> (shuffle [{:label (tr "labels.figma") :img-width "48px" :img-height "60px"
@ -192,6 +183,9 @@
:on-prev on-prev
:class (stl/css :step-2)}
[:div {:class (stl/css :paginator)} (str/ffmt "2/%" (if @show-step-3 4 3))]
[:h1 {:class (stl/css :modal-title)}
(tr "onboarding.questions.step2.title")]
[:div {:class (stl/css :radio-wrapper)}
@ -216,51 +210,22 @@
[:map {:title "QuestionsFormStep3"}
[:team-size
[:enum "more-than-50" "31-50" "11-30" "2-10" "freelancer" "personal-project"]]
[:role
[:enum "ux" "developer" "student-teacher" "designer" "marketing" "manager" "other"]]
[:responsability
[:enum "team-leader" "team-member" "freelancer" "ceo-founder" "director" "other"]]
[:role-other {:optional true} [::sm/text {:max 512}]]
[:responsability-other {:optional true} [::sm/text {:max 512}]]]
[:planning ::sm/text]
[:fn {:error/field :role-other}
(fn [{:keys [role role-other]}]
(or (not= role "other")
(and (= role "other")
(not (str/blank? role-other)))))]
[:planning-other {:optional true}
[::sm/text {:max 512}]]]
[:fn {:error/field :responsability-other}
(fn [{:keys [responsability responsability-other]}]
(or (not= responsability "other")
(and (= responsability "other")
(not (str/blank? responsability-other)))))]])
[:fn {:error/field :planning-other}
(fn [{:keys [planning planning-other]}]
(or (not= planning "other")
(and (= planning "other")
(not (str/blank? planning-other)))))]])
(mf/defc step-3
{::mf/props :obj}
[{:keys [on-next on-prev form]}]
(let [role-options
(mf/with-memo []
(-> (shuffle [{:label (tr "labels.select-option") :value "" :key "role" :disabled true}
{:label (tr "labels.product-design") :value "ux" :key "ux"}
{:label (tr "labels.developer") :value "developer" :key "developer"}
{:label (tr "labels.student-teacher") :value "student-teacher" :key "student"}
{:label (tr "labels.graphic-design") :value "designer" :key "design"}
{:label (tr "labels.marketing") :value "marketing" :key "marketing"}
{:label (tr "labels.product-management") :value "manager" :key "manager"}])
(conj {:label (tr "labels.other-short") :value "other"})))
responsability-options
(mf/with-memo []
(-> (shuffle [{:label (tr "labels.select-option") :value "" :key "responsability" :disabled true}
{:label (tr "labels.team-leader") :value "team-leader"}
{:label (tr "labels.team-member") :value "team-member"}
{:label (tr "labels.freelancer") :value "freelancer"}
{:label (tr "labels.founder") :value "ceo-founder"}
{:label (tr "labels.director") :value "director"}])
(conj {:label (tr "labels.other-short") :value "other"})))
team-size-options
[{:keys [on-next on-prev form show-step-3]}]
(let [team-size-options
(mf/with-memo []
[{:label (tr "labels.select-option") :value "" :key "team-size" :disabled true}
{:label (tr "onboarding.questions.team-size.more-than-50") :value "more-than-50" :key "more-than-50"}
@ -270,11 +235,27 @@
{:label (tr "onboarding.questions.team-size.freelancer") :value "freelancer" :key "freelancer"}
{:label (tr "onboarding.questions.team-size.personal-project") :value "personal-project" :key "personal-project"}])
current-role
(dm/get-in @form [:data :role])
planning-options
(mf/with-memo []
(-> (shuffle [{:label (tr "labels.select-option")
:value "" :key "questions:what-brings-you-here"
:disabled true}
{:label (tr "onboarding.questions.reasons.exploring")
:value "discover-more-about-penpot"
:key "discover-more-about-penpot"}
{:label (tr "onboarding.questions.reasons.fit")
:value "test-penpot-to-see-if-its-a-fit-for-team"
:key "test-penpot-to-see-if-its-a-fit-for-team"}
{:label (tr "onboarding.questions.reasons.alternative")
:value "alternative-to-figma"
:key "alternative-to-figma"}
{:label (tr "onboarding.questions.reasons.testing")
:value "try-out-before-using-penpot-on-premise"
:key "try-out-before-using-penpot-on-premise"}])
(conj {:label (tr "labels.other-short") :value "other"})))
current-responsability
(dm/get-in @form [:data :responsability])]
current-planning
(dm/get-in @form [:data :planning])]
[:& step-container {:form form
:step 3
@ -283,35 +264,27 @@
:on-prev on-prev
:class (stl/css :step-3)}
[:div {:class (stl/css :paginator)} (str/ffmt "3/%" (if @show-step-3 4 3))]
[:h1 {:class (stl/css :modal-title)}
(tr "onboarding.questions.step3.title")]
[:div {:class (stl/css :modal-question)}
[:h3 {:class (stl/css :modal-subtitle)} (tr "onboarding.questions.step3.question1")]
[:& fm/select {:options role-options
:select-class (stl/css :select-class)
:default ""
:name :role}]
[:h3 {:class (stl/css :modal-subtitle)}
(tr "onboarding.questions.step1.question2")]
(when (= current-role "other")
[:& fm/input {:name :role-other
:class (stl/css :input-spacing)
:placeholder (tr "labels.other")
:show-error false
:label ""}])]
[:& fm/select
{:options planning-options
:select-class (stl/css :select-class)
:default ""
:name :planning
:dropdown-class (stl/css :question-dropdown)}]]
[:div {:class (stl/css :modal-question)}
[:h3 {:class (stl/css :modal-subtitle)} (tr "onboarding.questions.step3.question2")]
[:& fm/select {:options responsability-options
:select-class (stl/css :select-class)
:default ""
:name :responsability}]
(when (= current-responsability "other")
[:& fm/input {:name :responsability-other
:class (stl/css :input-spacing)
:placeholder (tr "labels.other")
:show-error false
:label ""}])]
(when (= current-planning "other")
[:& fm/input {:name :planning-other
:class (stl/css :input-spacing)
:placeholder (tr "labels.other")
:show-error false
:label ""}])
[:div {:class (stl/css :modal-question)}
[:h3 {:class (stl/css :modal-subtitle)} (tr "onboarding.questions.step3.question3")]
@ -335,7 +308,7 @@
(mf/defc step-4
{::mf/props :obj}
[{:keys [on-next on-prev form]}]
[{:keys [on-next on-prev form show-step-3]}]
(let [start-options
(mf/with-memo []
(-> (shuffle [{:label (tr "onboarding.questions.start-with.ui")
@ -367,6 +340,8 @@
:on-prev on-prev
:class (stl/css :step-4)}
[:div {:class (stl/css :paginator)} (str/ffmt "%/%" (if @show-step-3 4 3) (if @show-step-3 4 3))]
[:h1 {:class (stl/css :modal-title)} (tr "onboarding.questions.step4.title")]
[:div {:class (stl/css :radio-wrapper)}
[:& fm/image-radio-buttons {:options start-options
@ -382,67 +357,14 @@
:show-error false
:placeholder (tr "labels.other")}])]]))
(def ^:private schema:questions-form-5
[:and
[:map {:title "QuestionsFormStep5"}
[:referer
[:enum "youtube" "event" "search" "social" "article" "other"]]
[:referer-other {:optional true} [::sm/text {:max 512}]]]
[:fn {:error/field :referer-other}
(fn [{:keys [referer referer-other]}]
(or (not= referer "other")
(and (= referer "other")
(not (str/blank? referer-other)))))]])
(mf/defc step-5
{::mf/props :obj}
[{:keys [on-next on-prev form]}]
(let [referer-options
(mf/with-memo []
(-> (shuffle [{:label (tr "labels.youtube") :value "youtube"}
{:label (tr "labels.event") :value "event"}
{:label (tr "onboarding.questions.referer.search") :value "search"}
{:label (tr "onboarding.questions.referer.social") :value "social"}
{:label (tr "onboarding.questions.referer.article") :value "article"}])
(conj {:label (tr "labels.other-short") :value "other"})))
current-referer
(dm/get-in @form [:data :referer])
on-referer-change
(mf/use-fn
(mf/deps current-referer)
(fn []
(when (not= current-referer "other")
(swap! form d/dissoc-in [:data :referer-other])
(swap! form d/dissoc-in [:errors :referer-other]))))]
[:& step-container {:form form
:step 5
:label "questions:referer"
:on-next on-next
:on-prev on-prev
:class (stl/css :step-5)}
[:h1 {:class (stl/css :modal-title)} (tr "onboarding.questions.step5.title")]
[:div {:class (stl/css :radio-wrapper)}
[:& fm/radio-buttons {:options referer-options
:class (stl/css :radio-btns)
:name :referer
:on-change on-referer-change}]
(when (= current-referer "other")
[:& fm/input {:name :referer-other
:class (stl/css :input-spacing)
:label ""
:show-error false
:placeholder (tr "labels.other")}])]]))
(mf/defc questions-modal
[]
(let [container (mf/use-ref)
step (mf/use-state 1)
clean-data (mf/use-state {})
show-step-3 (mf/use-state false)
;; Forms are initialized here because we can go back and forth between the steps
;; and we want to keep the filled info
@ -462,13 +384,13 @@
:initial {}
:schema schema:questions-form-4)
step-5-form (fm/use-form
:initial {}
:schema schema:questions-form-5)
on-next
(mf/use-fn
(fn [form]
(when (:expected-use (:clean-data @form))
(if (= (:expected-use (:clean-data @form)) "work")
(reset! show-step-3 true)
(reset! show-step-3 false)))
(swap! step inc)
(swap! clean-data merge (:clean-data @form))))
@ -491,8 +413,10 @@
:ref container}
(case @step
1 [:& step-1 {:on-next on-next :on-prev on-prev :form step-1-form}]
2 [:& step-2 {:on-next on-next :on-prev on-prev :form step-2-form}]
3 [:& step-3 {:on-next on-next :on-prev on-prev :form step-3-form}]
4 [:& step-4 {:on-next on-next :on-prev on-prev :form step-4-form}]
5 [:& step-5 {:on-next on-submit :on-prev on-prev :form step-5-form}])]]))
1 [:& step-1 {:on-next on-next :on-prev on-prev :form step-1-form :show-step-3 show-step-3}]
2 [:& step-2 {:on-next on-next :on-prev on-prev :form step-2-form :show-step-3 show-step-3}]
3 (if @show-step-3
[:& step-3 {:on-next on-next :on-prev on-prev :form step-3-form :show-step-3 show-step-3}]
[:& step-4 {:on-next on-submit :on-prev on-prev :form step-4-form :show-step-3 show-step-3}])
(when @show-step-3
4 [:& step-4 {:on-next on-submit :on-prev on-prev :form step-4-form :show-step-3 show-step-3}]))]]))

View file

@ -7,7 +7,6 @@
(ns app.main.ui.onboarding.team-choice
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
@ -26,8 +25,6 @@
::mf/private true}
[]
[:div {:class (stl/css :modal-left)}
[:h1 {:class (stl/css :modal-title)}
(tr "onboarding-v2.welcome.title")]
[:h2 {:class (stl/css :modal-subtitle)}
(tr "onboarding.team-modal.team-definition")]
[:p {:class (stl/css :modal-text)}
@ -54,27 +51,30 @@
[:p {:class (stl/css :modal-desc)}
(tr "onboarding.team-modal.create-team-feature-5")]]]])
(def ^:private schema:invite-form
[:map {:title "InviteForm"}
[:role :keyword]
[:emails {:optional true} [::sm/set ::sm/email]]])
(defn- get-available-roles
[]
[{:value "viewer" :label (tr "labels.viewer")}
{:value "editor" :label (tr "labels.editor")}
{:value "admin" :label (tr "labels.admin")}])
(mf/defc team-form-step-2
{::mf/props :obj}
[{:keys [name on-back go-to-team?]}]
(let [initial (mf/with-memo []
{:role "editor" :name name})
(def ^:private schema:team-form
[:map {:title "TeamForm"}
[:name [::sm/text {:max 250}]]
[:role :keyword]
[:emails {:optional true} [::sm/set ::sm/email]]])
form (fm/use-form :schema schema:invite-form
:initial initial)
(mf/defc team-form
{::mf/props :obj
::mf/private true}
[{:keys [go-to-team]}]
(let [initial (mf/with-memo []
{:role "editor"})
form (fm/use-form :schema schema:team-form
:initial initial)
roles (mf/use-memo get-available-roles)
error* (mf/use-state nil)
on-success
@ -83,7 +83,8 @@
(let [team-id (:id response)]
(st/emit! (du/update-profile-props {:onboarding-team-id team-id
:onboarding-viewed true})
(when go-to-team?
(println go-to-team)
(when go-to-team
(dcm/go-to-dashboard-recent :team-id team-id))))))
on-error
@ -145,81 +146,14 @@
(ptk/data-event ::ev/event
{::ev/name "onboarding-finish"})))))
on-submit
on-submit*
(mf/use-fn
(fn [form]
(let [params (:clean-data @form)
emails (:emails params)]
(if (> (count emails) 0)
(on-invite-now params)
(on-invite-later params)))))]
[:*
[:div {:class (stl/css :modal-right-invitations)}
[:h2 {:class (stl/css :modal-subtitle)} (tr "onboarding.choice.team-up.invite-members")]
[:p {:class (stl/css :modal-text)} (tr "onboarding.choice.team-up.invite-members-info")]
[:& fm/form {:form form
:class (stl/css :modal-form-invitations)
:on-submit on-submit}
(when-let [content (deref error*)]
[:& context-notification {:content content :level :error}])
[:div {:class (stl/css :role-select)}
[:p {:class (stl/css :role-title)} (tr "onboarding.choice.team-up.roles")]
[:& fm/select {:name :role :options roles}]]
[:div {:class (stl/css :invitation-row)}
[:& fm/multi-input {:type "email"
:name :emails
:auto-focus? true
:trim true
:valid-item-fn sm/parse-email
:caution-item-fn #{}
:label (tr "modals.invite-member.emails")
;; :on-submit on-submit
}]]
[:div {:class (stl/css :action-buttons)}
[:button {:class (stl/css :back-button)
:on-click on-back}
(tr "labels.back")]
(let [params (:clean-data @form)
emails (:emails params)]
[:> fm/submit-button*
{:class (stl/css :accept-button)
:label (if (> (count emails) 0)
(tr "onboarding.choice.team-up.create-team-and-invite")
(tr "onboarding.choice.team-up.create-team-without-invite"))}])]
[:div {:class (stl/css :modal-hint)}
"(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")"]]]
[:div {:class (stl/css :paginator)} "2/2"]]))
(def ^:private schema:team-form
[:map {:title "TeamForm"}
[:name [::sm/text {:max 250}]]])
(mf/defc team-form-step-1
{::mf/props :obj
::mf/private true}
[{:keys [on-submit]}]
(let [form (fm/use-form :schema schema:team-form
:initial {})
on-submit*
(mf/use-fn
(fn [form]
(let [name (dm/get-in @form [:clean-data :name])]
(st/emit! (ptk/data-event ::ev/event
{::ev/name "onboarding-step"
:label "team:choice-team-name"
:step 7}))
(on-submit name))))
(on-invite-later params)))))
on-skip
(mf/use-fn
@ -234,24 +168,56 @@
[:*
[:div {:class (stl/css :modal-right)}
[:div {:class (stl/css :first-block)}
[:h2 {:class (stl/css :modal-subtitle)}
(tr "onboarding.team-modal.create-team")]
[:p {:class (stl/css :modal-text)}
(tr "onboarding.choice.team-up.create-team-desc")]
[:& fm/form {:form form
:class (stl/css :modal-form)
:on-submit on-submit*}
[:h2 {:class (stl/css :modal-subtitle)}
(tr "onboarding.team-modal.create-team")]
[:p {:class (stl/css :modal-text)}
(tr "onboarding.choice.team-up.create-team-desc")]
[:& fm/input {:type "text"
:class (stl/css :team-name-input)
:name :name
:auto-focus? true
:placeholder "Team name"
:label (tr "onboarding.choice.team-up.create-team-placeholder")}]
[:div {:class (stl/css :action-buttons)}
[:> fm/submit-button*
{:class (stl/css :accept-button)
:label (tr "onboarding.choice.team-up.continue-creating-team")}]]]]
[:h2 {:class (stl/css :modal-subtitle :invite-subtitle)} (tr "onboarding.choice.team-up.invite-members")]
[:p {:class (stl/css :modal-text)} (tr "onboarding.choice.team-up.invite-members-info")]
(when-let [content (deref error*)]
[:& context-notification {:content content :level :error}])
[:div {:class (stl/css :role-select)}
[:p {:class (stl/css :role-title)} (tr "onboarding.choice.team-up.roles")]
[:& fm/select {:name :role :options roles}]]
[:div {:class (stl/css :invitation-row)}
[:& fm/multi-input {:type "email"
:name :emails
:trim true
:valid-item-fn sm/parse-email
:caution-item-fn #{}
:label (tr "modals.invite-member.emails")}]]
(let [params (:clean-data @form)
emails (:emails params)]
[:*
[:div {:class (stl/css :action-buttons)}
[:> fm/submit-button*
{:class (stl/css :accept-button)
:label (if (> (count emails) 0)
(tr "onboarding.choice.team-up.create-team-and-invite")
(tr "onboarding.choice.team-up.create-team-without-invite"))}]]
(when (= (count emails) 0)
[:> :div {:class (stl/css :modal-hint)}
"(" (tr "onboarding.choice.team-up.create-team-and-send-invites-description") ")"])])]]
[:div {:class (stl/css :second-block)}
[:h2 {:class (stl/css :modal-subtitle)}
(tr "onboarding.choice.team-up.start-without-a-team")]
@ -261,34 +227,20 @@
[:div {:class (stl/css :action-buttons)}
[:button {:class (stl/css :accept-button)
:on-click on-skip}
(tr "onboarding.choice.team-up.continue-without-a-team")]]]]
[:div {:class (stl/css :paginator)} "1/2"]]))
(tr "onboarding.choice.team-up.continue-without-a-team")]]]]]))
(mf/defc onboarding-team-modal
{::mf/props :obj}
[{:keys [go-to-team?]}]
(let [name* (mf/use-state nil)
name (deref name*)
[{:keys [go-to-team]}]
on-submit
(mf/use-fn
(fn [tname]
(swap! name* (constantly tname))))
[:div {:class (stl/css-case
:modal-overlay true)}
on-back
(mf/use-fn
(fn []
(swap! name* (constantly nil))))]
[:div {:class (stl/css-case
:modal-overlay true)}
[:div.animated.fadeIn {:class (stl/css :modal-container)}
[:& left-sidebar]
[:div {:class (stl/css :separator)}]
(if name
[:& team-form-step-2 {:name name :on-back on-back :go-to-team? go-to-team?}]
[:& team-form-step-1 {:on-submit on-submit}])]]))
[:div.animated.fadeIn {:class (stl/css :modal-container)}
[:h1 {:class (stl/css :modal-title)}
(tr "onboarding-v2.welcome.title")]
[:div {:class (stl/css :modal-sections)}
[:& left-sidebar]
[:div {:class (stl/css :separator)}]
[:& team-form {:go-to-team go-to-team}]]]])

View file

@ -12,17 +12,26 @@
.modal-container {
position: relative;
display: grid;
grid-template-columns: 1fr $s-32 1fr;
gap: $s-24;
width: $s-908;
height: $s-632;
max-height: $s-800;
height: 100%;
padding-inline: $s-100;
padding-block-start: $s-40;
padding-block-end: $s-72;
padding-block-end: $s-40;
border-radius: $br-8;
background-color: var(--modal-background-color);
border: $s-2 solid var(--modal-border-color);
display: flex;
flex-direction: column;
gap: $s-24;
}
.modal-sections {
display: grid;
grid-template-columns: 1fr $s-32 1fr;
gap: $s-24;
height: 100%;
overflow: hidden;
}
.paginator {
@ -41,14 +50,12 @@
grid-template-columns: 1fr;
grid-template-rows: $s-32 auto auto 1fr;
gap: $s-16;
max-height: $s-512;
padding-block-start: $s-44;
overflow: auto;
}
.modal-title {
@include bigTitleTipography;
color: var(--modal-title-foreground-color);
margin-bottom: $s-8;
}
.modal-subtitle {
@ -56,6 +63,10 @@
color: var(--modal-title-foreground-color);
}
.invite-subtitle {
padding-top: $s-16;
}
.modal-text {
@include bodyLargeTypography;
color: var(--modal-text-foreground-color);
@ -107,9 +118,8 @@
// SEPARATOR
.separator {
width: $s-8;
height: $s-420;
height: 100%;
border-radius: $br-8;
margin-block-start: $s-92;
opacity: 42%;
background-color: var(--modal-separator-backogrund-color);
}
@ -120,8 +130,12 @@
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
gap: $s-24;
max-height: $s-512;
margin-block-start: $s-92;
overflow: hidden;
}
.first-block {
overflow: auto;
flex-grow: 1;
}
.first-block,
@ -162,7 +176,6 @@
grid-template-rows: auto auto 1fr;
gap: $s-16;
max-height: $s-512;
margin-block-start: $s-92;
}
.modal-form-invitations {

View file

@ -162,7 +162,7 @@
:register-validate
[:div {:class (stl/css :form-container)}
[:& register/register-validate-form
[:& register/register-form
{:params {:token @register-token}
:on-success-callback register-email-sent}]
[:div {:class (stl/css :links)}

View file

@ -12,7 +12,7 @@
[app.main.store :as st]
[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 terms-register]]
[app.main.ui.auth.register :refer [register-methods register-success-page terms-register register-validate-form]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]