diff --git a/backend/src/app/rpc/mutations/demo.clj b/backend/src/app/rpc/mutations/demo.clj index 8072b38e2..12786757f 100644 --- a/backend/src/app/rpc/mutations/demo.clj +++ b/backend/src/app/rpc/mutations/demo.clj @@ -36,7 +36,8 @@ :is-active true :deleted-at (dt/in-future cf/deletion-delay) :password password - :props {:onboarding-viewed true}}] + :props {} + }] (when-not (contains? cf/flags :demo-users) (ex/raise :type :validation diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index bff16696c..8ec241b29 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -859,23 +859,23 @@ background-position: left top; background-size: 11%; } - + .modal-left:hover { background-image: url("/images/on-solo-hover.svg"); background-size: 15%; } - + .modal-right { background-image: url("/images/on-teamup.svg"); background-position: right top; background-size: 28%; } - + .modal-right:hover { background-image: url("/images/on-teamup-hover.svg"); background-size: 32%; } - + .modal-right, .modal-left { background-repeat: no-repeat; @@ -1001,17 +1001,17 @@ .template-item { width: 275px; border: 1px solid $color-gray-10; - + display: flex; flex-direction: column; text-align: left; border-radius: $br-small; - + &:not(:last-child) { margin-bottom: 22px; } } - + .template-item-content { // height: 144px; flex-grow: 1; @@ -1020,7 +1020,7 @@ border-radius: $br-small $br-small 0 0; } } - + .template-item-title { padding: 6px 12px; height: 64px; @@ -1135,3 +1135,45 @@ } } + + + +.questions-form { + .modal-overlay { + z-index: 2001; + } + + .modal-container { + width: 90vw; + height: 90vh; + display: flex; + flex-direction: row; + justify-content: center; + + .af-form { + --primary-color: #00C38B; + --input-background-color: #ffffff; + --label-font-size: $fs16; + --field-error-font-color: #E65244; + --message-success-font-color: #49D793; + --message-fail-font-color: #E65244; + --invalid-field-border-color: #E65244; + --dropdown-background-color: #ffffff; + --primary-font-color: #000; + --input-border-color: rgb(224, 230, 240); + --input-border-radius: 3px; + --button-border-radius: 3px; + --message-border-radius: 3px; + --checkbox-border-radius: 3px; + --dropdown-option-background-color: rgba(0,195,139,1); + --dropdown-option-active-background-color: rgba(0,138,98,1); + --invalid-field-background-color: rgba(238.51780000000002,205.7178,204.11780000000002,1); + --message-fail-background-color: rgba(238.51780000000002,205.7178,204.11780000000002,1); + --message-success-background-color: rgba(171,232,197,1); + } + } + + .modal-overlay { + background-color: rgba(0,0,0,0.9); + } +} diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 7e8e4c5c1..12ec5aa4a 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -78,6 +78,7 @@ (def translations (obj/get global "penpotTranslations")) (def themes (obj/get global "penpotThemes")) (def sentry-dsn (obj/get global "penpotSentryDsn")) +(def onboarding-form-id (obj/get global "penpotOnboardingQuestionsFormId")) (def flags (atom (parse-flags global))) (def version (atom (parse-version global))) diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index e3965278a..445d9aa19 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -161,14 +161,8 @@ (->> (rx/concat (rx/of (profile-fetched profile) (fetch-teams)) - (->> (rx/of (rt/nav' :dashboard-projects {:team-id team-id})) - (rx/delay 1000)) - - (when-not (get-in profile [:props :onboarding-viewed]) - (->> (rx/of (modal/show {:type :onboarding})) - (rx/delay 1000)))) - + (rx/delay 1000))) (rx/observe-on :async)))))) (s/def ::login-params @@ -355,12 +349,26 @@ ptk/WatchEvent (watch [_ state _] (let [version (or version (:main @cf/version)) - props (-> (get-in state [:profile :props]) - (assoc :onboarding-viewed true) - (assoc :release-notes-viewed version))] + props {:onboarding-viewed true + :release-notes-viewed version}] (->> (rp/mutation :update-profile-props {:props props}) (rx/map (constantly (fetch-profile))))))))) + +(defn mark-questions-as-answered + [] + (ptk/reify ::mark-questions-as-answered + ptk/UpdateEvent + (update [_ state] + (update-in state [:profile :props] assoc :onboarding-questions-answered true)) + + ptk/WatchEvent + (watch [_ _ _] + (let [props {:onboarding-questions-answered true}] + (->> (rp/mutation :update-profile-props {:props props}) + (rx/map (constantly (fetch-profile)))))))) + + ;; --- Update Photo (defn update-photo diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 223e87115..a3b43684f 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -6,6 +6,7 @@ (ns app.main.ui (:require + [app.config :as cf] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.auth :refer [auth]] @@ -17,6 +18,7 @@ [app.main.ui.icons :as i] [app.main.ui.messages :as msgs] [app.main.ui.onboarding] + [app.main.ui.releases] [app.main.ui.render :as render] [app.main.ui.settings :as settings] [app.main.ui.static :as static] @@ -32,7 +34,7 @@ (mf/defc main-page {::mf/wrap [#(mf/catch % {:fallback on-main-error})]} - [{:keys [route] :as props}] + [{:keys [route profile]}] (let [{:keys [data params]} route] [:& (mf/provider ctx/current-route) {:value route} (case (:name data) @@ -70,13 +72,29 @@ :dashboard-font-providers :dashboard-team-members :dashboard-team-settings) - [:* - #_[:div.modal-wrapper - #_[:& app.main.ui.onboarding/onboarding-templates-modal] - [:& app.main.ui.onboarding/onboarding-modal] - #_[:& app.main.ui.onboarding/onboarding-team-modal] - ] - [:& dashboard {:route route}]] + (when profile + (let [props (:props profile)] + [:* + #_[:div.modal-wrapper + #_[:& app.main.ui.onboarding/onboarding-templates-modal] + [:& app.main.ui.onboarding/onboarding-modal] + #_[:& app.main.ui.onboarding/onboarding-team-modal] + ] + (cond + (and cf/onboarding-form-id + (not (:onboarding-questions-answered props false))) + [:& app.main.ui.onboarding.questions/questions + {:profile profile + :form-id cf/onboarding-form-id}] + + (not (:onboarding-viewed props)) + [:& app.main.ui.onboarding/onboarding-modal {}] + + (and (:onboarding-viewed props) + (not= (:release-notes-viewed props) (:main @cf/version)) + (not= "0.0" (:main @cf/version))) + [:& app.main.ui.releases/release-notes-modal {}]) + [:& dashboard {:route route}]])) :viewer (let [{:keys [query-params path-params]} route @@ -124,12 +142,14 @@ (mf/defc app [] - (let [route (mf/deref refs/route) - edata (mf/deref refs/exception)] + (let [route (mf/deref refs/route) + edata (mf/deref refs/exception) + profile (mf/deref refs/profile)] [:& (mf/provider ctx/current-route) {:value route} - (if edata - [:& static/exception-page {:data edata}] - [:* - [:& msgs/notifications] - (when route - [:& main-page {:route route}])])])) + [:& (mf/provider ctx/current-profile) {:value profile} + (if edata + [:& static/exception-page {:data edata}] + [:* + [:& msgs/notifications] + (when route + [:& main-page {:route route :profile profile}])])]])) diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index e91e4e82a..4fd5d6a1f 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -15,8 +15,9 @@ ;; for text shapes in the export process (def text-plain-colors-ctx (mf/create-context false)) -(def current-route (mf/create-context nil)) -(def current-team-id (mf/create-context nil)) +(def current-route (mf/create-context nil)) +(def current-profile (mf/create-context nil)) +(def current-team-id (mf/create-context nil)) (def current-project-id (mf/create-context nil)) -(def current-page-id (mf/create-context nil)) -(def current-file-id (mf/create-context nil)) +(def current-page-id (mf/create-context nil)) +(def current-file-id (mf/create-context nil)) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index f99f8cd6b..f5a7a40ab 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -96,17 +96,6 @@ (mf/deps team-id) (st/emitf (dd/initialize {:id team-id}))) - (mf/use-effect - (mf/deps) - (fn [] - (let [props (:props profile) - version (:release-notes-viewed props)] - (when (and (:onboarding-viewed props) - (not= version (:main @cf/version)) - (not= "0.0" (:main @cf/version))) - (tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes - :version (:main @cf/version)}))))))) - [:& (mf/provider ctx/current-team-id) {:value team-id} [:& (mf/provider ctx/current-project-id) {:value project-id} ;; NOTE: dashboard events and other related functions assumes diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs index c2286f06c..0f31f117d 100644 --- a/frontend/src/app/main/ui/onboarding.cljs +++ b/frontend/src/app/main/ui/onboarding.cljs @@ -6,32 +6,16 @@ (ns app.main.ui.onboarding (:require - [app.common.spec :as us] [app.config :as cf] - [app.main.data.dashboard :as dd] - [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.data.users :as du] - [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.components.forms :as fm] - [app.main.ui.icons :as i] + [app.main.ui.onboarding.questions] + [app.main.ui.onboarding.team-choice] + [app.main.ui.onboarding.templates] [app.main.ui.releases.common :as rc] - [app.main.ui.releases.v1-10] - [app.main.ui.releases.v1-4] - [app.main.ui.releases.v1-5] - [app.main.ui.releases.v1-6] - [app.main.ui.releases.v1-7] - [app.main.ui.releases.v1-8] - [app.main.ui.releases.v1-9] - [app.util.dom :as dom] - [app.util.http :as http] [app.util.i18n :as i18n :refer [tr]] - [app.util.object :as obj] - [app.util.router :as rt] [app.util.timers :as tm] - [beicon.core :as rx] - [cljs.spec.alpha :as s] [rumext.alpha :as mf])) ;; --- ONBOARDING LIGHTBOX @@ -189,297 +173,3 @@ :slide @slide :navigate navigate :skip skip)))]])) - -(s/def ::name ::us/not-empty-string) -(s/def ::team-form - (s/keys :req-un [::name])) - -(mf/defc onboarding-choice-modal - {::mf/register modal/components - ::mf/register-as :onboarding-choice} - [] - (let [;; When user choices the option of `fly solo`, we proceed to show - ;; the onboarding templates modal. - on-fly-solo - (fn [] - (tm/schedule 400 #(st/emit! (modal/show {:type :onboarding-templates})))) - - ;; When user choices the option of `team up`, we proceed to show - ;; the team creation modal. - on-team-up - (fn [] - (st/emit! (modal/show {:type :onboarding-team}))) - ] - - [:div.modal-overlay - [:div.modal-container.onboarding.final.animated.fadeInUp - [:div.modal-top - [:h1 (tr "onboarding.choice.title")] - [:p (tr "onboarding.choice.desc")]] - [:div.modal-columns - [:div.modal-left - [:div.content-button {:on-click on-fly-solo} - [:h2 (tr "onboarding.choice.fly-solo")] - [:p (tr "onboarding.choice.fly-solo-desc")]]] - [:div.modal-right - [:div.content-button {:on-click on-team-up} - [:h2 (tr "onboarding.choice.team-up")] - [:p (tr "onboarding.choice.team-up-desc")]]]] - [:img.deco {:src "images/deco-left.png" :border "0"}] - [:img.deco.right {:src "images/deco-right.png" :border "0"}]]])) - -(mf/defc onboarding-team-modal - {::mf/register modal/components - ::mf/register-as :onboarding-team} - [] - (let [form (fm/use-form :spec ::team-form - :initial {}) - on-submit - (mf/use-callback - (fn [form _] - (let [tname (get-in @form [:clean-data :name])] - (st/emit! (modal/show {:type :onboarding-team-invitations :name tname})))))] - - [:div.modal-overlay - [:div.modal-container.onboarding-team - [:div.title - [:h2 (tr "onboarding.choice.team-up")] - [:p (tr "onboarding.choice.team-up-desc")]] - - [:& fm/form {:form form - :on-submit on-submit} - - [:div.team-row - [:& fm/input {:type "text" - :name :name - :label (tr "onboarding.team-input-placeholder")}]] - - [:div.buttons - [:button.btn-secondary.btn-large - {:on-click #(st/emit! (modal/show {:type :onboarding-choice}))} - (tr "labels.cancel")] - [:& fm/submit-button - {:label (tr "labels.next")}]]] - - [:img.deco {:src "images/deco-left.png" :border "0"}] - [:img.deco.right {:src "images/deco-right.png" :border "0"}]]])) - -(defn get-available-roles - [] - [{:value "editor" :label (tr "labels.editor")} - {:value "admin" :label (tr "labels.admin")}]) - -(s/def ::email ::us/email) -(s/def ::role ::us/keyword) -(s/def ::invite-form - (s/keys :req-un [::role ::email])) - -;; This is the final step of team creation, consists in provide a -;; shortcut for invite users. - -(mf/defc onboarding-team-invitations-modal - {::mf/register modal/components - ::mf/register-as :onboarding-team-invitations} - [{:keys [name] :as props}] - (let [initial (mf/use-memo (constantly - {:role "editor" - :name name})) - form (fm/use-form :spec ::invite-form - :initial initial) - - roles (mf/use-memo #(get-available-roles)) - - on-success - (mf/use-callback - (fn [_form response] - (let [project-id (:default-project-id response) - team-id (:id response)] - (st/emit! - (modal/hide) - (rt/nav :dashboard-projects {:team-id team-id})) - (tm/schedule 400 #(st/emit! - (modal/show {:type :onboarding-templates - :project-id project-id})))))) - - on-error - (mf/use-callback - (fn [_form _response] - (st/emit! (dm/error "Error on creating team.")))) - - ;; The SKIP branch only creates the team, without invitations - on-skip - (mf/use-callback - (fn [_] - (let [mdata {:on-success (partial on-success form) - :on-error (partial on-error form)} - params {:name name}] - (st/emit! (dd/create-team (with-meta params mdata)))))) - - ;; The SUBMIT branch creates the team with the invitations - on-submit - (mf/use-callback - (fn [form _] - (let [mdata {:on-success (partial on-success form) - :on-error (partial on-error form)} - params (:clean-data @form)] - (st/emit! (dd/create-team-with-invitations (with-meta params mdata))))))] - - [:div.modal-overlay - [:div.modal-container.onboarding-team - [:div.title - [:h2 (tr "onboarding.choice.team-up")] - [:p (tr "onboarding.choice.team-up-desc")]] - - [:& fm/form {:form form - :on-submit on-submit} - - [:div.invite-row - [:& fm/input {:name :email - :label (tr "labels.email")}] - [:& fm/select {:name :role - :options roles}]] - - [:div.buttons - [:button.btn-secondary.btn-large - {:on-click #(st/emit! (modal/show {:type :onboarding-choice}))} - (tr "labels.cancel")] - [:& fm/submit-button - {:label (tr "labels.create")}]] - [:div.skip-action - {:on-click on-skip} - [:div.action "Skip and invite later"]]] - [:img.deco {:src "images/deco-left.png" :border "0"}] - [:img.deco.right {:src "images/deco-right.png" :border "0"}]]])) - -(mf/defc template-item - [{:keys [name path image project-id]}] - (let [downloading? (mf/use-state false) - link (str (assoc cf/public-uri :path path)) - - on-finish-import - (fn [] - (st/emit! (dd/fetch-files {:project-id project-id}) - (dd/fetch-recent-files) - (dd/clear-selected-files))) - - open-import-modal - (fn [file] - (st/emit! (modal/show - {:type :import - :project-id project-id - :files [file] - :on-finish-import on-finish-import}))) - on-click - (fn [] - (reset! downloading? true) - (->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors}) - (rx/subs (fn [{:keys [body] :as response}] - (open-import-modal {:name name :uri (dom/create-uri body)})) - (fn [error] - (js/console.log "error" error)) - (fn [] - (reset! downloading? false))))) - ] - - [:div.template-item - [:div.template-item-content - [:img {:src image}]] - [:div.template-item-title - [:div.label name] - (if @downloading? - [:div.action "Fetching..."] - [:div.action {:on-click on-click} "+ Add to drafts"])]])) - -(mf/defc onboarding-templates-modal - {::mf/register modal/components - ::mf/register-as :onboarding-templates} - ;; NOTE: the project usually comes empty, it only comes fulfilled - ;; when a user creates a new team just after signup. - [{:keys [project-id] :as props}] - (let [close-fn (mf/use-callback #(st/emit! (modal/hide))) - profile (mf/deref refs/profile) - project-id (or project-id (:default-project-id profile))] - [:div.modal-overlay - [:div.modal-container.onboarding-templates - [:div.modal-header - [:div.modal-close-button - {:on-click close-fn} i/close]] - - [:div.modal-content - [:h3 (tr "onboarding.templates.title")] - [:p (tr "onboarding.templates.subtitle")] - - [:div.templates - [:& template-item - {:path "/github/penpot-files/Penpot-Design-system.penpot" - :image "https://penpot.app/images/libraries/cover-ds-penpot.jpg" - :name "Penpot Design System" - :project-id project-id}] - [:& template-item - {:path "/github/penpot-files/Material-Design-Kit.penpot" - :image "https://penpot.app/images/libraries/cover-material.jpg" - :name "Material Design Kit" - :project-id project-id}]]]]])) - - -;;; --- RELEASE NOTES MODAL - -(mf/defc release-notes - [{:keys [version] :as props}] - (let [slide (mf/use-state :start) - klass (mf/use-state "fadeInDown") - - navigate - (mf/use-callback #(reset! slide %)) - - next - (mf/use-callback - (mf/deps slide) - (fn [] - (if (= @slide :start) - (navigate 0) - (navigate (inc @slide))))) - - finish - (mf/use-callback - (st/emitf (modal/hide) - (du/mark-onboarding-as-viewed {:version version}))) - ] - - (mf/use-effect - (mf/deps) - (fn [] - (st/emitf (du/mark-onboarding-as-viewed {:version version})))) - - (mf/use-layout-effect - (mf/deps @slide) - (fn [] - (when (not= :start @slide) - (reset! klass "fadeIn")) - (let [sem (tm/schedule 300 #(reset! klass nil))] - (fn [] - (reset! klass nil) - (tm/dispose! sem))))) - - (rc/render-release-notes - {:next next - :navigate navigate - :finish finish - :klass klass - :slide slide - :version version}))) - -(mf/defc release-notes-modal - {::mf/wrap-props false - ::mf/register modal/components - ::mf/register-as :release-notes} - [props] - (let [versions (methods rc/render-release-notes) - version (obj/get props "version")] - (when (contains? versions version) - [:div.relnotes - [:> release-notes props]]))) - -(defmethod rc/render-release-notes "0.0" - [params] - (rc/render-release-notes (assoc params :version "1.10"))) diff --git a/frontend/src/app/main/ui/onboarding/questions.cljs b/frontend/src/app/main/ui/onboarding/questions.cljs new file mode 100644 index 000000000..bfe81c034 --- /dev/null +++ b/frontend/src/app/main/ui/onboarding/questions.cljs @@ -0,0 +1,50 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.onboarding.questions + "External form for onboarding questions." + (:require + [app.config :as cf] + [app.main.data.users :as du] + [app.main.store :as st] + [app.util.dom :as dom] + [goog.events :as ev] + [promesa.core :as p] + [rumext.alpha :as mf])) + +(defn load-arengu-sdk + [container-ref email form-id] + (letfn [(on-init [] + (let [container (mf/ref-val container-ref) + params #js {:email email}] + (-> (.embed js/ArenguForms form-id container) + (p/then (fn [form] + (.setHiddenField ^js form "email" email)))))) + + (on-submit-success [event] + (st/emit! (du/mark-questions-as-answered))) + ] + + (let [script (dom/create-element "script") + head (unchecked-get js/document "head") + lkey1 (ev/listen js/document "af-submitForm-success" on-submit-success)] + + (unchecked-set script "src" "https://sdk.arengu.com/forms.js") + (unchecked-set script "onload" on-init) + (dom/append-child! head script) + + (fn [] + (ev/unlistenByKey lkey1))))) + +(mf/defc questions + [{:keys [profile form-id]}] + (let [container (mf/use-ref)] + (mf/use-effect (partial load-arengu-sdk container (:email profile) form-id)) + [:div.modal-wrapper.questions-form + [:div.modal-overlay + [:div.modal-container {:ref container}]]])) + + diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs new file mode 100644 index 000000000..6e5961a8f --- /dev/null +++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs @@ -0,0 +1,181 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.onboarding.team-choice + (:require + [app.common.spec :as us] + [app.main.data.dashboard :as dd] + [app.main.data.messages :as dm] + [app.main.data.modal :as modal] + [app.main.store :as st] + [app.main.ui.components.forms :as fm] + [app.util.i18n :as i18n :refer [tr]] + [app.util.router :as rt] + [app.util.timers :as tm] + [cljs.spec.alpha :as s] + [rumext.alpha :as mf])) + +(s/def ::name ::us/not-empty-string) +(s/def ::team-form + (s/keys :req-un [::name])) + +(mf/defc onboarding-choice-modal + {::mf/register modal/components + ::mf/register-as :onboarding-choice} + [] + (let [;; When user choices the option of `fly solo`, we proceed to show + ;; the onboarding templates modal. + on-fly-solo + (fn [] + (tm/schedule 400 #(st/emit! (modal/show {:type :onboarding-templates})))) + + ;; When user choices the option of `team up`, we proceed to show + ;; the team creation modal. + on-team-up + (fn [] + (st/emit! (modal/show {:type :onboarding-team}))) + ] + + [:div.modal-overlay + [:div.modal-container.onboarding.final.animated.fadeInUp + [:div.modal-top + [:h1 (tr "onboarding.welcome.title")] + [:p (tr "onboarding.welcome.desc3")]] + [:div.modal-columns + [:div.modal-left + [:div.content-button {:on-click on-fly-solo} + [:h2 (tr "onboarding.choice.fly-solo")] + [:p (tr "onboarding.choice.fly-solo-desc")]]] + [:div.modal-right + [:div.content-button {:on-click on-team-up} + [:h2 (tr "onboarding.choice.team-up")] + [:p (tr "onboarding.choice.team-up-desc")]]]] + [:img.deco {:src "images/deco-left.png" :border "0"}] + [:img.deco.right {:src "images/deco-right.png" :border "0"}]]])) + +(mf/defc onboarding-team-modal + {::mf/register modal/components + ::mf/register-as :onboarding-team} + [] + (let [form (fm/use-form :spec ::team-form + :initial {}) + on-submit + (mf/use-callback + (fn [form _] + (let [tname (get-in @form [:clean-data :name])] + (st/emit! (modal/show {:type :onboarding-team-invitations :name tname})))))] + + [:div.modal-overlay + [:div.modal-container.onboarding-team + [:div.title + [:h2 (tr "onboarding.choice.team-up")] + [:p (tr "onboarding.choice.team-up-desc")]] + + [:& fm/form {:form form + :on-submit on-submit} + + [:div.team-row + [:& fm/input {:type "text" + :name :name + :label (tr "onboarding.team-input-placeholder")}]] + + [:div.buttons + [:button.btn-secondary.btn-large + {:on-click #(st/emit! (modal/show {:type :onboarding-choice}))} + (tr "labels.cancel")] + [:& fm/submit-button + {:label (tr "labels.next")}]]] + + [:img.deco {:src "images/deco-left.png" :border "0"}] + [:img.deco.right {:src "images/deco-right.png" :border "0"}]]])) + +(defn get-available-roles + [] + [{:value "editor" :label (tr "labels.editor")} + {:value "admin" :label (tr "labels.admin")}]) + +(s/def ::email ::us/email) +(s/def ::role ::us/keyword) +(s/def ::invite-form + (s/keys :req-un [::role ::email])) + +;; This is the final step of team creation, consists in provide a +;; shortcut for invite users. + +(mf/defc onboarding-team-invitations-modal + {::mf/register modal/components + ::mf/register-as :onboarding-team-invitations} + [{:keys [name] :as props}] + (let [initial (mf/use-memo (constantly + {:role "editor" + :name name})) + form (fm/use-form :spec ::invite-form + :initial initial) + + roles (mf/use-memo #(get-available-roles)) + + on-success + (mf/use-callback + (fn [_form response] + (let [project-id (:default-project-id response) + team-id (:id response)] + (st/emit! + (modal/hide) + (rt/nav :dashboard-projects {:team-id team-id})) + (tm/schedule 400 #(st/emit! + (modal/show {:type :onboarding-templates + :project-id project-id})))))) + + on-error + (mf/use-callback + (fn [_form _response] + (st/emit! (dm/error "Error on creating team.")))) + + ;; The SKIP branch only creates the team, without invitations + on-skip + (mf/use-callback + (fn [_] + (let [mdata {:on-success (partial on-success form) + :on-error (partial on-error form)} + params {:name name}] + (st/emit! (dd/create-team (with-meta params mdata)))))) + + ;; The SUBMIT branch creates the team with the invitations + on-submit + (mf/use-callback + (fn [form _] + (let [mdata {:on-success (partial on-success form) + :on-error (partial on-error form)} + params (:clean-data @form)] + (st/emit! (dd/create-team-with-invitations (with-meta params mdata))))))] + + [:div.modal-overlay + [:div.modal-container.onboarding-team + [:div.title + [:h2 (tr "onboarding.choice.team-up")] + [:p (tr "onboarding.choice.team-up-desc")]] + + [:& fm/form {:form form + :on-submit on-submit} + + [:div.invite-row + [:& fm/input {:name :email + :label (tr "labels.email")}] + [:& fm/select {:name :role + :options roles}]] + + [:div.buttons + [:button.btn-secondary.btn-large + {:on-click #(st/emit! (modal/show {:type :onboarding-choice}))} + (tr "labels.cancel")] + [:& fm/submit-button + {:label (tr "labels.create")}]] + [:div.skip-action + {:on-click on-skip} + [:div.action "Skip and invite later"]]] + [:img.deco {:src "images/deco-left.png" :border "0"}] + [:img.deco.right {:src "images/deco-right.png" :border "0"}]]])) + diff --git a/frontend/src/app/main/ui/onboarding/templates.cljs b/frontend/src/app/main/ui/onboarding/templates.cljs new file mode 100644 index 000000000..91a886d34 --- /dev/null +++ b/frontend/src/app/main/ui/onboarding/templates.cljs @@ -0,0 +1,88 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.onboarding.templates + (:require + [app.config :as cf] + [app.main.data.dashboard :as dd] + [app.main.data.modal :as modal] + [app.main.refs :as refs] + [app.main.store :as st] + [app.main.ui.icons :as i] + [app.util.dom :as dom] + [app.util.http :as http] + [app.util.i18n :as i18n :refer [tr]] + [beicon.core :as rx] + [rumext.alpha :as mf])) + +(mf/defc template-item + [{:keys [name path image project-id]}] + (let [downloading? (mf/use-state false) + link (str (assoc cf/public-uri :path path)) + + on-finish-import + (fn [] + (st/emit! (dd/fetch-recent-files))) + + open-import-modal + (fn [file] + (st/emit! (modal/show + {:type :import + :project-id project-id + :files [file] + :on-finish-import on-finish-import}))) + on-click + (fn [] + (reset! downloading? true) + (->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors}) + (rx/subs (fn [{:keys [body] :as response}] + (open-import-modal {:name name :uri (dom/create-uri body)})) + (fn [error] + (js/console.log "error" error)) + (fn [] + (reset! downloading? false))))) + ] + + [:div.template-item + [:div.template-item-content + [:img {:src image}]] + [:div.template-item-title + [:div.label name] + (if @downloading? + [:div.action "Fetching..."] + [:div.action {:on-click on-click} "+ Add to drafts"])]])) + +(mf/defc onboarding-templates-modal + {::mf/wrap-props false + ::mf/register modal/components + ::mf/register-as :onboarding-templates} + ;; NOTE: the project usually comes empty, it only comes fullfilled + ;; when a user creates a new team just after signup. + [{:keys [project-id] :as props}] + (let [close-fn (mf/use-callback #(st/emit! (modal/hide))) + profile (mf/deref refs/profile) + project-id (or project-id (:default-project-id profile))] + [:div.modal-overlay + [:div.modal-container.onboarding-templates + [:div.modal-header + [:div.modal-close-button + {:on-click close-fn} i/close]] + + [:div.modal-content + [:h3 (tr "onboarding.templates.title")] + [:p (tr "onboarding.templates.subtitle")] + + [:div.templates + [:& template-item + {:path "/github/penpot-files/Penpot-Design-system.penpot" + :image "https://penpot.app/images/libraries/cover-ds-penpot.jpg" + :name "Penpot Design System" + :project-id project-id}] + [:& template-item + {:path "/github/penpot-files/Material-Design-Kit.penpot" + :image "https://penpot.app/images/libraries/cover-material.jpg" + :name "Material Design Kit" + :project-id project-id}]]]]])) diff --git a/frontend/src/app/main/ui/releases.cljs b/frontend/src/app/main/ui/releases.cljs new file mode 100644 index 000000000..df4706ac1 --- /dev/null +++ b/frontend/src/app/main/ui/releases.cljs @@ -0,0 +1,83 @@ +;; 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) UXBOX Labs SL + +(ns app.main.ui.releases + (:require + [app.main.data.modal :as modal] + [app.main.data.users :as du] + [app.main.store :as st] + [app.main.ui.releases.common :as rc] + [app.main.ui.releases.v1-4] + [app.main.ui.releases.v1-5] + [app.main.ui.releases.v1-6] + [app.main.ui.releases.v1-7] + [app.main.ui.releases.v1-8] + [app.main.ui.releases.v1-9] + [app.util.object :as obj] + [app.util.timers :as tm] + [rumext.alpha :as mf])) + +;;; --- RELEASE NOTES MODAL + +(mf/defc release-notes + [{:keys [version] :as props}] + (let [slide (mf/use-state :start) + klass (mf/use-state "fadeInDown") + + navigate + (mf/use-callback #(reset! slide %)) + + next + (mf/use-callback + (mf/deps slide) + (fn [] + (if (= @slide :start) + (navigate 0) + (navigate (inc @slide))))) + + finish + (mf/use-callback + (st/emitf (modal/hide) + (du/mark-onboarding-as-viewed {:version version}))) + ] + + (mf/use-effect + (mf/deps) + (fn [] + (st/emitf (du/mark-onboarding-as-viewed {:version version})))) + + (mf/use-layout-effect + (mf/deps @slide) + (fn [] + (when (not= :start @slide) + (reset! klass "fadeIn")) + (let [sem (tm/schedule 300 #(reset! klass nil))] + (fn [] + (reset! klass nil) + (tm/dispose! sem))))) + + (rc/render-release-notes + {:next next + :navigate navigate + :finish finish + :klass klass + :slide slide + :version version}))) + +(mf/defc release-notes-modal + {::mf/wrap-props false + ::mf/register modal/components + ::mf/register-as :release-notes} + [props] + (let [versions (methods rc/render-release-notes) + version (obj/get props "version")] + (when (contains? versions version) + [:div.relnotes + [:> release-notes props]]))) + +(defmethod rc/render-release-notes "0.0" + [params] + (rc/render-release-notes (assoc params :version "1.10"))) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index f908634f0..dc09ea5bf 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -166,7 +166,7 @@ (defn append-child! [el child] - (.appendChild el child)) + (.appendChild ^js el child)) (defn get-first-child [el]