aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorAlexander Kiryukhin <a.kiryukhin@mail.ru>2022-06-03 19:04:53 +0300
committerAlexander Kiryukhin <a.kiryukhin@mail.ru>2022-06-03 19:04:53 +0300
commit0e7b36c2d443306325f17bb8850f5bb6176202bf (patch)
tree86629d4fe05d73f2d77dad423cc37d5a612430f3 /src/components
parent15d75cdc37e1459f7d11d004005d4305a6377ffd (diff)
initialHEADmaster
Diffstat (limited to 'src/components')
-rw-r--r--src/components/App.js97
-rw-r--r--src/components/TimePicker.js22
-rw-r--r--src/components/Timer.js62
-rw-r--r--src/components/Timers.js41
4 files changed, 222 insertions, 0 deletions
diff --git a/src/components/App.js b/src/components/App.js
new file mode 100644
index 0000000..4688bbb
--- /dev/null
+++ b/src/components/App.js
@@ -0,0 +1,97 @@
+import { useEffect, useState } from 'react';
+import { CopyTwoTone, PlusCircleFilled, ClearOutlined } from '@ant-design/icons';
+import { Button, Card, Col, Form, Input, Layout, message, Popconfirm, Row, Switch } from 'antd';
+
+import useInterval from '../hooks/interval';
+import useLocalStorage from '../hooks/local';
+import { copyTextToClipboard } from '../utils/clipboard';
+import { configFromLink } from '../utils/configFromLink';
+import { defaultTimer } from '../utils/timerTemplate';
+import { updateTimers } from '../utils/updateTimers';
+
+import Timers from './Timers';
+
+const { Footer } = Layout;
+
+function App() {
+ const [browserNotifications, setNotifications] = useLocalStorage("notifications", false); // доступны ли нативные уведомления
+ const [sound, setSound] = useLocalStorage("sound", true); // включен ли звук
+ const [timers, setTimers] = useLocalStorage("timers", configFromLink()); // хранилище таймеров
+ const [url, setUrl] = useState(""); // генерируемая ссылка на кофигурацию
+
+ // Действия при открытии приложения:
+ // 1. Инициализация конфигурации из адреса
+ // 2. Запрос разрешений на уведомления
+ useEffect(() => {
+ if (window.location.hash !== "") {
+ setTimers(configFromLink());
+ window.location.hash = "";
+ }
+ Notification.requestPermission().then((result) => {
+ if (result === 'granted') {
+ setNotifications(true);
+ } else {
+ setNotifications(false);
+ }
+ });
+ // eslint-disable-next-line
+ }, []);
+
+ // Формирование ссылки на конфигурацию при обновлении таймеров
+ useEffect(() => {
+ const d = timers.map(t => [t.name, t.initialTime]);
+ setUrl(btoa(unescape(encodeURIComponent(JSON.stringify(d)))));
+ }, [timers]);
+
+ // Обновление таймеров
+ useInterval(updateTimers.bind(this, timers, setTimers, sound, browserNotifications), 100, true); // интервал опроса таймеров - 100мс потому что если вкладка с таймерами в фоне, браузер начинает заметлять работу js
+
+ return <Layout>
+ <Row>
+ <Col xs={24} sm={12} md={12} lg={8} xl={6}>
+ <Card
+ title={<strong><img alt="Мультитаймер" src="/logo192.png" style={{ height: 32, width: 32 }} />&nbsp;Мультитаймер</strong>}
+ bodyStyle={{ padding: 4 }}
+ style={{ padding: 4, margin: 4 }}
+ >
+ <Form layout='vertical'>
+ <Form.Item>
+ <Button type={'primary'} block onClick={() => setTimers([...timers, { ...defaultTimer }])} icon={<PlusCircleFilled />}>Добавить таймер</Button>
+ </Form.Item>
+ <Form.Item label="Звук таймера">
+ <Switch checkedChildren="Включен" unCheckedChildren="Отключен" checked={sound} onChange={x => setSound(x)} />
+ </Form.Item>
+ <Form.Item label="Ссылка на конфигурацию">
+ <Input
+ readOnly
+ value={`https://timer.neonxp.dev/#${url}`}
+ addonAfter={<CopyTwoTone onClick={() => {
+ copyTextToClipboard(`https://timer.neonxp.dev/#${url}`)
+ message.success("Скопировано в буфер обмена")
+ }} />}
+ />
+ </Form.Item>
+ <Form.Item>
+ <Popconfirm
+ title="Очистить таймеры?"
+ onConfirm={() => setTimers([])}
+ okText="Да"
+ cancelText="Нет">
+ <Button type={'ghost'} block icon={<ClearOutlined />}>Очистить</Button>
+ </Popconfirm>
+ </Form.Item>
+ </Form>
+ </Card>
+ </Col>
+ <Timers
+ items={timers}
+ setTimers={setTimers}
+ />
+ </Row>
+ <Footer>
+ Сделал в 2022г <a href="https://neonxp.dev/">Александр NeonXP Кирюхин</a>.
+ </Footer>
+ </Layout>;
+}
+
+export default App;
diff --git a/src/components/TimePicker.js b/src/components/TimePicker.js
new file mode 100644
index 0000000..0419088
--- /dev/null
+++ b/src/components/TimePicker.js
@@ -0,0 +1,22 @@
+import { InputNumber } from "antd";
+
+const TimePicker = ({ setTime, time, disabled }) => {
+ const seconds = parseInt(Math.floor(time) % 60);
+ const minutes = parseInt(Math.floor(time / 60) % 60);
+ const hours = parseInt(Math.floor(time / 3600));
+ const updateHours = (hours) => {
+ setTime(parseInt(hours)*3600+parseInt(minutes)*60+parseInt(seconds));
+ }
+ const updateMinutes = (minutes) => {
+ setTime(parseInt(hours)*3600+parseInt(minutes)*60+parseInt(seconds));
+ }
+ const updateSeconds = (seconds) => {
+ setTime(parseInt(hours)*3600+parseInt(minutes)*60+parseInt(seconds));
+ }
+ return <>
+ <InputNumber disabled={disabled} type="number" min={0} max={24} style={{width: '90px'}} value={hours} onChange={x => {updateHours(x)}} addonAfter={`ч`} />
+ <InputNumber disabled={disabled} type="number" min={0} max={59} style={{width: '90px'}} value={minutes} onChange={x => {updateMinutes(x)}} addonAfter={`м`} />
+ <InputNumber disabled={disabled} type="number" min={0} max={59} style={{width: '90px'}} value={seconds} onChange={x => {updateSeconds(x)}} addonAfter={`с`} />
+ </>;
+}
+export default TimePicker;
diff --git a/src/components/Timer.js b/src/components/Timer.js
new file mode 100644
index 0000000..1333040
--- /dev/null
+++ b/src/components/Timer.js
@@ -0,0 +1,62 @@
+import { DeleteTwoTone, PlayCircleFilled, StopFilled, LeftCircleFilled, RightCircleFilled, DownCircleFilled, UpCircleFilled, EditFilled } from "@ant-design/icons";
+import { Button, Card, Col, Input, Progress, Space, Popconfirm, Grid } from "antd";
+import ButtonGroup from "antd/lib/button/button-group";
+import { useState } from "react";
+import TimePicker from "./TimePicker";
+
+const { useBreakpoint } = Grid;
+
+const Timer = ({ name, started, start, stop, setName, initialTime, time, setTime, remove, index, first, last, moveTimer }) => {
+ const [edit, setEdit] = useState(false);
+ const toggleEdit = () => {
+ setEdit(!edit);
+ }
+ const screens = useBreakpoint();
+ let seconds = ("0" + (Math.floor(time) % 60)).slice(-2);
+ let minutes = ("0" + (Math.floor(time / 60) % 60)).slice(-2);
+ let hours = ("0" + Math.floor(time / 3600)).slice(-2);
+ return <Col xs={24} sm={12} md={12} lg={8} xl={6}>
+ <Card
+ index={index}
+ title={
+ edit
+ ? (<Input value={name} onChange={ev => setName(ev.target.value)} />)
+ : (<div style={{ height: 32, overflow: 'hidden', textOverflow: 'ellipsis', width: '100%' }}>{name}</div>)
+ }
+ actions={[
+ <Button type='primary' block title='Запуск' onClick={start} disabled={started || initialTime === 0} icon={<PlayCircleFilled />}>Запуск</Button>,
+ <Button type='danger' block title='Стоп' onClick={stop} disabled={!started} icon={<StopFilled />}>Стоп</Button>,
+ ]}
+ bodyStyle={{ padding: 4 }}
+ style={{ padding: 4, margin: 4 }}
+ >
+ <Space align="baseline" style={{ justifyContent: "flex-end", display: "flex" }}>
+ {edit
+ ? (<ButtonGroup>
+ <Popconfirm
+ title="Удалить таймер?"
+ onConfirm={remove}
+ okText="Да"
+ cancelText="Нет">
+ <Button icon={<DeleteTwoTone />}></Button>
+ </Popconfirm>
+ <Button onClick={toggleEdit} icon={<EditFilled />}></Button>
+ </ButtonGroup>)
+ : (<ButtonGroup>
+ <Button disabled={first} onClick={() => moveTimer(-1)} icon={screens.sm ? <LeftCircleFilled /> : <UpCircleFilled />}></Button>
+ <Button disabled={last} onClick={() => moveTimer(1)} icon={screens.sm ? <RightCircleFilled /> : <DownCircleFilled />}></Button>
+ <Button onClick={toggleEdit} icon={<EditFilled />}></Button>
+ </ButtonGroup>)}
+ </Space>
+ <Space direction="vertical" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+ {screens.sm
+ ? (<Progress type="circle" status="active" percent={100 - (time / initialTime * 100)} format={percent => `${hours}:${minutes}:${seconds}`} width={200} />)
+ : (<Progress size="large" status="active" percent={100 - (time / initialTime * 100)} format={percent => `${hours}:${minutes}:${seconds}`} width={200} style={{ width: "200px" }} />)
+ }
+ <TimePicker disabled={started} time={initialTime} setTime={setTime} />
+ </Space>
+ </Card>
+ </Col>;
+}
+
+export default Timer;
diff --git a/src/components/Timers.js b/src/components/Timers.js
new file mode 100644
index 0000000..10b2557
--- /dev/null
+++ b/src/components/Timers.js
@@ -0,0 +1,41 @@
+import Timer from "./Timer";
+
+const Timers = ({ items, setTimers }) => {
+ const setTime = (idx, time) => {
+ setTimers(items.map((t, id) => id !== idx ? t : { ...t, time: time, initialTime: time }));
+ };
+ const initialTimer = (idx) => {
+ setTimers(items.map((t, id) => id !== idx ? t : { ...t, started: true, time: t.initialTime, startedAt: (new Date()).getTime() }));
+ };
+ const stopTimer = (idx) => {
+ setTimers(items.map((t, id) => id !== idx ? t : { ...t, started: false }));
+ };
+ const setTimerName = (idx, name) => {
+ setTimers(items.map((t, id) => id !== idx ? t : { ...t, name }));
+ };
+ const deleteTimer = (idx) => {
+ setTimers(items.filter((t, id) => id !== idx));
+ };
+ const moveTimer = (idx, offset) => {
+ [items[idx], items[idx + offset]] = [items[idx + offset], items[idx]];
+ setTimers(items);
+ };
+ return items.map((t, idx) => <Timer
+ key={`item-${idx}`}
+ index={idx}
+ first={idx === 0}
+ last={idx === (items.length-1)}
+ name={t.name}
+ started={t.started}
+ time={t.time || 0}
+ initialTime={t.initialTime || 0}
+ setTime={(time) => setTime(idx, time)}
+ start={() => initialTimer(idx)}
+ stop={() => stopTimer(idx)}
+ setName={(name) => setTimerName(idx, name)}
+ remove={() => deleteTimer(idx)}
+ moveTimer={(offset) => moveTimer(idx, offset)}
+ />);
+}
+
+export default Timers;