Merge pull request #2481 from omnivore-app/feature/rss-ui
feature/rss ui
This commit is contained in:
@ -158,9 +158,9 @@ export const createApp = (): {
|
||||
app.use('/svc/pubsub/upload', uploadServiceRouter())
|
||||
app.use('/svc/pubsub/webhooks', webhooksServiceRouter())
|
||||
app.use('/svc/pubsub/integrations', integrationsServiceRouter())
|
||||
app.use('/svc/pubsub/rss-feed', rssFeedRouter())
|
||||
app.use('/svc/reminders', remindersServiceRouter())
|
||||
app.use('/svc/email-attachment', emailAttachmentRouter())
|
||||
app.use('/svc/rss-feed', rssFeedRouter())
|
||||
|
||||
if (env.dev.isLocal) {
|
||||
app.use('/local/debug', localDebugRouter())
|
||||
|
||||
@ -1,26 +1,35 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
import { Subscription } from '../queries/useGetSubscriptionsQuery'
|
||||
import {
|
||||
Subscription,
|
||||
SubscriptionType,
|
||||
} from '../queries/useGetSubscriptionsQuery'
|
||||
|
||||
type SubscribeResult = {
|
||||
subscribe: Subscribe
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
type Subscribe = {
|
||||
subscriptions: Subscription[]
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
export type SubscribeMutationInput = {
|
||||
name?: string
|
||||
url?: string
|
||||
subscriptionType?: SubscriptionType
|
||||
}
|
||||
|
||||
export async function subscribeMutation(
|
||||
subscribeName: string
|
||||
input: SubscribeMutationInput
|
||||
): Promise<any | undefined> {
|
||||
const mutation = gql`
|
||||
mutation {
|
||||
subscribe(name: "${subscribeName}") {
|
||||
mutation Subscribe($input: SubscribeInput!) {
|
||||
subscribe(input: $input) {
|
||||
... on SubscribeSuccess {
|
||||
subscriptions {
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
}
|
||||
... on SubscribeError {
|
||||
errorCodes
|
||||
@ -29,8 +38,8 @@ export async function subscribeMutation(
|
||||
}
|
||||
`
|
||||
try {
|
||||
const data = (await gqlFetcher(mutation)) as SubscribeResult
|
||||
return data.errorCodes ? undefined : data.subscribe
|
||||
const data = (await gqlFetcher(mutation, { input })) as SubscribeResult
|
||||
return data.subscribe.errorCodes ? undefined : data.subscribe
|
||||
} catch (error) {
|
||||
console.log('subscribeMutation error', error)
|
||||
return undefined
|
||||
|
||||
@ -4,19 +4,20 @@ import { Subscription } from '../queries/useGetSubscriptionsQuery'
|
||||
|
||||
type UnsubscribeResult = {
|
||||
unsubscribe: Unsubscribe
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
type Unsubscribe = {
|
||||
subscription: Subscription
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
export async function unsubscribeMutation(
|
||||
subscribeName: string
|
||||
subscribeName: string,
|
||||
id = ''
|
||||
): Promise<any | undefined> {
|
||||
const mutation = gql`
|
||||
mutation {
|
||||
unsubscribe(name: "${subscribeName}") {
|
||||
unsubscribe(name: "${subscribeName}", subscriptionId: "${id}") {
|
||||
... on UnsubscribeSuccess {
|
||||
subscription {
|
||||
id
|
||||
@ -31,7 +32,9 @@ export async function unsubscribeMutation(
|
||||
|
||||
try {
|
||||
const data = (await gqlFetcher(mutation)) as UnsubscribeResult
|
||||
return data.errorCodes ? undefined : data.unsubscribe.subscription.id
|
||||
return data.unsubscribe.errorCodes
|
||||
? undefined
|
||||
: data.unsubscribe.subscription.id
|
||||
} catch (error) {
|
||||
console.log('unsubscribeMutation error', error)
|
||||
return undefined
|
||||
|
||||
@ -4,10 +4,15 @@ import { publicGqlFetcher } from '../networkHelpers'
|
||||
|
||||
export type SubscriptionStatus = 'ACTIVE' | 'DELETED' | 'UNSUBSCRIBED'
|
||||
|
||||
export enum SubscriptionType {
|
||||
RSS = 'RSS',
|
||||
NEWSLETTER = 'NEWSLETTER',
|
||||
}
|
||||
|
||||
export type Subscription = {
|
||||
id: string
|
||||
name: string
|
||||
newsletterEmail: string
|
||||
newsletterEmail?: string
|
||||
|
||||
url?: string
|
||||
description?: string
|
||||
@ -15,6 +20,7 @@ export type Subscription = {
|
||||
status: SubscriptionStatus
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
lastFetchedAt?: string
|
||||
}
|
||||
|
||||
type SubscriptionsQueryResponse = {
|
||||
@ -31,10 +37,13 @@ type SubscriptionsData = {
|
||||
subscriptions: unknown
|
||||
}
|
||||
|
||||
export function useGetSubscriptionsQuery(): SubscriptionsQueryResponse {
|
||||
export function useGetSubscriptionsQuery(
|
||||
subscriptionType = SubscriptionType.NEWSLETTER,
|
||||
sortBy = 'UPDATED_TIME'
|
||||
): SubscriptionsQueryResponse {
|
||||
const query = gql`
|
||||
query GetSubscriptions {
|
||||
subscriptions(sort: { by: UPDATED_TIME }) {
|
||||
subscriptions(sort: { by: ${sortBy} }, type: ${subscriptionType}) {
|
||||
... on SubscriptionsSuccess {
|
||||
subscriptions {
|
||||
id
|
||||
@ -47,6 +56,7 @@ export function useGetSubscriptionsQuery(): SubscriptionsQueryResponse {
|
||||
unsubscribeHttpUrl
|
||||
createdAt
|
||||
updatedAt
|
||||
lastFetchedAt
|
||||
}
|
||||
}
|
||||
... on SubscriptionsError {
|
||||
|
||||
129
packages/web/pages/settings/rss/add.tsx
Normal file
129
packages/web/pages/settings/rss/add.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import { styled } from '@stitches/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Button } from '../../../components/elements/Button'
|
||||
import { FormInput } from '../../../components/elements/FormElements'
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
VStack,
|
||||
} from '../../../components/elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../../components/elements/StyledText'
|
||||
import { PageMetaData } from '../../../components/patterns/PageMetaData'
|
||||
import { SettingsLayout } from '../../../components/templates/SettingsLayout'
|
||||
import { subscribeMutation } from '../../../lib/networking/mutations/subscribeMutation'
|
||||
import { SubscriptionType } from '../../../lib/networking/queries/useGetSubscriptionsQuery'
|
||||
import { showSuccessToast } from '../../../lib/toastHelpers'
|
||||
|
||||
// Styles
|
||||
const Header = styled(Box, {
|
||||
color: '$utilityTextDefault',
|
||||
fontSize: 'x-large',
|
||||
margin: '20px',
|
||||
})
|
||||
|
||||
export default function AddRssFeed(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [feedUrl, setFeedUrl] = useState<string>('')
|
||||
|
||||
const subscribe = useCallback(async () => {
|
||||
try {
|
||||
const result = await subscribeMutation({
|
||||
url: feedUrl,
|
||||
subscriptionType: SubscriptionType.RSS,
|
||||
})
|
||||
if (result) {
|
||||
router.push(`/settings/rss`)
|
||||
showSuccessToast('New RSS feed has been added.')
|
||||
} else {
|
||||
setErrorMessage('There was an error adding new RSS feed.')
|
||||
}
|
||||
} catch (err) {
|
||||
setErrorMessage('Error: ' + err)
|
||||
}
|
||||
}, [feedUrl, router])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMetaData title="Add new RSS Feed" path="/settings/rss/add" />
|
||||
<SettingsLayout>
|
||||
<VStack
|
||||
distribution={'start'}
|
||||
alignment={'start'}
|
||||
css={{
|
||||
margin: '0 auto',
|
||||
width: '80%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
alignment={'start'}
|
||||
distribution={'start'}
|
||||
css={{
|
||||
width: '80%',
|
||||
pb: '$2',
|
||||
borderBottom: '1px solid $utilityTextDefault',
|
||||
pr: '$1',
|
||||
}}
|
||||
>
|
||||
<Header>Add new RSS Feed</Header>
|
||||
</HStack>
|
||||
|
||||
<FormInput
|
||||
type="url"
|
||||
key="feedUrl"
|
||||
value={feedUrl}
|
||||
placeholder={'Enter the RSS feed URL here'}
|
||||
onChange={(e) => {
|
||||
e.preventDefault()
|
||||
setFeedUrl(e.target.value)
|
||||
}}
|
||||
disabled={false}
|
||||
hidden={false}
|
||||
required={true}
|
||||
css={{
|
||||
border: '1px solid $textNonessential',
|
||||
borderRadius: '8px',
|
||||
width: '80%',
|
||||
bg: 'transparent',
|
||||
fontSize: '16px',
|
||||
textIndent: '8px',
|
||||
my: '20px',
|
||||
height: '38px',
|
||||
color: '$grayTextContrast',
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
boxShadow: '0px 0px 2px 2px rgba(255, 234, 159, 0.56)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<StyledText style="error">{errorMessage}</StyledText>
|
||||
)}
|
||||
<HStack>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
css={{ marginRight: '10px' }}
|
||||
onClick={subscribe}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button
|
||||
style="ctaGray"
|
||||
css={{}}
|
||||
onClick={async () => {
|
||||
router.push('/settings/rss')
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</SettingsLayout>
|
||||
<div data-testid="settings-rss-subscribe-page-tag" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
100
packages/web/pages/settings/rss/index.tsx
Normal file
100
packages/web/pages/settings/rss/index.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import { StyledText } from '../../../components/elements/StyledText'
|
||||
import { ConfirmationModal } from '../../../components/patterns/ConfirmationModal'
|
||||
import {
|
||||
EmptySettingsRow,
|
||||
SettingsTable,
|
||||
SettingsTableRow,
|
||||
} from '../../../components/templates/settings/SettingsTable'
|
||||
import { formattedShortDate } from '../../../lib/dateFormatting'
|
||||
import { unsubscribeMutation } from '../../../lib/networking/mutations/unsubscribeMutation'
|
||||
import {
|
||||
SubscriptionType,
|
||||
useGetSubscriptionsQuery,
|
||||
} from '../../../lib/networking/queries/useGetSubscriptionsQuery'
|
||||
import { applyStoredTheme } from '../../../lib/themeUpdater'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
|
||||
export default function Rss(): JSX.Element {
|
||||
const router = useRouter()
|
||||
const { subscriptions, revalidate, isValidating } = useGetSubscriptionsQuery(
|
||||
SubscriptionType.RSS
|
||||
)
|
||||
const [onDeleteId, setOnDeleteId] = useState<string>('')
|
||||
|
||||
async function onDelete(id: string): Promise<void> {
|
||||
const result = await unsubscribeMutation('', id)
|
||||
if (result) {
|
||||
showSuccessToast('RSS feed unsubscribed', { position: 'bottom-right' })
|
||||
} else {
|
||||
showErrorToast('Failed to unsubscribe', { position: 'bottom-right' })
|
||||
}
|
||||
revalidate()
|
||||
}
|
||||
|
||||
applyStoredTheme(false)
|
||||
|
||||
return (
|
||||
<SettingsTable
|
||||
pageId={'rss'}
|
||||
pageInfoLink={''} // TODO: https://docs.omnivore.app/integrations/rss.html
|
||||
headerTitle={'Subscribed RSS feeds'}
|
||||
createTitle={'Add RSS feed'}
|
||||
createAction={() => {
|
||||
router.push('/settings/rss/add')
|
||||
}}
|
||||
>
|
||||
{subscriptions.length === 0 ? (
|
||||
<EmptySettingsRow
|
||||
text={isValidating ? '-' : 'No RSS feeds subscribed'}
|
||||
/>
|
||||
) : (
|
||||
subscriptions.map((subscription, i) => {
|
||||
return (
|
||||
<SettingsTableRow
|
||||
key={subscription.id}
|
||||
title={subscription.name}
|
||||
isLast={i === subscriptions.length - 1}
|
||||
onDelete={() => {
|
||||
console.log('onDelete triggered: ', subscription.id)
|
||||
setOnDeleteId(subscription.id)
|
||||
}}
|
||||
deleteTitle="Delete"
|
||||
sublineElement={
|
||||
<StyledText
|
||||
css={{
|
||||
my: '5px',
|
||||
fontSize: '11px',
|
||||
a: {
|
||||
color: '$omnivoreCtaYellow',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{`Last fetched: ${
|
||||
subscription.lastFetchedAt
|
||||
? formattedShortDate(subscription.lastFetchedAt)
|
||||
: 'Never'
|
||||
}`}
|
||||
</StyledText>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)}
|
||||
|
||||
{onDeleteId && (
|
||||
<ConfirmationModal
|
||||
message={
|
||||
'RSS feed will be unsubscribed. This action cannot be undone.'
|
||||
}
|
||||
onAccept={async () => {
|
||||
await onDelete(onDeleteId)
|
||||
setOnDeleteId('')
|
||||
}}
|
||||
onOpenChange={() => setOnDeleteId('')}
|
||||
/>
|
||||
)}
|
||||
</SettingsTable>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user