0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-15 09:11:21 -05:00

Merge pull request #2145 from penpot/palba-templates-carousel

🎉 Add Libraries & Templates carousel
This commit is contained in:
Eva Marco 2022-08-10 11:52:21 +02:00 committed by GitHub
commit 5b5fe8ebbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 456 additions and 43 deletions

View file

@ -8,6 +8,7 @@
- Add cosmetic changes in viewer mode [Taiga #3688](https://tree.taiga.io/project/penpot/us/3688)
- Outline highlights on layer hovering [Taiga #2645](https://tree.taiga.io/project/penpot/us/2645) by @andrewzhurov
- Add zoom to shape on double click up on its icon [Taiga #3929](https://tree.taiga.io/project/penpot/us/3929) by @andrewzhurov
- Add Libraries & Templates carousel [Taiga #3860](https://tree.taiga.io/project/penpot/us/3860)
### :bug: Bugs fixed

View file

@ -400,4 +400,4 @@
(sv/defmethod ::retrieve-list-of-builtin-templates
[cfg _params]
(mapv #(select-keys % [:id :name]) (:templates cfg)))
(mapv #(select-keys % [:id :name :thumbnail-uri]) (:templates cfg)))

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 132 132"><path d="m132 95-4 4-4 4-29-29-29-29-29 29-29 29-4-4-4-4 33-33 33-33 33 33Z"/></svg>

After

Width:  |  Height:  |  Size: 146 B

View file

@ -33,6 +33,7 @@
.dashboard-content {
display: flex;
flex-direction: column;
position: relative;
}
.verify-token {

View file

@ -6,6 +6,7 @@
.dashboard-sidebar {
background-color: $color-white;
z-index: 1;
.sidebar-inside {
display: flex;

View file

@ -254,3 +254,155 @@
height: 16px;
}
}
.dashboard-templates-section {
position: absolute;
bottom: 0;
width: 100%;
height: 285px;
transition: bottom 300ms;
&.collapsed {
bottom: -228px;
transition: bottom 300ms;
}
.title {
width: 100%;
text-align: right;
height: 56px;
cursor: pointer;
div {
height: 58px;
display: inline-block;
line-height: 58px;
text-align: center;
border-top: 2px solid #e4e4e4;
border-left: 2px solid #e4e4e4;
border-right: 2px solid #e4e4e4;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
margin-right: 30px;
background-color: $color-white;
position: relative;
z-index: 1;
span {
display: inline-block;
vertical-align: middle;
line-height: normal;
font-size: 18px;
font-weight: 600;
color: $color-black;
margin-left: 20px;
margin-right: 10px;
}
svg {
width: 12px;
height: 12px;
}
}
}
.button {
position: absolute;
top: 133px;
border: 2px solid #e0e4e9;
border-radius: 50%;
text-align: center;
line-height: 35px;
width: 35px;
height: 35px;
cursor: pointer;
background-color: $color-white;
svg {
width: 12px;
height: 12px;
}
&.left {
left: 0;
margin-left: 43px;
}
&.right {
right: 0;
margin-right: 43px;
}
&:hover {
border: 2px solid $color-primary;
}
}
.content {
background-color: $color-white;
width: 200%;
height: 229px;
border-top: 2px solid #e4e4e4;
border-left: 2px solid #e4e4e4;
margin-left: 5px;
position: absolute;
.card-container {
width: 275px;
height: 100%;
margin-top: 20px;
display: inline-block;
text-align: center;
vertical-align: top;
}
.template-card {
display: inline-block;
width: 255px;
font-size: 16px;
color: #181a22;
cursor: pointer;
.img-container {
width: 100%;
height: 135px;
margin-bottom: 15px;
border-radius: 5px;
border: 2px solid #e0e4e9;
display: flex;
justify-content: center;
flex-direction: column;
}
.card-name {
padding: 0 5px;
display: flex;
justify-content: space-between;
height: 23px;
svg {
width: 16px;
height: 16px;
}
}
.template-link {
border: 2px solid transparent;
margin: 30px;
padding: 32px 0;
}
.template-link-title {
font-size: 14px;
font-weight: 600;
color: $color-gray-60;
}
.template-link-text {
font-size: 12px;
color: $color-gray-30;
}
&:hover {
.img-container {
border: 2px solid $color-primary;
}
}
}
}
}

View file

@ -60,6 +60,7 @@
(declare fetch-projects)
(declare fetch-team-members)
(declare fetch-builtin-templates)
(defn initialize
[{:keys [id] :as params}]
@ -87,7 +88,8 @@
(ptk/watch (fetch-projects) state stream)
(ptk/watch (fetch-team-members) state stream)
(ptk/watch (du/fetch-teams) state stream)
(ptk/watch (du/fetch-users {:team-id id}) state stream)))))
(ptk/watch (du/fetch-users {:team-id id}) state stream)
(ptk/watch (fetch-builtin-templates) state stream)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Fetching (context aware: current team)
@ -293,6 +295,24 @@
(->> (rp/query :team-recent-files {:team-id team-id})
(rx/map recent-files-fetched)))))))
;; --- EVENT: fetch-team-invitations
(defn builtin-templates-fetched
[libraries]
(ptk/reify ::libraries-fetched
ptk/UpdateEvent
(update [_ state]
(assoc state :builtin-templates libraries))))
(defn fetch-builtin-templates
[]
(ptk/reify ::fetch-builtin-templates
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/command :retrieve-list-of-builtin-templates)
(rx/map builtin-templates-fetched)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Selection
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -795,6 +815,27 @@
(rx/tap on-success)
(rx/catch on-error))))))
;; --- EVENT: clone-template
(defn clone-template
[{:keys [template-id project-id] :as params}]
(us/assert ::us/uuid project-id)
(ptk/reify ::clone-template
IDeref
(-deref [_]
{:template-id template-id
:project-id project-id})
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/command! :clone-template {:project-id project-id :template-id template-id})
(rx/tap on-success)
(rx/catch on-error))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Navigation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -95,6 +95,7 @@
(derive :app.main.data.dashboard/set-file-shared ::generic-action)
(derive :app.main.data.dashboard/update-team-member-role ::generic-action)
(derive :app.main.data.dashboard/update-team-photo ::generic-action)
(derive :app.main.data.dashboard/clone-template ::generic-action)
(derive :app.main.data.fonts/add-font ::generic-action)
(derive :app.main.data.fonts/delete-font ::generic-action)
(derive :app.main.data.fonts/delete-font-variant ::generic-action)

