Skip to content
This repository has been archived by the owner on Aug 13, 2024. It is now read-only.

Commit

Permalink
Smart sumbit timer
Browse files Browse the repository at this point in the history
  • Loading branch information
mariomc committed Aug 29, 2021
1 parent 31cf94f commit 19fb8d4
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 46 deletions.
146 changes: 105 additions & 41 deletions src/content/components/smart-submit.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,120 @@
import React, { useEffect, useState, useCallback } from 'react'
import React, { useEffect, useState, useCallback, useRef } from 'react'
import { useContextSelector } from 'use-context-selector'
import Tooltip from '@material-ui/core/Tooltip'
import Badge from '@material-ui/core/Badge'

import Fab from '@material-ui/core/Fab'
import Icon from '@material-ui/core/Icon'
import Snackbar from '@material-ui/core/Snackbar'
import Alert from '@material-ui/core/Alert'

import { AppContext } from '../state'
import { selectors } from '../config'
import { getWaitingTime } from '../utils'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const expiresSelector = (state: any): number | null => {
return state?.review?.reviewData?.data?.expires
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isValidSelector = (state: any): number | null => {
return state?.review?.reviewResponse?.isValid
}

const timeFormat = {
minute: 'numeric',
second: 'numeric',
}

const timeFromMs = (ms: number) => {
return new Intl.DateTimeFormat('default', {
minute: 'numeric',
second: 'numeric',
}).format(new Date(ms))
return new Intl.DateTimeFormat('default', timeFormat).format(new Date(ms))
}

const Timer = ({ expires }: { expires: number }): JSX.Element => {
const [delta, setDelta] = useState(expires - new Date().valueOf())
const getDelta = (expires: number) => expires - new Date().valueOf()

const style = {
position: 'absolute',
right: 16,
}

const Countdown = ({
initialCount,
invalidValue = '',
}: {
initialCount: number
invalidValue?: string
}): JSX.Element => {
const [counter, setCounter] = useState(initialCount)
const STEP = 1000

useEffect(() => {
const cb = () => {
setDelta(expires - new Date().valueOf())
}
const interval = setInterval(cb, 1000)
const timeout = setTimeout(() => {
const nextStep = counter - STEP
setCounter(Math.max(0, nextStep))
}, STEP)

return () => {
clearInterval(interval)
clearTimeout(timeout)
}
}, [expires])
})

if (initialCount <= 0) {
return invalidValue
}

return <>{timeFromMs(counter)}</>
}

const Timer = ({ expires }: { expires: number }): JSX.Element => {
return <Countdown initialCount={getDelta(expires)} invalidValue="Expired" />
}

const formattedDelta = delta > 0 ? timeFromMs(delta) : `Expired`
const clickOnFirstEditOption = false // TODO: Change this into the preset. Here to test only

return <>{formattedDelta}</>
const clickOnSubmit = (callback) => {
const submitButton = document.querySelector(
selectors.smartSubmit.submit,
) as HTMLButtonElement

if (submitButton) {
if (clickOnFirstEditOption) {
const option = document.querySelector(
selectors.smartSubmit.what,
) as HTMLButtonElement
option?.click?.()
}
submitButton?.click?.()
callback(true)
}
}

