aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar Mo Tarbin <mhed.t91@gmail.com>2024-07-01 22:12:19 -0400
committerLibravatar Mo Tarbin <mhed.t91@gmail.com>2024-07-01 22:12:19 -0400
commit5e54da8271c7fac5062f0aca9b10e7370fd4bae2 (patch)
tree376e53b60c9639d9db80cbcd809212253f29f0d0 /src
parent6bc28be9e351662025a3eb0662f32902e64eb143 (diff)
downloaddonetick-frontend-5e54da8271c7fac5062f0aca9b10e7370fd4bae2.tar.gz
donetick-frontend-5e54da8271c7fac5062f0aca9b10e7370fd4bae2.tar.bz2
donetick-frontend-5e54da8271c7fac5062f0aca9b10e7370fd4bae2.zip
Update button re-enable timeout to 3 seconds, update thing history
Diffstat (limited to 'src')
-rw-r--r--src/utils/Fetcher.jsx8
-rw-r--r--src/views/ChoreEdit/ChoreEdit.jsx2
-rw-r--r--src/views/ChoreEdit/ThingTriggerSection.jsx3
-rw-r--r--src/views/Chores/ChoreCard.jsx2
-rw-r--r--src/views/Modals/Inputs/CreateThingModal.jsx151
-rw-r--r--src/views/Things/ThingsHistory.jsx164
-rw-r--r--src/views/Things/ThingsView.jsx87
7 files changed, 341 insertions, 76 deletions
diff --git a/src/utils/Fetcher.jsx b/src/utils/Fetcher.jsx
index 6444b34..be3971c 100644
--- a/src/utils/Fetcher.jsx
+++ b/src/utils/Fetcher.jsx
@@ -197,6 +197,13 @@ const DeleteThing = id => {
})
}
+const GetThingHistory = (id, offset) => {
+ return Fetch(`${API_URL}/things/${id}/history?offset=${offset}`, {
+ method: 'GET',
+ headers: HEADERS(),
+ })
+}
+
const CreateLongLiveToken = name => {
return Fetch(`${API_URL}/users/tokens`, {
method: 'POST',
@@ -236,6 +243,7 @@ export {
GetCircleMemberRequests,
GetLongLiveTokens,
GetSubscriptionSession,
+ GetThingHistory,
GetThings,
GetUserCircle,
GetUserProfile,
diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx
index e8eb17d..568b20a 100644
--- a/src/views/ChoreEdit/ChoreEdit.jsx
+++ b/src/views/ChoreEdit/ChoreEdit.jsx
@@ -506,7 +506,7 @@ const ChoreEdit = () => {
</FormControl>
)}
</Box>
- {!['once', 'no_repeat'].includes(frequencyType) && (
+ {!['once', 'no_repeat', 'trigger'].includes(frequencyType) && (
<Box mt={2}>
<Typography level='h4'>Scheduling Preferences: </Typography>
<Typography level='h5'>
diff --git a/src/views/ChoreEdit/ThingTriggerSection.jsx b/src/views/ChoreEdit/ThingTriggerSection.jsx
index 7a040ad..981f84b 100644
--- a/src/views/ChoreEdit/ThingTriggerSection.jsx
+++ b/src/views/ChoreEdit/ThingTriggerSection.jsx
@@ -6,7 +6,6 @@ import {
Card,
Chip,
FormControl,
- FormLabel,
Input,
ListItem,
ListItemContent,
@@ -91,7 +90,7 @@ const ThingTriggerSection = ({
<Typography level='h5'>
Trigger a task when a thing state changes to a desired state
</Typography>
- {things.length !== 0 && (
+ {things?.length === 0 && (
<Typography level='body-sm'>
it's look like you don't have any things yet, create a thing to
trigger a task when the state changes.
diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx
index 0b2a408..8efaf04 100644
--- a/src/views/Chores/ChoreCard.jsx
+++ b/src/views/Chores/ChoreCard.jsx
@@ -125,7 +125,7 @@ const ChoreCard = ({ chore, performers, onChoreUpdate, onChoreRemove, sx }) => {
}
})
setIsDisabled(true)
- setTimeout(() => setIsDisabled(false), 5000) // Re-enable the button after 5 seconds
+ setTimeout(() => setIsDisabled(false), 3000) // Re-enable the button after 5 seconds
}
const handleChangeDueDate = newDate => {
if (activeUserId === null) {
diff --git a/src/views/Modals/Inputs/CreateThingModal.jsx b/src/views/Modals/Inputs/CreateThingModal.jsx
index 59263ff..ffce51c 100644
--- a/src/views/Modals/Inputs/CreateThingModal.jsx
+++ b/src/views/Modals/Inputs/CreateThingModal.jsx
@@ -1,6 +1,8 @@
import {
Box,
Button,
+ FormControl,
+ FormHelperText,
FormLabel,
Input,
Modal,
@@ -14,8 +16,9 @@ import { useEffect, useState } from 'react'
function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
const [name, setName] = useState(currentThing?.name || '')
- const [type, setType] = useState(currentThing?.type || 'numeric')
+ const [type, setType] = useState(currentThing?.type || 'number')
const [state, setState] = useState(currentThing?.state || '')
+ const [errors, setErrors] = useState({})
useEffect(() => {
if (type === 'boolean') {
if (state !== 'true' && state !== 'false') {
@@ -27,7 +30,31 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
}
}
}, [type])
+
+ const isValid = () => {
+ const newErrors = {}
+ if (!name || name.trim() === '') {
+ newErrors.name = 'Name is required'
+ }
+
+ if (type === 'number' && isNaN(state)) {
+ newErrors.state = 'State must be a number'
+ }
+ if (type === 'boolean' && !['true', 'false'].includes(state)) {
+ newErrors.state = 'State must be true or false'
+ }
+ if ((type === 'text' && !state) || state.trim() === '') {
+ newErrors.state = 'State is required'
+ }
+
+ setErrors(newErrors)
+ return Object.keys(newErrors).length === 0
+ }
+
const handleSave = () => {
+ if (!isValid()) {
+ return
+ }
onSave({ name, type, id: currentThing?.id, state: state || null })
onClose()
}
@@ -36,64 +63,81 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
<Modal open={isOpen} onClose={onClose}>
<ModalDialog>
{/* <ModalClose /> */}
- <Typography variant='h4'>P;lease add info</Typography>
- <FormLabel>Name</FormLabel>
-
- <Textarea
- placeholder='Thing name'
- value={name}
- onChange={e => setName(e.target.value)}
- sx={{ minWidth: 300 }}
- />
- <FormLabel>Type</FormLabel>
- <Select value={type} sx={{ minWidth: 300 }}>
- {['text', 'number', 'boolean'].map(type => (
- <Option value={type} key={type} onClick={() => setType(type)}>
- {type.charAt(0).toUpperCase() + type.slice(1)}
- </Option>
- ))}
- </Select>
-
- {type === 'text' && (
- <>
- <FormLabel>Value</FormLabel>
- <Input
- placeholder='Thing value'
- value={state || ''}
- onChange={e => setState(e.target.value)}
+ <Typography level='h4'>
+ {currentThing?.id ? 'Edit' : 'Create'} Thing
+ </Typography>
+ <FormControl>
+ <FormLabel>
+ Name
+ <Textarea
+ placeholder='Thing name'
+ value={name}
+ onChange={e => setName(e.target.value)}
sx={{ minWidth: 300 }}
/>
- </>
+ </FormLabel>
+ <FormHelperText color='danger'>{errors.name}</FormHelperText>
+ </FormControl>
+ <FormControl>
+ <FormLabel>
+ Type
+ <Select value={type} sx={{ minWidth: 300 }}>
+ {['text', 'number', 'boolean'].map(type => (
+ <Option value={type} key={type} onClick={() => setType(type)}>
+ {type.charAt(0).toUpperCase() + type.slice(1)}
+ </Option>
+ ))}
+ </Select>
+ </FormLabel>
+ <FormHelperText color='danger'>{errors.type}</FormHelperText>
+ </FormControl>
+ {type === 'text' && (
+ <FormControl>
+ <FormLabel>
+ Value
+ <Input
+ placeholder='Thing value'
+ value={state || ''}
+ onChange={e => setState(e.target.value)}
+ sx={{ minWidth: 300 }}
+ />
+ </FormLabel>
+ <FormHelperText color='danger'>{errors.state}</FormHelperText>
+ </FormControl>
)}
{type === 'number' && (
- <>
- <FormLabel>Value</FormLabel>
- <Input
- placeholder='Thing value'
- type='number'
- value={state || ''}
- onChange={e => {
- setState(e.target.value)
- }}
- sx={{ minWidth: 300 }}
- />
- </>
+ <FormControl>
+ <FormLabel>
+ Value
+ <Input
+ placeholder='Thing value'
+ type='number'
+ value={state || ''}
+ onChange={e => {
+ setState(e.target.value)
+ }}
+ sx={{ minWidth: 300 }}
+ />
+ </FormLabel>
+ </FormControl>
)}
{type === 'boolean' && (
- <>
- <FormLabel>Value</FormLabel>
- <Select sx={{ minWidth: 300 }} value={state}>
- {['true', 'false'].map(value => (
- <Option
- value={value}
- key={value}
- onClick={() => setState(value)}
- >
- {value.charAt(0).toUpperCase() + value.slice(1)}
- </Option>
- ))}
- </Select>
- </>
+ <FormControl>
+ <FormLabel>
+ Value
+ <Select sx={{ minWidth: 300 }} value={state}>
+ {['true', 'false'].map(value => (
+ <Option
+ value={value}
+ key={value}
+ onClick={() => setState(value)}
+ >
+ {value.charAt(0).toUpperCase() + value.slice(1)}
+ </Option>
+ ))}
+ </Select>
+ </FormLabel>
+ </FormControl>
)}
<Box display={'flex'} justifyContent={'space-around'} mt={1}>
@@ -108,5 +152,4 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
</Modal>
)
}
-
export default CreateThingModal
diff --git a/src/views/Things/ThingsHistory.jsx b/src/views/Things/ThingsHistory.jsx
index 39f0e30..4b32b0e 100644
--- a/src/views/Things/ThingsHistory.jsx
+++ b/src/views/Things/ThingsHistory.jsx
@@ -1,11 +1,171 @@
-import { Container, Typography } from '@mui/joy'
+import { EventBusy } from '@mui/icons-material'
+import {
+ Box,
+ Button,
+ Chip,
+ Container,
+ List,
+ ListDivider,
+ ListItem,
+ ListItemContent,
+ Typography,
+} from '@mui/joy'
+import moment from 'moment'
+import { useEffect, useState } from 'react'
+import { Link, useParams } from 'react-router-dom'
+import { GetThingHistory } from '../../utils/Fetcher'
const ThingsHistory = () => {
+ const { id } = useParams()
+ const [thingsHistory, setThingsHistory] = useState([])
+ const [noMoreHistory, setNoMoreHistory] = useState(false)
+ const [errLoading, setErrLoading] = useState(false)
+ useEffect(() => {
+ GetThingHistory(id, 0, 10).then(resp => {
+ if (resp.ok) {
+ resp.json().then(data => {
+ setThingsHistory(data.res)
+ if (data.res.length < 10) {
+ setNoMoreHistory(true)
+ }
+ })
+ } else {
+ setErrLoading(true)
+ }
+ })
+ }, [])
+
+ const handleLoadMore = () => {
+ GetThingHistory(id, thingsHistory.length).then(resp => {
+ if (resp.ok) {
+ resp.json().then(data => {
+ setThingsHistory([...thingsHistory, ...data.res])
+ if (data.res.length < 10) {
+ setNoMoreHistory(true)
+ }
+ })
+ }
+ })
+ }
+
+ const formatTimeDifference = (startDate, endDate) => {
+ const diffInMinutes = moment(startDate).diff(endDate, 'minutes')
+ let timeValue = diffInMinutes
+ let unit = 'minute'
+
+ if (diffInMinutes >= 60) {
+ const diffInHours = moment(startDate).diff(endDate, 'hours')
+ timeValue = diffInHours
+ unit = 'hour'
+
+ if (diffInHours >= 24) {
+ const diffInDays = moment(startDate).diff(endDate, 'days')
+ timeValue = diffInDays
+ unit = 'day'
+ }
+ }
+
+ return `${timeValue} ${unit}${timeValue !== 1 ? 's' : ''}`
+ }
+ if (errLoading || !thingsHistory) {
+ return (
+ <Container
+ maxWidth='md'
+ sx={{
+ textAlign: 'center',
+ display: 'flex',
+ // make sure the content is centered vertically:
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexDirection: 'column',
+ height: '50vh',
+ }}
+ >
+ <EventBusy
+ sx={{
+ fontSize: '6rem',
+ // color: 'text.disabled',
+ mb: 1,
+ }}
+ />
+
+ <Typography level='h3' gutterBottom>
+ No history found
+ </Typography>
+ <Typography level='body1'>
+ It's look like there is no history for this thing yet.
+ </Typography>
+ <Button variant='soft' sx={{ mt: 2 }}>
+ <Link to='/things'>Go back to things</Link>
+ </Button>
+ </Container>
+ )
+ }
+
return (
<Container maxWidth='md'>
<Typography level='h3' mb={1.5}>
- Summary:
+ History:
</Typography>
+ <List sx={{ p: 0 }}>
+ {thingsHistory.map((history, index) => (
+ <>
+ <ListItem sx={{ gap: 1.5, alignItems: 'flex-start' }}>
+ <ListItemContent sx={{ my: 0 }}>
+ <Box
+ sx={{
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ }}
+ >
+ <Typography level='body1' sx={{ fontWeight: 'md' }}>
+ {moment(history.updatedAt).format(
+ 'ddd MM/DD/yyyy HH:mm:ss',
+ )}
+ </Typography>
+ <Chip>{history.state === '1' ? 'Active' : 'Inactive'}</Chip>
+ </Box>
+ </ListItemContent>
+ </ListItem>
+ {index < thingsHistory.length - 1 && (
+ <>
+ <ListDivider component='li'>
+ {/* time between two completion: */}
+ {index < thingsHistory.length - 1 &&
+ thingsHistory[index + 1].createdAt && (
+ <Typography level='body3' color='text.tertiary'>
+ {formatTimeDifference(
+ history.createdAt,
+ thingsHistory[index + 1].createdAt,
+ )}{' '}
+ before
+ </Typography>
+ )}
+ </ListDivider>
+ </>
+ )}
+ </>
+ ))}
+ </List>
+ {/* Load more Button */}
+ <Box
+ sx={{
+ display: 'flex',
+ justifyContent: 'center',
+ mt: 2,
+ }}
+ >
+ <Button
+ variant='plain'
+ fullWidth
+ color='primary'
+ onClick={handleLoadMore}
+ disabled={noMoreHistory}
+ >
+ {noMoreHistory ? 'No more history' : 'Load more'}
+ </Button>
+ </Box>
</Container>
)
}
diff --git a/src/views/Things/ThingsView.jsx b/src/views/Things/ThingsView.jsx
index deb2df5..8b2beb6 100644
--- a/src/views/Things/ThingsView.jsx
+++ b/src/views/Things/ThingsView.jsx
@@ -12,12 +12,15 @@ import {
Box,
Card,
Chip,
+ CircularProgress,
Container,
Grid,
IconButton,
+ Snackbar,
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
import {
CreateThing,
DeleteThing,
@@ -27,13 +30,14 @@ import {
} from '../../utils/Fetcher'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import CreateThingModal from '../Modals/Inputs/CreateThingModal'
-
const ThingCard = ({
thing,
onEditClick,
onStateChangeRequest,
onDeleteClick,
}) => {
+ const [isDisabled, setIsDisabled] = useState(false)
+ const Navigate = useNavigate()
const getThingIcon = type => {
if (type === 'text') {
return <Flip />
@@ -49,6 +53,15 @@ const ThingCard = ({
return <ToggleOff />
}
}
+
+ const handleRequestChange = thing => {
+ setIsDisabled(true)
+ onStateChangeRequest(thing)
+ setTimeout(() => {
+ setIsDisabled(false)
+ }, 2000)
+ }
+
return (
<Card
variant='outlined'
@@ -71,6 +84,9 @@ const ThingCard = ({
flexDirection: 'row',
gap: 1,
}}
+ onClick={() => {
+ Navigate(`/things/${thing?.id}`)
+ }}
>
<Typography level='title-lg' component='h2'>
{thing?.name}
@@ -91,21 +107,39 @@ const ThingCard = ({
<Grid item xs={3}>
<Box display='flex' justifyContent='flex-end' alignItems='flex-end'>
{/* <ButtonGroup> */}
- <IconButton
- variant='solid'
- color='success'
- onClick={() => {
- onStateChangeRequest(thing)
- }}
- sx={{
- borderRadius: '50%',
- width: 50,
- height: 50,
- zIndex: 1,
- }}
- >
- {getThingIcon(thing?.type)}
- </IconButton>
+ <div className='relative grid place-items-center'>
+ <IconButton
+ variant='solid'
+ color='success'
+ onClick={() => {
+ handleRequestChange(thing)
+ }}
+ sx={{
+ borderRadius: '50%',
+ width: 50,
+ minWidth: 50,
+ height: 50,
+ zIndex: 1,
+ }}
+ disabled={isDisabled}
+ >
+ {getThingIcon(thing?.type)}
+ </IconButton>
+ {isDisabled && (
+ <CircularProgress
+ variant='solid'
+ color='success'
+ size='md'
+ sx={{
+ color: 'success.main',
+ position: 'absolute',
+ '--CircularProgress-size': '55px',
+
+ zIndex: 0,
+ }}
+ />
+ )}
+ </div>
<IconButton
// sx={{ width: 15 }}
variant='soft'
@@ -154,6 +188,10 @@ const ThingsView = () => {
const [isShowCreateThingModal, setIsShowCreateThingModal] = useState(false)
const [createModalThing, setCreateModalThing] = useState(null)
const [confirmModelConfig, setConfirmModelConfig] = useState({})
+
+ const [isSnackbarOpen, setIsSnackbarOpen] = useState(false)
+ const [snackBarMessage, setSnackBarMessage] = useState('')
+
useEffect(() => {
// fetch things
GetThings().then(result => {
@@ -184,6 +222,8 @@ const ThingsView = () => {
}
})
})
+ setSnackBarMessage('Thing saved successfully')
+ setIsSnackbarOpen(true)
}
const handleEditClick = thing => {
setCreateModalThing(thing)
@@ -240,6 +280,8 @@ const ThingsView = () => {
})
})
}
+ setSnackBarMessage('Thing state updated successfully')
+ setIsSnackbarOpen(true)
}
return (
@@ -317,6 +359,19 @@ const ThingsView = () => {
)}
<ConfirmationModal config={confirmModelConfig} />
</Box>
+ <Snackbar
+ open={isSnackbarOpen}
+ onClose={() => {
+ setIsSnackbarOpen(false)
+ }}
+ autoHideDuration={3000}
+ variant='soft'
+ color='success'
+ size='lg'
+ invertedColors
+ >
+ <Typography level='title-md'>{snackBarMessage}</Typography>
+ </Snackbar>
</Container>
)
}