View file

@ -7,9 +7,13 @@
(ns app.main.ui.dashboard
(:require
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.spec :as us]
[app.main.data.dashboard :as dd]
[app.main.data.dashboard.shortcuts :as sc]
[app.main.data.events :as ev]
[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.context :as ctx]
@ -23,9 +27,16 @@
[app.main.ui.dashboard.sidebar :refer [sidebar]]
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page team-invitations-page]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.object :as obj]
[app.util.router :as rt]
[cuerdas.core :as str]
[goog.events :as events]
[okulary.core :as l]
[potok.core :as ptk]
[rumext.alpha :as mf])
(:import goog.events.EventType))
@ -48,40 +59,180 @@
(uuid-str? project-id)
(assoc :project-id (uuid project-id)))))
(def builtin-templates
(l/derived :builtin-templates st/state))
(mf/defc templates-section
[{:keys [default-project-id profile project team content-width] :as props}]
(let [templates (mf/deref builtin-templates)
route (mf/deref refs/route)
route-name (get-in route [:data :name])
section (if (= route-name :dashboard-files)
(if (= (:id project) default-project-id)
"dashboard-drafts"
"dashboard-project")
(name route-name))
props (some-> profile (get :props {}))
collapsed (:builtin-templates-collapsed-status props false)
card-offset (mf/use-state 0)
card-width 275
num-cards (count templates)
;; We need space for num-cards plus the libraries&templates link
more-cards (> (+ @card-offset (* (+ 1 num-cards) card-width)) content-width)
content-ref (mf/use-ref)
toggle-collapse
(fn []
(st/emit!
(du/update-profile-props {:builtin-templates-collapsed-status (not collapsed)})))
move-left
(fn []
(when-not (zero? @card-offset)
(dom/animate! (mf/ref-val content-ref)
[#js {:left (str @card-offset "px")}
#js {:left (str (+ @card-offset card-width) "px")}]
#js {:duration 200
:easing "linear"})
(reset! card-offset (+ @card-offset card-width))))
move-right
(fn []
(when more-cards (swap! card-offset inc)
(dom/animate! (mf/ref-val content-ref)
[#js {:left (str @card-offset "px")}
#js {:left (str (- @card-offset card-width) "px")}]
#js {:duration 200
:easing "linear"})
(reset! card-offset (- @card-offset card-width))))
on-finish-import
(fn [template]
(st/emit!
(ptk/event ::ev/event {::ev/name "import-template-finish"
::ev/origin "dashboard"
:template (:name template)
:section section})
(when (not (some? project)) (rt/nav :dashboard-files
{:team-id (:id team)
:project-id default-project-id}))))
import-template
(fn [template]
(let [templates-project-id (if project (:id project) default-project-id)]
(st/emit!
(ptk/event ::ev/event {::ev/name "import-template-launch"
::ev/origin "dashboard"
:template (:name template)
:section section})
(modal/show
{:type :import
:project-id templates-project-id
:files []
:template template
:on-finish-import (partial on-finish-import template)}))))
handle-template-link
(fn []
(st/emit! (ptk/event ::ev/event {::ev/name "explore-libraries-click"
::ev/origin "dashboard"
:section section})))]
[:div.dashboard-templates-section {:class (when collapsed "collapsed")}
[:div.title {:on-click toggle-collapse}
[:div
[:span (tr "dashboard.libraries-and-templates")]
[:span.icon (if collapsed i/arrow-up i/arrow-down)]]]
[:div.content {:ref content-ref
:style {:left @card-offset}}
(for [num-item (range (count templates)) :let [item (nth templates num-item)]]
[:div.card-container {:id (str/concat "card-container-" num-item)
:key (:id item)
:on-click #(import-template item)}
[:div.template-card
[:div.img-container
[:img {:src (:thumbnail-uri item)}]]
[:div.card-name [:span (:name item)] [:span.icon i/download]]]])
[:div.card-container
[:div.template-card
[:div.img-container
[:a {:href "https://penpot.app/libraries-templates.html" :target "_blank" :on-click handle-template-link}
[:div.template-link
[:div.template-link-title (tr "dashboard.libraries-and-templates")]
[:div.template-link-text (tr "dashboard.libraries-and-templates.explore")]]]]]]]
(when (< @card-offset 0)
[:div.button.left {:on-click move-left} i/go-prev])
(when more-cards
[:div.button.right {:on-click move-right} i/go-next])]))
(mf/defc dashboard-content
[{:keys [team projects project section search-term profile] :as props}]
[:div.dashboard-content {:on-click #(st/emit! (dd/clear-selected-files))}
(case section
:dashboard-projects
[:& projects-section {:team team :projects projects}]
(let [container (mf/use-ref)
content-width (mf/use-state 0)
default-project-id
(->> (vals projects)
(d/seek :is-default)
(:id))
on-resize
(fn [_]
(let [dom (mf/ref-val container)
width (obj/get dom "clientWidth")]
(reset! content-width width)))]
:dashboard-fonts
[:& fonts-page {:team team}]
(mf/use-effect
#(let [key1 (events/listen js/window "resize" on-resize)]
(fn []
(events/unlistenByKey key1))))
:dashboard-font-providers
[:& font-providers-page {:team team}]
(mf/use-effect on-resize)
[:div.dashboard-content {:on-click #(st/emit! (dd/clear-selected-files)) :ref container}
(case section
:dashboard-projects
[:*
[:& projects-section {:team team :projects projects}]
[:& templates-section {:profile profile
:project project
:default-project-id default-project-id
:team team
:content-width @content-width}]]
:dashboard-files
(when project
[:& files-section {:team team :project project}])
:dashboard-fonts
[:& fonts-page {:team team}]
:dashboard-search
[:& search-page {:team team
:search-term search-term}]
:dashboard-font-providers
[:& font-providers-page {:team team}]
:dashboard-libraries
[:& libraries-page {:team team}]
:dashboard-files
(when project
[:*
[:& files-section {:team team :project project}]
[:& templates-section {:profile profile
:project project
:default-project-id default-project-id
:team team
:content-width @content-width}]])
:dashboard-team-members
[:& team-members-page {:team team :profile profile}]
:dashboard-search
[:& search-page {:team team
:search-term search-term}]
:dashboard-team-invitations
[:& team-invitations-page {:team team}]
:dashboard-libraries
[*
[:& libraries-page {:team team}]]
:dashboard-team-settings
[:& team-settings-page {:team team :profile profile}]
:dashboard-team-members
[:& team-members-page {:team team :profile profile}]
nil)])
:dashboard-team-invitations
[:& team-invitations-page {:team team}]
:dashboard-team-settings
[:& team-settings-page {:team team :profile profile}]
nil)]))
(mf/defc dashboard
[{:keys [route profile] :as props}]

View file

@ -118,7 +118,7 @@
(dom/prevent-default event)
(st/emit! (with-meta (dd/create-file {:project-id (:id project)})
{::ev/origin origin}))))]
(mf/use-effect
(fn []
(let [node (mf/ref-val rowref)
@ -134,7 +134,7 @@
(fn []
(vreset! mnt? false)
(rx/dispose! sub)))))
(mf/use-effect
(mf/deps project)
@ -152,7 +152,7 @@
(dd/clear-selected-files))))
[:*
[:& header {:team team :project project
[:& header {:team team :project project
:on-create-clicked on-create-clicked}]
[:section.dashboard-container.no-bg {:ref rowref}
[:& grid {:project project

View file

@ -8,7 +8,9 @@
(:require
[app.common.data :as d]
[app.common.logging :as log]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
[app.main.data.messages :as dm]
[app.main.data.modal :as modal]
[app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader]]
@ -236,10 +238,11 @@
(mf/defc import-dialog
{::mf/register modal/components
::mf/register-as :import}
[{:keys [project-id files on-finish-import]}]
[{:keys [project-id files template on-finish-import]}]
(let [state (mf/use-state
{:status :analyzing
:editing nil
:importing-templates 0
:files (->> files
(mapv #(assoc % :status :analyzing)))})
@ -278,19 +281,50 @@
(dom/prevent-default event)
(st/emit! (modal/hide)))))
on-template-cloned-success
(fn []
(swap! state
(fn [state]
(-> state
(assoc :status :importing :importing-templates 0))))
(st/emit! (dd/fetch-recent-files)))
on-template-cloned-error
(fn []
(st/emit!
(modal/hide)
(dm/error (tr "dashboard.libraries-and-templates.import-error"))))
continue-files
(fn []
(let [files (->> @state :files (filterv #(and (= :ready (:status %)) (not (:deleted? %)))))]
(import-files project-id files))
(swap! state
(fn [state]
(-> state
(assoc :status :importing)
(update :files mark-files-importing)))))
continue-template
(fn []
(let [mdata {:on-success on-template-cloned-success :on-error on-template-cloned-error}
params {:project-id project-id :template-id (:id template)}]
(swap! state
(fn [state]
(-> state
(assoc :status :importing :importing-templates 1))))
(st/emit! (dd/clone-template (with-meta params mdata)))))
handle-continue
(mf/use-callback
(mf/deps project-id (:files @state))
(fn [event]
(dom/prevent-default event)
(let [files (->> @state :files (filterv #(and (= :ready (:status %)) (not (:deleted? %)))))]
(import-files project-id files))
(swap! state
(fn [state]
(-> state
(assoc :status :importing)
(update :files mark-files-importing))))))
(if (some? template)
(continue-template)
(continue-files))))
handle-accept
(mf/use-callback
@ -299,10 +333,15 @@
(st/emit! (modal/hide))
(when on-finish-import (on-finish-import))))
num-importing (+
(->> @state :files (filter #(= (:status %) :importing)) count)
(:importing-templates @state))
warning-files (->> @state :files (filter #(and (= (:status %) :import-finish) (d/not-empty? (:errors %)))) count)
success-files (->> @state :files (filter #(and (= (:status %) :import-finish) (empty? (:errors %)))) count)
pending-analysis? (> (->> @state :files (filter #(= (:status %) :analyzing)) count) 0)
pending-import? (> (->> @state :files (filter #(= (:status %) :importing)) count) 0)
pending-import? (> num-importing 0)
files (->> (:files @state) (filterv (comp not :deleted?)))]
(mf/use-effect
@ -334,7 +373,7 @@
[:div.feedback-banner
[:div.icon i/checkbox-checked]
[:div.message (tr "dashboard.import.import-message" success-files)]]))
[:div.message (tr "dashboard.import.import-message" (if (some? template) 1 success-files))]]))
(for [file files]
(let [editing? (and (some? (:file-id file))
@ -342,7 +381,13 @@
[:& import-entry {:state state
:file file
:editing? editing?
:can-be-deleted? (> (count files) 1)}]))]
:can-be-deleted? (> (count files) 1)}]))
(when (some? template)
[:& import-entry {:state state
:file (assoc template :status (if (= 1 (:importing-templates @state)) :importing :ready))
:editing? false
:can-be-deleted? false}])]
[:div.modal-footer
[:div.action-buttons

View file

@ -172,7 +172,7 @@
(dom/clean-value! search-input)
(dom/focus! search-input)
(emit! (dd/go-to-search)))))
on-key-press
(mf/use-callback
(fn [e]

View file

@ -24,6 +24,7 @@
(def arrow-down (icon-xref :arrow-down))
(def arrow-end (icon-xref :arrow-end))
(def arrow-slide (icon-xref :arrow-slide))
(def arrow-up (icon-xref :arrow-up))
(def artboard (icon-xref :artboard))
(def at (icon-xref :at))
(def auto-direction (icon-xref :auto-direction))

View file

@ -9,7 +9,7 @@
[app.config :as cf]
[app.main.data.events :as ev]
[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.ui.onboarding.newsletter]
[app.main.ui.onboarding.questions]
@ -22,7 +22,7 @@
;; --- ONBOARDING LIGHTBOX
(defn send-event
(defn send-event
[event-name]
(st/emit! (ptk/event ::ev/event {::ev/name event-name
::ev/origin "dashboard"})))

View file

@ -643,6 +643,15 @@ msgstr "Your name"
msgid "dashboard.your-penpot"
msgstr "Your Penpot"
msgid "dashboard.libraries-and-templates"
msgstr "Libraries & Templates"
msgid "dashboard.libraries-and-templates.explore"
msgstr "Explore more of them and know how to contribute"
msgid "dashboard.libraries-and-templates.import-error"
msgstr "There was a problem importing the template. The template wasn't imported."
#: src/app/main/ui/alert.cljs
msgid "ds.alert-ok"
msgstr "Ok"

View file

@ -663,6 +663,15 @@ msgstr "Tu nombre"
msgid "dashboard.your-penpot"
msgstr "Tu Penpot"
msgid "dashboard.libraries-and-templates"
msgstr "Bibliotecas y plantillas"
msgid "dashboard.libraries-and-templates.explore"
msgstr "Explora más y descubre cómo contribuir"
msgid "dashboard.libraries-and-templates.import-error"
msgstr "Hubo un problema importando la plantilla. No ha podido ser importada."
#: src/app/main/ui/alert.cljs
msgid "ds.alert-ok"
msgstr "Ok"