From 896d7726210e479e43bd1458a26aaed27aef274a Mon Sep 17 00:00:00 2001 From: Brent Jett Date: Thu, 25 Feb 2021 16:14:31 -0600 Subject: [PATCH 1/5] initial --- src/apps/fl-media/app.js | 13 +++++++++++-- src/apps/fl-media/ui/index.js | 4 +++- src/apps/fl-media/ui/shell/index.js | 16 ++++++++++++++++ src/apps/fl-media/ui/shell/style.scss | 5 +++++ src/apps/fl-media/{ => ui}/style.scss | 0 src/render/app/style.scss | 1 + 6 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 src/apps/fl-media/ui/shell/index.js create mode 100644 src/apps/fl-media/ui/shell/style.scss rename src/apps/fl-media/{ => ui}/style.scss (100%) diff --git a/src/apps/fl-media/app.js b/src/apps/fl-media/app.js index 07f493d16..7cba08b58 100644 --- a/src/apps/fl-media/app.js +++ b/src/apps/fl-media/app.js @@ -3,9 +3,8 @@ import { __ } from '@wordpress/i18n' import { App, Page, List, Filter, Button, Icon, Media } from 'assistant/ui' import { useAppState, getAppActions } from 'assistant/data' import { defaultState } from './data' -import { UploadCard, FileList } from './ui' +import { UploadCard, FileList, Shell } from './ui' import AppIcon from './icon' -import './style.scss' export default props => ( ( /> ) +const Home = () => { + + return ( + Test + ) +} + + + + const Main = ( { baseURL } ) => { const { listStyle, query, showUploader } = useAppState( 'fl-media' ) const { setListStyle, setQuery, setShowUploader } = getAppActions( 'fl-media' ) diff --git a/src/apps/fl-media/ui/index.js b/src/apps/fl-media/ui/index.js index bc9811ec5..95bb625dc 100644 --- a/src/apps/fl-media/ui/index.js +++ b/src/apps/fl-media/ui/index.js @@ -1,4 +1,6 @@ import UploadCard from './upload-card' import FileList from './file-list' +import Shell from './shell' +import './style.scss' -export { UploadCard, FileList } +export { UploadCard, FileList, Shell } diff --git a/src/apps/fl-media/ui/shell/index.js b/src/apps/fl-media/ui/shell/index.js new file mode 100644 index 000000000..4b8bfa469 --- /dev/null +++ b/src/apps/fl-media/ui/shell/index.js @@ -0,0 +1,16 @@ +import React from 'react' +import c from 'classnames' +import './style.scss' + +const MediaShell = ( { children, className, ...rest } ) => { + return ( +
+ {children} +
+ ) +} + +export default MediaShell diff --git a/src/apps/fl-media/ui/shell/style.scss b/src/apps/fl-media/ui/shell/style.scss new file mode 100644 index 000000000..5a0edafc3 --- /dev/null +++ b/src/apps/fl-media/ui/shell/style.scss @@ -0,0 +1,5 @@ +.fl-asst-media-app-shell { + flex: 1 1 auto; + display: grid; + grid-template-rows: 60px 1fr; +} diff --git a/src/apps/fl-media/style.scss b/src/apps/fl-media/ui/style.scss similarity index 100% rename from src/apps/fl-media/style.scss rename to src/apps/fl-media/ui/style.scss diff --git a/src/render/app/style.scss b/src/render/app/style.scss index 859f3242b..6cd7d5e73 100644 --- a/src/render/app/style.scss +++ b/src/render/app/style.scss @@ -20,6 +20,7 @@ flex: 1 1 auto; position: relative; display: flex; + flex-direction: column; max-height: 100%; min-height: 0; /*min-width: 360px;*/ From 165153f1565d72035fead2ee1b8d91c4228352b5 Mon Sep 17 00:00:00 2001 From: Brent Jett Date: Fri, 26 Feb 2021 11:39:56 -0600 Subject: [PATCH 2/5] Setup sidebar shell and new data fetching --- src/apps/fl-media/app.js | 39 +++++------ src/apps/fl-media/config/index.js | 12 ++++ src/apps/fl-media/data/index.js | 34 +++++++--- src/apps/fl-media/data/use-scroller.js | 42 ++++++++++++ src/apps/fl-media/index.js | 2 +- src/apps/fl-media/ui/index.js | 3 +- src/apps/fl-media/ui/list/index.js | 42 ++++++++++++ src/apps/fl-media/ui/list/style.scss | 18 +++++ src/apps/fl-media/ui/page-main/index.js | 13 ++++ src/apps/fl-media/ui/shell/index.js | 14 +++- src/apps/fl-media/ui/shell/style.scss | 81 ++++++++++++++++++++++- src/render/app/index.js | 2 +- src/system/utils/wordpress/hooks/index.js | 48 ++++++++++++++ src/system/utils/wordpress/index.js | 1 + 14 files changed, 314 insertions(+), 37 deletions(-) create mode 100644 src/apps/fl-media/config/index.js create mode 100644 src/apps/fl-media/data/use-scroller.js create mode 100644 src/apps/fl-media/ui/list/index.js create mode 100644 src/apps/fl-media/ui/list/style.scss create mode 100644 src/apps/fl-media/ui/page-main/index.js create mode 100644 src/system/utils/wordpress/hooks/index.js diff --git a/src/apps/fl-media/app.js b/src/apps/fl-media/app.js index 7cba08b58..1ebae4649 100644 --- a/src/apps/fl-media/app.js +++ b/src/apps/fl-media/app.js @@ -1,31 +1,27 @@ import React from 'react' -import { __ } from '@wordpress/i18n' +//import { __ } from '@wordpress/i18n' import { App, Page, List, Filter, Button, Icon, Media } from 'assistant/ui' -import { useAppState, getAppActions } from 'assistant/data' -import { defaultState } from './data' -import { UploadCard, FileList, Shell } from './ui' -import AppIcon from './icon' - -export default props => ( - -) - -const Home = () => { +//import { useAppState, getAppActions } from 'assistant/data' +//import { defaultState } from './config' +import { MediaAppProvider } from './data' +import { Main, UploadCard, FileList } from './ui' +//import AppIcon from './icon' +export default props => { return ( - Test + + + ) } - - - +/* const Main = ( { baseURL } ) => { const { listStyle, query, showUploader } = useAppState( 'fl-media' ) const { setListStyle, setQuery, setShowUploader } = getAppActions( 'fl-media' ) @@ -162,3 +158,4 @@ const Main = ( { baseURL } ) => { ) } +*/ diff --git a/src/apps/fl-media/config/index.js b/src/apps/fl-media/config/index.js new file mode 100644 index 000000000..c79d40d7e --- /dev/null +++ b/src/apps/fl-media/config/index.js @@ -0,0 +1,12 @@ +export const defaultState = { + listStyle: 'grid', + query: { + post_mime_type: 'all', + order: 'DESC', + orderby: 'date', + label: '0', + }, + showUploader: true, +} + +export const cache = [ 'listStyle', 'query', 'showUploader' ] diff --git a/src/apps/fl-media/data/index.js b/src/apps/fl-media/data/index.js index c79d40d7e..a4c8cacdd 100644 --- a/src/apps/fl-media/data/index.js +++ b/src/apps/fl-media/data/index.js @@ -1,12 +1,24 @@ -export const defaultState = { - listStyle: 'grid', - query: { - post_mime_type: 'all', - order: 'DESC', - orderby: 'date', - label: '0', - }, - showUploader: true, -} +import React, { useContext, createContext } from 'react' +import { useAppState } from 'assistant/data' +import { useAttachments } from 'assistant/utils/wordpress' + +export const MediaAppContext = createContext( {} ) + +export const useMediaApp = () => useContext( MediaAppContext ) -export const cache = [ 'listStyle', 'query', 'showUploader' ] +export const MediaAppProvider = ( { children } ) => { + const { query } = useAppState( 'fl-media' ) + const { items, loadItems, isFetching, hasMore } = useAttachments( query ) + const context = { + items, + loadItems, + isFetching, + hasMore, + } + + return ( + + {children} + + ) +} diff --git a/src/apps/fl-media/data/use-scroller.js b/src/apps/fl-media/data/use-scroller.js new file mode 100644 index 000000000..6f0a51553 --- /dev/null +++ b/src/apps/fl-media/data/use-scroller.js @@ -0,0 +1,42 @@ +import { useEffect, useRef } from 'react' + +const hasReachedBounds = e => { + const { scrollTop, clientHeight, scrollHeight } = e.target + const bottom = scrollTop + clientHeight + return bottom + 150 >= scrollHeight +} + +const defaultOptions = { + onScrollEnd: () => {}, + hasReachedBounds +} + +const useScroller = ( _options = {} ) => { + const { hasReachedBounds, onScrollEnd } = { ...defaultOptions, ..._options } + const hasHitBoundary = useRef( false ) + const ref = useRef( null ) + + useEffect( () => { + + if ( undefined === ref.current ) { + return + } + + const reset = () => hasHitBoundary.current = false + + const onScroll = e => { + if ( hasReachedBounds( e ) && ! hasHitBoundary.current ) { + hasHitBoundary.current = true + onScrollEnd( reset ) + } + } + + ref.current.addEventListener( 'scroll', onScroll ) + + return () => ref.current.removeEventListener( 'scroll', onScroll ) + }, [] ) + + return ref +} + +export default useScroller diff --git a/src/apps/fl-media/index.js b/src/apps/fl-media/index.js index e636a6ada..b6e4da1b2 100644 --- a/src/apps/fl-media/index.js +++ b/src/apps/fl-media/index.js @@ -4,7 +4,7 @@ import { __ } from '@wordpress/i18n' import { addQueryArgs } from 'assistant/utils/url' import { Page } from 'assistant/ui' import Icon from './icon' -import { defaultState, cache } from './data' +import { defaultState, cache } from './config' const App = lazy( () => import( /* webpackChunkName: "app-media" */ './app' diff --git a/src/apps/fl-media/ui/index.js b/src/apps/fl-media/ui/index.js index 95bb625dc..357d4e864 100644 --- a/src/apps/fl-media/ui/index.js +++ b/src/apps/fl-media/ui/index.js @@ -1,6 +1,7 @@ import UploadCard from './upload-card' import FileList from './file-list' import Shell from './shell' +import Main from './page-main' import './style.scss' -export { UploadCard, FileList, Shell } +export { UploadCard, FileList, Shell, Main } diff --git a/src/apps/fl-media/ui/list/index.js b/src/apps/fl-media/ui/list/index.js new file mode 100644 index 000000000..d04a25b52 --- /dev/null +++ b/src/apps/fl-media/ui/list/index.js @@ -0,0 +1,42 @@ +import React from 'react' +import { Layout } from 'assistant/ui' +import { useMediaApp } from '../../data' +import './style.scss' + +const hasReachedBounds = e => { + const { scrollTop, clientHeight, scrollHeight } = e.target + const bottom = scrollTop + clientHeight + return bottom + 150 >= scrollHeight +} + +const MediaList = () => { + const { items, isFetching, loadItems, hasMore } = useMediaApp() + + const onScroll = e => { + if ( hasMore && hasReachedBounds( e ) && ! isFetching ) { + loadItems( items.length ) + } + } + + return ( +
+
    + { items.map( item => { + const { id, thumbnail } = item + return ( +
  • + +
  • + ) + } ) } +
+
+ ) +} + +export default MediaList diff --git a/src/apps/fl-media/ui/list/style.scss b/src/apps/fl-media/ui/list/style.scss new file mode 100644 index 000000000..99cd7a310 --- /dev/null +++ b/src/apps/fl-media/ui/list/style.scss @@ -0,0 +1,18 @@ +.fl-asst-media-app-list-scroller { + flex: 1 1 auto; + max-height: 100%; + min-height: 0; + overflow: auto; +} +ul.fl-asst-media-app-list { + display: grid; + grid-gap: var(--fluid-lg-space); + grid-template-columns: repeat( auto-fill, minmax( 150px, 1fr ) ); + padding: var(--fluid-lg-space); + margin: 0; +} +.fluid-frame-size-medium { + ul.fl-asst-media-app-list { + padding: 30px; + } +} diff --git a/src/apps/fl-media/ui/page-main/index.js b/src/apps/fl-media/ui/page-main/index.js new file mode 100644 index 000000000..c5f79018b --- /dev/null +++ b/src/apps/fl-media/ui/page-main/index.js @@ -0,0 +1,13 @@ +import React from 'react' +import Shell from '../shell' +import MediaList from '../list' + +const Main = ( { baseURL } ) => { + return ( + + + + ) +} + +export default Main diff --git a/src/apps/fl-media/ui/shell/index.js b/src/apps/fl-media/ui/shell/index.js index 4b8bfa469..36b2361c3 100644 --- a/src/apps/fl-media/ui/shell/index.js +++ b/src/apps/fl-media/ui/shell/index.js @@ -1,5 +1,7 @@ import React from 'react' +import { __ } from '@wordpress/i18n' import c from 'classnames' +import MediaIcon from '../../icon' import './style.scss' const MediaShell = ( { children, className, ...rest } ) => { @@ -8,7 +10,17 @@ const MediaShell = ( { children, className, ...rest } ) => { className={ c( 'fl-asst-media-app-shell', className ) } { ...rest } > - {children} +
+
+
+
+ + { __( 'Media', 'fl-assistant' ) } +
+
+
+
+
{children}
) } diff --git a/src/apps/fl-media/ui/shell/style.scss b/src/apps/fl-media/ui/shell/style.scss index 5a0edafc3..4c39cfecf 100644 --- a/src/apps/fl-media/ui/shell/style.scss +++ b/src/apps/fl-media/ui/shell/style.scss @@ -1,5 +1,84 @@ .fl-asst-media-app-shell { flex: 1 1 auto; display: grid; - grid-template-rows: 60px 1fr; + grid-template-rows: minmax( 80px, auto ) 1fr; + min-height: 0; + max-height: 100%; + + .fl-asst-toolbar { + grid-row: 1 / 2; + grid-column: 1 / -1; + display: flex; + + .fl-asst-primary-toolbar-area, + .fl-asst-secondary-toolbar-area { + flex: 1 1 auto; + display: flex; + align-items: center; + z-index: 1; + padding: 20px; + } + .fl-asst-secondary-toolbar-area { + justify-content: flex-end; + } + .fl-asst-media-branding { + display: flex; + font-size: 18px; + + svg { + margin-right: 13px; + } + } + } + + .fl-asst-media-app-content { + grid-row: 2 / -1; + grid-column: 1 / -1; + + /* Scroller stuff */ + min-height: 0; + max-height: 100%; + overflow: auto; + } + + .fl-asst-media-app-sidebar-backdrop { + display: none; + background: hsl( var(--fluid-hue), 33%, 97% ); /* Slightly different from --fluid-opaque-13 */ + } +} + +.fluid-frame-size-medium { + .fl-asst-media-app-shell { + grid-template-columns: minmax( 120px, 200px ) 1fr; + + .fl-asst-toolbar { + display: contents; /* promote children to grid items */ + + .fl-asst-primary-toolbar-area { + grid-row: 1 / 2; + grid-column: 1 / 2; + } + .fl-asst-secondary-toolbar-area { + grid-row: 1 / 2; + grid-column: 2 / -1; + } + } + + .fl-asst-media-app-content { + grid-row: 2 / -1; + grid-column: 2 / -1; + } + .fl-asst-media-app-sidebar-backdrop { + display: block; + grid-column: 1 / 2; + grid-row: 1 / -1; + } + } +} +.fluid-color-scheme-dark { + .fl-asst-media-app-shell { + .fl-asst-media-app-sidebar-backdrop { + background: hsl( var(--fluid-hue), 10%, 12% ); + } + } } diff --git a/src/render/app/index.js b/src/render/app/index.js index 21f400060..134d1c32c 100644 --- a/src/render/app/index.js +++ b/src/render/app/index.js @@ -32,7 +32,7 @@ const AppMain = () => { } ), } - const displayContent = ! isAppHidden && ( ! isHidden || isBeaverBuilder ) + const displayContent = ! isAppHidden return (
diff --git a/src/system/utils/wordpress/hooks/index.js b/src/system/utils/wordpress/hooks/index.js new file mode 100644 index 000000000..58f8d82be --- /dev/null +++ b/src/system/utils/wordpress/hooks/index.js @@ -0,0 +1,48 @@ +import { useState, useEffect } from 'react' +import { getWpRest } from '../rest' +import { CancelToken, isCancel } from 'axios' + +const defaultQuery = { + posts_per_page: 36 +} + +export const useAttachments = ( _query = {} ) => { + const { getPagedContent } = getWpRest() + const source = CancelToken.source() + const query = { ...defaultQuery, ..._query } + const [ items, setItems ] = useState( [] ) + const [ isFetching, setIsFetching ] = useState( false ) + const [ hasMore, setHasMore ] = useState( false ) + + const loadItems = ( offset = 0 ) => { + setIsFetching( true ) + + return getPagedContent( 'attachments', query, offset, { + cancelToken: source.token, + } ).then( response => { + const { items: newItems, has_more } = response.data + setItems( [ ...items, ...newItems ] ) + setHasMore( has_more ) + setIsFetching( false ) + + } ).catch( error => { + if ( ! isCancel( error ) ) { + console.error( error ) // eslint-disable-line no-console + } + setIsFetching( false ) + } ) + } + + // Initial Load + useEffect( () => { + loadItems( items.length ) + return source.cancel + }, [] ) + + return { + items, + loadItems, + isFetching, + hasMore, + } +} diff --git a/src/system/utils/wordpress/index.js b/src/system/utils/wordpress/index.js index 1a5cd672c..bd0c52ff0 100644 --- a/src/system/utils/wordpress/index.js +++ b/src/system/utils/wordpress/index.js @@ -2,3 +2,4 @@ export * from './admin-ajax' export * from './heartbeat' export * from './user' export * from './rest' +export * from './hooks' From 856ad115869a2663e8d94973850999a211e6d45b Mon Sep 17 00:00:00 2001 From: Brent Jett Date: Tue, 23 Mar 2021 16:27:26 -0500 Subject: [PATCH 3/5] WIP --- packages/@beaverbuilder/app-core/package.json | 2 +- .../app-core/src/app/content.js | 6 +- src/apps/fl-home/ui/shell/style.scss | 12 +++ src/apps/fl-media/app.js | 35 ++++--- src/apps/fl-media/config/index.js | 1 + src/apps/fl-media/data/index.js | 66 +++++++++++-- src/apps/fl-media/data/use-scroller.js | 42 -------- src/apps/fl-media/index.js | 2 + src/apps/fl-media/loading/index.js | 13 +++ src/apps/fl-media/loading/style.scss | 5 + src/apps/fl-media/ui/file-list/index.js | 39 -------- src/apps/fl-media/ui/file-list/style.scss | 22 ----- src/apps/fl-media/ui/index.js | 5 +- src/apps/fl-media/ui/list/file-list/index.js | 25 +++++ src/apps/fl-media/ui/list/index.js | 88 ++++++++++++++--- src/apps/fl-media/ui/list/items/art/index.js | 25 +++++ src/apps/fl-media/ui/list/items/index.js | 77 +++++++++++++++ src/apps/fl-media/ui/list/style.scss | 27 ++++- src/apps/fl-media/ui/page-main/index.js | 13 --- src/apps/fl-media/ui/shell/index.js | 79 ++++++++++++++- src/apps/fl-media/ui/shell/style.scss | 24 ++--- src/apps/fl-media/ui/upload-card/index.js | 46 +++++---- src/apps/fl-media/ui/upload-card/style.scss | 6 -- src/render/app/index.js | 9 +- src/system/ui/icon/video.js | 5 +- src/system/ui/layout/index.js | 5 +- src/system/ui/layout/sidebar/index.js | 32 ++++++ src/system/ui/layout/sidebar/style.scss | 66 +++++++++++++ src/system/ui/media/use-media-uploads.js | 9 +- src/system/ui/pages/attachment/index.js | 8 +- src/system/ui/pages/detail/index.js | 4 +- src/system/ui/pages/detail/style.scss | 24 +---- src/system/utils/wordpress/hooks/index.js | 98 +++++++++++++++---- 33 files changed, 657 insertions(+), 263 deletions(-) delete mode 100644 src/apps/fl-media/data/use-scroller.js create mode 100644 src/apps/fl-media/loading/index.js create mode 100644 src/apps/fl-media/loading/style.scss delete mode 100644 src/apps/fl-media/ui/file-list/index.js delete mode 100644 src/apps/fl-media/ui/file-list/style.scss create mode 100644 src/apps/fl-media/ui/list/file-list/index.js create mode 100644 src/apps/fl-media/ui/list/items/art/index.js create mode 100644 src/apps/fl-media/ui/list/items/index.js delete mode 100644 src/apps/fl-media/ui/page-main/index.js create mode 100644 src/system/ui/layout/sidebar/index.js create mode 100644 src/system/ui/layout/sidebar/style.scss diff --git a/packages/@beaverbuilder/app-core/package.json b/packages/@beaverbuilder/app-core/package.json index 1b01ee13f..c5f799168 100644 --- a/packages/@beaverbuilder/app-core/package.json +++ b/packages/@beaverbuilder/app-core/package.json @@ -1,6 +1,6 @@ { "name": "@beaverbuilder/app-core", - "version": "0.2.2", + "version": "0.2.3", "description": "Utilities for managing an app registry.", "author": "The Beaver Builder Team", "license": "GPL-2.0+", diff --git a/packages/@beaverbuilder/app-core/src/app/content.js b/packages/@beaverbuilder/app-core/src/app/content.js index 5b6596b4e..618a09340 100644 --- a/packages/@beaverbuilder/app-core/src/app/content.js +++ b/packages/@beaverbuilder/app-core/src/app/content.js @@ -51,7 +51,7 @@ const Content = ( { } const CurrentApp = ( { - loading: LoadingScreen, + loading: defaultLoadingScreen, error: ErrorScreen = DefaultErrorScreen, apps, } ) => { @@ -72,7 +72,7 @@ const CurrentApp = ( { return null } - const { label = '', root } = apps[ handle ] + const { label = '', root, loading: appLoadingScreen } = apps[ handle ] const isAppRoot = 2 >= location.pathname.split( '/' ).length const context = { @@ -83,6 +83,8 @@ const CurrentApp = ( { isAppRoot, } + const LoadingScreen = appLoadingScreen ? appLoadingScreen : defaultLoadingScreen + return ( diff --git a/src/apps/fl-home/ui/shell/style.scss b/src/apps/fl-home/ui/shell/style.scss index c9f2dccea..a1f63a30e 100644 --- a/src/apps/fl-home/ui/shell/style.scss +++ b/src/apps/fl-home/ui/shell/style.scss @@ -80,6 +80,18 @@ .fluid-page.fl-asst-home-shell { .fl-asst-home-shell-sidebar { background: hsl( var(--fluid-hue), 10%, 12% ); + + .fl-asst-home-shell-sidebar-section { + ul { + li { + a, button { + &.is-selected { + background: var(--fluid-opaque-3); + } + } + } + } + } } } } diff --git a/src/apps/fl-media/app.js b/src/apps/fl-media/app.js index 1ebae4649..9af6a858a 100644 --- a/src/apps/fl-media/app.js +++ b/src/apps/fl-media/app.js @@ -1,26 +1,33 @@ import React from 'react' -//import { __ } from '@wordpress/i18n' -import { App, Page, List, Filter, Button, Icon, Media } from 'assistant/ui' -//import { useAppState, getAppActions } from 'assistant/data' -//import { defaultState } from './config' +import { AnimateSharedLayout } from 'framer-motion' +import { App, Page } from 'assistant/ui' +import { Shell, MediaList } from './ui' import { MediaAppProvider } from './data' -import { Main, UploadCard, FileList } from './ui' -//import AppIcon from './icon' export default props => { return ( - - + + + + ) } +const Main = () => { + return ( + + + + ) +} + /* const Main = ( { baseURL } ) => { const { listStyle, query, showUploader } = useAppState( 'fl-media' ) diff --git a/src/apps/fl-media/config/index.js b/src/apps/fl-media/config/index.js index c79d40d7e..4e6ecb24e 100644 --- a/src/apps/fl-media/config/index.js +++ b/src/apps/fl-media/config/index.js @@ -7,6 +7,7 @@ export const defaultState = { label: '0', }, showUploader: true, + lastViewed: null, } export const cache = [ 'listStyle', 'query', 'showUploader' ] diff --git a/src/apps/fl-media/data/index.js b/src/apps/fl-media/data/index.js index a4c8cacdd..9826b9fa8 100644 --- a/src/apps/fl-media/data/index.js +++ b/src/apps/fl-media/data/index.js @@ -1,5 +1,7 @@ import React, { useContext, createContext } from 'react' -import { useAppState } from 'assistant/data' +import { __ } from '@wordpress/i18n' +import { Icon, Media } from 'assistant/ui' +import { useAppState, getAppActions } from 'assistant/data' import { useAttachments } from 'assistant/utils/wordpress' export const MediaAppContext = createContext( {} ) @@ -7,13 +9,40 @@ export const MediaAppContext = createContext( {} ) export const useMediaApp = () => useContext( MediaAppContext ) export const MediaAppProvider = ( { children } ) => { - const { query } = useAppState( 'fl-media' ) - const { items, loadItems, isFetching, hasMore } = useAttachments( query ) + const { query, lastViewed, showUploader } = useAppState( 'fl-media' ) + const { setQuery, setLastViewed, setShowUploader } = getAppActions( 'fl-media' ) + const attachments = useAttachments( query ) + const { files, uploadFiles: _uploadFiles, current } = Media.useMediaUploads() + + console.log( 'use media app' ) + + const uploadFiles = files => { + setQuery( { ...query } ) + _uploadFiles( files, () => { + console.log( 'upload complete' ) + attachments.reloadItems() + } ) + } + const context = { - items, - loadItems, - isFetching, - hasMore, + + // All the attachment REST info + ...attachments, + + // Current query + query, + setQuery, + + // Info about file uploads and the uploader UI + uploads: files, + uploadFiles, + currentUpload: current, + showUploader, + setShowUploader, + + // ID of the last viewed attachment detail page + lastViewed, + setLastViewed, } return ( @@ -22,3 +51,26 @@ export const MediaAppProvider = ( { children } ) => { ) } + +export const attachmentTypes = { + all: { + label: __( 'Everything' ), + icon: Icon.Placeholder + }, + image: { + label: __( 'Photos' ), + icon: Icon.Placeholder + }, + video: { + label: __( 'Videos' ), + icon: Icon.Video + }, + audio: { + label: __( 'Audio' ), + icon: Icon.Audio + }, + document: { + label: __( 'Documents' ), + icon: Icon.Document + }, +} diff --git a/src/apps/fl-media/data/use-scroller.js b/src/apps/fl-media/data/use-scroller.js deleted file mode 100644 index 6f0a51553..000000000 --- a/src/apps/fl-media/data/use-scroller.js +++ /dev/null @@ -1,42 +0,0 @@ -import { useEffect, useRef } from 'react' - -const hasReachedBounds = e => { - const { scrollTop, clientHeight, scrollHeight } = e.target - const bottom = scrollTop + clientHeight - return bottom + 150 >= scrollHeight -} - -const defaultOptions = { - onScrollEnd: () => {}, - hasReachedBounds -} - -const useScroller = ( _options = {} ) => { - const { hasReachedBounds, onScrollEnd } = { ...defaultOptions, ..._options } - const hasHitBoundary = useRef( false ) - const ref = useRef( null ) - - useEffect( () => { - - if ( undefined === ref.current ) { - return - } - - const reset = () => hasHitBoundary.current = false - - const onScroll = e => { - if ( hasReachedBounds( e ) && ! hasHitBoundary.current ) { - hasHitBoundary.current = true - onScrollEnd( reset ) - } - } - - ref.current.addEventListener( 'scroll', onScroll ) - - return () => ref.current.removeEventListener( 'scroll', onScroll ) - }, [] ) - - return ref -} - -export default useScroller diff --git a/src/apps/fl-media/index.js b/src/apps/fl-media/index.js index b6e4da1b2..5c4d5c450 100644 --- a/src/apps/fl-media/index.js +++ b/src/apps/fl-media/index.js @@ -4,6 +4,7 @@ import { __ } from '@wordpress/i18n' import { addQueryArgs } from 'assistant/utils/url' import { Page } from 'assistant/ui' import Icon from './icon' +import LoadingScreen from './loading' import { defaultState, cache } from './config' const App = lazy( () => import( @@ -14,6 +15,7 @@ registerApp( 'fl-media', { label: __( 'Media' ), root: App, icon: Icon, + loading: LoadingScreen, state: { ...defaultState }, search: { label: __( 'Media' ), diff --git a/src/apps/fl-media/loading/index.js b/src/apps/fl-media/loading/index.js new file mode 100644 index 000000000..9a44a2387 --- /dev/null +++ b/src/apps/fl-media/loading/index.js @@ -0,0 +1,13 @@ +import React from 'react' +import { Layout } from 'assistant/ui' +import './style.scss' + +const LoadingScreen = () => { + return ( +
+ +
+ ) +} + +export default LoadingScreen diff --git a/src/apps/fl-media/loading/style.scss b/src/apps/fl-media/loading/style.scss new file mode 100644 index 000000000..2baf0cfec --- /dev/null +++ b/src/apps/fl-media/loading/style.scss @@ -0,0 +1,5 @@ +.fl-asst-media-loading-screen { + flex: 1 1 auto; + display: grid; + grid-template-columns: 220px 1fr; +} diff --git a/src/apps/fl-media/ui/file-list/index.js b/src/apps/fl-media/ui/file-list/index.js deleted file mode 100644 index 6980e1505..000000000 --- a/src/apps/fl-media/ui/file-list/index.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react' -import { Icon } from 'assistant/ui' -import './style.scss' - -const FileList = ( { files = [], current } ) => { - if ( current > files.length ) { - return null - } - return ( - <> -
    - { files.map( ( file, i ) => { - - // Remove items that have already been uploaded - if ( i + 1 < current ) { - return null - } - - return ( -
  • - - { file.name } - -
  • - ) - } ) } -
- - ) -} - -export default FileList diff --git a/src/apps/fl-media/ui/file-list/style.scss b/src/apps/fl-media/ui/file-list/style.scss deleted file mode 100644 index b0c5ad292..000000000 --- a/src/apps/fl-media/ui/file-list/style.scss +++ /dev/null @@ -1,22 +0,0 @@ -.fl-asst { - .fl-asst-file-list { - background: var(--fluid-transparent-12); - padding: var(--fluid-lg-space); - border-radius: var(--fluid-radius); - margin: var(--fluid-med-space) var(--fluid-med-space) 0; - - li { - min-height: 36px; - display: flex; - align-items: center; - padding: 5px 0; - font-size: 12px; - } - } -} -.fluid-color-scheme-dark { - .fl-asst-file-list { - background: var(--fluid-transparent-1); - color: var(--fluid-color); - } -} diff --git a/src/apps/fl-media/ui/index.js b/src/apps/fl-media/ui/index.js index 357d4e864..f27ecabf6 100644 --- a/src/apps/fl-media/ui/index.js +++ b/src/apps/fl-media/ui/index.js @@ -1,7 +1,6 @@ import UploadCard from './upload-card' -import FileList from './file-list' +import MediaList from './list' import Shell from './shell' -import Main from './page-main' import './style.scss' -export { UploadCard, FileList, Shell, Main } +export { UploadCard, Shell, MediaList } diff --git a/src/apps/fl-media/ui/list/file-list/index.js b/src/apps/fl-media/ui/list/file-list/index.js new file mode 100644 index 000000000..742dba5a2 --- /dev/null +++ b/src/apps/fl-media/ui/list/file-list/index.js @@ -0,0 +1,25 @@ +import React from 'react' +import { UploadingItem } from '../items' + +const FileList = ( { files = [], current } ) => { + if ( current > files.length ) { + return null + } + return files.map( ( file, i ) => { + + // Remove items that have already been uploaded + if ( i + 1 < current ) { + return null + } + + const src = URL.createObjectURL( file ) + + return ( +
  • + +
  • + ) + } ) +} + +export default FileList diff --git a/src/apps/fl-media/ui/list/index.js b/src/apps/fl-media/ui/list/index.js index d04a25b52..a1b0f2b5c 100644 --- a/src/apps/fl-media/ui/list/index.js +++ b/src/apps/fl-media/ui/list/index.js @@ -1,6 +1,10 @@ -import React from 'react' -import { Layout } from 'assistant/ui' +import React, { useLayoutEffect } from 'react' +import { __, sprintf } from '@wordpress/i18n' +import { Text, Media } from 'assistant/ui' import { useMediaApp } from '../../data' +import { AttachmentItem, PlaceholderItem } from './items' +import UploadCard from '../upload-card' +import FileList from './file-list' import './style.scss' const hasReachedBounds = e => { @@ -10,7 +14,23 @@ const hasReachedBounds = e => { } const MediaList = () => { - const { items, isFetching, loadItems, hasMore } = useMediaApp() + const { + items, + isFetching, + loadItems, + hasMore, + lastViewed, + setLastViewed, + uploads, + uploadFiles, + currentUpload, + showUploader, + setShowUploader, + } = useMediaApp() + const hasItems = 0 < items.length + + const placeholders = new Array( 18 ).fill( 18 ) + const displayPlaceholders = isFetching && ! hasItems const onScroll = e => { if ( hasMore && hasReachedBounds( e ) && ! isFetching ) { @@ -18,25 +38,71 @@ const MediaList = () => { } } + useLayoutEffect( () => { + + if ( null === lastViewed ) { + return + } + const el = document.getElementById( `attachment-${lastViewed}` ) + if ( el ) { + el.scrollIntoView( { block: 'center' } ) + setLastViewed( null ) + } + }, [] ) + return ( -
    +
    +
      + + { showUploader && ( +
    • + setShowUploader( false ) } + /> +
    • + ) } + + { 0 < uploads.length && ( + + ) } + + { displayPlaceholders && placeholders.map( ( _, i ) => ( +
    • + +
    • + ) ) } + { items.map( item => { - const { id, thumbnail } = item return ( -
    • - + setLastViewed( item.id ) } + { ...item } />
    • ) } ) } +
    +
    ) } +const Endcap = ( { isFetching = false, totalItems = 0 } ) => { + return ( +
  • + { isFetching && { __( 'Loading...', 'fl-assistant' ) } } + { ! isFetching && ( + { sprintf( '%s Items', totalItems ) } + ) } +
  • + ) +} + export default MediaList diff --git a/src/apps/fl-media/ui/list/items/art/index.js b/src/apps/fl-media/ui/list/items/art/index.js new file mode 100644 index 000000000..d3a0bff8d --- /dev/null +++ b/src/apps/fl-media/ui/list/items/art/index.js @@ -0,0 +1,25 @@ +import React from 'react' + +const Image = () => {} + +Image.Video = () => ( + + + + +) + +Image.Doc = () => ( + + + +) + +Image.Audio = () => ( + + + + +) + +export default Image diff --git a/src/apps/fl-media/ui/list/items/index.js b/src/apps/fl-media/ui/list/items/index.js new file mode 100644 index 000000000..ddf975e07 --- /dev/null +++ b/src/apps/fl-media/ui/list/items/index.js @@ -0,0 +1,77 @@ +import React from 'react' +import { Link } from 'react-router-dom' +import { motion } from 'framer-motion' +import { Layout } from 'assistant/ui' +import { getSrcSet } from 'assistant/utils/image' +import Image from './art' + +const filterSizes = sizes => { + const smallSizes = {} + const allow = [ 'thumbnail', 'medium' ] + for ( let key in sizes ) { + if ( allow.includes( key ) ) { + smallSizes[key] = sizes[key] + } + } + return smallSizes +} + +export const AttachmentItem = props => { + const { + thumbnail, + type, + subtype, + alt, + title, + sizes, + id, + isTrashed, + onClick = () => {} + } = props + const isDocument = ( 'application' === type || ( 'pdf' === subtype && ! thumbnail ) ) + + // Filter down to just the smaller sizes for srcset + const smallSizes = filterSizes( sizes ) + const to = isTrashed ? '' : { + pathname: `/fl-media/attachment/${id}`, + state: { item: props } + } + + return ( + + + { ( 'image' === type || 'pdf' === subtype ) && thumbnail && ( + { + ) } + { 'video' === type && } + { 'audio' === type && } + { isDocument && } + + + ) +} + +export const PlaceholderItem = () => { + return ( + + ) +} + +export const UploadingItem = ( { ...rest } ) => { + return ( + + +
    + +
    +
    + ) +} diff --git a/src/apps/fl-media/ui/list/style.scss b/src/apps/fl-media/ui/list/style.scss index 99cd7a310..9d196134a 100644 --- a/src/apps/fl-media/ui/list/style.scss +++ b/src/apps/fl-media/ui/list/style.scss @@ -7,12 +7,37 @@ ul.fl-asst-media-app-list { display: grid; grid-gap: var(--fluid-lg-space); - grid-template-columns: repeat( auto-fill, minmax( 150px, 1fr ) ); + grid-template-columns: repeat( auto-fill, minmax( 135px, 1fr ) ); padding: var(--fluid-lg-space); + padding-top: 0; margin: 0; + + li { + a, button { + display: block; + } + .fluid-aspect-box { + border-radius: var(--fluid-radius); + z-index: 2; + filter: brightness( 98% ); + } + } + + .fl-asst-media-app-list-endcap { + grid-column: 1 / -1; + min-height: 80px; + padding: 30px; + display: flex; + align-items: center; + justify-content: center; + } } .fluid-frame-size-medium { ul.fl-asst-media-app-list { + max-width: 688px; + margin: 0 auto; padding: 30px; + padding-top: 0; + grid-gap: 30px; } } diff --git a/src/apps/fl-media/ui/page-main/index.js b/src/apps/fl-media/ui/page-main/index.js deleted file mode 100644 index c5f79018b..000000000 --- a/src/apps/fl-media/ui/page-main/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react' -import Shell from '../shell' -import MediaList from '../list' - -const Main = ( { baseURL } ) => { - return ( - - - - ) -} - -export default Main diff --git a/src/apps/fl-media/ui/shell/index.js b/src/apps/fl-media/ui/shell/index.js index 36b2361c3..9d24fb2e1 100644 --- a/src/apps/fl-media/ui/shell/index.js +++ b/src/apps/fl-media/ui/shell/index.js @@ -1,16 +1,24 @@ import React from 'react' import { __ } from '@wordpress/i18n' import c from 'classnames' +import { Layout, Button, Icon } from 'assistant/ui' +import { getSystemHooks } from 'assistant/data' +import { attachmentTypes, useMediaApp } from '../../data' import MediaIcon from '../../icon' import './style.scss' const MediaShell = ( { children, className, ...rest } ) => { + const { showUploader, setShowUploader } = useMediaApp() return (
    -
    + + + + +
    @@ -18,11 +26,78 @@ const MediaShell = ( { children, className, ...rest } ) => { { __( 'Media', 'fl-assistant' ) }
    -
    +
    +
    {children}
    ) } +const AttachmentTypesSection = () => { + const { query, setQuery } = useMediaApp() + const setType = value => setQuery( { ...query, post_mime_type: value, label: '' } ) + + return ( + +
      + { Object.keys( attachmentTypes ).map( key => { + const { label, icon: TypeIcon } = attachmentTypes[ key ] + + return ( +
    • + +
    • + ) + } ) } +
    +
    + ) +} + +const LabelsSection = () => { + const { query, setQuery } = useMediaApp() + const { useLabels } = getSystemHooks() + const [ labels ] = useLabels() + + const LabelDot = ( { color } ) => ( +
    + ) + + return ( + +
      + { labels.map( item => { + const { id, label, color } = item + return ( +
    • + +
    • + ) + } ) } +
    +
    + ) +} + export default MediaShell diff --git a/src/apps/fl-media/ui/shell/style.scss b/src/apps/fl-media/ui/shell/style.scss index 4c39cfecf..936b145bf 100644 --- a/src/apps/fl-media/ui/shell/style.scss +++ b/src/apps/fl-media/ui/shell/style.scss @@ -18,6 +18,9 @@ z-index: 1; padding: 20px; } + .fl-asst-primary-toolbar-area { + padding-left: 25px; // Logo offset + } .fl-asst-secondary-toolbar-area { justify-content: flex-end; } @@ -34,22 +37,19 @@ .fl-asst-media-app-content { grid-row: 2 / -1; grid-column: 1 / -1; + display: flex; + flex-direction: column; /* Scroller stuff */ min-height: 0; max-height: 100%; overflow: auto; } - - .fl-asst-media-app-sidebar-backdrop { - display: none; - background: hsl( var(--fluid-hue), 33%, 97% ); /* Slightly different from --fluid-opaque-13 */ - } } .fluid-frame-size-medium { .fl-asst-media-app-shell { - grid-template-columns: minmax( 120px, 200px ) 1fr; + grid-template-columns: minmax( 120px, 220px ) 1fr; .fl-asst-toolbar { display: contents; /* promote children to grid items */ @@ -68,17 +68,5 @@ grid-row: 2 / -1; grid-column: 2 / -1; } - .fl-asst-media-app-sidebar-backdrop { - display: block; - grid-column: 1 / 2; - grid-row: 1 / -1; - } - } -} -.fluid-color-scheme-dark { - .fl-asst-media-app-shell { - .fl-asst-media-app-sidebar-backdrop { - background: hsl( var(--fluid-hue), 10%, 12% ); - } } } diff --git a/src/apps/fl-media/ui/upload-card/index.js b/src/apps/fl-media/ui/upload-card/index.js index 224b885f8..113a238a9 100644 --- a/src/apps/fl-media/ui/upload-card/index.js +++ b/src/apps/fl-media/ui/upload-card/index.js @@ -9,30 +9,28 @@ const UploadCard = ( { ...rest } ) => { return ( -
    -
    - -
    +
    + +
    ) } diff --git a/src/apps/fl-media/ui/upload-card/style.scss b/src/apps/fl-media/ui/upload-card/style.scss index e392471c3..e95178f69 100644 --- a/src/apps/fl-media/ui/upload-card/style.scss +++ b/src/apps/fl-media/ui/upload-card/style.scss @@ -1,9 +1,4 @@ .fl-asst { - .fl-asst-upload-card-wrapper { - max-width: 550px; - margin: 0 auto; - padding-top: 20px; - } .fl-asst-upload-card { --space-around: clamp( 10px, 4%, 40px ); @@ -13,7 +8,6 @@ background: var(--fluid-transparent-12); padding: 30px 20px 20px; border-radius: var(--fluid-radius); - margin: 0 var(--space-around); .close-action { position: absolute; diff --git a/src/render/app/index.js b/src/render/app/index.js index 134d1c32c..99cf716a3 100644 --- a/src/render/app/index.js +++ b/src/render/app/index.js @@ -9,11 +9,10 @@ import './style.scss' const AppMain = () => { const { apps, window: windowFrame, isAppHidden, homeKey } = useSystemState( [ 'apps', 'window', 'isAppHidden', 'homeKey' ] ) - const { origin, isHidden } = windowFrame + const { origin } = windowFrame const sideName = origin[0] ? 'right' : 'left' - const { isMobile, application } = Env.use() + const { isMobile } = Env.use() const rowDirection = 'right' === sideName ? 'row-reverse' : 'row' - const isBeaverBuilder = 'beaver-builder' === application const classes = classname( { 'fl-asst-main': true, @@ -37,7 +36,7 @@ const AppMain = () => { return (
    - + <> { displayContent && ( { /> ) } - +
    ) } diff --git a/src/system/ui/icon/video.js b/src/system/ui/icon/video.js index 64d758648..9ba5d6cde 100644 --- a/src/system/ui/icon/video.js +++ b/src/system/ui/icon/video.js @@ -1,8 +1,9 @@ import React from 'react' const Video = () => ( - - + + + ) diff --git a/src/system/ui/layout/index.js b/src/system/ui/layout/index.js index 552bc244b..7075671be 100644 --- a/src/system/ui/layout/index.js +++ b/src/system/ui/layout/index.js @@ -3,6 +3,7 @@ import Attention from './attention' import PublishBar from './publish-bar' import Table from './table' import { Tabs, TabsToolbar, CurrentTab } from './nav' +import { SidebarBackdrop, SidebarSection } from './sidebar' const Layout = { ...FLUID_Layout, @@ -11,7 +12,9 @@ const Layout = { Table, Tabs, TabsToolbar, - CurrentTab + CurrentTab, + SidebarBackdrop, + SidebarSection, } export default Layout diff --git a/src/system/ui/layout/sidebar/index.js b/src/system/ui/layout/sidebar/index.js new file mode 100644 index 000000000..fb520c75b --- /dev/null +++ b/src/system/ui/layout/sidebar/index.js @@ -0,0 +1,32 @@ +import React from 'react' +import c from 'classnames' +import './style.scss' + +export const SidebarBackdrop = ( { className, ...rest } ) => ( +
    +) + +export const SidebarSection = ( { + title, + className, + children, + ...rest +} ) => { + + return ( +
    + { title && ( +
    + {title} +
    + ) } +
    {children}
    +
    + ) +} diff --git a/src/system/ui/layout/sidebar/style.scss b/src/system/ui/layout/sidebar/style.scss new file mode 100644 index 000000000..715918cbc --- /dev/null +++ b/src/system/ui/layout/sidebar/style.scss @@ -0,0 +1,66 @@ +.fl-asst { + .fl-asst-sidebar-backdrop { + display: none; + background: hsl( var(--fluid-hue), 33%, 97% ); /* Slightly different from --fluid-opaque-13 */ + padding-top: 80px; /* usually under toolbar */ + } + .fluid-frame-size-medium { + .fl-asst-sidebar-backdrop { + display: block; + grid-column: 1 / 2; + grid-row: 1 / -1; + } + } + .fluid-color-scheme-dark { + .fl-asst-sidebar-backdrop { + background: hsl( var(--fluid-hue), 10%, 12% ); + } + } + + + .fl-asst-sidebar-section { + padding: 15px; + + .fl-asst-sidebar-section-title { + padding: 5px var(--fluid-med-space); + } + ul { + margin: 0; + + li { + display: flex; + flex-direction: row; + + .fluid-button { + flex: 1 1 auto; + justify-content: flex-start; + background: transparent; + + .fluid-button-icon { + color: var(--fluid-accent); + margin-right: 10px; + } + + &.is-selected, + &:focus.is-selected { + background: var(--fluid-opaque-11) !important; + color: var(--fluid-blue); + } + } + } + } + } + .fluid-color-scheme-dark { + .fl-asst-sidebar-section ul li { + .fluid-button { + color: var(--fluid-opaque-8); + + &.is-selected, + &:focus.is-selected { + color: var(--fluid-opaque-14); + background: var(--fluid-opaque-3) !important; + } + } + } + } +} diff --git a/src/system/ui/media/use-media-uploads.js b/src/system/ui/media/use-media-uploads.js index c78d49611..1cbc59461 100644 --- a/src/system/ui/media/use-media-uploads.js +++ b/src/system/ui/media/use-media-uploads.js @@ -10,15 +10,15 @@ const useMediaUploads = () => { const { current, items } = useStore( key ) const { setCurrent, setItems } = getDispatch( key ) - const uploadFiles = files => { + const uploadFiles = ( files, callback = () => {} ) => { setItems( [ ...items, ...files ] ) if ( ! current ) { - uploadNext() + uploadNext( callback ) } } - const uploadNext = () => { + const uploadNext = callback => { const { current, items } = getStore( key ).getState() const file = items[current] const data = new FormData() @@ -26,6 +26,7 @@ const useMediaUploads = () => { if ( ! file ) { setItems( [] ) setCurrent( 0 ) + callback() return } @@ -38,7 +39,7 @@ const useMediaUploads = () => { } ) .finally( () => { setCurrent( current + 1 ) - uploadNext() + uploadNext( callback ) } ) } diff --git a/src/system/ui/pages/attachment/index.js b/src/system/ui/pages/attachment/index.js index d11af2111..3f228d24d 100644 --- a/src/system/ui/pages/attachment/index.js +++ b/src/system/ui/pages/attachment/index.js @@ -1,6 +1,7 @@ import React, { memo } from 'react' import { __ } from '@wordpress/i18n' import { useHistory } from 'react-router-dom' +import { motion } from 'framer-motion' import { Page, Form, Layout, Notice, Button } from 'ui' import { getSrcSet } from 'utils/image' import { getWpRest } from 'utils/wordpress' @@ -13,7 +14,7 @@ export const Attachment = () => { const wpRest = getWpRest() const { setCurrentHistoryState } = getSystemActions() const { createNotice } = Notice.useNotices() - const { id, title, type, subtype } = item + const { id, type, subtype } = item const onSubmit = ( { changed, ids } ) => { const data = { @@ -231,7 +232,7 @@ export const Attachment = () => { ) } -const Hero = memo( ( { width, sizes, height, alt, type, url, mime, thumbnail } ) => { +const Hero = memo( ( { width, sizes, height, alt, type, url, mime, thumbnail, id } ) => { const srcSet = getSrcSet( sizes ) // Temp - Handle non-image heroes. @@ -249,9 +250,12 @@ const Hero = memo( ( { width, sizes, height, alt, type, url, mime, thumbnail } ) return ( {mediaContent} diff --git a/src/system/ui/pages/detail/index.js b/src/system/ui/pages/detail/index.js index 36a79175a..fdada1004 100644 --- a/src/system/ui/pages/detail/index.js +++ b/src/system/ui/pages/detail/index.js @@ -2,7 +2,7 @@ import React from 'react' import c from 'classnames' import { useHistory } from 'react-router-dom' import { motion } from 'framer-motion' -import { Button, Icon, Text, Frame } from 'ui' +import { Button, Icon, Text, Frame, Layout } from 'ui' import './style.scss' const DetailPage = ( { @@ -22,7 +22,7 @@ const DetailPage = ( { const classes = c( 'fluid-page', 'fluid-detail-page', className ) return ( -
    +