Merge pull request #973 from filecoin-project/@aminejv/filter-updates

Add tags to the filter sidebar
This commit is contained in:
martinalong 2021-11-29 13:34:46 -08:00 committed by GitHub
commit 9106c47a06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 753 additions and 733 deletions

View File

@ -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 = {

View File

@ -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];
};

View File

@ -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];
};

View File

@ -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 ? (
<div
css={Styles.CONTAINER_CENTERED}
style={{
width: "100vw",
height: "100vh",
}}
>
<LoaderSpinner style={{ height: 32, width: 32 }} />
</div>
) : (
scene
)}
<Filter
isActive={!!this.state.viewer}
viewer={this.state.viewer}
page={page}
data={this.state.data}
isMobile={this.props.isMobile}
onAction={this._handleAction}
>
{this.state.loading ? (
<div
css={Styles.CONTAINER_CENTERED}
style={{
width: "100%",
height: "100vh",
}}
>
<LoaderSpinner style={{ height: 32, width: 32 }} />
</div>
) : (
scene
)}
</Filter>
</ApplicationLayout>
</PortalsProvider>
<GlobalModal />

View File

@ -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 }) {
</div>
<div css={STYLES_MIDDLE}>
{/**TODO: update Search component */}
<Input
containerStyle={{ height: "100%" }}
full
placeholder={`Search ${!viewer ? "slate.host" : ""}`}
inputCss={STYLES_SEARCH_COMPONENT}
onSubmit={handleSearch}
name="search"
{...getFieldProps()}
onChange={_handleInputChange}
value={state.query}
/>
<Search.Input viewer={viewer} data={data} onAction={onAction} page={page} />
</div>
<Upload.Provider page={page} data={data} viewer={viewer}>
<Upload.Root data={data}>
@ -238,7 +177,6 @@ export default function ApplicationHeader({ viewer, page, data, onAction }) {
isSearching={isSearching}
isSignedOut={isSignedOut}
onAction={onAction}
onDismissSearch={handleDismissSearch}
/>
</div>
</Upload.Root>
@ -261,14 +199,14 @@ export default function ApplicationHeader({ viewer, page, data, onAction }) {
</div>
<div css={STYLES_FILTER_NAVBAR}>
<Show when={!!viewer}>
<Filter.Navbar />
<FilterNavbar />
</Show>
</div>
</>
);
}
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 }}
>
<button
onClick={onDismissSearch}
style={{ marginRight: 4 }}
css={STYLES_DISMISS_BUTTON}
>
<SVG.Dismiss style={{ display: "block" }} height={16} width={16} />
</button>
<Search.Dismiss style={{ marginLeft: 4 }} />
</motion.div>
</Match>
</Switch>

View File

