diff --git a/CHANGES.md b/CHANGES.md index ff08da4326..a169138a31 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ ### :arrow_up: Deps updates ### :heart: Community contributions by (Thank you!) + ## 1.13.0-beta ### :boom: Breaking changes diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 6a88a10c48..455964fdb7 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -209,6 +209,9 @@ {:cron #app/cron "0 0 0 * * ?" ;; daily :task :tasks-gc} + {:cron #app/cron "0 30 */3,23 * * ?" + :task :telemetry} + (when (cf/get :fdata-storage-backed) {:cron #app/cron "0 0 * * * ?" ;; hourly :task :file-offload}) @@ -219,12 +222,7 @@ (when (contains? cf/flags :audit-log-gc) {:cron #app/cron "0 0 0 * * ?" ;; daily - :task :audit-log-gc}) - - (when (or (contains? cf/flags :telemetry) - (cf/get :telemetry-enabled)) - {:cron #app/cron "0 30 */3,23 * * ?" - :task :telemetry})]} + :task :audit-log-gc})]} :app.worker/registry {:metrics (ig/ref :app.metrics/metrics) diff --git a/backend/src/app/rpc/mutations/profile.clj b/backend/src/app/rpc/mutations/profile.clj index 5834b0588e..855a6ef324 100644 --- a/backend/src/app/rpc/mutations/profile.clj +++ b/backend/src/app/rpc/mutations/profile.clj @@ -6,6 +6,7 @@ (ns app.rpc.mutations.profile (:require + [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] [app.common.uuid :as uuid] @@ -30,7 +31,7 @@ (s/def ::email ::us/email) (s/def ::fullname ::us/not-empty-string) -(s/def ::lang (s/nilable ::us/not-empty-string)) +(s/def ::lang ::us/string) (s/def ::path ::us/string) (s/def ::profile-id ::us/uuid) (s/def ::password ::us/not-empty-string) @@ -342,27 +343,41 @@ ;; --- MUTATION: Update Profile (own) -(defn- update-profile - [conn {:keys [id fullname lang theme] :as params}] - (let [profile (db/update! conn :profile - {:fullname fullname - :lang lang - :theme theme} - {:id id})] - (-> profile - (profile/decode-profile-row) - (profile/strip-private-attrs)))) - - +(s/def ::newsletter-subscribed ::us/boolean) (s/def ::update-profile - (s/keys :req-un [::id ::fullname] - :opt-un [::lang ::theme])) + (s/keys :req-un [::fullname ::profile-id] + :opt-un [::lang ::theme ::newsletter-subscribed])) (sv/defmethod ::update-profile - [{:keys [pool] :as cfg} params] + [{:keys [pool] :as cfg} {:keys [profile-id fullname lang theme newsletter-subscribed] :as params}] (db/with-atomic [conn pool] - (let [profile (update-profile conn params)] - (with-meta profile + ;; NOTE: we need to retrieve the profile independently if we use + ;; it or not for explicit locking and avoid concurrent updates of + ;; the same row/object. + (let [profile (-> (db/get-by-id conn :profile profile-id {:for-update true}) + (profile/decode-profile-row)) + + ;; Update the profile map with direct params + profile (-> profile + (assoc :fullname fullname) + (assoc :lang lang) + (assoc :theme theme)) + + ;; Update profile props if the indirect prop is coming in + ;; the params map and update the profile props data + ;; acordingly. + profile (cond-> profile + (some? newsletter-subscribed) + (update :props assoc :newsletter-subscribed newsletter-subscribed))] + + (db/update! conn :profile + {:fullname fullname + :lang lang + :theme theme + :props (db/tjson (:props profile))} + {:id profile-id}) + + (with-meta (-> profile profile/strip-private-attrs d/without-nils) {::audit/props (audit/profile->props profile)})))) ;; --- MUTATION: Update Password diff --git a/backend/src/app/tasks/telemetry.clj b/backend/src/app/tasks/telemetry.clj index 11c1411576..cf45eeeba0 100644 --- a/backend/src/app/tasks/telemetry.clj +++ b/backend/src/app/tasks/telemetry.clj @@ -12,7 +12,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.spec :as us] - [app.config :as cfg] + [app.config :as cf] [app.db :as db] [app.util.async :refer [thread-sleep]] [app.util.json :as json] @@ -25,6 +25,7 @@ (declare get-stats) (declare send!) +(declare get-subscriptions) (s/def ::http-client fn?) (s/def ::version ::us/string) @@ -38,18 +39,39 @@ (defmethod ig/init-key ::handler [_ {:keys [pool sprops version] :as cfg}] - (fn [{:keys [send?] :or {send? true}}] - ;; Sleep randomly between 0 to 10s - (when send? - (thread-sleep (rand-int 10000))) + (fn [{:keys [send? enabled?] :or {send? true enabled? false}}] + (let [subs (get-subscriptions pool) + enabled? (or enabled? + (contains? cf/flags :telemetry) + (cf/get :telemetry-enabled)) - (let [instance-id (:instance-id sprops) - stats (-> (get-stats pool version) - (assoc :instance-id instance-id))] - (when send? - (send! cfg stats)) + data {:subscriptions subs + :version version + :instance-id (:instance-id sprops)}] + (cond + ;; If we have telemetry enabled, then proceed the normal + ;; operation. + enabled? + (let [data (merge data (get-stats pool))] + (when send? + (thread-sleep (rand-int 10000)) + (send! cfg data)) + data) - stats))) + ;; If we have telemetry disabled, but there are users that are + ;; explicitly checked the newsletter subscription on the + ;; onboarding dialog or the profile section, then proceed to + ;; send a limited telemetry data, that consists in the list of + ;; subscribed emails and the running penpot version. + (seq subs) + (do + (when send? + (thread-sleep (rand-int 10000)) + (send! cfg data)) + data) + + :else + data)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; IMPL @@ -68,6 +90,12 @@ :response-status (:status response) :response-body (:body response))))) +(defn- get-subscriptions + [conn] + (let [sql "select email from profile where props->>'~:newsletter-subscribed' = 'true'"] + (->> (db/exec! conn [sql]) + (mapv :email)))) + (defn- retrieve-num-teams [conn] (-> (db/exec-one! conn ["select count(*) as count from team;"]) :count)) @@ -166,12 +194,11 @@ :user-tz (System/getProperty "user.timezone")})) (defn get-stats - [conn version] - (let [referer (if (cfg/get :telemetry-with-taiga) + [conn] + (let [referer (if (cf/get :telemetry-with-taiga) "taiga" - (cfg/get :telemetry-referer))] - (-> {:version version - :referer referer + (cf/get :telemetry-referer))] + (-> {:referer referer :total-teams (retrieve-num-teams conn) :total-projects (retrieve-num-projects conn) :total-files (retrieve-num-files conn) diff --git a/backend/test/app/tasks_telemetry_test.clj b/backend/test/app/tasks_telemetry_test.clj index 3178755546..60012716f6 100644 --- a/backend/test/app/tasks_telemetry_test.clj +++ b/backend/test/app/tasks_telemetry_test.clj @@ -21,13 +21,16 @@ (with-mocks [mock {:target 'app.tasks.telemetry/send! :return nil}] (let [task-fn (-> th/*system* :app.worker/registry :telemetry) - prof (th/create-profile* 1 {:is-active true})] + prof (th/create-profile* 1 {:is-active true + :props {:newsletter-subscribed true}})] ;; run the task - (task-fn nil) + (task-fn {:send? true :enabled? true}) (t/is (:called? @mock)) (let [[_ data] (-> @mock :call-args)] + (t/is (contains? data :subscriptions)) + (t/is (= [(:email prof)] (get data :subscriptions))) (t/is (contains? data :total-fonts)) (t/is (contains? data :total-users)) (t/is (contains? data :total-projects)) diff --git a/frontend/resources/images/deco-news-left.png b/frontend/resources/images/deco-news-left.png new file mode 100644 index 0000000000..a705b26401 Binary files /dev/null and b/frontend/resources/images/deco-news-left.png differ diff --git a/frontend/resources/images/deco-news-right.png b/frontend/resources/images/deco-news-right.png new file mode 100644 index 0000000000..f8feec2614 Binary files /dev/null and b/frontend/resources/images/deco-news-right.png differ diff --git a/frontend/resources/images/deco-newsletter.png b/frontend/resources/images/deco-newsletter.png new file mode 100644 index 0000000000..d322c62b82 Binary files /dev/null and b/frontend/resources/images/deco-newsletter.png differ diff --git a/frontend/resources/styles/main/partials/dashboard-settings.scss b/frontend/resources/styles/main/partials/dashboard-settings.scss index c5c19597b9..d45d1661ee 100644 --- a/frontend/resources/styles/main/partials/dashboard-settings.scss +++ b/frontend/resources/styles/main/partials/dashboard-settings.scss @@ -109,6 +109,38 @@ flex-direction: column; max-width: 368px; width: 100%; + + .newsletter-subs { + border-bottom: 1px solid $color-gray-20; + border-top: 1px solid $color-gray-20; + padding: 30px 0; + margin-bottom: 31px; + + .newsletter-title { + font-family: "worksans", sans-serif; + color: $color-gray-30; + font-size: $fs14; + } + + label { + font-family: "worksans", sans-serif; + color: $color-gray-60; + font-size: $fs12; + margin-right: -17px; + margin-bottom: 13px; + } + + .info { + font-family: "worksans", sans-serif; + color: $color-gray-30; + font-size: $fs12; + margin-bottom: 8px; + } + + .input-checkbox label { + align-items: flex-start; + } + } } .options-form, diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index 26f352471a..b5a8f3ce27 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -996,6 +996,57 @@ } } } + + &.newsletter { + padding: $size-5 0 0 0; + flex-direction: column; + min-width: 555px; + .modal-top { + padding: 87px 40px 0 40px; + color: $color-gray-60; + display: flex; + flex-direction: column; + + h1 { + font-family: sourcesanspro; + font-weight: bold; + font-size: $fs36; + margin-bottom: 0.75rem; + } + + p { + font-family: sourcesanspro; + font-weight: 500; + font-size: $fs16; + margin-bottom: 1.5rem; + } + } + + .modal-bottom { + margin: 0 32px; + padding: 32px 0; + color: $color-gray-60; + display: flex; + flex-direction: column; + border-top: 1px solid $color-gray-10; + + p { + font-family: "worksans", sans-serif; + text-align: left; + color: $color-gray-30; + } + } + + .modal-footer { + padding: 17px; + display: flex; + justify-content: flex-end; + + .btn-secondary { + margin-right: 16px; + } + } + } } .deco { @@ -1004,6 +1055,23 @@ top: -18px; width: 60px; + &.top { + width: 183px; + top: -106px; + left: 161px; + } + + &.newsletter-right { + left: 515px; + top: 50px; + } + + &.newsletter-left { + width: 26px; + left: -15px; + top: -15px; + } + &.right { left: 590px; top: 0; diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 456f163a78..96cd3baaf1 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -54,11 +54,14 @@ :browser :webworker)) +(def default-flags + [:enable-newsletter-subscription]) + (defn- parse-flags [global] (let [flags (obj/get global "penpotFlags" "") flags (sequence (map keyword) (str/words flags))] - (flags/parse flags/default flags))) + (flags/parse flags/default default-flags flags))) (defn- parse-version [global] diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index cd5ca770e9..1eaa37d847 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -303,8 +303,8 @@ (watch [_ _ stream] (let [mdata (meta data) on-success (:on-success mdata identity) - on-error (:on-error mdata #(rx/throw %))] - (->> (rp/mutation :update-profile data) + on-error (:on-error mdata rx/throw)] + (->> (rp/mutation :update-profile (dissoc data :props)) (rx/catch on-error) (rx/mapcat (fn [_] @@ -392,7 +392,6 @@ (->> (rp/mutation :update-profile-props {:props props}) (rx/map (constantly (fetch-profile))))))))) - (defn mark-questions-as-answered [] (ptk/reify ::mark-questions-as-answered diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index 3c61dbce5f..ec49222ae4 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -59,15 +59,15 @@ klass (str more-classes " " (dom/classnames - :focus @focus? - :valid (and touched? (not error)) - :invalid (and touched? error) - :disabled disabled - :empty (and is-text? (str/empty? value)) - :with-icon (not (nil? help-icon')) - :custom-input is-text? - :input-radio is-radio? - :input-checkbox is-checkbox?)) + :focus @focus? + :valid (and touched? (not error)) + :invalid (and touched? error) + :disabled disabled + :empty (and is-text? (str/empty? value)) + :with-icon (not (nil? help-icon')) + :custom-input is-text? + :input-radio is-radio? + :input-checkbox is-checkbox?)) swap-text-password (fn [] @@ -78,7 +78,7 @@ on-focus #(reset! focus? true) on-change (fn [event] - (let [value (-> event dom/get-target dom/get-input-value)] + (let [value (-> event dom/get-target dom/get-input-value)] (fm/on-input-change form input-name value trim))) on-blur @@ -87,16 +87,23 @@ (when-not (get-in @form [:touched input-name]) (swap! form assoc-in [:touched input-name] true))) + on-click + (fn [_] + (when-not (get-in @form [:touched input-name]) + (swap! form assoc-in [:touched input-name] true))) + props (-> props (dissoc :help-icon :form :trim :children) (assoc :id (name input-name) :value value :auto-focus auto-focus? + :on-click (when (or is-radio? is-checkbox?) on-click) :on-focus on-focus :on-blur on-blur :placeholder label :on-change on-change :type @type') + (cond-> (and value is-checkbox?) (assoc :default-checked value)) (obj/clj->props))] [:div @@ -210,7 +217,7 @@ (let [form (or form (mf/use-ctx form-ctx))] [:input.btn-primary.btn-large {:name "submit" - :class (when-not (:valid @form) "btn-disabled") + :class (when (or (not (:valid @form)) (true? disabled)) "btn-disabled") :disabled (or (not (:valid @form)) (true? disabled)) :on-click on-click :value label diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs index 76fb137557..1f904576a6 100644 --- a/frontend/src/app/main/ui/onboarding.cljs +++ b/frontend/src/app/main/ui/onboarding.cljs @@ -10,6 +10,7 @@ [app.main.data.modal :as modal] [app.main.data.users :as du] [app.main.store :as st] + [app.main.ui.onboarding.newsletter] [app.main.ui.onboarding.questions] [app.main.ui.onboarding.team-choice] [app.main.ui.onboarding.templates] @@ -134,8 +135,10 @@ [:p (tr "onboarding.slide.3.desc1")] [:p (tr "onboarding.slide.3.desc2")]] [:div.modal-navigation - [:button.btn-secondary {:on-click skip - :data-test "slide-3-btn"} (tr "labels.start")] + [:button.btn-secondary + {:on-click skip + :data-test "slide-3-btn"} + (tr "labels.start")] [:& rc/navigation-bullets {:slide slide :navigate navigate @@ -149,23 +152,23 @@ klass (mf/use-state "fadeInDown") navigate - (mf/use-callback #(reset! slide %)) + (mf/use-fn #(reset! slide %)) skip - (mf/use-callback - (st/emitf (modal/hide) - (modal/show {:type :onboarding-choice}) - (du/mark-onboarding-as-viewed)))] + (mf/use-fn + #(st/emit! (modal/hide) + (if (contains? @cf/flags :newsletter-subscription) + (modal/show {:type :onboarding-newsletter-modal}) + (modal/show {:type :onboarding-choice})) + (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))))) + (mf/with-effect [@slide] + (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} diff --git a/frontend/src/app/main/ui/onboarding/newsletter.cljs b/frontend/src/app/main/ui/onboarding/newsletter.cljs new file mode 100644 index 0000000000..bf72d5639f --- /dev/null +++ b/frontend/src/app/main/ui/onboarding/newsletter.cljs @@ -0,0 +1,47 @@ +;; 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.newsletter + (:require + [app.main.data.messages :as dm] + [app.main.data.modal :as modal] + [app.main.data.users :as du] + [app.main.store :as st] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) + +(mf/defc onboarding-newsletter-modal + {::mf/register modal/components + ::mf/register-as :onboarding-newsletter-modal} + [] + (let [message (tr "onboarding.newsletter.acceptance-message") + accept + (mf/use-callback + (fn [] + (st/emit! (dm/success message) + (modal/show {:type :onboarding-choice}) + (du/update-profile-props {:newsletter-subscribed true})))) + + decline + (mf/use-callback + (fn [] + (st/emit! (modal/show {:type :onboarding-choice}) + (du/update-profile-props {:newsletter-subscribed false}))))] + + [:div.modal-overlay + [:div.modal-container.onboarding.newsletter.animated.fadeInUp + [:div.modal-top + [:h1.newsletter-title {:data-test "onboarding-newsletter-title"} (tr "onboarding.newsletter.title")] + [:p (tr "onboarding.newsletter.desc")]] + [:div.modal-bottom + [:p (tr "onboarding.newsletter.privacy1") [:a {:target "_blank" :href "https://penpot.app/privacy.html"} (tr "onboarding.newsletter.policy")]] + [:p (tr "onboarding.newsletter.privacy2")]] + [:div.modal-footer + [:button.btn-secondary {:on-click decline} (tr "onboarding.newsletter.decline")] + [:button.btn-primary {:on-click accept} (tr "onboarding.newsletter.accept")]] + [:img.deco.top {:src "images/deco-newsletter.png" :border "0"}] + [:img.deco.newsletter-left {:src "images/deco-news-left.png" :border "0"}] + [:img.deco.newsletter-right {:src "images/deco-news-right.png" :border "0"}]]])) diff --git a/frontend/src/app/main/ui/settings/options.cljs b/frontend/src/app/main/ui/settings/options.cljs index 1793d5bfb2..a79e0000ec 100644 --- a/frontend/src/app/main/ui/settings/options.cljs +++ b/frontend/src/app/main/ui/settings/options.cljs @@ -13,7 +13,7 @@ [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [t tr]] + [app.util.i18n :as i18n :refer [tr]] [cljs.spec.alpha :as s] [rumext.alpha :as mf])) @@ -30,51 +30,51 @@ (defn- on-submit [form _event] (let [data (:clean-data @form) - data (cond-> data - (empty? (:lang data)) - (assoc :lang nil)) mdata {:on-success (partial on-success form)}] (st/emit! (du/update-profile (with-meta data mdata))))) (mf/defc options-form - [{:keys [locale] :as props}] + [] (let [profile (mf/deref refs/profile) + initial (mf/with-memo [profile] + (update profile :lang #(or % ""))) form (fm/use-form :spec ::options-form - :initial profile)] + :initial initial)] + [:& fm/form {:class "options-form" :on-submit on-submit :form form} - [:h2 (t locale "labels.language")] + [:h2 (tr "labels.language")] [:div.fields-row - [:& fm/select {:options (into [{:label "Auto (browser)" :value "default"}] + [:& fm/select {:options (into [{:label "Auto (browser)" :value ""}] i18n/supported-locales) - :label (t locale "dashboard.select-ui-language") + :label (tr "dashboard.select-ui-language") :default "" :name :lang :data-test "setting-lang"}]] - + ;; TODO: Do not show as long as we only have one theme - #_[:h2 (t locale "dashboard.theme-change")] + #_[:h2 (tr "dashboard.theme-change")] #_[:div.fields-row - [:& fm/select {:label (t locale "dashboard.select-ui-theme") + [:& fm/select {:label (tr "dashboard.select-ui-theme") :name :theme :default "default" :options [{:label "Default" :value "default"}] :data-test "theme-lang"}]] [:& fm/submit-button - {:label (t locale "dashboard.update-settings") + {:label (tr "dashboard.update-settings") :data-test "submit-lang-change"}]])) ;; --- Password Page (mf/defc options-page - [{:keys [locale]}] + [] (mf/use-effect #(dom/set-html-title (tr "title.settings.options"))) [:div.dashboard-settings [:div.form-container {:data-test "settings-form"} - [:& options-form {:locale locale}]]]) + [:& options-form {}]]]) diff --git a/frontend/src/app/main/ui/settings/profile.cljs b/frontend/src/app/main/ui/settings/profile.cljs index e92a71eaa6..2e549ef71e 100644 --- a/frontend/src/app/main/ui/settings/profile.cljs +++ b/frontend/src/app/main/ui/settings/profile.cljs @@ -7,7 +7,7 @@ (ns app.main.ui.settings.profile (:require [app.common.spec :as us] - [app.config :as cfg] + [app.config :as cf] [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.data.users :as du] @@ -17,7 +17,7 @@ [app.main.ui.components.forms :as fm] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [tr t]] + [app.util.i18n :as i18n :refer [tr]] [cljs.spec.alpha :as s] [rumext.alpha :as mf])) @@ -40,10 +40,13 @@ ;; --- Profile Form (mf/defc profile-form - [{:keys [locale] :as props}] + [] (let [profile (mf/deref refs/profile) - form (fm/use-form :spec ::profile-form - :initial profile)] + initial (mf/with-memo [profile] + (let [subscribed? (-> profile :props :newsletter-subscribed)] + (assoc profile :newsletter-subscribed subscribed?))) + form (fm/use-form :spec ::profile-form :initial initial)] + [:& fm/form {:on-submit on-submit :form form :class "profile-form"} @@ -51,7 +54,7 @@ [:& fm/input {:type "text" :name :fullname - :label (t locale "dashboard.your-name")}]] + :label (tr "dashboard.your-name")}]] [:div.fields-row [:& fm/input @@ -59,29 +62,40 @@ :name :email :disabled true :help-icon i/at - :label (t locale "dashboard.your-email")}] + :label (tr "dashboard.your-email")}] [:div.options [:div.change-email [:a {:on-click #(modal/show! :change-email {})} - (t locale "dashboard.change-email")]]]] + (tr "dashboard.change-email")]]]] + + (when (contains? @cf/flags :newsletter-subscription) + [:div.newsletter-subs + [:p.newsletter-title (tr "dashboard.newsletter-title")] + [:& fm/input {:name :newsletter-subscribed + :class "check-primary" + :type "checkbox" + :label (tr "dashboard.newsletter-msg")}] + [:p.info (tr "onboarding.newsletter.privacy1") + [:a {:target "_blank" :href "https://penpot.app/privacy.html"} (tr "onboarding.newsletter.policy")]] + [:p.info (tr "onboarding.newsletter.privacy2")]]) [:& fm/submit-button - {:label (t locale "dashboard.update-settings")}] + {:label (tr "dashboard.save-settings") + :disabled (empty? (:touched @form))}] [:div.links [:div.link-item [:a {:on-click #(modal/show! :delete-account {}) :data-test "remove-acount-btn"} - (t locale "dashboard.remove-account")]]]])) + (tr "dashboard.remove-account")]]]])) ;; --- Profile Photo Form -(mf/defc profile-photo-form - [{:keys [locale] :as props}] - (let [file-input (mf/use-ref nil) - profile (mf/deref refs/profile) - photo (cfg/resolve-profile-photo-url profile) +(mf/defc profile-photo-form [] + (let [file-input (mf/use-ref nil) + profile (mf/deref refs/profile) + photo (cf/resolve-profile-photo-url profile) on-image-click #(dom/click (mf/ref-val file-input)) on-file-selected @@ -90,7 +104,7 @@ [:form.avatar-form [:div.image-change-field - [:span.update-overlay {:on-click on-image-click} (t locale "labels.update")] + [:span.update-overlay {:on-click on-image-click} (tr "labels.update")] [:img {:src photo}] [:& file-uploader {:accept "image/jpeg,image/png" :multi false @@ -100,14 +114,11 @@ ;; --- Profile Page -(mf/defc profile-page - [{:keys [locale]}] - - (mf/use-effect - #(dom/set-html-title (tr "title.settings.profile"))) - +(mf/defc profile-page [] + (mf/with-effect [] + (dom/set-html-title (tr "title.settings.profile"))) [:div.dashboard-settings [:div.form-container.two-columns - [:& profile-photo-form {:locale locale}] - [:& profile-form {:locale locale}]]]) + [:& profile-photo-form] + [:& profile-form]]]) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index ce91504364..46081d3e3c 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -581,7 +581,19 @@ msgstr "Search results" msgid "dashboard.type-something" msgstr "Type to search results" -#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.save-settings" +msgstr "Save settings" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.newsletter-title" +msgstr "Newsletter subscription" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.newsletter-msg" +msgstr "Send me news, product updates and recommendations about Penpot." + +#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Update settings" @@ -1855,6 +1867,30 @@ msgstr "" msgid "onboarding.welcome.title" msgstr "Welcome to Penpot" +msgid "onboarding.newsletter.title" +msgstr "Want to receive Penpot news?" + +msgid "onboarding.newsletter.desc" +msgstr "Subscribe to our newsletter to stay up to date with product development progress and news." + +msgid "onboarding.newsletter.privacy1" +msgstr "Because we care about privacy, here's our " + +msgid "onboarding.newsletter.policy" +msgstr "Privacy Policy." + +msgid "onboarding.newsletter.privacy2" +msgstr "We will only send relevant emails to you. You can unsubscribe at any time in your user profile or via the unsubscribe link in any of our newsletters." + +msgid "onboarding.newsletter.accept" +msgstr "Yes, subscribe" + +msgid "onboarding.newsletter.decline" +msgstr "No, thanks" + +msgid "onboarding.newsletter.acceptance-message" +msgstr "Your subscription request has been sent, we will send you an email to confirm it." + #: src/app/main/ui/auth/recovery.cljs msgid "profile.recovery.go-to-login" msgstr "Go to login" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index c09e0e91b4..0aa4de77c2 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -587,7 +587,20 @@ msgstr "Resultados de búsqueda" msgid "dashboard.type-something" msgstr "Escribe algo para buscar" -#: src/app/main/ui/settings/profile.cljs, src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.save-settings" +msgstr "Guardar opciones" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.newsletter-title" +msgstr "Suscripción a newsletter" + +#: src/app/main/ui/settings/profile.cljs +msgid "dashboard.newsletter-msg" +msgstr "Envíame noticias, actualizaciones de producto y recomendaciones sobre Penpot." + + +#: src/app/main/ui/settings/password.cljs, src/app/main/ui/settings/options.cljs msgid "dashboard.update-settings" msgstr "Actualizar opciones" @@ -1876,6 +1889,31 @@ msgstr "" msgid "onboarding.welcome.title" msgstr "Te damos la bienvenida a Penpot" +msgid "onboarding.newsletter.title" +msgstr "¿Quieres recibir noticias sobre Penpot?" + +msgid "onboarding.newsletter.desc" +msgstr "Suscríbete a nuestra newsletter para estar al día de los progresos del producto y noticias." + +msgid "onboarding.newsletter.privacy1" +msgstr "Porque nos importa la privacidad, aquí puedes ver nuestra " + +msgid "onboarding.newsletter.policy" +msgstr "Política de Privacidad." + +msgid "onboarding.newsletter.privacy2" +msgstr "Sólo te enviaremos emails relevantes para ti. Puedes desuscribirte en cualquier momento desde tu perfil o usando el vínculo de desuscripción en cualquiera de nuestras newsletters." + +msgid "onboarding.newsletter.accept" +msgstr "Si, suscribirme" + +msgid "onboarding.newsletter.decline" +msgstr "No, gracias" + +msgid "onboarding.newsletter.acceptance-message" +msgstr "Tu solicitud de suscripción ha sido enviada, te haremos una confirmación a tu email" + + #: src/app/main/ui/auth/recovery.cljs msgid "profile.recovery.go-to-login" msgstr "Ir al login"