<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

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js
  3. https://unpkg.com/[email protected]/dist/styled-components.min.js