export const SmartSubmit = (): JSX.Element | null => {
const expires = useContextSelector(AppContext, expiresSelector)
const isValid = useContextSelector(AppContext, isValidSelector)
const timerRef = useRef(null)
const [submitted, setSubmitted] = useState(false)
const [waitingTime, setWaiting] = useState(0)

const handleClick = useCallback(() => {
const submitButton = document.querySelector(
selectors.smartSubmit.submit,
) as HTMLButtonElement

if (submitButton) {
if (submitButton.disabled) {
const option = document.querySelector(
selectors.smartSubmit.what,
) as HTMLButtonElement
option?.click?.()
}
submitButton?.click?.()
setSubmitted(true)
const waitingMs = getWaitingTime(expires)

setWaiting(waitingMs)

timerRef.current = setTimeout(() => {
clickOnSubmit(setSubmitted)
setWaiting(0)
}, waitingMs)
}, [expires])

useEffect(() => {
timerRef.current = null
setSubmitted(false)
setWaiting(0)
clearTimeout(timerRef.current)
return () => {
clearTimeout(timerRef.current)
timerRef.current = null
}
}, [expires])

Expand All @@ -65,19 +123,25 @@ export const SmartSubmit = (): JSX.Element | null => {
}

return (
<Tooltip title="Smart Submit">
<Fab
color="primary"
onClick={handleClick}
sx={{
position: 'absolute',
right: 16,
}}
<>
<Snackbar
open={waitingTime > 0}
sx={{ bottom: 60 }}
autoHideDuration={waitingTime}
>
<Badge color="secondary" badgeContent={<Timer expires={expires} />}>
<Icon>send</Icon>
</Badge>
</Fab>
</Tooltip>
<Alert severity="info">
Submitting in: <Countdown initialCount={waitingTime} />
</Alert>
</Snackbar>
<Tooltip title="Smart Submit">
<span style={style}>
<Fab color="primary" disabled={!isValid} onClick={handleClick}>
<Badge color="secondary" badgeContent={<Timer expires={expires} />}>
<Icon>send</Icon>
</Badge>
</Fab>
</span>
</Tooltip>
</>
)
}
48 changes: 46 additions & 2 deletions src/content/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,55 @@
import { getScore } from './utils'
import {
getScore,
getWaitingTime,
MIN_WAIT_TIME,
MAX_REVIEW_TIME,
} from './utils'

test('getScore should be defined', () => {
expect(getScore).toBeDefined()
})

test('getScore should be defined', () => {
test('getScore result should be defined', () => {
expect(
getScore({ name: 'test', score: { location: 2 } }, 'location'),
).toBeDefined()
})

test('getWaitingTime should be defined', () => {
expect(getWaitingTime).toBeDefined()
})

// test('getWaitingTime result should be defined', () => {
// expect(
// getWaitingTime(1000),
// ).toBeDefined()
// })

test.each([
{ currentTime: 0, targetTime: MAX_REVIEW_TIME, expected: MIN_WAIT_TIME }, // Before safe zone 1
{
currentTime: 100,
targetTime: MAX_REVIEW_TIME,
expected: MIN_WAIT_TIME - 100,
}, // Before safe zone 2
{
currentTime: 1000,
targetTime: MAX_REVIEW_TIME,
expected: MIN_WAIT_TIME - 1000,
}, // Before safe zone 3
{
currentTime: MIN_WAIT_TIME,
targetTime: MAX_REVIEW_TIME,
expected: 0,
}, // Inside safe zone 1
{
currentTime: MIN_WAIT_TIME + 1000,
targetTime: MAX_REVIEW_TIME,
expected: 0,
}, // Inside safe zone 2
])(
'getWaitingTime($currentTime, $targetTime)',
({ currentTime, targetTime, expected }) => {
expect(getWaitingTime(targetTime, currentTime, 0)).toBe(expected)
},
)
35 changes: 32 additions & 3 deletions src/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,36 @@ export const applyPreset = (presetConfig: PresetConfig): void => {
})
}

export const getFilledInValue = (key:PresetScoreKey):number => {
const filledInStars = document.querySelectorAll(`${selectors.presets[key]}${selectors.presets.selected}`)
return filledInStars.length;
export const getFilledInValue = (key: PresetScoreKey): number => {
const filledInStars = document.querySelectorAll(
`${selectors.presets[key]}${selectors.presets.selected}`,
)
return filledInStars.length
}

export const MIN_WAIT_TIME = 1_000 * 20 // 20 Seconds
export const VARIANCE_TIME = 1_000 * 10 // 10 Seconds
export const MAX_REVIEW_TIME = 1_000 * 60 * 20 // 20 minutes

const randomInteger = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min
}

export const getWaitingTime = (
targetTime: number,
currentTime: number = new Date().valueOf(),
randomVariance: number = randomInteger(0, VARIANCE_TIME),
minWaitTime: number = MIN_WAIT_TIME,
maxReviewTime: number = MAX_REVIEW_TIME,
): number => {
const initialTime = targetTime - maxReviewTime
const safeLowestTime = initialTime + minWaitTime + randomVariance
const deltaToSafety = safeLowestTime - currentTime

if (deltaToSafety <= 0) {
// After the safe window. No need to wait
return 0
}

return deltaToSafety
}

0 comments on commit 19fb8d4

Please sign in to comment.