diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 94d3aaa3ac..c1ec7bd664 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -115,6 +115,9 @@ {:name "0033-mod-comment-thread-table" :fn (mg/resource "app/migrations/sql/0033-mod-comment-thread-table.sql")} + + {:name "0034-mod-profile-table-add-props-field" + :fn (mg/resource "app/migrations/sql/0034-mod-profile-table-add-props-field.sql")} ]}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/migrations/sql/0034-mod-profile-table-add-props-field.sql b/backend/src/app/migrations/sql/0034-mod-profile-table-add-props-field.sql new file mode 100644 index 0000000000..11f80ec320 --- /dev/null +++ b/backend/src/app/migrations/sql/0034-mod-profile-table-add-props-field.sql @@ -0,0 +1 @@ +ALTER TABLE profile ADD COLUMN props jsonb NULL DEFAULT NULL; diff --git a/backend/src/app/services/mutations/profile.clj b/backend/src/app/services/mutations/profile.clj index a45ddcbca9..01f11d69c7 100644 --- a/backend/src/app/services/mutations/profile.clj +++ b/backend/src/app/services/mutations/profile.clj @@ -280,7 +280,7 @@ (defn- validate-password! [conn {:keys [profile-id old-password] :as params}] - (let [profile (profile/retrieve-profile-data conn profile-id)] + (let [profile (db/get-by-id conn :profile profile-id)] (when-not (:valid (verify-password old-password (:password profile))) (ex/raise :type :validation :code :old-password-not-match)))) @@ -310,7 +310,7 @@ [{:keys [profile-id file] :as params}] (media/validate-media-type (:content-type file)) (db/with-atomic [conn db/pool] - (let [profile (profile/retrieve-profile conn profile-id) + (let [profile (db/get-by-id conn :profile profile-id) _ (media/run {:cmd :info :input {:path (:tempfile file) :mtype (:content-type file)}}) photo (teams/upload-photo conn params)] @@ -409,6 +409,27 @@ (update-password conn)) nil))) +;; --- Mutation: Update Profile Props + +(s/def ::props map?) +(s/def ::update-profile-props + (s/keys :req-un [::profile-id ::props])) + +(sm/defmutation ::update-profile-props + [{:keys [profile-id props]}] + (db/with-atomic [conn db/pool] + (let [profile (profile/retrieve-profile-data conn profile-id) + props (reduce-kv (fn [props k v] + (if (nil? v) + (dissoc props k) + (assoc props k v))) + (:props profile) + props)] + (db/update! conn :profile + {:props (db/tjson props)} + {:id profile-id}) + nil))) + ;; --- Mutation: Delete Profile diff --git a/backend/src/app/services/queries/profile.clj b/backend/src/app/services/queries/profile.clj index 14da3b4535..db954905d7 100644 --- a/backend/src/app/services/queries/profile.clj +++ b/backend/src/app/services/queries/profile.clj @@ -73,9 +73,15 @@ {:default-team-id (:id team) :default-project-id (:id project)})) +(defn decode-profile-row + [{:keys [props] :as row}] + (cond-> row + (db/pgobject? props) (assoc :props (db/decode-transit-pgobject props)))) + (defn retrieve-profile-data [conn id] - (db/get-by-id conn :profile id)) + (-> (db/get-by-id conn :profile id) + (decode-profile-row))) (defn retrieve-profile [conn id] @@ -97,7 +103,8 @@ (defn retrieve-profile-data-by-email [conn email] (let [email (str/lower email)] - (db/exec-one! conn [sql:profile-by-email email]))) + (-> (db/exec-one! conn [sql:profile-by-email email]) + (decode-profile-row)))) ;; --- Attrs Helpers diff --git a/frontend/resources/images/dashboard-img.svg b/frontend/resources/images/dashboard-img.svg deleted file mode 100644 index 00544e925d..0000000000 --- a/frontend/resources/images/dashboard-img.svg +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - Octoface - - - - Mark Github - - - - Twitter - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/frontend/resources/images/deco-left.png b/frontend/resources/images/deco-left.png new file mode 100644 index 0000000000..bd14661c7a Binary files /dev/null and b/frontend/resources/images/deco-left.png differ diff --git a/frontend/resources/images/deco-right.png b/frontend/resources/images/deco-right.png new file mode 100644 index 0000000000..cc108924e5 Binary files /dev/null and b/frontend/resources/images/deco-right.png differ diff --git a/frontend/resources/images/login-bg.jpg b/frontend/resources/images/login-bg.jpg deleted file mode 100644 index e670a377fe..0000000000 Binary files a/frontend/resources/images/login-bg.jpg and /dev/null differ diff --git a/frontend/resources/images/on-design.gif b/frontend/resources/images/on-design.gif new file mode 100644 index 0000000000..94a3925b54 Binary files /dev/null and b/frontend/resources/images/on-design.gif differ diff --git a/frontend/resources/images/on-feed.gif b/frontend/resources/images/on-feed.gif new file mode 100644 index 0000000000..a850edc58f Binary files /dev/null and b/frontend/resources/images/on-feed.gif differ diff --git a/frontend/resources/images/on-handoff.gif b/frontend/resources/images/on-handoff.gif new file mode 100644 index 0000000000..e5feb0af9c Binary files /dev/null and b/frontend/resources/images/on-handoff.gif differ diff --git a/frontend/resources/images/on-proto.gif b/frontend/resources/images/on-proto.gif new file mode 100644 index 0000000000..9ccb7fbf86 Binary files /dev/null and b/frontend/resources/images/on-proto.gif differ diff --git a/frontend/resources/images/onboarding-start.jpg b/frontend/resources/images/onboarding-start.jpg new file mode 100644 index 0000000000..f089ba4a15 Binary files /dev/null and b/frontend/resources/images/onboarding-start.jpg differ diff --git a/frontend/resources/images/onboarding-team.jpg b/frontend/resources/images/onboarding-team.jpg new file mode 100644 index 0000000000..dbd28f9d47 Binary files /dev/null and b/frontend/resources/images/onboarding-team.jpg differ diff --git a/frontend/resources/images/open-source.svg b/frontend/resources/images/open-source.svg new file mode 100644 index 0000000000..7bc4f583e3 --- /dev/null +++ b/frontend/resources/images/open-source.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/resources/images/penpot-login2.jpg b/frontend/resources/images/penpot-login2.jpg deleted file mode 100644 index 3c9409fb5c..0000000000 Binary files a/frontend/resources/images/penpot-login2.jpg and /dev/null differ diff --git a/frontend/resources/images/pot.png b/frontend/resources/images/pot.png new file mode 100644 index 0000000000..ab8a96da49 Binary files /dev/null and b/frontend/resources/images/pot.png differ diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index 12a9ab1d04..8638e05ca3 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -362,3 +362,190 @@ } } +//- ONBOARDING +.onboarding { + background-color: $color-white; + box-shadow: 0 10px 10px rgba(0,0,0,.2); + display: flex; + height: 350px; + flex-direction: row; + font-family: "sourcesanspro", sans-serif; + min-width: 620px; + position: relative; + + .modal-left { + align-items: center; + background-color: $color-primary; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + display: flex; + flex-shrink: 0; + justify-content: center; + padding: $x-big; + width: 230px; + } + + .modal-right { + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + display: flex; + flex-direction: column; + padding: $x-big; + + .modal-title h2 { + color: $color-black; + font-size: $fs28; + font-weight: 900; + } + + .release { + background-color: $color-primary; + color: $color-black; + font-size: $fs12; + font-weight: bold; + margin-top: $small; + padding: 2px $x-small; + width: max-content; + } + + .modal-content { + border: none; + padding: $big 0; + + p { + color: $color-black; + font-size: 16px; + margin-top: $small; + } + } + + .modal-navigation { + align-items: center; + display: flex; + margin-top: auto; + + .skip { + cursor: pointer; + font-family: "worksans", sans-serif; + font-size: $fs13; + margin-left: $big; + + &:hover { + color: $color-black; + } + } + } + + .step-dots { + align-items: center; + display: flex; + margin-bottom: 0; + margin-left: auto; + + li { + background-color: $color-gray-10; + border-radius: 50%; + height: $small; + margin-left: $small; + width: $small; + + &.current { + background-color: $color-primary; + } + } + } + } + + &.black { + .modal-left { + background-color: $color-black; + } + } + + button { + font-family: "worksans", sans-serif; + } + + &.feature { + .modal-left { + padding: 0; + + img { + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + } + } + } + + &.final { + padding: $big 0 0 0; + + .modal-left, + .modal-right { + align-items: center; + background-color: $color-white; + color: $color-black; + flex: 1; + flex-direction: column; + padding: $x-big 40px; + text-align: center; + + h2 { + font-weight: 900; + margin-bottom: $big; + font-size: $fs24; + } + + p { + font-size: $fs14; + } + + .btn-primary { + margin-bottom: 0; + margin-top: auto; + width: 200px; + } + + img { + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.25); + border-radius: $br-medium; + margin-bottom: $x-big; + margin-top: -90px; + width: 150px; + } + } + + .modal-left { + border-right: 1px solid $color-gray-10; + + form { + align-items: center; + display: flex; + flex-direction: column; + margin-top: auto; + + .custom-input { + margin-bottom: $medium; + + input { + width: 200px; + } + } + } + } + + } +} + +.deco { + left: -10px; + position: absolute; + top: -18px; + width: 60px; + + &.right { + left: 590px; + top: 0; + } +} + diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 31da7e8a38..a3f5904bc3 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -156,6 +156,16 @@ (rx/ignore)))))) +(defn mark-onboarding-as-viewed + [] + (ptk/reify ::mark-oboarding-as-viewed + ptk/WatchEvent + (watch [_ state stream] + (let [{:keys [id] :as profile} (:profile state)] + (->> (rp/mutation :update-profile-props {:props {:onboarding-viewed true}}) + (rx/map (constantly fetch-profile))))))) + + ;; --- Update Photo (defn update-photo @@ -165,7 +175,6 @@ ptk/WatchEvent (watch [_ state stream] (let [on-success di/notify-finished-loading - on-error #(do (di/notify-finished-loading) (di/process-error %)) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 8d194113c0..635d2afa64 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -22,6 +22,7 @@ [app.main.ui.auth.verify-token :refer [verify-token]] [app.main.ui.cursors :as c] [app.main.ui.context :as ctx] + [app.main.ui.onboarding] [app.main.ui.dashboard :refer [dashboard]] [app.main.ui.icons :as i] [app.main.ui.messages :as msgs] diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index d1abccf4e7..6c5350f0c6 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -13,6 +13,7 @@ [app.common.spec :as us] [app.common.uuid :as uuid] [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.dashboard.files :refer [files-section]] @@ -24,6 +25,7 @@ [app.main.ui.icons :as i] [app.util.i18n :as i18n :refer [t]] [app.util.router :as rt] + [app.util.storage :refer [storage]] [cuerdas.core :as str] [okulary.core :as l] [rumext.alpha :as mf])) @@ -101,6 +103,11 @@ projects (mf/deref projects-ref) project (get projects project-id)] + (mf/use-effect + (fn [] + (when (and profile (not (get-in profile [:props :onboarding-viewed]))) + (st/emit! (modal/show {:type :onboarding}))))) + (mf/use-effect (mf/deps team-id) (st/emitf (dd/fetch-bundle {:id team-id}))) diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs index 87d7ad5982..e9fa25c7a0 100644 --- a/frontend/src/app/main/ui/modal.cljs +++ b/frontend/src/app/main/ui/modal.cljs @@ -61,6 +61,7 @@ [props] (let [data (unchecked-get props "data") wrapper-ref (mf/use-ref nil) + components (mf/deref dm/components) allow-click-outside (:allow-click-outside data) @@ -86,9 +87,7 @@ (events/unlistenByKey key))))) [:div.modal-wrapper {:ref wrapper-ref} - (mf/element - (get @dm/components (:type data)) - (:props data))])) + (mf/element (get components (:type data)) (:props data))])) (def modal-ref @@ -97,5 +96,6 @@ (mf/defc modal [] (let [modal (mf/deref modal-ref)] - (when modal [:& modal-wrapper {:data modal - :key (:id modal)}]))) + (when modal + [:& modal-wrapper {:data modal + :key (:id modal)}]))) diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs new file mode 100644 index 0000000000..77b88823aa --- /dev/null +++ b/frontend/src/app/main/ui/onboarding.cljs @@ -0,0 +1,230 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns app.main.ui.onboarding + (: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.data.users :as du] + [app.main.store :as st] + [app.main.ui.components.forms :as fm :refer [input submit-button form]] + [app.util.router :as rt] + [app.util.timers :as tm] + [cljs.spec.alpha :as s] + [rumext.alpha :as mf])) + +(defmulti render-slide :slide) + +(defmethod render-slide :start + [{:keys [navigate] :as props}] + (mf/html + [:div.modal-container.onboarding + [:div.modal-left + [:img {:src "images/pot.png" :border "0" :alt "Penpot"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Welcome to Penpot!"]] + [:span.release "Alpha version 1.0"] + [:div.modal-content + [:p "We are very happy to introduce you to the very first Alpha 1.0 release."] + [:p "Penpot is still at development stage and there will be constant updates. We hope you enjoy the first stable version."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click #(navigate :opensource)} "Continue"]]] + [:img.deco {:src "images/deco-left.png" :border "0"}] + [:img.deco.right {:src "images/deco-right.png" :border "0"}]])) + + +(defmethod render-slide :opensource + [{:keys [navigate] :as props}] + (mf/html + [:div.modal-container.onboarding.black + [:div.modal-left + [:img {:src "images/open-source.svg" :border "0" :alt "Open Source"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Open Source Contributor?"]] + [:div.modal-content + [:p "Penpot is Open Source, made by and for the community. If you want to collaborate, you are more than welcome!"] + [:p "You can access the " [:a {:href "https://github.com/penpot" :target "_blank"} "project on github"] " and follow the contribution instructions :)"]] + [:div.modal-navigation + [:button.btn-secondary {:on-click #(navigate :feature1)} "Continue"]]]])) + +(defmethod render-slide :feature1 + [{:keys [navigate skip] :as props}] + (mf/html + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/on-design.gif" :border "0" :alt "Create designs"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Design libraries, styles and components"]] + [:div.modal-content + [:p "Create beautiful user interfaces in collaboration with all team members."] + [:p "Maintain consistency at scale with components, libraries and design systems."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click #(navigate :feature2)} "Continue"] + [:span.skip {:on-click skip} "Skip"] + [:ul.step-dots + [:li.current] + [:li] + [:li] + [:li]]]]])) + +(defmethod render-slide :feature2 + [{:keys [navigate skip] :as props}] + (mf/html + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/on-proto.gif" :border "0" :alt "Interactive prototypes"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Bring your designs to life with interactions"]] + [:div.modal-content + [:p "Create rich interactions to mimic the product behaviour."] + [:p "Share to stakeholders, present proposals to your team and start user testing with your designs, all in one place."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click #(navigate :feature3)} "Continue"] + [:span.skip {:on-click skip} "Skip"] + [:ul.step-dots + [:li] + [:li.current] + [:li] + [:li]]]]])) + +(defmethod render-slide :feature3 + [{:keys [navigate skip] :as props}] + (mf/html + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/on-feed.gif" :border "0" :alt "Get feedback"}]] + [:div.modal-right + [:div.modal-title + [:h2 "Get feedback, present and share your work"]] + [:div.modal-content + [:p "All team members working simultaneously with real time design multiplayer and centralised comments, ideas and feedback right over the designs."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click #(navigate :feature4)} "Continue"] + [:span.skip {:on-click skip} "Skip"] + [:ul.step-dots + [:li] + [:li] + [:li.current] + [:li]]]]])) + +(defmethod render-slide :feature4 + [{:keys [navigate skip] :as props}] + (mf/html + [:div.modal-container.onboarding.feature + [:div.modal-left + [:img {:src "images/on-handoff.gif" :border "0" :alt "Handoff and lowcode"}]] + [:div.modal-right + [:div.modal-title + [:h2 "One shared source of truth"]] + [:div.modal-content + [:p "Sync the design and code of all your components and styles and get code snippets."] + [:p "Get and provide code specifications like markup (SVG, HTML) or styles (CSS, Less, Stylus…)."]] + [:div.modal-navigation + [:button.btn-secondary {:on-click skip} "Continue"] + [:span.skip {:on-click skip} "Skip"] + [:ul.step-dots + [:li] + [:li] + [:li] + [:li.current]]]]])) + +(mf/defc onboarding-modal + {::mf/register modal/components + ::mf/register-as :onboarding} + [props] + (let [slide (mf/use-state :start) + klass (mf/use-state "fadeInDown") + + navigate + (mf/use-callback #(reset! slide %)) + + skip + (mf/use-callback + (st/emitf (modal/hide) + (modal/show {:type :onboarding-team}) + (du/mark-onboarding-as-viewed)))] + + (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))))) + + [:div.modal-overlay + [:div.animated {:class @klass} + (render-slide + (assoc props + :slide @slide + :navigate navigate + :skip skip))]])) + +(s/def ::name ::us/not-empty-string) +(s/def ::team-form + (s/keys :req-un [::name])) + +(defn- on-success + [form response] + (st/emit! (modal/hide) + (rt/nav :dashboard-projects {:team-id (:id response)}))) + +(defn- on-error + [form response] + (st/emit! (dm/error "Error on creating team."))) + +(defn- on-submit + [form event] + (let [mdata {:on-success (partial on-success form) + :on-error (partial on-error form)} + params {:name (get-in @form [:clean-data :name])}] + (st/emit! (dd/create-team (with-meta params mdata))))) + +(mf/defc onboarding-team-modal + {::mf/register modal/components + ::mf/register-as :onboarding-team} + [props] + (let [close (mf/use-fn (st/emitf (modal/hide))) + form (fm/use-form :spec ::team-form + :initial {}) + + on-submit + (mf/use-callback (partial on-submit form))] + [:div.modal-overlay + [:div.modal-container.onboarding.final.animated.fadeInUp + [:div.modal-left + [:img {:src "images/onboarding-team.jpg" :border "0" :alt "Create a team"}] + [:h2 "Create a team"] + [:p "Are you working with someone? Create a team to work together on projects and share design assets."] + + [:& fm/form {:form form + :on-submit on-submit} + [:& fm/input {:type "text" + :name :name + :label "Enter new team name"}] + [:& fm/submit-button + {:label "Create team"}]]] + [:div.modal-right + [:img {:src "images/onboarding-start.jpg" :border "0" :alt "Start designing"}] + [:h2 "Start designing"] + [:p "Jump right away into Penpot and start designing by your own. You will still have the chance to create teams later."] + [:button.btn-primary.btn-large {:on-click close} "Start right away"]] + + + [: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/util/dom.cljs b/frontend/src/app/util/dom.cljs index 2095bae52b..1dad84d487 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -29,6 +29,7 @@ [e] (.-target e)) + (defn classnames [& params] (assert (even? (count params))) @@ -39,6 +40,7 @@ [] (partition 2 params)))) + ;; --- New methods (defn get-element-by-class diff --git a/frontend/src/app/util/timers.cljs b/frontend/src/app/util/timers.cljs index 1861613449..a7afaff2b4 100644 --- a/frontend/src/app/util/timers.cljs +++ b/frontend/src/app/util/timers.cljs @@ -21,6 +21,10 @@ (-dispose [_] (js/clearTimeout sem)))))) +(defn dispose! + [v] + (rx/dispose! v)) + (defn asap [f] (-> (p/resolved nil)