diff --git a/CHANGES.md b/CHANGES.md index 86e915656..ae4ad9f1b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,20 @@ # CHANGELOG +## 2.8.0 (Next / Unreleased) + +### :rocket: Epics and highlights + +### :boom: Breaking changes & Deprecations + +### :heart: Community contributions (Thank you!) + +### :sparkles: New features + +- Optimize profile setup flow for better user experience [Taiga #10028](https://tree.taiga.io/project/penpot/us/10028) + +### :bug: Bugs fixed + + ## 2.7.0 (Unreleased) ### :rocket: Epics and highlights @@ -10,7 +25,6 @@ ### :sparkles: New features - - Update board presets with a newer devices [Taiga #10610](https://tree.taiga.io/project/penpot/us/10610) - Propagate "sharing a prototype" to editors and viewers [Taiga #8853](https://tree.taiga.io/project/penpot/us/8853) - Design improvements to the Invitations page with an empty state [Taiga #4554](https://tree.taiga.io/project/penpot/us/4554) diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index 9307ebab8..04c28dc02 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -231,7 +231,7 @@ :hint "email has complaint reports"))) (defn prepare-register - [{:keys [::db/pool] :as cfg} {:keys [email] :as params}] + [{:keys [::db/pool] :as cfg} {:keys [email accept-newsletter-updates] :as params}] (validate-register-attempt! cfg params) @@ -243,7 +243,8 @@ :backend "penpot" :iss :prepared-register :profile-id (:id profile) - :exp (dt/in-future {:days 7})} + :exp (dt/in-future {:days 7}) + :props {:newsletter-updates (or accept-newsletter-updates false)}} params (d/without-nils params) token (tokens/generate (::setup/props cfg) params)] diff --git a/frontend/playwright/ui/pages/OnboardingPage.js b/frontend/playwright/ui/pages/OnboardingPage.js index 81e199588..3ba9f3097 100644 --- a/frontend/playwright/ui/pages/OnboardingPage.js +++ b/frontend/playwright/ui/pages/OnboardingPage.js @@ -9,7 +9,7 @@ export class OnboardingPage extends BaseWebSocketPage { async fillOnboardingInputsStep1() { await this.page.getByText("Personal").click(); await this.page.getByText("Select option").click(); - await this.page.getByText("Testing before self-hosting").click(); + await this.page.getByText("Product Managment").click(); await this.submitButton.click(); } @@ -21,24 +21,8 @@ export class OnboardingPage extends BaseWebSocketPage { } async fillOnboardingInputsStep3() { - await this.page.getByText("Select option").first().click(); - await this.page.getByText("Product Managment").click(); - await this.page.getByText("Select option").first().click(); - await this.page.getByText("Director").click(); - await this.page.getByText("Select option").click(); - await this.page.getByText("11-30").click(); - - await this.submitButton.click(); - } - - async fillOnboardingInputsStep4() { await this.page.getByText("Other").click(); await this.page.getByPlaceholder("Other (specify)").fill("Another"); - await this.submitButton.click(); - } - - async fillOnboardingInputsStep5() { - await this.page.getByText("Event").click(); } } diff --git a/frontend/playwright/ui/specs/onboarding.spec.js b/frontend/playwright/ui/specs/onboarding.spec.js index 968c88825..b39c3b958 100644 --- a/frontend/playwright/ui/specs/onboarding.spec.js +++ b/frontend/playwright/ui/specs/onboarding.spec.js @@ -26,20 +26,11 @@ test("User can complete the onboarding", async ({ page }) => { ).toBeVisible(); await onboardingPage.fillOnboardingInputsStep2(); - await expect( - page.getByRole("heading", { name: "Tell us about your job" }), - ).toBeVisible(); - - await onboardingPage.fillOnboardingInputsStep3(); await expect( page.getByRole("heading", { name: "Where would you like to get" }), ).toBeVisible(); - await onboardingPage.fillOnboardingInputsStep4(); - await expect( - page.getByRole("heading", { name: "How did you hear about Penpot?" }), - ).toBeVisible(); + await onboardingPage.fillOnboardingInputsStep3(); - await onboardingPage.fillOnboardingInputsStep5(); await expect(page.getByRole("button", { name: "Start" })).toBeEnabled(); }); diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index f43e86cba..ddf43430e 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -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)}])) diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs index 7a5acbd44..a39b2b310 100644 --- a/frontend/src/app/main/ui/auth.cljs +++ b/frontend/src/app/main/ui/auth.cljs @@ -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}] diff --git a/frontend/src/app/main/ui/auth.scss b/frontend/src/app/main/ui/auth.scss index 4b3caeefc..7a4d67c72 100644 --- a/frontend/src/app/main/ui/auth.scss +++ b/frontend/src/app/main/ui/auth.scss @@ -22,6 +22,16 @@ display: flex; justify-content: center; } + + &.register { + display: flex; + justify-content: center; + align-items: center; + + .login-illustration { + display: none; + } + } } .logo-container { diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index ea75d0605..8e290054f 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -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")])]))) - diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index ce5ea9996..f8f0ea302 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -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 diff --git a/frontend/src/app/main/ui/components/forms.scss b/frontend/src/app/main/ui/components/forms.scss index b31713aad..7d09baa4e 100644 --- a/frontend/src/app/main/ui/components/forms.scss +++ b/frontend/src/app/main/ui/components/forms.scss @@ -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); diff --git a/frontend/src/app/main/ui/onboarding/questions.cljs b/frontend/src/app/main/ui/onboarding/questions.cljs index 6f5592db5..da576fe4c 100644 --- a/frontend/src/app/main/ui/onboarding/questions.cljs +++ b/frontend/src/app/main/ui/onboarding/questions.cljs @@ -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}]))]])) diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs index 1bec75b40..59933d184 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.cljs +++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs @@ -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}]]]]) diff --git a/frontend/src/app/main/ui/onboarding/team_choice.scss b/frontend/src/app/main/ui/onboarding/team_choice.scss index 25437a67c..95d275e86 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.scss +++ b/frontend/src/app/main/ui/onboarding/team_choice.scss @@ -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 { diff --git a/frontend/src/app/main/ui/static.cljs b/frontend/src/app/main/ui/static.cljs index d81d5959a..6773b12d3 100644 --- a/frontend/src/app/main/ui/static.cljs +++ b/frontend/src/app/main/ui/static.cljs @@ -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)} diff --git a/frontend/src/app/main/ui/viewer/login.cljs b/frontend/src/app/main/ui/viewer/login.cljs index c8b02b2f1..e17904888 100644 --- a/frontend/src/app/main/ui/viewer/login.cljs +++ b/frontend/src/app/main/ui/viewer/login.cljs @@ -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]]