Landing page improvements and various supporting improvements

This commit is contained in:
Luke Channings 2023-10-23 22:20:47 +01:00
parent 836267aa56
commit c4773dc904
No known key found for this signature in database
105 changed files with 1831 additions and 780 deletions

View file

@ -11,10 +11,16 @@ on:
paths-ignore:
- 'apple/**'
env:
NEXT_PUBLIC_APP_ENV: prod
NEXT_PUBLIC_BASE_URL: http://localhost:3000
NEXT_PUBLIC_SERVER_BASE_URL: http://localhost:4000
NEXT_PUBLIC_HIGHLIGHTS_BASE_URL: http://localhost:3000
jobs:
run-code-tests:
name: Run Codebase tests
runs-on: ubuntu-latest-m
runs-on: ${{ github.repository_owner == 'omnivore-app' && 'ubuntu-latest-m' || 'ubuntu-latest' }}
services:
postgres:
image: ankane/pgvector
@ -71,6 +77,7 @@ jobs:
- name: TypeScript Build and Lint
run: |
source ~/.nvm/nvm.sh
env
yarn build
yarn lint
- name: Tests

2
.gitignore vendored
View file

@ -69,3 +69,5 @@ data.json
# android
*.aab
*.apk
tsconfig.tsbuildinfo

View file

@ -10,5 +10,8 @@
"express": "^4.18.1",
"express-graphql": "^0.12.0",
"graphql": "^16.8.1"
},
"volta": {
"extends": "../../package.json"
}
}
}

View file

@ -6,16 +6,16 @@
"workspaces": [
"packages/*"
],
"license": "UNLICENSED",
"license": "AGPL-3.0-only",
"scripts": {
"test": "lerna run --no-bail test --ignore @omnivore/web",
"lint": "lerna run lint --ignore @omnivore/web",
"build": "lerna run build --ignore @omnivore/web",
"bootstrap": "lerna bootstrap",
"test": "lerna run --no-bail test",
"lint": "lerna run lint",
"build": "lerna run build",
"test:scoped:example": "lerna run test --scope={@omnivore/pdf-handler,@omnivore/web}",
"gql-typegen": "graphql-codegen",
"deploy:web": "vercel --prod"
},
"dependencies": {},
"devDependencies": {
"@ardatan/aggregate-error": "^0.0.6",
"@graphql-codegen/cli": "^2.6.2",
@ -33,11 +33,10 @@
"graphql-tag": "^2.11.0",
"lerna": "^7.4.1",
"prettier": "^2.5.1",
"typescript": "^4.4.3"
"typescript": "4.5.2"
},
"volta": {
"node": "18.16.1",
"yarn": "1.22.19"
},
"dependencies": {}
}
}
}

View file

@ -8,6 +8,7 @@
"start": "node dist/server.js",
"lint": "eslint src --ext ts,js,tsx,jsx",
"lint:fix": "eslint src --fix --ext ts,js,tsx,jsx",
"test:typecheck": "tsc --noEmit",
"test": "nyc mocha -r ts-node/register --config mocha-config.json --timeout 10000",
"copy-files": "copyfiles -u 1 src/**/*.html dist/"
},
@ -145,7 +146,6 @@
"node": "18.16.1"
},
"volta": {
"node": "18.16.1",
"yarn": "1.22.19"
"extends": "../../package.json"
}
}

View file

@ -32,7 +32,6 @@
"webpack-dev-server": "^4.7.4"
},
"volta": {
"node": "18.16.0",
"yarn": "1.22.10"
"extends": "../../package.json"
}
}
}

View file

