Olga_Brekhuntsova před 2 roky
rodič
revize
813dd0565d

+ 23 - 0
hw-react-03-jsx/.gitignore

@@ -0,0 +1,23 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*

+ 94 - 0
hw-react-03-jsx/README.md

@@ -0,0 +1,94 @@
+Хостинг http://hw_react_03.olgapistryak.fe.a-level.com.ua/
+
+-------------------КОММЕНТАНРИИ---------------
+
+RangeInput
+Хотела, чтобы при некорректном вводе, граница input краснела.
+Покраснение реализовано через дата-атрибут - "флажок", свойства которого изменяются в css.
+Пробовала стейт в один элемент value инпута - граница краснеет только при смене фокуса.
+Попробовала сделать стейт массивом [value, attribut]. Результат такой же: граница краснеет только после смены фокусировки.
+При этом в обих случаях дата-атрибут в ДОМе отображаестся корректно (т.е. при каждом изменении ввода в инпут).
+=> решила проверить работу других свойств инпута: color и background-color. Оба свойства отрабатываются корректно
+=> думаю, что отработка свойства при смене фокусировки - это заморочка именно свойства border-color.
+
+TimerControl
+
+Сделала 2 варианта:
+
+1. инпут отдельно от аутпута
+2. инпут и аутпут в одном месте: меняются ролями в зависимости от кнопки Старт
+
+LCD
+Т.к. кнопка старт/пауза относится к логике компонента, решила, что ее выносить в компонент presentation неуместно. Где она все-таки должна быть?
+
+--------------Условия ДЗ---------------
+React JSX Homework
+Spoiler
+Реализуйте компонент Spoiler, скрывающий контент и открывающий его по клику. Компонент будет получать 3 пропс:
+header, который будет выводится всегда
+open, может быть true или false, если написать в JSX без значения, это значит open={true}
+вложенный контент, т. е. children, который отображается в открытом состоянии спойлера и не отображается в закрытом
+Изначально компонент имеет состояние переданное через пропс open По клику на <div> в котором будет отображаться header должно меняться состояние на противоположное Обеспечьте условие, которое будет показывать или нет children.
+const Spoiler = ({header="+", open, children}) => {
+//напишите тут код
+}
+
+///......
+
+<Spoiler header={<h1>Заголовок</h1>} open>
+Контент 1
+
+<p>
+лорем ипсум траливали и тп.
+</p>
+</Spoiler>
+
+<Spoiler>
+    <h2>Контент 2</h2>
+    <p>
+        лорем ипсум траливали и тп.
+    </p>
+</Spoiler>
+RangeInput
+Реализовать компонент RangeInput, отображающий обычный <input /> со следующими возможностями:
+prop min - минимальная длина строки в инпуте, если меньше - инпут становится красным
+prop max - максимальная длина строки в инпуте, если большe - инпут становится красным
+Используйте компонент-класс и setState для отслеживания и валидации длины инпута. Или useState
+    <RangeInput min={2} max={10} />
+PasswordConfirm
+Реализовать компонент PasswordConfirm, отображающий два <input type='password'/> со следующими возможностями:
+prop min - минимальная длина пароля
+Используйте компонент-класс и setState для отслеживания и валидации совпадения паролей и проверки на длину.Или useState
+По желанию добавьте более хитрые валидации типа проверки на размеры буков и наличие цифр в пароле.
+    <PasswordConfirm min={2} />
+Timer
+Напишите компонент, в который передается через props количество секунд, а компонент при этом реализует обратный отсчет раз в секунду уменьшая количество секунд на 1. Останавливается на 0. Добавьте в компонент кнопку паузы.
+Компонент должен отображать часы, минуты и секунды.
+TimerControl
+Напишите компонент, с тремя полями ввода (часы, минуты и секунды) и кнопкой Start, по которой будет стартовать компонент Timer
+TimerContainer
+const SecondsTimer = ({seconds}) => <h2>{seconds}</h2>
+SecondsTimer в данном случае играет роль presentation компонента, который сам ничего не умеет делать, а просто является шаблоном для отображения своих props в удобном для пользователя виде.
+Реализуйте контейнерный компонент, который будет обеспечивать состояние и логику для любого таймера:
+<TimerContainer seconds={1800} refresh={100} render={SecondsTimer}/>
+TimerContainer должен:
+воспринимать три пропса:
+seconds - секунды для обратного отсчета
+refresh - периодичность обновления таймера в миллисекундах
+render - компонент для отрисовки, которому передается текущее время
+Время вычисляется не по количеству обновлений, а по разности между стартовым и текущим моментом. Иначе таймер будет очень неточен
+так как JSX понимает переменные с маленькой буквы не как компоненты-функции, а как тэги HTML, переприсвойте props render в переменную с большой буквы и используйте её в JSX, как имя компонента, передавая пропс seconds.
+Так как при любом обновлении состояния функция-компонент, как и любая другая функция, запускается целиком используйте setInterval в useEffect
+LCD
+Сделайте из компонента Timer presentation компонент без state, прикрутите его к TimerContainer
+Watch
+Реализуйте часы со стрелками в качестве presentation компонента:
+квадратный блок-контейнер
+стрелки и, возможно, цифры позиционируются с помощью transform: rotate(УГОЛdeg)
+В верстке используйте position absolute для накладывания блоков стрелок и цифр друг на друга (это даст общий центр вращения)
+для корректного центра вращения блок со стрелкой или цифрой должен быть шириной с родительский квадратный блок
+есть еще всякий css (text-orientation) для вращения цифр внутри повернутого блока
+Картинки
+циферблат стрелка часовая стрелка минутная стрелка секундная
+TimerControl + TimerContainer
+Используя TimerControl обновите его код, в котором будет использоваться не Timer, а новый контейнерный компонент

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 11450 - 0
hw-react-03-jsx/package-lock.json


