move to Donetick Org, First commit frontend

This commit is contained in:
Mo Tarbin 2024-06-30 18:55:39 -04:00
commit 2657469964
105 changed files with 21572 additions and 0 deletions

View file

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

View file

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

View file

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

View file

View file

View file

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