@ -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 (
<div css={STYLES_CONTAINER}>
@ -228,7 +229,7 @@ function Metrics({ fileCount, owner, isOwner, onAction }) {
</div>
<div style={{ alignItems: "end" }} css={Styles.CONTAINER_CENTERED}>
{!isOwner && (
{isOwner && (
<>
<Link
href={`/$/user/${owner.id}`}

View File

@ -1,63 +0,0 @@
import * as React from "react";
import * as System from "~/components/system";
import * as Styles from "~/common/styles";
import * as Strings from "~/common/strings";
import { useFilterContext } from "~/components/core/Filter/Provider";
import { css } from "@emotion/react";
import { FILTER_VIEWS_IDS, FILTER_TYPES } from "~/common/filter-utilities";
import { Show } from "~/components/utility/Show";
const STYLES_BREADCRUMB_BUTTON = (theme) => css`
${Styles.BUTTON_RESET};
:hover {
color: ${theme.semantic.textBlack};
}
`;
function Item({ children, color, includeDelimiter, ...props }) {
return (
<>
{includeDelimiter && (
<System.P2 as="span" color="textGray">
{" "}
/{" "}
</System.P2>
)}
<button css={STYLES_BREADCRUMB_BUTTON} {...props}>
<System.P2 color={color}>{children}</System.P2>
</button>
</>
);
}
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 (
<div {...props}>
<Show when={!isCurrentViewInitial}>
<Item onClick={resetFilterState} color="textGray">
All
</Item>
<Item
includeDelimiter
color={filterState.subview ? "textGray" : "textBlack"}
onClick={changeFilterToBrowerView}
>
{Strings.capitalize(filterState.view)}
</Item>
</Show>
<Show when={filterState.subview}>
<Item includeDelimiter>{Strings.capitalize(filterState.subview)}</Item>
</Show>
</div>
);
}

View File

@ -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 (
<div css={STYLES_DATAVIEWER_WRAPPER} {...props}>
{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>
);
}

View File

@ -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 }) => (
<li>
<Typography.P2
as="button"
css={[STYLES_FILTER_BUTTON, isSelected && STYLES_FILTER_BUTTON_HIGHLIGHTED]}
{...props}
>
<Icon height={16} width={16} />
<span style={{ marginLeft: 6 }}>{children}</span>
</Typography.P2>
<Link {...props}>
<span as="span" css={[STYLES_FILTER_BUTTON, isSelected && STYLES_FILTER_BUTTON_HIGHLIGHTED]}>
<Icon height={16} width={16} style={{ flexShrink: 0 }} />
<Typography.P2 as="span" nbrOflines={1} style={{ marginLeft: 6 }}>
{children}
</Typography.P2>
</span>
</Link>
</li>
);
const FilterSection = ({ title, children, ...props }) => (
<div {...props}>
{title && (
<Typography.H6 style={{ paddingLeft: 8, paddingBottom: 4 }} color="textGray">
<Typography.H6 style={{ paddingLeft: 8, marginBottom: 4 }} color="textGray">
{title}
</Typography.H6>
)}
@ -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 */}
<FilterSection>
<FilterButton
href="/_/data"
isSelected={isSelected}
onAction={onAction}
Icon={SVG.Clock}
isSelected={currentFilterType === filters.library}
onClick={resetFilterState}
onClick={hidePopup}
>
My Library
</FilterButton>
</FilterSection>
<FilterSection title="Connected" style={{ marginTop: 16 }}>
<FilterButton Icon={SVG.Layout} onClick={goToBrowserView}>
Browser
</FilterButton>
</FilterSection>
<FilterSection style={{ marginTop: 16 }} title="Types">
<FilterButton
Icon={SVG.Image}
isSelected={currentFilterType === filters.images}
onClick={() => changeFilter({ type: filters.images })}
>
Images
</FilterButton>
<FilterButton
Icon={SVG.Radio}
isSelected={currentFilterType === filters.audios}
onClick={() => changeFilter({ type: filters.audios })}
>
Audios
</FilterButton>
<FilterButton
Icon={SVG.Video}
isSelected={currentFilterType === filters.videos}
onClick={() => changeFilter({ type: filters.videos })}
>
Videos
</FilterButton>
<FilterButton
Icon={SVG.FileText}
isSelected={currentFilterType === filters.documents}
onClick={() => changeFilter({ type: filters.documents })}
>
Documents
</FilterButton>
</FilterSection>
</>
);
}
/* -------------------------------------------------------------------------------------------------
* Browser Filters
* -----------------------------------------------------------------------------------------------*/
function Browser({ filters, goToSavedSubview }) {
const [{ filterState }] = useFilterContext();
const currentFilterType = filterState.type;
function Tags({ viewer, data, onAction, ...props }) {
const [, { hidePopup }] = useFilterContext();
return (
<FilterSection>
<FilterButton Icon={SVG.Clock} isSelected={currentFilterType === filters.all}>
All
</FilterButton>
<FilterButton disabled Icon={SVG.Clock}>
History
</FilterButton>
<FilterButton disabled Icon={SVG.Bookmark}>
Bookmarks
</FilterButton>
<FilterButton Icon={SVG.FilePlus} onClick={goToSavedSubview}>
Saved
</FilterButton>
<FilterSection title="Tags" {...props}>
{viewer.slates.map((slate) => (
<FilterButton
key={slate.id}
href={`/$/slate/${slate.id}`}
isSelected={slate.id === data?.id}
onAction={onAction}
Icon={slate.isPublic ? SVG.Hash : SVG.SecurityLock}
onClick={hidePopup}
>
{slate.slatename}
</FilterButton>
))}
</FilterSection>
);
}
const BrowserSaved = ({ filters }) => {
const [{ filterState }, { setFilterType }] = useFilterContext();
const currentFilterType = filterState.type;
const changeSavedFilterType = (type) =>
setFilterType({ view: filterState.view, subview: filterState.subview, type });
return (
<FilterSection>
<FilterButton
Icon={SVG.Clock}
isSelected={currentFilterType === filters.all}
onClick={() => changeSavedFilterType(filters.all)}
>
All
</FilterButton>
<FilterButton
Icon={SVG.Twitter}
isSelected={currentFilterType === filters.twitter}
onClick={() => changeSavedFilterType(filters.twitter)}
>
Twitter
</FilterButton>
<FilterButton
Icon={SVG.Youtube}
isSelected={currentFilterType === filters.youtube}
onClick={() => changeSavedFilterType(filters.youtube)}
>
Youtube
</FilterButton>
<FilterButton
Icon={SVG.Github}
isSelected={currentFilterType === filters.github}
onClick={() => changeSavedFilterType(filters.github)}
>
Github
</FilterButton>
<FilterButton
Icon={SVG.Twitch}
isSelected={currentFilterType === filters.twitch}
onClick={() => changeSavedFilterType(filters.twitch)}
>
Twitch
</FilterButton>
<FilterButton
Icon={SVG.Instagram}
isSelected={currentFilterType === filters.instagram}
onClick={() => changeSavedFilterType(filters.instagram)}
>
Instagram
</FilterButton>
</FilterSection>
);
};
export { Initial, Browser, BrowserSaved };
export { Library, Tags };

View File

@ -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(
<>
<Divider height="0.5px" />
<Divider height="0.5px" color="borderGrayLight" css={Styles.MOBILE_HIDDEN} />
<div css={STYLES_NAVBAR}>{children}</div>
<Divider height="0.5px" color="borderGrayLight" css={Styles.MOBILE_ONLY} />
</>,
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 <div ref={setFilterElement}>{children}</div>;
return (
<div ref={setFilterElement}>
{children}
<div css={STYLES_NAVBAR_BACKGROUND} />
</div>
);
}

View File

@ -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 (
<button
onClick={togglePopup}
css={[Styles.BUTTON_RESET, Styles.HORIZONTAL_CONTAINER_CENTERED]}
{...props}
>
{children}
<motion.div initial={null} animate={{ rotateX: popupState.isVisible ? 0 : 180 }}>
<SVG.ChevronUp style={{ display: "block" }} />
</motion.div>
</button>
);
}
/* -------------------------------------------------------------------------------------------------
* 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 (
<div css={[STYLES_SIDEBAR_FILTER_WRAPPER, css]} {...props}>
<Filters.Library page={page} onAction={onAction} />
<Filters.Tags
viewer={viewer}
data={data}
page={page}
onAction={onAction}
style={{ marginTop: 12 }}
/>
</div>
);
}

View File

@ -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 <UploadContext.Provider value={contextValue}>{children}</UploadContext.Provider>;
@ -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 }];
};

View File

@ -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 (
<button
onClick={toggleSidebar}
css={[
STYLES_SIDEBAR_TRIGGER,
(theme) =>
css({
backgroundColor: isSidebarVisible ? theme.semantic.bgGrayLight : "none",
color: isSidebarVisible ? theme.semantic.textBlack : theme.semantic.textGray,
}),
]}
css={[STYLES_SIDEBAR_TRIGGER, css]}
style={{
backgroundColor: sidebarState.isVisible ? Constants.semantic.bgGrayLight : "unset",
color: sidebarState.isVisible ? Constants.semantic.textBlack : Constants.semantic.textGray,
}}
>
<SVG.Sidebar style={{ display: "block" }} />
</button>
@ -46,24 +42,13 @@ export function SidebarTrigger() {
* Sidebar
* -----------------------------------------------------------------------------------------------*/
export function Sidebar() {
const [{ isSidebarVisible }] = useFilterContext();
return (
<Show when={isSidebarVisible}>
<div css={STYLES_SIDEBAR_FILTER_WRAPPER}>
<SidebarContent />
</div>
</Show>
);
}
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 <Filters.BrowserSaved filters={filters} />;
}
return (
<Filters.Browser
filters={filters}
goToSavedSubview={() => changeFilterSubview(FILTER_SUBVIEWS_IDS.browser.saved)}
/>
);
}
if (!sidebarState.isVisible || isMobile) return null;
return (
<Filters.Initial
filters={filters}
goToBrowserView={() => changeFilterView(FILTER_VIEWS_IDS.browser)}
/>
<div css={STYLES_SIDEBAR_FILTER_WRAPPER}>
<Filters.Library page={page} onAction={onAction} />
<Filters.Tags
page={page}
onAction={onAction}
data={data}
viewer={viewer}
style={{ marginTop: 12 }}
/>
</div>
);
}

