Merge branch 'us/newsletter_subscription' into staging

This commit is contained in:
Andrey Antukh 2022-04-04 23:12:03 +02:00
commit 7105255212
19 changed files with 402 additions and 114 deletions

View file

@ -8,6 +8,7 @@
### :arrow_up: Deps updates ### :arrow_up: Deps updates
### :heart: Community contributions by (Thank you!) ### :heart: Community contributions by (Thank you!)
## 1.13.0-beta ## 1.13.0-beta
### :boom: Breaking changes ### :boom: Breaking changes

View file

@ -209,6 +209,9 @@
{:cron #app/cron "0 0 0 * * ?" ;; daily {:cron #app/cron "0 0 0 * * ?" ;; daily
:task :tasks-gc} :task :tasks-gc}
{:cron #app/cron "0 30 */3,23 * * ?"
:task :telemetry}
(when (cf/get :fdata-storage-backed) (when (cf/get :fdata-storage-backed)
{:cron #app/cron "0 0 * * * ?" ;; hourly {:cron #app/cron "0 0 * * * ?" ;; hourly
:task :file-offload}) :task :file-offload})
@ -219,12 +222,7 @@
(when (contains? cf/flags :audit-log-gc) (when (contains? cf/flags :audit-log-gc)
{:cron #app/cron "0 0 0 * * ?" ;; daily {:cron #app/cron "0 0 0 * * ?" ;; daily
:task :audit-log-gc}) :task :audit-log-gc})]}
(when (or (contains? cf/flags :telemetry)
(cf/get :telemetry-enabled))
{:cron #app/cron "0 30 */3,23 * * ?"
:task :telemetry})]}
:app.worker/registry :app.worker/registry
{:metrics (ig/ref :app.metrics/metrics) {:metrics (ig/ref :app.metrics/metrics)

View file

@ -6,6 +6,7 @@
(ns app.rpc.mutations.profile (ns app.rpc.mutations.profile
(:require (:require
[app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
@ -30,7 +31,7 @@
(s/def ::email ::us/email) (s/def ::email ::us/email)
(s/def ::fullname ::us/not-empty-string) (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 ::path ::us/string)
(s/def ::profile-id ::us/uuid) (s/def ::profile-id ::us/uuid)
(s/def ::password ::us/not-empty-string) (s/def ::password ::us/not-empty-string)
@ -342,27 +343,41 @@
;; --- MUTATION: Update Profile (own) ;; --- MUTATION: Update Profile (own)
(defn- update-profile (s/def ::newsletter-subscribed ::us/boolean)
[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 ::update-profile (s/def ::update-profile
(s/keys :req-un [::id ::fullname] (s/keys :req-un [::fullname ::profile-id]
:opt-un [::lang ::theme])) :opt-un [::lang ::theme ::newsletter-subscribed]))
(sv/defmethod ::update-profile (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] (db/with-atomic [conn pool]
(let [profile (update-profile conn params)] ;; NOTE: we need to retrieve the profile independently if we use
(with-meta profile ;; 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)})))) {::audit/props (audit/profile->props profile)}))))
;; --- MUTATION: Update Password ;; --- MUTATION: Update Password

View file

@ -12,7 +12,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cfg] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.util.async :refer [thread-sleep]] [app.util.async :refer [thread-sleep]]
[app.util.json :as json] [app.util.json :as json]
@ -25,6 +25,7 @@
(declare get-stats) (declare get-stats)
(declare send!) (declare send!)
(declare get-subscriptions)
(s/def ::http-client fn?) (s/def ::http-client fn?)
(s/def ::version ::us/string) (s/def ::version ::us/string)
@ -38,18 +39,39 @@
(defmethod ig/init-key ::handler (defmethod ig/init-key ::handler
[_ {:keys [pool sprops version] :as cfg}] [_ {:keys [pool sprops version] :as cfg}]
(fn [{:keys [send?] :or {send? true}}] (fn [{:keys [send? enabled?] :or {send? true enabled? false}}]
;; Sleep randomly between 0 to 10s (let [subs (get-subscriptions pool)
(when send? enabled? (or enabled?
(thread-sleep (rand-int 10000))) (contains? cf/flags :telemetry)
(cf/get :telemetry-enabled))
(let [instance-id (:instance-id sprops) data {:subscriptions subs
stats (-> (get-stats pool version) :version version
(assoc :instance-id instance-id))] :instance-id (:instance-id sprops)}]
(when send? (cond
(send! cfg stats)) ;; 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 ;; IMPL
@ -68,6 +90,12 @@
:response-status (:status response) :response-status (:status response)
:response-body (:body 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 (defn- retrieve-num-teams
[conn] [conn]
(-> (db/exec-one! conn ["select count(*) as count from team;"]) :count)) (-> (db/exec-one! conn ["select count(*) as count from team;"]) :count))
@ -166,12 +194,11 @@
:user-tz (System/getProperty "user.timezone")})) :user-tz (System/getProperty "user.timezone")}))
(defn get-stats (defn get-stats
[conn version] [conn]
(let [referer (if (cfg/get :telemetry-with-taiga) (let [referer (if (cf/get :telemetry-with-taiga)
"taiga" "taiga"
(cfg/get :telemetry-referer))] (cf/get :telemetry-referer))]
(-> {:version version (-> {:referer referer
:referer referer
:total-teams (retrieve-num-teams conn) :total-teams (retrieve-num-teams conn)
:total-projects (retrieve-num-projects conn) :total-projects (retrieve-num-projects conn)
:total-files (retrieve-num-files conn) :total-files (retrieve-num-files conn)

View file

@ -21,13 +21,16 @@
(with-mocks [mock {:target 'app.tasks.telemetry/send! (with-mocks [mock {:target 'app.tasks.telemetry/send!
:return nil}] :return nil}]
(let [task-fn (-> th/*system* :app.worker/registry :telemetry) (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 ;; run the task
(task-fn nil) (task-fn {:send? true :enabled? true})
(t/is (:called? @mock)) (t/is (:called? @mock))
(let [[_ data] (-> @mock :call-args)] (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-fonts))
(t/is (contains? data :total-users)) (t/is (contains? data :total-users))
(t/is (contains? data :total-projects)) (t/is (contains? data :total-projects))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -109,6 +109,38 @@
flex-direction: column; flex-direction: column;
max-width: 368px; max-width: 368px;
width: 100%; 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, .options-form,

View file

@ -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 { .deco {
@ -1004,6 +1055,23 @@
top: -18px; top: -18px;
width: 60px; width: 60px;
&.top {
width: 183px;
top: -106px;
left: 161px;
}
&.newsletter-right {
left: 515px;
top: 50px;
}
&.newsletter-left {
width: 26px;
left: -15px;
top: -15px;
}
&.right { &.right {
left: 590px; left: 590px;
top: 0; top: 0;

View file

@ -54,11 +54,14 @@
:browser :browser
:webworker)) :webworker))
(def default-flags
[:enable-newsletter-subscription])
(defn- parse-flags (defn- parse-flags
[global] [global]
(let [flags (obj/get global "penpotFlags" "") (let [flags (obj/get global "penpotFlags" "")
flags (sequence (map keyword) (str/words flags))] flags (sequence (map keyword) (str/words flags))]
(flags/parse flags/default flags))) (flags/parse flags/default default-flags flags)))
(defn- parse-version (defn- parse-version
[global] [global]

View file

@ -303,8 +303,8 @@
(watch [_ _ stream] (watch [_ _ stream]
(let [mdata (meta data) (let [mdata (meta data)
on-success (:on-success mdata identity) on-success (:on-success mdata identity)
on-error (:on-error mdata #(rx/throw %))] on-error (:on-error mdata rx/throw)]
(->> (rp/mutation :update-profile data) (->> (rp/mutation :update-profile (dissoc data :props))
(rx/catch on-error) (rx/catch on-error)
(rx/mapcat (rx/mapcat
(fn [_] (fn [_]
@ -392,7 +392,6 @@
(->> (rp/mutation :update-profile-props {:props props}) (->> (rp/mutation :update-profile-props {:props props})
(rx/map (constantly (fetch-profile))))))))) (rx/map (constantly (fetch-profile)))))))))
(defn mark-questions-as-answered (defn mark-questions-as-answered
[] []
(ptk/reify ::mark-questions-as-answered (ptk/reify ::mark-questions-as-answered

View file

@ -59,15 +59,15 @@
klass (str more-classes " " klass (str more-classes " "
(dom/classnames (dom/classnames
:focus @focus? :focus @focus?
:valid (and touched? (not error)) :valid (and touched? (not error))
:invalid (and touched? error) :invalid (and touched? error)
:disabled disabled :disabled disabled
:empty (and is-text? (str/empty? value)) :empty (and is-text? (str/empty? value))
:with-icon (not (nil? help-icon')) :with-icon (not (nil? help-icon'))
:custom-input is-text? :custom-input is-text?
:input-radio is-radio? :input-radio is-radio?
:input-checkbox is-checkbox?)) :input-checkbox is-checkbox?))
swap-text-password swap-text-password
(fn [] (fn []
@ -78,7 +78,7 @@
on-focus #(reset! focus? true) on-focus #(reset! focus? true)
on-change (fn [event] 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))) (fm/on-input-change form input-name value trim)))
on-blur on-blur
@ -87,16 +87,23 @@
(when-not (get-in @form [:touched input-name]) (when-not (get-in @form [:touched input-name])
(swap! form assoc-in [:touched input-name] true))) (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 props (-> props
(dissoc :help-icon :form :trim :children) (dissoc :help-icon :form :trim :children)
(assoc :id (name input-name) (assoc :id (name input-name)
:value value :value value
:auto-focus auto-focus? :auto-focus auto-focus?
:on-click (when (or is-radio? is-checkbox?) on-click)
:on-focus on-focus :on-focus on-focus
:on-blur on-blur :on-blur on-blur
:placeholder label :placeholder label
:on-change on-change :on-change on-change
:type @type') :type @type')
(cond-> (and value is-checkbox?) (assoc :default-checked value))
(obj/clj->props))] (obj/clj->props))]
[:div [:div
@ -210,7 +217,7 @@
(let [form (or form (mf/use-ctx form-ctx))] (let [form (or form (mf/use-ctx form-ctx))]
[:input.btn-primary.btn-large [:input.btn-primary.btn-large
{:name "submit" {: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)) :disabled (or (not (:valid @form)) (true? disabled))
:on-click on-click :on-click on-click
:value label :value label

View file

@ -10,6 +10,7 @@
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.onboarding.newsletter]
[app.main.ui.onboarding.questions] [app.main.ui.onboarding.questions]
[app.main.ui.onboarding.team-choice] [app.main.ui.onboarding.team-choice]
[app.main.ui.onboarding.templates] [app.main.ui.onboarding.templates]
@ -134,8 +135,10 @@
[:p (tr "onboarding.slide.3.desc1")] [:p (tr "onboarding.slide.3.desc1")]
[:p (tr "onboarding.slide.3.desc2")]] [:p (tr "onboarding.slide.3.desc2")]]
[:div.modal-navigation [:div.modal-navigation
[:button.btn-secondary {:on-click skip [:button.btn-secondary
:data-test "slide-3-btn"} (tr "labels.start")] {:on-click skip
:data-test "slide-3-btn"}
(tr "labels.start")]
[:& rc/navigation-bullets [:& rc/navigation-bullets
{:slide slide {:slide slide
:navigate navigate :navigate navigate
@ -149,23 +152,23 @@
klass (mf/use-state "fadeInDown") klass (mf/use-state "fadeInDown")
navigate navigate
(mf/use-callback #(reset! slide %)) (mf/use-fn #(reset! slide %))
skip skip
(mf/use-callback (mf/use-fn
(st/emitf (modal/hide) #(st/emit! (modal/hide)
(modal/show {:type :onboarding-choice}) (if (contains? @cf/flags :newsletter-subscription)
(du/mark-onboarding-as-viewed)))] (modal/show {:type :onboarding-newsletter-modal})
(modal/show {:type :onboarding-choice}))
(du/mark-onboarding-as-viewed)))]
(mf/use-layout-effect (mf/with-effect [@slide]
(mf/deps @slide) (when (not= :start @slide)
(fn [] (reset! klass "fadeIn"))
(when (not= :start @slide) (let [sem (tm/schedule 300 #(reset! klass nil))]
(reset! klass "fadeIn")) (fn []
(let [sem (tm/schedule 300 #(reset! klass nil))] (reset! klass nil)
(fn [] (tm/dispose! sem))))
(reset! klass nil)
(tm/dispose! sem)))))
[:div.modal-overlay [:div.modal-overlay
[:div.animated {:class @klass} [:div.animated {:class @klass}

View file

@ -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"}]]]))

View file

@ -13,7 +13,7 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.forms :as fm] [app.main.ui.components.forms :as fm]
[app.util.dom :as dom] [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] [cljs.spec.alpha :as s]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
@ -30,51 +30,51 @@
(defn- on-submit (defn- on-submit
[form _event] [form _event]
(let [data (:clean-data @form) (let [data (:clean-data @form)
data (cond-> data
(empty? (:lang data))
(assoc :lang nil))
mdata {:on-success (partial on-success form)}] mdata {:on-success (partial on-success form)}]
(st/emit! (du/update-profile (with-meta data mdata))))) (st/emit! (du/update-profile (with-meta data mdata)))))
(mf/defc options-form (mf/defc options-form
[{:keys [locale] :as props}] []
(let [profile (mf/deref refs/profile) (let [profile (mf/deref refs/profile)
initial (mf/with-memo [profile]
(update profile :lang #(or % "")))
form (fm/use-form :spec ::options-form form (fm/use-form :spec ::options-form
:initial profile)] :initial initial)]
[:& fm/form {:class "options-form" [:& fm/form {:class "options-form"
:on-submit on-submit :on-submit on-submit
:form form} :form form}
[:h2 (t locale "labels.language")] [:h2 (tr "labels.language")]
[:div.fields-row [:div.fields-row
[:& fm/select {:options (into [{:label "Auto (browser)" :value "default"}] [:& fm/select {:options (into [{:label "Auto (browser)" :value ""}]
i18n/supported-locales) i18n/supported-locales)
:label (t locale "dashboard.select-ui-language") :label (tr "dashboard.select-ui-language")
:default "" :default ""
:name :lang :name :lang
:data-test "setting-lang"}]] :data-test "setting-lang"}]]
;; TODO: Do not show as long as we only have one theme ;; 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 #_[:div.fields-row
[:& fm/select {:label (t locale "dashboard.select-ui-theme") [:& fm/select {:label (tr "dashboard.select-ui-theme")
:name :theme :name :theme
:default "default" :default "default"
:options [{:label "Default" :value "default"}] :options [{:label "Default" :value "default"}]
:data-test "theme-lang"}]] :data-test "theme-lang"}]]
[:& fm/submit-button [:& fm/submit-button
{:label (t locale "dashboard.update-settings") {:label (tr "dashboard.update-settings")
:data-test "submit-lang-change"}]])) :data-test "submit-lang-change"}]]))
;; --- Password Page ;; --- Password Page
(mf/defc options-page (mf/defc options-page
[{:keys [locale]}] []
(mf/use-effect (mf/use-effect
#(dom/set-html-title (tr "title.settings.options"))) #(dom/set-html-title (tr "title.settings.options")))
[:div.dashboard-settings [:div.dashboard-settings
[:div.form-container [:div.form-container
{:data-test "settings-form"} {:data-test "settings-form"}
[:& options-form {:locale locale}]]]) [:& options-form {}]]])

View file

@ -7,7 +7,7 @@
(ns app.main.ui.settings.profile (ns app.main.ui.settings.profile
(:require (:require
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cfg] [app.config :as cf]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.users :as du] [app.main.data.users :as du]
@ -17,7 +17,7 @@
[app.main.ui.components.forms :as fm] [app.main.ui.components.forms :as fm]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.util.dom :as dom] [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] [cljs.spec.alpha :as s]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
@ -40,10 +40,13 @@
;; --- Profile Form ;; --- Profile Form
(mf/defc profile-form (mf/defc profile-form
[{:keys [locale] :as props}] []
(let [profile (mf/deref refs/profile) (let [profile (mf/deref refs/profile)
form (fm/use-form :spec ::profile-form initial (mf/with-memo [profile]
:initial 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 [:& fm/form {:on-submit on-submit
:form form :form form
:class "profile-form"} :class "profile-form"}
@ -51,7 +54,7 @@
[:& fm/input [:& fm/input
{:type "text" {:type "text"
:name :fullname :name :fullname
:label (t locale "dashboard.your-name")}]] :label (tr "dashboard.your-name")}]]
[:div.fields-row [:div.fields-row
[:& fm/input [:& fm/input
@ -59,29 +62,40 @@
:name :email :name :email
:disabled true :disabled true
:help-icon i/at :help-icon i/at
:label (t locale "dashboard.your-email")}] :label (tr "dashboard.your-email")}]
[:div.options [:div.options
[:div.change-email [:div.change-email
[:a {:on-click #(modal/show! :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 [:& fm/submit-button
{:label (t locale "dashboard.update-settings")}] {:label (tr "dashboard.save-settings")
:disabled (empty? (:touched @form))}]
[:div.links [:div.links
[:div.link-item [:div.link-item
[:a {:on-click #(modal/show! :delete-account {}) [:a {:on-click #(modal/show! :delete-account {})
:data-test "remove-acount-btn"} :data-test "remove-acount-btn"}
(t locale "dashboard.remove-account")]]]])) (tr "dashboard.remove-account")]]]]))
;; --- Profile Photo Form ;; --- Profile Photo Form
(mf/defc profile-photo-form (mf/defc profile-photo-form []
[{:keys [locale] :as props}] (let [file-input (mf/use-ref nil)
(let [file-input (mf/use-ref nil) profile (mf/deref refs/profile)
profile (mf/deref refs/profile) photo (cf/resolve-profile-photo-url profile)
photo (cfg/resolve-profile-photo-url profile)
on-image-click #(dom/click (mf/ref-val file-input)) on-image-click #(dom/click (mf/ref-val file-input))
on-file-selected on-file-selected
@ -90,7 +104,7 @@
[:form.avatar-form [:form.avatar-form
[:div.image-change-field [: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}] [:img {:src photo}]
[:& file-uploader {:accept "image/jpeg,image/png" [:& file-uploader {:accept "image/jpeg,image/png"
:multi false :multi false
@ -100,14 +114,11 @@
;; --- Profile Page ;; --- Profile Page
(mf/defc profile-page (mf/defc profile-page []
[{:keys [locale]}] (mf/with-effect []
(dom/set-html-title (tr "title.settings.profile")))
(mf/use-effect
#(dom/set-html-title (tr "title.settings.profile")))
[:div.dashboard-settings [:div.dashboard-settings
[:div.form-container.two-columns [:div.form-container.two-columns
[:& profile-photo-form {:locale locale}] [:& profile-photo-form]
[:& profile-form {:locale locale}]]]) [:& profile-form]]])

View file

@ -581,7 +581,19 @@ msgstr "Search results"
msgid "dashboard.type-something" msgid "dashboard.type-something"
msgstr "Type to search results" 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" msgid "dashboard.update-settings"
msgstr "Update settings" msgstr "Update settings"
@ -1855,6 +1867,30 @@ msgstr ""
msgid "onboarding.welcome.title" msgid "onboarding.welcome.title"
msgstr "Welcome to Penpot" 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 #: src/app/main/ui/auth/recovery.cljs
msgid "profile.recovery.go-to-login" msgid "profile.recovery.go-to-login"
msgstr "Go to login" msgstr "Go to login"

View file

@ -587,7 +587,20 @@ msgstr "Resultados de búsqueda"
msgid "dashboard.type-something" msgid "dashboard.type-something"
msgstr "Escribe algo para buscar" 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" msgid "dashboard.update-settings"
msgstr "Actualizar opciones" msgstr "Actualizar opciones"
@ -1876,6 +1889,31 @@ msgstr ""
msgid "onboarding.welcome.title" msgid "onboarding.welcome.title"
msgstr "Te damos la bienvenida a Penpot" 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 #: src/app/main/ui/auth/recovery.cljs
msgid "profile.recovery.go-to-login" msgid "profile.recovery.go-to-login"
msgstr "Ir al login" msgstr "Ir al login"