Merge pull request #1325 from omnivore-app/feat/integrations-page

This commit is contained in:
Jackson Harper 2022-10-26 18:59:20 +08:00 committed by GitHub
commit 32edce9771
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 656 additions and 0 deletions

View file

@ -21,6 +21,7 @@ export type HeaderDropdownAction =
| 'navigate-to-profile'
| 'navigate-to-subscriptions'
| 'navigate-to-api'
| 'navigate-to-integrations'
| 'increaseFontSize'
| 'decreaseFontSize'
| 'logout'
@ -87,6 +88,10 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
onSelect={() => props.actionHandler('navigate-to-api')}
title="API Keys"
/>
{/* <DropdownOption
onSelect={() => props.actionHandler('navigate-to-integrations')}
title="Integrations"
/> */}
<DropdownOption
onSelect={() => window.Intercom('show')}
title="Feedback"

View file

@ -95,6 +95,9 @@ export function PrimaryHeader(props: HeaderProps): JSX.Element {
case 'navigate-to-api':
router.push('/settings/api')
break
case 'navigate-to-integrations':
router.push('/settings/integrations')
break
case 'logout':
props.setShowLogoutConfirmation(true)
break

View file

@ -0,0 +1,128 @@
import { useCallback, useMemo, useState } from 'react'
import { styled } from '@stitches/react'
import Link from 'next/link'
import Image from 'next/image'
import { Box, HStack, VStack } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { StyledText } from '../../elements/StyledText'
import { FormInput } from '../../elements/FormElements'
import { setIntegrationMutation } from '../../../lib/networking/mutations/setIntegrationMutation'
import { Integration, useGetIntegrationsQuery } from '../../../lib/networking/queries/useGetIntegrationsQuery'
import { Router, useRouter } from 'next/router'
import { showSuccessToast } from '../../../lib/toastHelpers'
// Styles
const Header = styled(Box, {
color: '$utilityTextDefault',
fontSize: 'x-large',
margin: '20px',
})
export function Readwise(): JSX.Element {
const router = useRouter()
const [token, setToken] = useState<string>('')
const [errorMessage, setErrorMessage] =
useState<string | undefined>(undefined)
const setReadwiseToken = useCallback(async () => {
try {
const result = await setIntegrationMutation({
token,
type: 'READWISE',
enabled: true,
})
router.push(`/settings/integrations`)
showSuccessToast('Your Readwise API token has been set.')
} catch (err) {
setErrorMessage('Error: ' + err)
}
}, [token])
return (
<VStack
distribution={'start'}
alignment={'start'}
css={{
margin: '0 auto',
width: '80%',
height: '500px',
}}
>
<HStack
alignment={'start'}
distribution={'start'}
css={{
width: '100%',
pb: '$2',
borderBottom: '1px solid $utilityTextDefault',
pr: '$1',
}}
>
<Image
src="/static/icons/readwise.svg"
alt="integration Image"
width={75}
height={75}
/>
<Header>Readwise</Header>
</HStack>
<HStack
css={{
fontSize: '18px',
color: '$utilityTextDefault',
m: '20px',
whiteSpace: 'pre-wrap'
}}
>
Enter your API key from Readwise below. You can get your token{' '}
<Link
style={{ color: '$utilityTextDefault' }}
href="https://readwise.io/access_token"
>
here
</Link>.
</HStack>
<FormInput
type="token"
key="token"
value={token}
placeholder={'Readwise Token'}
onChange={(e) => {
e.preventDefault()
setToken(e.target.value)
}}
disabled={false}
hidden={false}
required={true}
css={{
border: '1px solid $textNonessential',
borderRadius: '8px',
width: '80%',
bg: 'transparent',
fontSize: '16px',
textIndent: '8px',
margin: '20px',
height: '38px',
color: '$grayTextContrast',
'&:focus': {
outline: 'none',
boxShadow: '0px 0px 2px 2px rgba(255, 234, 159, 0.56)',
},
}}
min={200}
/>
{errorMessage && <StyledText style="error">{errorMessage}</StyledText>}
<Button
style="ctaDarkYellow"
css={{ my: '$2', margin: '20px' }}
onClick={setReadwiseToken}
>
Set Token
</Button>
</VStack>
)
}

View file

@ -0,0 +1,121 @@
import { styled } from '@stitches/react'
import Image from 'next/image'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { Plus } from 'phosphor-react'
import { useGetWebhooksQuery } from '../../../lib/networking/queries/useGetWebhooksQuery'
import { useMemo } from 'react'
// Styles
const Header = styled(Box, {
color: '$utilityTextDefault',
fontSize: 'x-large',
margin: '20px',
})
interface Webhook {
id?: string
url: string
eventTypes: string
contentType?: string
method?: string
enabled?: string
createdAt?: Date
updatedAt?: Date
}
export function Webhooks(): JSX.Element {
const { webhooks } = useGetWebhooksQuery()
const webhooksList = useMemo(() => {
const webhooksList = new Map<string, Webhook>()
webhooks.forEach((webhook) =>
webhooksList.set(webhook.id, {
url: webhook.url,
eventTypes: webhook.eventTypes.join(', '),
method: webhook.method,
contentType: webhook.contentType,
createdAt: webhook.createdAt,
})
)
return webhooksList
}, [webhooks])
console.log('webhooksList', webhooksList)
return (
<VStack
distribution={'start'}
css={{
width: '80%',
margin: '0 auto',
height: '500px',
}}
>
<HStack
alignment={'start'}
distribution={'start'}
css={{
width: '100%',
pb: '$2',
borderBottom: '1px solid $utilityTextDefault',
pr: '$1',
}}
>
<Image
src="/static/icons/webhooks.svg"
alt="integration Image"
width={75}
height={75}
/>
<Header>Webhooks</Header>
<HStack
alignment={'center'}
distribution={'end'}
css={{
width: '80%',
height: '100%',
}}
>
<Button
style="ctaDarkYellow"
css={{
py: '10px',
px: '14px',
}}
>
<Plus size={16} weight="bold" />
<SpanBox css={{ pl: '10px', fontWeight: '600', fontSize: '16px' }}>
Add New Webhook
</SpanBox>
</Button>
</HStack>
</HStack>
<HStack
css={{
fontSize: '16px',
color: '$utilityTextDefault',
m: '20px 0',
}}
>
Use Webhooks to Ut enim ad minim veniam, quis nostrud exercitation.
Tityre, tu patulae recubans sub tegmine fagi dolor. Etiam habebis sem
dicantur magna mollis euismod.
</HStack>
<HStack
css={{
border: '1px solid white',
height: '100%',
width: '100%',
}}>
</HStack>
</VStack>
)
}

View file

@ -0,0 +1,68 @@
import { gql } from 'graphql-request'
import { gqlFetcher } from '../networkHelpers'
import { IntegrationType } from '../queries/useGetIntegrationsQuery'
export type SetIntegrationInput = {
id?: string
type: IntegrationType
token: string,
enabled: boolean
}
type SetIntegrationResult = {
setIntegration?: SetIntegrationData
}
type SetIntegrationData = {
setIntegration?: SetIntegrationSuccess
errorCodes?: unknown[]
}
type SetIntegrationSuccess = {
integration: Integration
}
type Integration = {
id: string
type: IntegrationType
token: string
enabled: boolean
createdAt: Date
updatedAt: Date
}
export async function setIntegrationMutation(
input: SetIntegrationInput
): Promise<Integration | undefined> {
const mutation = gql`
mutation SetIntegration(
$input: SetIntegrationInput!
) {
setIntegration(input: $input) {
... on SetIntegrationSuccess {
integration {
id
type
token
enabled
createdAt
updatedAt
}
}
... on SetIntegrationError {
errorCodes
}
}
}
`
const data = await gqlFetcher(mutation, { input }) as SetIntegrationResult
const output = data as any
const error = data.setIntegration?.errorCodes?.find(() => true)
if (error) {
if (error === 'INVALID_TOKEN')
throw 'Your token is invalid.'
throw error
}
return output.setIntegration?.setIntegration?.integration
}

View file

@ -0,0 +1,76 @@
import { gql } from 'graphql-request'
import useSWR from 'swr'
import { publicGqlFetcher } from '../networkHelpers'
export interface Integration {
id: string
type: IntegrationType
token: string
enabled: boolean
createdAt: Date
updatedAt: Date
}
export type IntegrationType =
| 'READWISE'
interface IntegrationsQueryResponse {
isValidating: boolean
integrations: Integration[]
revalidate: () => void
}
interface IntegrationsQueryResponseData {
integrations: IntegrationsData
}
interface IntegrationsData {
integrations: unknown
}
export function useGetIntegrationsQuery(): IntegrationsQueryResponse {
const query = gql`
query GetIntegrations {
integrations {
... on IntegrationsSuccess {
integrations {
id
type
token
enabled
createdAt
updatedAt
}
}
... on IntegrationsError {
errorCodes
}
}
}
`
const { data, mutate, error, isValidating } = useSWR(query, publicGqlFetcher)
console.log('integrations data', data)
try {
if (data) {
const result = data as IntegrationsQueryResponseData
const integrations = result.integrations.integrations as Integration[]
return {
isValidating,
integrations,
revalidate: () => {
mutate()
},
}
}
} catch (error) {
console.log('error', error)
}
return {
isValidating: false,
integrations: [],
// eslint-disable-next-line @typescript-eslint/no-empty-function
revalidate: () => {},
}
}

View file

@ -0,0 +1,193 @@
import { useEffect, useMemo, useState} from 'react'
import { useRouter } from 'next/router'
import { styled } from '@stitches/react'
import { Toaster } from 'react-hot-toast'
import { DownloadSimple, Eye, Link } from 'phosphor-react'
import Image from 'next/image'
import {
Box,
HStack,
SpanBox,
VStack,
} from '../../components/elements/LayoutPrimitives'
import { PrimaryLayout } from '../../components/templates/PrimaryLayout'
import { applyStoredTheme } from '../../lib/themeUpdater'
import { Button } from '../../components/elements/Button'
import { useGetIntegrationsQuery } from '../../lib/networking/queries/useGetIntegrationsQuery'
import { useGetWebhooksQuery } from '../../lib/networking/queries/useGetWebhooksQuery'
// Styles
const Header = styled(Box, {
color: '$utilityTextDefault',
fontSize: 'x-large',
margin: '10px',
})
const Subheader = styled(Box, {
padding: '20px',
color: '$utilityTextDefault',
borderBottom: '1px solid $grayLine',
margin: '0 auto',
width: '80%',
})
//interface
interface Integrations {
id: string
}
type integrationsCard = {
icon: string
title: string
subText?: string
button: {
text: string
icon?: JSX.Element
style: string
action: () => void
}
}
export default function Integrations(): JSX.Element {
applyStoredTheme(false)
const { integrations } = useGetIntegrationsQuery()
const { webhooks } = useGetWebhooksQuery()
const [integrationsArray, setIntegrationsArray] = useState(
Array<integrationsCard>()
)
const router = useRouter()
const readwiseConnected = useMemo(() => {
return integrations.some((i) => i.type == 'READWISE')
}, [integrations])
useEffect(() => {
setIntegrationsArray([
{
icon: '/static/icons/logseq.svg',
title: 'Logseq',
subText: 'Logseq is an open-source knowledge base. Use the Omnivore Logseq plugin to sync articles, highlights, and notes to Logseq.',
button: {
text: `Install Logseq Plugin`,
icon: <DownloadSimple size={16} weight={'bold'} />,
style: 'ctaDarkYellow',
action: () => {
router.push(`https://github.com/omnivore-app/logseq-omnivore`)
}
},
},
{
icon: '/static/icons/readwise.svg',
title: 'ReadWise',
subText: 'Readwise makes it easy to revisit and learn from your ebook & article highlights. Use our Readwise integration to sync your highlights from Omnivore to Readwise.',
button: {
text: readwiseConnected ? 'Remove' : 'Connect to Readwise',
icon: <Link size={16} weight={'bold'} />,
style: readwiseConnected ? 'ctaWhite' : 'ctaDarkYellow',
action: () => router.push("/settings/integrations/readwise")
},
},
{
icon: '/static/icons/webhooks.svg',
title: 'Webhooks',
subText: `${webhooks.length} Webhooks`,
button: {
text: 'View Webhooks',
icon: <Eye size={16} weight={'bold'} />,
style: 'ctaWhite',
action: () =>router.push("/settings/integrations/webhooks"),
},
},
])
}, [readwiseConnected, integrations, webhooks])
return (
<PrimaryLayout pageTestId={'integrations'}>
<Toaster
containerStyle={{
top: '5rem',
}}
/>
<Header css={{ textAlign: 'center' }}>Integrations</Header>
<Subheader>
Connect with other applications can help enhance and streamline your
experience with Omnivore, below are some useful apps to connect your
Omnivore account to.
</Subheader>
<VStack
distribution={'start'}
css={{
width: '80%',
margin: '0 auto',
height: '500px',
}}
>
<Header>Applications</Header>
{integrationsArray.map((item) => {
return (
<HStack
key={item.title}
css={{
width: '100%',
borderRadius: '5px',
backgroundColor: '$grayBg',
margin: '10px 0',
padding: '20px',
display: 'flex',
alignItems: 'center',
'@smDown': {
flexWrap: 'wrap',
},
}}
>
<Image
src={item.icon}
alt="integration Image"
width={75}
height={75}
/>
<Box
css={{
width: '60%',
padding: '10px',
color: '$utilityTextDefault',
m: '10px',
'h3, p': {
margin: '0',
},
}}
>
<h3>{item.title}</h3>
<p>{item.subText}</p>
</Box>
<HStack>
<Button
style={
item.button.style === 'ctaDarkYellow'
? 'ctaDarkYellow'
: 'ctaWhite'
}
css={{
py: '10px',
px: '14px',
minWidth: '230px',
}}
onClick={item.button.action}
>
{item.button.icon}
<SpanBox
css={{ pl: '10px', fontWeight: '600', fontSize: '16px' }}
>
{item.button.text}
</SpanBox>
</Button>
</HStack>
</HStack>
)
})}
</VStack>
</PrimaryLayout>
)
}

View file

@ -0,0 +1,15 @@
import { PageMetaData } from '../../../components/patterns/PageMetaData'
import { Readwise } from '../../../components/templates/integrations/Readwise'
import { SettingsLayout } from '../../../components/templates/SettingsLayout'
export default function ReadwisePage(): JSX.Element {
return (
<>
<PageMetaData title="Readwise - Omnivore" path="/integrations/readwise" />
<SettingsLayout>
<Readwise />
</SettingsLayout>
<div data-testid="integrations-readwise-page-tag" />
</>
)
}

View file

@ -0,0 +1,15 @@
import { PageMetaData } from '../../../components/patterns/PageMetaData'
import { Webhooks } from '../../../components/templates/integrations/Webhooks'
import { SettingsLayout } from '../../../components/templates/SettingsLayout'
export default function ReadwisePage(): JSX.Element {
return (
<>
<PageMetaData title="Readwise - Omnivore" path="/integrations/webhooks" />
<SettingsLayout>
<Webhooks />
</SettingsLayout>
<div data-testid="integrations-readwise-page-tag" />
</>
)
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 364 KiB