Skip to content

Commit 67aed36

Browse files
authored
Merge pull request #252 from djyde/subscription
Subscription
2 parents 47a172e + c07f9e4 commit 67aed36

File tree

21 files changed

+851
-48
lines changed

21 files changed

+851
-48
lines changed

components/Layout.tsx

Lines changed: 136 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useMutation, useQuery } from "react-query"
33
import { useRouter } from "next/router"
44
import { AiOutlineLogout, AiOutlineSetting, AiOutlineFileText, AiOutlineAlert, AiOutlinePlus, AiOutlineComment, AiOutlineCode, AiOutlineRight, AiOutlineDown, AiOutlineFile, AiOutlineQuestion, AiOutlineQuestionCircle } from 'react-icons/ai'
55
import { signout, signOut } from "next-auth/client"
6-
import { Anchor, AppShell, Avatar, Badge, Box, Button, Code, Group, Header, Menu, Modal, Navbar, NavLink, ScrollArea, Select, Space, Stack, Switch, Text, TextInput, Title } from "@mantine/core"
6+
import { Anchor, AppShell, Avatar, Badge, Box, Button, Code, Grid, Group, Header, List, Menu, Modal, Navbar, NavLink, Paper, Progress, ScrollArea, Select, Space, Stack, Switch, Text, TextInput, Title } from "@mantine/core"
77
import Link from "next/link"
88
import type { ProjectServerSideProps } from "../pages/dashboard/project/[projectId]/settings"
99
import { modals } from "@mantine/modals"
@@ -13,6 +13,8 @@ import { apiClient } from "../utils.client"
1313
import { useForm } from "react-hook-form"
1414
import { MainLayoutData } from "../service/viewData.service"
1515
import { Head } from "./Head"
16+
import dayjs from "dayjs"
17+
import { usageLimitation } from "../config.common"
1618

1719
// From https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript
1820
function validateEmail(email) {
@@ -55,6 +57,25 @@ export function MainLayout(props: {
5557
},
5658
})
5759