View File

@ -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 <System.H5 color="textBlack">All</System.H5>;
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 (
<System.H5 title={title} style={{ maxWidth: 400 }} as="p" nbrOflines={1} color="textBlack">
{title}
</System.H5>
);
}
/* -------------------------------------------------------------------------------------------------
* Actions
* -----------------------------------------------------------------------------------------------*/
function Actions() {
return <div />;
}
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 ? (
<Search.Content viewer={viewer} page={page} onAction={onAction} />
) : (
children
);
}
return (
<>
<Provider>
<NavbarPortal>
<div css={STYLES_FILTER_POPUP_WRAPPER}>
<Popup viewer={viewer} onAction={onAction} data={data} page={page} />
</div>
<div css={Styles.CONTAINER_CENTERED}>
<div css={Styles.MOBILE_HIDDEN}>
<SidebarTrigger />
</div>
<PopupTrigger isMobile={isMobile} style={{ marginLeft: 2 }}>
<span css={Styles.MOBILE_ONLY} style={{ marginRight: 8 }}>
<Title page={page} data={data} />
</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>
</>
);
}

View File

@ -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);
});
}, []);

View File

@ -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>
);

View File

@ -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) => {

View File

@ -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&apos;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 };

View File

@ -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,
};
});

View File

@ -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)) {

View File

@ -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}
/>
);

View File

@ -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>
);

View File

@ -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"

View File

@ -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);
};