+ 39 - 0
hw-react-03-jsx/package.json

@@ -0,0 +1,39 @@
+{
+  "name": "hw-react-03-jsx",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@testing-library/jest-dom": "^5.16.1",
+    "@testing-library/react": "^12.1.2",
+    "@testing-library/user-event": "^13.5.0",
+    "react": "^17.0.2",
+    "react-dom": "^17.0.2",
+    "react-hook-form": "^7.26.0",
+    "react-scripts": "5.0.0",
+    "web-vitals": "^2.1.4"
+  },
+  "scripts": {
+    "start": "react-scripts start",
+    "build": "react-scripts build",
+    "test": "react-scripts test",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  }
+}

binární
hw-react-03-jsx/public/favicon.ico


+ 44 - 0
hw-react-03-jsx/public/index.html

@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+      <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500&display=swap" rel="stylesheet">
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta
+      name="description"
+      content="Web site created using create-react-app"
+    />
+    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <!--
+      manifest.json provides metadata used when your web app is installed on a
+      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
+    -->
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+    <!--
+      Notice the use of %PUBLIC_URL% in the tags above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
+
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>React App</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
+</html>

binární
hw-react-03-jsx/public/logo192.png


+ 25 - 0
hw-react-03-jsx/public/manifest.json

@@ -0,0 +1,25 @@
+{
+  "short_name": "React App",
+  "name": "Create React App Sample",
+  "icons": [
+    {
+      "src": "favicon.ico",
+      "sizes": "64x64 32x32 24x24 16x16",
+      "type": "image/x-icon"
+    },
+    {
+      "src": "logo192.png",
+      "type": "image/png",
+      "sizes": "192x192"
+    },
+    {
+      "src": "logo512.png",
+      "type": "image/png",
+      "sizes": "512x512"
+    }
+  ],
+  "start_url": ".",
+  "display": "standalone",
+  "theme_color": "#000000",
+  "background_color": "#ffffff"
+}

+ 3 - 0
hw-react-03-jsx/public/robots.txt

@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:

+ 138 - 0
hw-react-03-jsx/src/App.css