60+
const downgradePlanMutation = useMutation(async () => {
61+
await apiClient.delete('/subscription')
62+
}, {
63+
onSuccess() {
64+
notifications.show({
65+
title: 'Success',
66+
message: 'Downgrade success',
67+
color: 'green'
68+
})
69+
},
70+
onError() {
71+
notifications.show({
72+
title: 'Error',
73+
message: 'Something went wrong, please contact [email protected]',
74+
color: 'red'
75+
})
76+
}
77+
})
78+
5879
const updateNewCommentNotification = useMutation(updateUserSettings, {
5980
onSuccess() {
6081
notifications.show({
@@ -209,11 +230,14 @@ export function MainLayout(props: {
209230
}, [])
210231

211232
const badge = React.useMemo(() => {
212-
if (!props.config.isHosted) {
213-
return <Badge color="green" size="xs">OSS</Badge>
233+
if (props.subscription.isActived) {
234+
return <Badge color="green" size="xs">PRO</Badge>
214235
}
215236

216-
return <Badge color="green" size="xs">PRO</Badge>
237+
if (props.config.isHosted) {
238+
return <Badge color="gray" size="xs">OSS</Badge>
239+
}
240+
return <Badge color="green" size="xs">FREE</Badge>
217241
}, [])
218242

219243
const header = React.useMemo(() => {
@@ -242,12 +266,43 @@ export function MainLayout(props: {
242266
<Group spacing={4}>
243267
<Button onClick={_ => {
244268
openUserModal()
245-
}} size="xs" rightIcon={<AiOutlineRight />} variant='subtle'>{props.session.user.name}</Button>
269+
}} size="xs" rightIcon={<AiOutlineRight />} variant='subtle'>{props.session.user.name} {badge}</Button>
246270
</Group>
247271
</Group>
248272
)
249273
}, [])
250274

275+
const usageBoard = React.useMemo(() => {
276+
return (
277+
<>
278+
<Text size="sm" weight={900}>
279+
Usage (per month)
280+
</Text>
281+
<Stack spacing={4}>
282+
<Group spacing={4}>
283+
<Text weight={500} size="sm">Sites:</Text>
284+
<Text size='sm'>
285+
{`${props.usage.projectCount} / ${usageLimitation['create_site']}`}
286+
</Text>
287+
</Group>
288+
289+
<Group spacing={4}>
290+
<Text weight={500} size="sm">Approve comments:</Text>
291+
<Text size='sm'>
292+
{`${props.usage.approveCommentUsage} / ${usageLimitation['approve_comment']}`}
293+
</Text>
294+
</Group>
295+
<Group spacing={4}>
296+
<Text weight={500} size="sm">Quick Approve:</Text>
297+
<Text size='sm'>
298+
{`${props.usage.quickApproveUsage} / ${usageLimitation['quick_approve']}`}
299+
</Text>
300+
</Group>
301+
</Stack>
302+
</>
303+
)
304+
}, [])
305+
251306
return (
252307
<>
253308
<Head title={`${props.project.title} - Cusdis`} />
@@ -273,7 +328,7 @@ export function MainLayout(props: {
273328
}
274329
}}
275330
>
276-
<Modal opened={isUserPannelOpen} onClose={closeUserModal}
331+
<Modal opened={isUserPannelOpen} size="lg" onClose={closeUserModal}
277332
title="User Settings"
278333
>
279334
<Stack>
@@ -298,11 +353,81 @@ export function MainLayout(props: {
298353
<Text weight={500} size="sm">Display name</Text>
299354
<TextInput placeholder={props.userInfo.name} {...userSettingsForm.register("displayName")} size="sm" />
300355
</Stack>
301-
{/* <Stack spacing={8}>
302-
<Text weight={500} size="sm">Subscription </Text>
303-
<Text size="sm">Current plan: {badge}</Text>
304-
<Anchor size="sm">Manage subscription</Anchor>
305-
</Stack> */}
356+
{props.config.checkout.enabled && (
357+
<>
358+
{usageBoard}
359+
<Stack spacing={8}>
360+
<Text weight={900} size="sm">Subscription </Text>
361+
<Grid>
362+
<Grid.Col span={6}>
363+
<Paper sx={theme => ({
364+
border: '1px solid #eaeaea',
365+
padding: theme.spacing.md
366+
})}>
367+
<Stack>
368+
<Title order={4}>
369+
Free
370+
</Title>
371+
<List size='sm' sx={{
372+
}}>
373+
<List.Item>
374+
Up to 1 site
375+
</List.Item>
376+
<List.Item>
377+
10 Quick Approve / month
378+
</List.Item>
379+
<List.Item>
380+
100 approved comments / month
381+
</List.Item>
382+
</List>
383+
{!props.subscription.isActived || props.subscription.status === 'cancelled' ? (
384+
<Button disabled size="xs">Current plan</Button>
385+
) : (
386+
<Button size="xs" variant={'outline'} loading={downgradePlanMutation.isLoading} onClick={_ => {
387+
if (window.confirm('Are you sure to downgrade?')) {
388+
downgradePlanMutation.mutate()
389+
}
390+
}}>Downgrade</Button>
391+
)}
392+
</Stack>
393+
</Paper>
394+
</Grid.Col>
395+
<Grid.Col span={6}>
396+
<Paper sx={theme => ({
397+
border: '1px solid #eaeaea',
398+
padding: theme.spacing.md
399+
})}>
400+
<Stack>
401+
<Title order={4}>
402+
Pro
403+
</Title>
404+
<List size='sm' sx={{
405+
}}>
406+
<List.Item>
407+
Unlimited sites
408+
</List.Item>
409+
<List.Item>
410+
Unlimited Quick Approve
411+
</List.Item>
412+
<List.Item>
413+
Unlimited approved comments
414+
</List.Item>
415+
</List>
416+
{props.subscription.isActived ? (
417+
<>
418+
<Button size="xs" component="a" href={props.subscription.updatePaymentMethodUrl}>Manage payment method</Button>
419+
{props.subscription.status === 'cancelled' && (<Text size='xs' align='center'>Expire on {dayjs(props.subscription.endAt).format('YYYY/MM/DD')}</Text>)}
420+
</>
421+
) : (
422+
<Button size='xs' component="a" href={`${props.config.checkout.url}?checkout[custom][user_id]=${props.session.uid}`}>Upgrade $5/month</Button>
423+
)}
424+
</Stack>
425+
</Paper>
426+
</Grid.Col>
427+
</Grid>
428+
</Stack>
429+
</>
430+
)}
306431
<Button loading={updateUserSettingsMutation.isLoading} onClick={onClickSaveUserSettings}>Save</Button>
307432
<Button onClick={_ => signOut()} variant={'outline'} color='red'>
308433
Logout

components/Navbar.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {
2+
Box,
3+
Container,
4+
Flex,
5+
Link,
6+
Menu,
7+
MenuButton,
8+
MenuItem,
9+
MenuList,
10+
Spacer,
11+
} from "@chakra-ui/react"
12+
import React from "react"
13+
import { signOut } from "next-auth/client"
14+
import { UserSession } from "../service"
15+
16+
export function Navbar(props: { session: UserSession }) {
17+
return (
18+
<Box py={4}>
19+
<Container maxWidth={"5xl"}>
20+
<Flex>
21+
<Box>
22+
<Link fontWeight="bold" fontSize="xl" href="/dashboard">
23+
Cusdis
24+
</Link>
25+
</Box>
26+
<Spacer />
27+
<Box>
28+
<Menu>
29+
<MenuButton>{props.session.user.name}</MenuButton>
30+
<MenuList>
31+
<MenuItem><Link width="100%" href="/user">Settings</Link></MenuItem>
32+
<MenuItem onClick={() => signOut()}>Logout</MenuItem>
33+
</MenuList>
34+
</Menu>
35+
</Box>
36+
</Flex>
37+
</Container>
38+
</Box>
39+
)
40+
}

config.common.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export enum UsageLabel {
2+
ApproveComment = 'approve_comment',
3+
QuickApprove = 'quick_approve',
4+
CreateSite = 'create_site'
5+
}
6+
7+
export const usageLimitation = {
8+
[UsageLabel.ApproveComment]: 100,
9+
[UsageLabel.QuickApprove]: 10,
10+
[UsageLabel.CreateSite]: 1
11+
}

pages/api/comment/[commentId]/approve.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { NextApiRequest, NextApiResponse } from 'next'
22
import { AuthService } from '../../../../service/auth.service'
33
import { CommentService } from '../../../../service/comment.service'
4+
import { SubscriptionService, usageLimitation } from '../../../../service/subscription.service'
5+
import { UsageLabel, UsageService } from '../../../../service/usage.service'
6+
import { getSession } from '../../../../utils.server'
47

58
export default async function handler(
69
req: NextApiRequest,
710
res: NextApiResponse,
811
) {
912
const commentService = new CommentService(req)
1013
const authService = new AuthService(req, res)
14+
const usageService = new UsageService(req)
15+
const session = await getSession(req)
16+
17+
const subscriptionService = new SubscriptionService()
1118

1219
if (req.method === 'POST') {
1320
const commentId = req.query.commentId as string
@@ -18,7 +25,18 @@ export default async function handler(
1825
return
1926
}
2027

28+
// check usage
29+
if (!await subscriptionService.approveCommentValidate(session.uid)) {
30+
res.status(402).json({
31+
error:
32+
`You have reached the maximum number of approving comments on free plan (${usageLimitation['approve_comment']}/month). Please upgrade to Pro plan to approve more comments.`,
33+
})
34+
return
35+
}
36+
2137
await commentService.approve(commentId)
38+
await usageService.incr(UsageLabel.ApproveComment)
39+
2240
res.json({
2341
message: 'success',
2442
})

pages/api/open/approve.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ import { resolvedConfig } from '../../../utils.server'
33
import jwt from 'jsonwebtoken'
44
import { CommentService } from '../../../service/comment.service'
55
import { SecretKey, TokenBody, TokenService } from '../../../service/token.service'
6+
import { UsageService } from '../../../service/usage.service'
7+
import { SubscriptionService } from '../../../service/subscription.service'
8+
import { UsageLabel, usageLimitation } from '../../../config.common'
69

710
export default async function handler(
811
req: NextApiRequest,
912
res: NextApiResponse,
1013
) {
1114
const commentService = new CommentService(req)
15+
const usageService = new UsageService(req)
16+
const subscriptionService = new SubscriptionService()
17+
1218
const tokenService = new TokenService()
1319

1420
if (req.method === 'GET') {
@@ -59,6 +65,14 @@ export default async function handler(
5965
return
6066
}
6167

68+
// check usage
69+
if (!await subscriptionService.quickApproveValidate(tokenBody.owner.id)) {
70+
res.status(402).json({
71+
error: `You have reached the maximum number of Quick Approve on free plan (${usageLimitation.quick_approve}/month). Please upgrade to Pro plan to use Quick Approve more.`
72+
})
73+
return
74+
}
75+
6276
// firstly, approve comment
6377
await commentService.approve(tokenBody.commentId)
6478

@@ -69,6 +83,8 @@ export default async function handler(
6983
})
7084
}
7185

86+
await usageService.incr(UsageLabel.QuickApprove)
87+
7288
res.json({
7389
message: 'success'
7490
})

pages/api/projects.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,34 @@
11
import { NextApiRequest, NextApiResponse } from "next";
22
import { ProjectService } from "../../service/project.service";
3+
import { SubscriptionService } from "../../service/subscription.service";
4+
import { getSession, prisma } from "../../utils.server";
35

46
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
57
const projectService = new ProjectService(req)
8+
const subscriptionService = new SubscriptionService()
9+
const session = await getSession(req)
610

711
if (req.method === 'POST') {
12+
if (!session) {
13+
res.status(401).json({
14+
error: 'Unauthorized'
15+
})
16+
return
17+
}
18+
19+
// check subscription
20+
if (!await subscriptionService.createProjectValidate(session.uid)) {
21+
// if (true) {
22+
res.status(402).json({
23+
error: 'You have reached the maximum number of sites on free plan. Please upgrade to Pro plan to create more sites.'
24+
})
25+
return
26+
}
27+
828
const { title } = req.body as {
929
title: string
1030
}
31+
1132
const created = await projectService.create(title)
1233

1334
res.json({

0 commit comments

Comments
 (0)