Update button re-enable timeout to 3 seconds, update thing history

This commit is contained in:
Mo Tarbin 2024-07-01 22:12:19 -04:00
parent 6bc28be9e3
commit 5e54da8271
9 changed files with 1004 additions and 92 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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 }}
/>
</>
)}
{type === 'number' && (
<>
<FormLabel>Value</FormLabel>
<Input
placeholder='Thing value'
type='number'
value={state || ''}
onChange={e => {
setState(e.target.value)
}}
sx={{ minWidth: 300 }}
/>
</>
)}
{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)}
</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' && (
<FormControl>
<FormLabel>
Value
<Input
placeholder='Thing value'
type='number'
value={state || ''}
onChange={e => {
setState(e.target.value)
}}
sx={{ minWidth: 300 }}
/>
</FormLabel>
</FormControl>
)}
{type === 'boolean' && (
<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

View file

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

View file

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