Add basic UI for subscriptions

This commit is contained in:
Hongbo Wu
2022-04-22 16:05:07 +08:00
committed by Jackson Harper
parent 38c190a08f
commit bcbda50daa
6 changed files with 295 additions and 57 deletions

View File

@ -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 {

View File

@ -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;

View File

@ -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;

View File

@ -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 (
<VStack
distribution="center"
css={{
mx: '10px',
color: '$grayText',
paddingBottom: '5rem',
paddingTop: '2rem',
'@md': {
m: '16px',
alignSelf: 'center',
mx: '42px',
paddingTop: '0',
},
}}
>
<HeaderWrapper>
<Box style={{ display: 'flex', alignItems: 'center' }}>
<Box>
<StyledText style="fixedHeadline">{props.heading}</StyledText>
</Box>
{props.infoLink && <InfoLink href={props.infoLink} />}
{props.onAdd && (
<Button
onClick={props.onAdd}
style="ctaDarkYellow"
css={{
display: 'flex',
alignItems: 'center',
marginLeft: 'auto',
}}
>
<SpanBox
css={{
display: 'none',
'@md': {
display: 'flex',
},
}}
>
<SpanBox>Add More</SpanBox>
</SpanBox>
<SpanBox
css={{
p: '0',
display: 'flex',
'@md': {
display: 'none',
},
}}
>
<Plus size={24} />
</SpanBox>
</Button>
)}
</Box>
</HeaderWrapper>
<TableHeading>
{props.headers.map((header, index) => (
<Box
key={index}
css={{
flex: 'auto',
}}
>
<StyledText
key={index}
style="highlightTitle"
css={{
color: '$grayTextContrast',
'@md': {
fontWeight: '600',
color: '$grayTextContrast',
textTransform: 'uppercase',
},
}}
>
{header}
</StyledText>
</Box>
))}
</TableHeading>
{props.rows.map((row, index) => (
<TableCard
key={index}
css={{
'&:hover': {
background: 'rgba(255, 234, 159, 0.12)',
},
'@mdDown': {
borderTopLeftRadius: index === 0 ? '5px' : '',
borderTopRightRadius: index === 0 ? '5px' : '',
},
borderBottomLeftRadius: index == props.rows.length - 1 ? '5px' : '',
borderBottomRightRadius:
index == props.rows.length - 1 ? '5px' : '',
}}
>
<Box
css={{
display: 'flex',
width: '100%',
flexDirection: 'column',
'@md': {
flexDirection: 'row',
},
}}
>
{row.map((cell, index) => (
<HStack
key={index}
css={{
flex: 'auto',
display: 'flex',
padding: '4px 4px 4px 0px',
}}
>
<Input
type="text"
value={cell}
disabled
css={{
width: '100%',
}}
></Input>
</HStack>
))}
{props.onDelete && (
<IconButton
style="ctaWhite"
css={{ mr: '$1', background: '$labelButtonsBg' }}
onClick={() => {
props.onDelete(row[0])
}}
>
<Trash size={16} color={iconColor} />
</IconButton>
)}
</Box>
</TableCard>
))}
</VStack>
)
}

View File

@ -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

View File

@ -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<string | null>(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 (
<PrimaryLayout pageTestId="settings-subscriptions-tag">
<Toaster
@ -38,37 +40,25 @@ export default function SubscriptionsPage(): JSX.Element {
top: '5rem',
}}
/>
<VStack
css={{
mx: '10px',
color: '$grayText',
}}
>
{confirmUnsubscribeName ? (
<ConfirmationModal
message={
'Are you sure? You will stop receiving newsletters from this subscription.'
}
onAccept={async () => {
await onUnsubscribe(confirmUnsubscribeName)
setConfirmUnsubscribeName(null)
}}
onOpenChange={() => setConfirmUnsubscribeName(null)}
/>
) : null}
<HeaderWrapper>
<Box style={{ display: 'flex', alignItems: 'center' }}>
<Box>
<StyledText style="fixedHeadline">Subscriptions</StyledText>
</Box>
</Box>
</HeaderWrapper>
{subscriptions
? subscriptions.map((subscription, i) => {
return
})
: null}
</VStack>
{confirmUnsubscribeName ? (
<ConfirmationModal
message={
'Are you sure? You will stop receiving newsletters from this subscription.'
}
onAccept={async () => {
await onUnsubscribe(confirmUnsubscribeName)
setConfirmUnsubscribeName(null)
}}
onOpenChange={() => setConfirmUnsubscribeName(null)}
/>
) : null}
<Table
heading={'Subscriptions'}
headers={headers}
rows={rows}
onDelete={setConfirmUnsubscribeName}
/>
</PrimaryLayout>
)
}