diff --git a/packages/web/components/elements/InfoLink.tsx b/packages/web/components/elements/InfoLink.tsx index 0424f0b2e..c250ca7e9 100644 --- a/packages/web/components/elements/InfoLink.tsx +++ b/packages/web/components/elements/InfoLink.tsx @@ -1,6 +1,6 @@ import Link from 'next/link' import { Info } from 'phosphor-react' -import { Box } from '../elements/LayoutPrimitives' +import { Box, VStack } from '../elements/LayoutPrimitives' import { theme } from '../tokens/stitches.config' import { TooltipWrapped } from './Tooltip' @@ -15,9 +15,12 @@ const TooltipStyle = { export function InfoLink(props: InfoLinkProps): JSX.Element { return ( - - - + + - - + ) } diff --git a/packages/web/components/templates/settings/SettingsTable.tsx b/packages/web/components/templates/settings/SettingsTable.tsx new file mode 100644 index 000000000..293d2f38e --- /dev/null +++ b/packages/web/components/templates/settings/SettingsTable.tsx @@ -0,0 +1,306 @@ +import Link from 'next/link' +import { Plus, Trash } from 'phosphor-react' +import { Toaster } from 'react-hot-toast' +import { Button } from '../../elements/Button' +import { Dropdown, DropdownOption } from '../../elements/DropdownElements' +import { MoreOptionsIcon } from '../../elements/images/MoreOptionsIcon' +import { InfoLink } from '../../elements/InfoLink' +import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives' +import { StyledText } from '../../elements/StyledText' +import { styled, theme } from '../../tokens/stitches.config' +import { PrimaryLayout } from '../PrimaryLayout' + +type SettingsTableProps = { + pageId: string + pageHeadline: string + pageInfoLink: string + headerTitle: string + + createTitle?: string + createAction?: () => void + + children: React.ReactNode +} + +type CreateButtonProps = { + title: string + action: () => void +} + +type SettingsTableRowProps = { + key: string + title: string + isLast: boolean + + sublineElement: JSX.Element + titleElement?: JSX.Element + extraElement?: JSX.Element + + deleteTitle: string + onDelete: () => void +} + +type MoreOptionsProps = { + title: string + onDelete: () => void +} + +const MoreOptions = (props: MoreOptionsProps) => ( + + + + } + > + { + return true + }} + > + + + + + + +) + +type EmptySettingsRowProps = { + text: string +} + +export const EmptySettingsRow = (props: EmptySettingsRowProps): JSX.Element => { + return ( + + {props.text} + + ) +} + +export const SettingsTableRow = (props: SettingsTableRowProps): JSX.Element => { + return ( + + + + + + {props.title} + + {props.sublineElement} + + {props.titleElement} + + + + + {props.extraElement} + + + + + + + + ) +} + +const CreateButton = (props: CreateButtonProps): JSX.Element => { + return ( + + ) +} + +export const SettingsTable = (props: SettingsTableProps): JSX.Element => { + return ( + + + + + + + {props.createAction && props.createTitle && ( + + )} + + + + + {props.headerTitle} + + + + + + {props.children} + + + + + ) +} diff --git a/packages/web/lib/dateFormatting.ts b/packages/web/lib/dateFormatting.ts index 3326a26e8..93be4d330 100644 --- a/packages/web/lib/dateFormatting.ts +++ b/packages/web/lib/dateFormatting.ts @@ -1,6 +1,6 @@ //https://github.com/you-dont-need/You-Dont-Need-Momentjs -const locale = 'en-US' //navigator?.language ?? 'en-US' +const locale = Intl.DateTimeFormat().resolvedOptions().locale || 'en-US' export function formattedLongDate(rawDate: string): string { return new Intl.DateTimeFormat(locale, { @@ -8,6 +8,12 @@ export function formattedLongDate(rawDate: string): string { }).format(new Date(rawDate)) } +export function formattedShortDate(rawDate: string): string { + return new Intl.DateTimeFormat(locale, { + dateStyle: 'short', + }).format(new Date(rawDate)) +} + export function readableUpdatedAtMessage( rawDate: string, customPrefix?: string diff --git a/packages/web/lib/networking/queries/useGetNewsletterEmailsQuery.tsx b/packages/web/lib/networking/queries/useGetNewsletterEmailsQuery.tsx index ece0316be..2020c21ab 100644 --- a/packages/web/lib/networking/queries/useGetNewsletterEmailsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetNewsletterEmailsQuery.tsx @@ -19,6 +19,8 @@ type NewsletterEmailsData = { export type NewsletterEmail = { id: string address: string + createdAt: string + subscriptionCount: number confirmationCode?: string } @@ -30,6 +32,8 @@ export function useGetNewsletterEmailsQuery(): NewsletterEmailsQueryResponse { newsletterEmails { id address + createdAt + subscriptionCount confirmationCode } } @@ -46,13 +50,14 @@ export function useGetNewsletterEmailsQuery(): NewsletterEmailsQueryResponse { try { if (data) { const result = data as NewsletterEmailsResponseData - const emailAddresses = result.newsletterEmails?.newsletterEmails as NewsletterEmail[] + const emailAddresses = result.newsletterEmails + ?.newsletterEmails as NewsletterEmail[] return { isValidating, emailAddresses, revalidate: () => { mutate(undefined, true) - } + }, } } } catch (error) { @@ -62,6 +67,6 @@ export function useGetNewsletterEmailsQuery(): NewsletterEmailsQueryResponse { isValidating: false, emailAddresses: [], // eslint-disable-next-line @typescript-eslint/no-empty-function - revalidate: () => {} + revalidate: () => {}, } } diff --git a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx index 92ec8241a..bacb0e5bb 100644 --- a/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetSubscriptionsQuery.tsx @@ -13,8 +13,8 @@ export type Subscription = { description?: string status: SubscriptionStatus - createdAt: Date - updatedAt: Date + createdAt: string + updatedAt: string } type SubscriptionsQueryResponse = { @@ -57,7 +57,6 @@ export function useGetSubscriptionsQuery(): SubscriptionsQueryResponse { ` const { data, mutate, error, isValidating } = useSWR(query, publicGqlFetcher) - console.log('subscriptions data', data) try { if (data) { @@ -75,7 +74,7 @@ export function useGetSubscriptionsQuery(): SubscriptionsQueryResponse { console.log('error', error) } return { - isValidating: false, + isValidating: true, subscriptions: [], // eslint-disable-next-line @typescript-eslint/no-empty-function revalidate: () => {}, diff --git a/packages/web/pages/settings/emails.tsx b/packages/web/pages/settings/emails.tsx index ce3bc53a4..389cfc0a6 100644 --- a/packages/web/pages/settings/emails.tsx +++ b/packages/web/pages/settings/emails.tsx @@ -1,30 +1,28 @@ -import { PrimaryLayout } from '../../components/templates/PrimaryLayout' import { Button } from '../../components/elements/Button' import { useGetNewsletterEmailsQuery } from '../../lib/networking/queries/useGetNewsletterEmailsQuery' import { createNewsletterEmailMutation } from '../../lib/networking/mutations/createNewsletterEmailMutation' import { deleteNewsletterEmailMutation } from '../../lib/networking/mutations/deleteNewsletterEmailMutation' import { MoreOptionsIcon } from '../../components/elements/images/MoreOptionsIcon' -import { Info, Plus, Trash, Copy } from 'phosphor-react' +import { Trash, Copy } from 'phosphor-react' import { Dropdown, DropdownOption, } from '../../components/elements/DropdownElements' -import { TooltipWrapped } from '../../components/elements/Tooltip' import { theme, styled } from '../../components/tokens/stitches.config' -import { - Box, - SpanBox, - HStack, - VStack, -} from '../../components/elements/LayoutPrimitives' +import { Box, HStack } from '../../components/elements/LayoutPrimitives' import { useCopyLink } from '../../lib/hooks/useCopyLink' -import { Toaster } from 'react-hot-toast' -import { useCallback } from 'react' +import { useCallback, useMemo, useState } from 'react' import { StyledText } from '../../components/elements/StyledText' import { applyStoredTheme } from '../../lib/themeUpdater' import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers' +import { formattedShortDate } from '../../lib/dateFormatting' import Link from 'next/link' -import { InfoLink } from '../../components/elements/InfoLink' +import { + EmptySettingsRow, + SettingsTable, + SettingsTableRow, +} from '../../components/templates/settings/SettingsTable' +import { ConfirmationModal } from '../../components/patterns/ConfirmationModal' enum TextType { EmailAddress, @@ -36,117 +34,21 @@ type CopyTextButtonProps = { type: TextType } -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', - marginLeft: '38px', - '&[disabled]': { - border: 'none', - }, -}) - const CopyTextBtnWrapper = styled(Box, { - padding: '1px', background: '$grayBgActive', borderRadius: '6px', border: '1px solid rgba(0, 0, 0, 0.06)', + width: '32px', + height: '32px', + + display: 'flex', + + color: '#3D3D3D', + + alignItems: 'center', + justifyContent: 'center', }) -const InfoIcon = styled(Info, { - marginTop: '8px', - '&:hover': { - cursor: 'pointer', - }, -}) - -const TooltipStyle = { - backgroundColor: '#F9D354', - color: '#0A0806', -} - -const MoreOptions = ({ onDelete }: { onDelete: () => void }) => ( - - - - } - > - { - return true - }} - > - - - - - - -) - function CopyTextButton(props: CopyTextButtonProps): JSX.Element { const { copyLink, isLinkCopied } = useCopyLink( props.text, @@ -158,12 +60,20 @@ function CopyTextButton(props: CopyTextButtonProps): JSX.Element { const copy = useCallback(() => { copyLink() - showSuccessToast(props.type == TextType.EmailAddress ? 'Email Address Copied' : 'Confirmation Code Copied'); + showSuccessToast( + props.type == TextType.EmailAddress + ? 'Email Address Copied' + : 'Confirmation Code Copied' + ) }, []) return ( ) } @@ -171,6 +81,8 @@ function CopyTextButton(props: CopyTextButtonProps): JSX.Element { export default function EmailsPage(): JSX.Element { const { emailAddresses, revalidate, isValidating } = useGetNewsletterEmailsQuery() + const [confirmDeleteEmailId, setConfirmDeleteEmailId] = + useState(undefined) applyStoredTheme(false) @@ -193,228 +105,123 @@ export default function EmailsPage(): JSX.Element { revalidate() showSuccessToast('Email Deleted') } + + const sortedEmailAddresses = useMemo(() => { + return emailAddresses.sort((a, b) => a.createdAt.localeCompare(b.createdAt)) + }, [emailAddresses]) + return ( - - - + - - - - - Email Addresses{' '} - - - - - - - - - EMAIL - - - - - CONFIRMATION CODE - - - - - {emailAddresses && - emailAddresses.map((email, i) => { - const { address, confirmationCode, id } = email - const isLastChild = i === emailAddresses.length - 1 - + {sortedEmailAddresses.length > 0 ? ( + sortedEmailAddresses.map((email, i) => { return ( - - - setConfirmDeleteEmailId(email.id)} + deleteTitle="Delete" + sublineElement={ + - - - - - - deleteEmail(id)} /> - - - {confirmationCode && ( + {`created ${formattedShortDate(email.createdAt)}, `} + {`${email.subscriptionCount} subscriptions`} + + } + titleElement={ + + + + } + extraElement={ + email.confirmationCode ? ( <> - + + {`Gmail: ${email.confirmationCode}`} + - + - )} - - - - deleteEmail(id)} /> - - - + ) : ( + <> + ) + } + /> ) - })} - - - + }) + ) : ( + + )} + + + {confirmDeleteEmailId ? ( + { + await deleteEmail(confirmDeleteEmailId) + setConfirmDeleteEmailId(undefined) + }} + onOpenChange={() => setConfirmDeleteEmailId(undefined)} + /> + ) : null} + ) } diff --git a/packages/web/pages/settings/subscriptions.tsx b/packages/web/pages/settings/subscriptions.tsx index 704edb121..25212657a 100644 --- a/packages/web/pages/settings/subscriptions.tsx +++ b/packages/web/pages/settings/subscriptions.tsx @@ -1,18 +1,22 @@ -import { useState } from 'react' -import { PrimaryLayout } from '../../components/templates/PrimaryLayout' -import { Toaster } from 'react-hot-toast' +import { useMemo, useState } from 'react' import { applyStoredTheme } from '../../lib/themeUpdater' 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' -import { Table } from '../../components/elements/Table' +import { + EmptySettingsRow, + SettingsTable, + SettingsTableRow, +} from '../../components/templates/settings/SettingsTable' +import { StyledText } from '../../components/elements/StyledText' +import Link from 'next/link' +import { formattedShortDate } from '../../lib/dateFormatting' export default function SubscriptionsPage(): JSX.Element { - const { subscriptions, revalidate } = useGetSubscriptionsQuery() - const [confirmUnsubscribeName, setConfirmUnsubscribeName] = useState< - string | null - >(null) + const { subscriptions, revalidate, isValidating } = useGetSubscriptionsQuery() + const [confirmUnsubscribeName, setConfirmUnsubscribeName] = + useState(null) applyStoredTheme(false) @@ -26,42 +30,66 @@ export default function SubscriptionsPage(): JSX.Element { revalidate() } - const headers = ['Name', 'Email', 'Updated Time'] - const rows = new Map() - subscriptions.forEach((subscription) => - rows.set(subscription.name, [ - subscription.name, - subscription.newsletterEmail, - subscription.updatedAt.toString(), - ]) - ) + const sortedSubscriptions = useMemo(() => { + return subscriptions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) + }, [subscriptions]) return ( - - + + <> + {sortedSubscriptions.length > 0 ? ( + sortedSubscriptions.map((subscription, i) => { + return ( + setConfirmUnsubscribeName(subscription.name)} + deleteTitle="Unsubscribe" + sublineElement={ + + {`Last received ${formattedShortDate( + subscription.updatedAt + )} at `} + + {subscription.newsletterEmail} + + + } + /> + ) + }) + ) : ( + + )} - {confirmUnsubscribeName ? ( - { - await onUnsubscribe(confirmUnsubscribeName) - setConfirmUnsubscribeName(null) - }} - onOpenChange={() => setConfirmUnsubscribeName(null)} - /> - ) : null} - - + {confirmUnsubscribeName ? ( + { + await onUnsubscribe(confirmUnsubscribeName) + setConfirmUnsubscribeName(null) + }} + onOpenChange={() => setConfirmUnsubscribeName(null)} + /> + ) : null} + + ) }