Support Sortinging in My Chore by Date
Show API Token and allow manual copy Show Error when sign up disabled Support Pushover
This commit is contained in:
commit
4fd959b277
7 changed files with 389 additions and 104 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "donetick",
|
||||
"private": true,
|
||||
"version": "0.1.80",
|
||||
"version": "0.1.82",
|
||||
"type": "module",
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
|
|
|
@ -225,6 +225,14 @@ const UpdateUserDetails = userDetails => {
|
|||
})
|
||||
}
|
||||
|
||||
const UpdateNotificationTarget = notificationTarget => {
|
||||
return Fetch(`${API_URL}/users/targets`, {
|
||||
method: 'PUT',
|
||||
headers: HEADERS(),
|
||||
body: JSON.stringify(notificationTarget),
|
||||
})
|
||||
}
|
||||
|
||||
const GetSubscriptionSession = () => {
|
||||
return Fetch(API_URL + `/payments/create-subscription`, {
|
||||
method: 'GET',
|
||||
|
@ -373,6 +381,7 @@ export {
|
|||
UpdateChoreHistory,
|
||||
UpdateChorePriority,
|
||||
UpdateLabel,
|
||||
UpdateNotificationTarget,
|
||||
UpdatePassword,
|
||||
UpdateThingState,
|
||||
UpdateUserDetails,
|
||||
|
|
|
@ -103,12 +103,13 @@ const SignupView = () => {
|
|||
signUp(username, password, displayName, email).then(response => {
|
||||
if (response.status === 201) {
|
||||
handleLogin(username, password)
|
||||
} else if (response.status === 403) {
|
||||
setError('Signup disabled, please contact admin')
|
||||
} else {
|
||||
console.log('Signup failed')
|
||||
response.json().then(res => {
|
||||
setError(res.error)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -466,7 +466,6 @@ const ChoreCard = ({
|
|||
>
|
||||
<Grid container>
|
||||
<Grid
|
||||
item
|
||||
xs={9}
|
||||
onClick={() => {
|
||||
navigate(`/chores/${chore.id}`)
|
||||
|
@ -556,7 +555,6 @@ const ChoreCard = ({
|
|||
</Box> */}
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={3}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
|
|
|
@ -2,15 +2,20 @@ import {
|
|||
Add,
|
||||
CancelRounded,
|
||||
EditCalendar,
|
||||
ExpandCircleDown,
|
||||
FilterAlt,
|
||||
PriorityHigh,
|
||||
Style,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionGroup,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Container,
|
||||
Divider,
|
||||
IconButton,
|
||||
Input,
|
||||
List,
|
||||
|
@ -38,6 +43,8 @@ const MyChores = () => {
|
|||
const [chores, setChores] = useState([])
|
||||
const [filteredChores, setFilteredChores] = useState([])
|
||||
const [selectedFilter, setSelectedFilter] = useState('All')
|
||||
const [choreSections, setChoreSections] = useState([])
|
||||
const [openChoreSections, setOpenChoreSections] = useState({})
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [activeUserId, setActiveUserId] = useState(0)
|
||||
const [performers, setPerformers] = useState([])
|
||||
|
@ -74,6 +81,108 @@ const MyChores = () => {
|
|||
return aDueDate - bDueDate // Sort ascending by due date
|
||||
}
|
||||
|
||||
const sectionSorter = (t, chores) => {
|
||||
// sort by priority then due date:
|
||||
chores.sort((a, b) => {
|
||||
// no priority is lowest priority:
|
||||
if (a.priority === 0) {
|
||||
return 1
|
||||
}
|
||||
if (a.priority !== b.priority) {
|
||||
return a.priority - b.priority
|
||||
}
|
||||
if (a.nextDueDate === null) {
|
||||
return 1
|
||||
}
|
||||
if (b.nextDueDate === null) {
|
||||
return -1
|
||||
}
|
||||
return new Date(a.nextDueDate) - new Date(b.nextDueDate)
|
||||
})
|
||||
|
||||
var groups = []
|
||||
switch (t) {
|
||||
case 'due_date':
|
||||
var groupRaw = {
|
||||
Today: [],
|
||||
'In a week': [],
|
||||
'This month': [],
|
||||
Later: [],
|
||||
Overdue: [],
|
||||
Anytime: [],
|
||||
}
|
||||
chores.forEach(chore => {
|
||||
if (chore.nextDueDate === null) {
|
||||
groupRaw['Anytime'].push(chore)
|
||||
} else if (new Date(chore.nextDueDate) < new Date()) {
|
||||
groupRaw['Overdue'].push(chore)
|
||||
} else if (
|
||||
new Date(chore.nextDueDate).toDateString() ===
|
||||
new Date().toDateString()
|
||||
) {
|
||||
groupRaw['Today'].push(chore)
|
||||
} else if (
|
||||
new Date(chore.nextDueDate) <
|
||||
new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) &&
|
||||
new Date(chore.nextDueDate) > new Date()
|
||||
) {
|
||||
groupRaw['In a week'].push(chore)
|
||||
} else if (
|
||||
new Date(chore.nextDueDate).getMonth() === new Date().getMonth()
|
||||
) {
|
||||
groupRaw['This month'].push(chore)
|
||||
} else {
|
||||
groupRaw['Later'].push(chore)
|
||||
}
|
||||
})
|
||||
groups = [
|
||||
{ name: 'Overdue', content: groupRaw['Overdue'] },
|
||||
{ name: 'Today', content: groupRaw['Today'] },
|
||||
{ name: 'In a week', content: groupRaw['In a week'] },
|
||||
{ name: 'This month', content: groupRaw['This month'] },
|
||||
{ name: 'Later', content: groupRaw['Later'] },
|
||||
{ name: 'Anytime', content: groupRaw['Anytime'] },
|
||||
]
|
||||
break
|
||||
case 'priority':
|
||||
groupRaw = {
|
||||
p1: [],
|
||||
p2: [],
|
||||
p3: [],
|
||||
p4: [],
|
||||
no_priority: [],
|
||||
}
|
||||
chores.forEach(chore => {
|
||||
switch (chore.priority) {
|
||||
case 1:
|
||||
groupRaw['p1'].push(chore)
|
||||
break
|
||||
case 2:
|
||||
groupRaw['p2'].push(chore)
|
||||
break
|
||||
case 3:
|
||||
groupRaw['p3'].push(chore)
|
||||
break
|
||||
case 4:
|
||||
groupRaw['p4'].push(chore)
|
||||
break
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'labels':
|
||||
groupRaw = {}
|
||||
chores.forEach(chore => {
|
||||
chore.labelsV2.forEach(label => {
|
||||
if (groupRaw[label.id] === undefined) {
|
||||
groupRaw[label.id] = []
|
||||
}
|
||||
groupRaw[label.id].push(chore)
|
||||
})
|
||||
})
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (userProfile === null) {
|
||||
GetUserProfile()
|
||||
|
@ -100,6 +209,14 @@ const MyChores = () => {
|
|||
const sortedChores = choresData.res.sort(choreSorter)
|
||||
setChores(sortedChores)
|
||||
setFilteredChores(sortedChores)
|
||||
const sections = sectionSorter('due_date', sortedChores)
|
||||
setChoreSections(sections)
|
||||
setOpenChoreSections(
|
||||
Object.keys(sections).reduce((acc, key) => {
|
||||
acc[key] = true
|
||||
return acc
|
||||
}, {}),
|
||||
)
|
||||
}
|
||||
}, [choresData, choresLoading])
|
||||
|
||||
|
@ -231,11 +348,6 @@ const MyChores = () => {
|
|||
|
||||
return (
|
||||
<Container maxWidth='md'>
|
||||
{/* <Typography level='h3' mb={1.5}>
|
||||
My Chores
|
||||
</Typography> */}
|
||||
{/* <Sheet> */}
|
||||
{/* Search box to filter */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
|
@ -253,7 +365,6 @@ const MyChores = () => {
|
|||
mt: 1,
|
||||
mb: 1,
|
||||
borderRadius: 24,
|
||||
// border: '1px solid',
|
||||
height: 24,
|
||||
borderColor: 'text.disabled',
|
||||
padding: 1,
|
||||
|
@ -271,7 +382,6 @@ const MyChores = () => {
|
|||
}
|
||||
/>
|
||||
<IconButtonWithMenu
|
||||
key={'icon-menu-labels-filter'}
|
||||
icon={<PriorityHigh />}
|
||||
options={Priorities}
|
||||
selectedItem={selectedFilter}
|
||||
|
@ -282,7 +392,6 @@ const MyChores = () => {
|
|||
isActive={selectedFilter.startsWith('Priority: ')}
|
||||
/>
|
||||
<IconButtonWithMenu
|
||||
key={'icon-menu-labels-filter'}
|
||||
icon={<Style />}
|
||||
// TODO : this need simplification we want to display both user labels and chore labels
|
||||
// that why we are merging them here.
|
||||
|
@ -395,7 +504,6 @@ const MyChores = () => {
|
|||
Current Filter: {selectedFilter}
|
||||
</Chip>
|
||||
)}
|
||||
{/* </Sheet> */}
|
||||
{filteredChores.length === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
|
@ -419,7 +527,10 @@ const MyChores = () => {
|
|||
{chores.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => setFilteredChores(chores)}
|
||||
onClick={() => {
|
||||
setFilteredChores(chores)
|
||||
setSearchTerm('')
|
||||
}}
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
>
|
||||
|
@ -429,19 +540,92 @@ const MyChores = () => {
|
|||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{filteredChores.map(chore => (
|
||||
<ChoreCard
|
||||
key={chore.id}
|
||||
chore={chore}
|
||||
onChoreUpdate={handleChoreUpdated}
|
||||
onChoreRemove={handleChoreDeleted}
|
||||
performers={performers}
|
||||
userLabels={userLabels}
|
||||
onChipClick={handleLabelFiltering}
|
||||
/>
|
||||
))}
|
||||
|
||||
{(searchTerm?.length > 0 || selectedFilter !== 'All') &&
|
||||
filteredChores.map(chore => (
|
||||
<ChoreCard
|
||||
key={`filtered-${chore.id} `}
|
||||
chore={chore}
|
||||
onChoreUpdate={handleChoreUpdated}
|
||||
onChoreRemove={handleChoreDeleted}
|
||||
performers={performers}
|
||||
userLabels={userLabels}
|
||||
onChipClick={handleLabelFiltering}
|
||||
/>
|
||||
))}
|
||||
{searchTerm.length === 0 && selectedFilter === 'All' && (
|
||||
<AccordionGroup transition='0.2s ease' disableDivider>
|
||||
{choreSections.map((section, index) => {
|
||||
if (section.content.length === 0) return null
|
||||
return (
|
||||
<Accordion
|
||||
title={section.name}
|
||||
key={section.name + index}
|
||||
sx={{
|
||||
my: 0,
|
||||
}}
|
||||
expanded={Boolean(openChoreSections[index])}
|
||||
>
|
||||
<Divider orientation='horizontal'>
|
||||
<Chip
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
size='md'
|
||||
onClick={() => {
|
||||
if (openChoreSections[index]) {
|
||||
const newOpenChoreSections = { ...openChoreSections }
|
||||
delete newOpenChoreSections[index]
|
||||
setOpenChoreSections(newOpenChoreSections)
|
||||
} else {
|
||||
setOpenChoreSections({
|
||||
...openChoreSections,
|
||||
[index]: true,
|
||||
})
|
||||
}
|
||||
}}
|
||||
endDecorator={
|
||||
openChoreSections[index] ? (
|
||||
<ExpandCircleDown
|
||||
color='primary'
|
||||
sx={{ transform: 'rotate(180deg)' }}
|
||||
/>
|
||||
) : (
|
||||
<ExpandCircleDown color='primary' />
|
||||
)
|
||||
}
|
||||
startDecorator={
|
||||
<>
|
||||
<Chip color='primary' size='sm' variant='soft'>
|
||||
{section?.content?.length}
|
||||
</Chip>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{section.name}
|
||||
</Chip>
|
||||
</Divider>
|
||||
<AccordionDetails
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
my: 0,
|
||||
}}
|
||||
>
|
||||
{section.content?.map(chore => (
|
||||
<ChoreCard
|
||||
key={chore.id}
|
||||
chore={chore}
|
||||
onChoreUpdate={handleChoreUpdated}
|
||||
onChoreRemove={handleChoreDeleted}
|
||||
performers={performers}
|
||||
userLabels={userLabels}
|
||||
onChipClick={handleLabelFiltering}
|
||||
/>
|
||||
))}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
})}
|
||||
</AccordionGroup>
|
||||
)}
|
||||
<Box
|
||||
// variant='outlined'
|
||||
sx={{
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
import { Box, Button, Card, Chip, Divider, Typography } from '@mui/joy'
|
||||
import { CopyAll } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Chip,
|
||||
Divider,
|
||||
IconButton,
|
||||
Input,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
|
@ -13,6 +23,7 @@ import TextModal from '../Modals/Inputs/TextModal'
|
|||
const APITokenSettings = () => {
|
||||
const [tokens, setTokens] = useState([])
|
||||
const [isGetTokenNameModalOpen, setIsGetTokenNameModalOpen] = useState(false)
|
||||
const [showTokenId, setShowTokenId] = useState(null)
|
||||
const { userProfile, setUserProfile } = useContext(UserContext)
|
||||
useEffect(() => {
|
||||
GetLongLiveTokens().then(resp => {
|
||||
|
@ -61,19 +72,21 @@ const APITokenSettings = () => {
|
|||
</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='primary'
|
||||
sx={{ mr: 1 }}
|
||||
onClick={() => {
|
||||
if (showTokenId === token.id) {
|
||||
setShowTokenId(null)
|
||||
return
|
||||
}
|
||||
|
||||
setShowTokenId(token.id)
|
||||
}}
|
||||
>
|
||||
{showTokenId === token?.id ? 'Hide' : 'Show'} Token
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant='outlined'
|
||||
|
@ -97,6 +110,28 @@ const APITokenSettings = () => {
|
|||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
{showTokenId === token?.id && (
|
||||
<Box>
|
||||
<Input
|
||||
value={token.token}
|
||||
sx={{ width: '100%', mt: 2 }}
|
||||
readOnly
|
||||
endDecorator={
|
||||
<IconButton
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(token.token)
|
||||
alert('Token copied to clipboard')
|
||||
setShowTokenId(null)
|
||||
}}
|
||||
>
|
||||
<CopyAll />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
|
||||
|
|
|
@ -1,85 +1,143 @@
|
|||
import { Button, Divider, Input, Option, Select, Typography } from '@mui/joy'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { useContext, useState } from 'react'
|
||||
import { UserContext } from '../../contexts/UserContext'
|
||||
import { GetUserProfile, UpdateUserDetails } from '../../utils/Fetcher'
|
||||
import { UpdateNotificationTarget } 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)
|
||||
const [notificationTarget, setNotificationTarget] = useState(
|
||||
userProfile?.notification_target
|
||||
? String(userProfile.notification_target.type)
|
||||
: '0',
|
||||
)
|
||||
|
||||
const [chatID, setChatID] = useState(
|
||||
userProfile?.notification_target?.target_id,
|
||||
)
|
||||
const [error, setError] = useState('')
|
||||
const SaveValidation = () => {
|
||||
switch (notificationTarget) {
|
||||
case '1':
|
||||
if (chatID === '') {
|
||||
setError('Chat ID is required')
|
||||
return false
|
||||
} else if (isNaN(chatID) || chatID === '0') {
|
||||
setError('Invalid Chat ID')
|
||||
return false
|
||||
}
|
||||
break
|
||||
case '2':
|
||||
if (chatID === '') {
|
||||
setError('User key is required')
|
||||
return false
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
setError('')
|
||||
return true
|
||||
}
|
||||
const handleSave = () => {
|
||||
if (!SaveValidation()) return
|
||||
|
||||
UpdateNotificationTarget({
|
||||
target: chatID,
|
||||
type: Number(notificationTarget),
|
||||
}).then(resp => {
|
||||
alert('Notification target updated')
|
||||
setUserProfile({
|
||||
...userProfile,
|
||||
notification_target: {
|
||||
target: chatID,
|
||||
type: Number(notificationTarget),
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
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
|
||||
value={notificationTarget}
|
||||
sx={{ maxWidth: '200px' }}
|
||||
onChange={(e, selected) => setNotificationTarget(selected)}
|
||||
>
|
||||
<Option value='0'>None</Option>
|
||||
<Option value='1'>Telegram</Option>
|
||||
<Option value='2'>Pushover</Option>
|
||||
</Select>
|
||||
{notificationTarget === '1' && (
|
||||
<>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<Typography level='body-sm'>Chat ID</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>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{notificationTarget === '2' && (
|
||||
<>
|
||||
<Typography level='body-sm'>User key</Typography>
|
||||
<Input
|
||||
value={chatID}
|
||||
onChange={e => setChatID(e.target.value)}
|
||||
placeholder='User ID'
|
||||
sx={{
|
||||
width: '200px',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{error && (
|
||||
<Typography color='warning' level='body-sm'>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Button
|
||||
sx={{
|
||||
width: '110px',
|
||||
mb: 1,
|
||||
}}
|
||||
onClick={() => {
|
||||
UpdateUserDetails({
|
||||
chatID: Number(chatID),
|
||||
}).then(resp => {
|
||||
resp.json().then(data => {
|
||||
setUserProfile(data)
|
||||
})
|
||||
})
|
||||
}}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
|
Loading…
Add table
Reference in a new issue