@@ -0,0 +1,138 @@
+.App {
+  text-align: center;
+  padding-left: 30px;
+  padding-right: 30px;
+  padding-top: 15px;
+  background-color: lightgrey;
+}
+
+.inputForm {
+  margin-right: 20px;
+}
+.sectionWrapper {
+  margin-bottom: 20px;
+  background-color: whitesmoke;
+  width: 90%;
+  margin-right: auto;
+  margin-left: auto;
+  border-radius: 8px;
+  padding: 20px;
+}
+.passInput {
+  border-color: grey;
+  color: grey;
+  background-color: transparent;
+}
+.passInput[data-confirm="true"] {
+  border-color: red;
+  color: red;
+  background-color: rgb(22, 17, 26);
+}
+.confirmComment {
+  color: red;
+}
+.confirmComment[data-confirm="true"] {
+  color: green;
+}
+.timerBoard {
+  width: 220px;
+  border-radius: 5px;
+  background-color: black;
+  color: #daf6ff;
+  text-shadow: 0 0 20px rgba(10, 175, 230, 1), 0 0 20px rgba(10, 175, 230, 0);
+  font-family: "Orbitron", sans-serif;
+  text-align: center;
+  margin-right: auto;
+  margin-left: auto;
+  margin-bottom: 20px;
+  font-size: 30px;
+  padding: 15px;
+}
+.timerBoard.ms {
+  width: 240px;
+}
+.controlsWrapper {
+  width: 220px;
+  display: flex;
+  justify-content: space-between;
+  margin-left: auto;
+  margin-right: auto;
+}
+.timerBtn {
+  width: 100px;
+  font-size: 20px;
+  color: tomato;
+  font-weight: 600;
+  padding: 10px;
+  background-color: rgb(219, 221, 223);
+  border-radius: 8px;
+}
+
+.timeCell {
+  display: inline-block;
+  width: 50px;
+  text-align: left;
+}
+input.timeCell {
+  font-family: inherit;
+  color: inherit;
+  background-color: inherit;
+  width: 60px;
+  font-size: 100%;
+  margin: 0;
+  overflow: visible;
+  border-radius: 6px;
+  text-align: center;
+}
+@media (prefers-reduced-motion: no-preference) {
+  .App-logo {
+    animation: App-logo-spin infinite 20s linear;
+  }
+}
+
+.clockWrapper {
+  position: relative;
+  margin-left: auto;
+  margin-right: auto;
+  width: 400px;
+  height: 400px;
+  text-align: center;
+  margin-bottom: 10px;
+}
+.clock {
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+/* .arrowH {
+  transform: rotate(90deg);
+}
+.arrowM {
+  transform: rotate(45deg);
+}
+.arrowS {
+  transform: rotate(10deg);
+} */
+/* .App-header {
+  background-color: #282c34;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  font-size: calc(10px + 2vmin);
+  color: white;
+} */
+
+.App-link {
+  color: #61dafb;
+}
+
+@keyframes App-logo-spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}

+ 429 - 0
hw-react-03-jsx/src/App.js

