diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index bc87360ae..3a936c60a 100755 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -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()) diff --git a/packages/web/lib/networking/mutations/subscribeMutation.ts b/packages/web/lib/networking/mutations/subscribeMutation.ts index 5dd599f82..1351592a1 100644 --- a/packages/web/lib/networking/mutations/subscribeMutation.ts +++ b/packages/web/lib/networking/mutations/subscribeMutation.ts @@ -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 { 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 diff --git a/packages/web/lib/networking/mutations/unsubscribeMutation.ts b/packages/web/lib/networking/mutations/unsubscribeMutation.ts index 3eba481ab..24e2505e1 100644 --- a/packages/web/lib/networking/mutations/unsubscribeMutation.ts +++ b/packages/web/lib/networking/mutations/unsubscribeMutation.ts @@ -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 { 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 diff --git a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx index 051b20d70..48fcef432 100644 --- a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx @@ -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 { diff --git a/packages/web/pages/settings/rss/add.tsx b/packages/web/pages/settings/rss/add.tsx new file mode 100644 index 000000000..da7196792 --- /dev/null +++ b/packages/web/pages/settings/rss/add.tsx @@ -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( + undefined + ) + const [feedUrl, setFeedUrl] = useState('') + + 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 ( + <> + + + + +
Add new RSS Feed
+
+ + { + 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 && ( + {errorMessage} + )} + + + + +
+
+
+ + ) +} diff --git a/packages/web/pages/settings/rss/index.tsx b/packages/web/pages/settings/rss/index.tsx new file mode 100644 index 000000000..a08903a6f --- /dev/null +++ b/packages/web/pages/settings/rss/index.tsx @@ -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('') + + async function onDelete(id: string): Promise { + 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 ( + { + router.push('/settings/rss/add') + }} + > + {subscriptions.length === 0 ? ( + + ) : ( + subscriptions.map((subscription, i) => { + return ( + { + console.log('onDelete triggered: ', subscription.id) + setOnDeleteId(subscription.id) + }} + deleteTitle="Delete" + sublineElement={ + + {`Last fetched: ${ + subscription.lastFetchedAt + ? formattedShortDate(subscription.lastFetchedAt) + : 'Never' + }`} + + } + /> + ) + }) + )} + + {onDeleteId && ( + { + await onDelete(onDeleteId) + setOnDeleteId('') + }} + onOpenChange={() => setOnDeleteId('')} + /> + )} + + ) +}