diff --git a/common/constants.js b/common/constants.js index 7ba22b1e..92802658 100644 --- a/common/constants.js +++ b/common/constants.js @@ -4,15 +4,16 @@ export const values = { }; export const sizes = { - mobile: 768, - navigation: 288, - sidebar: 416, - // NOTE(amine): header's height + filter navbar's height header: 52, + navigation: 288, + intercomWidget: 60, + sidebar: 416, filterNavbar: 40, + topOffset: 0, //NOTE(martina): Pushes UI down. 16 when there is a persistent announcement banner, 0 otherwise + + mobile: 768, tablet: 960, desktop: 1024, - topOffset: 0, //NOTE(martina): Pushes UI down. 16 when there is a persistent announcement banner, 0 otherwise }; export const system = { diff --git a/common/filter-utilities.js b/common/filter-utilities.js deleted file mode 100644 index c4119ef3..00000000 --- a/common/filter-utilities.js +++ /dev/null @@ -1,86 +0,0 @@ -import { - isImageType, - isVideoType, - isAudioType, - isDocument, - isTwitterLink, - isYoutubeLink, - isTwitchLink, - isGithubLink, - isInstagramLink, -} from "~/common/validations"; - -export const FILTER_VIEWS_IDS = { - initial: "initial", - browser: "browser", -}; - -export const FILTER_SUBVIEWS_IDS = { - browser: { saved: "saved" }, -}; - -export const FILTER_TYPES = { - [FILTER_VIEWS_IDS.initial]: { - filters: { - initial: "library", - library: "library", - images: "images", - videos: "videos", - audios: "audios", - documents: "documents", - }, - }, - [FILTER_VIEWS_IDS.browser]: { - filters: { all: "all", initial: "all" }, - subviews: { - [FILTER_SUBVIEWS_IDS.browser.saved]: { - filters: { - initial: "all", - all: "all", - twitter: "twitter", - youtube: "youtube", - twitch: "twitch", - github: "github", - instagram: "instagram", - }, - }, - }, - }, -}; - -const FILTERING_HANDLERS = { - [FILTER_VIEWS_IDS.initial]: { - filters: { - library: (object) => object, - images: (object) => isImageType(object.type), - videos: (object) => isVideoType(object.type), - audios: (object) => isAudioType(object.type), - documents: (object) => isDocument(object.filename, object.type), - }, - }, - [FILTER_VIEWS_IDS.browser]: { - filters: { all: (object) => object.isLink }, - subviews: { - [FILTER_SUBVIEWS_IDS.browser.saved]: { - filters: { - all: (object) => object.isLink, - twitter: isTwitterLink, - youtube: isYoutubeLink, - twitch: isTwitchLink, - github: isGithubLink, - instagram: isInstagramLink, - }, - }, - }, - }, -}; - -export const getViewData = (view) => { - return FILTER_TYPES[view]; -}; - -export const getFilterHandler = ({ view, subview, type }) => { - const nextView = FILTERING_HANDLERS[view]; - if (subview) return nextView.subviews[subview].filters[type]; - return nextView.filters[type]; -}; diff --git a/common/hooks.js b/common/hooks.js index 1f89da6a..27958978 100644 --- a/common/hooks.js +++ b/common/hooks.js @@ -403,33 +403,6 @@ export const useLockScroll = ({ lock = true } = { lock: true }) => { }, [lock]); }; -export const useWorker = ({ onStart, onMessage, onError } = {}, dependencies = []) => { - const workerRef = React.useRef(); - - const onStartRef = React.useRef(); - onStartRef.current = onStart; - - const onMessageRef = React.useRef(); - onMessageRef.current = onMessage; - - const onErrorRef = React.useRef(); - onErrorRef.current = onError; - - React.useEffect(() => { - const worker = new Worker(new URL("../workers/filter-files.js", import.meta.url)); - if (!worker) return; - - workerRef.current = worker; - worker.onmessage = onMessageRef.current; - worker.onerror = onErrorRef.current; - - onStartRef.current(worker); - return () => worker?.terminate(); - }, dependencies); - - return workerRef.current; -}; - export const useHover = () => { const [isHovered, setHoverState] = React.useState(false); @@ -486,3 +459,9 @@ export const useDetectTextOverflow = ({ ref }, dependencies) => { return isTextOverflowing; }; + +let cache = {}; +export const useCache = () => { + const setCache = ({ key, value }) => (cache[key] = value); + return [cache, setCache]; +}; diff --git a/components/core/Application.js b/components/core/Application.js index 7385baca..0b3a02f3 100644 --- a/components/core/Application.js +++ b/components/core/Application.js @@ -46,6 +46,7 @@ import ApplicationLayout from "~/components/core/ApplicationLayout"; import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper"; import PortalsProvider from "~/components/core/PortalsProvider"; import CTATransition from "~/components/core/CTATransition"; +import Filter from "~/components/core/Filter"; import { GlobalModal } from "~/components/system/components/GlobalModal"; import { OnboardingModal } from "~/components/core/OnboardingModal"; @@ -347,6 +348,7 @@ export default class ApplicationPage extends React.Component { let body = document.documentElement || document.body; if (page.id === "NAV_SLATE" || page.id === "NAV_PROFILE") { state.loading = true; + state.data = { id: details.id }; } this.setState(state, () => { if (!popstate) { @@ -494,19 +496,28 @@ export default class ApplicationPage extends React.Component { isMac={this.props.isMac} viewer={this.state.viewer} > - {this.state.loading ? ( -
- -
- ) : ( - scene - )} + + {this.state.loading ? ( +
+ +
+ ) : ( + scene + )} +
diff --git a/components/core/ApplicationHeader.js b/components/core/ApplicationHeader.js index 8ce18ec3..d3534f03 100644 --- a/components/core/ApplicationHeader.js +++ b/components/core/ApplicationHeader.js @@ -1,11 +1,9 @@ import * as React from "react"; import * as Constants from "~/common/constants"; import * as SVG from "~/common/svg"; -import * as Events from "~/common/custom-events"; import * as Styles from "~/common/styles"; import * as Upload from "~/components/core/Upload"; -import * as Filter from "~/components/core/Filter"; -import * as Actions from "~/common/actions"; +import * as Search from "~/components/core/Search"; import { ApplicationUserControls, @@ -18,30 +16,10 @@ import { Link } from "~/components/core/Link"; import { ButtonPrimary, ButtonTertiary } from "~/components/system/components/Buttons"; import { Match, Switch } from "~/components/utility/Switch"; import { Show } from "~/components/utility/Show"; -import { useField, useMediaQuery } from "~/common/hooks"; -import { Input } from "~/components/system/components/Input"; +import { useMediaQuery } from "~/common/hooks"; import { AnimatePresence, motion } from "framer-motion"; - -const STYLES_SEARCH_COMPONENT = (theme) => css` - background-color: transparent; - box-shadow: none; - height: 100%; - input { - height: 100%; - padding: 0px 4px; - border-radius: 0px; - } - input::placeholder { - color: ${theme.semantic.textGray}; - font-size: ${theme.typescale.lvl1}; - } -`; - -const STYLES_DISMISS_BUTTON = (theme) => css` - display: block; - ${Styles.BUTTON_RESET}; - color: ${theme.semantic.textGray}; -`; +import { Navbar as FilterNavbar } from "~/components/core/Filter/Navbar"; +import { useSearchStore } from "~/components/core/Search/store"; const STYLES_APPLICATION_HEADER_BACKGROUND = (theme) => css` position: absolute; @@ -51,7 +29,6 @@ const STYLES_APPLICATION_HEADER_BACKGROUND = (theme) => css` left: 0; z-index: -1; background-color: ${theme.system.white}; - box-shadow: 0 0 0 1px ${theme.semantic.bgGrayLight}; @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { -webkit-backdrop-filter: blur(75px); backdrop-filter: blur(75px); @@ -105,9 +82,10 @@ const STYLES_BACKGROUND = css` animation: fade-in 200ms ease-out; `; -const STYLES_HEADER = css` - z-index: ${Constants.zindex.header}; +const STYLES_HEADER = (theme) => css` + z-index: ${theme.zindex.header}; width: 100vw; + height: ${theme.sizes.header}px; position: fixed; right: 0; top: 0; @@ -118,7 +96,7 @@ const STYLES_FILTER_NAVBAR = (theme) => css` width: 100vw; position: fixed; right: 0; - top: ${theme.sizes.header}; + top: ${theme.sizes.header}px; `; const STYLES_UPLOAD_BUTTON = css` @@ -142,7 +120,6 @@ export default function ApplicationHeader({ viewer, page, data, onAction }) { showDropdown: false, popup: null, isRefreshing: false, - query: "", }); const _handleTogglePopup = (value) => { @@ -153,38 +130,10 @@ export default function ApplicationHeader({ viewer, page, data, onAction }) { } }; - const _handleInputChange = (e) => { - setState((prev) => ({ ...prev, query: e.target.value })); - }; - - //TODO(amine): plug in the right values - //for more context, look at node_common/managers/search/search.js - //userId: (optional) the id of the user whose stuff we are searching through. If specified, globalSearch is disregarded since the search will be limited to that user's public items. Does not apply when searching for type USER - //types: leaving it null searches everything. Doing ["SLATE"] searches just slates, doing ["USER", "FILE"] searches users and files. - //globalSearch: whether you are just searching the user's files/slates or global files/slates. This option doesn't exist for searching users since there is no notion of public or private users - //tagIds: only applies when searching files. the ids of the tags (aka collections) you are searching within. aka if you only want to search for files in a given slate, provide that slate's id. If no tag ids are provided, it searches all files - //grouped: whether to group the results by type (slate, user, file) when searching multiple types. Doesn't apply when searching only one type e.g. types: ["SLATE"] - const handleSearch = async () => { - const response = await Actions.search({ - types: null, - query: state.query, - globalSearch: true, - tagIds: null, - grouped: true, - }); - console.log(response); - }; - - const { getFieldProps, value: searchQuery, setFieldValue } = useField({ - initialValue: "", - onSubmit: handleSearch, - }); - - const handleDismissSearch = () => setFieldValue(""); + const { isSearching } = useSearchStore(); const { mobile } = useMediaQuery(); const isSignedOut = !viewer; - const isSearching = searchQuery.length !== 0; return ( <> @@ -210,17 +159,7 @@ export default function ApplicationHeader({ viewer, page, data, onAction }) {
{/**TODO: update Search component */} - +
@@ -238,7 +177,6 @@ export default function ApplicationHeader({ viewer, page, data, onAction }) { isSearching={isSearching} isSignedOut={isSignedOut} onAction={onAction} - onDismissSearch={handleDismissSearch} /> @@ -261,14 +199,14 @@ export default function ApplicationHeader({ viewer, page, data, onAction }) {
- +
); } -const UserActions = ({ uploadAction, isSignedOut, isSearching, onAction, onDismissSearch }) => { +const UserActions = ({ uploadAction, isSignedOut, isSearching, onAction }) => { const authActions = React.useMemo( () => ( <> @@ -316,13 +254,7 @@ const UserActions = ({ uploadAction, isSignedOut, isSearching, onAction, onDismi animate={{ opacity: 1, y: 0 }} exit={{ y: -10, opacity: 0 }} > - + diff --git a/components/core/CollectionPreviewBlock/index.js b/components/core/CollectionPreviewBlock/index.js index c7985cbe..6b495765 100644 --- a/components/core/CollectionPreviewBlock/index.js +++ b/components/core/CollectionPreviewBlock/index.js @@ -118,9 +118,10 @@ export default function CollectionPreview({ collection, viewer, owner, onAction const title = collection.name || collection.slatename; const isOwner = viewer?.id === collection.ownerId; - const preview = React.useMemo(() => getObjectToPreview(collection.coverImage), [ - collection.coverImage, - ]); + const preview = React.useMemo( + () => getObjectToPreview(collection.coverImage), + [collection.coverImage] + ); return (
@@ -228,7 +229,7 @@ function Metrics({ fileCount, owner, isOwner, onAction }) {
- {!isOwner && ( + {isOwner && ( <> css` - ${Styles.BUTTON_RESET}; - :hover { - color: ${theme.semantic.textBlack}; - } -`; - -function Item({ children, color, includeDelimiter, ...props }) { - return ( - <> - {includeDelimiter && ( - - {" "} - /{" "} - - )} - - - ); -} - -export function Breadcrumb(props) { - const [{ filterState }, { setFilterType, resetFilterState }] = useFilterContext(); - const isCurrentViewInitial = filterState.view === FILTER_VIEWS_IDS.initial; - - const changeFilterToBrowerView = () => - setFilterType({ - view: FILTER_VIEWS_IDS.browser, - type: FILTER_TYPES[FILTER_VIEWS_IDS.browser].filters.initial, - }); - - return ( -
- - - All - - - {Strings.capitalize(filterState.view)} - - - - {Strings.capitalize(filterState.subview)} - -
- ); -} diff --git a/components/core/Filter/Content.js b/components/core/Filter/Content.js deleted file mode 100644 index 965da507..00000000 --- a/components/core/Filter/Content.js +++ /dev/null @@ -1,43 +0,0 @@ -import * as React from "react"; - -import { FileTypeGroup } from "~/components/core/FileTypeIcon"; -import { css } from "@emotion/react"; - -import DataView from "~/components/core/DataView"; -import EmptyState from "~/components/core/EmptyState"; -import { useFilterContext } from "~/components/core/Filter/Provider"; - -const STYLES_DATAVIEWER_WRAPPER = (theme) => css` - width: 100%; - min-height: 100vh; - padding: calc(20px + ${theme.sizes.filterNavbar}px) 24px 44px; - @media (max-width: ${theme.sizes.mobile}px) { - padding: 31px 16px 44px; - } -`; - -export function Content({ viewer, onAction, page, ...props }) { - const [{ filterState }] = useFilterContext(); - const { objects } = filterState; - - return ( -
- {objects.length ? ( - - ) : ( - - -
Drag and drop files into Slate to upload
-
- )} -
- ); -} diff --git a/components/core/Filter/Filters.js b/components/core/Filter/Filters.js index da8d28dc..c6ab5e9c 100644 --- a/components/core/Filter/Filters.js +++ b/components/core/Filter/Filters.js @@ -5,6 +5,7 @@ import * as Typography from "~/components/system/components/Typography"; import { css } from "@emotion/react"; import { useFilterContext } from "~/components/core/Filter/Provider"; +import { Link } from "~/components/core/Link"; /* ------------------------------------------------------------------------------------------------- * Shared components between filters @@ -19,7 +20,7 @@ const STYLES_FILTER_BUTTON = (theme) => css` align-items: center; width: 100%; ${Styles.BUTTON_RESET}; - padding: 4px 8px; + padding: 5px 8px 3px; border-radius: 8px; color: ${theme.semantic.textBlack}; &:hover { @@ -45,21 +46,21 @@ const STYLES_FILTERS_GROUP = css` const FilterButton = ({ children, Icon, isSelected, ...props }) => (
  • - - - {children} - + + + + + {children} + + +
  • ); const FilterSection = ({ title, children, ...props }) => (
    {title && ( - + {title} )} @@ -71,143 +72,47 @@ const FilterSection = ({ title, children, ...props }) => ( * InitialFilters * -----------------------------------------------------------------------------------------------*/ -function Initial({ filters, goToBrowserView }) { - const [{ filterState }, { setFilterType, resetFilterState }] = useFilterContext(); - const currentFilterType = filterState.type; - const currentFilterView = filterState.view; +function Library({ page, onAction }) { + const [, { hidePopup }] = useFilterContext(); - const changeFilter = ({ type }) => setFilterType({ view: currentFilterView, type }); + const isSelected = page.id === "NAV_DATA"; return ( <> - {/** Breadcrumb All */} My Library - - - Browser - - - - changeFilter({ type: filters.images })} - > - Images - - changeFilter({ type: filters.audios })} - > - Audios - - changeFilter({ type: filters.videos })} - > - Videos - - changeFilter({ type: filters.documents })} - > - Documents - - ); } -/* ------------------------------------------------------------------------------------------------- - * Browser Filters - * -----------------------------------------------------------------------------------------------*/ - -function Browser({ filters, goToSavedSubview }) { - const [{ filterState }] = useFilterContext(); - const currentFilterType = filterState.type; +function Tags({ viewer, data, onAction, ...props }) { + const [, { hidePopup }] = useFilterContext(); return ( - - - All - - - History - - - Bookmarks - - - Saved - + + {viewer.slates.map((slate) => ( + + {slate.slatename} + + ))} ); } -const BrowserSaved = ({ filters }) => { - const [{ filterState }, { setFilterType }] = useFilterContext(); - const currentFilterType = filterState.type; - - const changeSavedFilterType = (type) => - setFilterType({ view: filterState.view, subview: filterState.subview, type }); - - return ( - - changeSavedFilterType(filters.all)} - > - All - - changeSavedFilterType(filters.twitter)} - > - Twitter - - changeSavedFilterType(filters.youtube)} - > - Youtube - - changeSavedFilterType(filters.github)} - > - Github - - changeSavedFilterType(filters.twitch)} - > - Twitch - - changeSavedFilterType(filters.instagram)} - > - Instagram - - - ); -}; - -export { Initial, Browser, BrowserSaved }; +export { Library, Tags }; diff --git a/components/core/Filter/Navbar.js b/components/core/Filter/Navbar.js index 0625a095..7a182917 100644 --- a/components/core/Filter/Navbar.js +++ b/components/core/Filter/Navbar.js @@ -1,5 +1,6 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; +import * as Styles from "~/common/styles"; import { usePortals } from "~/components/core/PortalsProvider"; import { css } from "@emotion/react"; @@ -16,8 +17,9 @@ export function NavbarPortal({ children }) { return filterNavbarElement ? ReactDOM.createPortal( <> - +
    {children}
    + , filterNavbarElement ) @@ -29,12 +31,27 @@ export function NavbarPortal({ children }) { * -----------------------------------------------------------------------------------------------*/ const STYLES_NAVBAR = (theme) => css` + position: relative; display: flex; justify-content: space-between; align-items: center; - background-color: ${theme.semantic.bgWhite}; padding: 9px 24px 11px; box-shadow: ${theme.shadow.lightSmall}; + + @media (max-width: ${theme.sizes.mobile}px) { + padding: 9px 16px 11px; + } +`; + +const STYLES_NAVBAR_BACKGROUND = (theme) => css` + position: absolute; + width: 100%; + height: 100%; + top: 0px; + left: 0px; + z-index: -1; + + background-color: ${theme.semantic.bgWhite}; @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { -webkit-backdrop-filter: blur(75px); backdrop-filter: blur(75px); @@ -49,5 +66,10 @@ const STYLES_NAVBAR = (theme) => css` export function Navbar({ children }) { const { filterNavbar } = usePortals(); const [, setFilterElement] = filterNavbar; - return
    {children}
    ; + return ( +
    + {children} +
    +
    + ); } diff --git a/components/core/Filter/Popup.js b/components/core/Filter/Popup.js new file mode 100644 index 00000000..822192bb --- /dev/null +++ b/components/core/Filter/Popup.js @@ -0,0 +1,86 @@ +import * as React from "react"; +import * as SVG from "~/common/svg"; +import * as Styles from "~/common/styles"; +import * as Filters from "~/components/core/Filter/Filters"; + +import { useFilterContext } from "~/components/core/Filter/Provider"; +import { motion } from "framer-motion"; +import { css } from "@emotion/react"; + +/* ------------------------------------------------------------------------------------------------- + * Popup trigger + * -----------------------------------------------------------------------------------------------*/ + +export function PopupTrigger({ children, isMobile, ...props }) { + const [{ sidebarState, popupState }, { hidePopup, togglePopup }] = useFilterContext(); + + React.useEffect(() => { + if (sidebarState.isVisible) { + hidePopup(); + } + }, [sidebarState.isVisible]); + + if (sidebarState.isVisible && !isMobile) return null; + + return ( + + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Popup + * -----------------------------------------------------------------------------------------------*/ +const STYLES_SIDEBAR_FILTER_WRAPPER = (theme) => css` + position: sticky; + top: ${theme.sizes.header + theme.sizes.filterNavbar}px; + width: 236px; + max-height: 420px; + overflow-y: auto; + overflow-x: hidden; + border-radius: 16px; + padding: 20px 16px; + box-shadow: ${theme.shadow.lightLarge}; + border: 1px solid ${theme.semantic.borderGrayLight}; + + background-color: ${theme.semantic.bgLight}; + @supports ((-webkit-backdrop-filter: blur(75px)) or (backdrop-filter: blur(75px))) { + background-color: ${theme.semantic.bgBlurWhite}; + -webkit-backdrop-filter: blur(75px); + backdrop-filter: blur(75px); + } + + @media (max-width: ${theme.sizes.mobile}px) { + border-radius: unset; + width: 100%; + max-height: 375px; + padding: 15px 8px; + } +`; + +export function Popup({ viewer, onAction, css, data, page, ...props }) { + const [{ popupState }] = useFilterContext(); + + if (!popupState.isVisible) return null; + + return ( +
    + + +
    + ); +} diff --git a/components/core/Filter/Provider.js b/components/core/Filter/Provider.js index be5517d5..5c7db6d1 100644 --- a/components/core/Filter/Provider.js +++ b/components/core/Filter/Provider.js @@ -1,26 +1,22 @@ import * as React from "react"; -import { useWorker } from "~/common/hooks"; - const UploadContext = React.createContext({}); - export const useFilterContext = () => React.useContext(UploadContext); -export const Provider = ({ children, viewer }) => { +export const Provider = ({ children }) => { const [isSidebarVisible, toggleSidebar] = useFilterSidebar(); - const [filterState, { setFilterType, setFilterObjects, resetFilterState }] = useFilter({ - library: viewer.library, - }); - - const workerState = useFilterWorker({ filterState, setFilterObjects, library: viewer.library }); + const [isPopupVisible, { hidePopup, togglePopup }] = useFilterPopup(); const contextValue = React.useMemo( () => [ - { isSidebarVisible, filterState, ...workerState }, - { toggleSidebar, setFilterType, resetFilterState }, + { + sidebarState: { isVisible: isSidebarVisible }, + popupState: { isVisible: isPopupVisible }, + }, + { toggleSidebar, hidePopup, togglePopup }, ], - [isSidebarVisible, filterState, workerState] + [isSidebarVisible, isPopupVisible] ); return {children}; @@ -32,61 +28,10 @@ const useFilterSidebar = () => { return [isSidebarVisible, toggleSidebar]; }; -const useFilter = ({ library }) => { - const DEFAULT_STATE = { - view: "initial", - subview: undefined, - type: "library", - objects: library, - search: { - objects: [], - tags: [], - startDate: null, - endDate: null, - }, - }; +const useFilterPopup = () => { + const [isPopupVisible, setPopupVisibility] = React.useState(false); - const [filterState, setFilterState] = React.useState(DEFAULT_STATE); - - const setFilterType = ({ view, subview = undefined, type }) => - setFilterState((prev) => ({ ...prev, view, subview, type })); - - const setFilterObjects = (objects) => setFilterState((prev) => ({ ...prev, objects })); - - const resetFilterState = () => setFilterState(DEFAULT_STATE); - - return [filterState, { setFilterType, resetFilterState, setFilterObjects }]; -}; - -const useFilterWorker = ({ filterState, setFilterObjects, library }) => { - const DEFAULT_STATE = { loading: false, error: false }; - const [workerState, setWorkerState] = React.useState(DEFAULT_STATE); - - const { view, subview, type } = filterState; - - /** - * NOTE(amine): Web workers are usually pretty fast, - * but if it takes more than 500ms to handle a task, we'll show a loading screen - */ - const timeoutRef = React.useRef(); - - useWorker( - { - onStart: (worker) => { - worker.postMessage({ objects: library, view, subview, type }); - timeoutRef.current = setTimeout(() => { - setWorkerState((prev) => ({ ...prev, loading: true })); - }, 500); - }, - onError: () => setWorkerState((prev) => ({ ...prev, error: true })), - onMessage: (e) => { - clearTimeout(timeoutRef.current); - setWorkerState(DEFAULT_STATE); - setFilterObjects(e.data); - }, - }, - [view, subview, library, type] - ); - - return workerState; + const hidePopup = () => setPopupVisibility(false); + const togglePopup = () => setPopupVisibility((prev) => !prev); + return [isPopupVisible, { hidePopup, togglePopup }]; }; diff --git a/components/core/Filter/Sidebar.js b/components/core/Filter/Sidebar.js index defd1a1f..23b2ed30 100644 --- a/components/core/Filter/Sidebar.js +++ b/components/core/Filter/Sidebar.js @@ -2,10 +2,9 @@ import * as React from "react"; import * as SVG from "~/common/svg"; import * as Styles from "~/common/styles"; import * as Filters from "~/components/core/Filter/Filters"; +import * as Constants from "~/common/constants"; -import * as FilterUtilities from "~/common/filter-utilities"; import { useFilterContext } from "~/components/core/Filter/Provider"; -import { Show } from "~/components/utility/Show"; import { css } from "@emotion/react"; /* ------------------------------------------------------------------------------------------------- @@ -23,19 +22,16 @@ const STYLES_SIDEBAR_TRIGGER = (theme) => css` } `; -export function SidebarTrigger() { - const [{ isSidebarVisible }, { toggleSidebar }] = useFilterContext(); +export function SidebarTrigger({ css }) { + const [{ sidebarState }, { toggleSidebar }] = useFilterContext(); return ( @@ -46,24 +42,13 @@ export function SidebarTrigger() { * Sidebar * -----------------------------------------------------------------------------------------------*/ -export function Sidebar() { - const [{ isSidebarVisible }] = useFilterContext(); - return ( - -
    - -
    -
    - ); -} - const STYLES_SIDEBAR_FILTER_WRAPPER = (theme) => css` position: sticky; top: ${theme.sizes.header + theme.sizes.filterNavbar}px; - width: 236px; - height: 100vh; + width: 300px; max-height: calc(100vh - ${theme.sizes.header + theme.sizes.filterNavbar}px); - padding: 20px; + overflow-y: auto; + padding: 20px 24px calc(16px + ${theme.sizes.intercomWidget}px + ${theme.sizes.filterNavbar}px); background-color: ${theme.semantic.bgLight}; @media (max-width: ${theme.sizes.mobile}px) { @@ -71,46 +56,21 @@ const STYLES_SIDEBAR_FILTER_WRAPPER = (theme) => css` } `; -/* ------------------------------------------------------------------------------------------------- - * SidebarContent - * -----------------------------------------------------------------------------------------------*/ +export function Sidebar({ viewer, onAction, data, page, isMobile }) { + const [{ sidebarState }] = useFilterContext(); -function SidebarContent() { - const [{ filterState }, { setFilterType }] = useFilterContext(); - const currentView = filterState.view; - const currentSubview = filterState.subview; - - const { FILTER_VIEWS_IDS, FILTER_SUBVIEWS_IDS } = FilterUtilities; - const { filters, subviews } = FilterUtilities.getViewData(currentView); - - const changeFilterView = (view) => { - const { filters } = FilterUtilities.getViewData(view); - setFilterType({ view: view, type: filters.initial }); - }; - - const changeFilterSubview = (subview) => { - const initialType = subviews[subview].filters.initial; - setFilterType({ view: currentView, subview, type: initialType }); - }; - - if (currentView === FILTER_VIEWS_IDS.browser) { - if (currentSubview === FILTER_SUBVIEWS_IDS.browser.saved) { - const { filters } = subviews[currentSubview]; - return ; - } - - return ( - changeFilterSubview(FILTER_SUBVIEWS_IDS.browser.saved)} - /> - ); - } + if (!sidebarState.isVisible || isMobile) return null; return ( - changeFilterView(FILTER_VIEWS_IDS.browser)} - /> +
    + + +
    ); } diff --git a/components/core/Filter/index.js b/components/core/Filter/index.js index aeed491e..1d27e0de 100644 --- a/components/core/Filter/index.js +++ b/components/core/Filter/index.js @@ -1,35 +1,124 @@ import * as React from "react"; import * as System from "~/components/system"; +import * as Styles from "~/common/styles"; +import * as Search from "~/components/core/Search"; -import { Navbar, NavbarPortal } from "~/components/core/Filter/Navbar"; -import { Breadcrumb } from "~/components/core/Filter/Breadcrumb"; +import { css } from "@emotion/react"; +import { NavbarPortal } from "~/components/core/Filter/Navbar"; import { Provider } from "~/components/core/Filter/Provider"; import { Sidebar, SidebarTrigger } from "~/components/core/Filter/Sidebar"; -import { Content } from "~/components/core/Filter/Content"; +import { Popup, PopupTrigger } from "~/components/core/Filter/Popup"; +import { useSearchStore } from "~/components/core/Search/store"; /* ------------------------------------------------------------------------------------------------- * Title * -----------------------------------------------------------------------------------------------*/ -function Title() { - return All; +function Title({ page, data }) { + const { query, isSearching } = useSearchStore(); + let title = React.useMemo(() => { + if (isSearching) return `Searching for ${query}`; + if (page.id === "NAV_DATA") return "My library"; + if (page.id === "NAV_SLATE" && data?.slatename) return "# " + data?.slatename; + }, [page, data, query, isSearching]); + + return ( + + {title} + + ); } /* ------------------------------------------------------------------------------------------------- * Actions * -----------------------------------------------------------------------------------------------*/ + function Actions() { return
    ; } -export { - Title, - Actions, - Sidebar, - SidebarTrigger, - Provider, - Navbar, - Content, - Breadcrumb, - NavbarPortal, -}; +const STYLES_FILTER_TITLE_WRAPPER = css` + ${Styles.MOBILE_HIDDEN}; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +`; + +const STYLES_FILTER_POPUP_WRAPPER = (theme) => css` + position: absolute; + top: calc(${theme.sizes.filterNavbar}px + 4px); + left: 4px; + width: 100%; + @media (max-width: ${theme.sizes.mobile}px) { + top: ${theme.sizes.filterNavbar}px; + left: 0px; + } +`; + +/* ------------------------------------------------------------------------------------------------- + * Filter + * -----------------------------------------------------------------------------------------------*/ + +const STYLES_FILTER_CONTENT = (theme) => css` + ${Styles.HORIZONTAL_CONTAINER}; + width: 100%; + margin-top: ${theme.sizes.filterNavbar}px; +`; + +export default function Filter({ isActive, viewer, onAction, page, data, isMobile, children }) { + const { results, isSearching } = useSearchStore(); + const showSearchResult = isSearching && !!results; + + if (!isActive) { + return showSearchResult ? ( + + ) : ( + children + ); + } + + return ( + <> + + +
    + +
    + +
    +
    + +
    + + + + </span> + </PopupTrigger> + </div> + + <div css={STYLES_FILTER_TITLE_WRAPPER}> + <Title page={page} data={data} /> + </div> + <Actions /> + </NavbarPortal> + <div css={STYLES_FILTER_CONTENT}> + <Sidebar + viewer={viewer} + onAction={onAction} + data={data} + page={page} + isMobile={isMobile} + /> + <div style={{ flexGrow: 1 }}> + {showSearchResult ? ( + <Search.Content viewer={viewer} page={page} onAction={onAction} /> + ) : ( + children + )} + </div> + </div> + </Provider> + </> + ); +} diff --git a/components/core/MarkdownFrame.js b/components/core/MarkdownFrame.js index c2339d46..70f8e68d 100644 --- a/components/core/MarkdownFrame.js +++ b/components/core/MarkdownFrame.js @@ -6,6 +6,7 @@ import * as Strings from "~/common/strings"; import { css } from "@emotion/react"; import { Markdown } from "~/components/system/components/Markdown"; import { H1, H2, H3, H4, P1, UL, OL, LI, A } from "~/components/system/components/Typography"; +import { useCache, useIsomorphicLayoutEffect } from "~/common/hooks"; const STYLES_ASSET = (theme) => css` padding: 120px calc(32px + 16px + 8px); @@ -133,12 +134,18 @@ const STYLES_INTENT = (theme) => css` ); `; -export default function MarkdownFrame({ url, date }) { - const [content, setContent] = React.useState(""); +export default function MarkdownFrame({ cid, url, date }) { + const [cache, setCache] = useCache(); + const cachedContent = cache[cid] || ""; + + const [content, setContent] = React.useState(cachedContent); + + useIsomorphicLayoutEffect(() => { + if (cachedContent) return; - React.useEffect(() => { fetch(url).then(async (res) => { const content = await res.text(); + setCache(content); setContent(content); }); }, []); diff --git a/components/core/ObjectPreview/ImageObjectPreview.js b/components/core/ObjectPreview/ImageObjectPreview.js index b84676f8..4d1f1e87 100644 --- a/components/core/ObjectPreview/ImageObjectPreview.js +++ b/components/core/ObjectPreview/ImageObjectPreview.js @@ -6,8 +6,10 @@ import { AspectRatio } from "~/components/system"; import { useInView } from "~/common/hooks"; import { Blurhash } from "react-blurhash"; import { isBlurhashValid } from "blurhash"; - +import { AnimatePresence, motion } from "framer-motion"; import { css } from "@emotion/react"; +import { useCache } from "~/common/hooks"; + import ObjectPreviewPrimitive from "~/components/core/ObjectPreview/ObjectPreviewPrimitive"; const STYLES_PLACEHOLDER_ABSOLUTE = css` @@ -29,8 +31,8 @@ const STYLES_IMAGE = css` width: 100%; `; -const ImagePlaceholder = ({ blurhash }) => ( - <div css={STYLES_PLACEHOLDER_ABSOLUTE}> +const ImagePlaceholder = ({ blurhash, ...props }) => ( + <motion.div css={STYLES_PLACEHOLDER_ABSOLUTE} {...props}> <div css={[Styles.CONTAINER_CENTERED, STYLES_FLUID_CONTAINER]}> <AspectRatio ratio={1}> <div> @@ -45,12 +47,9 @@ const ImagePlaceholder = ({ blurhash }) => ( </div> </AspectRatio> </div> - </div> + </motion.div> ); -// NOTE(amine): cache -const cidsLoaded = {}; - export default function ImageObjectPreview({ url, file, @@ -58,22 +57,20 @@ export default function ImageObjectPreview({ tag, ...props }) { - const isCached = cidsLoaded[file.cid]; + /** NOTE(amine): To minimize the network load, we only load images when they're in view. + This creates an issue where cached images will reload each time they came to view. + To prevent reloading we'll keep track of the images that already loaded */ + const [cache, setCache] = useCache(); + const isCached = cache[file.cid]; const previewerRef = React.useRef(); - const [isLoading, setLoading] = React.useState(isCached); - const handleOnLoaded = () => { - cidsLoaded[file.cid] = true; - setLoading(false); - }; - const { isInView } = useInView({ ref: previewerRef, }); const { type, coverImage } = file; const imgTag = type.split("/")[1]; - + const imageUrl = coverImage ? coverImage?.url || Strings.getURLfromCID(coverImage?.cid) : url; const blurhash = React.useMemo(() => { return file.blurhash && isBlurhashValid(file.blurhash).result ? file.blurhash @@ -82,14 +79,14 @@ export default function ImageObjectPreview({ : null; }, [file]); - const shouldShowPlaceholder = isLoading && blurhash; - - const imageUrl = coverImage ? coverImage?.url || Strings.getURLfromCID(coverImage?.cid) : url; + const [isLoading, setLoading] = React.useState(true); + const handleOnLoaded = () => (setCache({ key: file.cid, value: true }), setLoading(false)); + const shouldShowPlaceholder = !isCached && isLoading && !!blurhash; return ( <ObjectPreviewPrimitive file={file} tag={tag || imgTag} isImage {...props}> <div ref={previewerRef} css={[Styles.CONTAINER_CENTERED, STYLES_FLUID_CONTAINER]}> - {isInView && ( + {(isCached || isInView) && ( <img css={STYLES_IMAGE} src={imageUrl} @@ -97,7 +94,11 @@ export default function ImageObjectPreview({ onLoad={handleOnLoaded} /> )} - {shouldShowPlaceholder && <ImagePlaceholder blurhash={blurhash} />} + <AnimatePresence> + {shouldShowPlaceholder && ( + <ImagePlaceholder blurhash={blurhash} initial={{ opacity: 1 }} exit={{ opacity: 0 }} /> + )} + </AnimatePresence> </div> </ObjectPreviewPrimitive> ); diff --git a/components/core/ObjectPreview/TextObjectPreview.js b/components/core/ObjectPreview/TextObjectPreview.js index 56066ba4..f8f0dcec 100644 --- a/components/core/ObjectPreview/TextObjectPreview.js +++ b/components/core/ObjectPreview/TextObjectPreview.js @@ -5,7 +5,7 @@ import * as Styles from "~/common/styles"; import * as Utilities from "~/common/utilities"; import { P3 } from "~/components/system"; -import { useIsomorphicLayoutEffect } from "~/common/hooks"; +import { useCache, useIsomorphicLayoutEffect } from "~/common/hooks"; import { css } from "@emotion/react"; import FilePlaceholder from "~/components/core/ObjectPreview/placeholders/File"; @@ -29,12 +29,21 @@ const STYLES_TEXT_PREVIEW = (theme) => }); export default function TextObjectPreview({ url, file, ...props }) { - const [{ content, error }, setState] = React.useState({ content: "", error: undefined }); + const [cache, setCache] = useCache(); + const cachedContent = cache[file.cid] || ""; + + const [{ content, error }, setState] = React.useState({ + content: cachedContent, + error: undefined, + }); useIsomorphicLayoutEffect(() => { + if (cachedContent) return; + fetch(url) .then(async (res) => { const content = await res.text(); + setCache({ key: file.cid, value: content }); setState({ content }); }) .catch((e) => { diff --git a/components/core/Search/index.js b/components/core/Search/index.js new file mode 100644 index 00000000..7484dc93 --- /dev/null +++ b/components/core/Search/index.js @@ -0,0 +1,231 @@ +import * as React from "react"; +import * as SVG from "~/common/svg"; +import * as Styles from "~/common/styles"; + +import { css } from "@emotion/react"; +import { Input as InputPrimitive } from "~/components/system/components/Input"; +import { useSearchStore } from "~/components/core/Search/store"; +import { LoaderSpinner } from "~/components/system/components/Loaders"; +import { FileTypeGroup } from "~/components/core/FileTypeIcon"; +import { Link } from "~/components/core/Link"; + +import DataView from "~/components/core/DataView"; +import CollectionPreviewBlock from "~/components/core/CollectionPreviewBlock"; +import EmptyState from "~/components/core/EmptyState"; +import omit from "lodash.omit"; + +/* ------------------------------------------------------------------------------------------------- + * Input + * -----------------------------------------------------------------------------------------------*/ + +const STYLES_SEARCH_COMPONENT = (theme) => css` + background-color: transparent; + box-shadow: none; + height: 100%; + input { + height: 100%; + padding: 0px 4px; + border-radius: 0px; + } + &::placeholder { + color: ${theme.semantic.textGray}; + } +`; + +const useSearchViaParams = ({ params, handleSearch }) => { + const { setQuery, clearSearch } = useSearchStore(); + + React.useEffect(() => { + if (params?.s) { + setQuery(params.s); + handleSearch(params.s); + } + }, []); + + // NOTE(amine): if we change + React.useEffect(() => { + if (!params.s) clearSearch(); + }, [params.s]); +}; + +const useDebouncedSearch = ({ handleSearch }) => { + const { query } = useSearchStore(); + + const timeRef = React.useRef(); + React.useEffect(() => { + timeRef.current = setTimeout(() => handleSearch(query), 300); + return () => clearTimeout(timeRef.current); + }, [query]); +}; + +function Input({ viewer, data, page, onAction }) { + const { search, query, isFetchingResults, setQuery } = useSearchStore(); + + const handleSearch = async (query) => { + // NOTE(amine): update params with search query + onAction({ + type: "UPDATE_PARAMS", + params: query?.length > 0 ? { s: query } : omit(page.params, ["s"]), + }); + + if (!query) return; + + // NOTE(amine): searching on your own tag. + if (page.id === "NAV_SLATE" && data?.ownerId === viewer?.id) { + search({ + types: ["FILE"], + tagIds: [data.id], + query, + }); + return; + } + + //NOTE(amine): searching on another user's tag + if (page.id === "NAV_SLATE" && data?.ownerId !== viewer?.id) { + search({ + types: ["FILE"], + tagIds: [data.id], + query, + globalSearch: true, + }); + return; + } + + //NOTE(amine): searching on another user's profile + if (page.id === "NAV_PROFILE" && data?.id !== viewer?.id) { + console.log(data?.id, page.id, page.id === "NAV_PROFILE", data?.id !== viewer?.id); + search({ + types: ["SLATE", "FILE"], + userId: data.id, + query, + globalSearch: true, + grouped: true, + }); + return; + } + + //NOTE(amine): searching on library + if (viewer) { + search({ + types: ["FILE", "SLATE"], + query, + grouped: true, + }); + return; + } + + // NOTE(amine): global search + search({ + types: ["FILE", "SLATE", "USER"], + globalSearch: true, + query: query, + grouped: true, + }); + }; + + useSearchViaParams({ params: page.params, onAction, handleSearch }); + + useDebouncedSearch({ handleSearch }); + + return ( + <div style={{ position: "relative" }}> + <InputPrimitive + full + containerStyle={{ height: "100%" }} + inputCss={STYLES_SEARCH_COMPONENT} + name="search" + placeholder={`Search ${!viewer ? "slate.host" : ""}`} + onSubmit={() => handleSearch(query)} + value={query} + onChange={(e) => setQuery(e.target.value)} + /> + + {isFetchingResults && ( + <div style={{ position: "absolute", right: 0, top: 0 }}> + <LoaderSpinner style={{ position: "block", height: 16, width: 16 }} /> + </div> + )} + </div> + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Dismiss + * -----------------------------------------------------------------------------------------------*/ + +const STYLES_DISMISS_BUTTON = (theme) => css` + display: block; + ${Styles.BUTTON_RESET}; + color: ${theme.semantic.textGray}; +`; + +function Dismiss({ css, ...props }) { + const { clearSearch } = useSearchStore(); + + return ( + <button onClick={clearSearch} css={[STYLES_DISMISS_BUTTON, css]} {...props}> + <SVG.Dismiss style={{ display: "block" }} height={16} width={16} /> + </button> + ); +} + +/* ------------------------------------------------------------------------------------------------- + * Content + * -----------------------------------------------------------------------------------------------*/ + +const STYLES_SEARCH_CONTENT = (theme) => css` + width: 100%; + min-height: 100vh; + padding: calc(20px + ${theme.sizes.filterNavbar}px) 24px 44px; + @media (max-width: ${theme.sizes.mobile}px) { + padding: calc(31px + ${theme.sizes.filterNavbar}px) 16px 44px; + } +`; + +function Content({ onAction, viewer, page }) { + const { results } = useSearchStore(); + const { files, slates } = results; + + if (results.files.length === 0 && results.slates.length === 0) { + return ( + <div css={STYLES_SEARCH_CONTENT}> + <EmptyState> + <FileTypeGroup /> + <div style={{ marginTop: 24 }}>Sorry we couldn't find any results.</div> + </EmptyState> + </div> + ); + } + + return ( + <div css={STYLES_SEARCH_CONTENT}> + <DataView + key="scene-files-folder" + isOwner={true} + items={files} + onAction={onAction} + viewer={viewer} + page={page} + view="grid" + /> + {slates.length > 0 ? ( + <div style={{ marginTop: 24 }} css={Styles.COLLECTIONS_PREVIEW_GRID}> + {slates.map((slate) => ( + <Link key={slate.id} href={`/$/slate/${slate.id}`} onAction={onAction}> + <CollectionPreviewBlock + key={slate.id} + collection={slate} + viewer={viewer} + // TODO(amine): use owner's info instead of viewer + owner={viewer} + onAction={onAction} + /> + </Link> + ))} + </div> + ) : null} + </div> + ); +} + +export { Input, Dismiss, Content }; diff --git a/components/core/Search/store.js b/components/core/Search/store.js new file mode 100644 index 00000000..aedad5b7 --- /dev/null +++ b/components/core/Search/store.js @@ -0,0 +1,48 @@ +import * as Actions from "~/common/actions"; +import * as Events from "~/common/custom-events"; + +import create from "zustand"; + +const DEFAULT_STATE = { + query: "", + results: null, + isSearching: false, + isFetchingResults: false, +}; + +export const useSearchStore = create((set) => { + const search = async ({ types, query, globalSearch, tagIds, grouped }) => { + //for more context, look at node_common/managers/search/search.js + //userId: (optional) the id of the user whose stuff we are searching through. If specified, globalSearch is disregarded since the search will be limited to that user's public items. Does not apply when searching for type USER + //types: leaving it null searches everything. Doing ["SLATE"] searches just slates, doing ["USER", "FILE"] searches users and files. + //globalSearch: whether you are just searching the user's files/slates or global files/slates. This option doesn't exist for searching users since there is no notion of public or private users + //tagIds: only applies when searching files. the ids of the tags (aka collections) you are searching within. aka if you only want to search for files in a given slate, provide that slate's id. If no tag ids are provided, it searches all files + //grouped: whether to group the results by type (slate, user, file) when searching multiple types. Doesn't apply when searching only one type e.g. types: ["SLATE"] + set((prev) => ({ ...prev, isFetchingResults: true })); + const response = await Actions.search({ + types, + query, + globalSearch, + tagIds, + grouped, + }); + + Events.hasError(response); + const { results = [] } = response; + const files = results?.files || results; + const slates = results?.slates || []; + + set((prev) => ({ ...prev, results: { files, slates }, isFetchingResults: false })); + }; + + const clearSearch = () => set((prev) => ({ ...prev, ...DEFAULT_STATE })); + + const setQuery = (query) => set((prev) => ({ ...prev, isSearching: true, query })); + + return { + ...DEFAULT_STATE, + search, + setQuery, + clearSearch, + }; +}); diff --git a/components/core/SlateMediaObject.js b/components/core/SlateMediaObject.js index c01b2f78..602be0d8 100644 --- a/components/core/SlateMediaObject.js +++ b/components/core/SlateMediaObject.js @@ -198,7 +198,7 @@ export default class SlateMediaObject extends React.Component { } if (Validations.isMarkdown(file.filename, type)) { - return <MarkdownFrame date={file.createdAt} url={url} />; + return <MarkdownFrame date={file.createdAt} cid={file?.cid} url={url} />; } if (Validations.isPreviewableImage(type)) { diff --git a/components/system/components/Divider.js b/components/system/components/Divider.js index 3af52c18..a516cece 100644 --- a/components/system/components/Divider.js +++ b/components/system/components/Divider.js @@ -6,16 +6,20 @@ export const Divider = ({ width = "100%", height = "0.5px", color = Constants.system.grayLight4, + css, ...props }) => { return ( <div - css={(theme) => ({ - height, - width, - minHeight: height, - backgroundColor: theme.system?.[color] || theme.semantic?.[color] || color, - })} + css={[ + (theme) => ({ + height, + width, + minHeight: height, + backgroundColor: theme.system?.[color] || theme.semantic?.[color] || color, + }), + css, + ]} {...props} /> ); diff --git a/scenes/SceneFilesFolder.js b/scenes/SceneFilesFolder.js index a8f47194..3c8ab9b8 100644 --- a/scenes/SceneFilesFolder.js +++ b/scenes/SceneFilesFolder.js @@ -1,13 +1,15 @@ import * as React from "react"; import * as Constants from "~/common/constants"; -import * as Filter from "~/components/core/Filter"; import * as Styles from "~/common/styles"; import { css } from "@emotion/react"; import { GlobalCarousel } from "~/components/system/components/GlobalCarousel"; +import { FileTypeGroup } from "~/components/core/FileTypeIcon"; import ScenePage from "~/components/core/ScenePage"; import WebsitePrototypeWrapper from "~/components/core/WebsitePrototypeWrapper"; +import DataView from "~/components/core/DataView"; +import EmptyState from "~/components/core/EmptyState"; const STYLES_SCENE_PAGE = css` padding: 0px; @@ -16,11 +18,13 @@ const STYLES_SCENE_PAGE = css` } `; -const STYLES_FILTER_TITLE_WRAPPER = css` - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); +const STYLES_DATAVIEWER_WRAPPER = (theme) => css` + width: 100%; + min-height: calc(100vh - ${theme.sizes.filterNavbar}px); + padding: calc(20px + ${theme.sizes.filterNavbar}px) 24px 44px; + @media (max-width: ${theme.sizes.mobile}px) { + padding: calc(31px + ${theme.sizes.filterNavbar}px) 16px 44px; + } `; export default function SceneFilesFolder({ viewer, page, onAction, isMobile }) { @@ -45,23 +49,24 @@ export default function SceneFilesFolder({ viewer, page, onAction, isMobile }) { index={index} onChange={(index) => setIndex(index)} /> - <Filter.Provider viewer={viewer}> - <Filter.NavbarPortal> - <div css={Styles.CONTAINER_CENTERED}> - <Filter.SidebarTrigger /> - <Filter.Breadcrumb style={{ marginLeft: 16 }} /> - </div> - <div css={STYLES_FILTER_TITLE_WRAPPER}> - <Filter.Title /> - </div> - <Filter.Actions /> - </Filter.NavbarPortal> - - <div css={Styles.HORIZONTAL_CONTAINER}> - <Filter.Sidebar /> - <Filter.Content onAction={onAction} viewer={viewer} page={page} /> - </div> - </Filter.Provider> + <div css={STYLES_DATAVIEWER_WRAPPER}> + {objects.length ? ( + <DataView + key="scene-files-folder" + isOwner={true} + items={objects} + onAction={onAction} + viewer={viewer} + page={page} + view="grid" + /> + ) : ( + <EmptyState> + <FileTypeGroup /> + <div style={{ marginTop: 24 }}>Drag and drop files into Slate to upload</div> + </EmptyState> + )} + </div> </ScenePage> </WebsitePrototypeWrapper> ); diff --git a/scenes/SceneSlate.js b/scenes/SceneSlate.js index 3ae2d3b8..c4f45591 100644 --- a/scenes/SceneSlate.js +++ b/scenes/SceneSlate.js @@ -261,6 +261,26 @@ export default class SceneSlate extends React.Component { } } +const STYLES_RESET_SCENE_PAGE_PADDING = css` + padding: 0px; + @media (max-width: ${Constants.sizes.mobile}px) { + padding: 0px; + } +`; + +const STYLES_DATAVIEWER_WRAPPER = (theme) => css` + width: 100%; + min-height: calc(100vh - ${theme.sizes.filterNavbar}px); + padding: calc(20px + ${theme.sizes.filterNavbar}px) 24px 44px; + @media (max-width: ${theme.sizes.mobile}px) { + padding: calc(31px + ${theme.sizes.filterNavbar}px) 16px 44px; + } +`; + +const STYLES_DATAVIEWER_WRAPPER_EXTERNAL = css` + margin-top: 40px; +`; + class SlatePage extends React.Component { _copy = null; _timeout = null; @@ -394,74 +414,34 @@ class SlatePage extends React.Component { const { user, name, objects, body, isPublic, ownerId } = this.props.data; const isOwner = this.props.viewer ? ownerId === this.props.viewer.id : false; - let actions = isOwner ? ( - <span css={Styles.HORIZONTAL_CONTAINER}> - <SquareButtonGray onClick={this._handleDownload} style={{ marginRight: 16 }}> - <SVG.Download height="16px" /> - </SquareButtonGray> - <Upload.Trigger viewer={this.props.viewer} style={{ marginRight: 16 }}> - <SquareButtonGray> - <SVG.Plus height="16px" /> - </SquareButtonGray> - </Upload.Trigger> - <SquareButtonGray onClick={this._handleShowSettings}> - <SVG.Settings height="16px" /> - </SquareButtonGray> - </span> - ) : ( - <div style={{ display: `flex` }}> - <SquareButtonGray onClick={this._handleDownload} style={{ marginRight: 16 }}> - <SVG.Download height="16px" /> - </SquareButtonGray> - <div onClick={this._handleSubscribe}> - {this.state.isSubscribed ? ( - <ButtonSecondary>Unsubscribe</ButtonSecondary> - ) : ( - <ButtonPrimary>Subscribe</ButtonPrimary> - )} - </div> - </div> - ); return ( - <ScenePage> - <ScenePageHeader - wide - title={ - user && !isOwner ? ( - <span> - <Link href={`/$/user/${user.id}`} onAction={this.props.onAction}> - <span - // onClick={() => - // this.props.onAction({ - // type: "NAVIGATE", - // value: "NAV_PROFILE", - // shallow: true, - // data: user, - // }) - // } - css={STYLES_USERNAME} - > - {user.username} - </span>{" "} - </Link> - / {name} - </span> - ) : ( - <div css={Styles.HORIZONTAL_CONTAINER_CENTERED}> - <span>{name}</span> - {isOwner && !isPublic && ( - <div css={STYLES_SECURITY_LOCK_WRAPPER} style={{ marginLeft: 16 }}> - <SVG.SecurityLock height="16px" style={{ display: "block" }} /> - </div> - )} - </div> - ) - } - actions={<span css={STYLES_MOBILE_HIDDEN}>{actions}</span>} - > - {body} - </ScenePageHeader> - <span css={STYLES_MOBILE_ONLY}>{actions}</span> + <ScenePage css={!this.props.external && STYLES_RESET_SCENE_PAGE_PADDING}> + {this.props.external ? ( + <ScenePageHeader + wide + title={ + user && !isOwner ? ( + <span> + <Link href={`/$/user/${user.id}`} onAction={this.props.onAction}> + <span css={STYLES_USERNAME}>{user.username}</span>{" "} + </Link> + / {name} + </span> + ) : ( + <div css={Styles.HORIZONTAL_CONTAINER_CENTERED}> + <span>{name}</span> + {isOwner && !isPublic && ( + <div css={STYLES_SECURITY_LOCK_WRAPPER} style={{ marginLeft: 16 }}> + <SVG.SecurityLock height="16px" style={{ display: "block" }} /> + </div> + )} + </div> + ) + } + > + {body} + </ScenePageHeader> + ) : null} {objects && objects.length ? ( <> <GlobalCarousel @@ -476,7 +456,11 @@ class SlatePage extends React.Component { index={this.state.index} onChange={(index) => this.setState({ index })} /> - <div style={{ marginTop: 40 }}> + <div + css={ + this.props.external ? STYLES_DATAVIEWER_WRAPPER_EXTERNAL : STYLES_DATAVIEWER_WRAPPER + } + > <DataView key="scene-files-folder" type="collection" diff --git a/workers/filter-files.js b/workers/filter-files.js deleted file mode 100644 index 4993d557..00000000 --- a/workers/filter-files.js +++ /dev/null @@ -1,8 +0,0 @@ -import { getFilterHandler } from "~/common/filter-utilities"; - -onmessage = function (e) { - const { objects = [], view, subview, type } = e.data; - const filterCallback = getFilterHandler({ view, subview, type }); - const result = objects.filter(filterCallback); - postMessage(result); -};