aboutsummaryrefslogtreecommitdiffstats
path: root/src/views/Settings
diff options
context:
space:
mode:
authorLibravatar Mo Tarbin <mhed.t91@gmail.com>2024-06-30 18:55:39 -0400
committerLibravatar Mo Tarbin <mhed.t91@gmail.com>2024-06-30 18:55:39 -0400
commit2657469964e24ffbeb905024532120395f6e797c (patch)
tree2fe9db8a4ecfa92d854ca94f7586d81163c8bd25 /src/views/Settings
downloaddonetick-frontend-2657469964e24ffbeb905024532120395f6e797c.tar.gz
donetick-frontend-2657469964e24ffbeb905024532120395f6e797c.tar.bz2
donetick-frontend-2657469964e24ffbeb905024532120395f6e797c.zip
move to Donetick Org, First commit frontend
Diffstat (limited to 'src/views/Settings')
-rw-r--r--src/views/Settings/APITokenSettings.jsx130
-rw-r--r--src/views/Settings/NotificationSetting.jsx90
-rw-r--r--src/views/Settings/Settings.jsx384
-rw-r--r--src/views/Settings/Sharing.jsx0
-rw-r--r--src/views/Settings/SharingSettings.jsx0
-rw-r--r--src/views/Settings/ThemeToggle.jsx62
6 files changed, 666 insertions, 0 deletions
diff --git a/src/views/Settings/APITokenSettings.jsx b/src/views/Settings/APITokenSettings.jsx
new file mode 100644
index 0000000..5bd9887
--- /dev/null
+++ b/src/views/Settings/APITokenSettings.jsx
@@ -0,0 +1,130 @@
+import { Box, Button, Card, Chip, Divider, Typography } from '@mui/joy'
+import moment from 'moment'
+import { useContext, useEffect, useState } from 'react'
+import { UserContext } from '../../contexts/UserContext'
+import {
+ CreateLongLiveToken,
+ DeleteLongLiveToken,
+ GetLongLiveTokens,
+} from '../../utils/Fetcher'
+import { isPlusAccount } from '../../utils/Helpers'
+import TextModal from '../Modals/Inputs/TextModal'
+
+const APITokenSettings = () => {
+ const [tokens, setTokens] = useState([])
+ const [isGetTokenNameModalOpen, setIsGetTokenNameModalOpen] = useState(false)
+ const { userProfile, setUserProfile } = useContext(UserContext)
+ useEffect(() => {
+ GetLongLiveTokens().then(resp => {
+ resp.json().then(data => {
+ setTokens(data.res)
+ })
+ })
+ }, [])
+
+ const handleSaveToken = name => {
+ CreateLongLiveToken(name).then(resp => {
+ if (resp.ok) {
+ resp.json().then(data => {
+ // add the token to the list:
+ console.log(data)
+ const newTokens = [...tokens]
+ newTokens.push(data.res)
+ setTokens(newTokens)
+ })
+ }
+ })
+ }
+
+ return (
+ <div className='grid gap-4 py-4' id='apitokens'>
+ <Typography level='h3'>Long Live Token</Typography>
+ <Divider />
+ <Typography level='body-sm'>
+ Create token to use with the API to update things that trigger task or
+ chores
+ </Typography>
+ {!isPlusAccount(userProfile) && (
+ <Chip variant='soft' color='warning'>
+ Not available in Basic Plan
+ </Chip>
+ )}
+
+ {tokens.map(token => (
+ <Card key={token.token} className='p-4'>
+ <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
+ <Box>
+ <Typography level='body-md'>{token.name}</Typography>
+ <Typography level='body-xs'>
+ {moment(token.createdAt).fromNow()}(
+ {moment(token.createdAt).format('lll')})
+ </Typography>
+ </Box>
+ <Box>
+ {token.token && (
+ <Button
+ variant='outlined'
+ color='primary'
+ sx={{ mr: 1 }}
+ onClick={() => {
+ navigator.clipboard.writeText(token.token)
+ alert('Token copied to clipboard')
+ }}
+ >
+ Copy Token
+ </Button>
+ )}
+
+ <Button
+ variant='outlined'
+ color='danger'
+ onClick={() => {
+ const confirmed = confirm(
+ `Are you sure you want to remove ${token.name} ?`,
+ )
+ if (confirmed) {
+ DeleteLongLiveToken(token.id).then(resp => {
+ if (resp.ok) {
+ alert('Token removed')
+ const newTokens = tokens.filter(t => t.id !== token.id)
+ setTokens(newTokens)
+ }
+ })
+ }
+ }}
+ >
+ Remove
+ </Button>
+ </Box>
+ </Box>
+ </Card>
+ ))}
+
+ <Button
+ variant='soft'
+ color='primary'
+ disabled={!isPlusAccount(userProfile)}
+ sx={{
+ width: '210px',
+ mb: 1,
+ }}
+ onClick={() => {
+ setIsGetTokenNameModalOpen(true)
+ }}
+ >
+ Generate New Token
+ </Button>
+ <TextModal
+ isOpen={isGetTokenNameModalOpen}
+ title='Give a name for your new token, something to remember it by.'
+ onClose={() => {
+ setIsGetTokenNameModalOpen(false)
+ }}
+ okText={'Generate Token'}
+ onSave={handleSaveToken}
+ />
+ </div>
+ )
+}
+
+export default APITokenSettings
diff --git a/src/views/Settings/NotificationSetting.jsx b/src/views/Settings/NotificationSetting.jsx
new file mode 100644
index 0000000..4ead3b9
--- /dev/null
+++ b/src/views/Settings/NotificationSetting.jsx
@@ -0,0 +1,90 @@
+import { Button, Divider, Input, Option, Select, Typography } from '@mui/joy'
+import { useContext, useEffect, useState } from 'react'
+import { UserContext } from '../../contexts/UserContext'
+import { GetUserProfile, UpdateUserDetails } from '../../utils/Fetcher'
+
+const NotificationSetting = () => {
+ const { userProfile, setUserProfile } = useContext(UserContext)
+ useEffect(() => {
+ if (!userProfile) {
+ GetUserProfile().then(resp => {
+ resp.json().then(data => {
+ setUserProfile(data.res)
+ setChatID(data.res.chatID)
+ })
+ })
+ }
+ }, [])
+ const [chatID, setChatID] = useState(userProfile?.chatID)
+
+ return (
+ <div className='grid gap-4 py-4' id='notifications'>
+ <Typography level='h3'>Notification Settings</Typography>
+ <Divider />
+ <Typography level='body-md'>Manage your notification settings</Typography>
+
+ <Select defaultValue='telegram' sx={{ maxWidth: '200px' }} disabled>
+ <Option value='telegram'>Telegram</Option>
+ <Option value='discord'>Discord</Option>
+ </Select>
+
+ <Typography level='body-xs'>
+ You need to initiate a message to the bot in order for the Telegram
+ notification to work{' '}
+ <a
+ style={{
+ textDecoration: 'underline',
+ color: '#0891b2',
+ }}
+ href='https://t.me/DonetickBot'
+ >
+ Click here
+ </a>{' '}
+ to start a chat
+ </Typography>
+
+ <Input
+ value={chatID}
+ onChange={e => setChatID(e.target.value)}
+ placeholder='User ID / Chat ID'
+ sx={{
+ width: '200px',
+ }}
+ />
+ <Typography mt={0} level='body-xs'>
+ If you don't know your Chat ID, start chat with userinfobot and it will
+ send you your Chat ID.{' '}
+ <a
+ style={{
+ textDecoration: 'underline',
+ color: '#0891b2',
+ }}
+ href='https://t.me/userinfobot'
+ >
+ Click here
+ </a>{' '}
+ to start chat with userinfobot{' '}
+ </Typography>
+
+ <Button
+ sx={{
+ width: '110px',
+ mb: 1,
+ }}
+ onClick={() => {
+ UpdateUserDetails({
+ chatID: Number(chatID),
+ }).then(resp => {
+ resp.json().then(data => {
+ setUserProfile(data)
+ })
+ })
+ }}
+ >
+ Save
+ </Button>
+ </div>
+ )
+}
+
+export default NotificationSetting
diff --git a/src/views/Settings/Settings.jsx b/src/views/Settings/Settings.jsx
new file mode 100644
index 0000000..d612eec
--- /dev/null
+++ b/src/views/Settings/Settings.jsx
@@ -0,0 +1,384 @@
+import {
+ Box,
+ Button,
+ Card,
+ Chip,
+ CircularProgress,
+ Container,
+ Divider,
+ Input,
+ Typography,
+} from '@mui/joy'
+import moment from 'moment'
+import { useContext, useEffect, useState } from 'react'
+import { UserContext } from '../../contexts/UserContext'
+import Logo from '../../Logo'
+import {
+ AcceptCircleMemberRequest,
+ CancelSubscription,
+ DeleteCircleMember,
+ GetAllCircleMembers,
+ GetCircleMemberRequests,
+ GetSubscriptionSession,
+ GetUserCircle,
+ GetUserProfile,
+ JoinCircle,
+ LeaveCircle,
+} from '../../utils/Fetcher'
+import APITokenSettings from './APITokenSettings'
+import NotificationSetting from './NotificationSetting'
+import ThemeToggle from './ThemeToggle'
+
+const Settings = () => {
+ const { userProfile, setUserProfile } = useContext(UserContext)
+ const [userCircles, setUserCircles] = useState([])
+ const [circleMemberRequests, setCircleMemberRequests] = useState([])
+ const [circleInviteCode, setCircleInviteCode] = useState('')
+ const [circleMembers, setCircleMembers] = useState([])
+ useEffect(() => {
+ GetUserProfile().then(resp => {
+ resp.json().then(data => {
+ setUserProfile(data.res)
+ })
+ })
+ GetUserCircle().then(resp => {
+ resp.json().then(data => {
+ setUserCircles(data.res ? data.res : [])
+ })
+ })
+ GetCircleMemberRequests().then(resp => {
+ resp.json().then(data => {
+ setCircleMemberRequests(data.res ? data.res : [])
+ })
+ })
+ GetAllCircleMembers()
+ .then(res => res.json())
+ .then(data => {
+ setCircleMembers(data.res ? data.res : [])
+ })
+ }, [])
+
+ useEffect(() => {
+ const hash = window.location.hash
+ if (hash) {
+ const sharingSection = document.getElementById(
+ window.location.hash.slice(1),
+ )
+ if (sharingSection) {
+ sharingSection.scrollIntoView({ behavior: 'smooth' })
+ }
+ }
+ }, [])
+
+ const getSubscriptionDetails = () => {
+ if (userProfile?.subscription === 'active') {
+ return `You are currently subscribed to the Plus plan. Your subscription will renew on ${moment(
+ userProfile?.expiration,
+ ).format('MMM DD, YYYY')}.`
+ } else if (userProfile?.subscription === 'canceled') {
+ return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${moment(
+ userProfile?.expiration,
+ ).format('MMM DD, YYYY')}.`
+ } else {
+ return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.`
+ }
+ }
+ const getSubscriptionStatus = () => {
+ if (userProfile?.subscription === 'active') {
+ return `Plus`
+ } else if (userProfile?.subscription === 'canceled') {
+ if (moment().isBefore(userProfile?.expiration)) {
+ return `Plus(until ${moment(userProfile?.expiration).format(
+ 'MMM DD, YYYY',
+ )})`
+ }
+ return `Free`
+ } else {
+ return `Free`
+ }
+ }
+
+ if (userProfile === null) {
+ return (
+ <Container className='flex h-full items-center justify-center'>
+ <Box className='flex flex-col items-center justify-center'>
+ <CircularProgress
+ color='success'
+ sx={{ '--CircularProgress-size': '200px' }}
+ >
+ <Logo />
+ </CircularProgress>
+ </Box>
+ </Container>
+ )
+ }
+ return (
+ <Container>
+ <div className='grid gap-4 py-4' id='sharing'>
+ <Typography level='h3'>Sharing settings</Typography>
+ <Divider />
+ <Typography level='body-md'>
+ Your account is automatically connected to a Circle when you create or
+ join one. Easily invite friends by sharing the unique Circle code or
+ link below. You'll receive a notification below when someone requests
+ to join your Circle.
+ </Typography>
+ <Typography level='title-sm' mb={-1}>
+ {userCircles[0]?.userRole === 'member'
+ ? `You part of ${userCircles[0]?.name} `
+ : `You circle code is:`}
+
+ <Input
+ value={userCircles[0]?.invite_code}
+ disabled
+ size='lg'
+ sx={{
+ width: '220px',
+ mb: 1,
+ }}
+ />
+ <Button
+ variant='soft'
+ onClick={() => {
+ navigator.clipboard.writeText(userCircles[0]?.invite_code)
+ alert('Code Copied to clipboard')
+ }}
+ >
+ Copy Code
+ </Button>
+ <Button
+ variant='soft'
+ sx={{ ml: 1 }}
+ onClick={() => {
+ navigator.clipboard.writeText(
+ window.location.protocol +
+ '//' +
+ window.location.host +
+ `/circle/join?code=${userCircles[0]?.invite_code}`,
+ )
+ alert('Link Copied to clipboard')
+ }}
+ >
+ Copy Link
+ </Button>
+ {userCircles.length > 0 && userCircles[0]?.userRole === 'member' && (
+ <Button
+ sx={{ ml: 1 }}
+ onClick={() => {
+ const confirmed = confirm(
+ `Are you sure you want to leave your circle?`,
+ )
+ if (confirmed) {
+ LeaveCircle(userCircles[0]?.id).then(resp => {
+ if (resp.ok) {
+ alert('Left circle successfully.')
+ } else {
+ alert('Failed to leave circle.')
+ }
+ })
+ }
+ }}
+ >
+ Leave Circle
+ </Button>
+ )}
+ </Typography>
+ <Typography level='title-md'>Circle Members</Typography>
+ {circleMembers.map(member => (
+ <Card key={member.id} className='p-4'>
+ <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
+ <Box>
+ <Typography level='body-md'>
+ {member.displayName.charAt(0).toUpperCase() +
+ member.displayName.slice(1)}
+ {member.userId === userProfile.id ? '(You)' : ''}{' '}
+ <Chip>
+ {' '}
+ {member.isActive ? member.role : 'Pending Approval'}
+ </Chip>
+ </Typography>
+ {member.isActive ? (
+ <Typography level='body-sm'>
+ Joined on {moment(member.createdAt).format('MMM DD, YYYY')}
+ </Typography>
+ ) : (
+ <Typography level='body-sm' color='danger'>
+ Request to join{' '}
+ {moment(member.updatedAt).format('MMM DD, YYYY')}
+ </Typography>
+ )}
+ </Box>
+ {member.userId !== userProfile.id && member.isActive && (
+ <Button
+ disabled={
+ circleMembers.find(m => userProfile.id == m.userId).role !==
+ 'admin'
+ }
+ variant='outlined'
+ color='danger'
+ size='sm'
+ onClick={() => {
+ const confirmed = confirm(
+ `Are you sure you want to remove ${member.displayName} from your circle?`,
+ )
+ if (confirmed) {
+ DeleteCircleMember(member.circleId, member.userId).then(
+ resp => {
+ if (resp.ok) {
+ alert('Removed member successfully.')
+ }
+ },
+ )
+ }
+ }}
+ >
+ Remove
+ </Button>
+ )}
+ </Box>
+ </Card>
+ ))}
+
+ {circleMemberRequests.length > 0 && (
+ <Typography level='title-md'>Circle Member Requests</Typography>
+ )}
+ {circleMemberRequests.map(request => (
+ <Card key={request.id} className='p-4'>
+ <Typography level='body-md'>
+ {request.displayName} wants to join your circle.
+ </Typography>
+ <Button
+ variant='soft'
+ color='success'
+ onClick={() => {
+ const confirmed = confirm(
+ `Are you sure you want to accept ${request.displayName}(username:${request.username}) to join your circle?`,
+ )
+ if (confirmed) {
+ AcceptCircleMemberRequest(request.id).then(resp => {
+ if (resp.ok) {
+ alert('Accepted request successfully.')
+ // reload the page
+ window.location.reload()
+ }
+ })
+ }
+ }}
+ >
+ Accept
+ </Button>
+ </Card>
+ ))}
+ <Divider> or </Divider>
+
+ <Typography level='body-md'>
+ if want to join someone else's Circle? Ask them for their unique
+ Circle code or join link. Enter the code below to join their Circle.
+ </Typography>
+
+ <Typography level='title-sm' mb={-1}>
+ Enter Circle code:
+ <Input
+ placeholder='Enter code'
+ value={circleInviteCode}
+ onChange={e => setCircleInviteCode(e.target.value)}
+ size='lg'
+ sx={{
+ width: '220px',
+ mb: 1,
+ }}
+ />
+ <Button
+ variant='soft'
+ onClick={() => {
+ const confirmed = confirm(
+ `Are you sure you want to leave you circle and join '${circleInviteCode}'?`,
+ )
+ if (confirmed) {
+ JoinCircle(circleInviteCode).then(resp => {
+ if (resp.ok) {
+ alert(
+ 'Joined circle successfully, wait for the circle owner to accept your request.',
+ )
+ }
+ })
+ }
+ }}
+ >
+ Join Circle
+ </Button>
+ </Typography>
+ </div>
+
+ <div className='grid gap-4 py-4' id='account'>
+ <Typography level='h3'>Account Settings</Typography>
+ <Divider />
+ <Typography level='body-md'>
+ Change your account settings, including your password, display name
+ </Typography>
+ <Typography level='title-md' mb={-1}>
+ Account Type : {getSubscriptionStatus()}
+ </Typography>
+ <Typography level='body-sm'>{getSubscriptionDetails()}</Typography>
+ <Box>
+ <Button
+ sx={{
+ width: '110px',
+ mb: 1,
+ }}
+ disabled={
+ userProfile?.subscription === 'active' ||
+ moment(userProfile?.expiration).isAfter(moment())
+ }
+ onClick={() => {
+ GetSubscriptionSession().then(data => {
+ data.json().then(data => {
+ console.log(data)
+ window.location.href = data.sessionURL
+ // open in new window:
+ // window.open(data.sessionURL, '_blank')
+ })
+ })
+ }}
+ >
+ Upgrade
+ </Button>
+
+ {userProfile?.subscription === 'active' && (
+ <Button
+ sx={{
+ width: '110px',
+ mb: 1,
+ ml: 1,
+ }}
+ variant='outlined'
+ onClick={() => {
+ CancelSubscription().then(resp => {
+ if (resp.ok) {
+ alert('Subscription cancelled.')
+ window.location.reload()
+ }
+ })
+ }}
+ >
+ Cancel
+ </Button>
+ )}
+ </Box>
+ </div>
+ <NotificationSetting />
+ <APITokenSettings />
+ <div className='grid gap-4 py-4'>
+ <Typography level='h3'>Theme preferences</Typography>
+ <Divider />
+ <Typography level='body-md'>
+ Choose how the site looks to you. Select a single theme, or sync with
+ your system and automatically switch between day and night themes.
+ </Typography>
+ <ThemeToggle />
+ </div>
+ </Container>
+ )
+}
+
+export default Settings
diff --git a/src/views/Settings/Sharing.jsx b/src/views/Settings/Sharing.jsx
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/views/Settings/Sharing.jsx
diff --git a/src/views/Settings/SharingSettings.jsx b/src/views/Settings/SharingSettings.jsx
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/views/Settings/SharingSettings.jsx
diff --git a/src/views/Settings/ThemeToggle.jsx b/src/views/Settings/ThemeToggle.jsx
new file mode 100644
index 0000000..6ff33f1
--- /dev/null
+++ b/src/views/Settings/ThemeToggle.jsx
@@ -0,0 +1,62 @@
+import useStickyState from '@/hooks/useStickyState'
+import {
+ DarkModeOutlined,
+ LaptopOutlined,
+ LightModeOutlined,
+} from '@mui/icons-material'
+import {
+ Button,
+ FormControl,
+ FormLabel,
+ ToggleButtonGroup,
+ useColorScheme,
+} from '@mui/joy'
+
+const ELEMENTID = 'select-theme-mode'
+
+const ThemeToggle = () => {
+ const { mode, setMode } = useColorScheme()
+ const [themeMode, setThemeMode] = useStickyState(mode, 'themeMode')
+
+ const handleThemeModeChange = (_, newThemeMode) => {
+ if (!newThemeMode) return
+ setThemeMode(newThemeMode)
+ setMode(newThemeMode)
+ }
+
+ const FormThemeModeToggleLabel = () => (
+ <FormLabel
+ level='title-md'
+ id={`${ELEMENTID}-label`}
+ htmlFor='select-theme-mode'
+ >
+ Theme mode
+ </FormLabel>
+ )
+
+ return (
+ <FormControl>
+ <FormThemeModeToggleLabel />
+ <div className='flex items-center gap-4'>
+ <ToggleButtonGroup
+ id={ELEMENTID}
+ variant='outlined'
+ value={themeMode}
+ onChange={handleThemeModeChange}
+ >
+ <Button startDecorator={<LightModeOutlined />} value='light'>
+ Light
+ </Button>
+ <Button startDecorator={<DarkModeOutlined />} value='dark'>
+ Dark
+ </Button>
+ <Button startDecorator={<LaptopOutlined />} value='system'>
+ System
+ </Button>
+ </ToggleButtonGroup>
+ </div>
+ </FormControl>
+ )
+}
+
+export default ThemeToggle