@@ -0,0 +1,429 @@
+import './App.css';
+import { useState, useEffect } from 'react';
+import ClockFace from './images/ClockFace.png';
+import ClockFace_H from './images/ClockFace_H.png';
+import ClockFace_M from './images/ClockFace_M.png';
+import ClockFace_S from'./images/ClockFace_S.png';
+
+const LoginForm = ({ onLogin }) => { 
+  const [login, setLogin] = useState('');
+  const [password, setPassword] = useState('');
+  return <>
+    <input className='inputForm' value={login } onChange={e => {setLogin(e.target.value);  }}/>
+    <input className='inputForm' value={password } onChange={e => {setPassword(e.target.value)}}/>
+    <button disabled={!(login&&password)}  onClick={(e) => {
+         onLogin(login, password);
+         }}>Login</button>
+          </>
+}
+const Section = ({name, children}) =>
+<section className='sectionWrapper'>
+    <h2>{name.toUpperCase()}</h2>
+    { children}
+  </section>
+
+//Task1
+const Spoiler = ({ header = "+", open, children }) => {
+  const [value, setValue] = useState(open);
+    return <>
+      <div onClick={e => {setValue(!value) }}>{header}</div><div>{value&&children}</div>
+    </>
+}
+
+//Task2
+const RangeInput = ({ min, max }) => {
+    let unConfirmed = false;
+  let [state, setState] = useState(['', unConfirmed]);
+  let value = state[0];
+  (value&&((value.length < min) || (value.length > max))) && (unConfirmed = true);
+  return <input className="passInput" data-confirm={unConfirmed} onInput={e => { setState(state=>[e.target.value, unConfirmed]) }}/>
+}
+
+//Task3
+const PasswordConfirm = ({ min }) => {
+  const [value1, setValue1] = useState('');
+  const [value2, setValue2] = useState('');
+  const [commentState, setComment] = useState(['', false]);
+  let [comment, confirmed] = commentState;
+  const checkPass = () => {
+      if (value1 && value2) {
+      // setComment('');
+           //Проверка на минимальное кол-во символов
+      if ((value1.length < min) || (value2.length < min)) { (setComment([('Password should contain more  than ' + min + ' symbols'), false])) }
+      else {
+        //Проверка на равенство паролей
+        if (!(value1 === value2)) { (setComment(["Passwords don't match", false])) }
+        else {
+          //Проверка на наличие заглавной буквы и цифры
+          let character = '';
+          let i = null;
+          let [upC, num]  = [false, false];
+       
+          while ((i < value1.length) || (!upC && !num)) {
+            character = value1.charAt(i);
+            if (isNaN(character)) { (character === character.toUpperCase()) && (upC = true); }
+            else { num = true };
+            i++;
+          }; 
+          
+          !upC && (setComment(["Password should include at least 1 uppercase letter", false]));
+          !num && (setComment(["Password should include at least 1 number", false]));
+          (upC&&num) &&(setComment(["Password confirmed", true]));
+             }
+      }
+    }
+    else (setComment(["Enter password", false]));
+  
+  }
+  
+  return <>
+    <input type='password' placeholder='Enter password' className='inputForm' onChange={e => {setValue1(e.target.value) } }/>
+    <input type='password' placeholder='Repeat password' className='inputForm' onChange={e => { setValue2(e.target.value) } }/>
+       <button onClick={(e) => {checkPass()}}>Confirm</button>
+    <p className="confirmComment" data-confirm={confirmed}>{comment}</p>
+          </>
+ }
+
+//Task4
+const Timer = ({ sec }) => {
+  const [counter, setCounter] = useState(sec);
+  const [btnState, setBtnState] = useState(false);
+  let [hours, minutes, secs] = [Math.floor(counter / (60 * 60)), Math.floor(counter % (60 * 60) / 60), counter % (60 * 60) % 60];
+  const funcShow = (number) => (number < 10) ? ("0" + number) : number;
+     useEffect(() => {
+       let interval;
+       let step = counter;
+              if (btnState) {
+                interval = setInterval(() => { (step > 0) ? ((step--) && setCounter(counter => (counter - 1))) : (setBtnState(false)&&clearInterval(interval))}, 1000);
+          }
+      return () => { clearInterval(interval) }
+    }, [btnState])
+
+  return <>
+    <div className='timerBoard'> <span className="timeCell">{funcShow(hours)}</span>:<span className="timeCell">{ funcShow(minutes)}</span>:<span className="timeCell">{ funcShow(secs)}</span></div>
+    <button className='timerBtn' onClick={(e) => { setBtnState(!btnState)}}> {  btnState ? "Pause" : "Start"}</button>
+  </>
+};
+
+//Task5
+const TimerControl1 = () => {
+ //Начальные значения состояний
+  const initialState = { resBtn: true, startBtn: false, inCount: 0 }
+  //Состояние таймера
+  const [counter, setCounter] = useState(initialState.inCount);
+ // Состояние кнопки Старта/Пауза
+  const [btnState, setBtnState] = useState(initialState.startBtn);
+ // Состояние кнопки сброса
+  const [btnResetState, setResetState] = useState(initialState.resBtn);
+//Состояние инпутов
+  const [time, setTime] = useState({hours: '', mins:'', secs:''});
+  let [hoursMod, minsMod, secsMod] = [Math.floor(counter / (60 * 60)), Math.floor(counter % (60 * 60) / 60), counter % (60 * 60) % 60];
+  let [hoursPart, minsPart, secsPart] = [0, 0, 0];
+
+  //Вносит ввод пользователя в стейт инпутов
+  const handleUserInput = (inputValue, timeType) => {
+    (timeType === 'hours') && setTime({ ...time, hours: inputValue });
+    (timeType === 'mins') && setTime({ ...time, mins: inputValue });
+    (timeType === 'secs') && setTime({ ...time, secs: inputValue });
+  };
+  
+  //Показывает числа в аутпуте: если был сброс, то нули. Если этап монтирования компонента, то нули. Если запущен таймер, то отсчет
+  const timeShow = (timeType) => {return btnResetState ? '00' : (counter?addZero((timeType==='hours'&& hoursMod)||(timeType==='mins'&& minsMod)||(timeType==='secs'&& secsMod)):'00'); }
+ 
+  //Добавляет ноль в аутпут, если число однозначное (для красоты)
+  const addZero = (number) => { !number && (number = 0); return ((number < 10) ? ("0" + number) : number) };
+ 
+  //Пересчитывает введенное время в секундах и устанавливает начальное значение таймера
+  const handleCounter = ({hours, mins, secs }) => {
+    hoursPart = hours * 60 * 60;
+    minsPart = mins * 60;
+    secsPart = Number(secs);
+    console.log(hoursPart, minsPart, secsPart);
+    return (!counter ? hoursPart + minsPart + secsPart : counter);
+  };
+   //Сброс до начальных значений
+  function clearFunc({resBtn, startBtn, inCount }=initialState) {
+    setResetState( resBtn );
+    setBtnState( startBtn );
+    setCounter(inCount);
+    setTime({ hours:'', mins:'', secs:''});
+     }
+ //Запуск таймера
+      useEffect(() => {
+       let interval;
+       let step = counter;
+              if (btnState) {
+                interval = setInterval(() => { (step > 0) ? ((step--) && setCounter(counter => (counter - 1))) : (setBtnState(false)&&clearInterval(interval))}, 1000);
+          }
+      return () => { clearInterval(interval) }
+    }, [btnState])
+ 
+  return <>
+     <div className='timerBoard'>
+      <input className="timeCell" name="inputHours" maxLength="2" disabled={!counter ? false : true} value={time.hours} placeholder="hh" onClick={e => { setResetState(false) }} onInput={e => {handleUserInput(e.target.value, 'hours') }} />
+       <span>:</span>
+      <input className="timeCell"name="inputMins"  maxLength="2" disabled={!counter?false:true  } value={time.mins} placeholder="mm"  onClick={e=>setResetState(false)}  onInput={e => {handleUserInput(e.target.value, 'mins') }}/>
+      <span>:</span>
+      <input className="timeCell" name="inputSecs" maxLength="2" disabled={!counter ? false : true} value={time.secs} placeholder="ss" onClick={e => setResetState(false)} onInput={e => {handleUserInput(e.target.value, 'secs') }}/>
+      </div>
+    <div className='timerBoard'>
+      <span className="timeCell">{timeShow('hours')}</span> : <span className="timeCell">{timeShow('mins')}</span> : <span className="timeCell">{timeShow('secs')}</span>
+        </div>
+       <div className='controlsWrapper'>
+    <button className='timerBtn' onClick={(e) => { setBtnState(!btnState); setCounter(()=>handleCounter(time))}}> {  btnState ? "Pause" : "Start"}</button>
+      <button className='timerBtn' onClick={() => clearFunc() }> Reset</button>
+      </div>
+    
+  </>
+};
+
+const TimerControl2 = () => {
+ //Начальные значения состояний
+  const initialState = { resBtn: true, startBtn: false, inCount: 0 }
+  //Состояние таймера
+  const [counter, setCounter] = useState(initialState.inCount);
+ // Состояние кнопки Старта/Пауза
+  const [btnState, setBtnState] = useState(initialState.startBtn);
+ // Состояние кнопки сброса
+  const [btnResetState, setResetState] = useState(initialState.resBtn);
+//Состояние инпутов
+  const [time, setTime] = useState({hours: '', mins:'', secs:''});
+  let [hoursMod, minsMod, secsMod] = [Math.floor(counter / (60 * 60)), Math.floor(counter % (60 * 60) / 60), counter % (60 * 60) % 60];
+  let [hoursPart, minsPart, secsPart] = [0, 0, 0];
+
+  //Вносит ввод пользователя в стейт инпутов
+  const handleUserInput = (inputValue, timeType) => {
+    (timeType === 'hours') && setTime({ ...time, hours: inputValue });
+    (timeType === 'mins') && setTime({ ...time, mins: inputValue });
+    (timeType === 'secs') && setTime({ ...time, secs: inputValue });
+  };
+  
+  //Показывает числа в аутпуте: если был сброс, то нули. Если этап монтирования компонента, то нули. Если запущен таймер, то отсчет
+  const timeShow = (timeType) => {return btnResetState ? '00' : (counter?addZero((timeType==='hours'&& hoursMod)||(timeType==='mins'&& minsMod)||(timeType==='secs'&& secsMod)):'00'); }
+ 
+  //Добавляет ноль в аутпут, если число однозначное (для красоты)
+  const addZero = (number) => { !number && (number = 0); return ((number < 10) ? ("0" + number) : number) };
+ 
+  //Пересчитывает введенное время в секундах и устанавливает начальное значение таймера
+  const handleCounter = ({hours, mins, secs }) => {
+    hoursPart = hours * 60 * 60;
+    minsPart = mins * 60;
+    secsPart = Number(secs);
+    // console.log(hoursPart, minsPart, secsPart);
+    return (!counter ? hoursPart + minsPart + secsPart : counter);
+  };
+   //Сброс до начальных значений
+  function clearFunc({resBtn, startBtn, inCount }=initialState) {
+    setResetState( resBtn );
+    setBtnState( startBtn );
+    setCounter(inCount);
+    setTime({ hours:'', mins:'', secs:''});
+     }
+  useEffect(() => {
+    let interval;
+       let step = counter;
+       if (btnState) {
+            interval = setInterval(() => {(step>0)?((step--)&&setCounter(counter =>  (counter - 1))):(setBtnState(false)&&clearInterval(interval))}, 1000);
+          }
+      return () => { clearInterval(interval) }
+    }, [btnState])
+
+  return <>
+    <div className='timerBoard'>
+      <input className="timeCell" name="inputHours" maxLength="2" disabled={!counter ? false : true} value={!btnState ? (time.hours):timeShow('hours')} placeholder="hh" onClick={e => { setResetState(false)}} onInput={e => {handleUserInput(e.target.value, 'hours') } }/>
+      <span>:</span>
+      <input className="timeCell"name="inputMins"  maxLength="2" disabled={!counter?false:true  } value={!btnState ? (time.mins):timeShow('mins')} placeholder="mm"  onClick={e=>setResetState(false)}  onInput={e => {handleUserInput(e.target.value, 'mins') }}/>
+      <span>:</span>
+      <input className="timeCell" name="inputSecs" maxLength="2" disabled={!counter ? false : true} value={!btnState ? (time.secs):timeShow('secs')} placeholder="ss" onClick={e => setResetState(false)} onInput={ e => {handleUserInput(e.target.value, 'secs') }}/>
+       </div>
+    <div className='controlsWrapper'>
+    <button className='timerBtn' onClick={(e) => { setBtnState(!btnState); setCounter(()=>handleCounter(time))}}> {  btnState ? "Pause" : "Start"}</button>
+      <button className='timerBtn' onClick={(e) => { clearFunc(e)}}> Reset</button>
+</div>
+  </>
+};
+
+
+const TimerContainer = ({ seconds, refresh, render }) => {
+  const [counter, setCounter] = useState(seconds);
+  const [btnState, setBtnState] = useState(false);
+  const t0 = performance.now();
+  const handleClick = () => setBtnState(!btnState);
+  let pauseState = seconds;
+  useEffect(() => {
+    let interval;
+    // (btnState && pauseState <= refresh) && (pauseState=seconds);
+    (pauseState*1000 <= refresh) && (pauseState=seconds);
+    setCounter(pauseState);
+    if (btnState) {
+      interval = setInterval(() => {
+         const currentState = counter - (performance.now() - t0) / 1000;
+        (!(currentState < 0)) ? setCounter(currentState) : (setBtnState(false) && clearInterval(interval));
+      }, refresh);
+    }
+    return () => {clearInterval(interval) }
+  }, [btnState]);
+  const Render = (props) => { return render(props) };  
+  pauseState = counter;
+  return <><Render seconds={counter.toFixed(2)} />
+    <button className='timerBtn' onClick= { handleClick } > {btnState ? "Pause" : "Start"}</button>
+    </>
+};         
+
+const SecondsTimer = ({ seconds } ) => <h2>{seconds}</h2>;
+
+const LCD = ({ seconds}) => {
+    let [hoursMod, minsMod, secsMod, mSecsMod] = [Math.floor(seconds / (60 * 60)), Math.floor(seconds % (60 * 60) / 60), Math.floor(seconds % (60 * 60) % 60), Math.trunc((seconds-Math.trunc(seconds))*100) ];
+ const addZero = (number) => { !number && (number = 0); return ((number < 10) ? ("0" + number) : number) };
+ return <>
+    <div className='timerBoard ms'> <span className="timeCell">{addZero(hoursMod)}</span>:<span className="timeCell">{ addZero(minsMod)}</span>:<span className="timeCell">{ addZero(secsMod)}</span>:<span className="timeCell">{addZero(mSecsMod)}</span></div>
+  </>
+};
+ 
+const Watch = ({ seconds }) => { 
+  let [hoursMod, minsMod, secsMod] = [seconds / (60 * 60), seconds % (60 * 60) / 60, seconds -Math.floor(seconds / (60 * 60))*3600-Math.floor(seconds % (60 * 60) / 60)*60];
+  const hourDeg = 'rotate(' + 30 * hoursMod + 'deg)';
+  const minDeg = 'rotate(' + 6 * minsMod + 'deg)';
+  const secDeg = 'rotate(' + 6 * secsMod + 'deg)';
+  // console.log(hoursMod, minsMod, secsMod);
+  return <div className='clockWrapper'>
+    <img className="clock" src={ClockFace} alt=''></img>
+    <img className="clock arrowH" style={{ transform: hourDeg }} src={ClockFace_H} alt=''></img>
+    <img className="clock arrowM" style={{ transform: minDeg }} src={ClockFace_M} alt=''></img>
+    <img className="clock arrowS" style={{ transform: secDeg }} src={ClockFace_S} alt=''></img>
+    </div>
+};
+
+
+const TimerContainerReset = ({ seconds, refresh, render }) => {
+//Начальные значения состояний
+  const initialState = { resBtn: true, startBtn: false, inCount: seconds }
+  //Состояние таймера
+  const [counter, setCounter] = useState(initialState.inCount);
+ // Состояние кнопки Старта/Пауза
+  const [btnState, setBtnState] = useState(initialState.startBtn);
+ // Состояние кнопки сброса
+  const [btnResetState, setResetState] = useState(initialState.resBtn);
+  let [hoursMod, minsMod, secsMod] = [Math.floor(counter / (60 * 60)), Math.floor(counter % (60 * 60) / 60), counter % (60 * 60) % 60];
+     //Состояние инпутов
+  const [time, setTime] = useState({ hours: '', mins: '', secs: '' });
+
+  let [hoursPart, minsPart, secsPart] = [0, 0, 0];
+  let t0;
+  const handleClick = () => { setBtnState(!btnState); setCounter(()=>handleCounter(time)) }
+   function clearFunc({resBtn, startBtn, inCount }=initialState) {
+    setResetState( resBtn );
+    setBtnState( startBtn );
+    setCounter(inCount);
+    setTime({ hours:'', mins:'', secs:''});
+   }
+      //Вносит ввод пользователя в стейт инпутов
+    const handleUserInput = (inputValue, timeType) => {
+    (timeType === 'hours') && setTime({ ...time, hours: inputValue });
+    (timeType === 'mins') && setTime({ ...time, mins: inputValue });
+    (timeType === 'secs') && setTime({ ...time, secs: inputValue });
+  };
+    //Пересчитывает введенное время в секундах и устанавливает начальное значение таймера
+  const handleCounter = (time) => {
+    hoursPart = time.hours * 60 * 60;
+    minsPart = time.mins * 60;
+    secsPart = Number(time.secs);
+    console.log(hoursPart, minsPart, secsPart);
+    return (!counter ? hoursPart + minsPart + secsPart : counter);
+  };
+  //  console.log(time);  
+  
+  let pauseState=counter;
+   useEffect(() => {
+    t0 = performance.now();
+     let interval;
+     (pauseState*1000 <= refresh) && (pauseState=initialState.inCount);
+    setCounter(pauseState);
+     
+    if (btnState) {
+      interval = setInterval(() => {
+         const currentState = counter - (performance.now() - t0) / 1000;
+        (!(currentState < 0)) ? setCounter(currentState) : (setBtnState(false) && clearInterval(interval));
+          console.log(counter);
+      }, refresh);
+    }
+    return () => {clearInterval(interval) }
+  }, [btnState]);
+  const Render = (props) => { return render(props) };  
+  pauseState = counter;
+  return <><TimerControlInput counter={counter} handleUserInput={handleUserInput} time={ time}/>
+    <Render seconds={counter.toFixed(2)} />
+     <div className='controlsWrapper'>
+   <button className='timerBtn' onClick={handleClick}> {  btnState ? "Pause" : "Start"}</button>
+      <button className='timerBtn' onClick={() => clearFunc() }> Reset</button>
+      </div>
+    </>
+};         
+
+
+const TimerControlInput = ({ counter, handleUserInput, time}) => {
+  return  <> 
+   <div className='timerBoard ms'>
+      <input className="timeCell" name="inputHours" maxLength="2" disabled={!counter ? false : true} value={time.hours} placeholder="hh" onInput={e => { handleUserInput(e.target.value, 'hours') }} />
+       <span>:</span>
+      <input className="timeCell"name="inputMins"  maxLength="2" disabled={!counter?false:true  } value={time.mins} placeholder="mm" onInput={e => {handleUserInput(e.target.value, 'mins') }}/>
+      <span>:</span>
+      <input className="timeCell" name="inputSecs" maxLength="2" disabled={!counter ? false : true} value={time.secs} placeholder="ss" onInput={e => {handleUserInput(e.target.value, 'secs') }}/>
+      </div> 
+  </>
+}
+
+function App() {
+  return (
+    <div className="App">
+      <header className="App-header">
+      </header>
+      <Section name='classwork'>
+        <LoginForm onLogin={(login, password) => console.log(login, password)} />
+      </Section>
+      <Section name='task-1 Spoiler'>
+       <Spoiler header={<h1>Заголовок</h1>} open>
+    Контент 1
+    <p>
+        лорем ипсум траливали и тп.
+    </p>
+</Spoiler>
+<Spoiler>
+    <h2>Контент 2</h2>
+    <p>
+        лорем ипсум траливали и тп.
+    </p>
+</Spoiler>
+      </Section>
+      <Section name='task-2 Range_Input'>
+           <RangeInput min={2} max={10} />
+      </Section>
+          <Section name='task-3 Password_Confirm'>
+          <PasswordConfirm min={2} />
+      </Section>
+       <Section name='task-4 Timer'>
+          <Timer sec={20} />
+      </Section>
+        <Section name='task-5.1 Timer_Control1'>
+        <TimerControl1  />
+      </Section>
+              <Section name='task-5.2 Timer_Control2'>
+        <TimerControl2  />
+      </Section>
+           <Section name='task-6 Timer_Container'>
+        <TimerContainer  seconds={10} refresh={50} render={SecondsTimer} />
+      </Section>
+       <Section name='task-7 LCD'>
+       <TimerContainer  seconds={10} refresh={50} render={LCD} />
+      </Section>
+       <Section name='task-8 Watch'>
+       <TimerContainer  seconds={3467} refresh={50} render={Watch} />
+      </Section>
+        <Section name='task-9 TimerControl + TimerContainer'>
+        <TimerContainerReset seconds={0} refresh={50} render={LCD} getData={TimerControlInput } />
+             </Section>
+    </div>
+  );
+}
+
+export default App;

