mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Colour clean ups
This commit is contained in:
parent
88a4dc6c28
commit
11463ba6f7
11 changed files with 842 additions and 114 deletions
|
|
@ -532,6 +532,7 @@ const processSubscription = async (
|
|||
if (itemCount == 100) {
|
||||
logger.info(`Max limit reached for feed ${feedUrl}`)
|
||||
}
|
||||
itemCount = itemCount + 1
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,21 @@ export const Button = styled('button', {
|
|||
border: '1px solid $grayBorderHover',
|
||||
},
|
||||
},
|
||||
ctaBlue: {
|
||||
borderRadius: '5px',
|
||||
px: '20px',
|
||||
py: '8px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '500',
|
||||
cursor: 'pointer',
|
||||
border: '1px solid $yellow3',
|
||||
bg: '$ctaBlue',
|
||||
'&:hover': {
|
||||
opacity: '0.6',
|
||||
border: '0px solid $ctaBlue',
|
||||
},
|
||||
},
|
||||
|
||||
ctaDarkYellow: {
|
||||
border: '1px solid transparent',
|
||||
fontSize: '14px',
|
||||
|
|
@ -215,6 +230,36 @@ export const Button = styled('button', {
|
|||
color: '$thLibraryMenuUnselected',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
tab: {
|
||||
px: '15px',
|
||||
py: '6px',
|
||||
border: 'none',
|
||||
bg: 'transparent',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500',
|
||||
fontFamily: '$inter',
|
||||
color: '$thBorderSubtle',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '5px',
|
||||
'&:hover': {
|
||||
color: '$thTextContrast',
|
||||
},
|
||||
},
|
||||
tabSelected: {
|
||||
px: '15px',
|
||||
py: '6px',
|
||||
border: 'none',
|
||||
bg: '$thBorderSubtle',
|
||||
fontSize: '12px',
|
||||
fontWeight: '500',
|
||||
fontFamily: '$inter',
|
||||
color: '$textContrast',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '5px',
|
||||
'&:hover': {
|
||||
color: '$thTextContrast',
|
||||
},
|
||||
},
|
||||
squareIcon: {
|
||||
mx: '$1',
|
||||
display: 'flex',
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ export function CloseButton(props: CloseButtonProps): JSX.Element {
|
|||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
height: '20px',
|
||||
width: '20px',
|
||||
height: '25px',
|
||||
width: '25px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '1000px',
|
||||
|
|
@ -44,8 +44,8 @@ export function CloseButton(props: CloseButtonProps): JSX.Element {
|
|||
}}
|
||||
>
|
||||
<X
|
||||
width={10}
|
||||
height={10}
|
||||
width={12}
|
||||
height={12}
|
||||
weight="bold"
|
||||
color={hover ? '#EBEBEB' : '#898989'}
|
||||
className="xMark"
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ import { Button } from './Button'
|
|||
import { Dropdown, DropdownOption } from './DropdownElements'
|
||||
import { CaretDownIcon } from './icons/CaretDownIcon'
|
||||
|
||||
type ShowLinkMode = 'none' | 'link' | 'pdf'
|
||||
|
||||
type SplitButtonProps = {
|
||||
title: string
|
||||
setShowLinkMode: (mode: ShowLinkMode) => void
|
||||
}
|
||||
|
||||
const CaretButton = (): JSX.Element => {
|
||||
|
|
@ -41,35 +44,39 @@ const CaretButton = (): JSX.Element => {
|
|||
|
||||
export const SplitButton = (props: SplitButtonProps): JSX.Element => {
|
||||
return (
|
||||
<HStack css={{ height: '27px', gap: '1px' }}>
|
||||
<HStack css={{ height: '32px', gap: '1px' }}>
|
||||
<Button
|
||||
css={{
|
||||
display: 'flex',
|
||||
minWidth: '70px',
|
||||
// minWidth: '70px',
|
||||
bg: '$ctaBlue',
|
||||
color: '#EDEDED',
|
||||
fontSize: '12px',
|
||||
fontSize: '14px',
|
||||
fontFamily: '$inter',
|
||||
border: '0px solid transparent',
|
||||
borderTopLeftRadius: '5px',
|
||||
borderBottomLeftRadius: '5px',
|
||||
borderTopRightRadius: '0px',
|
||||
borderBottomRightRadius: '0px',
|
||||
borderTopRightRadius: '5px',
|
||||
borderBottomRightRadius: '5px',
|
||||
'&:hover': {
|
||||
opacity: 1.0,
|
||||
color: 'white',
|
||||
opacity: 0.6,
|
||||
border: '0px solid transparent',
|
||||
},
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
border: '0px solid transparent',
|
||||
},
|
||||
}}
|
||||
onClick={(event) => {
|
||||
props.setShowLinkMode('link')
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</Button>
|
||||
<Dropdown triggerElement={<CaretButton />}>
|
||||
{/* <Dropdown triggerElement={<CaretButton />}>
|
||||
<DropdownOption onSelect={() => console.log()} title="Archive (e)" />
|
||||
</Dropdown>
|
||||
</Dropdown> */}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,12 +49,12 @@ const TriggerButton = (props: TriggerButtonProps): JSX.Element => {
|
|||
return (
|
||||
<HStack
|
||||
css={{
|
||||
mx: '10px',
|
||||
gap: '10px',
|
||||
alignItems: 'center',
|
||||
borderRadius: '5px',
|
||||
height: '27px',
|
||||
paddingLeft: '5px',
|
||||
paddingRight: '5px',
|
||||
height: '32px',
|
||||
padding: '5px',
|
||||
'&:hover': {
|
||||
bg: '#6A6968',
|
||||
opacity: '0.7px',
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import * as Progress from '@radix-ui/react-progress'
|
||||
import { File } from 'phosphor-react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { locale, timeZone } from '../../../lib/dateFormatting'
|
||||
import { saveUrlMutation } from '../../../lib/networking/mutations/saveUrlMutation'
|
||||
import { showErrorToast } from '../../../lib/toastHelpers'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
import { Box, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import {
|
||||
ModalButtonBar,
|
||||
ModalContent,
|
||||
|
|
@ -13,6 +15,29 @@ import {
|
|||
ModalRoot,
|
||||
ModalTitleBar,
|
||||
} from '../../elements/ModalPrimitives'
|
||||
import { CloseButton } from '../../elements/CloseButton'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { styled } from '@stitches/react'
|
||||
import Dropzone, {
|
||||
Accept,
|
||||
DropEvent,
|
||||
DropzoneRef,
|
||||
FileRejection,
|
||||
} from 'react-dropzone'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { validateCsvFile } from '../../../utils/csvValidator'
|
||||
import {
|
||||
uploadImportFileRequestMutation,
|
||||
UploadImportFileType,
|
||||
} from '../../../lib/networking/mutations/uploadImportFileMutation'
|
||||
import { uploadFileRequestMutation } from '../../../lib/networking/mutations/uploadFileMutation'
|
||||
import axios from 'axios'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { formatMessage } from '../../../locales/en/messages'
|
||||
import { subscribeMutation } from '../../../lib/networking/mutations/subscribeMutation'
|
||||
import { SubscriptionType } from '../../../lib/networking/queries/useGetSubscriptionsQuery'
|
||||
|
||||
type TabName = 'link' | 'feed' | 'opml' | 'pdf' | 'import'
|
||||
|
||||
type AddLinkModalProps = {
|
||||
onOpenChange: (open: boolean) => void
|
||||
|
|
@ -24,9 +49,127 @@ type AddLinkModalProps = {
|
|||
}
|
||||
|
||||
export function AddLinkModal(props: AddLinkModalProps): JSX.Element {
|
||||
const [link, setLink] = useState('')
|
||||
const [selectedTab, setSelectedTab] = useState('link')
|
||||
|
||||
const validateLink = useCallback(
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={props.onOpenChange} css={{}}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
css={{
|
||||
p: '20px',
|
||||
bg: '$modalBackground',
|
||||
maxWidth: '600',
|
||||
maxHeight: '300',
|
||||
fontFamily: '$inter',
|
||||
}}
|
||||
onInteractOutside={() => {
|
||||
// remove focus from modal
|
||||
;(document.activeElement as HTMLElement).blur()
|
||||
}}
|
||||
>
|
||||
<VStack distribution="start" css={{ gap: '20px' }}>
|
||||
<TabBar
|
||||
selectedTab={selectedTab}
|
||||
setSelectedTab={setSelectedTab}
|
||||
onOpenChange={props.onOpenChange}
|
||||
/>
|
||||
<Box css={{ width: '100%' }}>
|
||||
{selectedTab == 'link' && <AddLinkTab {...props} />}
|
||||
{selectedTab == 'feed' && <AddFeedTab {...props} />}
|
||||
{selectedTab == 'opml' && <UploadOPMLTab {...props} />}
|
||||
{selectedTab == 'pdf' && <UploadPDFTab {...props} />}
|
||||
{selectedTab == 'import' && <UploadImportTab {...props} />}
|
||||
</Box>
|
||||
</VStack>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
)
|
||||
}
|
||||
|
||||
const AddLinkTab = (props: AddLinkModalProps): JSX.Element => {
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const addLink = useCallback(
|
||||
async (link: string) => {
|
||||
await props.handleLinkSubmission(link, timeZone, locale)
|
||||
props.onOpenChange(false)
|
||||
},
|
||||
[errorMessage, setErrorMessage]
|
||||
)
|
||||
|
||||
return (
|
||||
<AddFromURL
|
||||
placeholder="https://example.com/"
|
||||
errorMessage={errorMessage}
|
||||
setErrorMessage={setErrorMessage}
|
||||
onSubmit={addLink}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const AddFeedTab = (props: AddLinkModalProps): JSX.Element => {
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const subscribe = useCallback(
|
||||
async (feedUrl: string) => {
|
||||
if (!feedUrl) {
|
||||
setErrorMessage('Please enter a valid feed URL')
|
||||
return
|
||||
}
|
||||
|
||||
let normailizedUrl: string
|
||||
// normalize the url
|
||||
try {
|
||||
normailizedUrl = new URL(feedUrl.trim()).toString()
|
||||
} catch (e) {
|
||||
setErrorMessage('Please enter a valid feed URL')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await subscribeMutation({
|
||||
url: normailizedUrl,
|
||||
subscriptionType: SubscriptionType.RSS,
|
||||
})
|
||||
|
||||
if (result.subscribe.errorCodes) {
|
||||
const errorMessage = formatMessage({
|
||||
id: `error.${result.subscribe.errorCodes[0]}`,
|
||||
})
|
||||
setErrorMessage(`There was an error adding new feed: ${errorMessage}`)
|
||||
return
|
||||
}
|
||||
|
||||
showSuccessToast('New feed has been added.')
|
||||
},
|
||||
[errorMessage, setErrorMessage]
|
||||
)
|
||||
|
||||
return (
|
||||
<AddFromURL
|
||||
placeholder="https://example.com/feed.atom"
|
||||
errorMessage={errorMessage}
|
||||
setErrorMessage={setErrorMessage}
|
||||
onSubmit={subscribe}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type AddFromURLProps = {
|
||||
placeholder: string
|
||||
errorMessage: string | undefined
|
||||
setErrorMessage: (message: string) => void
|
||||
onSubmit: (url: string) => Promise<void>
|
||||
}
|
||||
|
||||
const AddFromURL = (props: AddFromURLProps): JSX.Element => {
|
||||
const [url, setURL] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState(props.errorMessage)
|
||||
|
||||
const validateURL = useCallback(
|
||||
(link: string) => {
|
||||
try {
|
||||
const url = new URL(link)
|
||||
|
|
@ -38,66 +181,577 @@ export function AddLinkModal(props: AddLinkModalProps): JSX.Element {
|
|||
}
|
||||
return true
|
||||
},
|
||||
[link]
|
||||
[url]
|
||||
)
|
||||
|
||||
return (
|
||||
<ModalRoot defaultOpen onOpenChange={props.onOpenChange}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
css={{ bg: '$grayBg', px: '24px' }}
|
||||
onInteractOutside={() => {
|
||||
// remove focus from modal
|
||||
;(document.activeElement as HTMLElement).blur()
|
||||
<VStack css={{ width: '100%', height: '180px' }}>
|
||||
<form
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
paddingTop: '5px',
|
||||
}}
|
||||
onSubmit={async (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!validateURL(url)) {
|
||||
setErrorMessage('Invalid URL')
|
||||
return
|
||||
}
|
||||
|
||||
props.onSubmit(url)
|
||||
}}
|
||||
>
|
||||
<VStack distribution="start">
|
||||
<ModalTitleBar title="Add Link" onOpenChange={props.onOpenChange} />
|
||||
<Box css={{ width: '100%', py: '16px' }}>
|
||||
<form
|
||||
onSubmit={async (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
let submitLink = link
|
||||
if (!validateLink(link)) {
|
||||
// If validation fails, attempting adding
|
||||
// `https` to give the link a protocol.
|
||||
const newLink = `https://${link}`
|
||||
if (!validateLink(newLink)) {
|
||||
showErrorToast('Invalid link', { position: 'bottom-right' })
|
||||
return
|
||||
}
|
||||
setLink(newLink)
|
||||
submitLink = newLink
|
||||
}
|
||||
await props.handleLinkSubmission(submitLink, timeZone, locale)
|
||||
props.onOpenChange(false)
|
||||
}}
|
||||
>
|
||||
<FormInput
|
||||
type="url"
|
||||
value={link}
|
||||
autoFocus={true}
|
||||
placeholder="https://example.com"
|
||||
onChange={(event) => setLink(event.target.value)}
|
||||
css={{
|
||||
borderRadius: '8px',
|
||||
border: '1px solid $textNonessential',
|
||||
width: '100%',
|
||||
height: '38px',
|
||||
p: '6px',
|
||||
mb: '13px',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
/>
|
||||
<ModalButtonBar
|
||||
onOpenChange={props.onOpenChange}
|
||||
acceptButtonLabel="Add Link"
|
||||
/>
|
||||
</form>
|
||||
</Box>
|
||||
</VStack>
|
||||
</ModalContent>
|
||||
</ModalRoot>
|
||||
<FormInput
|
||||
type="url"
|
||||
value={url}
|
||||
autoFocus={true}
|
||||
placeholder={props.placeholder}
|
||||
onChange={(event) => setURL(event.target.value)}
|
||||
css={{
|
||||
borderRadius: '4px',
|
||||
width: '100%',
|
||||
height: '38px',
|
||||
p: '6px',
|
||||
mb: '13px',
|
||||
fontSize: '14px',
|
||||
color: '$thTextContrast',
|
||||
bg: '$thLibrarySearchbox',
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style="ctaBlue"
|
||||
type="submit"
|
||||
css={{ marginLeft: 'auto', marginTop: 'auto' }}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</form>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const UploadOPMLTab = (props: AddLinkModalProps): JSX.Element => {
|
||||
return (
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ height: '180px', width: '100%' }}
|
||||
>
|
||||
<UploadPad
|
||||
description="Drag OPML file to add feeds"
|
||||
accept={{
|
||||
'text/csv': ['.csv'],
|
||||
'application/zip': ['.zip'],
|
||||
'application/pdf': ['.pdf'],
|
||||
'application/epub+zip': ['.epub'],
|
||||
}}
|
||||
/>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const UploadPDFTab = (props: AddLinkModalProps): JSX.Element => {
|
||||
return (
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ height: '180px', width: '100%' }}
|
||||
>
|
||||
<UploadPad
|
||||
description="Drag PDFs here to add to your library"
|
||||
accept={{
|
||||
'application/pdf': ['.pdf'],
|
||||
}}
|
||||
/>
|
||||
<SpanBox css={{ fontSize: '12px', color: '$thTextSubtle' }}>
|
||||
* PDFs have a maximum size of 8MB
|
||||
</SpanBox>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const UploadImportTab = (props: AddLinkModalProps): JSX.Element => {
|
||||
return (
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ height: '180px', width: '100%' }}
|
||||
>
|
||||
<UploadPad
|
||||
description="Drop import files here"
|
||||
accept={{
|
||||
'text/csv': ['.csv'],
|
||||
'application/zip': ['.zip'],
|
||||
}}
|
||||
/>
|
||||
<SpanBox css={{ fontSize: '12px', color: '$thTextSubtle' }}>
|
||||
* Imports must be in a supported format{' '}
|
||||
<a
|
||||
href="https://docs.omnivore.app/using/importing.html"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
read more
|
||||
</a>
|
||||
.
|
||||
</SpanBox>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const DragnDropContainer = styled('div', {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: '1',
|
||||
alignSelf: 'center',
|
||||
left: 0,
|
||||
flexDirection: 'column',
|
||||
})
|
||||
|
||||
const DragnDropStyle = styled('div', {
|
||||
border: '1px solid $grayBorder',
|
||||
borderRadius: '5px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
color: '$thTextSubtle2',
|
||||
padding: '10px',
|
||||
})
|
||||
|
||||
const DragnDropIndicator = styled('div', {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: '5px',
|
||||
})
|
||||
|
||||
const ProgressIndicator = styled(Progress.Indicator, {
|
||||
backgroundColor: '$omnivoreCtaYellow',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
})
|
||||
|
||||
const ProgressRoot = styled(Progress.Root, {
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background: '$omnivoreGray',
|
||||
borderRadius: '99999px',
|
||||
width: '100%',
|
||||
height: '5px',
|
||||
transform: 'translateZ(0)',
|
||||
})
|
||||
|
||||
type UploadingFile = {
|
||||
id: string
|
||||
file: any
|
||||
name: string
|
||||
progress: number
|
||||
status: 'inprogress' | 'success' | 'error'
|
||||
openUrl: string | undefined
|
||||
contentType: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
type UploadInfo = {
|
||||
uploadSignedUrl?: string
|
||||
requestId?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
type UploadPadProps = {
|
||||
description: string
|
||||
accept: Accept
|
||||
}
|
||||
|
||||
const UploadPad = (props: UploadPadProps): JSX.Element => {
|
||||
const [uploadFiles, setUploadFiles] = useState<UploadingFile[]>([])
|
||||
const [inDragOperation, setInDragOperation] = useState(false)
|
||||
const dropzoneRef = useRef<DropzoneRef | null>(null)
|
||||
|
||||
const openDialog = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (dropzoneRef.current) {
|
||||
dropzoneRef.current.open()
|
||||
}
|
||||
event?.preventDefault()
|
||||
},
|
||||
[dropzoneRef]
|
||||
)
|
||||
|
||||
const uploadSignedUrlForFile = async (
|
||||
file: UploadingFile
|
||||
): Promise<UploadInfo> => {
|
||||
let { contentType } = file
|
||||
if (
|
||||
contentType == 'application/vnd.ms-excel' &&
|
||||
file.name.endsWith('.csv')
|
||||
) {
|
||||
contentType = 'text/csv'
|
||||
}
|
||||
switch (contentType) {
|
||||
case 'text/csv': {
|
||||
let urlCount = 0
|
||||
try {
|
||||
const csvData = await validateCsvFile(file.file)
|
||||
urlCount = csvData.data.length
|
||||
if (urlCount > 5000) {
|
||||
return {
|
||||
message:
|
||||
'Due to an increase in traffic we are limiting CSV imports to 5000 items.',
|
||||
}
|
||||
}
|
||||
if (csvData.inValidData.length > 0) {
|
||||
return {
|
||||
message: csvData.inValidData[0].message,
|
||||
}
|
||||
}
|
||||
if (urlCount === 0) {
|
||||
return {
|
||||
message: 'No URLs found in CSV file.',
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
message: 'Invalid CSV file.',
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await uploadImportFileRequestMutation(
|
||||
UploadImportFileType.URL_LIST,
|
||||
contentType
|
||||
)
|
||||
return {
|
||||
uploadSignedUrl: result?.uploadSignedUrl,
|
||||
message: `Importing ${urlCount} URLs`,
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('caught error', error)
|
||||
if (error == 'UPLOAD_DAILY_LIMIT_EXCEEDED') {
|
||||
return {
|
||||
message: 'You have exceeded your maximum daily upload limit.',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'application/zip': {
|
||||
const result = await uploadImportFileRequestMutation(
|
||||
UploadImportFileType.MATTER,
|
||||
contentType
|
||||
)
|
||||
return {
|
||||
uploadSignedUrl: result?.uploadSignedUrl,
|
||||
}
|
||||
}
|
||||
case 'application/pdf':
|
||||
case 'application/epub+zip': {
|
||||
const request = await uploadFileRequestMutation({
|
||||
// This will tell the backend not to save the URL
|
||||
// and give it the local filename as the title.
|
||||
url: `file://local/${file.id}/${file.file.path}`,
|
||||
contentType: contentType,
|
||||
createPageEntry: true,
|
||||
})
|
||||
return {
|
||||
uploadSignedUrl: request?.uploadSignedUrl,
|
||||
requestId: request?.createdPageId,
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
message: `Invalid content type: ${contentType}`,
|
||||
}
|
||||
}
|
||||
|
||||
const handleAcceptedFiles = useCallback(
|
||||
(acceptedFiles: any, event: DropEvent) => {
|
||||
setInDragOperation(false)
|
||||
|
||||
const addedFiles = acceptedFiles.map(
|
||||
(file: { name: any; type: string }) => {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
file: file,
|
||||
name: file.name,
|
||||
progress: 0,
|
||||
status: 'inprogress',
|
||||
contentType: file.type,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const allFiles = [...uploadFiles, ...addedFiles]
|
||||
|
||||
setUploadFiles(allFiles)
|
||||
;(async () => {
|
||||
for (const file of addedFiles) {
|
||||
try {
|
||||
const uploadInfo = await uploadSignedUrlForFile(file)
|
||||
if (!uploadInfo.uploadSignedUrl) {
|
||||
const message = uploadInfo.message || 'No upload URL available'
|
||||
showErrorToast(message, { duration: 10000 })
|
||||
file.status = 'error'
|
||||
setUploadFiles([...allFiles])
|
||||
return
|
||||
}
|
||||
|
||||
const uploadResult = await axios.request({
|
||||
method: 'PUT',
|
||||
url: uploadInfo.uploadSignedUrl,
|
||||
data: file.file,
|
||||
withCredentials: false,
|
||||
headers: {
|
||||
'Content-Type': file.file.type,
|
||||
},
|
||||
onUploadProgress: (p) => {
|
||||
if (!p.total) {
|
||||
console.warn('No total available for upload progress')
|
||||
return
|
||||
}
|
||||
const progress = (p.loaded / p.total) * 100
|
||||
file.progress = progress
|
||||
|
||||
setUploadFiles([...allFiles])
|
||||
},
|
||||
})
|
||||
|
||||
file.progress = 100
|
||||
file.status = 'success'
|
||||
file.openUrl = uploadInfo.requestId
|
||||
? `/article/sr/${uploadInfo.requestId}`
|
||||
: undefined
|
||||
file.message = uploadInfo.message
|
||||
|
||||
setUploadFiles([...allFiles])
|
||||
} catch (error) {
|
||||
file.status = 'error'
|
||||
setUploadFiles([...allFiles])
|
||||
}
|
||||
}
|
||||
})()
|
||||
},
|
||||
[uploadFiles]
|
||||
)
|
||||
|
||||
return (
|
||||
<VStack distribution="start" css={{ width: '100%', height: '100%' }}>
|
||||
<Dropzone
|
||||
ref={dropzoneRef}
|
||||
onDragEnter={() => {
|
||||
setInDragOperation(true)
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
setInDragOperation(false)
|
||||
}}
|
||||
onDropAccepted={handleAcceptedFiles}
|
||||
onDropRejected={(fileRejections: FileRejection[], event: DropEvent) => {
|
||||
console.log('onDropRejected: ', fileRejections, event)
|
||||
alert('You can only upload PDF files to your Omnivore Library.')
|
||||
setInDragOperation(false)
|
||||
event.preventDefault()
|
||||
}}
|
||||
preventDropOnDocument={true}
|
||||
noClick={true}
|
||||
accept={props.accept}
|
||||
>
|
||||
{({ getRootProps, getInputProps, acceptedFiles, fileRejections }) => (
|
||||
<div
|
||||
{...getRootProps({ className: 'dropzone' })}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<DragnDropContainer>
|
||||
<DragnDropStyle>
|
||||
<DragnDropIndicator
|
||||
css={{
|
||||
border: inDragOperation ? '2px dashed blue' : 'unset',
|
||||
}}
|
||||
>
|
||||
<VStack
|
||||
alignment="center"
|
||||
css={{ gap: '20px', height: '100%' }}
|
||||
>
|
||||
<File
|
||||
size={40}
|
||||
color={theme.colors.thTextSubtle2.toString()}
|
||||
/>
|
||||
{inDragOperation ? (
|
||||
<>
|
||||
<Box
|
||||
css={{
|
||||
p: '0px',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
Drop to upload your file
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
css={{
|
||||
fontSize: '14px',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{props.description}
|
||||
<br /> or{' '}
|
||||
<a href="" onClick={openDialog}>
|
||||
choose your files
|
||||
</a>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
</DragnDropIndicator>
|
||||
</DragnDropStyle>
|
||||
<VStack css={{ width: '100%', mt: '25px', gap: '5px' }}>
|
||||
{uploadFiles.map((file) => {
|
||||
return (
|
||||
<HStack
|
||||
key={file.id}
|
||||
css={{
|
||||
width: '100%',
|
||||
height: '54px',
|
||||
border: '1px dashed $grayBorder',
|
||||
borderRadius: '5px',
|
||||
padding: '15px',
|
||||
gap: '10px',
|
||||
color: '$thTextContrast',
|
||||
}}
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
width: '280px',
|
||||
maxLines: '1',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: '200px',
|
||||
overflow: 'hidden',
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{file.name}
|
||||
</Box>
|
||||
{file.status != 'inprogress' ? (
|
||||
<HStack
|
||||
alignment="center"
|
||||
css={{ marginLeft: 'auto', fontSize: '14px' }}
|
||||
>
|
||||
{file.status == 'success' && file.openUrl && (
|
||||
<a href={file.openUrl}>Read Now</a>
|
||||
)}
|
||||
{file.status == 'success' && !file.openUrl && (
|
||||
<span>
|
||||
{file.message || 'Your import has started'}
|
||||
</span>
|
||||
)}
|
||||
{file.status == 'error' && (
|
||||
<SpanBox css={{ color: 'red' }}>
|
||||
Error Uploading
|
||||
</SpanBox>
|
||||
)}
|
||||
</HStack>
|
||||
) : (
|
||||
<ProgressRoot value={file.progress} max={100}>
|
||||
<ProgressIndicator
|
||||
style={{
|
||||
transform: `translateX(-${100 - file.progress}%)`,
|
||||
}}
|
||||
/>{' '}
|
||||
</ProgressRoot>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
})}
|
||||
</VStack>
|
||||
</DragnDropContainer>
|
||||
<input {...getInputProps()} />
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
type TabBarProps = {
|
||||
selectedTab: string
|
||||
setSelectedTab: (selected: TabName) => void
|
||||
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
const TabBar = (props: TabBarProps) => {
|
||||
return (
|
||||
<HStack
|
||||
distribution="between"
|
||||
alignment="center"
|
||||
css={{ width: '100%', gap: '4px' }}
|
||||
>
|
||||
<Button
|
||||
style={props.selectedTab == 'link' ? 'tabSelected' : 'tab'}
|
||||
onClick={(event) => {
|
||||
props.setSelectedTab('link')
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
Link
|
||||
</Button>
|
||||
<Button
|
||||
style={props.selectedTab == 'pdf' ? 'tabSelected' : 'tab'}
|
||||
onClick={(event) => {
|
||||
props.setSelectedTab('pdf')
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
PDF
|
||||
</Button>
|
||||
<Button
|
||||
style={props.selectedTab == 'feed' ? 'tabSelected' : 'tab'}
|
||||
onClick={(event) => {
|
||||
props.setSelectedTab('feed')
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
Feed
|
||||
</Button>
|
||||
{/* <Button
|
||||
style={props.selectedTab == 'opml' ? 'tabSelected' : 'tab'}
|
||||
onClick={(event) => {
|
||||
props.setSelectedTab('opml')
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
OPML
|
||||
</Button> */}
|
||||
<Button
|
||||
style={props.selectedTab == 'import' ? 'tabSelected' : 'tab'}
|
||||
onClick={(event) => {
|
||||
props.setSelectedTab('import')
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
|
||||
<SpanBox css={{ ml: 'auto' }}>
|
||||
<CloseButton close={() => props.onOpenChange(false)} />
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -963,9 +963,6 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
|
|||
applySearchQuery={(searchQuery: string) => {
|
||||
props.applySearchQuery(searchQuery)
|
||||
}}
|
||||
handleLinkSubmission={props.handleLinkSubmission}
|
||||
allowSelectMultiple={props.mode !== 'highlights'}
|
||||
alwaysShowHeader={props.mode == 'highlights'}
|
||||
showFilterMenu={showFilterMenu}
|
||||
setShowFilterMenu={setShowFilterMenu}
|
||||
multiSelectMode={props.multiSelectMode}
|
||||
|
|
@ -1234,7 +1231,8 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element {
|
|||
outline: 'none',
|
||||
},
|
||||
'&> div': {
|
||||
bg: '$thBackground3',
|
||||
bg: '$thLeftMenuBackground',
|
||||
// bg: '$thLibraryBackground',
|
||||
},
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
|
|
@ -1246,6 +1244,7 @@ function LibraryItems(props: LibraryItemsProps): JSX.Element {
|
|||
'&:hover': {
|
||||
'> div': {
|
||||
bg: '$thBackgroundActive',
|
||||
boxShadow: '$cardBoxShadow',
|
||||
},
|
||||
'> a': {
|
||||
bg: '$thBackgroundActive',
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element {
|
|||
<SavedSearches {...props} savedSearches={savedSearches} />
|
||||
<Subscriptions {...props} subscriptions={subscriptions} />
|
||||
<Labels {...props} labels={labels} />
|
||||
<Footer />
|
||||
<Footer {...props} />
|
||||
<Box css={{ height: '250px ' }} />
|
||||
</Box>
|
||||
{/* This spacer pushes library content to the right of
|
||||
|
|
@ -611,16 +611,18 @@ function EditButton(props: EditButtonProps): JSX.Element {
|
|||
)
|
||||
}
|
||||
|
||||
const Footer = (): JSX.Element => {
|
||||
const Footer = (props: LibraryFilterMenuProps): JSX.Element => {
|
||||
return (
|
||||
<HStack
|
||||
css={{
|
||||
gap: '10px',
|
||||
height: '55px',
|
||||
height: '65px',
|
||||
position: 'fixed',
|
||||
bottom: '0%',
|
||||
alignItems: 'center',
|
||||
padding: '5px',
|
||||
paddingX: '5px',
|
||||
paddingY: '5px',
|
||||
|
||||
backgroundColor: '$thBackground2',
|
||||
width: LIBRARY_LEFT_MENU_WIDTH,
|
||||
overflowY: 'auto',
|
||||
|
|
@ -638,10 +640,12 @@ const Footer = (): JSX.Element => {
|
|||
css={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: '5px',
|
||||
'&:hover': { opacity: 1.0, color: 'white' },
|
||||
}}
|
||||
>
|
||||
<SplitButton title="Add Link" />
|
||||
<SplitButton
|
||||
title="Add"
|
||||
setShowLinkMode={() => props.setShowAddLinkModal(true)}
|
||||
/>
|
||||
</SpanBox>
|
||||
</HStack>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
|
|
@ -41,6 +41,7 @@ import { HeaderCheckboxIcon } from '../../elements/icons/HeaderCheckboxIcon'
|
|||
import { HeaderSearchIcon } from '../../elements/icons/HeaderSearchIcon'
|
||||
import { HeaderToggleGridIcon } from '../../elements/icons/HeaderToggleGridIcon'
|
||||
import { HeaderToggleListIcon } from '../../elements/icons/HeaderToggleListIcon'
|
||||
import useWindowDimensions from '../../../lib/hooks/useGetWindowDimensions'
|
||||
|
||||
export type MultiSelectMode = 'off' | 'none' | 'some' | 'visible' | 'search'
|
||||
|
||||
|
|
@ -83,7 +84,6 @@ const controlWidths = (
|
|||
}
|
||||
|
||||
export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
|
||||
const headerHeight = useGetHeaderHeight()
|
||||
const [small, setSmall] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -105,7 +105,7 @@ export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
|
|||
position: 'fixed',
|
||||
left: LIBRARY_LEFT_MENU_WIDTH,
|
||||
height: small ? '60px' : DEFAULT_HEADER_HEIGHT,
|
||||
transition: '0.5s',
|
||||
transition: 'height 0.5s',
|
||||
'@mdDown': {
|
||||
left: '0px',
|
||||
right: '0',
|
||||
|
|
@ -123,6 +123,7 @@ export function LibraryHeader(props: LibraryHeaderProps): JSX.Element {
|
|||
}
|
||||
|
||||
function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
|
||||
const dimensions = useWindowDimensions()
|
||||
const [showSearchBar, setShowSearchBar] = useState(false)
|
||||
const [pinnedSearches, setPinnedSearches] = usePersistedState<
|
||||
PinnedSearch[] | null
|
||||
|
|
@ -132,6 +133,10 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
|
|||
isSessionStorage: false,
|
||||
})
|
||||
|
||||
const isWideWindow = useMemo(() => {
|
||||
return dimensions.width >= 480
|
||||
}, [dimensions])
|
||||
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
|
|
@ -148,25 +153,29 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
|
|||
</HStack>
|
||||
) : (
|
||||
<>
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'none',
|
||||
'@mdDown': { display: 'flex' },
|
||||
}}
|
||||
>
|
||||
<MenuHeaderButton {...props} />
|
||||
</SpanBox>
|
||||
<Button
|
||||
title="Select multiple"
|
||||
style="plainIcon"
|
||||
css={{ display: 'flex', '&:hover': { opacity: '1.0' } }}
|
||||
onClick={(e) => {
|
||||
props.setMultiSelectMode('visible')
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<HeaderCheckboxIcon />
|
||||
</Button>
|
||||
{(!showSearchBar || isWideWindow) && (
|
||||
<>
|
||||
<SpanBox
|
||||
css={{
|
||||
display: 'none',
|
||||
'@mdDown': { display: 'flex' },
|
||||
}}
|
||||
>
|
||||
<MenuHeaderButton {...props} />
|
||||
</SpanBox>
|
||||
<Button
|
||||
title="Select multiple"
|
||||
style="plainIcon"
|
||||
css={{ display: 'flex', '&:hover': { opacity: '1.0' } }}
|
||||
onClick={(e) => {
|
||||
props.setMultiSelectMode('visible')
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<HeaderCheckboxIcon />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showSearchBar ? (
|
||||
<SearchBox {...props} setShowSearchBar={setShowSearchBar} />
|
||||
|
|
@ -308,7 +317,7 @@ export function SearchBox(props: SearchBoxProps): JSX.Element {
|
|||
bg: '$thLibrarySearchbox',
|
||||
borderRadius: '100px',
|
||||
border: focused
|
||||
? '2px solid $omnivoreCtaYellow'
|
||||
? '2px solid $searchActiveOutline'
|
||||
: '2px solid transparent',
|
||||
boxShadow: focused
|
||||
? 'none'
|
||||
|
|
|
|||
|
|
@ -103,7 +103,8 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
|
|||
borderWidths: {},
|
||||
borderStyles: {},
|
||||
shadows: {
|
||||
cardBoxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05);',
|
||||
// cardBoxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05);',
|
||||
cardBoxShadow: '0px 4px 4px rgba(0, 0, 0, 0.20);',
|
||||
},
|
||||
zIndices: {},
|
||||
transitions: {},
|
||||
|
|
@ -130,6 +131,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
|
|||
grayText: '#6A6968',
|
||||
|
||||
ctaBlue: '#007AFF',
|
||||
modalBackground: '#FFFFFF',
|
||||
|
||||
highlightBackground: '255, 210, 52',
|
||||
recommendedHighlightBackground: '#E5FFE5',
|
||||
|
|
@ -143,6 +145,7 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
|
|||
omnivoreYellow: 'rgb(255, 234, 159)',
|
||||
omnivoreLightGray: 'rgb(125, 125, 125)',
|
||||
omnivoreCtaYellow: 'rgb(255, 210, 52)',
|
||||
searchActiveOutline: '#866D15',
|
||||
|
||||
// Reader Colors
|
||||
readerBg: 'white',
|
||||
|
|
@ -234,6 +237,9 @@ const darkThemeSpec = {
|
|||
colorScheme: {
|
||||
colorScheme: 'dark',
|
||||
},
|
||||
shadows: {
|
||||
cardBoxShadow: '0px 4px 8px rgba(0, 0, 0, 0.50);',
|
||||
},
|
||||
colors: {
|
||||
grayBase: '#252525',
|
||||
grayBg: '#3B3938',
|
||||
|
|
@ -250,6 +256,8 @@ const darkThemeSpec = {
|
|||
grayBorderHover: 'hsl(0 0% 31.2%)',
|
||||
grayText: '#CDCDCD',
|
||||
|
||||
modalBackground: '#2A2A2A',
|
||||
|
||||
// Semantic Colors
|
||||
highlightBackground: '134, 109, 21',
|
||||
recommendedHighlightBackground: '#1F4315',
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ function getWindowDimensions() {
|
|||
}
|
||||
}
|
||||
export default function useWindowDimensions() {
|
||||
const [windowDimensions, setWindowDimensions] = useState(
|
||||
getWindowDimensions()
|
||||
)
|
||||
const [windowDimensions, setWindowDimensions] = useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
function handleResize() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue