diff --git a/packages/web/lib/networking/queries/useGetRecentEmails.tsx b/packages/web/lib/networking/queries/useGetRecentEmails.tsx
new file mode 100644
index 000000000..a8b10aee8
--- /dev/null
+++ b/packages/web/lib/networking/queries/useGetRecentEmails.tsx
@@ -0,0 +1,78 @@
+import { gql } from 'graphql-request'
+import useSWR from 'swr'
+import { publicGqlFetcher } from '../networkHelpers'
+
+export interface RecentEmail {
+ id: string
+ from: string
+ to: string
+ subject: string
+ type: string
+ text: string
+ createdAt: string
+}
+
+interface RecentEmailsResponse {
+ isValidating: boolean
+ recentEmails: RecentEmail[]
+ revalidate: () => void
+}
+
+interface RecentEmailsResponseData {
+ recentEmails: RecentEmailsData
+}
+
+interface RecentEmailsData {
+ recentEmails: RecentEmail[]
+}
+
+export function useGetRecentEmailsQuery(): RecentEmailsResponse {
+ const query = gql`
+ query GetRecentEmails {
+ recentEmails {
+ ... on RecentEmailsSuccess {
+ recentEmails {
+ id
+ from
+ to
+ subject
+ type
+ text
+ createdAt
+ }
+ }
+ ... on RecentEmailsError {
+ errorCodes
+ }
+ }
+ }
+ `
+
+ const { data, mutate, error, isValidating } = useSWR(query, publicGqlFetcher)
+
+ try {
+ if (error) {
+ throw error
+ }
+
+ if (data) {
+ const result = data as RecentEmailsResponseData
+ const recentEmails = result.recentEmails.recentEmails as RecentEmail[]
+ return {
+ isValidating,
+ recentEmails,
+ revalidate: () => {
+ mutate()
+ },
+ }
+ }
+ } catch (error) {
+ console.log('error', error)
+ }
+ return {
+ isValidating: false,
+ recentEmails: [],
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ revalidate: () => {},
+ }
+}
diff --git a/packages/web/pages/settings/emails/index.tsx b/packages/web/pages/settings/emails/index.tsx
new file mode 100644
index 000000000..4f064d53e
--- /dev/null
+++ b/packages/web/pages/settings/emails/index.tsx
@@ -0,0 +1,234 @@
+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 { Trash, Copy } from 'phosphor-react'
+import {
+ Dropdown,
+ DropdownOption,
+} from '../../../components/elements/DropdownElements'
+import { theme, styled } from '../../../components/tokens/stitches.config'
+import { Box, HStack } from '../../../components/elements/LayoutPrimitives'
+import { useCopyLink } from '../../../lib/hooks/useCopyLink'
+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 {
+ EmptySettingsRow,
+ SettingsTable,
+ SettingsTableRow,
+} from '../../../components/templates/settings/SettingsTable'
+import { ConfirmationModal } from '../../../components/patterns/ConfirmationModal'
+
+enum TextType {
+ EmailAddress,
+ ConfirmationCode,
+}
+
+type CopyTextButtonProps = {
+ text: string
+ type: TextType
+}
+
+const CopyTextBtnWrapper = styled(Box, {
+ 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',
+})
+
+function CopyTextButton(props: CopyTextButtonProps): JSX.Element {
+ const { copyLink, isLinkCopied } = useCopyLink(
+ props.text,
+ 'newsletter_' +
+ (props.type == TextType.EmailAddress
+ ? 'email_address'
+ : 'confirmation_code')
+ )
+
+ const copy = useCallback(() => {
+ copyLink()
+ showSuccessToast(
+ props.type == TextType.EmailAddress
+ ? 'Email Address Copied'
+ : 'Confirmation Code Copied'
+ )
+ }, [])
+
+ return (
+
+ )
+}
+
+export default function EmailsPage(): JSX.Element {
+ const { emailAddresses, revalidate, isValidating } =
+ useGetNewsletterEmailsQuery()
+ const [confirmDeleteEmailId, setConfirmDeleteEmailId] = useState<
+ undefined | string
+ >(undefined)
+
+ applyStoredTheme(false)
+
+ async function createEmail(): Promise {
+ const email = await createNewsletterEmailMutation()
+ if (!email) {
+ showErrorToast('Error Creating Email')
+ return
+ }
+ showSuccessToast('Email Created')
+ revalidate()
+ }
+
+ async function deleteEmail(id: string): Promise {
+ const result = await deleteNewsletterEmailMutation(id)
+ if (!result) {
+ showErrorToast('Error Deleting Email')
+ return
+ }
+ revalidate()
+ showSuccessToast('Email Deleted')
+ }
+
+ const sortedEmailAddresses = useMemo(() => {
+ if (!emailAddresses) {
+ return []
+ }
+ return emailAddresses.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
+ }, [emailAddresses])
+
+ return (
+ <>
+
+ {sortedEmailAddresses.length > 0 ? (
+ sortedEmailAddresses.map((email, i) => {
+ return (
+ setConfirmDeleteEmailId(email.id)}
+ deleteTitle="Delete"
+ sublineElement={
+
+ {`created ${formattedShortDate(email.createdAt)}, `}
+ {`${email.subscriptionCount} subscriptions`}
+
+ }
+ titleElement={
+
+
+
+ }
+ extraElement={
+ email.confirmationCode ? (
+
+ <>
+
+ {`Gmail: ${email.confirmationCode}`}
+
+
+
+
+
+
+ >
+
+ ) : (
+ <>>
+ )
+ }
+ />
+ )
+ })
+ ) : (
+
+ )}
+
+
+ {confirmDeleteEmailId ? (
+ {
+ await deleteEmail(confirmDeleteEmailId)
+ setConfirmDeleteEmailId(undefined)
+ }}
+ onOpenChange={() => setConfirmDeleteEmailId(undefined)}
+ />
+ ) : null}
+ >
+ )
+}
diff --git a/packages/web/pages/settings/emails/recent.tsx b/packages/web/pages/settings/emails/recent.tsx
new file mode 100644
index 000000000..d5eec181c
--- /dev/null
+++ b/packages/web/pages/settings/emails/recent.tsx
@@ -0,0 +1,196 @@
+import { useEffect, useMemo, useState } from 'react'
+import { useRouter } from 'next/router'
+
+import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
+import { applyStoredTheme } from '../../../lib/themeUpdater'
+
+import { FormInputProps } from '../../../components/elements/FormElements'
+import {
+ EmptySettingsRow,
+ SettingsTable,
+ SettingsTableRow,
+} from '../../../components/templates/settings/SettingsTable'
+import { StyledText } from '../../../components/elements/StyledText'
+import { formattedShortDate } from '../../../lib/dateFormatting'
+import {
+ RecentEmail,
+ useGetRecentEmailsQuery,
+} from '../../../lib/networking/queries/useGetRecentEmails'
+import Link from 'next/link'
+
+export default function RecentEmails(): JSX.Element {
+ const { recentEmails, revalidate, isValidating } = useGetRecentEmailsQuery()
+ const [onDeleteId, setOnDeleteId] = useState('')
+ const [addModalOpen, setAddModalOpen] = useState(false)
+ const [name, setName] = useState('')
+ const [value, setValue] = useState('')
+ const [expiresAt, setExpiresAt] = useState(new Date())
+ const [formInputs, setFormInputs] = useState([])
+ const [apiKeyGenerated, setApiKeyGenerated] = useState('')
+ const neverExpiresDate = new Date(8640000000000000)
+ const defaultExpiresAt = 'Never'
+
+ applyStoredTheme(false)
+
+ // async function onDelete(id: string): Promise {
+ // const result = await revokeApiKeyMutation(id)
+ // if (result) {
+ // showSuccessToast('API Key deleted', { position: 'bottom-right' })
+ // } else {
+ // showErrorToast('Failed to delete', { position: 'bottom-right' })
+ // }
+ // revalidate()
+ // }
+
+ // async function onCreate(): Promise {
+ // const result = await generateApiKeyMutation({ name, expiresAt })
+ // if (result) {
+ // setApiKeyGenerated(result)
+ // showSuccessToast('API key generated', { position: 'bottom-right' })
+ // } else {
+ // showErrorToast('Failed to add', { position: 'bottom-right' })
+ // }
+ // revalidate()
+ // }
+
+ // function onAdd() {
+ // return setFormInputs([
+ // {
+ // label: 'Name',
+ // onChange: setName,
+ // name: 'name',
+ // value: value,
+ // required: true,
+ // },
+ // {
+ // label: 'Expires',
+ // name: 'expiredAt',
+ // required: true,
+ // onChange: (e) => {
+ // console.log('onChange: ', e)
+ // let additionalDays = 0
+ // switch (e.target.value) {
+ // case 'in 7 days':
+ // additionalDays = 7
+ // break
+ // case 'in 30 days':
+ // additionalDays = 30
+ // break
+ // case 'in 90 days':
+ // additionalDays = 90
+ // break
+ // case 'in 1 year':
+ // additionalDays = 365
+ // break
+ // case 'Never':
+ // break
+ // }
+ // const newExpires = additionalDays ? new Date() : neverExpiresDate
+ // if (additionalDays) {
+ // newExpires.setDate(newExpires.getDate() + additionalDays)
+ // }
+ // setExpiresAt(newExpires)
+ // },
+ // type: 'select',
+ // options: [
+ // 'in 7 days',
+ // 'in 30 days',
+ // 'in 90 days',
+ // 'in 1 year',
+ // 'Never',
+ // ],
+ // value: defaultExpiresAt,
+ // },
+ // ])
+ // }
+
+ const sortedRecentEmails = useMemo(() => {
+ if (!recentEmails) {
+ return []
+ }
+ return recentEmails.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
+ }, [recentEmails])
+
+ return (
+
+ {sortedRecentEmails.length > 0 ? (
+ sortedRecentEmails.map((recentEmail: RecentEmail, i) => {
+ return (
+ {
+ console.log('onDelete triggered: ', recentEmail.id)
+ setOnDeleteId(recentEmail.id)
+ }}
+ deleteTitle="Delete"
+ sublineElement={
+
+ {`From ${formattedShortDate(recentEmail.from)},`}
+ {`Received ${formattedShortDate(recentEmail.createdAt)} at `}
+
+ {recentEmail.to}
+
+
+ }
+ />
+ )
+ })
+ ) : (
+
+ )}
+ {/*
+ {addModalOpen && (
+
+ )}
+
+ {apiKeyGenerated && (
+ {
+ await navigator.clipboard.writeText(apiKeyGenerated)
+ setApiKeyGenerated('')
+ }}
+ onOpenChange={() => setApiKeyGenerated('')}
+ />
+ )}
+
+ {onDeleteId && (
+ {
+ await onDelete(onDeleteId)
+ setOnDeleteId('')
+ }}
+ onOpenChange={() => setOnDeleteId('')}
+ />
+ )} */}
+
+ )
+}