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}
+ >
+
)
}