diff --git a/packages/web/components/patterns/DropdownMenu.tsx b/packages/web/components/patterns/DropdownMenu.tsx index 248b692de..6d72af9cd 100644 --- a/packages/web/components/patterns/DropdownMenu.tsx +++ b/packages/web/components/patterns/DropdownMenu.tsx @@ -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" /> + {/* props.actionHandler('navigate-to-integrations')} + title="Integrations" + /> */} window.Intercom('show')} title="Feedback" diff --git a/packages/web/components/patterns/PrimaryHeader.tsx b/packages/web/components/patterns/PrimaryHeader.tsx index bf70b245e..cb8f65a81 100644 --- a/packages/web/components/patterns/PrimaryHeader.tsx +++ b/packages/web/components/patterns/PrimaryHeader.tsx @@ -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 diff --git a/packages/web/components/templates/integrations/Readwise.tsx b/packages/web/components/templates/integrations/Readwise.tsx new file mode 100644 index 000000000..da5e62472 --- /dev/null +++ b/packages/web/components/templates/integrations/Readwise.tsx @@ -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('') + const [errorMessage, setErrorMessage] = + useState(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 ( + + + integration Image +
Readwise
+
+ + + Enter your API key from Readwise below. You can get your token{' '} + + here + . + + + { + 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 && {errorMessage}} + +
+ ) +} diff --git a/packages/web/components/templates/integrations/Webhooks.tsx b/packages/web/components/templates/integrations/Webhooks.tsx new file mode 100644 index 000000000..0a9acd18d --- /dev/null +++ b/packages/web/components/templates/integrations/Webhooks.tsx @@ -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() + 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 ( + + + integration Image +
Webhooks
+ + + + +
+ + + 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. + + + + + + +
+ ) +} diff --git a/packages/web/lib/networking/mutations/setIntegrationMutation.ts b/packages/web/lib/networking/mutations/setIntegrationMutation.ts new file mode 100644 index 000000000..8b387ac9d --- /dev/null +++ b/packages/web/lib/networking/mutations/setIntegrationMutation.ts @@ -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 { + 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 +} diff --git a/packages/web/lib/networking/queries/useGetIntegrationsQuery.tsx b/packages/web/lib/networking/queries/useGetIntegrationsQuery.tsx new file mode 100644 index 000000000..6c0aafbae --- /dev/null +++ b/packages/web/lib/networking/queries/useGetIntegrationsQuery.tsx @@ -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: () => {}, + } +} diff --git a/packages/web/pages/settings/integrations.tsx b/packages/web/pages/settings/integrations.tsx new file mode 100644 index 000000000..d59320c9e --- /dev/null +++ b/packages/web/pages/settings/integrations.tsx @@ -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() + ) + 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: , + 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: , + 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: , + style: 'ctaWhite', + action: () =>router.push("/settings/integrations/webhooks"), + }, + }, + ]) + }, [readwiseConnected, integrations, webhooks]) + + return ( + + +
Integrations
+ + Connect with other applications can help enhance and streamline your + experience with Omnivore, below are some useful apps to connect your + Omnivore account to. + + +
Applications
+ + {integrationsArray.map((item) => { + return ( + + integration Image + +

{item.title}

+

{item.subText}

+
+ + + +
+ ) + })} +
+
+ ) +} diff --git a/packages/web/pages/settings/integrations/readwise.tsx b/packages/web/pages/settings/integrations/readwise.tsx new file mode 100644 index 000000000..a8fcc5c5c --- /dev/null +++ b/packages/web/pages/settings/integrations/readwise.tsx @@ -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 ( + <> + + + + +
+ + ) +} diff --git a/packages/web/pages/settings/integrations/webhooks.tsx b/packages/web/pages/settings/integrations/webhooks.tsx new file mode 100644 index 000000000..212ff18cf --- /dev/null +++ b/packages/web/pages/settings/integrations/webhooks.tsx @@ -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 ( + <> + + + + +
+ + ) +} diff --git a/packages/web/public/static/icons/logseq.svg b/packages/web/public/static/icons/logseq.svg new file mode 100644 index 000000000..9aaf43643 --- /dev/null +++ b/packages/web/public/static/icons/logseq.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/web/public/static/icons/readwise.svg b/packages/web/public/static/icons/readwise.svg new file mode 100644 index 000000000..d40b7fa81 --- /dev/null +++ b/packages/web/public/static/icons/readwise.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/web/public/static/icons/webhooks.svg b/packages/web/public/static/icons/webhooks.svg new file mode 100644 index 000000000..81377821e --- /dev/null +++ b/packages/web/public/static/icons/webhooks.svg @@ -0,0 +1,9 @@ + + + + + + + + +