mirror of
https://github.com/filecoin-project/slate.git
synced 2024-08-17 17:50:50 +03:00
Merge pull request #973 from filecoin-project/@aminejv/filter-updates
Add tags to the filter sidebar
This commit is contained in:
commit
9106c47a06
@ -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 = {
|
||||
|
@ -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];
|
||||
};
|
@ -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];
|
||||
};
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
|
@ -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}`}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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 };
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
86
components/core/Filter/Popup.js
Normal file
86
components/core/Filter/Popup.js
Normal 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>
|
||||
);
|
||||
}
|
@ -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 }];
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
}, []);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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) => {
|
||||
|
231
components/core/Search/index.js
Normal file
231
components/core/Search/index.js
Normal 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'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 };
|
48
components/core/Search/store.js
Normal file
48
components/core/Search/store.js
Normal 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,
|
||||
};
|
||||
});
|
@ -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)) {
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
};
|
Loading…
Reference in New Issue
Block a user