@ -60,6 +60,11 @@ const config: Configuration = {
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
fallback: {
stream: false,
fs: false,
zlib: false,
}
},
output: {
path: path.resolve(__dirname, 'build'),

View file

@ -20,5 +20,8 @@
"start_gcf": "npx functions-framework --port=9090 --target=puppeteer",
"start_preview": "npx functions-framework --target=preview",
"test": "mocha test/*.js"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -10,6 +10,7 @@
"license": "Apache-2.0",
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc"
@ -38,5 +39,8 @@
"redis": "^4.3.1",
"underscore": "^1.13.6",
"uuid": "^9.0.0"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -228,7 +228,7 @@ const getTweetIds = async (
timeout: 60000, // 60 seconds
})
return (await page.evaluate(async (author) => {
return await page.evaluate(async (author) => {
/**
* Wait for `ms` amount of milliseconds
* @param {number} ms
@ -278,7 +278,7 @@ const getTweetIds = async (
}
return ids
}, author)) as string[]
}, author)
} catch (error) {
console.error('Error getting tweets', error)

View file

@ -11,7 +11,6 @@
},
"devDependencies": {},
"volta": {
"node": "18.16.1",
"yarn": "1.22.10"
"extends": "../../package.json"
}
}
}

View file

@ -4,6 +4,7 @@
"description": "",
"scripts": {
"migrate": "ts-node ./migrate.ts",
"test:typecheck": "tsc --noEmit",
"generate": "plop"
},
"author": "",

View file

@ -10,6 +10,7 @@
"license": "Apache-2.0",
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc && yarn copy-files",
@ -52,5 +53,8 @@
"unzip-stream": "^0.3.1",
"urlsafe-base64": "^1.0.0",
"uuid": "^9.0.0"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -11,6 +11,7 @@
"keywords": [],
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -42,5 +43,8 @@
"parse-multipart-data": "^1.2.1",
"rfc2047": "^4.0.1",
"showdown": "^2.1.0"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -11,6 +11,7 @@
"keywords": [],
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -33,5 +34,8 @@
"axios": "^0.27.2",
"concurrently": "^7.0.0",
"pdfjs-dist": "^2.9.359"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -25,5 +25,8 @@
},
"scripts": {
"test": "mocha test/*.js"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -8,6 +8,7 @@
"license": "Apache-2.0",
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -28,5 +29,8 @@
"axios": "^1.4.0",
"dotenv": "^16.0.1",
"jsonwebtoken": "^8.5.1"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -41,5 +41,8 @@
"dependencies": {
"html-entities": "^2.3.2",
"parse-srcset": "^1.0.2"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -8,6 +8,7 @@
"license": "Apache-2.0",
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -28,5 +29,8 @@
"dotenv": "^16.0.1",
"jsonwebtoken": "^8.5.1",
"rss-parser": "^3.13.0"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -8,6 +8,7 @@
"license": "Apache-2.0",
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -26,5 +27,8 @@
"dotenv": "^16.0.1",
"jsonwebtoken": "^8.5.1",
"search-query-parser": "^1.6.0"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -11,6 +11,7 @@
"keywords": [],
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -45,5 +46,8 @@
"natural": "^6.2.0",
"redis": "^4.3.1",
"underscore": "^1.13.4"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -8,6 +8,7 @@
"license": "Apache-2.0",
"scripts": {
"test": "yarn mocha -r ts-node/register --config mocha-config.json",
"test:typecheck": "tsc --noEmit",
"lint": "eslint src --ext ts,js,tsx,jsx",
"compile": "tsc",
"build": "tsc",
@ -30,5 +31,8 @@
"jsonwebtoken": "^8.5.1",
"linkedom": "^0.14.26",
"urlsafe-base64": "^1.0.0"
},
"volta": {
"extends": "../../package.json"
}
}

View file

@ -28,11 +28,9 @@ export const Button = styled('button', {
color: '#3D3D3D',
bg: '#FFEA9F',
p: '10px 15px',
'&:hover': {
'&:hover, &:focus': {
bg: '$omnivoreCtaYellow',
},
'&:focus': {
border: '1px solid $omnivoreCtaYellow',
outline: '1px solid $omnivoreCtaYellow',
},
},
cancelGeneric: {
@ -45,18 +43,13 @@ export const Button = styled('button', {
border: '1px solid transparent',
p: '10px 15px',
bg: 'transparent',
'&:hover': {
'&:hover, &:focus': {
bg: '#EBEBEB',
},
'&:focus': {
outline: 'none !important',
border: '1px solid $omnivoreCtaYellow',
outline: '1px solid $omnivoreCtaYellow',
},
},
ctaOutlineYellow: {
boxSizing: 'border-box',
'-moz-box-sizing': 'border-box',
'-webkit-box-sizing': 'border-box',
borderColor: 'unset',
border: '1px solid $omnivoreCtaYellow',
fontSize: '14px',
@ -159,7 +152,6 @@ export const Button = styled('button', {
outlineColor: 'rgba(0, 0, 0, 0)',
border: '1px solid rgba(0, 0, 0, 0.06)',
cursor: 'pointer',
'&:focus': { outline: 'none' },
},
ctaModal: {
height: '32px',
@ -172,7 +164,6 @@ export const Button = styled('button', {
border: '1px solid $grayBorder',
cursor: 'pointer',
borderRadius: '8px',
'&:focus': { outline: 'none' },
},
ctaSecondary: {
color: '$grayText',
@ -245,7 +236,6 @@ export const Button = styled('button', {
'&:hover': {
opacity: 0.7,
},
'&:focus': { outline: 'none' },
},
highlightBarIcon: {
p: '0px',
@ -257,7 +247,6 @@ export const Button = styled('button', {
opacity: 0.5,
},
'&:focus': { outline: 'none' },
},
articleActionIcon: {
bg: 'transparent',

View file

@ -11,37 +11,35 @@ type HeaderNavLinkProps = {
export function HeaderNavLink(props: HeaderNavLinkProps): JSX.Element {
return (
<Link passHref href={props.href}>
<a style={{ textDecoration: 'none' }}>
<VStack
alignment="center"
distribution="center"
<Link passHref href={props.href} style={{ textDecoration: 'none' }}>
<VStack
alignment="center"
distribution="center"
css={{
cursor: 'pointer',
px: '$3',
color: 'inherit',
textDecoration: 'inherit',
fontFamily: 'inherit',
fontSize: '100%',
}}
>
<StyledText style="navLink">{props.text}</StyledText>
<Box
css={{
cursor: 'pointer',
px: '$3',
color: 'inherit',
textDecoration: 'inherit',
fontFamily: 'inherit',
fontSize: '100%',
width: '100%',
bg: 'rgb(255, 210, 52)',
height: '2px',
opacity: props.isActive ? 1 : 0,
display: props.isActive ? 'unset' : 'none',
mt: '4px',
animation: `${expandWidthAnim('0%', '100%')} 0.2s ease-out`,
'&:hover': {
opacity: props.isActive ? 0.7 : 0,
},
}}
>
<StyledText style="navLink">{props.text}</StyledText>
<Box
css={{
width: '100%',
bg: 'rgb(255, 210, 52)',
height: '2px',
opacity: props.isActive ? 1 : 0,
display: props.isActive ? 'unset' : 'none',
mt: '4px',
animation: `${expandWidthAnim('0%', '100%')} 0.2s ease-out`,
'&:hover': {
opacity: props.isActive ? 0.7 : 0,
},
}}
/>
</VStack>
</a>
/>
</VStack>
</Link>
)
}

View file

@ -1,4 +1,4 @@
import React, { useRef, useState } from 'react'
import { useRef, useState } from 'react'
import { styled } from '../tokens/stitches.config'
import { Box, HStack } from './LayoutPrimitives'
import { StyledText } from './StyledText'
@ -6,9 +6,14 @@ import {
LabelColorDropdownProps,
LabelOptionProps,
} from '../../utils/settings-page/labels/types'
import { TwitterPicker } from 'react-color'
import { TwitterPicker as TwitterPicker_, TwitterPickerProps } from 'react-color'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
// TwitterPicker is a Class component, but the types are broken in React 18.
// TODO: Maybe move away from this component, since it hasn't been updated for 3 years.
// https://github.com/casesandberg/react-color/issues/883
const TwitterPicker = TwitterPicker_ as unknown as React.FunctionComponent<TwitterPickerProps>
const DropdownMenuContent = styled(DropdownMenuPrimitive.Content, {
borderRadius: 6,
backgroundColor: '$grayBg',

View file

@ -1,4 +1,4 @@
import AutosizeInput from 'react-input-autosize'
import AutosizeInput_, { AutosizeInputProps } from 'react-input-autosize'
import { Box, SpanBox } from './LayoutPrimitives'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Label } from '../../lib/networking/fragments/labelFragment'
@ -8,6 +8,11 @@ import { EditLabelChip } from './EditLabelChip'
import { LabelsDispatcher } from '../../lib/hooks/useSetPageLabels'
import { EditLabelChipStack } from './EditLabelChipStack'
// AutosizeInput is a Class component, but the types are broken in React 18.
// TODO: Maybe move away from this component, since it hasn't been updated for 3 years.
// https://github.com/JedWatson/react-input-autosize/issues
const AutosizeInput = AutosizeInput_ as unknown as React.FunctionComponent<AutosizeInputProps>
const MaxUnstackedLabels = 7
type LabelsPickerProps = {

View file

@ -40,14 +40,14 @@ const InternalOrExternalLink = (props: InternalOrExternalLinkProps) => {
}}
>
{!isExternal ? (
<Link href={props.link}>{props.children}</Link>
<Link href={props.link} legacyBehavior>{props.children}</Link>
) : (
<a href={props.link} target="_blank" rel="noreferrer">
{props.children}
</a>
)}
</SpanBox>
)
);
}
export const SuggestionBox = (props: SuggestionBoxProps) => {

View file

@ -53,6 +53,7 @@ export const TooltipContent = StyledContent
export const TooltipArrow = StyledArrow
type TooltipWrappedProps = {
children: React.ReactNode;
tooltipContent: string;
active?: boolean;
tooltipSide?: TooltipPrimitive.TooltipContentProps['side']

View file

@ -1,5 +1,5 @@
import Image from 'next/image'
export function ChromeIcon(): JSX.Element {
return <Image src="/static/icons/chrome@2x.png" width="24" height="24" />
return <Image src="/static/icons/chrome@2x.png" width="24" height="24" alt="" />
}

View file

@ -1,5 +1,5 @@
import Image from 'next/image'
export function EdgeIcon(): JSX.Element {
return <Image src="/static/icons/edge@2x.png" width="24" height="24" />
return <Image src="/static/icons/edge@2x.png" width="24" height="24" alt="" />
}

View file

@ -1,5 +1,5 @@
import Image from 'next/image'
export function FirefoxIcon(): JSX.Element {
return <Image src="/static/icons/firefox@2x.png" width="24" height="24" />
return <Image src="/static/icons/firefox@2x.png" width="24" height="24" alt="" />
}

View file

@ -14,20 +14,22 @@ export function OmnivoreFestiveLogo(
const href = props.href || '/home'
return (
<Link passHref href={href}>
<a
style={{
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
}}
>
<Image
src="/static/images/omnivore-logo-santa.png"
width="27"
height="27"
/>
</a>
</Link>
)
(<Link
passHref
href={href}
style={{
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
}}>
<Image
src="/static/images/omnivore-logo-santa.png"
width="27"
height="27"
alt=""
/>
</Link>)
);
}

View file

@ -1,8 +1,5 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ReactChildren } from 'react'
import { config } from '../../tokens/stitches.config'
export type OmnivoreLogoBaseProps = {
color?: string
href?: string
@ -15,23 +12,25 @@ export function OmnivoreLogoBase(props: OmnivoreLogoBaseProps): JSX.Element {
const router = useRouter()
return (
<Link passHref href={href}>
<a
style={{
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
}}
onClick={(event) => {
const query = window.sessionStorage.getItem('q')
if (query) {
router.push(`/home?${query}`)
event.preventDefault()
}
}}
>
{props.children}
</a>
<Link
passHref
href={href}
style={{
textDecoration: 'none',
display: 'flex',
alignItems: 'center',
}}
onClick={(event) => {
const query = window.sessionStorage.getItem('q')
if (query) {
router.push(`/home?${query}`)
event.preventDefault()
}
}}
tabIndex={-1}
aria-label="Omnivore logo"
>
{props.children}
</Link>
)
}

View file

@ -1,5 +1,5 @@
import Image from 'next/image'
export function SafariIcon(): JSX.Element {
return <Image src="/static/icons/safari@2x.png" width="24" height="24" />
return <Image src="/static/icons/safari@2x.png" width="24" height="24" alt="" />
}

View file

@ -40,7 +40,7 @@ type NoteSectionProps = {
export function ArticleNotes(props: NoteSectionProps): JSX.Element {
const saveText = useCallback(
(text) => {
(text: string) => {
props.saveText(text)
},
[props]
@ -78,7 +78,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const saveText = useCallback(
(text) => {
(text: string) => {
;(async () => {
const success = await updateHighlightMutation({
annotation: text,

View file

@ -48,7 +48,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
const [errorSaving, setErrorSaving] = useState<string | undefined>(undefined)
const saveText = useCallback(
(text, updateTime, interactive) => {
(text: string, updateTime: Date, interactive: boolean) => {
;(async () => {
const success = await updateHighlightMutation({
annotation: text,

View file

@ -30,10 +30,10 @@ export function LibraryHighlightGridCard(
props: LibraryHighlightGridCardProps
): JSX.Element {
const [expanded, setExpanded] = useState(false)
const higlightCount = props.item.highlights?.length ?? 0
const highlightCount = props.item.highlights?.length ?? 0
const router = useRouter()
const viewInReader = useCallback(
(highlightId) => {
(highlightId: string) => {
if (!router || !router.isReady || !props.viewer) {
showErrorToast('Error navigating to highlight')
return
@ -197,7 +197,7 @@ export function LibraryHighlightGridCard(
event.preventDefault()
}}
>
{`View ${higlightCount} highlight${higlightCount > 1 ? 's' : ''}`}
{`View ${highlightCount} highlight${highlightCount > 1 ? 's' : ''}`}
<CaretDown
size={10}
weight="bold"

View file

@ -33,13 +33,14 @@ export function About(props: AboutProps): JSX.Element {
}}
>
<Box
as="p"
css={{
fontWeight: '700',
color: '#3D3D3D',
fontSize: 45,
lineHeight: '53px',
padding: '10px',
paddingBottom: '0px',
padding: '10px 10px 0',
margin: 0,
textAlign: 'center',
}}
>
@ -49,9 +50,11 @@ export function About(props: AboutProps): JSX.Element {
readers.`}
</Box>
<Box
as="p"
css={{
color: 'rgb(125, 125, 125)',
padding: '10px',
margin: 0,
textAlign: 'center',
width: '100%',
fontWeight: '600',
@ -64,9 +67,11 @@ export function About(props: AboutProps): JSX.Element {
</Box>
<Box
as="p"
css={{
color: 'rgb(125, 125, 125)',
padding: '10px',
margin: 0,
textAlign: 'center',
}}
>

View file

@ -32,11 +32,11 @@ export function ErrorLayout(props: ErrorLayoutProps): JSX.Element {
</StyledText>
</HStack>
<SpanBox css={{ height: '64px' }} />
<Link passHref href={viewerData?.me ? '/home' : '/login'}>
<Link passHref href={viewerData?.me ? '/home' : '/login'} legacyBehavior>
<Button style="ctaDarkYellow">
{viewerData?.me ? 'Go Home' : 'Login'}
</Button>
</Link>
</VStack>
)
);
}

View file

@ -50,21 +50,21 @@ export function LoginForm(props: LoginFormProps): JSX.Element {
>
Save articles and read them later in our distraction-free reader.
</StyledText>
<Link passHref href="/about">
<a style={{ textDecoration: 'none' }}>
<StyledText
css={{
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '120%',
m: '0px',
color: '$omnivoreGray',
}}
>
Learn More -&gt;
</StyledText>
</a>
<Link passHref href="/about" style={{ textDecoration: 'none' }}>
<StyledText
css={{
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '120%',
m: '0px',
color: '$omnivoreGray',
}}
>
Learn More -&gt;
</StyledText>
</Link>
<SpanBox css={{ height: '24px' }} />
@ -103,7 +103,7 @@ export function LoginForm(props: LoginFormProps): JSX.Element {
/>
</Box>
)}
<Link href="/auth/email-login" passHref>
<Link href="/auth/email-login" passHref legacyBehavior>
<StyledTextSpan
style="actionLink"
css={{ color: '$omnivoreGray', pt: '12px' }}
@ -114,7 +114,7 @@ export function LoginForm(props: LoginFormProps): JSX.Element {
</VStack>
<TermAndConditionsFooter />
</VStack>
)
);
}
function GoogleAuthButton() {
@ -154,17 +154,17 @@ export function TermAndConditionsFooter(): JSX.Element {
}}
>
By signing up, you agree to Omnivores{' '}
<Link href="/terms" passHref>
<Link href="/terms" passHref legacyBehavior>
<StyledTextSpan style="captionLink" css={{ color: '$omnivoreGray' }}>
Terms of Service
</StyledTextSpan>
</Link>{' '}
and{' '}
<Link href="/privacy" passHref>
<Link href="/privacy" passHref legacyBehavior>
<StyledTextSpan style="captionLink" css={{ color: '$omnivoreGray' }}>
Privacy Policy
</StyledTextSpan>
</Link>
</StyledText>
)
);
}

View file

@ -1,3 +1,4 @@
import { unstable_getImgProps as getImgProps } from 'next/image'
import {
Box,
HStack,
@ -9,6 +10,9 @@ import type { LoginFormProps } from './LoginForm'
import { OmnivoreNameLogo } from '../elements/images/OmnivoreNameLogo'
import { theme } from '../tokens/stitches.config'
import featureFullWidthImage from '../../public/static/images/login/login-feature-image-full.png'
import featureHalfWidthImage from '../../public/static/images/login/login-feature-image-half.png'
export function LoginLayout(props: LoginFormProps): JSX.Element {
return (
<>
@ -86,7 +90,30 @@ function MediumLoginLayout(props: LoginFormProps) {
)
}
const srcSetToImageSet = (srcFallback: string, srcSet?: string): string => {
if (!srcSet) return `url(${srcFallback})`
return `image-set( ${srcSet
.split(', ')
.map((subSrc) => {
const [src, resolution] = subSrc.split(' ')
return `url("${decodeURIComponent(src)}") ${resolution}`
})
.join(',')}
)`
}
function OmnivoreIllustration() {
const { props: halfWidthImgProps } = getImgProps({
src: featureHalfWidthImage,
alt: '',
})
const { props: fullWidthImgProps } = getImgProps({
src: featureFullWidthImage,
alt: '',
})
return (
<Box
css={{
@ -95,14 +122,12 @@ function OmnivoreIllustration() {
marginLeft: 'auto',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundImage: `-webkit-image-set(
url('/static/images/landingPage-feature@1x.png') 1x,
url('/static/landing/landingPage-feature@2x.png') 2x
)`,
'background-image': `image-set(
url('/static/images/landingPage-feature@1x.png') 1x,
url('/static/landing/landingPage-feature@2x.png') 2x
)`,
backgroundPosition: 'left',
backgroundImage: srcSetToImageSet(halfWidthImgProps.src, halfWidthImgProps.srcSet),
'@media (min-aspect-ratio: 2/1)': {
backgroundImage: srcSetToImageSet(fullWidthImgProps.src, fullWidthImgProps.srcSet),
},
}}
/>
)

View file

@ -204,7 +204,7 @@ function SettingsButton(props: SettingsButtonProps): JSX.Element {
}, [props, router])
return (
<Link href={props.destination} passHref title={props.name}>
<Link href={props.destination} passHref title={props.name} legacyBehavior>
<SpanBox
css={{
mx: '10px',
@ -249,5 +249,5 @@ function SettingsButton(props: SettingsButtonProps): JSX.Element {
{props.name}
</SpanBox>
</Link>
)
);
}

View file

@ -110,7 +110,7 @@ export function UploadModal(props: UploadModalProps): JSX.Element {
const dropzoneRef = useRef<DropzoneRef | null>(null)
const openDialog = useCallback(
(event) => {
(event: React.MouseEvent) => {
if (dropzoneRef.current) {
dropzoneRef.current.open()
}

View file

@ -163,7 +163,7 @@ export function NotebookContent(props: NotebookContentProps): JSX.Element {
}, [highlights])
const handleSaveNoteText = useCallback(
(text) => {
(text: string) => {
const changeTime = new Date()
setLastChanged(changeTime)

View file

@ -49,7 +49,7 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
props.onClose(allAnnotations ?? [], deletedHighlights ?? [])
}, [props, allAnnotations])
const handleAnnotationsChange = useCallback((allAnnotations) => {
const handleAnnotationsChange = useCallback((allAnnotations: Highlight[]) => {
setAllAnnotations(allAnnotations)
}, [])
@ -66,7 +66,7 @@ export function NotebookModal(props: NotebookModalProps): JSX.Element {
}, [allAnnotations])
const viewInReader = useCallback(
(highlightId) => {
(highlightId: string) => {
props.viewHighlightInReader(highlightId)
handleClose()
},

View file

@ -292,7 +292,7 @@ function FontControls(props: FontControlsProps): JSX.Element {
})
const handleFontSizeChange = useCallback(
(value) => {
(value: number) => {
readerSettings.actionHandler('setFontSize', value)
},
[readerSettings]
@ -399,7 +399,7 @@ function LayoutControls(props: LayoutControlsProps): JSX.Element {
const { readerSettings } = props
const handleMarginWidthChange = useCallback(
(value) => {
(value: number) => {
readerSettings.setMarginWidth(value)
},
[readerSettings]

View file

@ -122,7 +122,7 @@ export function EmailLogin(): JSX.Element {
}}
>
Don&apos;t have an account?{' '}
<Link href="/auth/email-signup" passHref>
<Link href="/auth/email-signup" passHref legacyBehavior>
<StyledTextSpan style="actionLink" css={{ color: '$omnivoreGray' }}>
Sign up
</StyledTextSpan>
@ -140,7 +140,7 @@ export function EmailLogin(): JSX.Element {
}}
>
Forgot your password?{' '}
<Link href="/auth/forgot-password" passHref>
<Link href="/auth/forgot-password" passHref legacyBehavior>
<StyledTextSpan style="actionLink" css={{ color: '$omnivoreGray' }}>
Click here
</StyledTextSpan>
@ -148,5 +148,5 @@ export function EmailLogin(): JSX.Element {
</StyledText>
</VStack>
</form>
)
);
}

View file

@ -206,7 +206,7 @@ export function EmailSignup(): JSX.Element {
}}
>
Already have an account?{' '}
<Link href="/auth/email-login" passHref>
<Link href="/auth/email-login" passHref legacyBehavior>
<StyledTextSpan style="actionLink" css={{ color: '$omnivoreGray' }}>
Login instead
</StyledTextSpan>
@ -215,5 +215,5 @@ export function EmailSignup(): JSX.Element {
<TermAndConditionsFooter />
</VStack>
</form>
)
);
}

View file

@ -108,7 +108,7 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
<DropdownSeparator />
<Link
href={`/${props.viewer.profile.username}/${props.item.slug}#${props.highlight.id}`}
>
legacyBehavior>
<StyledLinkItem
onClick={(event) => {
console.log('event.ctrlKey: ', event.ctrlKey, event.metaKey)
@ -129,7 +129,7 @@ export function HighlightsMenu(props: HighlightsMenuProps): JSX.Element {
</Link>
</Dropdown>
</VStack>
)
);
}
const sortHighlights = (highlights: Highlight[]) => {

View file

@ -368,7 +368,7 @@ function HighlightList(props: HighlightListProps): JSX.Element {
}, [props.item.node.highlights])
const viewInReader = useCallback(
(highlightId) => {
(highlightId: string) => {
if (!router || !router.isReady || !props.viewer) {
showErrorToast('Error navigating to highlight')
return

View file

@ -351,7 +351,7 @@ export function HomeFeedContainer(): JSX.Element {
}, [libraryItems, activeCardId])
const getItem = useCallback(
(itemId) => {
(itemId: string) => {
return libraryItems.find((item) => item.node.id === itemId)
},
[libraryItems]

View file

@ -553,7 +553,7 @@ type EditButtonProps = {
function EditButton(props: EditButtonProps): JSX.Element {
return (
<Link href={props.destination} passHref>
<Link href={props.destination} passHref legacyBehavior>
<SpanBox
css={{
ml: '10px',
@ -584,5 +584,5 @@ function EditButton(props: EditButtonProps): JSX.Element {
{props.title}
</SpanBox>
</Link>
)
);
}

View file

@ -140,7 +140,7 @@ export function Webhooks(): JSX.Element {
}}
>
<h3>{item.method}</h3>
<p>{item.createdAt}</p>
<p>{item.createdAt?.toLocaleDateString()}</p>
</Box>
</Box>
)

View file

@ -1,4 +1,4 @@
import { HStack, VStack } from '../../elements/LayoutPrimitives'
import { Box, HStack, VStack } from '../../elements/LayoutPrimitives'
import { styled } from '../../tokens/stitches.config'
import { StyledText } from '../../elements/StyledText'
@ -30,11 +30,29 @@ export function LandingFooter(): JSX.Element {
textDecoration: 'underline',
},
},
'@mdDown': {
columns: 2,
width: '100%',
marginTop: "8px",
marginBottom: "30px",
},
})
return (
<HStack css={containerStyles}>
<HStack css={{ width: '100%', maxWidth: '1224px' }}>
<Box
css={{
display: 'flex',
flexDirection: 'column',
width: '100%',
maxWidth: '1024px',
'@md': {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
},
}}
>
<VStack>
<StyledText style="aboutFooter">Install</StyledText>
<FooterList>
@ -77,7 +95,7 @@ export function LandingFooter(): JSX.Element {
</a>
</li>
<li>
<a href="mailto:feedback@omnivore.app">Contact us via email</a>
<a href="mailto:feedback@omnivore.app">Contact&nbsp;us via&nbsp;email</a>
</li>
<li>
<a href="https://discord.gg/h2z5rppzz9">
@ -112,7 +130,7 @@ export function LandingFooter(): JSX.Element {
</li>
</FooterList>
</VStack>
</HStack>
</Box>
</HStack>
)
}

View file

@ -1,16 +1,18 @@
import { Box } from '../../elements/LayoutPrimitives'
import { OmnivoreNameLogo } from '../../elements/images/OmnivoreNameLogo'
import { Button } from '../../elements/Button'
import Link from 'next/link'
const LoginButton = (): JSX.Element => {
return (
<Button
as={Link}
href="/login"
style="ctaDarkYellow"
css={{
display: 'flex',
marginLeft: 'auto',
borderRadius: 4,
border: 'unset',
background: 'unset',
color: '#3D3D3D',
height: '42px',
@ -19,10 +21,8 @@ const LoginButton = (): JSX.Element => {
fontWeight: 'normal',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={(e) => {
document.location.href = '/login'
e.preventDefault()
textDecoration: "none",
transition: "all ease-in 50ms"
}}
>
Login

View file

@ -1,50 +1,11 @@
import { HStack, VStack, Box } from '../../elements/LayoutPrimitives'
type LandingSectionProps = {
export interface LandingSectionProps {
titleText: string
descriptionText: React.ReactElement
descriptionText: React.ReactElement | string | number
icon?: React.ReactElement
image: React.ReactElement
}
const titleTextStyles = {
fontWeight: '700',
color: '#3D3D3D',
lineHeight: 1.25,
'@mdDown': {
fontSize: 24,
},
'@md': {
fontSize: '$5',
},
'@xl': {
fontSize: 45,
},
}
const imageContainerStyles = {
display: 'flex',
width: '49%',
alignSelf: 'center',
justifyContent: 'center',
'@md': {
marginBottom: '60px',
},
'@mdDown': {
width: '100%',
},
}
const layoutStyles = {
width: '49%',
alignSelf: 'start',
'@mdDown': {
width: '100%',
paddingTop: '30px',
},
paddingLeft: '30px',
paddingRight: '30px',
paddingBottom: '30px',
imagePosition?: 'left' | 'right'
}
export function LandingSection(props: LandingSectionProps): JSX.Element {
@ -53,24 +14,72 @@ export function LandingSection(props: LandingSectionProps): JSX.Element {
css={{
width: '100%',
flexWrap: 'wrap',
flexDirection: 'row-reverse',
flexDirection: (props?.imagePosition ?? 'left') === 'left' ? 'row-reverse' : 'row',
marginBottom: 20,
'@mdDown': {
width: '100%',
},
}}
>
<VStack distribution="center" alignment={'center'} css={layoutStyles}>
<Box css={titleTextStyles}>{props.titleText}</Box>
<VStack
distribution="center"
alignment="center"
css={{
width: '49%',
alignSelf: 'start',
'@mdDown': {
width: '100%',
paddingTop: '30px',
},
paddingLeft: '30px',
paddingRight: '30px',
paddingBottom: '30px',
}}
>
<Box
as="h2"
css={{
color: 'rgb(125, 125, 125)',
fontWeight: '700',
color: '#3D3D3D',
lineHeight: 1.25,
'@mdDown': {
fontSize: 24,
},
'@md': {
fontSize: '$5',
},
'@xl': {
fontSize: 45,
},
}}
>
{props.titleText}
</Box>
<Box
as="p"
css={{
color: '#666',
}}
>
{props.descriptionText}
</Box>
</VStack>
<Box css={imageContainerStyles}>{props.image}</Box>
<Box
css={{
display: 'flex',
width: '49%',
alignSelf: 'center',
justifyContent: 'center',
'@md': {
marginBottom: '60px',
},
'@mdDown': {
width: '100%',
},
}}
>
{props.image}
</Box>
</HStack>
)
}

View file

@ -1,29 +1,35 @@
import Image from 'next/image'
import { VStack, Box } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { LandingSection } from './LandingSection'
type GetStartedButtonProps = {
lang: 'en' | 'zh'
}
import landingPageHeroImage from '../../../public/static/images/landing/landing-00-hero.png'
import landingSection1Image from '../../../public/static/images/landing/landing-01-save-it-now.png'
import landingSection2Image from '../../../public/static/images/landing/landing-02-newsletters.png'
import landingSection3Image from '../../../public/static/images/landing/landing-03-organisation.png'
import landingSection4Image from '../../../public/static/images/landing/landing-04-highlights-and-notes.png'
import landingSection5Image from '../../../public/static/images/landing/landing-05-sync.png'
import landingSection6Image from '../../../public/static/images/landing/landing-06-tts.png'
import landingSection7Image from '../../../public/static/images/landing/landing-07-oss.png'
import Link from 'next/link'
export function GetStartedButton(props: GetStartedButtonProps): JSX.Element {
export function GetStartedButton(props: { lang: 'en' | 'zh' }): JSX.Element {
return (
<Button
as={Link}
href="/login"
style="ctaDarkYellow"
css={{
display: 'flex',
borderRadius: 4,
background: 'rgb(255, 210, 52)',
background: '$omnivoreCtaYellow',
padding: '12px 25px',
color: '#3D3D3D',
width: '172px',
height: '42px',
alignItems: 'center',
justifyContent: 'center',
fontWeight: '600',
}}
onClick={(e) => {
document.location.href = '/login'
e.preventDefault()
textDecoration: 'none',
transition: 'background-color ease-out 50ms',
'&:hover': {
backgroundColor: '$omnivoreYellow',
},
}}
>
{props.lang == 'zh' ? `免费注册` : `Sign Up for Free`}
@ -50,38 +56,6 @@ const containerStyles = {
},
}
const callToActionStyles = {
background: 'white',
borderRadius: '24px',
boxSizing: 'border-box',
border: '1px solid #D8D7D5',
boxShadow:
'0px 7px 8px rgba(32, 31, 29, 0.03), 0px 18px 24px rgba(32, 31, 29, 0.03)',
padding: 40,
marginTop: 64,
minheight: 330,
width: 'inherit',
'@md': {
width: '100%',
},
'@xl': {
width: '95%',
},
}
const callToActionText = {
color: '#3D3D3D',
fontWeight: '700',
fontSize: 64,
lineHeight: '1.25',
textAlign: 'center',
paddingBottom: '20px',
'@mdDown': {
fontSize: '32px',
},
}
type LandingSectionsContainerProps = {
lang: 'en' | 'zh'
}
@ -98,7 +72,8 @@ const sections = [
titleText: `先保存,后阅读`,
descriptionText: `看到有趣的内容但没时间阅读不论是文章、PDF或是推特线程只需将它们保存下来等稍后有空再阅读。Omnivore 应用程序适用于iOS、Android 和主要网络浏览扩展程序。`,
},
imageIdx: `03`,
image: landingSection1Image,
imageAlt: '',
},
{
en: {
@ -111,7 +86,8 @@ const sections = [
titleText: `集合邮件订阅`,
descriptionText: `您不再需要到不同收件箱提取订阅的邮件,只要将它们发送到 Omnivore Library即可在同一处随心阅读不受其他电子邮件或 substack 的干扰。`,
},
imageIdx: `04`,
image: landingSection2Image,
imageAlt: '',
},
{
en: {
@ -125,7 +101,8 @@ const sections = [
titleText: `按喜好组织阅读系统`,
descriptionText: `我们不会限定您如何组织系统,只提供您所需的工具,如标签、过滤器和完整的文本索引搜索,让您按自己的喜好和需求设定组织规则。`,
},
imageIdx: `05`,
image: landingSection3Image,
imageAlt: '',
},
{
en: {
@ -139,7 +116,8 @@ const sections = [
titleText: `添加高亮和注释`,
descriptionText: `想提高阅读效率?积极动用大脑,为关键的段落添加高亮或注释,能提高您阅读记忆的保留。这些标注将永久保存在文件里,方便您随时搜索使用。`,
},
imageIdx: `06`,
image: landingSection4Image,
imageAlt: '',
},
{
en: {
@ -152,7 +130,8 @@ const sections = [
titleText: `与您的“第二大脑”同步`,
descriptionText: `Omnivore 应用程序能与个人知识管理系统如 Logseq 和 Obsidian 同步,让您轻而易举地综合所有保存文章、高亮和注释。`,
},
imageIdx: `07`,
image: landingSection5Image,
imageAlt: '',
},
{
en: {
@ -165,8 +144,8 @@ const sections = [
titleText: `使用 text-to-speech 功能聆听阅读`,
descriptionText: `使用TTS逼真、自然的人工智能声音为您阅读待读列表中的读物让眼睛好好休息一下。这便是我们iOS 版Omnivore 应用程序的独家功能。`,
},
imageIdx: `08`,
maxWidth: '85%',
image: landingSection6Image,
imageAlt: '',
},
{
en: {
@ -180,7 +159,8 @@ const sections = [
titleText: `开源软件给予您控制权`,
descriptionText: `阅读是终身的活动,不应担心失去自己多年辛苦建立的图书馆。我们的开源平台,就是为了确保您的阅读不会受限于任何专有系统。`,
},
imageIdx: `09`,
image: landingSection7Image,
imageAlt: '',
},
]
export function LandingSectionsContainer(
@ -198,42 +178,65 @@ export function LandingSectionsContainer(
},
}}
>
<img
height="647"
width="1015"
srcSet="/static/landing/landingPage-feature@1x.png,
/static/landing/landingPage-feature@2x.png 2x,
/static/landing/landingPage-feature@3x.png 3x"
alt="landingHero-1"
<Image
src={landingPageHeroImage}
alt="Hero image"
sizes="(max-width: 1024px) 100vw"
style={{
width: '85%',
maxWidth: '85%',
height: 'auto',
}}
priority
/>
</Box>
{sections.map((section) => {
{sections.map((section, sectionIndex) => {
return (
<LandingSection
key={section.imageIdx}
key={sectionIndex}
titleText={section[props.lang].titleText}
descriptionText={<p>{section[props.lang].descriptionText}</p>}
descriptionText={section[props.lang].descriptionText}
imagePosition={sectionIndex % 2 ? 'left' : 'right'}
image={
<img
srcSet={`/static/landing/landingPage-${section.imageIdx}@1x.png,
/static/landing/landingPage-${section.imageIdx}@2x.png 2x,
/static/landing/landingPage-${section.imageIdx}@3x.png 3x`}
alt={`landing-${section.imageIdx}`}
style={{ maxWidth: section.maxWidth ?? '100%' }}
<Image
alt={section.imageAlt}
src={section.image}
sizes="(max-width: 512px) 50vw, (max-width: 512px) 100vw"
style={{ maxWidth: '100%', height: 'auto' }}
/>
}
/>
)
})}
<VStack alignment="center" css={callToActionStyles}>
<VStack alignment="center" css={{
width: "100vw",
backgroundColor: "#fff",
paddingBottom: "40px",
marginTop: "40px",
borderTop: "1px solid var(--colors-omnivoreYellow)",
borderBottom: "1px solid var(--colors-omnivoreYellow)",
"@md": {
marginTop: 0,
}
}}>
{props.lang == 'en' && (
<Box css={callToActionText}>Get Started With Omnivore Today</Box>
<Box
as="p"
css={{
color: '#3D3D3D',
fontWeight: '700',
fontSize: '2.5rem',
lineHeight: '1.25',
textAlign: 'center',
marginBottom: '40px',
'@mdDown': {
fontSize: '2rem',
},
}}
>
Get Started With Omnivore Today
</Box>
)}
<GetStartedButton lang={props.lang} />
</VStack>

View file

@ -269,7 +269,7 @@ const darkThemeSpec = {
labelButtonsBg: '#5F5E58',
// New theme, special naming to keep things straigh
// New theme, special naming to keep things straight
// once all switch over, we will rename
// DARK
colorScheme: 'dark',

View file

@ -23,7 +23,7 @@ function disableWordSnap(str: string): boolean {
}
const isWhitespace = (c?: string): boolean => {
return !!c && /\u2014|\u2013|,|\s/.test(c)
return !!c && /\u2014|\u2013|,|\s/.test(c);
}
function findNextWord(

View file

@ -25,7 +25,7 @@ export function useSelection(
)
const handleFinishTouch = useCallback(
async (mouseEvent) => {
async (mouseEvent: any) => {
let wasDragEvent = false
const tapAttributes = {
tapX: mouseEvent.clientX,

View file

@ -105,7 +105,7 @@ export const useKeyboardShortcuts = (commands: KeyboardCommand[]): void => {
)
const keydownListener = useCallback(
(keydownEvent) => {
(keydownEvent: any) => {
const { target } = keydownEvent
if (!keydownEvent.key) return
const key = keydownEvent.key.toLowerCase()
@ -129,7 +129,7 @@ export const useKeyboardShortcuts = (commands: KeyboardCommand[]): void => {
)
const keyupListener = useCallback(
(keyupEvent) => {
(keyupEvent: any) => {
if (!keyupEvent.key) return
const key = keyupEvent.key.toLowerCase()
if (keys[key] === undefined) return

View file

@ -17,6 +17,7 @@ const ContentSecurityPolicy = `
const moduleExports = {
images: {
formats: ['image/avif', 'image/webp'],
domains: [
'proxy-demo.omnivore-image-cache.app',
'proxy-dev.omnivore-image-cache.app',
@ -56,7 +57,7 @@ const moduleExports = {
},
],
},
]
];
},
async redirects() {
return [

View file

@ -13,6 +13,7 @@
"test": "jest",
"test:watch": "jest --watch",
"test:build": "jest && next build",
"test:typecheck": "tsc --noEmit",
"upgrade-psdpdfkit": "cp -R '../../node_modules/pspdfkit/dist/pspdfkit-lib' public/pspdfkit-lib",
"storybook": "start-storybook -p 6006 -s ./public",
"build-storybook": "build-storybook -s public"
@ -32,7 +33,6 @@
"@radix-ui/react-tooltip": "^0.1.7",
"@sentry/nextjs": "^7.42.0",
"@stitches/react": "^1.2.5",
"@types/react-input-autosize": "^2.2.1",
"antd": "4.24.3",
"axios": "^1.2.0",
"color2k": "^2.0.0",
@ -47,16 +47,16 @@
"markdown-it": "^13.0.1",
"match-sorter": "^6.3.1",
"nanoid": "^3.1.29",
"next": "^12.1.0",
"next": "^13.5.6",
"node-html-markdown": "^1.3.0",
"papaparse": "^5.4.1",
"phosphor-react": "^1.4.0",
"posthog-js": "^1.78.2",
"pspdfkit": "^2022.2.3",
"react": "^17.0.2",
"react": "^18.2.0",
"react-color": "^2.19.3",
"react-colorful": "^5.5.1",
"react-dom": "^17.0.2",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-hot-toast": "^2.1.1",
"react-input-autosize": "^3.0.0",
@ -68,6 +68,7 @@
"react-super-responsive-table": "^5.2.1",
"react-topbar-progress-indicator": "^4.1.1",
"remark-gfm": "^3.0.1",
"sharp": "^0.32.6",
"swr": "^1.0.1",
"uuid": "^8.3.2",
"yet-another-react-lightbox": "^3.12.0"
@ -93,14 +94,14 @@
"@types/lodash.debounce": "^4.0.6",
"@types/markdown-it": "^12.2.3",
"@types/papaparse": "^5.3.7",
"@types/react": "17.0.2",
"@types/react-color": "^3.0.6",
"@types/react-dom": "^17.0.2",
"@types/react": "^18.2.0",
"@types/react-color": "^3.0.9",
"@types/react-dom": "^18.2.0",
"@types/react-input-autosize": "^2.2.1",
"@types/uuid": "^8.3.1",
"babel-jest": "^27.4.5",
"babel-loader": "^8.2.3",
"eslint-config-next": "12.0.7",
"eslint-config-next": "^13.5.6",
"eslint-plugin-functional": "^4.0.2",
"eslint-plugin-react": "^7.28.0",
"graphql": "^15.6.1",
@ -108,7 +109,6 @@
"storybook-addon-next-router": "^3.1.1"
},
"volta": {
"node": "18.16.1",
"yarn": "1.22.10"
"extends": "../../package.json"
}
}
}

View file

@ -11,7 +11,7 @@ export default function LandingPage(): JSX.Element {
description="Omnivore is the free, open source, read-it-later app for serious readers."
/>
<About lang="en" />
<About lang="zh" />
</>
)
}

View file

@ -4,8 +4,11 @@ import { locale, timeZone } from '../../lib/dateFormatting'
import { SaveResponseData } from '../../lib/networking/mutations/saveUrlMutation'
import { ssrFetcher } from '../../lib/networking/networkHelpers'
type Request = NextApiRequest & { cookies: { [key: string]: string } }
type Response = NextApiResponse
const saveUrl = async (
req: NextApiRequest,
req: Request,
url: URL,
labels: string[] | undefined,
state: string | undefined,
@ -53,11 +56,7 @@ const saveUrl = async (
}
}
// eslint-disable-next-line import/no-anonymous-default-export
export default async (
req: NextApiRequest,
res: NextApiResponse
): Promise<void> => {
export default async function handler(req: Request, res: Response) {
const urlStr = req.query['url']
if (req.query['labels'] && typeof req.query['labels'] === 'string') {
req.query['labels'] = [req.query['labels']]

View file

@ -32,7 +32,7 @@ export default function InvitePage(): JSX.Element {
}, [isLoading, router, viewerData, viewerDataError])
const acceptClicked = useCallback(
(event) => {
(event: any) => {
event?.stopPropagation()
if (!router.isReady) {
@ -63,108 +63,106 @@ export default function InvitePage(): JSX.Element {
[router, inviteCode]
)
return (
<>
<PageMetaData title="Accept Invite - Omnivore" path="/invite" />
<ProfileLayout>
<VStack
alignment="center"
return <>
<PageMetaData title="Accept Invite - Omnivore" path="/invite" />
<ProfileLayout>
<VStack
alignment="center"
css={{
padding: '16px',
background: 'white',
minWidth: '340px',
width: '70vw',
maxWidth: '576px',
borderRadius: '8px',
border: '1px solid #3D3D3D',
boxShadow: '#B1B1B1 9px 9px 9px -9px',
}}
>
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>
You&apos;re invited
</StyledText>
<StyledText
style="action"
css={{
padding: '16px',
background: 'white',
minWidth: '340px',
width: '70vw',
maxWidth: '576px',
borderRadius: '8px',
border: '1px solid #3D3D3D',
boxShadow: '#B1B1B1 9px 9px 9px -9px',
mt: '0px',
pt: '4px',
width: '100%',
color: '$omnivoreLightGray',
textAlign: 'center',
whiteSpace: 'normal',
}}
>
<StyledText style="subHeadline" css={{ color: '$omnivoreGray' }}>
You&apos;re invited
</StyledText>
You have been invited to join a recommendation group on Omnivore.
Recommendation groups allow you to share articles with other group
members.
</StyledText>
{errorMessage && (
<StyledText
style="action"
style="error"
css={{
mt: '0px',
pt: '4px',
width: '100%',
color: '$omnivoreLightGray',
textAlign: 'center',
whiteSpace: 'normal',
}}
>
You have been invited to join a recommendation group on Omnivore.
Recommendation groups allow you to share articles with other group
members.
{errorMessage}
</StyledText>
{errorMessage && (
<StyledText
style="error"
css={{
mt: '0px',
pt: '4px',
width: '100%',
textAlign: 'center',
whiteSpace: 'normal',
}}
>
{errorMessage}
</StyledText>
)}
)}
<HStack
alignment="center"
distribution="center"
css={{
gap: '10px',
width: '100%',
height: '80px',
}}
>
{viewerData?.me ? (
<Button
type="submit"
style={'ctaDarkYellow'}
onClick={acceptClicked}
>
Accept Invite
</Button>
) : (
<Button
type="submit"
style={'ctaDarkYellow'}
onClick={() => router.push('/login')}
>
Login
</Button>
)}
</HStack>
<StyledText
style="action"
css={{
m: '0px',
pt: '16px',
width: '100%',
color: '$omnivoreLightGray',
textAlign: 'center',
whiteSpace: 'normal',
}}
>
Don&apos;t have an Omnivore account?{' '}
<Link href="/login" passHref>
<StyledTextSpan
style="actionLink"
css={{ color: '$omnivoreGray' }}
>
Signup
</StyledTextSpan>
</Link>
</StyledText>
</VStack>
<div data-testid="invite-page-tag" />
</ProfileLayout>
</>
)
<HStack
alignment="center"
distribution="center"
css={{
gap: '10px',
width: '100%',
height: '80px',
}}
>
{viewerData?.me ? (
<Button
type="submit"
style={'ctaDarkYellow'}
onClick={acceptClicked}
>
Accept Invite
</Button>
) : (
<Button
type="submit"
style={'ctaDarkYellow'}
onClick={() => router.push('/login')}
>
Login
</Button>
)}
</HStack>
<StyledText
style="action"
css={{
m: '0px',
pt: '16px',
width: '100%',
color: '$omnivoreLightGray',
textAlign: 'center',
whiteSpace: 'normal',
}}
>
Don&apos;t have an Omnivore account?{' '}
<Link href="/login" passHref legacyBehavior>
<StyledTextSpan
style="actionLink"
css={{ color: '$omnivoreGray' }}
>
Signup
</StyledTextSpan>
</Link>
</StyledText>
</VStack>
<div data-testid="invite-page-tag" />
</ProfileLayout>
</>;
}

View file

@ -114,145 +114,143 @@ export default function EmailsPage(): JSX.Element {
return emailAddresses.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
}, [emailAddresses])
return (
<>
<SettingsTable
pageId="settings-emails-tag"
pageInfoLink="https://docs.omnivore.app/using/inbox.html"
headerTitle="Address"
createTitle="Create a new email address"
createAction={createEmail}
suggestionInfo={{
title: 'Subscribe to newsletters with an Omnivore Email Address',
message:
'Create an Omnivore email address and use it to subscribe to newsletters or send yourself documents. Newsletters and documents will be categorized and added to your library when we receive a message. View all received emails with the "Recently Received Emails" link at the bottom of this page.',
docs: 'https://docs.omnivore.app/using/inbox.html',
key: '--settings-emails-show-help',
CTAText: 'Create an email address',
onClickCTA: () => {
createEmail()
return <>
<SettingsTable
pageId="settings-emails-tag"
pageInfoLink="https://docs.omnivore.app/using/inbox.html"
headerTitle="Address"
createTitle="Create a new email address"
createAction={createEmail}
suggestionInfo={{
title: 'Subscribe to newsletters with an Omnivore Email Address',
message:
'Create an Omnivore email address and use it to subscribe to newsletters or send yourself documents. Newsletters and documents will be categorized and added to your library when we receive a message. View all received emails with the "Recently Received Emails" link at the bottom of this page.',
docs: 'https://docs.omnivore.app/using/inbox.html',
key: '--settings-emails-show-help',
CTAText: 'Create an email address',
onClickCTA: () => {
createEmail()
},
}}
>
{sortedEmailAddresses.length > 0 ? (
sortedEmailAddresses.map((email, i) => {
return (
<SettingsTableRow
key={email.address}
title={email.address}
isLast={i === sortedEmailAddresses.length - 1}
onDelete={() => setConfirmDeleteEmailId(email.id)}
deleteTitle="Delete"
sublineElement={
<StyledText
css={{
my: '5px',
fontSize: '11px',
a: {
color: '$omnivoreCtaYellow',
},
}}
>
{`created ${formattedShortDate(email.createdAt)}, `}
<Link href="/settings/subscriptions" legacyBehavior>{`${email.subscriptionCount} subscriptions`}</Link>
</StyledText>
}
titleElement={
<CopyTextBtnWrapper
css={{
marginLeft: '20px',
'@mdDown': {
marginRight: '10px',
},
}}
>
<CopyTextButton
text={email.address}
type={TextType.EmailAddress}
/>
</CopyTextBtnWrapper>
}
extraElement={
email.confirmationCode ? (
<HStack
alignment="start"
distribution="center"
css={{
width: '100%',
backgroundColor: '$grayBgActive',
borderRadius: '6px',
padding: '4px 4px 4px 0px',
'@md': {
width: '30%',
backgroundColor: 'transparent',
},
}}
>
<>
<StyledText
css={{
fontSize: '11px',
'@md': {
marginTop: '5px',
},
'@mdDown': {
marginLeft: 'auto',
},
marginRight: '10px',
}}
>
{`Gmail: ${email.confirmationCode}`}
</StyledText>
<Box>
<CopyTextBtnWrapper>
<CopyTextButton
text={email.confirmationCode || ''}
type={TextType.ConfirmationCode}
/>
</CopyTextBtnWrapper>
</Box>
</>
</HStack>
) : (
<></>
)
}
/>
);
})
) : (
<EmptySettingsRow
text={isValidating ? '-' : 'No Email Addresses Found'}
/>
)}
<SpanBox
css={{
pt: '15px',
fontSize: '12px',
marginLeft: 'auto',
a: {
color: '$omnivoreCtaYellow',
},
}}
>
{sortedEmailAddresses.length > 0 ? (
sortedEmailAddresses.map((email, i) => {
return (
<SettingsTableRow
key={email.address}
title={email.address}
isLast={i === sortedEmailAddresses.length - 1}
onDelete={() => setConfirmDeleteEmailId(email.id)}
deleteTitle="Delete"
sublineElement={
<StyledText
css={{
my: '5px',
fontSize: '11px',
a: {
color: '$omnivoreCtaYellow',
},
}}
>
{`created ${formattedShortDate(email.createdAt)}, `}
<Link href="/settings/subscriptions">{`${email.subscriptionCount} subscriptions`}</Link>
</StyledText>
}
titleElement={
<CopyTextBtnWrapper
css={{
marginLeft: '20px',
'@mdDown': {
marginRight: '10px',
},
}}
>
<CopyTextButton
text={email.address}
type={TextType.EmailAddress}
/>
</CopyTextBtnWrapper>
}
extraElement={
email.confirmationCode ? (
<HStack
alignment="start"
distribution="center"
css={{
width: '100%',
backgroundColor: '$grayBgActive',
borderRadius: '6px',
padding: '4px 4px 4px 0px',
'@md': {
width: '30%',
backgroundColor: 'transparent',
},
}}
>
<>
<StyledText
css={{
fontSize: '11px',
'@md': {
marginTop: '5px',
},
'@mdDown': {
marginLeft: 'auto',
},
marginRight: '10px',
}}
>
{`Gmail: ${email.confirmationCode}`}
</StyledText>
<Box>
<CopyTextBtnWrapper>
<CopyTextButton
text={email.confirmationCode || ''}
type={TextType.ConfirmationCode}
/>
</CopyTextBtnWrapper>
</Box>
</>
</HStack>
) : (
<></>
)
}
/>
)
})
) : (
<EmptySettingsRow
text={isValidating ? '-' : 'No Email Addresses Found'}
/>
)}
<SpanBox
css={{
pt: '15px',
fontSize: '12px',
marginLeft: 'auto',
a: {
color: '$omnivoreCtaYellow',
},
}}
>
<Link href="/settings/emails/recent">
View recently received emails
</Link>
</SpanBox>
</SettingsTable>
<Link href="/settings/emails/recent">
View recently received emails
</Link>
</SpanBox>
</SettingsTable>
{confirmDeleteEmailId ? (
<ConfirmationModal
message={
'Are you sure? You will stop receiving emails sent to this address.'
}
onAccept={async () => {
await deleteEmail(confirmDeleteEmailId)
setConfirmDeleteEmailId(undefined)
}}
onOpenChange={() => setConfirmDeleteEmailId(undefined)}
/>
) : null}
</>
)
{confirmDeleteEmailId ? (
<ConfirmationModal
message={
'Are you sure? You will stop receiving emails sent to this address.'
}
onAccept={async () => {
await deleteEmail(confirmDeleteEmailId)
setConfirmDeleteEmailId(undefined)
}}
onOpenChange={() => setConfirmDeleteEmailId(undefined)}
/>
) : null}
</>;
}

View file

@ -86,7 +86,7 @@ export default function SubscriptionsPage(): JSX.Element {
at{' '}
<Link
href={`/settings/emails?address=${subscription.newsletterEmail}`}
>
legacyBehavior>
{subscription.newsletterEmail}
</Link>
</>
@ -101,7 +101,7 @@ export default function SubscriptionsPage(): JSX.Element {
</StyledText>
}
/>
)
);
})
) : (
<EmptySettingsRow
@ -125,5 +125,5 @@ export default function SubscriptionsPage(): JSX.Element {
) : null}
</>
</SettingsTable>
)
);
}

View file

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 200 KiB

View file

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 345 KiB

View file

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View file

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View file

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

View file

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View file

Before

Width:  |  Height:  |  Size: 487 KiB

After

Width:  |  Height:  |  Size: 487 KiB

View file

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 626 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 839 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Some files were not shown because too many files have changed in this diff Show more