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
105 changed files with 1831 additions and 780 deletions

View File

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

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,37 +11,35 @@ type HeaderNavLinkProps = {
export function HeaderNavLink(props: HeaderNavLinkProps): JSX.Element { export function HeaderNavLink(props: HeaderNavLinkProps): JSX.Element {
return ( return (
<Link passHref href={props.href}> <Link passHref href={props.href} style={{ textDecoration: 'none' }}>
<a style={{ textDecoration: 'none' }}> <VStack
<VStack alignment="center"
alignment="center" distribution="center"
distribution="center" css={{
cursor: 'pointer',
px: '$3',
color: 'inherit',
textDecoration: 'inherit',
fontFamily: 'inherit',
fontSize: '100%',
}}
>
<StyledText style="navLink">{props.text}</StyledText>
<Box
css={{ css={{
cursor: 'pointer', width: '100%',
px: '$3', bg: 'rgb(255, 210, 52)',
color: 'inherit', height: '2px',
textDecoration: 'inherit', opacity: props.isActive ? 1 : 0,
fontFamily: 'inherit', display: props.isActive ? 'unset' : 'none',
fontSize: '100%', mt: '4px',
animation: `${expandWidthAnim('0%', '100%')} 0.2s ease-out`,
'&:hover': {
opacity: props.isActive ? 0.7 : 0,
},
}} }}
> />
<StyledText style="navLink">{props.text}</StyledText> </VStack>
<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>
</Link> </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 { styled } from '../tokens/stitches.config'
import { Box, HStack } from './LayoutPrimitives' import { Box, HStack } from './LayoutPrimitives'
import { StyledText } from './StyledText' import { StyledText } from './StyledText'
@ -6,9 +6,14 @@ import {
LabelColorDropdownProps, LabelColorDropdownProps,
LabelOptionProps, LabelOptionProps,
} from '../../utils/settings-page/labels/types' } 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' 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, { const DropdownMenuContent = styled(DropdownMenuPrimitive.Content, {
borderRadius: 6, borderRadius: 6,
backgroundColor: '$grayBg', 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 { Box, SpanBox } from './LayoutPrimitives'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Label } from '../../lib/networking/fragments/labelFragment' import { Label } from '../../lib/networking/fragments/labelFragment'
@ -8,6 +8,11 @@ import { EditLabelChip } from './EditLabelChip'
import { LabelsDispatcher } from '../../lib/hooks/useSetPageLabels' import { LabelsDispatcher } from '../../lib/hooks/useSetPageLabels'
import { EditLabelChipStack } from './EditLabelChipStack' 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 const MaxUnstackedLabels = 7
type LabelsPickerProps = { type LabelsPickerProps = {

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import Image from 'next/image' import Image from 'next/image'
export function ChromeIcon(): JSX.Element { 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' import Image from 'next/image'
export function EdgeIcon(): JSX.Element { 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' import Image from 'next/image'
export function FirefoxIcon(): JSX.Element { 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' const href = props.href || '/home'
return ( return (
<Link passHref href={href}> (<Link
<a passHref
style={{ href={href}
textDecoration: 'none', style={{
display: 'flex', textDecoration: 'none',
alignItems: 'center', display: 'flex',
}} alignItems: 'center',
> }}>
<Image
src="/static/images/omnivore-logo-santa.png" <Image
width="27" src="/static/images/omnivore-logo-santa.png"
height="27" width="27"
/> height="27"
</a> alt=""
</Link> />
)
</Link>)
);
} }

View File

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

View File

@ -1,5 +1,5 @@
import Image from 'next/image' import Image from 'next/image'
export function SafariIcon(): JSX.Element { 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 { export function ArticleNotes(props: NoteSectionProps): JSX.Element {
const saveText = useCallback( const saveText = useCallback(
(text) => { (text: string) => {
props.saveText(text) props.saveText(text)
}, },
[props] [props]
@ -78,7 +78,7 @@ export function HighlightViewNote(props: HighlightViewNoteProps): JSX.Element {
const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined) const [lastSaved, setLastSaved] = useState<Date | undefined>(undefined)
const saveText = useCallback( const saveText = useCallback(
(text) => { (text: string) => {
;(async () => { ;(async () => {
const success = await updateHighlightMutation({ const success = await updateHighlightMutation({
annotation: text, annotation: text,

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import { unstable_getImgProps as getImgProps } from 'next/image'
import { import {
Box, Box,
HStack, HStack,
@ -9,6 +10,9 @@ import type { LoginFormProps } from './LoginForm'
import { OmnivoreNameLogo } from '../elements/images/OmnivoreNameLogo' import { OmnivoreNameLogo } from '../elements/images/OmnivoreNameLogo'
import { theme } from '../tokens/stitches.config' 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 { export function LoginLayout(props: LoginFormProps): JSX.Element {
return ( 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() { function OmnivoreIllustration() {
const { props: halfWidthImgProps } = getImgProps({
src: featureHalfWidthImage,
alt: '',
})
const { props: fullWidthImgProps } = getImgProps({
src: featureFullWidthImage,
alt: '',
})
return ( return (
<Box <Box
css={{ css={{
@ -95,14 +122,12 @@ function OmnivoreIllustration() {
marginLeft: 'auto', marginLeft: 'auto',
backgroundSize: 'cover', backgroundSize: 'cover',
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
backgroundImage: `-webkit-image-set( backgroundPosition: 'left',
url('/static/images/landingPage-feature@1x.png') 1x, backgroundImage: srcSetToImageSet(halfWidthImgProps.src, halfWidthImgProps.srcSet),
url('/static/landing/landingPage-feature@2x.png') 2x
)`, '@media (min-aspect-ratio: 2/1)': {
'background-image': `image-set( backgroundImage: srcSetToImageSet(fullWidthImgProps.src, fullWidthImgProps.srcSet),
url('/static/images/landingPage-feature@1x.png') 1x, },
url('/static/landing/landingPage-feature@2x.png') 2x
)`,
}} }}
/> />
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -140,7 +140,7 @@ export function Webhooks(): JSX.Element {
}} }}
> >
<h3>{item.method}</h3> <h3>{item.method}</h3>
<p>{item.createdAt}</p> <p>{item.createdAt?.toLocaleDateString()}</p>
</Box> </Box>
</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 { styled } from '../../tokens/stitches.config'
import { StyledText } from '../../elements/StyledText' import { StyledText } from '../../elements/StyledText'
@ -30,11 +30,29 @@ export function LandingFooter(): JSX.Element {
textDecoration: 'underline', textDecoration: 'underline',
}, },
}, },
'@mdDown': {
columns: 2,
width: '100%',
marginTop: "8px",
marginBottom: "30px",
},
}) })
return ( return (
<HStack css={containerStyles}> <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> <VStack>
<StyledText style="aboutFooter">Install</StyledText> <StyledText style="aboutFooter">Install</StyledText>
<FooterList> <FooterList>
@ -77,7 +95,7 @@ export function LandingFooter(): JSX.Element {
</a> </a>
</li> </li>
<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>
<li> <li>
<a href="https://discord.gg/h2z5rppzz9"> <a href="https://discord.gg/h2z5rppzz9">
@ -112,7 +130,7 @@ export function LandingFooter(): JSX.Element {
</li> </li>
</FooterList> </FooterList>
</VStack> </VStack>
</HStack> </Box>
</HStack> </HStack>
) )
} }

View File

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

View File

@ -1,50 +1,11 @@
import { HStack, VStack, Box } from '../../elements/LayoutPrimitives' import { HStack, VStack, Box } from '../../elements/LayoutPrimitives'
type LandingSectionProps = { export interface LandingSectionProps {
titleText: string titleText: string
descriptionText: React.ReactElement descriptionText: React.ReactElement | string | number
icon?: React.ReactElement icon?: React.ReactElement
image: React.ReactElement image: React.ReactElement
} imagePosition?: 'left' | 'right'
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',
} }
export function LandingSection(props: LandingSectionProps): JSX.Element { export function LandingSection(props: LandingSectionProps): JSX.Element {
@ -53,24 +14,72 @@ export function LandingSection(props: LandingSectionProps): JSX.Element {
css={{ css={{
width: '100%', width: '100%',
flexWrap: 'wrap', flexWrap: 'wrap',
flexDirection: 'row-reverse', flexDirection: (props?.imagePosition ?? 'left') === 'left' ? 'row-reverse' : 'row',
marginBottom: 20, marginBottom: 20,
'@mdDown': { '@mdDown': {
width: '100%', width: '100%',
}, },
}} }}
> >
<VStack distribution="center" alignment={'center'} css={layoutStyles}> <VStack
<Box css={titleTextStyles}>{props.titleText}</Box> distribution="center"
alignment="center"
css={{
width: '49%',
alignSelf: 'start',
'@mdDown': {
width: '100%',
paddingTop: '30px',
},
paddingLeft: '30px',
paddingRight: '30px',
paddingBottom: '30px',
}}
>
<Box <Box
as="h2"
css={{ 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} {props.descriptionText}
</Box> </Box>
</VStack> </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> </HStack>
) )
} }

View File

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

View File

@ -269,7 +269,7 @@ const darkThemeSpec = {
labelButtonsBg: '#5F5E58', 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 // once all switch over, we will rename
// DARK // DARK
colorScheme: 'dark', colorScheme: 'dark',

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -114,145 +114,143 @@ export default function EmailsPage(): JSX.Element {
return emailAddresses.sort((a, b) => a.createdAt.localeCompare(b.createdAt)) return emailAddresses.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
}, [emailAddresses]) }, [emailAddresses])
return ( return <>
<> <SettingsTable
<SettingsTable pageId="settings-emails-tag"
pageId="settings-emails-tag" pageInfoLink="https://docs.omnivore.app/using/inbox.html"
pageInfoLink="https://docs.omnivore.app/using/inbox.html" headerTitle="Address"
headerTitle="Address" createTitle="Create a new email address"
createTitle="Create a new email address" createAction={createEmail}
createAction={createEmail} suggestionInfo={{
suggestionInfo={{ title: 'Subscribe to newsletters with an Omnivore Email Address',
title: 'Subscribe to newsletters with an Omnivore Email Address', message:
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.',
'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',
docs: 'https://docs.omnivore.app/using/inbox.html', key: '--settings-emails-show-help',
key: '--settings-emails-show-help', CTAText: 'Create an email address',
CTAText: 'Create an email address', onClickCTA: () => {
onClickCTA: () => { createEmail()
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 ? ( <Link href="/settings/emails/recent">
sortedEmailAddresses.map((email, i) => { View recently received emails
return ( </Link>
<SettingsTableRow </SpanBox>
key={email.address} </SettingsTable>
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>
{confirmDeleteEmailId ? ( {confirmDeleteEmailId ? (
<ConfirmationModal <ConfirmationModal
message={ message={
'Are you sure? You will stop receiving emails sent to this address.' 'Are you sure? You will stop receiving emails sent to this address.'
} }
onAccept={async () => { onAccept={async () => {
await deleteEmail(confirmDeleteEmailId) await deleteEmail(confirmDeleteEmailId)
setConfirmDeleteEmailId(undefined) setConfirmDeleteEmailId(undefined)
}} }}
onOpenChange={() => setConfirmDeleteEmailId(undefined)} onOpenChange={() => setConfirmDeleteEmailId(undefined)}
/> />
) : null} ) : null}
</> </>;
)
} }

View File

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