Update the library layout to be closer to the design spec
This commit is contained in:
48
packages/web/components/elements/images/GridSelectorIcon.tsx
Normal file
48
packages/web/components/elements/images/GridSelectorIcon.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { config } from '../../tokens/stitches.config'
|
||||
|
||||
export type GridSelectorIconProps = {
|
||||
color?: string
|
||||
}
|
||||
|
||||
export function GridSelectorIcon(props: GridSelectorIconProps): JSX.Element {
|
||||
const fillColor = props.color || config.theme.colors.graySolid
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_1646_7379)">
|
||||
<path
|
||||
d="M8.32487 0.170105H1.6582C0.967847 0.170105 0.408203 0.729749 0.408203 1.4201V8.08677C0.408203 8.77713 0.967847 9.33677 1.6582 9.33677H8.32487C9.01523 9.33677 9.57487 8.77713 9.57487 8.08677V1.4201C9.57487 0.729749 9.01523 0.170105 8.32487 0.170105Z"
|
||||
fill="#FFEA9F"
|
||||
/>
|
||||
<path
|
||||
d="M19.1582 0.170105H12.4915C11.8012 0.170105 11.2415 0.729749 11.2415 1.4201V8.08677C11.2415 8.77713 11.8012 9.33677 12.4915 9.33677H19.1582C19.8486 9.33677 20.4082 8.77713 20.4082 8.08677V1.4201C20.4082 0.729749 19.8486 0.170105 19.1582 0.170105Z"
|
||||
fill="#FFEA9F"
|
||||
/>
|
||||
<path
|
||||
d="M8.32487 11.0034H1.6582C0.967847 11.0034 0.408203 11.5631 0.408203 12.2534V18.9201C0.408203 19.6105 0.967847 20.1701 1.6582 20.1701H8.32487C9.01523 20.1701 9.57487 19.6105 9.57487 18.9201V12.2534C9.57487 11.5631 9.01523 11.0034 8.32487 11.0034Z"
|
||||
fill="#FFEA9F"
|
||||
/>
|
||||
<path
|
||||
d="M19.1582 11.0034H12.4915C11.8012 11.0034 11.2415 11.5631 11.2415 12.2534V18.9201C11.2415 19.6105 11.8012 20.1701 12.4915 20.1701H19.1582C19.8486 20.1701 20.4082 19.6105 20.4082 18.9201V12.2534C20.4082 11.5631 19.8486 11.0034 19.1582 11.0034Z"
|
||||
fill="#FFEA9F"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1646_7379">
|
||||
<rect
|
||||
width="20"
|
||||
height="20"
|
||||
fill="white"
|
||||
transform="translate(0.408203 0.172607)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
44
packages/web/components/elements/images/ListSelectorIcon.tsx
Normal file
44
packages/web/components/elements/images/ListSelectorIcon.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { config } from '../../tokens/stitches.config'
|
||||
|
||||
export type ListSelectorIconProps = {
|
||||
color?: string
|
||||
}
|
||||
|
||||
export function ListSelectorIcon(props: ListSelectorIconProps): JSX.Element {
|
||||
const fillColor = props.color || config.theme.colors.graySolid
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_1646_7375)">
|
||||
<path
|
||||
d="M19.1582 0.795105H1.6582C0.967847 0.795105 0.408203 1.35475 0.408203 2.0451V4.12844C0.408203 4.81879 0.967847 5.37844 1.6582 5.37844H19.1582C19.8486 5.37844 20.4082 4.81879 20.4082 4.12844V2.0451C20.4082 1.35475 19.8486 0.795105 19.1582 0.795105Z"
|
||||
fill="#6A6968"
|
||||
/>
|
||||
<path
|
||||
d="M19.1582 7.87845H1.6582C0.967847 7.87845 0.408203 8.43809 0.408203 9.12845V11.2118C0.408203 11.9021 0.967847 12.4618 1.6582 12.4618H19.1582C19.8486 12.4618 20.4082 11.9021 20.4082 11.2118V9.12845C20.4082 8.43809 19.8486 7.87845 19.1582 7.87845Z"
|
||||
fill="#6A6968"
|
||||
/>
|
||||
<path
|
||||
d="M19.1582 14.9618H1.6582C0.967847 14.9618 0.408203 15.5214 0.408203 16.2118V18.2951C0.408203 18.9855 0.967847 19.5451 1.6582 19.5451H19.1582C19.8486 19.5451 20.4082 18.9855 20.4082 18.2951V16.2118C20.4082 15.5214 19.8486 14.9618 19.1582 14.9618Z"
|
||||
fill="#6A6968"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1646_7375">
|
||||
<rect
|
||||
width="20"
|
||||
height="20"
|
||||
fill="white"
|
||||
transform="translate(0.408203 0.172607)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
61
packages/web/components/elements/images/OmnivoreFullLogo.tsx
Normal file
61
packages/web/components/elements/images/OmnivoreFullLogo.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { config } from '../../tokens/stitches.config'
|
||||
|
||||
export type OmnivoreFullLogoProps = {
|
||||
color?: string
|
||||
href?: string
|
||||
showTitle?: boolean
|
||||
}
|
||||
|
||||
export function OmnivoreFullLogo(props: OmnivoreFullLogoProps): JSX.Element {
|
||||
const fillColor = props.color || config.theme.colors.graySolid
|
||||
const href = props.href || '/home'
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="129"
|
||||
height="26"
|
||||
viewBox="0 0 129 26"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M43.4131 13.2862C43.4131 9.75626 41.2214 7.74365 38.3606 7.74365C35.4839 7.74365 33.308 9.75626 33.308 13.2862C33.308 16.8004 35.4839 18.8288 38.3606 18.8288C41.2214 18.8288 43.4131 16.8162 43.4131 13.2862ZM41.1002 13.2862C41.1002 15.5728 40.0149 16.8109 38.3606 16.8109C36.701 16.8109 35.6209 15.5728 35.6209 13.2862C35.6209 10.9996 36.701 9.76153 38.3606 9.76153C40.0149 9.76153 41.1002 10.9996 41.1002 13.2862Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M47.3285 7.89117V18.6813H49.5413V11.6319H49.6309L52.4232 18.6286H53.9301L56.7224 11.6582H56.812V18.6813H59.0248V7.89117H56.2114L53.2399 15.1408H53.1134L50.1419 7.89117H47.3285Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M72.1549 7.89117H69.8842V14.6771H69.7893L65.1319 7.89117H63.1298V18.6813H65.4111V11.89H65.4901L70.1845 18.6813H72.1549V7.89117Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M78.5465 7.89117H76.2652V18.6813H78.5465V7.89117Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M84.5983 7.89117H82.0641L85.789 18.6813H88.7289L92.4485 7.89117H89.9196L87.3063 16.0891H87.2062L84.5983 7.89117Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M105.28 13.2862C105.28 9.75626 103.088 7.74365 100.227 7.74365C97.3504 7.74365 95.1745 9.75626 95.1745 13.2862C95.1745 16.8004 97.3504 18.8288 100.227 18.8288C103.088 18.8288 105.28 16.8162 105.28 13.2862ZM102.967 13.2862C102.967 15.5728 101.881 16.8109 100.227 16.8109C98.5675 16.8109 97.4874 15.5728 97.4874 13.2862C97.4874 10.9996 98.5675 9.76153 100.227 9.76153C101.881 9.76153 102.967 10.9996 102.967 13.2862Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M109.195 18.6813H111.476V14.8563H113.141L115.185 18.6813H117.704L115.412 14.4875C116.64 13.9606 117.319 12.8911 117.319 11.4159C117.319 9.27155 115.902 7.89117 113.452 7.89117H109.195V18.6813ZM111.476 13.0228V9.75626H113.015C114.332 9.75626 114.969 10.3411 114.969 11.4159C114.969 12.4854 114.332 13.0228 113.025 13.0228H111.476Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M121.157 18.6813H128.449V16.8004H123.438V14.224H128.053V12.3431H123.438V9.77206H128.427V7.89117H121.157V18.6813Z"
|
||||
fill={fillColor}
|
||||
/>
|
||||
<path
|
||||
d="M8.42285 17.9061V10.5447C8.42285 9.91527 9.16173 9.55951 9.65432 9.99737L11.9257 13.3087C12.3909 13.6918 13.0477 13.6918 13.5129 13.3087L15.7296 10.0247C16.2222 9.61424 16.961 9.94263 16.961 10.5721V14.458C16.961 16.3463 18.2199 17.8788 20.1081 17.8788H20.1629C21.7775 17.8788 23.1731 16.7841 23.5563 15.2243C23.7478 14.4033 23.912 13.5549 23.912 12.8982C23.8847 6.46715 18.4388 1.596 11.9257 2.03385C6.39776 2.41698 1.9371 6.87764 1.55397 12.4056C1.11612 18.9187 6.26093 24.3645 12.7193 24.3645"
|
||||
stroke={fillColor}
|
||||
stroke-width="2.18182"
|
||||
stroke-miterlimit="10"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@ -42,6 +42,7 @@ export function OmnivoreLogoIcon(props: OmnivoreLogoProps): JSX.Element {
|
||||
export type OmnivoreNameLogoProps = {
|
||||
color?: string
|
||||
href?: string
|
||||
showTitle?: boolean
|
||||
}
|
||||
|
||||
export function OmnivoreNameLogo(props: OmnivoreNameLogoProps): JSX.Element {
|
||||
@ -50,9 +51,22 @@ export function OmnivoreNameLogo(props: OmnivoreNameLogoProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<Link passHref href={href}>
|
||||
<a style={{ textDecoration: 'none', display: 'flex', alignItems: 'center' }}>
|
||||
<a
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<OmnivoreLogoIcon size={27} strokeColor={fillColor}></OmnivoreLogoIcon>
|
||||
{/* <StyledText style="logoTitle" css={{ color: fillColor, paddingLeft: '12px' }}>Omnivore</StyledText> */}
|
||||
{props.showTitle && (
|
||||
<StyledText
|
||||
style="logoTitle"
|
||||
css={{ color: fillColor, paddingLeft: '12px' }}
|
||||
>
|
||||
Omnivore
|
||||
</StyledText>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@ -1,11 +1,6 @@
|
||||
import { PageMetaData, PageMetaDataProps } from '../patterns/PageMetaData'
|
||||
import { Box } from '../elements/LayoutPrimitives'
|
||||
import {
|
||||
ReactNode,
|
||||
MutableRefObject,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { ReactNode, MutableRefObject, useEffect, useState } from 'react'
|
||||
import { PrimaryHeader } from './../patterns/PrimaryHeader'
|
||||
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import { navigationCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
@ -61,15 +56,17 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
|
||||
{props.pageMetaDataProps ? (
|
||||
<PageMetaData {...props.pageMetaDataProps} />
|
||||
) : null}
|
||||
<Box css={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
bg: 'transparent',
|
||||
'@smDown': {
|
||||
bg: '$grayBase',
|
||||
}
|
||||
}}>
|
||||
<PrimaryHeader
|
||||
<Box
|
||||
css={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
bg: 'transparent',
|
||||
'@smDown': {
|
||||
bg: '$grayBase',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* <PrimaryHeader
|
||||
user={viewerData?.me}
|
||||
hideHeader={props.hideHeader}
|
||||
userInitials={viewerData?.me?.name.charAt(0) ?? ''}
|
||||
@ -79,7 +76,7 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
|
||||
alwaysDisplayToolbar={props.alwaysDisplayToolbar}
|
||||
setShowLogoutConfirmation={setShowLogoutConfirmation}
|
||||
setShowKeyboardCommandsModal={setShowKeyboardCommandsModal}
|
||||
/>
|
||||
/> */}
|
||||
<Box
|
||||
css={{
|
||||
height: '100%',
|
||||
@ -87,12 +84,12 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
|
||||
bg: '$grayBase',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
height: '48px',
|
||||
bg: '$grayBase',
|
||||
}}
|
||||
></Box>
|
||||
{/* <Box
|
||||
css={{
|
||||
height: '48px',
|
||||
bg: '$grayBase',
|
||||
}}
|
||||
></Box> */}
|
||||
{props.children}
|
||||
{showLogoutConfirmation ? (
|
||||
<ConfirmationModal
|
||||
|
||||
@ -55,6 +55,8 @@ import {
|
||||
import axios from 'axios'
|
||||
import { uploadFileRequestMutation } from '../../../lib/networking/mutations/uploadFileMutation'
|
||||
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
|
||||
import { LibraryHeader } from './LibraryHeader'
|
||||
import { LibraryFilterMenu } from './LibraryFilterMenu'
|
||||
|
||||
export type LayoutType = 'LIST_LAYOUT' | 'GRID_LAYOUT'
|
||||
|
||||
@ -793,21 +795,42 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<VStack
|
||||
alignment="center"
|
||||
<VStack css={{ width: '100%', height: '100%' }}>
|
||||
<LibraryHeader
|
||||
searchTerm={props.searchTerm}
|
||||
applySearchQuery={props.applySearchQuery}
|
||||
/>
|
||||
<Box
|
||||
css={{
|
||||
px: '$3',
|
||||
width: '100%',
|
||||
'@smDown': {
|
||||
px: '$2',
|
||||
},
|
||||
height: '105px',
|
||||
bg: '$grayBase',
|
||||
}}
|
||||
>
|
||||
<Toaster />
|
||||
></Box>
|
||||
<HStack css={{ width: '100%', height: '100%' }}>
|
||||
<LibraryFilterMenu />
|
||||
<Box
|
||||
css={{
|
||||
width: '233px',
|
||||
minWidth: '233px',
|
||||
height: '100%',
|
||||
bg: '$grayBase',
|
||||
}}
|
||||
></Box>
|
||||
|
||||
{props.isValidating && props.items.length == 0 && <TopBarProgress />}
|
||||
<HStack alignment="center" distribution="start" css={{ width: '100%' }}>
|
||||
<VStack
|
||||
alignment="center"
|
||||
css={{
|
||||
px: '$3',
|
||||
width: '100%',
|
||||
'@smDown': {
|
||||
px: '$2',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Toaster />
|
||||
|
||||
{props.isValidating && props.items.length == 0 && <TopBarProgress />}
|
||||
{/* <HStack alignment="center" distribution="start" css={{ width: '100%' }}>
|
||||
<StyledText
|
||||
style="subHeadline"
|
||||
css={{
|
||||
@ -856,236 +879,243 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
||||
<LibrarySearchBar
|
||||
searchTerm={props.searchTerm}
|
||||
applySearchQuery={props.applySearchQuery}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
{viewerData?.me && (
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '44px',
|
||||
marginTop: '16px',
|
||||
gap: '8px',
|
||||
flexDirection: 'row',
|
||||
overflowY: 'scroll',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
{/* {viewerData?.me && (
|
||||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '44px',
|
||||
marginTop: '16px',
|
||||
gap: '8px',
|
||||
flexDirection: 'row',
|
||||
overflowY: 'scroll',
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.keys(SAVED_SEARCHES).map((key) => {
|
||||
const isInboxTerm = (term: string) => {
|
||||
return !term || term === 'in:inbox'
|
||||
}
|
||||
|
||||
const searchQuery = SAVED_SEARCHES[key]
|
||||
const style =
|
||||
searchQuery === props.searchTerm ||
|
||||
(!props.searchTerm && isInboxTerm(searchQuery))
|
||||
? 'ctaDarkYellow'
|
||||
: 'ctaLightGray'
|
||||
return (
|
||||
<Button
|
||||
key={key}
|
||||
style={style}
|
||||
onClick={() => {
|
||||
props.applySearchQuery(searchQuery)
|
||||
}}
|
||||
css={{
|
||||
p: '10px 12px',
|
||||
height: '37.5px',
|
||||
borderRadius: '6px',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)} */}
|
||||
<Dropzone
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={() => {
|
||||
setInDragOperation(true)
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
setInDragOperation(false)
|
||||
}}
|
||||
preventDropOnDocument={true}
|
||||
noClick={true}
|
||||
accept={{
|
||||
'application/pdf': ['.pdf'],
|
||||
}}
|
||||
>
|
||||
{Object.keys(SAVED_SEARCHES).map((key) => {
|
||||
const isInboxTerm = (term: string) => {
|
||||
return !term || term === 'in:inbox'
|
||||
}
|
||||
|
||||
const searchQuery = SAVED_SEARCHES[key]
|
||||
const style =
|
||||
searchQuery === props.searchTerm ||
|
||||
(!props.searchTerm && isInboxTerm(searchQuery))
|
||||
? 'ctaDarkYellow'
|
||||
: 'ctaLightGray'
|
||||
return (
|
||||
<Button
|
||||
key={key}
|
||||
style={style}
|
||||
onClick={() => {
|
||||
props.applySearchQuery(searchQuery)
|
||||
}}
|
||||
css={{
|
||||
p: '10px 12px',
|
||||
height: '37.5px',
|
||||
borderRadius: '6px',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
<Dropzone
|
||||
onDrop={handleDrop}
|
||||
onDragEnter={() => {
|
||||
setInDragOperation(true)
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
setInDragOperation(false)
|
||||
}}
|
||||
preventDropOnDocument={true}
|
||||
noClick={true}
|
||||
accept={{
|
||||
'application/pdf': ['.pdf'],
|
||||
}}
|
||||
>
|
||||
{({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => (
|
||||
<div
|
||||
{...getRootProps({ className: 'dropzone' })}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
{inDragOperation && uploadingFiles.length < 1 && (
|
||||
<DragnDropContainer>
|
||||
<DragnDropStyle>
|
||||
<Box
|
||||
css={{
|
||||
color: '$utilityTextDefault',
|
||||
fontWeight: '800',
|
||||
fontSize: '$4',
|
||||
}}
|
||||
>
|
||||
Drop PDF document to to upload and add to your library
|
||||
</Box>
|
||||
</DragnDropStyle>
|
||||
</DragnDropContainer>
|
||||
)}
|
||||
{uploadingFiles.length > 0 && (
|
||||
<DragnDropContainer>
|
||||
<DragnDropStyle>
|
||||
<Box
|
||||
css={{
|
||||
color: '$utilityTextDefault',
|
||||
fontWeight: '800',
|
||||
fontSize: '$4',
|
||||
width: '80%',
|
||||
}}
|
||||
>
|
||||
<Progress.Root
|
||||
className="ProgressRoot"
|
||||
value={uploadProgress}
|
||||
>
|
||||
<Progress.Indicator
|
||||
className="ProgressIndicator"
|
||||
style={{
|
||||
transform: `translateX(-${100 - uploadProgress}%)`,
|
||||
}}
|
||||
/>
|
||||
</Progress.Root>
|
||||
<StyledText
|
||||
style="boldText"
|
||||
{({
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
acceptedFiles,
|
||||
fileRejections,
|
||||
}) => (
|
||||
<div
|
||||
{...getRootProps({ className: 'dropzone' })}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
{inDragOperation && uploadingFiles.length < 1 && (
|
||||
<DragnDropContainer>
|
||||
<DragnDropStyle>
|
||||
<Box
|
||||
css={{
|
||||
color: theme.colors.omnivoreGray.toString(),
|
||||
color: '$utilityTextDefault',
|
||||
fontWeight: '800',
|
||||
fontSize: '$4',
|
||||
}}
|
||||
>
|
||||
Uploading file
|
||||
</StyledText>
|
||||
</Box>
|
||||
</DragnDropStyle>
|
||||
</DragnDropContainer>
|
||||
)}
|
||||
<input {...getInputProps()} />
|
||||
{!props.isValidating && props.items.length == 0 ? (
|
||||
<EmptyLibrary
|
||||
onAddLinkClicked={() => {
|
||||
props.setShowAddLinkModal(true)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
ref={props.gridContainerRef}
|
||||
css={{
|
||||
py: '$3',
|
||||
display: 'grid',
|
||||
width: '100%',
|
||||
gridAutoRows: 'auto',
|
||||
borderRadius: '8px',
|
||||
gridGap: layout == 'LIST_LAYOUT' ? '0' : '$3',
|
||||
marginTop: layout == 'LIST_LAYOUT' ? '21px' : '0',
|
||||
marginBottom: '0px',
|
||||
paddingTop: layout == 'LIST_LAYOUT' ? '0' : '21px',
|
||||
paddingBottom: layout == 'LIST_LAYOUT' ? '0px' : '21px',
|
||||
overflow: 'hidden',
|
||||
'@smDown': {
|
||||
border: 'unset',
|
||||
width: layout == 'LIST_LAYOUT' ? '100vw' : undefined,
|
||||
margin:
|
||||
layout == 'LIST_LAYOUT' ? '16px -16px' : undefined,
|
||||
borderRadius: layout == 'LIST_LAYOUT' ? 0 : undefined,
|
||||
},
|
||||
'@md': {
|
||||
gridTemplateColumns:
|
||||
layout == 'LIST_LAYOUT' ? 'none' : '1fr 1fr',
|
||||
},
|
||||
'@lg': {
|
||||
gridTemplateColumns:
|
||||
layout == 'LIST_LAYOUT' ? 'none' : 'repeat(3, 1fr)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.items.map((linkedItem) => (
|
||||
<Box
|
||||
className="linkedItemCard"
|
||||
data-testid="linkedItemCard"
|
||||
id={linkedItem.node.id}
|
||||
tabIndex={0}
|
||||
key={linkedItem.node.id}
|
||||
css={{
|
||||
width: '100%',
|
||||
'&> div': {
|
||||
bg: '$grayBg',
|
||||
},
|
||||
'&:focus': {
|
||||
'> div': {
|
||||
bg: '$grayBgActive',
|
||||
},
|
||||
},
|
||||
'&:hover': {
|
||||
'> div': {
|
||||
bg: '$grayBgActive',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{viewerData?.me && (
|
||||
<LinkedItemCard
|
||||
layout={layout}
|
||||
item={linkedItem.node}
|
||||
viewer={viewerData.me}
|
||||
handleAction={(action: LinkedItemCardAction) => {
|
||||
if (action === 'delete') {
|
||||
setShowRemoveLinkConfirmation(true)
|
||||
props.setLinkToRemove(linkedItem)
|
||||
} else if (action === 'editTitle') {
|
||||
props.setShowEditTitleModal(true)
|
||||
props.setLinkToEdit(linkedItem)
|
||||
} else if (action == 'unsubscribe') {
|
||||
setShowUnsubscribeConfirmation(true)
|
||||
props.setLinkToUnsubscribe(linkedItem)
|
||||
} else {
|
||||
props.actionHandler(action, linkedItem)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<HStack
|
||||
distribution="center"
|
||||
css={{ width: '100%', mt: '$2', mb: '$4' }}
|
||||
>
|
||||
{props.hasMore ? (
|
||||
<Button
|
||||
style="ctaGray"
|
||||
css={{
|
||||
cursor: props.isValidating ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
onClick={props.loadMore}
|
||||
disabled={props.isValidating}
|
||||
>
|
||||
{props.isValidating ? 'Loading' : 'Load More'}
|
||||
</Button>
|
||||
) : (
|
||||
<StyledText style="caption"></StyledText>
|
||||
Drop PDF document to to upload and add to your library
|
||||
</Box>
|
||||
</DragnDropStyle>
|
||||
</DragnDropContainer>
|
||||
)}
|
||||
</HStack>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</VStack>
|
||||
{/* Temporary code */}
|
||||
{/* <div>
|
||||
{uploadingFiles.length > 0 && (
|
||||
<DragnDropContainer>
|
||||
<DragnDropStyle>
|
||||
<Box
|
||||
css={{
|
||||
color: '$utilityTextDefault',
|
||||
fontWeight: '800',
|
||||
fontSize: '$4',
|
||||
width: '80%',
|
||||
}}
|
||||
>
|
||||
<Progress.Root
|
||||
className="ProgressRoot"
|
||||
value={uploadProgress}
|
||||
>
|
||||
<Progress.Indicator
|
||||
className="ProgressIndicator"
|
||||
style={{
|
||||
transform: `translateX(-${
|
||||
100 - uploadProgress
|
||||
}%)`,
|
||||
}}
|
||||
/>
|
||||
</Progress.Root>
|
||||
<StyledText
|
||||
style="boldText"
|
||||
css={{
|
||||
color: theme.colors.omnivoreGray.toString(),
|
||||
}}
|
||||
>
|
||||
Uploading file
|
||||
</StyledText>
|
||||
</Box>
|
||||
</DragnDropStyle>
|
||||
</DragnDropContainer>
|
||||
)}
|
||||
<input {...getInputProps()} />
|
||||
{!props.isValidating && props.items.length == 0 ? (
|
||||
<EmptyLibrary
|
||||
onAddLinkClicked={() => {
|
||||
props.setShowAddLinkModal(true)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
ref={props.gridContainerRef}
|
||||
css={{
|
||||
py: '$3',
|
||||
display: 'grid',
|
||||
width: '100%',
|
||||
gridAutoRows: 'auto',
|
||||
borderRadius: '8px',
|
||||
gridGap: layout == 'LIST_LAYOUT' ? '0' : '$3',
|
||||
marginTop: layout == 'LIST_LAYOUT' ? '21px' : '0',
|
||||
marginBottom: '0px',
|
||||
paddingTop: layout == 'LIST_LAYOUT' ? '0' : '21px',
|
||||
paddingBottom: layout == 'LIST_LAYOUT' ? '0px' : '21px',
|
||||
overflow: 'hidden',
|
||||
'@smDown': {
|
||||
border: 'unset',
|
||||
width: layout == 'LIST_LAYOUT' ? '100vw' : undefined,
|
||||
margin:
|
||||
layout == 'LIST_LAYOUT' ? '16px -16px' : undefined,
|
||||
borderRadius: layout == 'LIST_LAYOUT' ? 0 : undefined,
|
||||
},
|
||||
'@md': {
|
||||
gridTemplateColumns:
|
||||
layout == 'LIST_LAYOUT' ? 'none' : '1fr 1fr',
|
||||
},
|
||||
'@lg': {
|
||||
gridTemplateColumns:
|
||||
layout == 'LIST_LAYOUT' ? 'none' : 'repeat(3, 1fr)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.items.map((linkedItem) => (
|
||||
<Box
|
||||
className="linkedItemCard"
|
||||
data-testid="linkedItemCard"
|
||||
id={linkedItem.node.id}
|
||||
tabIndex={0}
|
||||
key={linkedItem.node.id}
|
||||
css={{
|
||||
width: '100%',
|
||||
'&> div': {
|
||||
bg: '$grayBg',
|
||||
},
|
||||
'&:focus': {
|
||||
'> div': {
|
||||
bg: '$grayBgActive',
|
||||
},
|
||||
},
|
||||
'&:hover': {
|
||||
'> div': {
|
||||
bg: '$grayBgActive',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{viewerData?.me && (
|
||||
<LinkedItemCard
|
||||
layout={layout}
|
||||
item={linkedItem.node}
|
||||
viewer={viewerData.me}
|
||||
handleAction={(action: LinkedItemCardAction) => {
|
||||
if (action === 'delete') {
|
||||
setShowRemoveLinkConfirmation(true)
|
||||
props.setLinkToRemove(linkedItem)
|
||||
} else if (action === 'editTitle') {
|
||||
props.setShowEditTitleModal(true)
|
||||
props.setLinkToEdit(linkedItem)
|
||||
} else if (action == 'unsubscribe') {
|
||||
setShowUnsubscribeConfirmation(true)
|
||||
props.setLinkToUnsubscribe(linkedItem)
|
||||
} else {
|
||||
props.actionHandler(action, linkedItem)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<HStack
|
||||
distribution="center"
|
||||
css={{ width: '100%', mt: '$2', mb: '$4' }}
|
||||
>
|
||||
{props.hasMore ? (
|
||||
<Button
|
||||
style="ctaGray"
|
||||
css={{
|
||||
cursor: props.isValidating ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
onClick={props.loadMore}
|
||||
disabled={props.isValidating}
|
||||
>
|
||||
{props.isValidating ? 'Loading' : 'Load More'}
|
||||
</Button>
|
||||
) : (
|
||||
<StyledText style="caption"></StyledText>
|
||||
)}
|
||||
</HStack>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</VStack>
|
||||
{/* Temporary code */}
|
||||
{/* <div>
|
||||
<strong>Files:</strong>
|
||||
<ul>
|
||||
{uploadingFiles.map((fileName) => (
|
||||
@ -1093,142 +1123,143 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
||||
))}
|
||||
</ul>
|
||||
</div> */}
|
||||
{/* Temporary code */}
|
||||
{props.showAddLinkModal && (
|
||||
<AddLinkModal onOpenChange={() => props.setShowAddLinkModal(false)} />
|
||||
)}
|
||||
{props.showEditTitleModal && (
|
||||
<EditTitleModal
|
||||
updateItem={(item: LibraryItem) =>
|
||||
props.actionHandler('update-item', item)
|
||||
}
|
||||
onOpenChange={() => props.setShowEditTitleModal(false)}
|
||||
item={props.linkToEdit as LibraryItem}
|
||||
/>
|
||||
)}
|
||||
{props.shareTarget && viewerData?.me?.profile.username && (
|
||||
<ShareArticleModal
|
||||
url={`${webBaseURL}${viewerData?.me?.profile.username}/${props.shareTarget.node.slug}/highlights?r=true`}
|
||||
title={props.shareTarget.node.title}
|
||||
imageURL={props.shareTarget.node.image}
|
||||
author={props.shareTarget.node.author}
|
||||
publishedAt={
|
||||
props.shareTarget.node.publishedAt ??
|
||||
props.shareTarget.node.createdAt
|
||||
}
|
||||
description={props.shareTarget.node.description}
|
||||
originalArticleUrl={props.shareTarget.node.originalArticleUrl}
|
||||
onOpenChange={() => {
|
||||
if (props.shareTarget) {
|
||||
const item = document.getElementById(props.shareTarget.node.id)
|
||||
if (item) {
|
||||
item.focus()
|
||||
{/* Temporary code */}
|
||||
{props.showAddLinkModal && (
|
||||
<AddLinkModal onOpenChange={() => props.setShowAddLinkModal(false)} />
|
||||
)}
|
||||
{props.showEditTitleModal && (
|
||||
<EditTitleModal
|
||||
updateItem={(item: LibraryItem) =>
|
||||
props.actionHandler('update-item', item)
|
||||
}
|
||||
onOpenChange={() => props.setShowEditTitleModal(false)}
|
||||
item={props.linkToEdit as LibraryItem}
|
||||
/>
|
||||
)}
|
||||
{props.shareTarget && viewerData?.me?.profile.username && (
|
||||
<ShareArticleModal
|
||||
url={`${webBaseURL}${viewerData?.me?.profile.username}/${props.shareTarget.node.slug}/highlights?r=true`}
|
||||
title={props.shareTarget.node.title}
|
||||
imageURL={props.shareTarget.node.image}
|
||||
author={props.shareTarget.node.author}
|
||||
publishedAt={
|
||||
props.shareTarget.node.publishedAt ??
|
||||
props.shareTarget.node.createdAt
|
||||
}
|
||||
description={props.shareTarget.node.description}
|
||||
originalArticleUrl={props.shareTarget.node.originalArticleUrl}
|
||||
onOpenChange={() => {
|
||||
if (props.shareTarget) {
|
||||
const item = document.getElementById(props.shareTarget.node.id)
|
||||
if (item) {
|
||||
item.focus()
|
||||
}
|
||||
props.setShareTarget(undefined)
|
||||
}
|
||||
props.setShareTarget(undefined)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{props.snoozeTarget && (
|
||||
<SnoozeLinkModal
|
||||
submit={(option: string, sendReminder: boolean, msg: string) => {
|
||||
if (!props.snoozeTarget) return
|
||||
createReminderMutation(
|
||||
props.snoozeTarget?.node.id,
|
||||
ReminderType.Tonight,
|
||||
true,
|
||||
sendReminder
|
||||
)
|
||||
.then(() => {
|
||||
return props.actionHandler('archive', props.snoozeTarget)
|
||||
})
|
||||
.then(() => {
|
||||
showSuccessToast(msg, { position: 'bottom-right' })
|
||||
})
|
||||
.catch((error) => {
|
||||
showErrorToast('There was an error snoozing your link.', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
})
|
||||
}}
|
||||
onOpenChange={() => {
|
||||
if (props.snoozeTarget) {
|
||||
const item = document.getElementById(props.snoozeTarget.node.id)
|
||||
if (item) {
|
||||
item.focus()
|
||||
}
|
||||
props.setSnoozeTarget(undefined)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showRemoveLinkConfirmation && (
|
||||
<ConfirmationModal
|
||||
richMessage={
|
||||
<VStack alignment="center" distribution="center">
|
||||
<StyledText style="modalTitle" css={{ margin: '0px 8px' }}>
|
||||
Are you sure you want to delete this item? All associated notes
|
||||
and highlights will be deleted.
|
||||
</StyledText>
|
||||
{props.linkToRemove?.node && viewerData?.me && (
|
||||
<Box
|
||||
css={{
|
||||
transform: 'scale(0.6)',
|
||||
opacity: 0.8,
|
||||
pointerEvents: 'none',
|
||||
filter: 'grayscale(1)',
|
||||
}}
|
||||
>
|
||||
<LinkedItemCard
|
||||
item={props.linkToRemove?.node}
|
||||
viewer={viewerData.me}
|
||||
layout="GRID_LAYOUT"
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handleAction={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
}
|
||||
onAccept={removeItem}
|
||||
acceptButtonLabel="Delete Item"
|
||||
onOpenChange={() => setShowRemoveLinkConfirmation(false)}
|
||||
/>
|
||||
)}
|
||||
{showUnsubscribeConfirmation && (
|
||||
<ConfirmationModal
|
||||
message={'Are you sure you want to unsubscribe?'}
|
||||
onAccept={unsubscribe}
|
||||
onOpenChange={() => setShowUnsubscribeConfirmation(false)}
|
||||
/>
|
||||
)}
|
||||
{props.labelsTarget?.node.id && (
|
||||
<SetLabelsModal
|
||||
provider={props.labelsTarget.node}
|
||||
onLabelsUpdated={(labels: Label[]) => {
|
||||
if (props.labelsTarget) {
|
||||
props.labelsTarget.node.labels = labels
|
||||
updateState({})
|
||||
}
|
||||
}}
|
||||
save={(labels: Label[]) => {
|
||||
if (props.labelsTarget?.node.id) {
|
||||
return setLabelsMutation(
|
||||
props.labelsTarget.node.id,
|
||||
labels.map((label) => label.id)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{props.snoozeTarget && (
|
||||
<SnoozeLinkModal
|
||||
submit={(option: string, sendReminder: boolean, msg: string) => {
|
||||
if (!props.snoozeTarget) return
|
||||
createReminderMutation(
|
||||
props.snoozeTarget?.node.id,
|
||||
ReminderType.Tonight,
|
||||
true,
|
||||
sendReminder
|
||||
)
|
||||
.then(() => {
|
||||
return props.actionHandler('archive', props.snoozeTarget)
|
||||
})
|
||||
.then(() => {
|
||||
showSuccessToast(msg, { position: 'bottom-right' })
|
||||
})
|
||||
.catch((error) => {
|
||||
showErrorToast('There was an error snoozing your link.', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
})
|
||||
}}
|
||||
onOpenChange={() => {
|
||||
if (props.snoozeTarget) {
|
||||
const item = document.getElementById(props.snoozeTarget.node.id)
|
||||
if (item) {
|
||||
item.focus()
|
||||
}
|
||||
props.setSnoozeTarget(undefined)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showRemoveLinkConfirmation && (
|
||||
<ConfirmationModal
|
||||
richMessage={
|
||||
<VStack alignment="center" distribution="center">
|
||||
<StyledText style="modalTitle" css={{ margin: '0px 8px' }}>
|
||||
Are you sure you want to delete this item? All associated
|
||||
notes and highlights will be deleted.
|
||||
</StyledText>
|
||||
{props.linkToRemove?.node && viewerData?.me && (
|
||||
<Box
|
||||
css={{
|
||||
transform: 'scale(0.6)',
|
||||
opacity: 0.8,
|
||||
pointerEvents: 'none',
|
||||
filter: 'grayscale(1)',
|
||||
}}
|
||||
>
|
||||
<LinkedItemCard
|
||||
item={props.linkToRemove?.node}
|
||||
viewer={viewerData.me}
|
||||
layout="GRID_LAYOUT"
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handleAction={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}}
|
||||
onOpenChange={() => {
|
||||
if (props.labelsTarget) {
|
||||
const activate = props.labelsTarget
|
||||
props.setActiveItem(activate)
|
||||
props.setLabelsTarget(undefined)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
onAccept={removeItem}
|
||||
acceptButtonLabel="Delete Item"
|
||||
onOpenChange={() => setShowRemoveLinkConfirmation(false)}
|
||||
/>
|
||||
)}
|
||||
{showUnsubscribeConfirmation && (
|
||||
<ConfirmationModal
|
||||
message={'Are you sure you want to unsubscribe?'}
|
||||
onAccept={unsubscribe}
|
||||
onOpenChange={() => setShowUnsubscribeConfirmation(false)}
|
||||
/>
|
||||
)}
|
||||
{props.labelsTarget?.node.id && (
|
||||
<SetLabelsModal
|
||||
provider={props.labelsTarget.node}
|
||||
onLabelsUpdated={(labels: Label[]) => {
|
||||
if (props.labelsTarget) {
|
||||
props.labelsTarget.node.labels = labels
|
||||
updateState({})
|
||||
}
|
||||
}}
|
||||
save={(labels: Label[]) => {
|
||||
if (props.labelsTarget?.node.id) {
|
||||
return setLabelsMutation(
|
||||
props.labelsTarget.node.id,
|
||||
labels.map((label) => label.id)
|
||||
)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}}
|
||||
onOpenChange={() => {
|
||||
if (props.labelsTarget) {
|
||||
const activate = props.labelsTarget
|
||||
props.setActiveItem(activate)
|
||||
props.setLabelsTarget(undefined)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
219
packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx
Normal file
219
packages/web/components/templates/homeFeed/LibraryFilterMenu.tsx
Normal file
@ -0,0 +1,219 @@
|
||||
import {
|
||||
InputHTMLAttributes,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { SearchIcon } from '../../elements/images/SearchIcon'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
import { searchBarCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import { Button, IconButton } from '../../elements/Button'
|
||||
import { Circle, MagnifyingGlass, Plus, Textbox, X } from 'phosphor-react'
|
||||
import { OmnivoreNameLogo } from '../../elements/images/OmnivoreNameLogo'
|
||||
import { OmnivoreFullLogo } from '../../elements/images/OmnivoreFullLogo'
|
||||
import { AvatarDropdown } from '../../elements/AvatarDropdown'
|
||||
import { ListSelectorIcon } from '../../elements/images/ListSelectorIcon'
|
||||
import { GridSelectorIcon } from '../../elements/images/GridSelectorIcon'
|
||||
import { useGetSubscriptionsQuery } from '../../../lib/networking/queries/useGetSubscriptionsQuery'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { Checkbox } from '@radix-ui/react-checkbox'
|
||||
|
||||
export function LibraryFilterMenu(): JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
left: '0px',
|
||||
top: '105px',
|
||||
position: 'fixed',
|
||||
bg: 'white',
|
||||
width: '233px',
|
||||
height: '100%',
|
||||
borderRight: '1px solid #E1E1E1',
|
||||
pr: '15px',
|
||||
}}
|
||||
>
|
||||
<SavedSearches />
|
||||
<Subscriptions />
|
||||
<Labels />
|
||||
|
||||
<AddLinkButton />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function SavedSearches(): JSX.Element {
|
||||
return (
|
||||
<MenuPanel title="Saved Searches">
|
||||
<FilterButton text="Inbox" selected={true} spaced={true} />
|
||||
<FilterButton text="Read Later" selected={false} spaced={true} />
|
||||
<FilterButton text="Today" selected={false} spaced={true} />
|
||||
<FilterButton text="Archived" selected={false} spaced={true} />
|
||||
<Box css={{ height: '10px' }}></Box>
|
||||
</MenuPanel>
|
||||
)
|
||||
}
|
||||
|
||||
function Subscriptions(): JSX.Element {
|
||||
const { subscriptions } = useGetSubscriptionsQuery()
|
||||
console.log('subscriptions: ', subscriptions)
|
||||
|
||||
return (
|
||||
<MenuPanel title="Subscriptions">
|
||||
{subscriptions.slice(0, 4).map((item) => {
|
||||
return <FilterButton key={item.id} text={item.name} selected={false} />
|
||||
})}
|
||||
<StyledText css={{ pl: '10px', color: '#BEBEBE', fontWeight: '600' }}>
|
||||
View All
|
||||
</StyledText>
|
||||
</MenuPanel>
|
||||
)
|
||||
}
|
||||
|
||||
function Labels(): JSX.Element {
|
||||
const { labels } = useGetLabelsQuery()
|
||||
console.log('labels: ', labels)
|
||||
|
||||
return (
|
||||
<MenuPanel title="Labels">
|
||||
{labels.slice(0, 4).map((item) => {
|
||||
return <LabelButton key={item.id} label={item} state="off" />
|
||||
})}
|
||||
<StyledText css={{ pl: '10px', color: '#BEBEBE', fontWeight: '600' }}>
|
||||
View All
|
||||
</StyledText>
|
||||
</MenuPanel>
|
||||
)
|
||||
}
|
||||
|
||||
type MenuPanelProps = {
|
||||
title: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function MenuPanel(props: MenuPanelProps): JSX.Element {
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
width: '100%',
|
||||
borderBottom: '1px solid #E1E1E1',
|
||||
pl: '15px',
|
||||
}}
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
>
|
||||
<StyledText
|
||||
css={{
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
lineHeight: '125%',
|
||||
color: '#1E1E1E',
|
||||
pl: '10px',
|
||||
my: '20px',
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</StyledText>
|
||||
{props.children}
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type FilterButtonProps = {
|
||||
text: string
|
||||
spaced?: boolean
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
function FilterButton(props: FilterButtonProps): JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
pl: '10px',
|
||||
pt: '2px', // TODO: hack to middle align
|
||||
mb: props.spaced ? '10px' : '0px',
|
||||
width: '100%',
|
||||
height: '30px',
|
||||
backgroundColor: props.selected ? '#FFEA9F' : 'unset',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'regular',
|
||||
color: '#3D3D3D',
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
}}
|
||||
>
|
||||
{props.text}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
type LabelButtonProps = {
|
||||
label: Label
|
||||
state: 'on' | 'off' | 'unset'
|
||||
}
|
||||
|
||||
function LabelButton(props: LabelButtonProps): JSX.Element {
|
||||
return (
|
||||
<HStack
|
||||
css={{
|
||||
pl: '10px',
|
||||
pt: '2px', // TODO: hack to middle align
|
||||
width: '100%',
|
||||
height: '30px',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'regular',
|
||||
color: '#3D3D3D',
|
||||
verticalAlign: 'middle',
|
||||
borderRadius: '3px',
|
||||
m: '0px',
|
||||
}}
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
>
|
||||
<Circle size={9} color={props.label.color} weight="fill" />
|
||||
<SpanBox css={{ pl: '10px' }}>{props.label.name}</SpanBox>
|
||||
<SpanBox css={{ ml: 'auto' }}>
|
||||
<input type="checkbox" />
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
function AddLinkButton(): JSX.Element {
|
||||
return (
|
||||
<VStack
|
||||
css={{
|
||||
position: 'fixed',
|
||||
bottom: '0',
|
||||
width: '233px',
|
||||
height: '80px',
|
||||
pl: '25px',
|
||||
}}
|
||||
distribution="center"
|
||||
>
|
||||
<Button
|
||||
css={{
|
||||
height: '40px',
|
||||
p: '15px',
|
||||
pr: '20px',
|
||||
fontSize: '14px',
|
||||
verticalAlign: 'center',
|
||||
color: '#3D3D3D',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: '600',
|
||||
}}
|
||||
>
|
||||
<Plus size={16} weight="bold" />
|
||||
<SpanBox css={{ width: '10px' }}></SpanBox>Add Link
|
||||
</Button>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
281
packages/web/components/templates/homeFeed/LibraryHeader.tsx
Normal file
281
packages/web/components/templates/homeFeed/LibraryHeader.tsx
Normal file
@ -0,0 +1,281 @@
|
||||
import {
|
||||
InputHTMLAttributes,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { SearchIcon } from '../../elements/images/SearchIcon'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
import { searchBarCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import { Button, IconButton } from '../../elements/Button'
|
||||
import { MagnifyingGlass, Textbox, X } from 'phosphor-react'
|
||||
import { OmnivoreNameLogo } from '../../elements/images/OmnivoreNameLogo'
|
||||
import { OmnivoreFullLogo } from '../../elements/images/OmnivoreFullLogo'
|
||||
import { AvatarDropdown } from '../../elements/AvatarDropdown'
|
||||
import { ListSelectorIcon } from '../../elements/images/ListSelectorIcon'
|
||||
import { GridSelectorIcon } from '../../elements/images/GridSelectorIcon'
|
||||
|
||||
type LibrarySearchBarProps = {
|
||||
searchTerm?: string
|
||||
applySearchQuery: (searchQuery: string) => void
|
||||
}
|
||||
|
||||
type LibraryFilter =
|
||||
| 'in:inbox'
|
||||
| 'in:all'
|
||||
| 'in:archive'
|
||||
| 'type:file'
|
||||
| 'type:highlights'
|
||||
| `saved:${string}`
|
||||
| `sort:read`
|
||||
|
||||
// get last week's date
|
||||
const recentlySavedStartDate = new Date(
|
||||
new Date().getTime() - 7 * 24 * 60 * 60 * 1000
|
||||
).toLocaleDateString('en-US')
|
||||
|
||||
const FOCUSED_BOXSHADOW = '0px 0px 2px 2px rgba(255, 234, 159, 0.56)'
|
||||
|
||||
export function LibraryHeader(props: LibrarySearchBarProps): JSX.Element {
|
||||
const [focused, setFocused] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [searchTerm, setSearchTerm] = useState(props.searchTerm || '')
|
||||
|
||||
useEffect(() => {
|
||||
setSearchTerm(props.searchTerm || '')
|
||||
}, [props.searchTerm])
|
||||
|
||||
useKeyboardShortcuts(
|
||||
searchBarCommands((action) => {
|
||||
if (action === 'focusSearchBar' && inputRef.current) {
|
||||
inputRef.current.select()
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<VStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
top: '0',
|
||||
left: '0',
|
||||
zIndex: 100,
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
height: '105px',
|
||||
bg: 'white',
|
||||
pt: '50px',
|
||||
borderBottom: '1px solid #E1E1E1',
|
||||
'@mdDown': {
|
||||
height: '40px',
|
||||
pt: '0px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<LogoBox />
|
||||
<SearchBox {...props} />
|
||||
<ControlButtonBox />
|
||||
</HStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
function SearchBox(props: LibrarySearchBarProps): JSX.Element {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [focused, setFocused] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
height: '38px',
|
||||
width: '100%',
|
||||
maxWidth: '521px',
|
||||
mr: '15px',
|
||||
bg: '#F3F3F3',
|
||||
borderRadius: '6px',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ height: '100%', px: '15px' }}
|
||||
>
|
||||
<MagnifyingGlass
|
||||
size={20}
|
||||
color={theme.colors.graySolid.toString()}
|
||||
/>
|
||||
</HStack>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
props.applySearchQuery(searchTerm || '')
|
||||
inputRef.current?.blur()
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
placeholder="Search"
|
||||
onFocus={(event) => {
|
||||
event.target.select()
|
||||
setFocused(true)
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocused(false)
|
||||
}}
|
||||
onChange={(event) => {
|
||||
setSearchTerm(event.target.value)
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
{searchTerm ? (
|
||||
<Button
|
||||
style="plainIcon"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
setSearchTerm('')
|
||||
props.applySearchQuery('')
|
||||
inputRef.current?.blur()
|
||||
}}
|
||||
css={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
mr: '8px',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<X
|
||||
width={16}
|
||||
height={16}
|
||||
color={theme.colors.grayTextContrast.toString()}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
<Box
|
||||
css={{
|
||||
py: '15px',
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
css={{
|
||||
mr: '5px',
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
color: '#898989',
|
||||
}}
|
||||
// onClick={() => requestAnimationFrame(() => inputRef.current.focus())}
|
||||
// we can make it unreachable via keyboard as we have the same message for the SR label
|
||||
tabIndex={-1}
|
||||
>
|
||||
<kbd aria-hidden>/</kbd>
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Displays the full logo on larger screens, small logo on mobile
|
||||
function LogoBox(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<SpanBox
|
||||
css={{
|
||||
ml: '25px',
|
||||
height: '24px',
|
||||
width: '232px',
|
||||
minWidth: '232px',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<OmnivoreFullLogo showTitle={true} />
|
||||
</SpanBox>
|
||||
<SpanBox
|
||||
css={{
|
||||
ml: '20px',
|
||||
mr: '20px',
|
||||
height: '22px',
|
||||
width: '22px',
|
||||
'@md': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<OmnivoreNameLogo />
|
||||
</SpanBox>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ControlButtonBox(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="end"
|
||||
css={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: '45px',
|
||||
width: '100px',
|
||||
height: '100%',
|
||||
gap: '20px',
|
||||
minWidth: '121px',
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListSelectorIcon />
|
||||
<GridSelectorIcon />
|
||||
<AvatarDropdown userInitials="JH" />
|
||||
</HStack>
|
||||
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="end"
|
||||
css={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: '20px',
|
||||
width: '100px',
|
||||
height: '100%',
|
||||
gap: '20px',
|
||||
'@md': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AvatarDropdown userInitials="JH" />
|
||||
</HStack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user