mirror of
https://github.com/penpot/penpot.git
synced 2025-05-17 17:46:10 +02:00
498 lines
20 KiB
Clojure
498 lines
20 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC
|
|
|
|
(ns app.main.ui.onboarding.questions
|
|
"External form for onboarding questions."
|
|
(:require-macros [app.main.style :as stl])
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.schema :as sm]
|
|
[app.main.data.events :as-alias ev]
|
|
[app.main.data.profile :as du]
|
|
[app.main.store :as st]
|
|
[app.main.ui.components.forms :as fm]
|
|
[app.main.ui.icons :as i]
|
|
[app.util.i18n :as i18n :refer [tr]]
|
|
[cuerdas.core :as str]
|
|
[potok.v2.core :as ptk]
|
|
[rumext.v2 :as mf]))
|
|
|
|
(mf/defc step-container
|
|
{::mf/props :obj}
|
|
[{:keys [form step on-next on-prev children class label]}]
|
|
|
|
(let [on-next*
|
|
(mf/use-fn
|
|
(mf/deps on-next step label)
|
|
(fn [form event]
|
|
(let [params (-> (:clean-data @form)
|
|
(assoc :label label)
|
|
(assoc :step step)
|
|
(assoc ::ev/name "onboarding-step"))]
|
|
(st/emit! (ptk/data-event ::ev/event params))
|
|
(on-next form event))))]
|
|
|
|
[:& 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
|
|
|
|
[:div {:class (stl/css :action-buttons)}
|
|
|
|
(when (some? on-prev)
|
|
[:button {:class (stl/css :prev-button)
|
|
:on-click on-prev}
|
|
(tr "labels.previous")])
|
|
|
|
[:> fm/submit-button*
|
|
{:label (if (< step 5)
|
|
(tr "labels.next")
|
|
(tr "labels.start"))
|
|
:class (stl/css :next-button)}]]]))
|
|
|
|
(def ^:private schema:questions-form-1
|
|
[:and
|
|
|
|
[:map {:title "QuestionsFormStep1"}
|
|
[:planning ::sm/text]
|
|
[:expected-use [:enum "work" "education" "personal"]]
|
|
[:planning-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)))))]])
|
|
|
|
(mf/defc step-1
|
|
{::mf/props :obj}
|
|
[{:keys [on-next form]}]
|
|
(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
|
|
(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-planning
|
|
(dm/get-in @form [:data :planning])]
|
|
|
|
[:& step-container {:form form
|
|
:step 1
|
|
:label "questions:about-you"
|
|
:on-next on-next
|
|
:class (stl/css :step-1)}
|
|
|
|
[:img {:class (stl/css :header-image)
|
|
:src "images/form/use-for-1.png"
|
|
:alt (tr "onboarding.questions.lets-get-started")}]
|
|
[:h1 {:class (stl/css :modal-title)}
|
|
(tr "onboarding.questions.step1.title")]
|
|
[:p {:class (stl/css :modal-text)}
|
|
(tr "onboarding.questions.step1.subtitle")]
|
|
|
|
[:div {:class (stl/css :modal-question)}
|
|
[:h3 {:class (stl/css :modal-subtitle)}
|
|
(tr "onboarding.questions.step1.question1")]
|
|
|
|
[:& fm/radio-buttons {:options use-options
|
|
:name :expected-use
|
|
:class (stl/css :radio-btns)}]
|
|
|
|
[:h3 {:class (stl/css :modal-subtitle)}
|
|
(tr "onboarding.questions.step1.question2")]
|
|
|
|
[:& 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
|
|
:class (stl/css :input-spacing)
|
|
:placeholder (tr "labels.other")
|
|
:show-error false
|
|
:label ""}])]]))
|
|
|
|
(def ^:private schema:questions-form-2
|
|
[:and
|
|
[:map {:title "QuestionsFormStep2"}
|
|
[:experience-design-tool
|
|
[:enum "figma" "sketch" "adobe-xd" "canva" "invision" "other"]]
|
|
[:experience-design-tool-other {:optional true}
|
|
[::sm/text {:max 512}]]]
|
|
|
|
[:fn {:error/field :experience-design-tool-other}
|
|
(fn [data]
|
|
(let [experience (:experience-design-tool data)
|
|
experience-other (:experience-design-tool-other data)]
|
|
(or (not= experience "other")
|
|
(and (= experience "other")
|
|
(not (str/blank? experience-other))))))]])
|
|
|
|
(mf/defc step-2
|
|
{::mf/props :obj}
|
|
[{:keys [on-next on-prev form]}]
|
|
(let [design-tool-options
|
|
(mf/with-memo []
|
|
(-> (shuffle [{:label (tr "labels.figma") :img-width "48px" :img-height "60px"
|
|
:value "figma" :image "images/form/figma.png"}
|
|
{:label (tr "labels.sketch") :img-width "48px" :img-height "60px"
|
|
:value "sketch" :image "images/form/sketch.png"}
|
|
{:label (tr "labels.adobe-xd") :img-width "48px" :img-height "60px"
|
|
:value "adobe-xd" :image "images/form/adobe-xd.png"}
|
|
{:label (tr "labels.canva") :img-width "48px" :img-height "60px"
|
|
:value "canva" :image "images/form/canva.png"}
|
|
{:label (tr "labels.invision") :img-width "48px" :img-height "60px"
|
|
:value "invision" :image "images/form/invision.png"}])
|
|
(conj {:label (tr "labels.other-short") :value "other" :icon i/curve})))
|
|
|
|
current-experience
|
|
(dm/get-in @form [:data :experience-design-tool])
|
|
|
|
on-design-tool-change
|
|
(mf/use-fn
|
|
(mf/deps current-experience)
|
|
(fn []
|
|
(when (not= current-experience "other")
|
|
(swap! form d/dissoc-in [:data :experience-design-tool-other])
|
|
(swap! form d/dissoc-in [:errors :experience-design-tool-other]))))]
|
|
|
|
[:& step-container {:form form
|
|
:step 2
|
|
:label "questions:experience-design-tool"
|
|
:on-next on-next
|
|
:on-prev on-prev
|
|
:class (stl/css :step-2)}
|
|
|
|
[:h1 {:class (stl/css :modal-title)}
|
|
(tr "onboarding.questions.step2.title")]
|
|
[:div {:class (stl/css :radio-wrapper)}
|
|
[:& fm/image-radio-buttons {:options design-tool-options
|
|
:img-width "48px"
|
|
:img-height "60px"
|
|
:name :experience-design-tool
|
|
:image true
|
|
:class (stl/css :image-radio)
|
|
:on-change on-design-tool-change}]
|
|
|
|
(when (= current-experience "other")
|
|
[:& fm/input {:name :experience-design-tool-other
|
|
:class (stl/css :input-spacing)
|
|
:placeholder (tr "labels.other")
|
|
:show-error false
|
|
:label ""}])]]))
|
|
|
|
|
|
(def ^:private schema:questions-form-3
|
|
[:and
|
|
[: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}]]]
|
|
|
|
[:fn {:error/field :role-other}
|
|
(fn [{:keys [role role-other]}]
|
|
(or (not= role "other")
|
|
(and (= role "other")
|
|
(not (str/blank? role-other)))))]
|
|
|
|
[:fn {:error/field :responsability-other}
|
|
(fn [{:keys [responsability responsability-other]}]
|
|
(or (not= responsability "other")
|
|
(and (= responsability "other")
|
|
(not (str/blank? responsability-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
|
|
(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"}
|
|
{:label (tr "onboarding.questions.team-size.31-50") :value "31-50" :key "31-50"}
|
|
{:label (tr "onboarding.questions.team-size.11-30") :value "11-30" :key "11-30"}
|
|
{:label (tr "onboarding.questions.team-size.2-10") :value "2-10" :key "2-10"}
|
|
{: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])
|
|
|
|
current-responsability
|
|
(dm/get-in @form [:data :responsability])]
|
|
|
|
[:& step-container {:form form
|
|
:step 3
|
|
:label "questions:about-your-job"
|
|
:on-next on-next
|
|
:on-prev on-prev
|
|
:class (stl/css :step-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}]
|
|
|
|
(when (= current-role "other")
|
|
[:& fm/input {:name :role-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.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 ""}])]
|
|
|
|
[:div {:class (stl/css :modal-question)}
|
|
[:h3 {:class (stl/css :modal-subtitle)} (tr "onboarding.questions.step3.question3")]
|
|
[:& fm/select {:options team-size-options
|
|
:default ""
|
|
:select-class (stl/css :select-class)
|
|
:name :team-size}]]]))
|
|
|
|
(def ^:private schema:questions-form-4
|
|
[:and
|
|
[:map {:title "QuestionsFormStep4"}
|
|
[:start-with
|
|
[:enum "ui" "wireframing" "prototyping" "ds" "code" "other"]]
|
|
[:start-with-other {:optional true} [::sm/text {:max 512}]]]
|
|
|
|
[:fn {:error/field :start-with-other}
|
|
(fn [{:keys [start-with start-with-other]}]
|
|
(or (not= start-with "other")
|
|
(and (= start-with "other")
|
|
(not (str/blank? start-with-other)))))]])
|
|
|
|
(mf/defc step-4
|
|
{::mf/props :obj}
|
|
[{:keys [on-next on-prev form]}]
|
|
(let [start-options
|
|
(mf/with-memo []
|
|
(-> (shuffle [{:label (tr "onboarding.questions.start-with.ui")
|
|
:value "ui" :image "images/form/Design.png"}
|
|
{:label (tr "onboarding.questions.start-with.wireframing")
|
|
:value "wireframing" :image "images/form/templates.png"}
|
|
{:label (tr "onboarding.questions.start-with.prototyping")
|
|
:value "prototyping" :image "images/form/Prototype.png"}
|
|
{:label (tr "onboarding.questions.start-with.ds")
|
|
:value "ds" :image "images/form/components.png"}
|
|
{:label (tr "onboarding.questions.start-with.code")
|
|
:value "code" :image "images/form/design-and-dev.png"}])
|
|
(conj {:label (tr "labels.other-short") :value "other" :icon i/curve})))
|
|
|
|
current-start (dm/get-in @form [:data :start-with])
|
|
|
|
on-start-change
|
|
(mf/use-fn
|
|
(mf/deps current-start)
|
|
(fn [_ _]
|
|
(when (not= current-start "other")
|
|
(swap! form d/dissoc-in [:data :start-with-other])
|
|
(swap! form d/dissoc-in [:errors :start-with-other]))))]
|
|
|
|
[:& step-container {:form form
|
|
:step 4
|
|
:label "questions:how-start"
|
|
:on-next on-next
|
|
:on-prev on-prev
|
|
:class (stl/css :step-4)}
|
|
|
|
[:h1 {:class (stl/css :modal-title)} (tr "onboarding.questions.step4.title")]
|
|
[:div {:class (stl/css :radio-wrapper)}
|
|
[:& fm/image-radio-buttons {:options start-options
|
|
:img-width "159px"
|
|
:img-height "120px"
|
|
:name :start-with
|
|
:on-change on-start-change}]
|
|
|
|
(when (= current-start "other")
|
|
[:& fm/input {:name :start-with-other
|
|
:class (stl/css :input-spacing)
|
|
:label ""
|
|
: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 {})
|
|
|
|
;; Forms are initialized here because we can go back and forth between the steps
|
|
;; and we want to keep the filled info
|
|
step-1-form (fm/use-form
|
|
:initial {}
|
|
:schema schema:questions-form-1)
|
|
|
|
step-2-form (fm/use-form
|
|
:initial {}
|
|
:schema schema:questions-form-2)
|
|
|
|
step-3-form (fm/use-form
|
|
:initial {}
|
|
:schema schema:questions-form-3)
|
|
|
|
step-4-form (fm/use-form
|
|
: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]
|
|
(swap! step inc)
|
|
(swap! clean-data merge (:clean-data @form))))
|
|
|
|
on-prev
|
|
(mf/use-fn
|
|
(fn []
|
|
(swap! step dec)))
|
|
|
|
on-submit
|
|
(mf/use-fn
|
|
(mf/deps @clean-data)
|
|
(fn [form]
|
|
(let [data (merge @clean-data (:clean-data @form))]
|
|
(reset! clean-data data)
|
|
(st/emit! (du/mark-questions-as-answered data)))))]
|
|
|
|
[:div {:class (stl/css-case
|
|
:modal-overlay true)}
|
|
[:div {:class (stl/css :modal-container)
|
|
: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}])]]))
|