<div id="root"></div>
const { useState, useRef, useCallback, useEffect } = React;
const { createGlobalStyle, css } = styled;
const TIME_LIMIT = {
hour: 1,
min: 1,
sec: 0
};
const GlobalStyle = createGlobalStyle`
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
:root {
--accent-color: rgb(62, 93, 228);
--text-color: #333;
--bg-color: #ffdf2b;
--button-color: #333;
--button-text-color: #fff;
--button-disabled-color: #ccc;
}
*, html {
font-family: 'Share Tech Mono', monospace;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
align-items: center;
justify-content: center;
font-size: 4vmin;
margin: 0;
height: 100vh;
width: 100%;
}
`;
const Spacer = ({ size, horizontal }) => {
return (
<div
style={
horizontal
? { width: size, height: 'auto', display: 'inline-block', flexShrink: 0 }
: { width: 'auto', height: size, flexShrink: 0 }
}
/>
)
}
const StyledButton = styled.button`
background-color: ${({ bgColor }) => bgColor ? bgColor : '#333'}
border: none;
border-radius: .2em;
color: ${({ textColor }) => textColor ? textColor : '#fff'};
cursor: pointer;
font-size: .6em;
padding: .8em 1.6em;
& a {
//background-color: ${({ bgColor }) => bgColor ? bgColor : '#333'}
border-radius: .2em;
color: var(--text-color);
cursor: pointer;
display: block;
font-size: 1em;
padding: .8em 1.6em;
text-decoration: none;
}
&:disabled {
background-color: var(--button-disabled-color);
cursor: default;
}
`;
const Button = ({ className, link, bgColor, textColor, children, ...props }) => (
<StyledButton className={className} {...props} link={link} bgColor={bgColor} textColor={textColor}>
{link ? <Link to={link}>{children}</Link> : children}
</StyledButton>
);
const useCountDownTimer = (time) => {
const [timeLimit, setTimeLimit] = useState(time);
const [countDownStatus, setCountDownStatus] = useState({
isStart: false,
isStop: false,
isReset: false,
isTimeUp: false
})
const [isStart, setIsStart] = useState(false)
const [isTimeUp, setIsTimeUp] = useState(false)
//const [count, setCount] = useState(0);
const intervalID = useRef(null);
const zeroPaddingNum = useCallback((num) => {
return String(num).padStart(2, "0")
}, [])
const initializeTime = useCallback((timeLimit) => {
const newTimeLimit = timeLimit;
if (timeLimit.hour >= 60) {
newTimeLimit.hour = 60;
}
if (timeLimit.min >= 60) {
newTimeLimit.min = 60;
}
if (timeLimit.sec >= 60) {
newTimeLimit.sec = 60;
}
setTimeLimit(newTimeLimit);
});
const stopTime = useCallback(() => {
clearInterval(intervalID.current);
setCountDownStatus(p => ({
...p,
isStop: true,
isStart: false
}))
});
const startTime = useCallback(() => {
//intervalID.current = setInterval(() => setCount((p) => p + 1), 1000);
intervalID.current = setInterval(() => tick(), 1000);
setCountDownStatus(p => ({
...p,
isReset: false,
isStart: true,
isStop: false,
//isTimeUp: false
}))
});
const resetTime = useCallback(() => {
clearInterval(intervalID.current);
setTimeLimit({
hour: zeroPaddingNum(time.hour),
min: zeroPaddingNum(time.min),
sec: zeroPaddingNum(time.sec)
})
setCountDownStatus(p => ({
...p,
isReset: true,
isStart: false,
isTimeUp: false,
isStop: false
}))
})
const tick = useCallback(() => {
setTimeLimit((prevTimeLimit) => {
const newTimeLimit = Object.assign({}, prevTimeLimit);
const { hour, min, sec } = newTimeLimit;
if (hour <= 0 && min <= 0 && sec <= 0) {
stopTime();
setCountDownStatus(p => ({
...p,
isTimeUp: true
}))
return newTimeLimit;
}
if (newTimeLimit.hour > 0 && min <= 0 && sec <= 0) {
newTimeLimit.hour -= 1;
newTimeLimit.min = 60;
}
if (newTimeLimit.min > 0 && newTimeLimit.sec <= 0) {
newTimeLimit.min -= 1;
newTimeLimit.sec = 60;
}
newTimeLimit.sec -= 1;
return {
hour: zeroPaddingNum(newTimeLimit.hour),
min: zeroPaddingNum(newTimeLimit.min),
sec: zeroPaddingNum(newTimeLimit.sec)
};
});
})
/*useEffect(() => {
if (count <= 0) return;
tick()
}, [count]);*/
//useEffect(() => tick(), [time])
//useEffect(() => console.log("year", time), [time])
// ピッカーで選択した値をそのままタイムリミットとして反映する
useEffect(() => {
setTimeLimit({
hour: zeroPaddingNum(time.hour),
min: zeroPaddingNum(time.min),
sec: zeroPaddingNum(time.sec)
})
}, [time])
// タイムアップした後にスタートボタンを押したときに選択したタイムからカウントダウンする
useEffect(() => {
if(countDownStatus.isStart && countDownStatus.isTimeUp) {
setTimeLimit({
hour: zeroPaddingNum(time.hour),
min: zeroPaddingNum(time.min),
sec: zeroPaddingNum(time.sec)
})
setCountDownStatus(p => ({
...p,
isTimeUp: false
}))
}
}, [countDownStatus.isStart])
return [
timeLimit,
initializeTime,
startTime,
stopTime,
resetTime,
isStart,
countDownStatus
];
};
const StyledTimeDisplay = styled.div`
color: ${({ textColor }) => textColor ? textColor : `#333`};
font-weight: bold;
font-size: ${({ fontSize }) => fontSize ? fontSize : '1em'};
`;
const TimeDisplay = ({ className, time, delimiter, fontSize, textColor }) => {
const newTime = Array.isArray(time) ? time : Object.values(time)
return (
<StyledTimeDisplay
className={className}
fontSize={fontSize}
textColor={textColor}
>
{
newTime.map((n, i, array) => (
<>
<span>{String(n).padStart(2, "0")}</span>
{(i !== array.length - 1) && delimiter}
</>
))
}
</StyledTimeDisplay>
);
};
const StyledItemPicker = styled.div`
text-align: center;
& .item-name {
color: ${({ textColor }) => textColor ? textColor : `#333`};
}
& select {
background-color: transparent;
border: none;
border-radius: .2em;
color: ${({ textColor }) => textColor ? textColor : `#333`};
cursor: pointer;
font-size: 1em;
outline: none;
padding: .2em;
}
`
const ItemPicker = ({ className, itemName, values, textColor, handleChange }) => {
return (
<StyledItemPicker className={className} textColor={textColor}>
<div className="item-name">
{itemName}
</div>
<Spacer size=".5em" />
<select id={itemName} name={itemName} onChange={handleChange}>
{
values.map((value, i) => (
<option
id={value}
name={value}
value={value}
key={i}
>
{typeof value !== 'string' ? String(value).padStart(2, "0") : value}
</option>
))
}
</select>
</StyledItemPicker>
)
}
const StyledItemPickers = styled.div`
display: flex;
justify-content: center;
`
const ItemPickers = ({ className, items, textColor, handleChange }) => {
const itemNames = Object.keys(items)
return (
<StyledItemPickers className={className} textColor={textColor}>
{
itemNames.map((itemName, i) => (
<>
<Spacer size=".2em" horizontal={true} />
<ItemPicker
className="item"
itemName={itemName}
values={items[itemName]}
handleChange={handleChange}
key={i}
/>
<Spacer size=".5em" horizontal={true} />
</>
))}
</StyledItemPickers>
)
}
const TIMES = {
hour: [...Array(60)].map((u, i) => i),
min: [...Array(60)].map((u, i) => i),
sec: [...Array(60)].map((u, i) => i),
//s: [40, 50, 60, 120]
}
const StyledApp = styled.div`
& .buttons {
display: flex;
justify-content: center;
}
`
const App = () => {
/*const [
selectNum,
handleDown,
handleWheel,
handleChange
] = useDialSelector(TIMES)*/
const [selectItems, setSelectItems] = useState(() => {
const newItems = {}
Object.keys(TIMES).forEach((name, i) => {
newItems[name] = TIMES[name][0]
})
return newItems
})
const handleChange = useCallback((e) => {
setSelectItems(p => ({
...p,
[e.target.id]: Number(e.target.value)
}))
}, [])
const [
time,
initializeTime,
startTime,
stopTime,
resetTime,
isStart,
countDownStatus
] = useCountDownTimer(
selectItems
);
return (
<>
<GlobalStyle />
<StyledApp>
{/*<DialNumSelector
className="dial-time-selector"
items={TIMES}
handleWheel={handleWheel}
handleMouseDown={handleDown}
/>
<Spacer size="1.6em" />*/}
<ItemPickers
className="time-picker"
items={TIMES}
handleChange={handleChange}
/>
<Spacer size="1.6em" />
<TimeDisplay
className="count-down-timer"
time={time}
delimiter=":"
fontSize="2.8em"
/>
<Spacer size="1.6em" />
<div className="buttons">
<Button
className="button"
onClick={startTime}
disabled={countDownStatus.isStart ? true : false}
>
start
</Button>
<Spacer
size=".65em"
horizontal={true}
/>
<Button
className="button"
onClick={stopTime}
disabled={countDownStatus.isStart ? false : true}
>
stop
</Button>
<Spacer
size=".65em"
horizontal={true}
/>
<Button
className="button"
onClick={resetTime}
disabled={countDownStatus.isStart || countDownStatus.isStop && !countDownStatus.isTimeUp && !countDownStatus.isReset ? false : true}
>
reset
</Button>
</div>
</StyledApp>
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
View Compiled
This Pen doesn't use any external CSS resources.