Add pomodoro timer
@@ -15,6 +15,7 @@
|
|||||||
"@mdi/js": "^6.5.95",
|
"@mdi/js": "^6.5.95",
|
||||||
"@mdi/react": "^1.5.0",
|
"@mdi/react": "^1.5.0",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.5",
|
||||||
|
"classcat": "^5.0.4",
|
||||||
"date-fns": "^2.27.0",
|
"date-fns": "^2.27.0",
|
||||||
"html-to-image": "^1.10.6",
|
"html-to-image": "^1.10.6",
|
||||||
"next": "^12.1.6",
|
"next": "^12.1.6",
|
||||||
@@ -2022,6 +2023,11 @@
|
|||||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
|
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/classcat": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g=="
|
||||||
|
},
|
||||||
"node_modules/classnames": {
|
"node_modules/classnames": {
|
||||||
"version": "2.2.6",
|
"version": "2.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
||||||
@@ -7235,6 +7241,11 @@
|
|||||||
"domutils": "^2.8.0"
|
"domutils": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"classcat": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g=="
|
||||||
|
},
|
||||||
"classnames": {
|
"classnames": {
|
||||||
"version": "2.2.6",
|
"version": "2.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"@mdi/js": "^6.5.95",
|
"@mdi/js": "^6.5.95",
|
||||||
"@mdi/react": "^1.5.0",
|
"@mdi/react": "^1.5.0",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.5",
|
||||||
|
"classcat": "^5.0.4",
|
||||||
"date-fns": "^2.27.0",
|
"date-fns": "^2.27.0",
|
||||||
"html-to-image": "^1.10.6",
|
"html-to-image": "^1.10.6",
|
||||||
"next": "^12.1.6",
|
"next": "^12.1.6",
|
||||||
|
|||||||
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 222 KiB |
@@ -6,6 +6,7 @@ import Head from '../../../components/atoms/Head'
|
|||||||
import Honkai3rdLayout from '../../../components/layouts/Honkai3rdLayout'
|
import Honkai3rdLayout from '../../../components/layouts/Honkai3rdLayout'
|
||||||
import { getI18NProps } from 'server/i18n'
|
import { getI18NProps } from 'server/i18n'
|
||||||
import { NextPageContext } from 'next'
|
import { NextPageContext } from 'next'
|
||||||
|
import NextLink from 'next/link'
|
||||||
|
|
||||||
const MediaPage = () => {
|
const MediaPage = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -27,42 +28,51 @@ const MediaPage = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Heading as='h1'>{t('common.media')}</Heading>
|
<Heading as='h1'>{t('common.media')}</Heading>
|
||||||
<Heading as='h2'>{t('common.novels')}</Heading>
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Heading as='h2'>{t('common.novels')}</Heading>
|
||||||
|
|
||||||
<Box sx={{ mb: 3, pl: 2 }} as='ul'>
|
<Box sx={{ mb: 3, pl: 2 }} as='ul'>
|
||||||
<Box as='li'>
|
<Box as='li'>
|
||||||
<Link href={'/honkai3rd/novels/ae'} target='_blank'>
|
<Link href={'/honkai3rd/novels/ae'} target='_blank'>
|
||||||
🇺🇸 Anti-Entropy Beta
|
🇺🇸 Anti-Entropy Beta
|
||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
<Box as='li'>
|
<Box as='li'>
|
||||||
<Link href={'/honkai3rd/novels/duriduri'} target='_blank'>
|
<Link href={'/honkai3rd/novels/duriduri'} target='_blank'>
|
||||||
🇺🇸 Durandal Beta
|
🇺🇸 Durandal Beta
|
||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
<Box as='li'>
|
<Box as='li'>
|
||||||
<Link href={'/honkai3rd/novels/7s'} target='_blank'>
|
<Link href={'/honkai3rd/novels/7s'} target='_blank'>
|
||||||
🇺🇸 Seven Swords (WIP)
|
🇺🇸 Seven Swords (WIP)
|
||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
<Box as='li'>
|
<Box as='li'>
|
||||||
<Link href={'/ko-KR/honkai3rd/novels/ae'} target='_blank'>
|
<Link href={'/ko-KR/honkai3rd/novels/ae'} target='_blank'>
|
||||||
🇰🇷 네겐트로피 Beta
|
🇰🇷 네겐트로피 Beta
|
||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
<Box as='li'>
|
<Box as='li'>
|
||||||
<Link href={'/ko-KR/honkai3rd/novels/duriduri'} target='_blank'>
|
<Link href={'/ko-KR/honkai3rd/novels/duriduri'} target='_blank'>
|
||||||
🇰🇷 듀란달 Beta(작업중)
|
🇰🇷 듀란달 Beta(작업중)
|
||||||
</Link>
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
<Box as='li'>
|
<Box as='li'>
|
||||||
<Link href={'/ko-KR/honkai3rd/novels/7s'} target='_blank'>
|
<Link href={'/ko-KR/honkai3rd/novels/7s'} target='_blank'>
|
||||||
🇰🇷 신주절검록 (작업중)
|
🇰🇷 신주절검록 (작업중)
|
||||||
</Link>
|
</Link>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Paragraph>Other novels will be available one day.</Paragraph>
|
<Heading as='h2'>Abyss Lab Custom</Heading>
|
||||||
|
<Box sx={{ mb: 3, pl: 2 }} as='ul'>
|
||||||
|
<Box as='li'>
|
||||||
|
<NextLink href='/honkai3rd/media/pomodoro' passHref>
|
||||||
|
<Link>Pomodoro Timer</Link>
|
||||||
|
</NextLink>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Honkai3rdLayout>
|
</Honkai3rdLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Box, ThemeProvider } from 'theme-ui'
|
||||||
|
import PomodoroAppMain from '../../../pomodoro/PomodoroAppMain'
|
||||||
|
import { loadTimerEvents } from '../../../pomodoro/data'
|
||||||
|
import { TimerEvent } from '../../../pomodoro/interfaces'
|
||||||
|
import { theme } from '../../../pomodoro/theme'
|
||||||
|
|
||||||
|
const PomodoroAppPage = () => {
|
||||||
|
const [initialized, setInitialized] = useState(false)
|
||||||
|
const [initialTimerEvents, setInitialTimerEvents] = useState<TimerEvent[]>([])
|
||||||
|
useEffect(() => {
|
||||||
|
setInitialTimerEvents(loadTimerEvents())
|
||||||
|
setInitialized(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
return <Box>Loading</Box>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<PomodoroAppMain initialTimerEvents={initialTimerEvents} />
|
||||||
|
</ThemeProvider>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PomodoroAppPage
|
||||||
@@ -0,0 +1,838 @@
|
|||||||
|
import classcat from 'classcat'
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
// Checkbox,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Link,
|
||||||
|
Slider,
|
||||||
|
Text,
|
||||||
|
} from 'theme-ui'
|
||||||
|
import {
|
||||||
|
defaultPomodoroSettings,
|
||||||
|
loadPomodoroSettings,
|
||||||
|
savePomodoroSettings,
|
||||||
|
saveTimerEvents,
|
||||||
|
} from './data'
|
||||||
|
import {
|
||||||
|
PauseTimerEvent,
|
||||||
|
PomodoroSettings,
|
||||||
|
ResumeTimerEvent,
|
||||||
|
StartTimerEvent,
|
||||||
|
TimerEvent,
|
||||||
|
TimerEventType,
|
||||||
|
TimerType,
|
||||||
|
} from './interfaces'
|
||||||
|
// import JSONDebugBox from './JSONDebugBox'
|
||||||
|
import PomodoroTimerTypeButtonGroup from './PomodoroTimerTypeButtonGroup'
|
||||||
|
import { createMemoFunction } from './utils'
|
||||||
|
import { useValueRef } from './hooks'
|
||||||
|
import ProgressCircle from './ProgressCircle'
|
||||||
|
import RemainingTimeBox from './RemainingTimeBox'
|
||||||
|
import PomodoroStatusBox from './PomodoroStatusBox'
|
||||||
|
import PomodoroImage from './PomodoroImage'
|
||||||
|
|
||||||
|
interface PomodoroAppMainProps {
|
||||||
|
initialTimerEvents: TimerEvent[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const PomodoroAppMain = ({ initialTimerEvents }: PomodoroAppMainProps) => {
|
||||||
|
const [events, setEvents] = useState<TimerEvent[]>(initialTimerEvents)
|
||||||
|
const initialRenderingRef = useRef(false)
|
||||||
|
const [timerType, setTimerType] = useState<TimerType>('pomodoro')
|
||||||
|
const [settings, setSettings] = useState<PomodoroSettings>(() => {
|
||||||
|
console.log(loadPomodoroSettings())
|
||||||
|
return loadPomodoroSettings()
|
||||||
|
})
|
||||||
|
const settingsRef = useRef<PomodoroSettings>(settings)
|
||||||
|
useEffect(() => {
|
||||||
|
settingsRef.current = settings
|
||||||
|
savePomodoroSettings(settings)
|
||||||
|
}, [settings])
|
||||||
|
|
||||||
|
const eventsRef = useRef<TimerEvent[]>(events)
|
||||||
|
const longBreakIntervalRef = useValueRef(settings.longBreakInterval)
|
||||||
|
useMemo(() => {
|
||||||
|
eventsRef.current = events
|
||||||
|
}, [events])
|
||||||
|
const alarmAudioRef = useRef<HTMLAudioElement>(null)
|
||||||
|
const tickingAudioRef = useRef<HTMLAudioElement>(null)
|
||||||
|
const rideonAudioRef = useRef<HTMLAudioElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (alarmAudioRef.current != null) {
|
||||||
|
alarmAudioRef.current.volume = settings.volume
|
||||||
|
}
|
||||||
|
if (tickingAudioRef.current != null) {
|
||||||
|
tickingAudioRef.current.volume = settings.volume
|
||||||
|
}
|
||||||
|
if (rideonAudioRef.current != null) {
|
||||||
|
rideonAudioRef.current.volume = settings.volume
|
||||||
|
}
|
||||||
|
}, [settings.volume])
|
||||||
|
|
||||||
|
const playTickingSound = useCallback(() => {
|
||||||
|
if (tickingAudioRef.current == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tickingAudioRef.current.currentTime = 0
|
||||||
|
tickingAudioRef.current.loop = true
|
||||||
|
tickingAudioRef.current.play()
|
||||||
|
}, [])
|
||||||
|
const stopTickingSound = useCallback(() => {
|
||||||
|
if (tickingAudioRef.current == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tickingAudioRef.current.pause()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const unshiftTimerEvent = useCallback((event: TimerEvent) => {
|
||||||
|
setEvents((previousEvents) => {
|
||||||
|
return [event, ...previousEvents]
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ((initialRenderingRef.current = false)) {
|
||||||
|
initialRenderingRef.current = true
|
||||||
|
} else {
|
||||||
|
console.log('save timer events')
|
||||||
|
saveTimerEvents(events)
|
||||||
|
}
|
||||||
|
}, [events])
|
||||||
|
|
||||||
|
const lastEvent = events[0]
|
||||||
|
|
||||||
|
const [
|
||||||
|
finishedPomodoroCountSinceLongBreak,
|
||||||
|
setFinishedPomodoroCountSinceLongBreak,
|
||||||
|
] = useState(() => {
|
||||||
|
return getFinishedPomodoroCountSinceLongBreak(events)
|
||||||
|
})
|
||||||
|
|
||||||
|
const [displayStatus, setDisplayStatus] = useState(() => {
|
||||||
|
const { status } = getTimerStatus(events)
|
||||||
|
return status
|
||||||
|
})
|
||||||
|
const [displayRemainingTime, setDisplayRemainingTime] = useState(() => {
|
||||||
|
const { remainingTime } = getTimerStatus(events)
|
||||||
|
return remainingTime
|
||||||
|
})
|
||||||
|
const [displayDuration, setDisplayDuration] = useState(() => {
|
||||||
|
const { lastTimerDuration } = getTimerStatus(events)
|
||||||
|
return lastTimerDuration
|
||||||
|
})
|
||||||
|
const [finishedCount, setFinishedCount] = useState(() => {
|
||||||
|
return getFinishedPomodoroCount(events)
|
||||||
|
})
|
||||||
|
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const refreshTimerIntervalRef = useRef<NodeJS.Timer | null>(null)
|
||||||
|
const startRefresh = useCallback(() => {
|
||||||
|
if (refreshTimerIntervalRef.current != null) {
|
||||||
|
clearInterval(refreshTimerIntervalRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshTimerIntervalRef.current = setInterval(() => {
|
||||||
|
const { status, remainingTime, lastTimerDuration } = getTimerStatus(
|
||||||
|
eventsRef.current
|
||||||
|
)
|
||||||
|
if (status !== 'idle') {
|
||||||
|
setDisplayRemainingTime(remainingTime)
|
||||||
|
setDisplayDuration(lastTimerDuration)
|
||||||
|
}
|
||||||
|
}, 1000 / 60)
|
||||||
|
}, [])
|
||||||
|
const endRefresh = useCallback(() => {
|
||||||
|
if (refreshTimerIntervalRef.current != null) {
|
||||||
|
clearInterval(refreshTimerIntervalRef.current)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
const startTimer = useCallback((callback: () => void, ms: number) => {
|
||||||
|
if (timeoutRef.current != null) {
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
}
|
||||||
|
startRefresh()
|
||||||
|
timeoutRef.current = setTimeout(callback, ms)
|
||||||
|
}, [])
|
||||||
|
const endTimer = useCallback(() => {
|
||||||
|
if (timeoutRef.current != null) {
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
}
|
||||||
|
endRefresh()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
endRefresh()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const finish = useCallback((silentAlarm: boolean = false) => {
|
||||||
|
endTimer()
|
||||||
|
const { lastTimerType } = getTimerStatus(eventsRef.current)
|
||||||
|
const finishedPomodoroCountSinceLongBreak =
|
||||||
|
getFinishedPomodoroCountSinceLongBreak(eventsRef.current)
|
||||||
|
const finishedCount = getFinishedPomodoroCount(eventsRef.current)
|
||||||
|
if (!silentAlarm) {
|
||||||
|
if (lastTimerType === 'pomodoro' && alarmAudioRef.current != null) {
|
||||||
|
alarmAudioRef.current.currentTime = 0
|
||||||
|
alarmAudioRef.current.play()
|
||||||
|
} else if (rideonAudioRef.current != null) {
|
||||||
|
rideonAudioRef.current.currentTime = 0
|
||||||
|
rideonAudioRef.current.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopTickingSound()
|
||||||
|
const nextBreakShouldBeLongBreak =
|
||||||
|
finishedPomodoroCountSinceLongBreak >= longBreakIntervalRef.current - 1
|
||||||
|
const nextTimerType =
|
||||||
|
lastTimerType === 'pomodoro'
|
||||||
|
? nextBreakShouldBeLongBreak
|
||||||
|
? 'long-break'
|
||||||
|
: 'short-break'
|
||||||
|
: 'pomodoro'
|
||||||
|
unshiftTimerEvent(createTimerEvent('finish'))
|
||||||
|
if (lastTimerType === 'pomodoro') {
|
||||||
|
setFinishedPomodoroCountSinceLongBreak(
|
||||||
|
nextBreakShouldBeLongBreak ? 0 : finishedPomodoroCountSinceLongBreak + 1
|
||||||
|
)
|
||||||
|
setFinishedCount(finishedCount + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextTimerDuration = getTimerDuration(
|
||||||
|
nextTimerType,
|
||||||
|
settingsRef.current
|
||||||
|
)
|
||||||
|
setDisplayDuration(nextTimerDuration)
|
||||||
|
setTimerType(nextTimerType)
|
||||||
|
setDisplayRemainingTime(nextTimerDuration * 1000)
|
||||||
|
setDisplayStatus('idle')
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { status, remainingTime } = getTimerStatus(events)
|
||||||
|
if (status === 'running') {
|
||||||
|
setTimeout(finish, remainingTime < 0 ? 0 : remainingTime)
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
endTimer()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onStartResumeButtonClick = useCallback(() => {
|
||||||
|
if (alarmAudioRef.current != null) {
|
||||||
|
alarmAudioRef.current.pause()
|
||||||
|
}
|
||||||
|
const { status, remainingTime } = getTimerStatus(events)
|
||||||
|
switch (status) {
|
||||||
|
case 'running': // pause
|
||||||
|
stopTickingSound()
|
||||||
|
if (remainingTime > 0) {
|
||||||
|
setDisplayStatus('paused')
|
||||||
|
unshiftTimerEvent(createTimerEvent('pause'))
|
||||||
|
endTimer()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'paused': // pause -> running
|
||||||
|
if (timerType === 'pomodoro') {
|
||||||
|
playTickingSound()
|
||||||
|
}
|
||||||
|
setDisplayStatus('running')
|
||||||
|
unshiftTimerEvent(createTimerEvent('resume'))
|
||||||
|
startTimer(finish, remainingTime)
|
||||||
|
break
|
||||||
|
case 'idle': // idle -> running
|
||||||
|
default:
|
||||||
|
if (timerType === 'pomodoro') {
|
||||||
|
playTickingSound()
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisplayStatus('running')
|
||||||
|
const newEvent = createStartTimerEvent({
|
||||||
|
duration: getTimerDuration(timerType, settings),
|
||||||
|
timerType,
|
||||||
|
})
|
||||||
|
unshiftTimerEvent(newEvent)
|
||||||
|
startTimer(finish, getTimerDuration(timerType, settings) * 1000)
|
||||||
|
}
|
||||||
|
}, [timerType, events, settings])
|
||||||
|
|
||||||
|
const onTimerButtonSelect = useCallback(
|
||||||
|
(selectedTimerType: TimerType) => {
|
||||||
|
const { status } = getTimerStatus(events)
|
||||||
|
|
||||||
|
if (status === 'idle') {
|
||||||
|
setTimerType(selectedTimerType)
|
||||||
|
setDisplayRemainingTime(
|
||||||
|
getTimerDuration(selectedTimerType, settings) * 1000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'running' || status === 'paused') {
|
||||||
|
// should ask to finish or to cancel
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[events, settings]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onFinishButtonClick = useCallback(() => {
|
||||||
|
const { status } = getTimerStatus(events)
|
||||||
|
if (status === 'idle') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
finish(true)
|
||||||
|
}, [events])
|
||||||
|
|
||||||
|
const onCancelButtonClick = useCallback(() => {
|
||||||
|
const { status } = getTimerStatus(events)
|
||||||
|
if (status === 'idle') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stopTickingSound()
|
||||||
|
endTimer()
|
||||||
|
unshiftTimerEvent(createTimerEvent('cancel'))
|
||||||
|
setDisplayRemainingTime(getTimerDuration(timerType, settings) * 1000)
|
||||||
|
setDisplayStatus('idle')
|
||||||
|
}, [events])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: '100vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'absolute',
|
||||||
|
width: '100%',
|
||||||
|
color: 'white',
|
||||||
|
bg:
|
||||||
|
timerType === 'pomodoro'
|
||||||
|
? 'red.6'
|
||||||
|
: timerType === 'short-break'
|
||||||
|
? 'green.9'
|
||||||
|
: 'blue.9',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
p: 0,
|
||||||
|
m: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 5,
|
||||||
|
p: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex sx={{ justifyContent: 'center' }}>
|
||||||
|
<PomodoroTimerTypeButtonGroup
|
||||||
|
timerType={timerType}
|
||||||
|
onTimerButtonSelect={onTimerButtonSelect}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
flexDirection: ['column', 'row'],
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex sx={{ justifyContent: 'center', mt: 4, mb: 2 }}>
|
||||||
|
<Box sx={{ width: 150, height: 150, position: 'relative' }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 150,
|
||||||
|
height: 150,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProgressCircle
|
||||||
|
size={150}
|
||||||
|
progress={
|
||||||
|
(displayDuration - displayRemainingTime / 1000) /
|
||||||
|
displayDuration
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<PomodoroImage
|
||||||
|
displayStatus={displayStatus}
|
||||||
|
timerType={timerType}
|
||||||
|
finishedCountSinceLongBreak={
|
||||||
|
finishedPomodoroCountSinceLongBreak
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
mx: 3,
|
||||||
|
mt: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RemainingTimeBox displayRemainingTime={displayRemainingTime} />
|
||||||
|
<PomodoroStatusBox
|
||||||
|
finishedCount={finishedCount}
|
||||||
|
timerType={timerType}
|
||||||
|
finishedCountSinceLongBreak={finishedPomodoroCountSinceLongBreak}
|
||||||
|
longBreakInterval={settings.longBreakInterval}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
variant='pomodoro'
|
||||||
|
sx={{ mr: 1, width: [100, 150, 200] }}
|
||||||
|
className={classcat([
|
||||||
|
lastEvent != null && lastEvent.type === 'start' && 'active',
|
||||||
|
])}
|
||||||
|
onClick={onStartResumeButtonClick}
|
||||||
|
>
|
||||||
|
{lastEvent != null && lastEvent.type === 'start'
|
||||||
|
? 'Pause'
|
||||||
|
: lastEvent != null && lastEvent.type === 'pause'
|
||||||
|
? 'Resume'
|
||||||
|
: 'Start'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='pomodoro'
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
onClick={onFinishButtonClick}
|
||||||
|
>
|
||||||
|
Finish
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='pomodoro'
|
||||||
|
onClick={onCancelButtonClick}
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Box sx={{ maxWidth: '460px', margin: '0 auto' }}>
|
||||||
|
<Box sx={{ mt: '60px', mb: 3 }}>
|
||||||
|
<Box sx={{ width: '100%', background: 'white', height: 1 }}></Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Heading>Settings</Heading>
|
||||||
|
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Box>Pomodoro Count: {finishedCount}</Box>
|
||||||
|
<Button
|
||||||
|
variant='pomodoro'
|
||||||
|
onClick={() => {
|
||||||
|
setEvents((prevEvents) => {
|
||||||
|
setFinishedPomodoroCountSinceLongBreak(0)
|
||||||
|
setFinishedCount(0)
|
||||||
|
return shiftFinishedEvents(prevEvents)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset Count
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ mb: 4 }}>
|
||||||
|
{/* <Label>Sound</Label> */}
|
||||||
|
{/* <Label>
|
||||||
|
<Checkbox
|
||||||
|
checked={settings.tickSound}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSettings((prevSettings) => {
|
||||||
|
return {
|
||||||
|
...prevSettings,
|
||||||
|
tickSound: event.target.checked,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
Tick Sound
|
||||||
|
</Label>
|
||||||
|
<Label>
|
||||||
|
<Checkbox
|
||||||
|
checked={settings.pomodoroFinishSound}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSettings((prevSettings) => {
|
||||||
|
return {
|
||||||
|
...prevSettings,
|
||||||
|
pomodoroFinishSound: event.target.checked,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
Alarm Sound when Pomodoro End
|
||||||
|
</Label> */}
|
||||||
|
{/* <Label>
|
||||||
|
<Checkbox
|
||||||
|
checked={settings.breakFinishSound}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSettings((prevSettings) => {
|
||||||
|
return {
|
||||||
|
...prevSettings,
|
||||||
|
breakFinishSound: event.target.checked,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
Rideon Sound when Break End
|
||||||
|
</Label> */}
|
||||||
|
<Box>
|
||||||
|
<Label>Sound Volumn</Label>
|
||||||
|
<Slider
|
||||||
|
value={settings.volume * 100}
|
||||||
|
max={100}
|
||||||
|
min={0}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSettings((prevSettings) => {
|
||||||
|
return {
|
||||||
|
...prevSettings,
|
||||||
|
volume: parseInt(event.target.value, 10) / 100,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{/* <Box sx={{ mb: 3 }}>
|
||||||
|
<Label>Auto Start</Label>
|
||||||
|
<Label>
|
||||||
|
<Checkbox
|
||||||
|
checked={settings.autoStartPomodoros}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSettings((prevSettings) => {
|
||||||
|
return {
|
||||||
|
...prevSettings,
|
||||||
|
autoStartPomodoros: event.target.checked,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
Auto Start Pomodoros
|
||||||
|
</Label>
|
||||||
|
<Label>
|
||||||
|
<Checkbox
|
||||||
|
checked={settings.autoStartBreaks}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSettings((prevSettings) => {
|
||||||
|
return {
|
||||||
|
...prevSettings,
|
||||||
|
autoStartBreaks: event.target.checked,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
Auto-start Breaks
|
||||||
|
</Label>
|
||||||
|
</Box> */}
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Label>Duration</Label>
|
||||||
|
<Label>Pomodoro Duration(seconds)</Label>
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
min={1}
|
||||||
|
value={settings.pomodoroDuration}
|
||||||
|
onChange={(event) => {
|
||||||
|
let newValue = parseInt(event.target.value, 10)
|
||||||
|
setSettings((prevSettings) => {
|
||||||
|
return {
|
||||||
|
...prevSettings,
|
||||||
|
pomodoroDuration: isNaN(newValue) ? 1 : newValue,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label>Short Break Duration(seconds)</Label>
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
min={1}
|
||||||
|
value={settings.shortBreakDuration}
|
||||||
|
onChange={(event) => {
|
||||||
|
let newValue = parseInt(event.target.value, 10)
|
||||||
|
setSettings((prevSettings) => {
|
||||||
|
return {
|
||||||
|
...prevSettings,
|
||||||
|
shortBreakDuration: isNaN(newValue) ? 1 : newValue,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label>Long Break Duration(seconds)</Label>
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
min={1}
|
||||||
|
value={settings.longBreakDuration}
|
||||||
|
onChange={(event) => {
|
||||||
|
let newValue = parseInt(event.target.value, 10)
|
||||||
|
setSettings((prevSettings) => {
|
||||||
|
return {
|
||||||
|
...prevSettings,
|
||||||
|
longBreakDuration: isNaN(newValue) ? 1 : newValue,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label>Long Break Interval</Label>
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
value={settings.longBreakInterval}
|
||||||
|
onChange={(event) => {
|
||||||
|
let newValue = parseInt(event.target.value, 10)
|
||||||
|
setSettings((prevSettings) => {
|
||||||
|
return {
|
||||||
|
...prevSettings,
|
||||||
|
longBreakInterval: isNaN(newValue) ? 0 : newValue,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<Label>Reset Settings</Label>
|
||||||
|
<Button
|
||||||
|
variant='pomodoro'
|
||||||
|
onClick={() => {
|
||||||
|
setSettings(defaultPomodoroSettings)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Box mb={4}>
|
||||||
|
<Label>Made by Sakura Knoll</Label>
|
||||||
|
<Label>
|
||||||
|
<Text sx={{ mr: 2 }}>Feedback:</Text>
|
||||||
|
<Link
|
||||||
|
href='https://github.com/sakura-knoll/abyss-lab/issues'
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
|
Github
|
||||||
|
</Link>
|
||||||
|
,
|
||||||
|
<Link
|
||||||
|
href='https://arca.live/b/cherryhill'
|
||||||
|
target='_blank'
|
||||||
|
sx={{ ml: 2 }}
|
||||||
|
>
|
||||||
|
arca.live
|
||||||
|
</Link>
|
||||||
|
</Label>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<audio src='/pomodoro/warning.ogg' ref={alarmAudioRef} />
|
||||||
|
<audio src='/pomodoro/ticking.wav' ref={tickingAudioRef} />
|
||||||
|
<audio src='/pomodoro/rideon.ogg' ref={rideonAudioRef} />
|
||||||
|
</Box>
|
||||||
|
{/* <JSONDebugBox data={events} /> */}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PomodoroAppMain
|
||||||
|
|
||||||
|
function createStartTimerEvent(
|
||||||
|
eventData: Omit<StartTimerEvent, 'type' | 'date'>
|
||||||
|
): StartTimerEvent {
|
||||||
|
return {
|
||||||
|
type: 'start',
|
||||||
|
...eventData,
|
||||||
|
date: new Date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTimerEvent(type: Exclude<TimerEventType, 'start'>): TimerEvent {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
date: new Date(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TimerStatus = 'idle' | 'running' | 'paused'
|
||||||
|
interface TimerStats {
|
||||||
|
status: TimerStatus
|
||||||
|
lastTimerType: TimerType
|
||||||
|
remainingTime: number
|
||||||
|
lastTimerDuration: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimerStatus(events: TimerEvent[]): TimerStats {
|
||||||
|
const unresolvedTimerEvents = getUnresolvedTimerEvents(events)
|
||||||
|
|
||||||
|
if (unresolvedTimerEvents.length === 0) {
|
||||||
|
return {
|
||||||
|
status: 'idle',
|
||||||
|
lastTimerType: 'pomodoro',
|
||||||
|
remainingTime: 0,
|
||||||
|
lastTimerDuration: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
lastEventType,
|
||||||
|
lastTimerType,
|
||||||
|
calculatedRemainingTimeInMs,
|
||||||
|
previousResumeDate,
|
||||||
|
lastTimerDuration,
|
||||||
|
} = getUnresolvedTimerStatus(unresolvedTimerEvents)
|
||||||
|
|
||||||
|
if (lastEventType === 'pause') {
|
||||||
|
return {
|
||||||
|
status: 'paused',
|
||||||
|
lastTimerType,
|
||||||
|
remainingTime: calculatedRemainingTimeInMs,
|
||||||
|
lastTimerDuration,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
status: 'running',
|
||||||
|
lastTimerType,
|
||||||
|
remainingTime:
|
||||||
|
calculatedRemainingTimeInMs -
|
||||||
|
(new Date().getTime() - previousResumeDate.getTime()),
|
||||||
|
lastTimerDuration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnresolvedTimerEvent = StartTimerEvent | PauseTimerEvent | ResumeTimerEvent
|
||||||
|
const getUnresolvedTimerEvents = createMemoFunction(function (
|
||||||
|
events: TimerEvent[]
|
||||||
|
): UnresolvedTimerEvent[] {
|
||||||
|
let unresolvedTimerEvents = []
|
||||||
|
|
||||||
|
if (events.length > 0) {
|
||||||
|
for (let targetIndex = 0; targetIndex < events.length; targetIndex++) {
|
||||||
|
const targetEvent = events[targetIndex]
|
||||||
|
if (targetEvent.type === 'cancel' || targetEvent.type === 'finish') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
unresolvedTimerEvents.unshift(targetEvent)
|
||||||
|
if (targetEvent.type === 'start') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unresolvedTimerEvents
|
||||||
|
})
|
||||||
|
|
||||||
|
interface UnresolvedTimerStatus {
|
||||||
|
lastEventType: 'pause' | 'resume' | 'start'
|
||||||
|
lastTimerType: TimerType
|
||||||
|
calculatedRemainingTimeInMs: number
|
||||||
|
previousResumeDate: Date
|
||||||
|
lastTimerDuration: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUnresolvedTimerStatus = createMemoFunction(function (
|
||||||
|
unresolvedTimerEvents: UnresolvedTimerEvent[]
|
||||||
|
): UnresolvedTimerStatus {
|
||||||
|
unresolvedTimerEvents = unresolvedTimerEvents.slice()
|
||||||
|
let startEvent = unresolvedTimerEvents.shift() as StartTimerEvent
|
||||||
|
let calculatedRemainingTimeInMs = startEvent.duration * 1000
|
||||||
|
|
||||||
|
let previousResumeDate = startEvent.date
|
||||||
|
let lastEventType: 'start' | 'pause' | 'resume' = startEvent.type
|
||||||
|
|
||||||
|
while (unresolvedTimerEvents.length > 0) {
|
||||||
|
const targetEvent = unresolvedTimerEvents.shift()
|
||||||
|
if (targetEvent == null) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lastEventType = targetEvent.type
|
||||||
|
if (targetEvent.type === 'pause') {
|
||||||
|
calculatedRemainingTimeInMs -=
|
||||||
|
targetEvent.date.getTime() - previousResumeDate.getTime()
|
||||||
|
}
|
||||||
|
if (targetEvent.type === 'resume') {
|
||||||
|
previousResumeDate = targetEvent.date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lastEventType,
|
||||||
|
lastTimerType: startEvent.timerType,
|
||||||
|
calculatedRemainingTimeInMs,
|
||||||
|
previousResumeDate,
|
||||||
|
lastTimerDuration: startEvent.duration,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function getFinishedPomodoroCountSinceLongBreak(events: TimerEvent[]): number {
|
||||||
|
let finishedPomodoroCount = 0
|
||||||
|
let starting = false
|
||||||
|
for (const event of events.slice()) {
|
||||||
|
if (!starting) {
|
||||||
|
if (event.type === 'finish') {
|
||||||
|
starting = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (event.type === 'start') {
|
||||||
|
starting = false
|
||||||
|
if (event.timerType === 'pomodoro') {
|
||||||
|
finishedPomodoroCount++
|
||||||
|
}
|
||||||
|
if (event.timerType === 'long-break') {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return finishedPomodoroCount
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFinishedPomodoroCount(events: TimerEvent[]): number {
|
||||||
|
let finishedPomodoroCount = 0
|
||||||
|
let starting = false
|
||||||
|
for (const event of events.slice()) {
|
||||||
|
if (!starting) {
|
||||||
|
if (event.type === 'finish') {
|
||||||
|
starting = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (event.type === 'start') {
|
||||||
|
starting = false
|
||||||
|
if (event.timerType === 'pomodoro') {
|
||||||
|
finishedPomodoroCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return finishedPomodoroCount
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimerDuration(timerType: TimerType, settings: PomodoroSettings) {
|
||||||
|
switch (timerType) {
|
||||||
|
case 'long-break':
|
||||||
|
return settings.longBreakDuration
|
||||||
|
case 'short-break':
|
||||||
|
return settings.shortBreakDuration
|
||||||
|
case 'pomodoro':
|
||||||
|
default:
|
||||||
|
return settings.pomodoroDuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shiftFinishedEvents(events: TimerEvent[]): TimerEvent[] {
|
||||||
|
let newEvents = []
|
||||||
|
for (const event of events) {
|
||||||
|
if (event.type === 'finish') {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
newEvents.unshift(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newEvents
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { Image } from 'theme-ui'
|
||||||
|
import { TimerType } from './interfaces'
|
||||||
|
import { TimerStatus } from './PomodoroAppMain'
|
||||||
|
|
||||||
|
const images: {
|
||||||
|
[key: string]: { size: number; src: string; offsetY?: number }
|
||||||
|
} = {
|
||||||
|
arrive: {
|
||||||
|
size: 100,
|
||||||
|
src: '/pomodoro/arrive.gif',
|
||||||
|
offsetY: -10,
|
||||||
|
},
|
||||||
|
ride: {
|
||||||
|
size: 100,
|
||||||
|
src: '/pomodoro/ride.gif',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
size: 76,
|
||||||
|
src: '/pomodoro/code.gif',
|
||||||
|
},
|
||||||
|
hop: {
|
||||||
|
size: 60,
|
||||||
|
src: '/pomodoro/hop.gif',
|
||||||
|
},
|
||||||
|
attack: {
|
||||||
|
size: 100,
|
||||||
|
src: '/pomodoro/attack.gif',
|
||||||
|
},
|
||||||
|
drink: {
|
||||||
|
size: 68,
|
||||||
|
src: '/pomodoro/drink.gif',
|
||||||
|
},
|
||||||
|
rest: {
|
||||||
|
size: 100,
|
||||||
|
src: '/pomodoro/rest.gif',
|
||||||
|
},
|
||||||
|
love: {
|
||||||
|
size: 60,
|
||||||
|
src: '/pomodoro/love.gif',
|
||||||
|
offsetY: 4,
|
||||||
|
},
|
||||||
|
sleep: {
|
||||||
|
size: 85,
|
||||||
|
src: '/pomodoro/sleep.gif',
|
||||||
|
offsetY: -5,
|
||||||
|
},
|
||||||
|
no: {
|
||||||
|
size: 80,
|
||||||
|
src: '/pomodoro/no.gif',
|
||||||
|
offsetY: -5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PomodoroImageProps {
|
||||||
|
displayStatus: TimerStatus
|
||||||
|
timerType: TimerType
|
||||||
|
finishedCountSinceLongBreak: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const PomodoroImage = ({
|
||||||
|
displayStatus,
|
||||||
|
timerType,
|
||||||
|
finishedCountSinceLongBreak,
|
||||||
|
}: PomodoroImageProps) => {
|
||||||
|
const imageKey = getImageKey(
|
||||||
|
displayStatus,
|
||||||
|
timerType,
|
||||||
|
finishedCountSinceLongBreak
|
||||||
|
)
|
||||||
|
const { size, src, offsetY = 0 } = images[imageKey]
|
||||||
|
const offset = (150 - size) / 2
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
sx={{
|
||||||
|
borderRadius: 10,
|
||||||
|
position: 'absolute',
|
||||||
|
left: offset,
|
||||||
|
top: offset + offsetY,
|
||||||
|
}}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
src={src}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PomodoroImage
|
||||||
|
|
||||||
|
function getImageKey(
|
||||||
|
displayStatus: TimerStatus,
|
||||||
|
timerType: TimerType,
|
||||||
|
finishedCountSinceLongBreak: number
|
||||||
|
) {
|
||||||
|
return displayStatus === 'paused'
|
||||||
|
? 'no'
|
||||||
|
: timerType === 'pomodoro'
|
||||||
|
? displayStatus === 'idle'
|
||||||
|
? 'arrive'
|
||||||
|
: finishedCountSinceLongBreak === 0
|
||||||
|
? 'ride'
|
||||||
|
: finishedCountSinceLongBreak === 1
|
||||||
|
? 'code'
|
||||||
|
: finishedCountSinceLongBreak === 2
|
||||||
|
? 'hop'
|
||||||
|
: 'attack'
|
||||||
|
: timerType === 'short-break'
|
||||||
|
? finishedCountSinceLongBreak === 1
|
||||||
|
? 'rest'
|
||||||
|
: finishedCountSinceLongBreak === 2
|
||||||
|
? 'drink'
|
||||||
|
: 'love'
|
||||||
|
: 'sleep'
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import { Text } from 'theme-ui'
|
||||||
|
import { TimerType } from './interfaces'
|
||||||
|
|
||||||
|
interface PomodoroStatusBoxProps {
|
||||||
|
finishedCount: number
|
||||||
|
finishedCountSinceLongBreak: number
|
||||||
|
longBreakInterval: number
|
||||||
|
timerType: TimerType
|
||||||
|
}
|
||||||
|
|
||||||
|
const PomodoroStatusBox = ({
|
||||||
|
finishedCount,
|
||||||
|
finishedCountSinceLongBreak,
|
||||||
|
longBreakInterval,
|
||||||
|
timerType,
|
||||||
|
}: PomodoroStatusBoxProps) => {
|
||||||
|
const nextBreakShouldBeLongBreak =
|
||||||
|
finishedCountSinceLongBreak >= longBreakInterval - 1
|
||||||
|
const nextTimerType =
|
||||||
|
timerType === 'pomodoro'
|
||||||
|
? nextBreakShouldBeLongBreak
|
||||||
|
? 'long-break'
|
||||||
|
: 'short-break'
|
||||||
|
: 'pomodoro'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text sx={{ textAlign: 'center', display: 'block' }}>
|
||||||
|
{finishedCount > 0 ? `🍅 Done: ${finishedCount}` : `Let's start!`}
|
||||||
|
<br />
|
||||||
|
|
||||||
|
{nextTimerType === 'pomodoro' ? (
|
||||||
|
<>Next: Pomodoro</>
|
||||||
|
) : nextTimerType === 'short-break' ? (
|
||||||
|
<>
|
||||||
|
Next: Short Break({finishedCountSinceLongBreak}/
|
||||||
|
{longBreakInterval - 1})
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>Next: Long Break</>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(PomodoroStatusBox)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import classcat from 'classcat'
|
||||||
|
import { memo } from 'react'
|
||||||
|
import { Button, Flex } from 'theme-ui'
|
||||||
|
import { TimerType } from './interfaces'
|
||||||
|
|
||||||
|
interface PomodoroTimerTypeButtonGroupProps {
|
||||||
|
timerType: TimerType
|
||||||
|
onTimerButtonSelect: (timerType: TimerType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PomodoroTimerTypeButtonGroup = ({
|
||||||
|
timerType,
|
||||||
|
onTimerButtonSelect,
|
||||||
|
}: PomodoroTimerTypeButtonGroupProps) => {
|
||||||
|
return (
|
||||||
|
<Flex sx={{ justifyContent: 'center', maxWidth: 400, width: '100%' }}>
|
||||||
|
<Button
|
||||||
|
variant='pomodoro'
|
||||||
|
sx={{ mr: 1, flex: 1 }}
|
||||||
|
className={classcat([timerType === 'pomodoro' && 'active'])}
|
||||||
|
onClick={() => onTimerButtonSelect('pomodoro')}
|
||||||
|
>
|
||||||
|
Pomodoro
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='pomodoro'
|
||||||
|
sx={{ mr: 1, flex: 1 }}
|
||||||
|
className={classcat([timerType === 'short-break' && 'active'])}
|
||||||
|
onClick={() => onTimerButtonSelect('short-break')}
|
||||||
|
>
|
||||||
|
Short Break
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
variant='pomodoro'
|
||||||
|
className={classcat([timerType === 'long-break' && 'active'])}
|
||||||
|
onClick={() => onTimerButtonSelect('long-break')}
|
||||||
|
>
|
||||||
|
Long Break
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(PomodoroTimerTypeButtonGroup)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
interface ProgressCircleProps {
|
||||||
|
size: number
|
||||||
|
progress: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProgressCircle = ({ size, progress }: ProgressCircleProps) => {
|
||||||
|
return (
|
||||||
|
<svg viewBox={`0 0 ${size} ${size}`} xmlns='http://www.w3.org/2000/svg'>
|
||||||
|
<path
|
||||||
|
fillRule='evenodd'
|
||||||
|
fill='rgba(0,0,0,0.3)'
|
||||||
|
d={progressPath(1, size, 10)}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fillRule='evenodd'
|
||||||
|
fill='white'
|
||||||
|
d={progressPath(progress, size, 10)}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function progressPath(progress: number, width: number, strokeWidth: number) {
|
||||||
|
const cx = width / 2
|
||||||
|
const cy = width / 2
|
||||||
|
const radius = width / 2
|
||||||
|
const innerRadius = width / 2 - strokeWidth
|
||||||
|
if (progress >= 1) {
|
||||||
|
return `M ${cx} 0 A ${radius} ${radius} 0 0 1 ${cx} ${width} A ${radius} ${radius} 0 0 1 ${cx} 0 z M ${cx} ${strokeWidth} A ${innerRadius} ${innerRadius} 0 0 1 ${cx} ${
|
||||||
|
cy + innerRadius
|
||||||
|
} A ${innerRadius} ${innerRadius} 0 0 1 ${cx} ${strokeWidth} Z`
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetRadians = -Math.PI / 2
|
||||||
|
const endRadians = offsetRadians + progress * Math.PI * 2
|
||||||
|
const outerEndX = cx + (width / 2) * Math.cos(endRadians)
|
||||||
|
const outerEndY = cy + (width / 2) * Math.sin(endRadians)
|
||||||
|
const innerEndX = cx + (width / 2 - strokeWidth) * Math.cos(endRadians)
|
||||||
|
const innerEndY = cy + (width / 2 - strokeWidth) * Math.sin(endRadians)
|
||||||
|
const flip = progress > 0.5 ? 1 : 0
|
||||||
|
|
||||||
|
return `M ${cx} 0 A ${cx} ${cy} 0 ${flip} 1 ${outerEndX} ${outerEndY} L ${innerEndX} ${innerEndY} A ${innerRadius} ${innerRadius} 0 ${flip} 0 ${cx} ${strokeWidth}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProgressCircle
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { secondsToMinutes } from 'date-fns'
|
||||||
|
import { Text } from 'theme-ui'
|
||||||
|
|
||||||
|
interface RemainingTimeBoxProps {
|
||||||
|
displayRemainingTime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const RemainingTimeBox = ({ displayRemainingTime }: RemainingTimeBoxProps) => {
|
||||||
|
if (displayRemainingTime < 0) {
|
||||||
|
displayRemainingTime = 0
|
||||||
|
}
|
||||||
|
const durationInSecs = Math.ceil(displayRemainingTime / 1000)
|
||||||
|
const minutes = secondsToMinutes(durationInSecs)
|
||||||
|
const secs = durationInSecs % 60
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text sx={{ fontSize: 72, fontFamily: 'monospace', lineHeight: '1em' }}>
|
||||||
|
{prependZero(minutes, 2)}:{prependZero(secs, 2)}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RemainingTimeBox
|
||||||
|
|
||||||
|
function prependZero(value: number, digits: number) {
|
||||||
|
const fillCount = digits - value.toString().length
|
||||||
|
|
||||||
|
return `${'0'.repeat(fillCount)}${value}`
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import openColors from 'open-color'
|
||||||
|
|
||||||
|
export const colors = Object.entries(openColors)
|
||||||
|
.map(([colorName, colorObjectMap]) => {
|
||||||
|
return [colorName, Object.values(colorObjectMap) as string[]] as [
|
||||||
|
string,
|
||||||
|
string[]
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.reduce<{ [key: string]: string[] }>((map, [colorName, colorArray]) => {
|
||||||
|
map[colorName] = colorArray
|
||||||
|
return map
|
||||||
|
}, {})
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { PomodoroSettings, TimerEvent } from './interfaces'
|
||||||
|
|
||||||
|
const pomodoroTasksKey = 'pomodoro:tasks'
|
||||||
|
export function loadTimerEvents(): TimerEvent[] {
|
||||||
|
try {
|
||||||
|
const parsedData = JSON.parse(
|
||||||
|
localStorage.getItem(pomodoroTasksKey) || '[]'
|
||||||
|
)
|
||||||
|
|
||||||
|
return (parsedData as any[]).map(({ date, ...otherProps }) => {
|
||||||
|
const task: TimerEvent = {
|
||||||
|
...otherProps,
|
||||||
|
date: new Date(date),
|
||||||
|
}
|
||||||
|
return task
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveTimerEvents(timerEvents: TimerEvent[]) {
|
||||||
|
const serializedTasks = timerEvents.map(({ ...otherTaskProps }) => {
|
||||||
|
const serializedTask: any = {
|
||||||
|
...otherTaskProps,
|
||||||
|
date: otherTaskProps.date.toString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return serializedTask
|
||||||
|
})
|
||||||
|
localStorage.setItem(pomodoroTasksKey, JSON.stringify(serializedTasks))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultPomodoroSettings: PomodoroSettings = {
|
||||||
|
tickSound: true,
|
||||||
|
pomodoroFinishSound: true,
|
||||||
|
breakFinishSound: true,
|
||||||
|
volume: 1,
|
||||||
|
autoStartBreaks: false,
|
||||||
|
autoStartPomodoros: false,
|
||||||
|
pomodoroDuration: 25 * 60,
|
||||||
|
shortBreakDuration: 5 * 60,
|
||||||
|
longBreakDuration: 15 * 60,
|
||||||
|
longBreakInterval: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
const pomodoroSettingsKey = 'pomodoro:settings'
|
||||||
|
export function loadPomodoroSettings(): PomodoroSettings {
|
||||||
|
try {
|
||||||
|
const storedValue = localStorage.getItem(pomodoroSettingsKey)
|
||||||
|
if (storedValue == null) {
|
||||||
|
return defaultPomodoroSettings
|
||||||
|
}
|
||||||
|
const settings = JSON.parse(storedValue)
|
||||||
|
return settings
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
return defaultPomodoroSettings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function savePomodoroSettings(settings: PomodoroSettings) {
|
||||||
|
localStorage.setItem(pomodoroSettingsKey, JSON.stringify(settings))
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
MutableRefObject,
|
||||||
|
SetStateAction,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
|
export function useRefState<S>(
|
||||||
|
initialState: S | (() => S)
|
||||||
|
): [S, Dispatch<SetStateAction<S>>, MutableRefObject<S>] {
|
||||||
|
const [state, setState] = useState<S>(initialState)
|
||||||
|
const stateRef = useRef(state)
|
||||||
|
useEffect(() => {
|
||||||
|
stateRef.current = state
|
||||||
|
}, [state])
|
||||||
|
return [state, setState, stateRef]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTimer() {}
|
||||||
|
|
||||||
|
export function useValueRef<V>(value: V) {
|
||||||
|
const ref = useRef(value)
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
return ref
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
export type TimerType = 'pomodoro' | 'short-break' | 'long-break'
|
||||||
|
|
||||||
|
export type TimerEventType = 'start' | 'pause' | 'resume' | 'cancel' | 'finish'
|
||||||
|
|
||||||
|
export interface StartTimerEvent {
|
||||||
|
type: 'start'
|
||||||
|
date: Date
|
||||||
|
duration: number
|
||||||
|
timerType: TimerType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PauseTimerEvent {
|
||||||
|
type: 'pause'
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResumeTimerEvent {
|
||||||
|
type: 'resume'
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
|
export interface CancelTimerEvent {
|
||||||
|
type: 'cancel'
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FinishTimerEvent {
|
||||||
|
type: 'finish'
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TimerEvent =
|
||||||
|
| StartTimerEvent
|
||||||
|
| PauseTimerEvent
|
||||||
|
| ResumeTimerEvent
|
||||||
|
| CancelTimerEvent
|
||||||
|
| FinishTimerEvent
|
||||||
|
|
||||||
|
export interface PomodoroSettings {
|
||||||
|
tickSound: boolean
|
||||||
|
pomodoroFinishSound: boolean
|
||||||
|
breakFinishSound: boolean
|
||||||
|
volume: number
|
||||||
|
autoStartPomodoros: boolean
|
||||||
|
autoStartBreaks: boolean
|
||||||
|
pomodoroDuration: number
|
||||||
|
shortBreakDuration: number
|
||||||
|
longBreakDuration: number
|
||||||
|
longBreakInterval: number
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
import { Theme } from 'theme-ui'
|
||||||
|
import { colors } from './colors'
|
||||||
|
|
||||||
|
export const theme: Theme = {
|
||||||
|
breakpoints: ['576px', '768px', '992px', '1200px'],
|
||||||
|
colors: {
|
||||||
|
...colors,
|
||||||
|
white: '#fff',
|
||||||
|
black: '#000',
|
||||||
|
text: '#212529',
|
||||||
|
background: '#fff',
|
||||||
|
primary: '#007bff',
|
||||||
|
secondary: '#6c757d',
|
||||||
|
muted: '#dee2e6',
|
||||||
|
success: '#28a745',
|
||||||
|
info: '#17a2b8',
|
||||||
|
warning: '#ffc107',
|
||||||
|
danger: '#dc3545',
|
||||||
|
light: '#f8f9fa',
|
||||||
|
dark: '#343a40',
|
||||||
|
textMuted: '#6c757d',
|
||||||
|
transparent: 'transparent',
|
||||||
|
},
|
||||||
|
space: ['0rem', '0.25rem', '0.5rem', '1rem', '1.5rem', '3rem'],
|
||||||
|
fonts: {
|
||||||
|
body: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
|
||||||
|
heading:
|
||||||
|
'SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||||
|
monospace:
|
||||||
|
'SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||||
|
sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
|
||||||
|
},
|
||||||
|
fontSizes: [
|
||||||
|
'0.75rem',
|
||||||
|
'0.875rem',
|
||||||
|
'1rem',
|
||||||
|
'1.25rem',
|
||||||
|
'1.5rem',
|
||||||
|
'1.75rem',
|
||||||
|
'2rem',
|
||||||
|
'2.5rem',
|
||||||
|
'3.5rem',
|
||||||
|
'4.5rem',
|
||||||
|
'5.5rem',
|
||||||
|
'6rem',
|
||||||
|
],
|
||||||
|
fontWeights: {
|
||||||
|
body: 400,
|
||||||
|
heading: 600,
|
||||||
|
bold: 700,
|
||||||
|
light: 300,
|
||||||
|
normal: 400,
|
||||||
|
display: 300,
|
||||||
|
},
|
||||||
|
borders: {
|
||||||
|
default: `1px solid ${colors.gray[5]}`,
|
||||||
|
primary: `1px solid #007bff`,
|
||||||
|
},
|
||||||
|
lineHeights: {
|
||||||
|
body: 1.6,
|
||||||
|
heading: 1.2,
|
||||||
|
},
|
||||||
|
sizes: {
|
||||||
|
sm: 540,
|
||||||
|
md: 720,
|
||||||
|
lg: 960,
|
||||||
|
xl: 1140,
|
||||||
|
},
|
||||||
|
shadows: {
|
||||||
|
default: '0 .5rem 1rem rgba(0, 0, 0, .15)',
|
||||||
|
sm: '0 .125rem .25rem rgba(0, 0, 0, .075)',
|
||||||
|
lg: '0 1rem 3rem rgba(0, 0, 0, .175)',
|
||||||
|
},
|
||||||
|
radii: {
|
||||||
|
default: '0.25rem',
|
||||||
|
sm: '0.2rem',
|
||||||
|
lg: '0.3rem',
|
||||||
|
pill: '50rem',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
heading: {
|
||||||
|
fontFamily: 'heading',
|
||||||
|
fontWeight: 'heading',
|
||||||
|
lineHeight: 'heading',
|
||||||
|
mt: 0,
|
||||||
|
mb: 2,
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
fontWeight: 'display',
|
||||||
|
lineHeight: 'heading',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
pomodoro: {
|
||||||
|
bg: 'rgba(0,0,0,0.2)',
|
||||||
|
color: 'white',
|
||||||
|
px: 2,
|
||||||
|
borderWidth: 1,
|
||||||
|
cursor: 'pointer',
|
||||||
|
textDecoration: 'none',
|
||||||
|
borderRadius: 'default',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
'&.active, &:active, &:hover:active, &.active:hover': {
|
||||||
|
color: 'background',
|
||||||
|
bg: 'rgba(0,0,0,0.55)',
|
||||||
|
},
|
||||||
|
transition:
|
||||||
|
'box-shadow 200ms ease-in-out, color 200ms ease-in-out, background-color 200ms ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
bg: 'rgba(0, 0, 0, 0.35)',
|
||||||
|
boxShadow: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
color: 'primary',
|
||||||
|
borderColor: 'primary',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: 1,
|
||||||
|
bg: 'background',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textDecoration: 'none',
|
||||||
|
borderRadius: 'default',
|
||||||
|
'&.active, &:active, &:hover:active, &.active:hover': {
|
||||||
|
color: 'background',
|
||||||
|
bg: 'primary',
|
||||||
|
},
|
||||||
|
transition:
|
||||||
|
'box-shadow 200ms ease-in-out, color 200ms ease-in-out, background-color 200ms ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
bg: 'rgba(0, 123, 255, 0.2)',
|
||||||
|
boxShadow: 'default',
|
||||||
|
},
|
||||||
|
'&.hidden': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
color: 'background',
|
||||||
|
bg: 'secondary',
|
||||||
|
},
|
||||||
|
gray: {
|
||||||
|
color: 'background',
|
||||||
|
bg: 'gray',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
root: {
|
||||||
|
fontFamily: 'body',
|
||||||
|
lineHeight: 'body',
|
||||||
|
fontWeight: 'body',
|
||||||
|
},
|
||||||
|
a: {
|
||||||
|
color: 'primary',
|
||||||
|
textDecoration: 'none',
|
||||||
|
},
|
||||||
|
p: {
|
||||||
|
mb: 3,
|
||||||
|
lineHeight: 'body',
|
||||||
|
},
|
||||||
|
h1: {
|
||||||
|
fontFamily: 'heading',
|
||||||
|
fontWeight: 'heading',
|
||||||
|
lineHeight: 'heading',
|
||||||
|
mt: 0,
|
||||||
|
mb: 2,
|
||||||
|
fontSize: 7,
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontFamily: 'heading',
|
||||||
|
fontWeight: 'heading',
|
||||||
|
lineHeight: 'heading',
|
||||||
|
mt: 0,
|
||||||
|
mb: 2,
|
||||||
|
fontSize: 6,
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontFamily: 'heading',
|
||||||
|
fontWeight: 'heading',
|
||||||
|
lineHeight: 'heading',
|
||||||
|
mt: 0,
|
||||||
|
mb: 2,
|
||||||
|
fontSize: 5,
|
||||||
|
},
|
||||||
|
h4: {
|
||||||
|
fontFamily: 'heading',
|
||||||
|
fontWeight: 'heading',
|
||||||
|
lineHeight: 'heading',
|
||||||
|
mt: 0,
|
||||||
|
mb: 2,
|
||||||
|
fontSize: 4,
|
||||||
|
},
|
||||||
|
h5: {
|
||||||
|
fontFamily: 'heading',
|
||||||
|
fontWeight: 'heading',
|
||||||
|
lineHeight: 'heading',
|
||||||
|
mt: 0,
|
||||||
|
mb: 2,
|
||||||
|
fontSize: 3,
|
||||||
|
},
|
||||||
|
h6: {
|
||||||
|
fontFamily: 'heading',
|
||||||
|
fontWeight: 'heading',
|
||||||
|
lineHeight: 'heading',
|
||||||
|
mt: 0,
|
||||||
|
mb: 2,
|
||||||
|
fontSize: 2,
|
||||||
|
},
|
||||||
|
blockquote: {
|
||||||
|
fontSize: 3,
|
||||||
|
mb: 3,
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
width: '100%',
|
||||||
|
marginBottom: 3,
|
||||||
|
color: 'gray.9',
|
||||||
|
borderCollapse: 'collapse',
|
||||||
|
},
|
||||||
|
th: {
|
||||||
|
verticalAlign: 'bottom',
|
||||||
|
borderTopWidth: 2,
|
||||||
|
borderTopStyle: 'solid',
|
||||||
|
borderTopColor: 'gray.3',
|
||||||
|
borderBottomWidth: 2,
|
||||||
|
borderBottomStyle: 'solid',
|
||||||
|
borderBottomColor: 'gray.3',
|
||||||
|
padding: '.75rem',
|
||||||
|
textAlign: 'inherit',
|
||||||
|
},
|
||||||
|
td: {
|
||||||
|
borderBottomWidth: 2,
|
||||||
|
borderBottomStyle: 'solid',
|
||||||
|
borderBottomColor: 'gray.3',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
padding: '.75rem',
|
||||||
|
},
|
||||||
|
inlineCode: {
|
||||||
|
color: 'pink',
|
||||||
|
},
|
||||||
|
img: {
|
||||||
|
maxWidth: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cards: {
|
||||||
|
primary: {
|
||||||
|
border: 'default',
|
||||||
|
borderRadius: 'default',
|
||||||
|
|
||||||
|
transition:
|
||||||
|
'box-shadow 200ms ease-in-out, color 200ms ease-in-out, background-color 200ms ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stigmata: {
|
||||||
|
padding: 2,
|
||||||
|
borderRadius: 4,
|
||||||
|
borderColor: 'gray.3',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
badges: {
|
||||||
|
primary: {
|
||||||
|
backgroundColor: 'primary',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
backgroundColor: 'secondary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export function createMemoFunction<A, R>(method: (arg: A) => R): (arg: A) => R {
|
||||||
|
let previousArg: any
|
||||||
|
let previousResult: any
|
||||||
|
return function (arg) {
|
||||||
|
if (previousArg === arg) {
|
||||||
|
return previousResult
|
||||||
|
}
|
||||||
|
|
||||||
|
previousResult = method(arg)
|
||||||
|
previousArg = arg
|
||||||
|
return previousResult
|
||||||
|
}
|
||||||
|
}
|
||||||