+ 8 - 0
hw-react-03-jsx/src/App.test.js

@@ -0,0 +1,8 @@
+import { render, screen } from '@testing-library/react';
+import App from './App';
+
+test('renders learn react link', () => {
+  render(<App />);
+  const linkElement = screen.getByText(/learn react/i);
+  expect(linkElement).toBeInTheDocument();
+});

binární
hw-react-03-jsx/src/images/ClockFace.png


binární
hw-react-03-jsx/src/images/ClockFace_H.png


binární
hw-react-03-jsx/src/images/ClockFace_M.png


binární
hw-react-03-jsx/src/images/ClockFace_S.png


+ 13 - 0
hw-react-03-jsx/src/index.css

@@ -0,0 +1,13 @@
+body {
+  margin: 0;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+    sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
+}

+ 14 - 0
hw-react-03-jsx/src/index.js

@@ -0,0 +1,14 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+import App from './App';
+import reportWebVitals from './reportWebVitals';
+
+ReactDOM.render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>,
+  document.getElementById('root')
+);
+
+reportWebVitals();

+ 13 - 0
hw-react-03-jsx/src/reportWebVitals.js

@@ -0,0 +1,13 @@
+const reportWebVitals = onPerfEntry => {
+  if (onPerfEntry && onPerfEntry instanceof Function) {
+    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+      getCLS(onPerfEntry);
+      getFID(onPerfEntry);
+      getFCP(onPerfEntry);
+      getLCP(onPerfEntry);
+      getTTFB(onPerfEntry);
+    });
+  }
+};
+
+export default reportWebVitals;

+ 5 - 0
hw-react-03-jsx/src/setupTests.js

@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom';