Update the library layout to be closer to the design spec

This commit is contained in:
Jackson Harper
2023-02-24 14:54:37 +08:00
parent 09328edfe5
commit ea734db683
8 changed files with 1088 additions and 393 deletions

View 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>
)
}

View 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>
)
}

View 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>
)
}

View File

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

View File

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

View File

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

View 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>
)
}

View 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>
</>
)
}