From bcbda50daabd940bf2f39cc041991e3a376c834e Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 22 Apr 2022 16:05:07 +0800 Subject: [PATCH] Add basic UI for subscriptions --- .../api/src/resolvers/subscriptions/index.ts | 30 +-- ...082.do.add_default_subscription_status.sql | 10 + ...2.undo.add_default_subscription_status.sql | 9 + packages/web/components/elements/Table.tsx | 228 ++++++++++++++++++ .../queries/useGetSubscriptionsQuery.tsx | 5 +- packages/web/pages/settings/subscriptions.tsx | 70 +++--- 6 files changed, 295 insertions(+), 57 deletions(-) create mode 100755 packages/db/migrations/0082.do.add_default_subscription_status.sql create mode 100755 packages/db/migrations/0082.undo.add_default_subscription_status.sql create mode 100644 packages/web/components/elements/Table.tsx diff --git a/packages/api/src/resolvers/subscriptions/index.ts b/packages/api/src/resolvers/subscriptions/index.ts index 766eb6b0f..7936b25e5 100644 --- a/packages/api/src/resolvers/subscriptions/index.ts +++ b/packages/api/src/resolvers/subscriptions/index.ts @@ -37,25 +37,22 @@ export const subscriptionsResolver = authorized< try { const sortBy = sort?.by === SortBy.UpdatedTime ? 'updatedAt' : 'createdAt' const sortOrder = sort?.order === SortOrder.Ascending ? 'ASC' : 'DESC' - const user = await getRepository(User).findOne({ - where: { id: uid, subscriptions: { status: SubscriptionStatus.Active } }, - relations: { - subscriptions: true, - }, - order: { - subscriptions: { - [sortBy]: sortOrder, - }, - }, - }) + const user = await getRepository(User).findOneBy({ id: uid }) if (!user) { return { errorCodes: [SubscriptionsErrorCode.Unauthorized], } } + const subscriptions = await getRepository(Subscription).find({ + where: { user: { id: uid }, status: SubscriptionStatus.Active }, + order: { + [sortBy]: sortOrder, + }, + }) + return { - subscriptions: user.subscriptions || [], + subscriptions, } } catch (error) { log.error(error) @@ -73,9 +70,12 @@ export const unsubscribeResolver = authorized< log.info('unsubscribeResolver') try { - const subscription = await getRepository(Subscription).findOneBy({ - name, - user: { id: uid }, + const subscription = await getRepository(Subscription).findOne({ + where: { + name, + user: { id: uid }, + }, + relations: ['user'], }) if (!subscription) { return { diff --git a/packages/db/migrations/0082.do.add_default_subscription_status.sql b/packages/db/migrations/0082.do.add_default_subscription_status.sql new file mode 100755 index 000000000..8683078f9 --- /dev/null +++ b/packages/db/migrations/0082.do.add_default_subscription_status.sql @@ -0,0 +1,10 @@ +-- Type: DO +-- Name: add_default_subscription_status +-- Description: Add default value to subscription status field + +BEGIN; + +ALTER TABLE omnivore.subscriptions + ALTER COLUMN status SET DEFAULT 'ACTIVE'; + +COMMIT; diff --git a/packages/db/migrations/0082.undo.add_default_subscription_status.sql b/packages/db/migrations/0082.undo.add_default_subscription_status.sql new file mode 100755 index 000000000..c5c4771a3 --- /dev/null +++ b/packages/db/migrations/0082.undo.add_default_subscription_status.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: add_default_subscription_status +-- Description: Add default value to subscription status field + +BEGIN; + +ALTER TABLE omnivore.subscriptions ALTER status DROP DEFAULT; + +COMMIT; diff --git a/packages/web/components/elements/Table.tsx b/packages/web/components/elements/Table.tsx new file mode 100644 index 000000000..94945f1cf --- /dev/null +++ b/packages/web/components/elements/Table.tsx @@ -0,0 +1,228 @@ +import { Box, HStack, SpanBox, VStack } from './LayoutPrimitives' +import { styled } from '../tokens/stitches.config' +import { StyledText } from './StyledText' +import { InfoLink } from './InfoLink' +import { Button } from './Button' +import { Plus, Trash } from 'phosphor-react' +import { isDarkTheme } from '../../lib/themeUpdater' + +interface TableProps { + heading: string + infoLink?: string + onAdd?: () => void + headers: string[] + rows: string[][] + onDelete?: (id: string) => void +} + +const HeaderWrapper = styled(Box, { + width: '100%', + '@md': { + display: 'block', + }, +}) + +const TableCard = styled(Box, { + backgroundColor: '$grayBg', + display: 'flex', + alignItems: 'center', + padding: '10px 12px', + border: '0.5px solid $grayBgActive', + width: '100%', + + '&:hover': { + border: '0.5px solid #FFD234', + }, + '@md': { + paddingLeft: '0', + }, +}) + +const TableHeading = styled(Box, { + backgroundColor: '$grayBgActive', + border: '1px solid rgba(0, 0, 0, 0.06)', + display: 'none', + alignItems: 'center', + padding: '14px 0 14px 40px', + borderRadius: '5px 5px 0px 0px', + width: '100%', + '@md': { + display: 'flex', + }, +}) + +const Input = styled('input', { + backgroundColor: 'transparent', + color: '$grayTextContrast', + marginTop: '5px', + '&[disabled]': { + border: 'none', + }, +}) + +const IconButton = styled(Button, { + variants: { + style: { + ctaWhite: { + color: 'red', + padding: '10px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + border: '1px solid $grayBorder', + boxSizing: 'border-box', + borderRadius: 6, + width: 40, + height: 40, + }, + }, + }, +}) + +export function Table(props: TableProps): JSX.Element { + const iconColor = isDarkTheme() ? '#D8D7D5' : '#5F5E58' + + return ( + + + + + {props.heading} + + {props.infoLink && } + {props.onAdd && ( + + )} + + + + {props.headers.map((header, index) => ( + + + {header} + + + ))} + + {props.rows.map((row, index) => ( + + + {row.map((cell, index) => ( + + + + ))} + {props.onDelete && ( + { + props.onDelete(row[0]) + }} + > + + + )} + + + ))} + + ) +} diff --git a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx index 7598dcb4a..92ec8241a 100644 --- a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx @@ -15,7 +15,7 @@ export type Subscription = { status: SubscriptionStatus createdAt: Date updatedAt: Date -}; +} type SubscriptionsQueryResponse = { isValidating: boolean @@ -34,11 +34,12 @@ type SubscriptionsData = { export function useGetSubscriptionsQuery(): SubscriptionsQueryResponse { const query = gql` query GetSubscriptions { - subscriptions { + subscriptions(sort: { by: UPDATED_TIME }) { ... on SubscriptionsSuccess { subscriptions { id name + newsletterEmail url description status diff --git a/packages/web/pages/settings/subscriptions.tsx b/packages/web/pages/settings/subscriptions.tsx index d6a4a93b9..ec9d7cab5 100644 --- a/packages/web/pages/settings/subscriptions.tsx +++ b/packages/web/pages/settings/subscriptions.tsx @@ -1,23 +1,18 @@ import { useState } from 'react' import { PrimaryLayout } from '../../components/templates/PrimaryLayout' -import { styled } from '../../components/tokens/stitches.config' -import { Box, VStack } from '../../components/elements/LayoutPrimitives' import { Toaster } from 'react-hot-toast' import { applyStoredTheme } from '../../lib/themeUpdater' -import { StyledText } from '../../components/elements/StyledText' import { ConfirmationModal } from '../../components/patterns/ConfirmationModal' import { useGetSubscriptionsQuery } from '../../lib/networking/queries/useGetSubscriptionsQuery' import { unsubscribeMutation } from '../../lib/networking/mutations/unsubscribeMutation' import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' - -const HeaderWrapper = styled(Box, { - width: '100%', -}) +import { Table } from '../../components/elements/Table' export default function SubscriptionsPage(): JSX.Element { const { subscriptions, revalidate } = useGetSubscriptionsQuery() - const [confirmUnsubscribeName, setConfirmUnsubscribeName] = - useState(null) + const [confirmUnsubscribeName, setConfirmUnsubscribeName] = useState< + string | null + >(null) applyStoredTheme(false) @@ -31,6 +26,13 @@ export default function SubscriptionsPage(): JSX.Element { revalidate() } + const headers = ['Name', 'Email', 'Updated Time'] + const rows = subscriptions.map((subscription) => [ + subscription.name, + subscription.newsletterEmail, + subscription.updatedAt.toString(), + ]) + return ( - - {confirmUnsubscribeName ? ( - { - await onUnsubscribe(confirmUnsubscribeName) - setConfirmUnsubscribeName(null) - }} - onOpenChange={() => setConfirmUnsubscribeName(null)} - /> - ) : null} - - - - Subscriptions - - - - {subscriptions - ? subscriptions.map((subscription, i) => { - return - }) - : null} - + + {confirmUnsubscribeName ? ( + { + await onUnsubscribe(confirmUnsubscribeName) + setConfirmUnsubscribeName(null) + }} + onOpenChange={() => setConfirmUnsubscribeName(null)} + /> + ) : null} + ) }