JIRA-1 feat(stat): add statistic
22
README.md
|
@ -1,6 +1,6 @@
|
||||||
# [Assayo](https://assayo.jp/)
|
# [Assayo](https://assayo.jp/)
|
||||||
|
|
||||||
Визуализация и анализ данных вашего git-репозитория.
|
Визуализация и анализ данных вашего git-репозитория ([демо](https://assayo.jp/demo/?dump=./test.git)).
|
||||||
|
|
||||||
##### Сотрудник может оценить новое место работы
|
##### Сотрудник может оценить новое место работы
|
||||||
- темп работы;
|
- темп работы;
|
||||||
|
@ -38,7 +38,7 @@ Alex B <alex@mail.uk> <alex@gov.tk>
|
||||||
Alex B <alex@mail.uk> <bakhirev@ya.kz>
|
Alex B <alex@mail.uk> <bakhirev@ya.kz>
|
||||||
Alex B <alex@mail.uk> <super_man@yahoo.com>
|
Alex B <alex@mail.uk> <super_man@yahoo.com>
|
||||||
```
|
```
|
||||||
Подробнее про формат этого файла можно прочитать тут [https://git-scm.com/docs/gitmailmap](gitmailmap).
|
Подробнее про формат этого файла можно прочитать [тут](https://git-scm.com/docs/gitmailmap).
|
||||||
|
|
||||||
### Как выгрузить данные из git?
|
### Как выгрузить данные из git?
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ Git создаст файл `dump.git`.
|
||||||
### Как посмотреть отчёт онлайн?
|
### Как посмотреть отчёт онлайн?
|
||||||
|
|
||||||
- Перейти на [сайт](https://assayo.jp/)
|
- Перейти на [сайт](https://assayo.jp/)
|
||||||
- Нажать кнопку "[Демо](https://assayo.jp/demo)"
|
- Нажать кнопку «[Демо](https://assayo.jp/demo)»
|
||||||
- Перетащить файл `dump.git` в окно браузера
|
- Перетащить файл `dump.git` в окно браузера
|
||||||
|
|
||||||
### Как посмотреть отчёт офлайн?
|
### Как посмотреть отчёт офлайн?
|
||||||
|
@ -73,8 +73,8 @@ Git создаст файл `dump.git`.
|
||||||
|
|
||||||
### Как посмотреть отчёт по группе микросервисов?
|
### Как посмотреть отчёт по группе микросервисов?
|
||||||
- Сгенерировать для каждого микросервиса `dump.git` (`dump-1.git`, `dump-2.git`, `dump-3.git` и т.д.)
|
- Сгенерировать для каждого микросервиса `dump.git` (`dump-1.git`, `dump-2.git`, `dump-3.git` и т.д.)
|
||||||
- См. "Как посмотреть отчёт онлайн?". На последнем шаге перетащить сразу все файлы в окно браузера.
|
- См. «Как посмотреть отчёт онлайн?». На последнем шаге перетащить сразу все файлы в окно браузера.
|
||||||
- См. "Как посмотреть отчёт офлайн?". На втором шаге перетащить все файлы микросервисов (`dump-1.git`, `dump-2.git`, `dump-3.git` и т.д.) в папку отчета (`/build`).
|
- См. «Как посмотреть отчёт офлайн?». На втором шаге перетащить все файлы микросервисов (`dump-1.git`, `dump-2.git`, `dump-3.git` и т.д.) в папку отчета (`/build`).
|
||||||
|
|
||||||
### Как подписывать коммиты?
|
### Как подписывать коммиты?
|
||||||
|
|
||||||
|
@ -87,15 +87,25 @@ JIRA-1234 feat(profile): Added avatar for user
|
||||||
- фича `(profile - раздел сайта, страница или новый функционал одним словом)`
|
- фича `(profile - раздел сайта, страница или новый функционал одним словом)`
|
||||||
- какую проблему решали `(Added avatar for user)`
|
- какую проблему решали `(Added avatar for user)`
|
||||||
|
|
||||||
|
### Как автоматизировать сбор данных (CI/CD)
|
||||||
|
|
||||||
|
#### Локально
|
||||||
|
- создайте клон нужного вам репозитория;
|
||||||
|
- скопируйте в корень папку `build`;
|
||||||
|
- откройте `build/index.html` в браузере и добавьте в закладки;
|
||||||
|
- добавьте ярлык на `build/assets/ci-cd.sh` в папку автозагрузки (Windows);
|
||||||
|
|
||||||
|
Каждый раз, при перезагрузке компьютера, скрипт будет обновлять статстику по всем данным, которые автоматически влились в основную ветку.
|
||||||
|
|
||||||
### RoadMap
|
### RoadMap
|
||||||
|
|
||||||
Релизы, примерно, раз в полгода. Что дальше:
|
Релизы, примерно, раз в полгода. Что дальше:
|
||||||
|
|
||||||
- больше советов и достижений;
|
- больше советов и достижений;
|
||||||
- итоги года / месяца, печать отчётов;
|
- итоги года / месяца, печать отчётов;
|
||||||
|
- локализация и интернационализация;
|
||||||
- разные роли для статистики (скрытие финансов);
|
- разные роли для статистики (скрытие финансов);
|
||||||
- разработка бекенда, интеграции с другими системами;
|
- разработка бекенда, интеграции с другими системами;
|
||||||
- локализация и интернационализация;
|
|
||||||
|
|
||||||
### Пожелания, предложения, замечания
|
### Пожелания, предложения, замечания
|
||||||
- [alexey-bakhirev@yandex.ru](mailto:alexey-bakhirev@yandex.ru)
|
- [alexey-bakhirev@yandex.ru](mailto:alexey-bakhirev@yandex.ru)
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.css": "./static/css/main.33cc195e.css",
|
"main.css": "./static/css/main.27b68202.css",
|
||||||
"main.js": "./static/js/main.405a9477.js",
|
"main.js": "./static/js/main.fcc567df.js",
|
||||||
|
"static/media/car.png": "./static/media/car.b8dd8738e37fe866285f.png",
|
||||||
"index.html": "./index.html",
|
"index.html": "./index.html",
|
||||||
"static/media/warning.svg": "./static/media/warning.e39a87773603f3ab157f.svg",
|
"static/media/warning.svg": "./static/media/warning.e39a87773603f3ab157f.svg",
|
||||||
"static/media/info.svg": "./static/media/info.954631f6b19e3fe9c495.svg",
|
"static/media/info.svg": "./static/media/info.954631f6b19e3fe9c495.svg",
|
||||||
"static/media/alert.svg": "./static/media/alert.41e2b99c481139c13074.svg",
|
"static/media/alert.svg": "./static/media/alert.41e2b99c481139c13074.svg",
|
||||||
"main.33cc195e.css.map": "./static/css/main.33cc195e.css.map",
|
"main.27b68202.css.map": "./static/css/main.27b68202.css.map",
|
||||||
"main.405a9477.js.map": "./static/js/main.405a9477.js.map"
|
"main.fcc567df.js.map": "./static/js/main.fcc567df.js.map"
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/css/main.33cc195e.css",
|
"static/css/main.27b68202.css",
|
||||||
"static/js/main.405a9477.js"
|
"static/js/main.fcc567df.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><path class="cls-1" d="M60.51,27.13L22.64,88.62c-1.76,2.93,.39,6.83,3.9,6.83H102.28c3.51,0,5.66-3.9,3.9-6.83L68.32,27.13c-1.76-2.73-5.86-2.73-7.81,0Z"/><rect class="cls-1" x="61.42" y="41.65" width="5.86" height="32.28"/><rect class="cls-1" x="61.45" y="80.91" width="5.86" height="5.86"/></svg>
|
|
Before Width: | Height: | Size: 535 B |
|
@ -1 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><g><line class="cls-1" x1="82.18" y1="89.01" x2="94.08" y2="105.01"/><line class="cls-1" x1="84.13" y1="102.28" x2="87.06" y2="95.84"/><line class="cls-1" x1="90.76" y1="110.87" x2="84.13" y2="102.28"/><line class="cls-1" x1="94.08" y1="105.01" x2="90.76" y2="110.87"/><line class="cls-1" x1="94.28" y1="94.47" x2="87.06" y2="95.84"/><line class="cls-1" x1="100.72" y1="103.06" x2="94.28" y2="94.47"/><line class="cls-1" x1="94.28" y1="104.82" x2="100.72" y2="103.06"/></g><g><line class="cls-1" x1="77.49" y1="50.75" x2="99.94" y2="29.08"/><line class="cls-1" x1="98.38" y1="39.23" x2="91.55" y2="37.09"/><line class="cls-1" x1="105.99" y1="31.62" x2="98.38" y2="39.23"/><line class="cls-1" x1="99.94" y1="29.08" x2="105.99" y2="31.62"/><line class="cls-1" x1="89.4" y1="30.25" x2="91.55" y2="37.09"/><line class="cls-1" x1="97.21" y1="22.64" x2="89.4" y2="30.25"/><line class="cls-1" x1="99.74" y1="28.89" x2="97.21" y2="22.64"/></g><g><line class="cls-1" x1="57.19" y1="61.68" x2="17.18" y2="45.09"/><line class="cls-1" x1="27.33" y1="42.36" x2="27.91" y2="49.58"/><line class="cls-1" x1="17.37" y1="38.45" x2="27.33" y2="42.36"/><line class="cls-1" x1="17.18" y1="45.09" x2="17.37" y2="38.45"/><line class="cls-1" x1="22.45" y1="54.26" x2="27.91" y2="49.58"/><line class="cls-1" x1="12.49" y1="49.97" x2="22.45" y2="54.26"/><line class="cls-1" x1="17.18" y1="45.28" x2="12.49" y2="49.97"/></g><circle class="cls-1" cx="64.02" cy="64.61" r="7.42"/><path class="cls-1" d="M44.89,60.9c-.2,1.17-.39,2.54-.39,3.71,0,10.74,8.78,19.32,19.32,19.32s19.32-8.78,19.32-19.32-8.78-19.32-19.32-19.32c-6.44,0-12.49,3.12-16.01,8.39"/><path class="cls-1" d="M33.96,58.75c-.39,1.95-.59,3.9-.59,5.86,0,16.79,13.86,30.45,30.45,30.45s30.45-13.86,30.45-30.45c0-6.05-1.76-12.1-5.27-17.18"/><path class="cls-1" d="M80.22,38.84c-4.88-3.12-10.54-4.68-16.2-4.68-10.15,0-20.1,5.27-25.57,13.86"/></svg>
|
|
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 725 B |
1
build/assets/achievements/moreRefactoring.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><line class="cls-1" x1="84.97" y1="58.68" x2="84.97" y2="24.47"/><line class="cls-1" x1="43.92" y1="58.68" x2="43.92" y2="34.88"/><line class="cls-1" x1="54.97" y1="24.47" x2="84.97" y2="24.47"/><line class="cls-1" x1="54.6" y1="38.3" x2="74.28" y2="38.3"/><line class="cls-1" x1="54.6" y1="46.78" x2="74.28" y2="46.78"/><line class="cls-1" x1="54.6" y1="55.26" x2="74.28" y2="55.26"/><line class="cls-1" x1="54.97" y1="24.47" x2="43.92" y2="34.88"/><line class="cls-1" x1="35.29" y1="64.88" x2="48.76" y2="64.88"/><line class="cls-1" x1="80.12" y1="64.88" x2="93.6" y2="64.88"/><line class="cls-1" x1="63.23" y1="80.85" x2="65.66" y2="80.85"/><line class="cls-1" x1="73.07" y1="80.85" x2="75.5" y2="80.85"/><line class="cls-1" x1="82.9" y1="80.85" x2="85.33" y2="80.85"/><line class="cls-1" x1="43.56" y1="80.85" x2="45.99" y2="80.85"/><line class="cls-1" x1="53.39" y1="80.85" x2="55.83" y2="80.85"/><line class="cls-1" x1="58.31" y1="86.2" x2="60.74" y2="86.2"/><line class="cls-1" x1="68.14" y1="86.2" x2="70.58" y2="86.2"/><line class="cls-1" x1="77.98" y1="86.2" x2="80.41" y2="86.2"/><line class="cls-1" x1="87.82" y1="86.2" x2="90.25" y2="86.2"/><line class="cls-1" x1="38.64" y1="86.2" x2="41.07" y2="86.2"/><line class="cls-1" x1="48.47" y1="86.2" x2="50.9" y2="86.2"/><path class="cls-1" d="M80.12,64.88c0,3.37-7.08,6.15-15.68,6.15s-15.4-2.65-15.67-5.95"/><line class="cls-1" x1="43.92" y1="55.26" x2="35.29" y2="55.26"/><line class="cls-1" x1="93.6" y1="55.26" x2="84.97" y2="55.26"/><line class="cls-1" x1="35.29" y1="75.94" x2="35.29" y2="55.26"/><line class="cls-1" x1="28.37" y1="71.03" x2="28.37" y2="60.69"/><line class="cls-1" x1="93.6" y1="75.94" x2="93.6" y2="55.26"/><rect class="cls-1" x="93.6" y="60.1" width="6.62" height="10.93"/><line class="cls-1" x1="35.29" y1="75.94" x2="93.6" y2="75.94"/><rect class="cls-1" x="24.62" y="91.55" width="79.65" height="11.16"/><line class="cls-1" x1="35.29" y1="75.94" x2="24.62" y2="91.55"/><line class="cls-1" x1="28.37" y1="71.03" x2="23.04" y2="78.84"/><line class="cls-1" x1="93.6" y1="75.94" x2="104.27" y2="91.55"/><line class="cls-1" x1="35.29" y1="60.1" x2="28.37" y2="60.1"/></svg>
|
After Width: | Height: | Size: 2.3 KiB |
1
build/assets/achievements/moreTasks.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><line class="cls-1" x1="50.16" y1="77.43" x2="34.16" y2="77.43"/><polyline class="cls-1" points="82.96 62.98 98.96 62.98 98.96 53.81 34.16 53.81 34.16 77.43"/><polygon class="cls-1" points="98.96 102.61 52.7 102.61 44.7 102.61 34.16 102.61 34.16 92.85 98.96 92.85 98.96 102.61"/><path class="cls-1" d="M40.4,77.43c7.03,9.37,0,15.42,0,15.42"/><path class="cls-1" d="M90.76,62.98c-16.2,17.96,0,29.86,0,29.86"/><path class="cls-1" d="M34.16,73.52l-18.74-10.54c-2.73-1.56-1.37-5.27,1.95-5.27h16.79v15.81Z"/><polygon class="cls-1" points="73 15.55 60.31 19.65 60.31 40.92 73 40.92 73 15.55"/><line class="cls-1" x1="56.22" y1="28.24" x2="60.31" y2="28.24"/><line class="cls-1" x1="73" y1="28.24" x2="102.87" y2="28.24"/><line class="cls-1" x1="38.84" y1="49.9" x2="30.84" y2="41.12"/><line class="cls-1" x1="40.8" y1="44.05" x2="30.84" y2="41.12"/><line class="cls-1" x1="38.65" y1="35.65" x2="40.8" y2="44.05"/><line class="cls-1" x1="45.09" y1="41.31" x2="38.65" y2="35.65"/><line class="cls-1" x1="78.86" y1="46.19" x2="85.3" y2="38.58"/><line class="cls-1" x1="86.86" y1="45.02" x2="85.3" y2="38.58"/><line class="cls-1" x1="95.45" y1="39.36" x2="86.86" y2="45.02"/><line class="cls-1" x1="92.52" y1="49.71" x2="95.45" y2="39.36"/><line class="cls-1" x1="46.46" y1="32.92" x2="45.09" y2="41.31"/><line class="cls-1" x1="53.29" y1="45.61" x2="46.46" y2="32.92"/></svg>
|
After Width: | Height: | Size: 1.6 KiB |
1
build/assets/achievements/shortestName.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><line class="cls-1" x1="66.07" y1="23.55" x2="66.07" y2="58.88"/><line class="cls-1" x1="66.07" y1="64.74" x2="66.07" y2="95.58"/><line class="cls-1" x1="54.56" y1="76.45" x2="54.56" y2="95.58"/><line class="cls-1" x1="42.84" y1="76.45" x2="42.84" y2="95.58"/><line class="cls-1" x1="99.84" y1="31.16" x2="66.07" y2="31.16"/><line class="cls-1" x1="99.84" y1="51.07" x2="66.07" y2="51.07"/><line class="cls-1" x1="99.84" y1="64.74" x2="23.72" y2="64.74"/><circle class="cls-1" cx="42.65" cy="39.75" r="13.86"/><path class="cls-1" d="M79.74,64.74c-1.95,10.74-2.15,27.13,12.49,36.31"/></svg>
|
After Width: | Height: | Size: 829 B |
1
build/assets/achievements/workNotWork.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><circle class="cls-1" cx="63.43" cy="66.41" r="19.33"/><g><line class="cls-1" x1="81.58" y1="90.8" x2="93.49" y2="106.81"/><line class="cls-1" x1="83.54" y1="104.08" x2="86.46" y2="97.64"/><line class="cls-1" x1="90.17" y1="112.67" x2="83.54" y2="104.08"/><line class="cls-1" x1="93.49" y1="106.81" x2="90.17" y2="112.67"/><line class="cls-1" x1="93.69" y1="96.27" x2="86.46" y2="97.64"/><line class="cls-1" x1="100.13" y1="104.86" x2="93.69" y2="96.27"/><line class="cls-1" x1="93.69" y1="106.62" x2="100.13" y2="104.86"/></g><g><line class="cls-1" x1="76.9" y1="52.55" x2="99.35" y2="30.88"/><line class="cls-1" x1="97.79" y1="41.03" x2="90.95" y2="38.88"/><line class="cls-1" x1="105.4" y1="33.42" x2="97.79" y2="41.03"/><line class="cls-1" x1="99.35" y1="30.88" x2="105.4" y2="33.42"/><line class="cls-1" x1="88.81" y1="32.05" x2="90.95" y2="38.88"/><line class="cls-1" x1="96.61" y1="24.44" x2="88.81" y2="32.05"/><line class="cls-1" x1="99.15" y1="30.69" x2="96.61" y2="24.44"/></g><g><line class="cls-1" x1="45.6" y1="58.92" x2="16.59" y2="46.89"/><line class="cls-1" x1="26.74" y1="44.15" x2="27.32" y2="51.38"/><line class="cls-1" x1="16.78" y1="40.25" x2="26.74" y2="44.15"/><line class="cls-1" x1="16.59" y1="46.89" x2="16.78" y2="40.25"/><line class="cls-1" x1="21.86" y1="56.06" x2="27.32" y2="51.38"/><line class="cls-1" x1="11.9" y1="51.77" x2="21.86" y2="56.06"/><line class="cls-1" x1="16.59" y1="47.08" x2="11.9" y2="51.77"/></g><circle class="cls-1" cx="63.43" cy="66.41" r="7.42"/><path class="cls-1" d="M33.29,60.98c-.34,1.81-.5,3.62-.5,5.43,0,16.79,13.86,30.45,30.45,30.45s30.45-13.86,30.45-30.45c0-5.82-1.63-11.64-4.88-16.6"/><path class="cls-1" d="M79.63,40.64c-4.88-3.12-10.54-4.68-16.2-4.68-9.94,0-19.7,5.06-25.23,13.34"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
2
build/assets/ci-cd.sh
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
git pull
|
||||||
|
git --no-pager log --numstat --oneline --all --no-merges --reverse --date=iso-strict --pretty=format:"%ad>%cN>%cE>%s" | sed -e 's/\\/\\\\/g' | sed -e 's/`/"/g' | sed -e 's/^/report.push(\`/g' | sed 's/$/\`\);/g' | sed 's/\$/_/g' > dump.git
|
BIN
build/assets/games/car.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
|
@ -1 +1 @@
|
||||||
<!doctype html><html lang="ru"><head><meta name="viewport" content="width=device-width,height=device-height,initial-scale=1,user-scalable=no,maximum-scale=1"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta http-equiv="Cache-Control" content="no-cache"><meta http-equiv="cleartype" content="on"><meta name="HandheldFriendly" content="True"><meta name="format-detection" content="telephone=no"><meta name="format-detection" content="address=no"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><script type="text/javascript">var report=[]</script><script src="/dump.git"></script><script src="./dump.git"></script><script src="../dump.git"></script><script src="./dump-0.git"></script><script src="./dump-1.git"></script><script src="./dump-2.git"></script><script src="./dump-3.git"></script><script src="./dump-4.git"></script><script src="./dump-5.git"></script><script src="./dump-6.git"></script><script src="./report/dump-0.git"></script><script src="./report/dump-1.git"></script><script src="./report/dump-2.git"></script><script src="./report/dump-3.git"></script><script src="./report/dump-4.git"></script><script src="./report/dump-5.git"></script><script src="./report/dump-6.git"></script><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>ASSAYO</title><meta name="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="keywords" content="git, статистика, аудит, история, log, мониторинг, контроль сотрудников"><meta name="author" content="Bakhirev Aleksei"><meta name="copyright" content="(c) Bakhirev Aleksei"><meta http-equiv="Reply-to" content="alexey-bakhirev@yandex.ru"><meta name="application-name" content="GIT Статистика"><meta name="msapplication-tooltip" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:title" content="GIT Статистика"><meta property="og:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta property="og:site_name" content="Assayo"><meta property="og:url" content="http://assayo.jp/"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="GIT Статистика"><meta name="twitter:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="twitter:creator" content="Bakhirev Aleksei"><meta name="twitter:image:src" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta name="twitter:domain" content="assayo.jp"><meta name="twitter:site" content="assayo.jp"><meta itemprop="name" content="GIT Статистика"><meta itemprop="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta itemprop="image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><script defer="defer" src="./static/js/main.405a9477.js"></script><link href="./static/css/main.33cc195e.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
<!doctype html><html lang="ru"><head><meta name="viewport" content="width=device-width,height=device-height,initial-scale=1,user-scalable=no,maximum-scale=1"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta http-equiv="Cache-Control" content="no-cache"><meta http-equiv="cleartype" content="on"><meta name="HandheldFriendly" content="True"><meta name="format-detection" content="telephone=no"><meta name="format-detection" content="address=no"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><script type="text/javascript">var report=[]</script><script src="/dump.git"></script><script src="./dump.git"></script><script src="../dump.git"></script><script src="./dump-0.git"></script><script src="./dump-1.git"></script><script src="./dump-2.git"></script><script src="./dump-3.git"></script><script src="./dump-4.git"></script><script src="./dump-5.git"></script><script src="./dump-6.git"></script><script src="./report/dump-0.git"></script><script src="./report/dump-1.git"></script><script src="./report/dump-2.git"></script><script src="./report/dump-3.git"></script><script src="./report/dump-4.git"></script><script src="./report/dump-5.git"></script><script src="./report/dump-6.git"></script><link rel="icon" href="./favicon.svg"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>ASSAYO</title><meta name="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="keywords" content="git, статистика, аудит, история, log, мониторинг, контроль сотрудников"><meta name="author" content="Bakhirev Aleksei"><meta name="copyright" content="(c) Bakhirev Aleksei"><meta http-equiv="Reply-to" content="alexey-bakhirev@yandex.ru"><meta name="application-name" content="GIT Статистика"><meta name="msapplication-tooltip" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:title" content="GIT Статистика"><meta property="og:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta property="og:image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta property="og:site_name" content="Assayo"><meta property="og:url" content="http://assayo.jp/"><meta name="twitter:card" content="summary"><meta name="twitter:title" content="GIT Статистика"><meta name="twitter:description" content="Простой и быстрый отчёт по истории коммитов в git."><meta name="twitter:creator" content="Bakhirev Aleksei"><meta name="twitter:image:src" content="http://assayo.jp/assets/seo/custom_icon_256.png"><meta name="twitter:domain" content="assayo.jp"><meta name="twitter:site" content="assayo.jp"><meta itemprop="name" content="GIT Статистика"><meta itemprop="description" content="Простой и быстрый отчёт по истории коммитов в git."><meta itemprop="image" content="http://assayo.jp/assets/seo/custom_icon_256.png"><script defer="defer" src="./static/js/main.fcc567df.js"></script><link href="./static/css/main.27b68202.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
2
build/static/css/main.27b68202.css
Normal file
1
build/static/css/main.27b68202.css.map
Normal file
3
build/static/js/main.fcc567df.js
Normal file
74
build/static/js/main.fcc567df.js.LICENSE.txt
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* react-dom.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* react-jsx-runtime.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* react.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* scheduler.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @remix-run/router v1.3.1
|
||||||
|
*
|
||||||
|
* Copyright (c) Remix Software Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE.md file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React Router DOM v6.8.0
|
||||||
|
*
|
||||||
|
* Copyright (c) Remix Software Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE.md file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React Router v6.8.0
|
||||||
|
*
|
||||||
|
* Copyright (c) Remix Software Inc.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE.md file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @license MIT
|
||||||
|
*/
|
1
build/static/js/main.fcc567df.js.map
Normal file
BIN
build/static/media/car.b8dd8738e37fe866285f.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
|
@ -1 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><path class="cls-1" d="M60.51,27.13L22.64,88.62c-1.76,2.93,.39,6.83,3.9,6.83H102.28c3.51,0,5.66-3.9,3.9-6.83L68.32,27.13c-1.76-2.73-5.86-2.73-7.81,0Z"/><rect class="cls-1" x="61.42" y="41.65" width="5.86" height="32.28"/><rect class="cls-1" x="61.45" y="80.91" width="5.86" height="5.86"/></svg>
|
|
Before Width: | Height: | Size: 535 B |
|
@ -1 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><g><line class="cls-1" x1="82.18" y1="89.01" x2="94.08" y2="105.01"/><line class="cls-1" x1="84.13" y1="102.28" x2="87.06" y2="95.84"/><line class="cls-1" x1="90.76" y1="110.87" x2="84.13" y2="102.28"/><line class="cls-1" x1="94.08" y1="105.01" x2="90.76" y2="110.87"/><line class="cls-1" x1="94.28" y1="94.47" x2="87.06" y2="95.84"/><line class="cls-1" x1="100.72" y1="103.06" x2="94.28" y2="94.47"/><line class="cls-1" x1="94.28" y1="104.82" x2="100.72" y2="103.06"/></g><g><line class="cls-1" x1="77.49" y1="50.75" x2="99.94" y2="29.08"/><line class="cls-1" x1="98.38" y1="39.23" x2="91.55" y2="37.09"/><line class="cls-1" x1="105.99" y1="31.62" x2="98.38" y2="39.23"/><line class="cls-1" x1="99.94" y1="29.08" x2="105.99" y2="31.62"/><line class="cls-1" x1="89.4" y1="30.25" x2="91.55" y2="37.09"/><line class="cls-1" x1="97.21" y1="22.64" x2="89.4" y2="30.25"/><line class="cls-1" x1="99.74" y1="28.89" x2="97.21" y2="22.64"/></g><g><line class="cls-1" x1="57.19" y1="61.68" x2="17.18" y2="45.09"/><line class="cls-1" x1="27.33" y1="42.36" x2="27.91" y2="49.58"/><line class="cls-1" x1="17.37" y1="38.45" x2="27.33" y2="42.36"/><line class="cls-1" x1="17.18" y1="45.09" x2="17.37" y2="38.45"/><line class="cls-1" x1="22.45" y1="54.26" x2="27.91" y2="49.58"/><line class="cls-1" x1="12.49" y1="49.97" x2="22.45" y2="54.26"/><line class="cls-1" x1="17.18" y1="45.28" x2="12.49" y2="49.97"/></g><circle class="cls-1" cx="64.02" cy="64.61" r="7.42"/><path class="cls-1" d="M44.89,60.9c-.2,1.17-.39,2.54-.39,3.71,0,10.74,8.78,19.32,19.32,19.32s19.32-8.78,19.32-19.32-8.78-19.32-19.32-19.32c-6.44,0-12.49,3.12-16.01,8.39"/><path class="cls-1" d="M33.96,58.75c-.39,1.95-.59,3.9-.59,5.86,0,16.79,13.86,30.45,30.45,30.45s30.45-13.86,30.45-30.45c0-6.05-1.76-12.1-5.27-17.18"/><path class="cls-1" d="M80.22,38.84c-4.88-3.12-10.54-4.68-16.2-4.68-10.15,0-20.1,5.27-25.57,13.86"/></svg>
|
|
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 725 B |
1
public/assets/achievements/moreRefactoring.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><line class="cls-1" x1="84.97" y1="58.68" x2="84.97" y2="24.47"/><line class="cls-1" x1="43.92" y1="58.68" x2="43.92" y2="34.88"/><line class="cls-1" x1="54.97" y1="24.47" x2="84.97" y2="24.47"/><line class="cls-1" x1="54.6" y1="38.3" x2="74.28" y2="38.3"/><line class="cls-1" x1="54.6" y1="46.78" x2="74.28" y2="46.78"/><line class="cls-1" x1="54.6" y1="55.26" x2="74.28" y2="55.26"/><line class="cls-1" x1="54.97" y1="24.47" x2="43.92" y2="34.88"/><line class="cls-1" x1="35.29" y1="64.88" x2="48.76" y2="64.88"/><line class="cls-1" x1="80.12" y1="64.88" x2="93.6" y2="64.88"/><line class="cls-1" x1="63.23" y1="80.85" x2="65.66" y2="80.85"/><line class="cls-1" x1="73.07" y1="80.85" x2="75.5" y2="80.85"/><line class="cls-1" x1="82.9" y1="80.85" x2="85.33" y2="80.85"/><line class="cls-1" x1="43.56" y1="80.85" x2="45.99" y2="80.85"/><line class="cls-1" x1="53.39" y1="80.85" x2="55.83" y2="80.85"/><line class="cls-1" x1="58.31" y1="86.2" x2="60.74" y2="86.2"/><line class="cls-1" x1="68.14" y1="86.2" x2="70.58" y2="86.2"/><line class="cls-1" x1="77.98" y1="86.2" x2="80.41" y2="86.2"/><line class="cls-1" x1="87.82" y1="86.2" x2="90.25" y2="86.2"/><line class="cls-1" x1="38.64" y1="86.2" x2="41.07" y2="86.2"/><line class="cls-1" x1="48.47" y1="86.2" x2="50.9" y2="86.2"/><path class="cls-1" d="M80.12,64.88c0,3.37-7.08,6.15-15.68,6.15s-15.4-2.65-15.67-5.95"/><line class="cls-1" x1="43.92" y1="55.26" x2="35.29" y2="55.26"/><line class="cls-1" x1="93.6" y1="55.26" x2="84.97" y2="55.26"/><line class="cls-1" x1="35.29" y1="75.94" x2="35.29" y2="55.26"/><line class="cls-1" x1="28.37" y1="71.03" x2="28.37" y2="60.69"/><line class="cls-1" x1="93.6" y1="75.94" x2="93.6" y2="55.26"/><rect class="cls-1" x="93.6" y="60.1" width="6.62" height="10.93"/><line class="cls-1" x1="35.29" y1="75.94" x2="93.6" y2="75.94"/><rect class="cls-1" x="24.62" y="91.55" width="79.65" height="11.16"/><line class="cls-1" x1="35.29" y1="75.94" x2="24.62" y2="91.55"/><line class="cls-1" x1="28.37" y1="71.03" x2="23.04" y2="78.84"/><line class="cls-1" x1="93.6" y1="75.94" x2="104.27" y2="91.55"/><line class="cls-1" x1="35.29" y1="60.1" x2="28.37" y2="60.1"/></svg>
|
After Width: | Height: | Size: 2.3 KiB |
1
public/assets/achievements/moreTasks.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><line class="cls-1" x1="50.16" y1="77.43" x2="34.16" y2="77.43"/><polyline class="cls-1" points="82.96 62.98 98.96 62.98 98.96 53.81 34.16 53.81 34.16 77.43"/><polygon class="cls-1" points="98.96 102.61 52.7 102.61 44.7 102.61 34.16 102.61 34.16 92.85 98.96 92.85 98.96 102.61"/><path class="cls-1" d="M40.4,77.43c7.03,9.37,0,15.42,0,15.42"/><path class="cls-1" d="M90.76,62.98c-16.2,17.96,0,29.86,0,29.86"/><path class="cls-1" d="M34.16,73.52l-18.74-10.54c-2.73-1.56-1.37-5.27,1.95-5.27h16.79v15.81Z"/><polygon class="cls-1" points="73 15.55 60.31 19.65 60.31 40.92 73 40.92 73 15.55"/><line class="cls-1" x1="56.22" y1="28.24" x2="60.31" y2="28.24"/><line class="cls-1" x1="73" y1="28.24" x2="102.87" y2="28.24"/><line class="cls-1" x1="38.84" y1="49.9" x2="30.84" y2="41.12"/><line class="cls-1" x1="40.8" y1="44.05" x2="30.84" y2="41.12"/><line class="cls-1" x1="38.65" y1="35.65" x2="40.8" y2="44.05"/><line class="cls-1" x1="45.09" y1="41.31" x2="38.65" y2="35.65"/><line class="cls-1" x1="78.86" y1="46.19" x2="85.3" y2="38.58"/><line class="cls-1" x1="86.86" y1="45.02" x2="85.3" y2="38.58"/><line class="cls-1" x1="95.45" y1="39.36" x2="86.86" y2="45.02"/><line class="cls-1" x1="92.52" y1="49.71" x2="95.45" y2="39.36"/><line class="cls-1" x1="46.46" y1="32.92" x2="45.09" y2="41.31"/><line class="cls-1" x1="53.29" y1="45.61" x2="46.46" y2="32.92"/></svg>
|
After Width: | Height: | Size: 1.6 KiB |
1
public/assets/achievements/shortestName.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><line class="cls-1" x1="66.07" y1="23.55" x2="66.07" y2="58.88"/><line class="cls-1" x1="66.07" y1="64.74" x2="66.07" y2="95.58"/><line class="cls-1" x1="54.56" y1="76.45" x2="54.56" y2="95.58"/><line class="cls-1" x1="42.84" y1="76.45" x2="42.84" y2="95.58"/><line class="cls-1" x1="99.84" y1="31.16" x2="66.07" y2="31.16"/><line class="cls-1" x1="99.84" y1="51.07" x2="66.07" y2="51.07"/><line class="cls-1" x1="99.84" y1="64.74" x2="23.72" y2="64.74"/><circle class="cls-1" cx="42.65" cy="39.75" r="13.86"/><path class="cls-1" d="M79.74,64.74c-1.95,10.74-2.15,27.13,12.49,36.31"/></svg>
|
After Width: | Height: | Size: 829 B |
1
public/assets/achievements/workNotWork.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.83 128.83"><defs><style>.cls-1{fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:2.93px;}</style></defs><circle class="cls-1" cx="63.43" cy="66.41" r="19.33"/><g><line class="cls-1" x1="81.58" y1="90.8" x2="93.49" y2="106.81"/><line class="cls-1" x1="83.54" y1="104.08" x2="86.46" y2="97.64"/><line class="cls-1" x1="90.17" y1="112.67" x2="83.54" y2="104.08"/><line class="cls-1" x1="93.49" y1="106.81" x2="90.17" y2="112.67"/><line class="cls-1" x1="93.69" y1="96.27" x2="86.46" y2="97.64"/><line class="cls-1" x1="100.13" y1="104.86" x2="93.69" y2="96.27"/><line class="cls-1" x1="93.69" y1="106.62" x2="100.13" y2="104.86"/></g><g><line class="cls-1" x1="76.9" y1="52.55" x2="99.35" y2="30.88"/><line class="cls-1" x1="97.79" y1="41.03" x2="90.95" y2="38.88"/><line class="cls-1" x1="105.4" y1="33.42" x2="97.79" y2="41.03"/><line class="cls-1" x1="99.35" y1="30.88" x2="105.4" y2="33.42"/><line class="cls-1" x1="88.81" y1="32.05" x2="90.95" y2="38.88"/><line class="cls-1" x1="96.61" y1="24.44" x2="88.81" y2="32.05"/><line class="cls-1" x1="99.15" y1="30.69" x2="96.61" y2="24.44"/></g><g><line class="cls-1" x1="45.6" y1="58.92" x2="16.59" y2="46.89"/><line class="cls-1" x1="26.74" y1="44.15" x2="27.32" y2="51.38"/><line class="cls-1" x1="16.78" y1="40.25" x2="26.74" y2="44.15"/><line class="cls-1" x1="16.59" y1="46.89" x2="16.78" y2="40.25"/><line class="cls-1" x1="21.86" y1="56.06" x2="27.32" y2="51.38"/><line class="cls-1" x1="11.9" y1="51.77" x2="21.86" y2="56.06"/><line class="cls-1" x1="16.59" y1="47.08" x2="11.9" y2="51.77"/></g><circle class="cls-1" cx="63.43" cy="66.41" r="7.42"/><path class="cls-1" d="M33.29,60.98c-.34,1.81-.5,3.62-.5,5.43,0,16.79,13.86,30.45,30.45,30.45s30.45-13.86,30.45-30.45c0-5.82-1.63-11.64-4.88-16.6"/><path class="cls-1" d="M79.63,40.64c-4.88-3.12-10.54-4.68-16.2-4.68-9.94,0-19.7,5.06-25.23,13.34"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
2
public/assets/ci-cd.sh
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
git pull
|
||||||
|
git --no-pager log --numstat --oneline --all --no-merges --reverse --date=iso-strict --pretty=format:"%ad>%cN>%cE>%s" | sed -e 's/\\/\\\\/g' | sed -e 's/`/"/g' | sed -e 's/^/report.push(\`/g' | sed 's/$/\`\);/g' | sed 's/\$/_/g' > dump.git
|
BIN
public/assets/games/car.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
src/assets/games/car.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
src/assets/games/tv100and1.png
Normal file
After Width: | Height: | Size: 156 B |
|
@ -25,6 +25,8 @@ function getParametersFromString(text: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderReactApplication() {
|
function renderReactApplication() {
|
||||||
|
// @ts-ignore
|
||||||
|
console.log(window?.report?.length);
|
||||||
render(
|
render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
padding: 16px 0 0 90px;
|
padding: 23px 0 0 90px;
|
||||||
margin: 0 10px 10px 0;
|
margin: 0 10px 10px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
|
83
src/ts/components/Console/index.module.scss
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.console {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&_header,
|
||||||
|
&_body {
|
||||||
|
font-size: var(--font-s);
|
||||||
|
font-weight: 100;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_header {
|
||||||
|
display: block;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 8px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #8F8F8F;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid #D4D4D4;
|
||||||
|
border-bottom: none;
|
||||||
|
cursor: default;
|
||||||
|
background-color: #F2F2F2;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
|
||||||
|
&_icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: 6px 8px 0 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #B5B5B5;
|
||||||
|
background: linear-gradient(90deg, #D7D8DB 0%, #B5B5B5 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&_body {
|
||||||
|
min-height: 250px;
|
||||||
|
padding: 8px 16px 16px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: #00B200;
|
||||||
|
white-space: normal;
|
||||||
|
background-color: #0C0C0C;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_copy {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 16px;
|
||||||
|
right: 16px;
|
||||||
|
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 100;
|
||||||
|
display: block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
line-height: 13px;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
color: #8F8F8F;
|
||||||
|
border: 1px solid #F2F2F2;
|
||||||
|
background-color: #F2F2F2;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
bottom: 15px;
|
||||||
|
right: 15px;
|
||||||
|
background-color: #EDEDED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
src/ts/components/Console/index.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import style from './index.module.scss';
|
||||||
|
|
||||||
|
interface IConsoleProps {
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Console({ className, children }: IConsoleProps) {
|
||||||
|
return (
|
||||||
|
<div className={`${style.console} ${className || ''}`}>
|
||||||
|
<div className={`${style.console_header}`}>
|
||||||
|
<span className={`${style.console_header_icon}`}></span>
|
||||||
|
<span className={`${style.console_header_icon}`}></span>
|
||||||
|
<span className={`${style.console_header_icon}`}></span>
|
||||||
|
</div>
|
||||||
|
<div className={`${style.console_body}`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<button className={`${style.console_copy}`}>
|
||||||
|
Копировать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.defaultProps = {
|
||||||
|
children: undefined,
|
||||||
|
className: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Console;
|
|
@ -45,7 +45,8 @@
|
||||||
font-size: var(--font-xs);
|
font-size: var(--font-xs);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 0 var(--space-xs) 0;
|
margin: 0 0 var(--space-xs) 0;
|
||||||
line-height: var(--font-s);
|
line-height: var(--font-m);
|
||||||
|
vertical-align: top;
|
||||||
color: var(--color-42);
|
color: var(--color-42);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +54,10 @@
|
||||||
margin-right: var(--space-l);
|
margin-right: var(--space-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_message {
|
||||||
|
width: calc(100% - 44px);
|
||||||
|
}
|
||||||
|
|
||||||
&_row {
|
&_row {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0 0 0 32px;
|
padding: 0 0 0 32px;
|
||||||
|
|
|
@ -47,22 +47,35 @@ function TaskInfo({ tasks }: { tasks: ITask }): React.ReactElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IDayInfoProps {
|
interface IDayInfoProps {
|
||||||
day: IDayInfo,
|
day: IDayInfo;
|
||||||
order: string[]
|
order: string[];
|
||||||
|
events?: any;
|
||||||
|
timestamp?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DayInfo({ day, order }: IDayInfoProps): React.ReactElement {
|
function DayInfo({ day, order, events, timestamp }: IDayInfoProps): React.ReactElement {
|
||||||
|
const firstCommit = events?.firstCommit?.[timestamp || ''] || [];
|
||||||
|
const lastCommit = events?.lastCommit?.[timestamp || ''] || [];
|
||||||
let taskNumber = 0;
|
let taskNumber = 0;
|
||||||
|
console.dir(firstCommit);
|
||||||
|
|
||||||
const items = Object.entries(day?.tasksByAuthor)
|
const items = Object.entries(day?.tasksByAuthor)
|
||||||
.sort((a: any, b: any) => (order.indexOf(a[0]) - order.indexOf(b[0])))
|
.sort((a: any, b: any) => (order.indexOf(a[0]) - order.indexOf(b[0])))
|
||||||
.map(([author, tasks]: [string, any]) => {
|
.map(([author, tasks]: [string, any]) => {
|
||||||
taskNumber += Object.keys(tasks).length;
|
taskNumber += Object.keys(tasks).length;
|
||||||
|
|
||||||
|
let suffix = '';
|
||||||
|
if (firstCommit.includes(author)) suffix = '(первый рабочий день)';
|
||||||
|
if (lastCommit.includes(author)) suffix = '(последний рабочий день)';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={author}
|
key={author}
|
||||||
className={style.day_info}
|
className={style.day_info}
|
||||||
>
|
>
|
||||||
<h3 className={style.day_info_author}>{author}</h3>
|
<h3 className={style.day_info_author}>
|
||||||
|
{`${author} ${suffix}`}
|
||||||
|
</h3>
|
||||||
<TaskInfo tasks={tasks}/>
|
<TaskInfo tasks={tasks}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -78,4 +91,9 @@ function DayInfo({ day, order }: IDayInfoProps): React.ReactElement {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DayInfo.defaultProps = {
|
||||||
|
events: undefined,
|
||||||
|
timestamp: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
export default DayInfo;
|
export default DayInfo;
|
||||||
|
|
19
src/ts/components/Extension/components/Icon.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import style from '../styles/index.module.scss';
|
||||||
|
|
||||||
|
interface IExtensionIconProps {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExtensionIcon({
|
||||||
|
title,
|
||||||
|
}: IExtensionIconProps): React.ReactElement | null {
|
||||||
|
return (
|
||||||
|
<div className={style.extension_icon}>
|
||||||
|
{title || ''}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExtensionIcon;
|
28
src/ts/components/Extension/components/Line.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import style from '../styles/index.module.scss';
|
||||||
|
|
||||||
|
interface IExtensionLineProps {
|
||||||
|
title: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ExtensionLine({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
}: IExtensionLineProps): React.ReactElement | null {
|
||||||
|
if (!value || !title) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.extension_line}>
|
||||||
|
<div className={style.extension_line_title}>
|
||||||
|
{title || ''}
|
||||||
|
</div>
|
||||||
|
<div className={style.extension_line_value}>
|
||||||
|
{value || ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExtensionLine;
|
44
src/ts/components/Extension/index.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Icon from './components/Icon';
|
||||||
|
import Line from './components/Line';
|
||||||
|
import style from './styles/index.module.scss';
|
||||||
|
|
||||||
|
interface IExtensionProps {
|
||||||
|
statistic: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Extension({
|
||||||
|
statistic,
|
||||||
|
}: IExtensionProps): React.ReactElement | null {
|
||||||
|
if (!statistic) return null;
|
||||||
|
|
||||||
|
const getValue = (more: any) => `${more.author} (${more.percent.toFixed(1)}%)`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.extension}>
|
||||||
|
<Icon title={statistic.extension} />
|
||||||
|
<h6>
|
||||||
|
Чаще всего
|
||||||
|
</h6>
|
||||||
|
<Line
|
||||||
|
title="Добавляет:"
|
||||||
|
value={getValue(statistic.more.added)}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
title="Меняет:"
|
||||||
|
value={getValue(statistic.more.changes)}
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
title="Удаляет:"
|
||||||
|
value={getValue(statistic.more.removed)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Extension.defaultProps = {
|
||||||
|
rows: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Extension;
|
57
src/ts/components/Extension/styles/index.module.scss
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
@import '../../../../styles/variables';
|
||||||
|
|
||||||
|
.extension {
|
||||||
|
display: inline-block;
|
||||||
|
width: 300px;
|
||||||
|
margin: 0 0 24px 24px;
|
||||||
|
padding: 24px;
|
||||||
|
vertical-align: top;
|
||||||
|
border-radius: var(--border-radius-l);
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
&_icon {
|
||||||
|
display: block;
|
||||||
|
width: 64px;
|
||||||
|
margin: 0 auto 24px auto;
|
||||||
|
padding: 24px 0;
|
||||||
|
|
||||||
|
font-size: var(--font-l);
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
border-radius: 32px;
|
||||||
|
background-color: var(--color-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_line {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&_title,
|
||||||
|
&_value {
|
||||||
|
font-weight: 100;
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
line-height: var(--font-m);
|
||||||
|
text-decoration: none;
|
||||||
|
vertical-align: top;
|
||||||
|
white-space: normal;
|
||||||
|
|
||||||
|
color: var(--color-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_title {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
src/ts/components/Notifications/components/Message.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import IMessage from '../interfaces/Message';
|
||||||
|
import style from '../styles/index.module.scss';
|
||||||
|
|
||||||
|
interface ICardProps {
|
||||||
|
message: IMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
function Card({ message }: ICardProps) {
|
||||||
|
const className = {
|
||||||
|
error: style.notifications_item_error,
|
||||||
|
warning: style.notifications_item_warning,
|
||||||
|
success: style.notifications_item_success,
|
||||||
|
info: style.notifications_item_info,
|
||||||
|
}[message.type || 'success'] || style.notifications_item_info;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${style.notifications_item} ${className}`}>
|
||||||
|
{message.title && (
|
||||||
|
<h6 className={style.notifications_item_title}>
|
||||||
|
{message.title}
|
||||||
|
</h6>
|
||||||
|
)}
|
||||||
|
{message.description && (
|
||||||
|
<p className={style.notifications_item_description}>
|
||||||
|
{message.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Card;
|
26
src/ts/components/Notifications/index.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
import Message from './components/Message';
|
||||||
|
import IMessage from './interfaces/Message';
|
||||||
|
import notificationsStore from './store/index';
|
||||||
|
import style from './styles/index.module.scss';
|
||||||
|
|
||||||
|
|
||||||
|
const Notifications = observer(() => {
|
||||||
|
const items = notificationsStore.messages.map((message: IMessage) => (
|
||||||
|
<Message
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
return ReactDOM.createPortal((
|
||||||
|
<div className={style.notifications}>
|
||||||
|
{items}
|
||||||
|
</div>
|
||||||
|
), document.body);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Notifications;
|
6
src/ts/components/Notifications/interfaces/Message.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default interface IMessage {
|
||||||
|
id?: number;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
type?: 'error' | 'warning' | 'success' | 'info';
|
||||||
|
}
|
48
src/ts/components/Notifications/store/index.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { makeObservable, observable, action } from 'mobx';
|
||||||
|
|
||||||
|
import IMessage from '../interfaces/Message';
|
||||||
|
|
||||||
|
interface INotificationsStore {
|
||||||
|
timer: any;
|
||||||
|
messages: IMessage[];
|
||||||
|
show: Function;
|
||||||
|
startClearTimer: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationsStore implements INotificationsStore {
|
||||||
|
timer: any = null;
|
||||||
|
|
||||||
|
messages: IMessage[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeObservable(this, {
|
||||||
|
messages: observable,
|
||||||
|
show: action,
|
||||||
|
startClearTimer: action,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
show(message?: any) {
|
||||||
|
this.messages.push({
|
||||||
|
id: Math.random(),
|
||||||
|
title: message?.title || message || 'Изменения сохранены',
|
||||||
|
description: message?.description || '',
|
||||||
|
type: message?.type || 'success',
|
||||||
|
});
|
||||||
|
this.startClearTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
startClearTimer() {
|
||||||
|
if (this.timer) return;
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.messages.shift();
|
||||||
|
if (this.messages.length) return;
|
||||||
|
clearInterval(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}, 3500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationsStore = new NotificationsStore();
|
||||||
|
|
||||||
|
export default notificationsStore;
|
55
src/ts/components/Notifications/styles/index.module.scss
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
@import '../../../../styles/variables';
|
||||||
|
|
||||||
|
.notifications {
|
||||||
|
position: fixed;
|
||||||
|
top: 12px;
|
||||||
|
right: 0;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&_item {
|
||||||
|
display: block;
|
||||||
|
width: 250px;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 0 12px 12px;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: 2px 2px 3px var(--color-grey);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&_error {
|
||||||
|
background-color: var(--color-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_warning {
|
||||||
|
background-color: var(--color-32);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_success {
|
||||||
|
background-color: var(--color-13);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_info {
|
||||||
|
background-color: #F1F4FC;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_title,
|
||||||
|
&_description {
|
||||||
|
font-size: var(--font-s);
|
||||||
|
padding: 0;
|
||||||
|
text-align: left;
|
||||||
|
color: var(--color-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_title {
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_description {
|
||||||
|
font-weight: 100;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/ts/components/Races/Info.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import style from './index.module.scss';
|
||||||
|
|
||||||
|
interface IInfoProps {
|
||||||
|
title: string;
|
||||||
|
duration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Info({
|
||||||
|
title,
|
||||||
|
duration,
|
||||||
|
}: IInfoProps): React.ReactElement | null {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={style.races_track_info}
|
||||||
|
style={{
|
||||||
|
animationDelay: `${duration + 1}s`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Info;
|
77
src/ts/components/Races/Track.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { shuffle } from './index';
|
||||||
|
import Info from './Info';
|
||||||
|
import style from './index.module.scss';
|
||||||
|
|
||||||
|
const DURATION = {
|
||||||
|
MIN: 5,
|
||||||
|
BASE: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const animations = [
|
||||||
|
'ease',
|
||||||
|
'ease-in',
|
||||||
|
'ease-out',
|
||||||
|
'ease-in-out',
|
||||||
|
'linear',
|
||||||
|
'cubic-bezier(0.1, 0.7, 1, 0.1)',
|
||||||
|
];
|
||||||
|
|
||||||
|
function getRandom(max: number) {
|
||||||
|
return Math.floor(Math.random() * (max - 0 + 1)) + 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITrackProps {
|
||||||
|
title: string;
|
||||||
|
speed: number;
|
||||||
|
type?: string;
|
||||||
|
canStart?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Track({
|
||||||
|
title,
|
||||||
|
speed,
|
||||||
|
type,
|
||||||
|
canStart,
|
||||||
|
}: ITrackProps): React.ReactElement | null {
|
||||||
|
const modeIndex = getRandom(animations.length - 1);
|
||||||
|
const [mode] = useState<string>(animations[modeIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
shuffle(animations);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!title) return null;
|
||||||
|
const duration = DURATION.MIN + (DURATION.BASE * (1 - speed)) * 3;
|
||||||
|
const classForMove = canStart ? style.races_track_animation : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${style.races_track} ${type || ''}`}>
|
||||||
|
{canStart && (
|
||||||
|
<Info
|
||||||
|
title={title}
|
||||||
|
duration={duration}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`${style.races_track_car} ${type || ''} ${classForMove || ''}`}
|
||||||
|
style={{
|
||||||
|
animationTimingFunction: mode,
|
||||||
|
animationDuration: `${duration}s`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={`${style.races_track_car_title} ${type || ''}`}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Track.defaultProps = {
|
||||||
|
type: '',
|
||||||
|
canStart: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Track;
|
106
src/ts/components/Races/index.module.scss
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.races {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto var(--space-xxl);
|
||||||
|
border: var(--space-xs) solid var(--color-border);
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
|
||||||
|
&_track {
|
||||||
|
position: relative;
|
||||||
|
height: 70px;
|
||||||
|
padding: var(--space-xxs);
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
text-align: right;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
background-color: var(--color-grey);
|
||||||
|
|
||||||
|
&_car {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 10%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background-image: url('../../../assets/games/car.png');
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: auto 70%;
|
||||||
|
|
||||||
|
&_title {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--space-xss);
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
border: 1px solid var(--color-grey);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
color: var(--color-black);
|
||||||
|
background-color: var(--color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&_info {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
width: 10%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 12%;
|
||||||
|
padding: var(--space-m);
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
border-radius: var(--border-radius-l);
|
||||||
|
border: 1px solid var(--color-grey);
|
||||||
|
color: var(--color-black);
|
||||||
|
background-color: var(--color-border);
|
||||||
|
|
||||||
|
animation-name: races_track_info;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
animation-duration: 1s;
|
||||||
|
animation-direction: alternate;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_animation {
|
||||||
|
animation-name: races_track_car;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
animation-direction: alternate;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&_button {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-l);
|
||||||
|
left: calc(50% - 100px);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes races_track_car {
|
||||||
|
from {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
left: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes races_track_info {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
57
src/ts/components/Races/index.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import UiKitButton from 'ts/components/UiKit/components/Button';
|
||||||
|
|
||||||
|
import Track from './Track';
|
||||||
|
import style from './index.module.scss';
|
||||||
|
|
||||||
|
export function shuffle(items: any[]) {
|
||||||
|
// @ts-ignore
|
||||||
|
for (let j, x, i = items.length; i; j = parseInt(Math.random() * i), x = items[--i], items[i] = items[j], items[j] = x) {}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRacesProps {
|
||||||
|
tracks: {
|
||||||
|
title: string,
|
||||||
|
speed: number,
|
||||||
|
type?: string,
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Races({
|
||||||
|
tracks,
|
||||||
|
}: IRacesProps): React.ReactElement | null {
|
||||||
|
const [showAnimation, setShowAnimation] = useState<boolean>(false);
|
||||||
|
|
||||||
|
if (!tracks.length) return null;
|
||||||
|
|
||||||
|
const lines = shuffle(tracks).map((track: any) => {
|
||||||
|
return (
|
||||||
|
<Track
|
||||||
|
key={track.title}
|
||||||
|
title={track.title}
|
||||||
|
speed={track.speed}
|
||||||
|
canStart={showAnimation}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.races}>
|
||||||
|
{!showAnimation && (
|
||||||
|
<UiKitButton
|
||||||
|
className={style.races_button}
|
||||||
|
onClick={() => {
|
||||||
|
setShowAnimation(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Поехали
|
||||||
|
</UiKitButton>
|
||||||
|
)}
|
||||||
|
{lines}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Races;
|
62
src/ts/components/SplashScreen/index.module.scss
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.splash_screen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--color-black);
|
||||||
|
|
||||||
|
animation: splash_screen .5s linear 5.5s forwards;
|
||||||
|
|
||||||
|
&_container {
|
||||||
|
display: block;
|
||||||
|
width: 300px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
animation: splash_screen_container 1s linear 4s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_description {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 100;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-transform: none;
|
||||||
|
color: #404148;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes splash_screen {
|
||||||
|
from {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
bottom: 100%;
|
||||||
|
left: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes splash_screen_container {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
56
src/ts/components/SplashScreen/index.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import Logo from 'ts/pages/PageWrapper/components/sidebar/Logo';
|
||||||
|
|
||||||
|
import style from './index.module.scss';
|
||||||
|
import progress from './progress.module.scss';
|
||||||
|
|
||||||
|
const TEXT: string[] = [
|
||||||
|
'обработка файлов',
|
||||||
|
'обработка коммитов',
|
||||||
|
'нормализация данных',
|
||||||
|
'анализ времени',
|
||||||
|
'анализ состава команды',
|
||||||
|
'оценка стоимости проекта',
|
||||||
|
'оценка затрат на разработку',
|
||||||
|
'расчёт общих рекомендаций',
|
||||||
|
'расчёт частных рекомендаций',
|
||||||
|
'аудит суммарных затрат',
|
||||||
|
'расчёт персональных ачивок',
|
||||||
|
'анализ эффективности',
|
||||||
|
].reverse();
|
||||||
|
|
||||||
|
function SplashScreen(): React.ReactElement | null {
|
||||||
|
const [timer, setTimer] = useState<any>(null);
|
||||||
|
const [index, setIndex] = useState<number>(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (timer) return;
|
||||||
|
|
||||||
|
let localIndex = 0;
|
||||||
|
setTimer(setInterval(() => {
|
||||||
|
localIndex = localIndex === 0
|
||||||
|
? TEXT.length - 1
|
||||||
|
: localIndex - 1;
|
||||||
|
setIndex(localIndex);
|
||||||
|
}, 200));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.splash_screen}>
|
||||||
|
<div className={style.splash_screen_container}>
|
||||||
|
<Logo />
|
||||||
|
<div className={progress.progress_bar}></div>
|
||||||
|
<p className={style.splash_screen_description}>
|
||||||
|
{TEXT[index] || ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SplashScreen;
|
69
src/ts/components/SplashScreen/progress.module.scss
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
.progress_bar {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 16px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
background-color: #404148;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
font-size: 0.8em;
|
||||||
|
display: block;
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
padding-top: 0.1em;
|
||||||
|
|
||||||
|
text-align: right;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
background-color: green;
|
||||||
|
background-size: 23px 22px;
|
||||||
|
|
||||||
|
background-image: -webkit-repeating-linear-gradient(135deg, rgba(255, 255, 255, .35), rgba(255, 255, 255, .35) 8px, rgba(255, 255, 255, 0) 9px, rgba(255, 255, 255, 0) 16px);
|
||||||
|
background-image: repeating-linear-gradient(-45deg, rgba(255, 255, 255, .35), rgba(255, 255, 255, .35) 8px, rgba(255, 255, 255, 0) 9px, rgba(255, 255, 255, 0) 16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress_bar,
|
||||||
|
.progress_bar:after {
|
||||||
|
transition: background-color 0.7s, width 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress_bar:after {
|
||||||
|
animation: progress_bar 4.3s linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress_bar {
|
||||||
|
from {
|
||||||
|
width: 0;
|
||||||
|
background-size: 23px 22px;
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
width: 1%;
|
||||||
|
background-size: 23px 32px;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
width: 50%;
|
||||||
|
background-size: 23px 32px;
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
width: 90%;
|
||||||
|
background-size: 23px 32px;
|
||||||
|
}
|
||||||
|
98% {
|
||||||
|
width: 92%;
|
||||||
|
background-size: 23px 32px;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: 100%;
|
||||||
|
background-size: 23px 44px;
|
||||||
|
}
|
||||||
|
}
|
68
src/ts/components/Table — копия/components/Body.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { IColumn } from '../interfaces/Column';
|
||||||
|
import style from '../styles/index.module.scss';
|
||||||
|
import DefaultCell from './cells/CellDefault';
|
||||||
|
|
||||||
|
interface IBodyProps {
|
||||||
|
rows: any[];
|
||||||
|
columns: IColumn[];
|
||||||
|
disabledRow?: (row: any) => boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Body({
|
||||||
|
rows,
|
||||||
|
disabledRow,
|
||||||
|
columns,
|
||||||
|
className,
|
||||||
|
}: IBodyProps) {
|
||||||
|
const formattedRows = rows?.map((row: any, index: number) => {
|
||||||
|
const cells = columns.map((column: IColumn, columnIndex: number) => {
|
||||||
|
const value = column.properties
|
||||||
|
? row[column.properties]
|
||||||
|
: row;
|
||||||
|
const formattedValue = column.formatter
|
||||||
|
? column.formatter(value)
|
||||||
|
: value;
|
||||||
|
const content: any = typeof column.template === 'function'
|
||||||
|
? column.template(formattedValue)
|
||||||
|
: `${column.prefixes ?? ''}${formattedValue ?? ''}${column.suffixes ?? ''}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaultCell
|
||||||
|
key={`${column.title}_${columnIndex}`}
|
||||||
|
column={column}
|
||||||
|
row={row}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</DefaultCell>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const rowClassName = disabledRow && disabledRow(row)
|
||||||
|
? style.disabled
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`${style.table_row} ${rowClassName} ${className}`}
|
||||||
|
>
|
||||||
|
{cells}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{formattedRows}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Body.defaultProps = {
|
||||||
|
className: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Body;
|
61
src/ts/components/Table — копия/components/Column.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { IColumn } from '../interfaces/Column';
|
||||||
|
|
||||||
|
function Column({
|
||||||
|
template,
|
||||||
|
title,
|
||||||
|
properties,
|
||||||
|
prefixes,
|
||||||
|
suffixes,
|
||||||
|
formatter,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
isFixed,
|
||||||
|
isSortable,
|
||||||
|
isResizable,
|
||||||
|
isDraggable,
|
||||||
|
isShow,
|
||||||
|
width,
|
||||||
|
onClick,
|
||||||
|
}: IColumn): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{{
|
||||||
|
template,
|
||||||
|
title,
|
||||||
|
properties,
|
||||||
|
prefixes,
|
||||||
|
suffixes,
|
||||||
|
formatter,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
isFixed,
|
||||||
|
isSortable,
|
||||||
|
isResizable,
|
||||||
|
isDraggable,
|
||||||
|
isShow,
|
||||||
|
width,
|
||||||
|
onClick,
|
||||||
|
}}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Column.defaultProps = {
|
||||||
|
title: '',
|
||||||
|
prefixes: [''],
|
||||||
|
suffixes: [''],
|
||||||
|
formatter: (value: any) => value,
|
||||||
|
className: '',
|
||||||
|
isDisabled: false,
|
||||||
|
isFixed: false,
|
||||||
|
isSortable: false,
|
||||||
|
isResizable: false,
|
||||||
|
isDraggable: false,
|
||||||
|
isShow: true,
|
||||||
|
width: undefined,
|
||||||
|
onClick: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Column;
|
64
src/ts/components/Table — копия/components/Header.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import localization from 'ts/helpers/Localization';
|
||||||
|
|
||||||
|
import { IColumn } from '../interfaces/Column';
|
||||||
|
import headerStyle from '../styles/header.module.scss';
|
||||||
|
import style from '../styles/index.module.scss';
|
||||||
|
|
||||||
|
interface ITitleProps {
|
||||||
|
columns: IColumn[];
|
||||||
|
className?: string;
|
||||||
|
updateSort?: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header({
|
||||||
|
columns,
|
||||||
|
className,
|
||||||
|
updateSort,
|
||||||
|
}: ITitleProps) {
|
||||||
|
const cells = columns.map((column: IColumn, columnIndex: number) => {
|
||||||
|
const columnClassName = typeof column.className === 'function'
|
||||||
|
? column.className('header', columnIndex)
|
||||||
|
: column.className;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${column.title}_${columnIndex}`}
|
||||||
|
className={`${style.table_header_cell} ${className} ${columnClassName || ''}`}
|
||||||
|
style={{ width: column.width }}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
onClick={() => {
|
||||||
|
if (!column.isSortable || !updateSort) return;
|
||||||
|
updateSort([{
|
||||||
|
property: typeof column.isSortable === 'string' ? column.isSortable : column.properties,
|
||||||
|
direction: [1, -1][column.sortDirection || 0] || 0,
|
||||||
|
}]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{localization.get(column.title)}
|
||||||
|
</span>
|
||||||
|
{column.title && column.sortDirection === -1 && (
|
||||||
|
<div className={headerStyle.sort_down} />
|
||||||
|
)}
|
||||||
|
{column.title && column.sortDirection === 1 && (
|
||||||
|
<div className={headerStyle.sort_up} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${style.table_row} ${className}`}>
|
||||||
|
{cells}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Header.defaultProps = {
|
||||||
|
className: '',
|
||||||
|
updateSort: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { IColumn } from '../../interfaces/Column';
|
||||||
|
import style from '../../styles/index.module.scss';
|
||||||
|
|
||||||
|
interface IDefaultCellProps {
|
||||||
|
column: IColumn,
|
||||||
|
row: any,
|
||||||
|
className?: string,
|
||||||
|
children?: ReactNode | string | number | boolean | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DefaultCell({
|
||||||
|
column,
|
||||||
|
row,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: IDefaultCellProps): JSX.Element {
|
||||||
|
const columnClassName = typeof column.className === 'function'
|
||||||
|
? column.className('body', row)
|
||||||
|
: column.className;
|
||||||
|
const onClick = column.onClick
|
||||||
|
? (() => { if (column.onClick) column.onClick(row); })
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={column.title}
|
||||||
|
className={`${style.table_cell} ${className || ''} ${columnClassName || ''}`}
|
||||||
|
style={{
|
||||||
|
width: column.width,
|
||||||
|
cursor: onClick ? 'pointer' : 'auto',
|
||||||
|
}} // @ts-ignore
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultCell.defaultPeops = {
|
||||||
|
className: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DefaultCell;
|
30
src/ts/components/Table — копия/helpers/getColumnConfigs.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import ISort from 'ts/interfaces/Sort';
|
||||||
|
import { IColumn } from '../interfaces/Column';
|
||||||
|
|
||||||
|
function getColumnConfigs(
|
||||||
|
dirtyColumns: IColumn[] = [],
|
||||||
|
defaultWidth?: number,
|
||||||
|
sort?: ISort[],
|
||||||
|
): IColumn[] {
|
||||||
|
const sortByColumns = sort?.reduce((ref: any, item: ISort) => {
|
||||||
|
ref[item.property] = item.direction;
|
||||||
|
return ref;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const columns: IColumn[] = dirtyColumns.map((column: IColumn) => ({
|
||||||
|
...column,
|
||||||
|
sortDirection: typeof column?.isSortable === 'string'
|
||||||
|
? (sortByColumns[column?.isSortable || ''] || 0)
|
||||||
|
: (sortByColumns[column?.properties || ''] || 0),
|
||||||
|
width: column.userWidth || column.defaultWidth || defaultWidth || column.width || 150,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const middle = Math.floor(columns.length / 2);
|
||||||
|
return [
|
||||||
|
...columns.filter((column: IColumn, index: number) => column.isFixed && index <= middle),
|
||||||
|
...columns.filter((column: IColumn) => !column.isFixed),
|
||||||
|
...columns.filter((column: IColumn, index: number) => column.isFixed && index > middle),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getColumnConfigs;
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type { IColumn } from '../interfaces/Column';
|
||||||
|
|
||||||
|
export default function getDefaultColumnWidth(
|
||||||
|
columns: IColumn[],
|
||||||
|
tableRef: any,
|
||||||
|
): number {
|
||||||
|
if (!tableRef?.current?.offsetWidth) return 150;
|
||||||
|
const visibleColumns = columns.filter(({ isShow }: IColumn) => isShow);
|
||||||
|
|
||||||
|
const columnsWidth = visibleColumns.map((column: IColumn) => (
|
||||||
|
column.userWidth || column.defaultWidth || 0
|
||||||
|
));
|
||||||
|
const fixedWidth = columnsWidth.reduce((sum: number, width: number) => sum + width, 0);
|
||||||
|
const adaptiveColumnsCount = columnsWidth.filter((width: number) => !width).length;
|
||||||
|
|
||||||
|
const tableWidth = tableRef?.current?.offsetWidth - fixedWidth;
|
||||||
|
const adaptiveColumnsWidth = tableWidth / adaptiveColumnsCount;
|
||||||
|
|
||||||
|
return Math.max(adaptiveColumnsWidth, 40);
|
||||||
|
}
|
41
src/ts/components/Table — копия/helpers/getDefaultProps.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ColumnTypesEnum } from '../interfaces/Column';
|
||||||
|
import style from '../styles/index.module.scss';
|
||||||
|
|
||||||
|
export default function getDefaultProps(children: React.ReactNode) {
|
||||||
|
return React.Children.map(children, (child: React.ReactNode) => {
|
||||||
|
if (!React.isValidElement(child)) return null;
|
||||||
|
|
||||||
|
const template = child?.props?.template || ColumnTypesEnum.STRING;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const className = child?.props?.className || {
|
||||||
|
[ColumnTypesEnum.STRING]: '',
|
||||||
|
[ColumnTypesEnum.NUMBER]: style.table_cell_number,
|
||||||
|
[ColumnTypesEnum.SHORT_NUMBER]: style.table_cell_number,
|
||||||
|
}[template || ''] || '';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const defaultWidth = child?.props?.width || {
|
||||||
|
[ColumnTypesEnum.STRING]: 200,
|
||||||
|
[ColumnTypesEnum.NUMBER]: 110,
|
||||||
|
[ColumnTypesEnum.SHORT_NUMBER]: 70,
|
||||||
|
}[template || ''] || 0;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const isSortable = child?.props?.isSortable // @ts-ignore
|
||||||
|
? child?.props?.isSortable
|
||||||
|
: [ColumnTypesEnum.STRING, ColumnTypesEnum.NUMBER, ColumnTypesEnum.SHORT_NUMBER].includes(template);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...child.props as object,
|
||||||
|
className,
|
||||||
|
template,
|
||||||
|
isSortable,
|
||||||
|
width: undefined,
|
||||||
|
userWidth: undefined,
|
||||||
|
defaultWidth,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
63
src/ts/components/Table — копия/index.tsx
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import ISort from 'ts/interfaces/Sort';
|
||||||
|
|
||||||
|
import { IColumn } from './interfaces/Column';
|
||||||
|
import Header from './components/Header';
|
||||||
|
import Body from './components/Body';
|
||||||
|
import getDefaultColumnWidth from './helpers/getDefaultColumnWidth';
|
||||||
|
import getColumnConfigs from './helpers/getColumnConfigs';
|
||||||
|
import getDefaultProps from './helpers/getDefaultProps';
|
||||||
|
|
||||||
|
import style from './styles/index.module.scss';
|
||||||
|
|
||||||
|
interface ITableProps {
|
||||||
|
rows: any[];
|
||||||
|
sort?: ISort[];
|
||||||
|
disabledRow?: (row: any) => boolean;
|
||||||
|
updateSort?: Function,
|
||||||
|
children: React.ReactNode | React.ReactNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Table({
|
||||||
|
rows = [],
|
||||||
|
sort = [],
|
||||||
|
disabledRow,
|
||||||
|
updateSort,
|
||||||
|
children,
|
||||||
|
}: ITableProps): React.ReactElement | null {
|
||||||
|
if (!rows || !rows.length) return null;
|
||||||
|
|
||||||
|
const refTable = React.useRef() as React.MutableRefObject<HTMLDivElement>;
|
||||||
|
|
||||||
|
const defaultColumns = getDefaultProps(children) as IColumn[];
|
||||||
|
const defaultWidth = getDefaultColumnWidth(defaultColumns, refTable);
|
||||||
|
const columns = getColumnConfigs(defaultColumns, defaultWidth, sort);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${style.table_wrapper}`}>
|
||||||
|
<div
|
||||||
|
ref={refTable}
|
||||||
|
className={`${style.table}`}
|
||||||
|
>
|
||||||
|
<Header
|
||||||
|
columns={columns}
|
||||||
|
updateSort={updateSort}
|
||||||
|
/>
|
||||||
|
<Body
|
||||||
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
|
disabledRow={disabledRow}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Table.defaultProps = {
|
||||||
|
rows: [],
|
||||||
|
sort: [],
|
||||||
|
updateSort: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Table;
|
49
src/ts/components/Table — копия/interfaces/Column.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
export type ColumnType = 'STRING' | 'NUMBER' | 'SHORT_NUMBER';
|
||||||
|
|
||||||
|
/** Тип столбца определяет тип содержимого всех ячеек столбца */
|
||||||
|
export enum ColumnTypesEnum {
|
||||||
|
STRING = 'STRING',
|
||||||
|
NUMBER = 'NUMBER',
|
||||||
|
SHORT_NUMBER = 'SHORT_NUMBER',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IColumn {
|
||||||
|
/** Тип столбца */
|
||||||
|
template?: ColumnTypesEnum | Function,
|
||||||
|
/** Уникальный ключ столбца */
|
||||||
|
properties?: string,
|
||||||
|
/** Заголовок столбца */
|
||||||
|
title?: string,
|
||||||
|
/** Префиксы для заголовка столбца */
|
||||||
|
prefixes?: string,
|
||||||
|
/** Суффиксы для заголовка столбца (%, $ и т.д.) */
|
||||||
|
suffixes?: string,
|
||||||
|
/** Функция для форматирования данных в столбце */
|
||||||
|
formatter?: Function,
|
||||||
|
|
||||||
|
/** Направление сортировки */
|
||||||
|
sortDirection?: number,
|
||||||
|
|
||||||
|
/** Фиксированный столбец */
|
||||||
|
isFixed?: boolean,
|
||||||
|
/** Сортировка столбца */
|
||||||
|
isSortable?: boolean | string,
|
||||||
|
/** Изменение ширины столбца */
|
||||||
|
isResizable?: boolean,
|
||||||
|
/** Drag-and-Drop столбца */
|
||||||
|
isDraggable?: boolean,
|
||||||
|
/** Видимость столбца */
|
||||||
|
isShow?: boolean,
|
||||||
|
/** Клас для колонки */
|
||||||
|
className?: string | Function
|
||||||
|
/** Стилья для колонки */
|
||||||
|
style?: Function,
|
||||||
|
/** Ширина столбца заданная в верстке */
|
||||||
|
defaultWidth?: number,
|
||||||
|
/** Ширина столбца установленная пользователем */
|
||||||
|
userWidth?: number,
|
||||||
|
/** Ширина столбца итоговая */
|
||||||
|
width?: number,
|
||||||
|
/** Клик на ячейку */
|
||||||
|
onClick?: Function,
|
||||||
|
}
|
28
src/ts/components/Table — копия/styles/header.module.scss
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
@import '../../../../styles/variables';
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--font-l);
|
||||||
|
font-weight: 100;
|
||||||
|
margin: 24px 0;
|
||||||
|
color: var(--color-black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort_up,
|
||||||
|
.sort_down {
|
||||||
|
display: inline-block;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
margin: 0 0 -5px 8px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
transform: rotateZ(-45deg);
|
||||||
|
|
||||||
|
border: 6px solid var(--color-grey);
|
||||||
|
border-left-color: white;
|
||||||
|
border-bottom-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort_down {
|
||||||
|
margin: 0 0 3px 8px;
|
||||||
|
transform: rotateZ(135deg);
|
||||||
|
}
|
92
src/ts/components/Table — копия/styles/index.module.scss
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
@import '../../../../styles/variables';
|
||||||
|
|
||||||
|
.table_wrapper {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
height: 8px;
|
||||||
|
background-color: #DDDDDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #AAAAAA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
display: block;
|
||||||
|
width: fit-content;
|
||||||
|
--table-cell-height: 48px;
|
||||||
|
--table-bar-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_tree {
|
||||||
|
--table-cell-height: 22px;
|
||||||
|
--table-bar-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_row {
|
||||||
|
position: relative;
|
||||||
|
font-weight: 100;
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-bottom: 1px solid #EEEEEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_row_hide {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_cell,
|
||||||
|
.table_header_cell {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
height: var(--table-cell-height);
|
||||||
|
line-height: var(--table-cell-height);
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_cell {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_header_cell {
|
||||||
|
font-weight: bold;
|
||||||
|
height: var(--table-cell-height);
|
||||||
|
padding: 0 4px;
|
||||||
|
line-height: var(--table-cell-height);
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_cell:first-child,
|
||||||
|
.table_header_cell:first-child {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_cell:first-child {
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_cell_number {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
filter: grayscale(0.6);
|
||||||
|
}
|
25
src/ts/components/Tv100And1/components/Title.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import style from '../styles/index.module.scss';
|
||||||
|
|
||||||
|
interface ITitleProps {
|
||||||
|
title: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
function Title({ title }: ITitleProps): React.ReactElement | null {
|
||||||
|
const [show, setShow] = useState<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${style.tv100and1_cell_title}`}>
|
||||||
|
{title}
|
||||||
|
<button
|
||||||
|
className={`${style.tv100and1_button} ${show ? style.animation : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
setShow(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Title;
|
51
src/ts/components/Tv100And1/index.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import LineChart from 'ts/components/LineChart';
|
||||||
|
import getOptions from 'ts/components/LineChart/helpers/getOptions';
|
||||||
|
|
||||||
|
import Title from './components/Title';
|
||||||
|
import style from './styles/index.module.scss';
|
||||||
|
|
||||||
|
interface ITv100And1Props {
|
||||||
|
rows: {
|
||||||
|
title: string,
|
||||||
|
value: number,
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tv100And1({
|
||||||
|
rows = [],
|
||||||
|
}: ITv100And1Props): React.ReactElement | null {
|
||||||
|
if (!rows || !rows.length) return null;
|
||||||
|
|
||||||
|
const chartOptions = getOptions({ max: rows[0].value, suffix: 'сиволов' });
|
||||||
|
const formattedRows = rows.map((row: any) => (
|
||||||
|
<div
|
||||||
|
key={row.title}
|
||||||
|
className={`${style.tv100and1_row}`}
|
||||||
|
>
|
||||||
|
<Title title={row.title} />
|
||||||
|
<div className={`${style.tv100and1_cell_value}`}>
|
||||||
|
{row.value}
|
||||||
|
</div>
|
||||||
|
<div className={`${style.tv100and1_cell_chart}`}>
|
||||||
|
<LineChart
|
||||||
|
options={chartOptions}
|
||||||
|
value={row.value}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${style.tv100and1}`}>
|
||||||
|
{formattedRows}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tv100And1.defaultProps = {
|
||||||
|
rows: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tv100And1;
|
90
src/ts/components/Tv100And1/styles/index.module.scss
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
@import '../../../../styles/variables';
|
||||||
|
|
||||||
|
.tv100and1 {
|
||||||
|
&_row {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_cell_title,
|
||||||
|
&_cell_value,
|
||||||
|
&_cell_chart {
|
||||||
|
position: relative;
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
padding: var(--space-m) 0;
|
||||||
|
|
||||||
|
line-height: var(--table-cell-height);
|
||||||
|
box-sizing: border-box;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
vertical-align: top;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_cell_title {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_cell_value {
|
||||||
|
width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_cell_chart {
|
||||||
|
width: calc(100% - 300px);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_button {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 2px;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
height: calc(100% - 4px);
|
||||||
|
|
||||||
|
transform: rotate3d(1, 0, 0, 0);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
background-color: var(--color-grey);
|
||||||
|
background-image: url('../../../../assets/games/tv100and1.png');
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
background-size: auto 100%;
|
||||||
|
|
||||||
|
&.animation {
|
||||||
|
animation-name: tv_100_and_1;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
animation-duration: 1s;
|
||||||
|
animation-direction: alternate;
|
||||||
|
animation-timing-function: ease-in;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tv_100_and_1 {
|
||||||
|
from {
|
||||||
|
transform: rotate3d(1, 0, 0, 0);
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
transform: rotate3d(1, 0, 0, 90deg);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: rotate3d(1, 0, 0, 0);
|
||||||
|
}
|
||||||
|
55% {
|
||||||
|
transform: rotate3d(1, 0, 0, 90deg);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: rotate3d(1, 0, 0, 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate3d(1, 0, 0, 90deg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,47 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import dataGripStore from 'ts/store/DataGrip';
|
||||||
|
|
||||||
|
import Day from './Day';
|
||||||
import IMonth from '../interfaces/Month';
|
import IMonth from '../interfaces/Month';
|
||||||
import style from '../styles/index.module.scss';
|
import style from '../styles/index.module.scss';
|
||||||
|
import { getEvents } from '../helpers/day';
|
||||||
function getPercentByMax(countCommit: number, max: number) {
|
|
||||||
const value = ((countCommit || 0) * 100) / max;
|
|
||||||
return (value - value % 1) / 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIconUrl(month: IMonth, dayInMonth: number) {
|
|
||||||
const addPerson = month.firstDay?.[dayInMonth];
|
|
||||||
const removePerson = month.lastDay?.[dayInMonth];
|
|
||||||
if (addPerson && removePerson) return './assets/chart/commit.svg';
|
|
||||||
if (removePerson) return './assets/chart/commit.svg';
|
|
||||||
if (addPerson) return './assets/chart/commit.svg';
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getColor(isWeekend: boolean, opacity: number): string {
|
|
||||||
const colors = isWeekend ? [
|
|
||||||
'#ED675F', // 1
|
|
||||||
'#EB817C', // 0.8
|
|
||||||
'#E98E8A', // 0.7
|
|
||||||
'#E89B99', // 0.6
|
|
||||||
'#E7A8A7', // 0.5
|
|
||||||
'#E7B5B6', // 0.4
|
|
||||||
'#E6C3C4', // 0.3
|
|
||||||
'#E4CFD3', // 0.2
|
|
||||||
] : [
|
|
||||||
'#4162B5', // 0 1
|
|
||||||
'#617DC1', // 1 0.8
|
|
||||||
'#718AC6', // 2 0.7
|
|
||||||
'#8198CD', // 3 0.6
|
|
||||||
'#91A6D2', // 4 0.5
|
|
||||||
'#A2B3D8', // 5 0.4
|
|
||||||
'#B2C1DE', // 6 0.3
|
|
||||||
'#C2CEE4', // 7 0.2
|
|
||||||
];
|
|
||||||
if (opacity >= 0.8) return colors[1];
|
|
||||||
if (opacity >= 0.6) return colors[3];
|
|
||||||
if (opacity >= 0.4) return colors[5];
|
|
||||||
return colors[7];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IBodyProps {
|
interface IBodyProps {
|
||||||
month: IMonth;
|
month: IMonth;
|
||||||
|
@ -53,30 +17,26 @@ function Body({
|
||||||
maxCommits,
|
maxCommits,
|
||||||
}: IBodyProps): React.ReactElement | null {
|
}: IBodyProps): React.ReactElement | null {
|
||||||
const firstDay = month.date.getDay() - 1;
|
const firstDay = month.date.getDay() - 1;
|
||||||
const weekend = [5, 6, 12, 13, 19, 20, 26, 27, 33, 34, 40, 41];
|
|
||||||
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||||
const lastDay = firstDay + daysInMonth[month.month];
|
const lastDay = firstDay + daysInMonth[month.month];
|
||||||
const allDays = (new Array(6 * 7)).fill(0);
|
const allDays = (new Array(6 * 7)).fill(0);
|
||||||
let currentDay = 0;
|
let currentDay = 0;
|
||||||
|
|
||||||
|
const events = getEvents(dataGripStore);
|
||||||
const days = allDays.map((v: any, index: number) => {
|
const days = allDays.map((v: any, index: number) => {
|
||||||
const dayInfo = month.commits[currentDay];
|
const dayInfo = month.commits[currentDay];
|
||||||
|
|
||||||
if (dayInfo?.dayInMonth === (index - firstDay + 1)) {
|
if (dayInfo?.dayInMonth === (index - firstDay + 1)) {
|
||||||
currentDay += 1;
|
currentDay += 1;
|
||||||
const opacity = getPercentByMax(dayInfo.commits, maxCommits);
|
|
||||||
const isWeekend = weekend.includes(index);
|
|
||||||
const backgroundColor = getColor(isWeekend, opacity);
|
|
||||||
const iconUrl = getIconUrl(month, dayInfo.dayInMonth);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Day
|
||||||
key={index}
|
key={index}
|
||||||
className={style.year_chart_month_body_day}
|
month={month}
|
||||||
style={{
|
maxCommits={maxCommits}
|
||||||
backgroundColor,
|
dayNumber={index}
|
||||||
backgroundImage: iconUrl ? `url(${iconUrl})` : '',
|
dayInfo={dayInfo}
|
||||||
}}
|
events={events}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
73
src/ts/components/YearChart/components/Day.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import DayInfo from 'ts/components/DayInfo';
|
||||||
|
import dataGripStore from 'ts/store/DataGrip';
|
||||||
|
import IHashMap from 'ts/interfaces/HashMap';
|
||||||
|
|
||||||
|
import IMonth from '../interfaces/Month';
|
||||||
|
import style from '../styles/index.module.scss';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getPercentByMax,
|
||||||
|
getColor,
|
||||||
|
getIconUrl, getDayText,
|
||||||
|
} from '../helpers/day';
|
||||||
|
|
||||||
|
interface IDayProps {
|
||||||
|
maxCommits: number;
|
||||||
|
dayNumber: any;
|
||||||
|
month: IMonth;
|
||||||
|
dayInfo: any;
|
||||||
|
events: IHashMap<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Day({
|
||||||
|
month,
|
||||||
|
dayInfo,
|
||||||
|
maxCommits,
|
||||||
|
dayNumber,
|
||||||
|
events,
|
||||||
|
}: IDayProps): React.ReactElement | null {
|
||||||
|
const [showInfo, setShowInfo] = useState<boolean>(false);
|
||||||
|
const weekend = [5, 6, 12, 13, 19, 20, 26, 27, 33, 34, 40, 41];
|
||||||
|
const opacity = getPercentByMax(dayInfo.commits, maxCommits);
|
||||||
|
const isWeekend = weekend.includes(dayNumber);
|
||||||
|
const backgroundColor = getColor(isWeekend, opacity);
|
||||||
|
const iconUrl = getIconUrl(month, dayInfo.dayInMonth);
|
||||||
|
const text = getDayText(events, dayInfo.timestamp);
|
||||||
|
|
||||||
|
return ( // @ts-ignore
|
||||||
|
<div
|
||||||
|
className={style.year_chart_month_body_day}
|
||||||
|
title={`коммитов: ${dayInfo.commits}, задач: ${dayInfo.tasksInDay || 0}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor,
|
||||||
|
backgroundImage: iconUrl ? `url(${iconUrl})` : '',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setShowInfo(!showInfo);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showInfo ? (
|
||||||
|
<>
|
||||||
|
{'📌'}
|
||||||
|
<div className={style.year_chart_month_body_day_arrow} />
|
||||||
|
<div className={style.year_chart_month_body_day_info}>
|
||||||
|
<DayInfo // @ts-ignore
|
||||||
|
day={dayInfo}
|
||||||
|
events={events}
|
||||||
|
timestamp={dayInfo.timestamp}
|
||||||
|
order={dataGripStore.dataGrip.author.list}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Day.defaultProps = {
|
||||||
|
rows: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Day;
|
|
@ -19,9 +19,7 @@ function Month({
|
||||||
}: IMonthProps): React.ReactElement | null {
|
}: IMonthProps): React.ReactElement | null {
|
||||||
return (
|
return (
|
||||||
<div className={`${style.year_chart_month}`}>
|
<div className={`${style.year_chart_month}`}>
|
||||||
<Header
|
<Header month={month} />
|
||||||
month={month}
|
|
||||||
/>
|
|
||||||
<Body
|
<Body
|
||||||
month={month}
|
month={month}
|
||||||
maxCommits={maxCommits}
|
maxCommits={maxCommits}
|
||||||
|
|
70
src/ts/components/YearChart/helpers/day.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import IHashMap from 'ts/interfaces/HashMap';
|
||||||
|
|
||||||
|
import IMonth from '../interfaces/Month';
|
||||||
|
|
||||||
|
export function getPercentByMax(countCommit: number, max: number) {
|
||||||
|
const value = ((countCommit || 0) * 100) / max;
|
||||||
|
return (value - value % 1) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIconUrl(month: IMonth, dayInMonth: number) {
|
||||||
|
const addPerson = month.firstDay?.[dayInMonth];
|
||||||
|
const removePerson = month.lastDay?.[dayInMonth];
|
||||||
|
if (addPerson && removePerson) return './assets/chart/commit.svg';
|
||||||
|
if (removePerson) return './assets/chart/commit.svg';
|
||||||
|
if (addPerson) return './assets/chart/commit.svg';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColor(isWeekend: boolean, opacity: number): string {
|
||||||
|
const colors = isWeekend ? [
|
||||||
|
'#ED675F', // 1
|
||||||
|
'#EB817C', // 0.8
|
||||||
|
'#E98E8A', // 0.7
|
||||||
|
'#E89B99', // 0.6
|
||||||
|
'#E7A8A7', // 0.5
|
||||||
|
'#E7B5B6', // 0.4
|
||||||
|
'#E6C3C4', // 0.3
|
||||||
|
'#E4CFD3', // 0.2
|
||||||
|
] : [
|
||||||
|
'#4162B5', // 0 1
|
||||||
|
'#617DC1', // 1 0.8
|
||||||
|
'#718AC6', // 2 0.7
|
||||||
|
'#8198CD', // 3 0.6
|
||||||
|
'#91A6D2', // 4 0.5
|
||||||
|
'#A2B3D8', // 5 0.4
|
||||||
|
'#B2C1DE', // 6 0.3
|
||||||
|
'#C2CEE4', // 7 0.2
|
||||||
|
];
|
||||||
|
if (opacity >= 0.8) return colors[1];
|
||||||
|
if (opacity >= 0.6) return colors[3];
|
||||||
|
if (opacity >= 0.4) return colors[5];
|
||||||
|
return colors[7];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDayText(events: IHashMap<any>, timestamp: string): string {
|
||||||
|
const addEmployees = events?.firstCommit?.[timestamp];
|
||||||
|
const removeEmployees = events?.lastCommit?.[timestamp];
|
||||||
|
if (addEmployees && removeEmployees) return '+-';
|
||||||
|
if (removeEmployees) return '-';
|
||||||
|
if (addEmployees) return '+';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRefAuthorByTime(list: any[], property: string) {
|
||||||
|
return list.reduce((refTimeAuthor: any, item: any) => {
|
||||||
|
if (item.isStaff) return refTimeAuthor;
|
||||||
|
const key = item?.[property]?.timestamp;
|
||||||
|
if (!refTimeAuthor[key]) refTimeAuthor[key] = [];
|
||||||
|
refTimeAuthor[key].push(item.author);
|
||||||
|
return refTimeAuthor;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEvents(dataGripStore: any) {
|
||||||
|
const list = dataGripStore.dataGrip.author.statistic;
|
||||||
|
return {
|
||||||
|
firstCommit: getRefAuthorByTime(list, 'firstCommit'),
|
||||||
|
lastCommit: getRefAuthorByTime(list, 'lastCommit'),
|
||||||
|
};
|
||||||
|
}
|
|
@ -3,5 +3,7 @@ export default interface IWorkDay {
|
||||||
year: number;
|
year: number;
|
||||||
day: number;
|
day: number;
|
||||||
dayInMonth: number;
|
dayInMonth: number;
|
||||||
|
tasksInDay: number;
|
||||||
|
timestamp: string;
|
||||||
commits: number;
|
commits: number;
|
||||||
}
|
}
|
|
@ -44,13 +44,64 @@
|
||||||
max-width: var(--month-size);
|
max-width: var(--month-size);
|
||||||
|
|
||||||
&_day {
|
&_day {
|
||||||
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: var(--day-size);
|
width: var(--day-size);
|
||||||
height: var(--day-size);
|
height: var(--day-size);
|
||||||
margin: 0 1px 1px 0;
|
margin: 0 1px 1px 0;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
background-color: var(--color-border);
|
background-color: var(--color-border);
|
||||||
background-blend-mode: screen;
|
background-blend-mode: screen;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&_arrow {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
|
||||||
|
transform: rotateZ(-45deg);
|
||||||
|
border: 16px solid white;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_info {
|
||||||
|
font-size: var(--font-s);
|
||||||
|
position: absolute;
|
||||||
|
left: -175px;
|
||||||
|
top: var(--space-xxl);
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
width: 350px;
|
||||||
|
max-height: 350px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: var(--space-s);
|
||||||
|
|
||||||
|
cursor: default;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: var(--border-radius-m);
|
||||||
|
box-shadow: 2px 2px 5px var(--color-border);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #AAAAAA;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -40,6 +40,9 @@ export default class DataGripByAuthor {
|
||||||
statistic.hours.push(commit.hours);
|
statistic.hours.push(commit.hours);
|
||||||
statistic.messageLength.push(commit.message.length);
|
statistic.messageLength.push(commit.message.length);
|
||||||
statistic.totalMessageLength += commit.message.length || 0;
|
statistic.totalMessageLength += commit.message.length || 0;
|
||||||
|
statistic.maxMessageLength = commit.message.length > statistic.maxMessageLength
|
||||||
|
? commit.message.length
|
||||||
|
: statistic.maxMessageLength;
|
||||||
statistic.commitsByDayAndHour[commit.day][commit.hours] += 1;
|
statistic.commitsByDayAndHour[commit.day][commit.hours] += 1;
|
||||||
statistic.wordStatistics = DataGripByAuthor.#updateWordStatistics(commit, statistic.wordStatistics);
|
statistic.wordStatistics = DataGripByAuthor.#updateWordStatistics(commit, statistic.wordStatistics);
|
||||||
}
|
}
|
||||||
|
@ -60,6 +63,7 @@ export default class DataGripByAuthor {
|
||||||
commitsByDayAndHour,
|
commitsByDayAndHour,
|
||||||
messageLength: [commit.message.length || 0],
|
messageLength: [commit.message.length || 0],
|
||||||
totalMessageLength: commit.message.length || 0,
|
totalMessageLength: commit.message.length || 0,
|
||||||
|
maxMessageLength: commit.message.length || 0,
|
||||||
wordStatistics: DataGripByAuthor.#updateWordStatistics(commit),
|
wordStatistics: DataGripByAuthor.#updateWordStatistics(commit),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -150,6 +154,7 @@ export default class DataGripByAuthor {
|
||||||
isStaff,
|
isStaff,
|
||||||
|
|
||||||
middleMessageLength,
|
middleMessageLength,
|
||||||
|
maxMessageLength: dot.maxMessageLength,
|
||||||
commitsByDayAndHourTotal: DataGripByAuthor.getTotalCommitsByDayAndHour(dot.commitsByDayAndHour),
|
commitsByDayAndHourTotal: DataGripByAuthor.getTotalCommitsByDayAndHour(dot.commitsByDayAndHour),
|
||||||
wordStatistics,
|
wordStatistics,
|
||||||
};
|
};
|
||||||
|
|
88
src/ts/helpers/DataGrip/components/extension.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import IHashMap from 'ts/interfaces/HashMap';
|
||||||
|
|
||||||
|
import MinMaxCounter from './counter';
|
||||||
|
|
||||||
|
const IGNORE_LIST = [
|
||||||
|
'.eslintrc',
|
||||||
|
'.gitignore',
|
||||||
|
'package.json',
|
||||||
|
'package-lock.json',
|
||||||
|
'tsconfig.json',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default class DataGripByExtension {
|
||||||
|
statistic: any = [];
|
||||||
|
|
||||||
|
statisticByName: IHashMap<any> = {};
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.statistic = [];
|
||||||
|
this.statisticByName = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTotalInfo(fileList: any[], byAuthor: any) {
|
||||||
|
const byExtension = {};
|
||||||
|
|
||||||
|
fileList.forEach((file: any) => {
|
||||||
|
if (!file.extension
|
||||||
|
|| IGNORE_LIST.includes(file.name)) return;
|
||||||
|
if (!byExtension[file.extension]) {
|
||||||
|
byExtension[file.extension] = {
|
||||||
|
extension: file.extension, authors: {}, more: {}, total: { added: 0, changes: 0, removed: 0, total: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let author in file.authors) {
|
||||||
|
if (!author
|
||||||
|
|| byAuthor.statisticByName[author]?.isStaff) return;
|
||||||
|
byExtension[file.extension].authors[author] = byExtension[file.extension].authors[author]
|
||||||
|
|| { added: 0, changes: 0, removed: 0 };
|
||||||
|
|
||||||
|
const statistic = file.authors[author];
|
||||||
|
const total = byExtension[file.extension].authors[author];
|
||||||
|
total.added += statistic.added;
|
||||||
|
total.changes += statistic.changes;
|
||||||
|
total.removed += statistic.removed;
|
||||||
|
|
||||||
|
byExtension[file.extension].total.added += statistic.added;
|
||||||
|
byExtension[file.extension].total.changes += statistic.changes;
|
||||||
|
byExtension[file.extension].total.removed += statistic.removed;
|
||||||
|
byExtension[file.extension].total.total += statistic.added + statistic.changes + statistic.removed;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.#addMorePercent(byExtension);
|
||||||
|
|
||||||
|
this.statistic = Object.entries(byExtension)
|
||||||
|
.sort((a: any, b: any) => b[1].total.total - a[1].total.total)
|
||||||
|
.map((item: any) => item[1]);
|
||||||
|
this.statisticByName = byExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addMorePercent(byExtension: any) {
|
||||||
|
for (let extension in byExtension) {
|
||||||
|
const moreAdded = new MinMaxCounter();
|
||||||
|
const moreChanges = new MinMaxCounter();
|
||||||
|
const moreRemoved = new MinMaxCounter();
|
||||||
|
|
||||||
|
for (let author in byExtension[extension].authors) {
|
||||||
|
const statistic = byExtension[extension].authors[author];
|
||||||
|
const total = statistic.added + statistic.changes + statistic.removed;
|
||||||
|
|
||||||
|
statistic.addedPercent = (statistic.added * 100) / total;
|
||||||
|
statistic.changesPercent = (statistic.changes * 100) / total;
|
||||||
|
statistic.removedPercent = (statistic.removed * 100) / total;
|
||||||
|
|
||||||
|
moreAdded.update(statistic.addedPercent, author);
|
||||||
|
moreChanges.update(statistic.changesPercent, author);
|
||||||
|
moreRemoved.update(statistic.removedPercent, author);
|
||||||
|
}
|
||||||
|
|
||||||
|
byExtension[extension].more = {
|
||||||
|
added: { percent: moreAdded.max, author: moreAdded.maxData },
|
||||||
|
changes: { percent: moreChanges.max, author: moreChanges.maxData },
|
||||||
|
removed: { percent: moreRemoved.max, author: moreRemoved.maxData },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import DataGripByType from './components/type';
|
||||||
import DataGripByTimestamp from './components/timestamp';
|
import DataGripByTimestamp from './components/timestamp';
|
||||||
import DataGripByWeek from './components/week';
|
import DataGripByWeek from './components/week';
|
||||||
import MinMaxCounter from './components/counter';
|
import MinMaxCounter from './components/counter';
|
||||||
|
import DataGripByExtension from './components/extension';
|
||||||
|
|
||||||
class DataGrip {
|
class DataGrip {
|
||||||
firstLastCommit: any = new MinMaxCounter();
|
firstLastCommit: any = new MinMaxCounter();
|
||||||
|
@ -27,6 +28,8 @@ class DataGrip {
|
||||||
|
|
||||||
recommendations: any = new Recommendations();
|
recommendations: any = new Recommendations();
|
||||||
|
|
||||||
|
extension: any = new DataGripByExtension();
|
||||||
|
|
||||||
initializationInfo: any = {};
|
initializationInfo: any = {};
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
|
@ -38,6 +41,7 @@ class DataGrip {
|
||||||
this.timestamp.clear();
|
this.timestamp.clear();
|
||||||
this.week.clear();
|
this.week.clear();
|
||||||
this.recommendations.clear();
|
this.recommendations.clear();
|
||||||
|
this.extension.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
addCommit(commit: ICommit) {
|
addCommit(commit: ICommit) {
|
||||||
|
@ -80,6 +84,10 @@ class DataGrip {
|
||||||
});
|
});
|
||||||
this.#updateTotalInfo();
|
this.#updateTotalInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateByFiles(fileList: any[]) {
|
||||||
|
this.extension.updateTotalInfo(fileList, this.author);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataGrip = new DataGrip();
|
const dataGrip = new DataGrip();
|
||||||
|
|
32
src/ts/helpers/Parser/file_info.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import ICommit from 'ts/interfaces/Commit';
|
||||||
|
|
||||||
|
export function getNewFileAuthor(
|
||||||
|
addedLines: number,
|
||||||
|
prev?: ICommit | null,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
added: addedLines,
|
||||||
|
changes: addedLines,
|
||||||
|
removed: 0,
|
||||||
|
commits: 1,
|
||||||
|
tasks: { [prev?.task || '']: 1 },
|
||||||
|
types: { [prev?.type || '']: 1 },
|
||||||
|
scopes: { [prev?.scope || '']: 1 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNewFileInfo(
|
||||||
|
name: string,
|
||||||
|
addedLines: number,
|
||||||
|
commit?: ICommit | null,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
extension: name?.split('.')?.pop(),
|
||||||
|
lines: addedLines,
|
||||||
|
created: commit,
|
||||||
|
authors: {
|
||||||
|
[commit?.author || '']: getNewFileAuthor(addedLines, commit),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
import { IDirtyFile } from 'ts/interfaces/FileInfo';
|
import { IDirtyFile } from 'ts/interfaces/FileInfo';
|
||||||
import IHashMap from 'ts/interfaces/HashMap';
|
import IHashMap from 'ts/interfaces/HashMap';
|
||||||
import ICommit from 'ts/interfaces/Commit';
|
import ICommit from 'ts/interfaces/Commit';
|
||||||
|
import settingsStore from 'ts/store/Settings';
|
||||||
|
|
||||||
import getUserInfo from './user_info';
|
import getUserInfo from './user_info';
|
||||||
import { getNewFileName, getFileList } from './files';
|
import { getNewFileName, getFileList } from './files';
|
||||||
import settingsStore from 'ts/store/Settings';
|
import { getNewFileInfo } from './file_info';
|
||||||
|
|
||||||
|
const uniq = {};
|
||||||
export default function Parser(
|
export default function Parser(
|
||||||
report: string[],
|
report: string[],
|
||||||
parseCommit: Function,
|
parseCommit: Function,
|
||||||
|
@ -62,23 +64,8 @@ export default function Parser(
|
||||||
delete allFiles[fileName];
|
delete allFiles[fileName];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
allFiles[fileName] = {
|
// @ts-ignore
|
||||||
name: fileName,
|
allFiles[fileName] = getNewFileInfo(fileName, added, prev);
|
||||||
lines: added,
|
|
||||||
// @ts-ignore
|
|
||||||
created: prev,
|
|
||||||
authors: {
|
|
||||||
[prev?.author || '']: {
|
|
||||||
added: added,
|
|
||||||
changes: added,
|
|
||||||
removed: 0,
|
|
||||||
commits: 1,
|
|
||||||
tasks: { [prev?.task || '']: 1 },
|
|
||||||
types: { [prev?.type || '']: 1 },
|
|
||||||
scopes: { [prev?.scope || '']: 1 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (removed > added) {
|
if (removed > added) {
|
||||||
removed -= added;
|
removed -= added;
|
||||||
|
@ -99,7 +86,15 @@ export default function Parser(
|
||||||
prev.removed += removed;
|
prev.removed += removed;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (prev) parseCommit(prev);
|
|
||||||
|
if (prev) {
|
||||||
|
if (uniq[prev.date]) {
|
||||||
|
// console.log(`double ${uniq[prev.date]} === ${i}`);
|
||||||
|
}
|
||||||
|
uniq[prev.date] = i;
|
||||||
|
parseCommit(prev);
|
||||||
|
}
|
||||||
|
|
||||||
const next = getUserInfo(message);
|
const next = getUserInfo(message);
|
||||||
if (next.milliseconds > weekEndTime) {
|
if (next.milliseconds > weekEndTime) {
|
||||||
week += 1;
|
week += 1;
|
||||||
|
@ -115,7 +110,6 @@ export default function Parser(
|
||||||
}
|
}
|
||||||
if (prev) parseCommit(prev);
|
if (prev) parseCommit(prev);
|
||||||
|
|
||||||
|
|
||||||
const { fileList, fileTree } = getFileList(allFiles);
|
const { fileList, fileTree } = getFileList(allFiles);
|
||||||
return {
|
return {
|
||||||
commits,
|
commits,
|
||||||
|
|
43
src/ts/helpers/ParserTelegramm/index.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import ICommit from 'ts/interfaces/Commit';
|
||||||
|
|
||||||
|
import getUserInfo from './user_info';
|
||||||
|
import settingsStore from 'ts/store/Settings';
|
||||||
|
|
||||||
|
export default function ParserTelegramm(
|
||||||
|
messages: any[],
|
||||||
|
parseCommit: Function,
|
||||||
|
) {
|
||||||
|
const commits: ICommit[] = [];
|
||||||
|
let week: number = 0;
|
||||||
|
let weekEndTime: number = 0;
|
||||||
|
|
||||||
|
let prev = null;
|
||||||
|
|
||||||
|
for (let i = 0, l = messages.length; i < l; i += 1) {
|
||||||
|
const message = messages[i];
|
||||||
|
if (!message?.text) continue;
|
||||||
|
|
||||||
|
if (prev) parseCommit(prev);
|
||||||
|
const next = getUserInfo(message);
|
||||||
|
if (next.milliseconds > weekEndTime) {
|
||||||
|
week += 1;
|
||||||
|
weekEndTime = next.milliseconds + (settingsStore.ONE_DAY * (6 - next.day));
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
next.week = week;
|
||||||
|
|
||||||
|
prev = next;
|
||||||
|
commits.push(prev);
|
||||||
|
}
|
||||||
|
if (prev) parseCommit(prev);
|
||||||
|
|
||||||
|
return {
|
||||||
|
commits,
|
||||||
|
fileList: [],
|
||||||
|
fileTree: {
|
||||||
|
id: Math.random(),
|
||||||
|
name: '',
|
||||||
|
content: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
54
src/ts/helpers/ParserTelegramm/user_info.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import ICommit from 'ts/interfaces/Commit';
|
||||||
|
|
||||||
|
export default function getUserInfo(message: any): ICommit {
|
||||||
|
/*
|
||||||
|
"id": -999973047,
|
||||||
|
"type": "message",
|
||||||
|
"date": "2021-02-03T17:21:10",
|
||||||
|
"date_unixtime": "1612362070",
|
||||||
|
"from": "Дарья Скуратова (ВТБ)",
|
||||||
|
"from_id": "user415945803",
|
||||||
|
"text": "Ребята, привет!",
|
||||||
|
"text_entities": [
|
||||||
|
{
|
||||||
|
"type": "plain",
|
||||||
|
"text": "Ребята, привет!"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
|
||||||
|
const date = new Date(message?.date);
|
||||||
|
const day = date.getDay() - 1;
|
||||||
|
const timestamp = message?.date.split('T')[0];
|
||||||
|
|
||||||
|
const author = message?.from || '';
|
||||||
|
const email = message?.from_id || '';
|
||||||
|
const text = Array.isArray(message?.text)
|
||||||
|
? message.text.map((subString: any) => subString?.text || subString).join(' ')
|
||||||
|
: (message?.text || '');
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: message?.date,
|
||||||
|
day: day < 0 ? 6 : day,
|
||||||
|
dayInMonth: date.getDate(),
|
||||||
|
hours: date.getHours(),
|
||||||
|
minutes: date.getMinutes(),
|
||||||
|
month: date.getMonth(),
|
||||||
|
year: date.getUTCFullYear(),
|
||||||
|
week: 0,
|
||||||
|
timestamp,
|
||||||
|
milliseconds: parseInt(message?.date_unixtime, 10),
|
||||||
|
|
||||||
|
author,
|
||||||
|
email,
|
||||||
|
message: text || '',
|
||||||
|
|
||||||
|
task: 'беседа',
|
||||||
|
type: 'не подписан',
|
||||||
|
scope: 'неопределенна',
|
||||||
|
|
||||||
|
changes: 0,
|
||||||
|
added: 0,
|
||||||
|
removed: 0,
|
||||||
|
};
|
||||||
|
}
|
|
@ -4,9 +4,8 @@ import ALL_ACHIEVEMENTS from './constants/list';
|
||||||
import byCompetition from './byCompetition';
|
import byCompetition from './byCompetition';
|
||||||
|
|
||||||
export default function getAchievementByAuthor(author: string) {
|
export default function getAchievementByAuthor(author: string) {
|
||||||
const statistic = dataGrip.author.statistic.find((item: any) => item.author === author);
|
const statistic = dataGrip.author.statisticByName[author];
|
||||||
if (!statistic) return;
|
if (!statistic) return;
|
||||||
|
|
||||||
const list = byCompetition.get(author);
|
const list = byCompetition.get(author);
|
||||||
|
|
||||||
// Сова - 70% коммитов после 15:00
|
// Сова - 70% коммитов после 15:00
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import IHashMap from 'ts/interfaces/HashMap';
|
import IHashMap from 'ts/interfaces/HashMap';
|
||||||
|
import dataGrip from 'ts/helpers/DataGrip';
|
||||||
|
|
||||||
class AchievementsByCompetition {
|
class AchievementsByCompetition {
|
||||||
authors: IHashMap<string[]> = {};
|
authors: IHashMap<string[]> = {};
|
||||||
|
@ -68,6 +69,14 @@ class AchievementsByCompetition {
|
||||||
// Главный редактор - сделал больше всех меток «рефакторинг»
|
// Главный редактор - сделал больше всех меток «рефакторинг»
|
||||||
achievements[moreRefactoring.first].push('moreRefactoring');
|
achievements[moreRefactoring.first].push('moreRefactoring');
|
||||||
|
|
||||||
|
const tasksInDay = this.#getFirstAndLast(total.tasksInDay);
|
||||||
|
// Спиди-гонщик - рекорд по количеству закрытых задач в день
|
||||||
|
achievements[tasksInDay.first].push('moreTasksInDay');
|
||||||
|
|
||||||
|
const commitsInDay = this.#getFirstAndLast(total.commitsInDay);
|
||||||
|
// Zerg Rush - рекорд по количеству коммитов в день
|
||||||
|
achievements[commitsInDay.first].push('moreCommits');
|
||||||
|
|
||||||
this.authors = achievements;
|
this.authors = achievements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +97,10 @@ class AchievementsByCompetition {
|
||||||
addData('days', statistic.days);
|
addData('days', statistic.days);
|
||||||
addData('moreRefactoring', statistic.types.refactor);
|
addData('moreRefactoring', statistic.types.refactor);
|
||||||
|
|
||||||
|
const byTimestamp = dataGrip.timestamp.statisticByAuthor[statistic.author];
|
||||||
|
addData('tasksInDay', byTimestamp.tasksByTimestampCounter.max);
|
||||||
|
addData('commitsInDay', byTimestamp.commitsByTimestampCounter.max);
|
||||||
|
|
||||||
if (statistic.isStaff) return;
|
if (statistic.isStaff) return;
|
||||||
addData('allDaysInProject', statistic.allDaysInProject);
|
addData('allDaysInProject', statistic.allDaysInProject);
|
||||||
addData('lazyDays', statistic.lazyDays);
|
addData('lazyDays', statistic.lazyDays);
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import ACHIEVEMENT_TYPE from './type';
|
import ACHIEVEMENT_TYPE from './type';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
// готово
|
||||||
commitsAfter1500: ['Сова', '70% коммитов после 15:00', ACHIEVEMENT_TYPE.NORMAL],
|
commitsAfter1500: ['Сова', '70% коммитов после 15:00', ACHIEVEMENT_TYPE.NORMAL],
|
||||||
commitsBefore1500: ['Раняя пташка', '70% коммитов до обеда', ACHIEVEMENT_TYPE.NORMAL],
|
commitsBefore1500: ['Раняя пташка', '70% коммитов до обеда', ACHIEVEMENT_TYPE.NORMAL],
|
||||||
commitsAfter1800: ['Делу время', 'нет ни одного коммита после 18:00', ACHIEVEMENT_TYPE.GOOD],
|
commitsAfter1800: ['Делу время', 'нет ни одного коммита после 18:00', ACHIEVEMENT_TYPE.GOOD],
|
||||||
workEveryTime: ['Раб божий', 'есть коммит на каждый час суток', ACHIEVEMENT_TYPE.BAD],
|
workEveryTime: ['Раб божий', 'есть коммит на каждый час суток', ACHIEVEMENT_TYPE.BAD],
|
||||||
workNotWork: ['Стрельба холостыми', 'коммиты есть, а закрытых задач нет', ACHIEVEMENT_TYPE.BAD], // нет картинки
|
workNotWork: ['Стрельба холостыми', 'коммиты есть, а закрытых задач нет', ACHIEVEMENT_TYPE.BAD],
|
||||||
userNotWork: ['Залётный', 'это не его основной проект', ACHIEVEMENT_TYPE.NORMAL],
|
userNotWork: ['Залётный', 'это не его основной проект', ACHIEVEMENT_TYPE.NORMAL],
|
||||||
userIsDied: ['Мёртвая душа', 'работал, но уволился', ACHIEVEMENT_TYPE.NORMAL],
|
userIsDied: ['Мёртвая душа', 'работал, но уволился', ACHIEVEMENT_TYPE.NORMAL],
|
||||||
lessTasks: ['Зашел и вышел', 'меньше всего закрытых задач', ACHIEVEMENT_TYPE.BAD],
|
lessTasks: ['Зашел и вышел', 'меньше всего закрытых задач', ACHIEVEMENT_TYPE.BAD],
|
||||||
moreTasks: ['Батя грит малаца', 'больше всего закрытых задач', ACHIEVEMENT_TYPE.GOOD], // нет картинки
|
moreTasks: ['Батя грит малаца', 'больше всего закрытых задач', ACHIEVEMENT_TYPE.GOOD],
|
||||||
everyMessageLong: ['Мастер красноречия', 'стабильно самые длинные подписи коммитов', ACHIEVEMENT_TYPE.NORMAL],
|
everyMessageLong: ['Мастер красноречия', 'стабильно самые длинные подписи коммитов', ACHIEVEMENT_TYPE.NORMAL],
|
||||||
everyMessageShort: ['Болтун находка для шпиона', 'стабильно, самые короткие подписи коммитов', ACHIEVEMENT_TYPE.BAD],
|
everyMessageShort: ['Болтун находка для шпиона', 'стабильно, самые короткие подписи коммитов', ACHIEVEMENT_TYPE.BAD],
|
||||||
shortestName: ['Размер не главное', 'самое короткое имя', ACHIEVEMENT_TYPE.NORMAL], // нет картинки
|
shortestName: ['Размер не главное', 'самое короткое имя', ACHIEVEMENT_TYPE.NORMAL], // нет картинки
|
||||||
|
@ -28,19 +29,21 @@ export default {
|
||||||
moreDaysInProject: ['Старожил', 'больше всего дней на проекте', ACHIEVEMENT_TYPE.GOOD],
|
moreDaysInProject: ['Старожил', 'больше всего дней на проекте', ACHIEVEMENT_TYPE.GOOD],
|
||||||
lessDaysInProject: ['А это кто?', 'меньше всего дней на проекте', ACHIEVEMENT_TYPE.NORMAL],
|
lessDaysInProject: ['А это кто?', 'меньше всего дней на проекте', ACHIEVEMENT_TYPE.NORMAL],
|
||||||
more90DaysInProject: ['Добро пожаловать', 'не уволили на испытательном', ACHIEVEMENT_TYPE.GOOD],
|
more90DaysInProject: ['Добро пожаловать', 'не уволили на испытательном', ACHIEVEMENT_TYPE.GOOD],
|
||||||
lessDaysForTask: ['Скорострел', 'работа по задачам идёт быстрее чем у остальных', ACHIEVEMENT_TYPE.GOOD],
|
lessDaysForTask: ['Скорострел', 'одна задача занимает меньше дня', ACHIEVEMENT_TYPE.GOOD],
|
||||||
|
|
||||||
|
moreRefactoring: ['Выпускающий редактор', 'сделал больше всех меток «рефакторинг»', ACHIEVEMENT_TYPE.GOOD],
|
||||||
// нет картинки
|
// нет картинки
|
||||||
longestMessage: ['А разговоров то было...', 'самая длинная подпись коммита за все время', ACHIEVEMENT_TYPE.NORMAL],
|
longestMessage: ['А разговоров то было...', 'самая длинная подпись коммита за все время', ACHIEVEMENT_TYPE.NORMAL],
|
||||||
moreRefactoring: ['Главный редактор', 'сделал больше всех меток «рефакторинг»', ACHIEVEMENT_TYPE.GOOD],
|
|
||||||
adam: ['Адам', 'первый стабильны сотрудник на проекте', ACHIEVEMENT_TYPE.NORMAL],
|
adam: ['Адам', 'первый стабильны сотрудник на проекте', ACHIEVEMENT_TYPE.NORMAL],
|
||||||
more666DaysInProject: ['Чёрт', 'отработал 666 дней на проекте', ACHIEVEMENT_TYPE.GOOD],
|
more666DaysInProject: ['Чёрт', 'отработал 666 дней на проекте', ACHIEVEMENT_TYPE.GOOD],
|
||||||
more777DaysInProject: ['Флеш-рояль', 'отработал 777 дней на проекте', ACHIEVEMENT_TYPE.GOOD],
|
more777DaysInProject: ['Азино 3 топора', 'отработал 777 дней на проекте', ACHIEVEMENT_TYPE.GOOD],
|
||||||
|
moreTasksInDay: ['Спиди-гонщик', 'рекорд по количеству закрытых задач в день', ACHIEVEMENT_TYPE.GOOD],
|
||||||
|
|
||||||
|
// нет кода
|
||||||
|
// moreFix: ['Bug hunter', 'больше всего закрытых багов', ACHIEVEMENT_TYPE.GOOD],
|
||||||
lessWorkDays: ['Дальше без меня', 'меньше всего рабочих дней', ACHIEVEMENT_TYPE.BAD],
|
lessWorkDays: ['Дальше без меня', 'меньше всего рабочих дней', ACHIEVEMENT_TYPE.BAD],
|
||||||
moreTasksInDay: ['Шумахер', 'рекорд по количеству закрытых задач в день', ACHIEVEMENT_TYPE.GOOD], // нет картинки
|
|
||||||
moreCreateCode: ['Созидатель', 'склонен больше остальных добавлять код', ACHIEVEMENT_TYPE.NORMAL],
|
moreCreateCode: ['Созидатель', 'склонен больше остальных добавлять код', ACHIEVEMENT_TYPE.NORMAL],
|
||||||
moreRemoveCode: ['Разрушитель', 'склонен больше остальных удалять код', ACHIEVEMENT_TYPE.NORMAL],
|
moreRemoveCode: ['Разрушитель', 'склонен больше остальных удалять код', ACHIEVEMENT_TYPE.NORMAL],
|
||||||
moreChangeCode: ['Реформатор', 'склонен больше остальных изменять код', ACHIEVEMENT_TYPE.NORMAL],
|
moreChangeCode: ['Реформатор', 'склонен больше остальных изменять код', ACHIEVEMENT_TYPE.NORMAL], // есть картинка
|
||||||
moreStyle: ['Полиция моды', 'склонен больше остальных изменять CSS', ACHIEVEMENT_TYPE.GOOD],
|
moreStyle: ['Полиция моды', 'склонен больше остальных изменять CSS', ACHIEVEMENT_TYPE.GOOD],
|
||||||
};
|
};
|
||||||
|
|
55
src/ts/pages/Common/components/UserSelect.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
import dataGripStore from 'ts/store/DataGrip';
|
||||||
|
import UiKitSelect from 'ts/components/UiKit/components/Select';
|
||||||
|
import UiKitButton from 'ts/components/UiKit/components/Button';
|
||||||
|
|
||||||
|
import style from '../styles/user.module.scss';
|
||||||
|
|
||||||
|
interface IUserSelectProps {
|
||||||
|
required?: boolean;
|
||||||
|
userId: number;
|
||||||
|
onChange: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserSelect = observer(({ required, userId, onChange }: IUserSelectProps): React.ReactElement => {
|
||||||
|
const authors = dataGripStore.dataGrip.author.list;
|
||||||
|
const options = authors.map((title: string, id: number) => ({ id, title }));
|
||||||
|
if (!required) {
|
||||||
|
options.unshift({ id: '', title: 'Не имеет значения' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.user_select}>
|
||||||
|
<UiKitButton
|
||||||
|
type="second"
|
||||||
|
disabled={userId <= 0}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(userId - 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
«
|
||||||
|
</UiKitButton>
|
||||||
|
<UiKitSelect
|
||||||
|
value={userId}
|
||||||
|
options={options}
|
||||||
|
className={style.user_name}
|
||||||
|
onChange={(newUserId: string) => {
|
||||||
|
onChange(newUserId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<UiKitButton
|
||||||
|
type="second"
|
||||||
|
disabled={userId >= (authors.length - 1)}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(userId + 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
»
|
||||||
|
</UiKitButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UserSelect;
|
13
src/ts/pages/Common/styles/user.module.scss
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
@import '../../../../styles/variables';
|
||||||
|
|
||||||
|
.user_select {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user_name {
|
||||||
|
display: inline-block;
|
||||||
|
width: 260px;
|
||||||
|
max-width: 260px;
|
||||||
|
margin: 0 12px;
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import Filter from './Filter';
|
||||||
|
|
||||||
import { IEmployees } from '../interfaces/Setting';
|
import { IEmployees } from '../interfaces/Setting';
|
||||||
import { getNewEmployeesSettings } from '../helpers/getEmptySettings';
|
import { getNewEmployeesSettings } from '../helpers/getEmptySettings';
|
||||||
|
import MailMap from './MailMap';
|
||||||
import formStore from '../store/Form';
|
import formStore from '../store/Form';
|
||||||
import style from '../styles/index.module.scss';
|
import style from '../styles/index.module.scss';
|
||||||
|
|
||||||
|
@ -45,38 +46,44 @@ const SettingForm = observer((response: any): React.ReactElement | null => {
|
||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<>
|
||||||
<PageColumn>
|
<PageWrapper>
|
||||||
<Filter />
|
<PageColumn>
|
||||||
<Salary />
|
<Filter />
|
||||||
<Common />
|
<Salary />
|
||||||
</PageColumn>
|
<Common />
|
||||||
<PageColumn>
|
</PageColumn>
|
||||||
<Title title="Индивидуальные настройки"/>
|
<PageColumn>
|
||||||
{employees.length > 0 ? (
|
<Title title="Индивидуальные настройки"/>
|
||||||
users
|
{employees.length > 0 ? (
|
||||||
) : (
|
users
|
||||||
<NothingFound
|
) : (
|
||||||
message="Индивидуальных настроек нет. Данные по всем сотрудникам вычисляются по общим параметрам."
|
<NothingFound
|
||||||
/>
|
message="Индивидуальных настроек нет. Данные по всем сотрудникам вычисляются по общим параметрам."
|
||||||
)}
|
/>
|
||||||
{authors.length && (
|
)}
|
||||||
<div className={style.buttons_footer}>
|
{authors.length && (
|
||||||
<UiKitButtonMenu
|
<div className={style.buttons_footer}>
|
||||||
options={authors}
|
<UiKitButtonMenu
|
||||||
onClick={(user: any) => {
|
options={authors}
|
||||||
formStore.updateState('employees', [
|
onClick={(user: any) => {
|
||||||
...employees,
|
formStore.updateState('employees', [
|
||||||
getNewEmployeesSettings(user?.title, formStore.state, selectedNames?.length),
|
...employees,
|
||||||
]);
|
getNewEmployeesSettings(user?.title, formStore.state, selectedNames?.length),
|
||||||
}}
|
]);
|
||||||
>
|
}}
|
||||||
Добавить пользователя
|
>
|
||||||
</UiKitButtonMenu>
|
Добавить пользователя
|
||||||
</div>
|
</UiKitButtonMenu>
|
||||||
)}
|
</div>
|
||||||
</PageColumn>
|
)}
|
||||||
</PageWrapper>
|
</PageColumn>
|
||||||
|
</PageWrapper>
|
||||||
|
<PageWrapper>
|
||||||
|
<Title title="Настройки .mailmap"/>
|
||||||
|
<MailMap />
|
||||||
|
</PageWrapper>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
29
src/ts/pages/Settings/components/MailMap.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import dataGripStore from 'ts/store/DataGrip';
|
||||||
|
import Console from 'ts/components/Console';
|
||||||
|
|
||||||
|
import style from '../styles/index.module.scss';
|
||||||
|
|
||||||
|
function MailMap(): React.ReactElement | null {
|
||||||
|
const statistic = dataGripStore.dataGrip.author.statistic.map((item: any) => (
|
||||||
|
<p key={item.author}>
|
||||||
|
{`${item.author} <${item.firstCommit.email}> <${item.firstCommit.email}>`}
|
||||||
|
</p>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.races_track}>
|
||||||
|
<Console>
|
||||||
|
{statistic}
|
||||||
|
</Console>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MailMap.defaultProps = {
|
||||||
|
type: '',
|
||||||
|
canStart: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MailMap;
|
50
src/ts/pages/Settings/helpers/settings.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import IHashMap from 'ts/interfaces/HashMap';
|
||||||
|
|
||||||
|
import { IEmployees, ISetting } from '../interfaces/Setting';
|
||||||
|
import getEmptySettings from '../helpers/getEmptySettings';
|
||||||
|
|
||||||
|
class Settings {
|
||||||
|
customSettings: ISetting = getEmptySettings();
|
||||||
|
|
||||||
|
employeesByName: IHashMap<IEmployees> = {};
|
||||||
|
|
||||||
|
workDaysInMonth: number = 22;
|
||||||
|
|
||||||
|
salaryInDay: number = 180000 / 22;
|
||||||
|
|
||||||
|
update(customSettings?: ISetting) {
|
||||||
|
this.customSettings = customSettings || getEmptySettings();
|
||||||
|
|
||||||
|
this.employeesByName = this.customSettings.employees.reduce((acc, user) => {
|
||||||
|
acc[user.name] = user;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const salary = this.customSettings.defaultSalary;
|
||||||
|
this.workDaysInMonth = Math.ceil(4.3 * salary.workDaysInWeek);
|
||||||
|
this.salaryInDay = salary.value / this.workDaysInMonth;
|
||||||
|
}
|
||||||
|
|
||||||
|
get() {
|
||||||
|
return this.customSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMiddleSalaryInMonth(name: string): number {
|
||||||
|
const user = this.employeesByName[name];
|
||||||
|
if (!user) return this.customSettings.defaultSalary.value;
|
||||||
|
const salary = user.salary[user.salary.length - 1];
|
||||||
|
return salary.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMiddleSalaryInDay(name: string) {
|
||||||
|
const user = this.employeesByName[name];
|
||||||
|
if (!user) return this.salaryInDay;
|
||||||
|
const salary = user.salary[user.salary.length - 1];
|
||||||
|
const workDaysInMonth = Math.ceil(4.3 * salary.workDaysInWeek);
|
||||||
|
return salary.value / workDaysInMonth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = new Settings();
|
||||||
|
|
||||||
|
export default settings;
|
7
src/ts/pages/Settings/styles/header.module.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
@import '../../../../styles/variables';
|
||||||
|
|
||||||
|
.buttons_header {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
|
@ -26,7 +26,6 @@ interface ITempoViewProps {
|
||||||
|
|
||||||
function TempoView({ response, order, user }: ITempoViewProps) {
|
function TempoView({ response, order, user }: ITempoViewProps) {
|
||||||
if (!response) return null;
|
if (!response) return null;
|
||||||
console.log(response.content?.length);
|
|
||||||
return (
|
return (
|
||||||
<TempoChart
|
<TempoChart
|
||||||
days={response.content as any[]}
|
days={response.content as any[]}
|
||||||
|
@ -41,7 +40,6 @@ TempoView.defaultProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function getPartOfData(filters: any, rows: any[]) {
|
function getPartOfData(filters: any, rows: any[]) {
|
||||||
console.log(filters);
|
|
||||||
return rows.filter((row: any) => (row.week === filters.week)).slice(0, 7);
|
return rows.filter((row: any) => (row.week === filters.week)).slice(0, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +51,7 @@ const Tempo = observer((): React.ReactElement => {
|
||||||
|
|
||||||
const [week, setWeek] = useState<number>(firstPoint.week);
|
const [week, setWeek] = useState<number>(firstPoint.week);
|
||||||
const [user, setUser] = useState<string>('');
|
const [user, setUser] = useState<string>('');
|
||||||
console.log(firstPoint.week);
|
|
||||||
if (!rows?.length) return (<NothingFound />);
|
if (!rows?.length) return (<NothingFound />);
|
||||||
const partOfData = getPartOfData({ week, user }, rows);
|
const partOfData = getPartOfData({ week, user }, rows);
|
||||||
const firstWeekDay = partOfData[0];
|
const firstWeekDay = partOfData[0];
|
||||||
|
|
78
src/ts/pages/Team/components/Top.tsx
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
import dataGripStore from 'ts/store/DataGrip';
|
||||||
|
|
||||||
|
import Title from 'ts/components/Title';
|
||||||
|
import PageWrapper from 'ts/components/Page/wrapper';
|
||||||
|
import Achievements from 'ts/components/Achievement';
|
||||||
|
import Extension from 'ts/components/Extension';
|
||||||
|
import Races from 'ts/components/Races';
|
||||||
|
|
||||||
|
import Tv100And1 from 'ts/components/Tv100And1';
|
||||||
|
|
||||||
|
import ACHIEVEMENT_TYPE from 'ts/helpers/achievement/constants/type';
|
||||||
|
import getAchievementByAuthor from 'ts/helpers/achievement/byAuthor';
|
||||||
|
import { getDate } from 'ts/helpers/formatter';
|
||||||
|
|
||||||
|
const Top = observer((): React.ReactElement => {
|
||||||
|
const extensions = dataGripStore.dataGrip.extension.statistic
|
||||||
|
.slice(0, 4).map((statistic: any) => {
|
||||||
|
return (
|
||||||
|
<Extension
|
||||||
|
key={statistic.extension}
|
||||||
|
statistic={statistic}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tracksAuth = dataGripStore.dataGrip.author.statistic
|
||||||
|
.filter((item: any) => !item.isStaff);
|
||||||
|
const value = tracksAuth.map((statistic: any) => statistic.taskInDay);
|
||||||
|
const max = Math.max(...value);
|
||||||
|
const tracks = tracksAuth.map((statistic: any) => ({
|
||||||
|
title: statistic.author,
|
||||||
|
speed: statistic.taskInDay / max,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const maxMessageLength = [...tracksAuth]
|
||||||
|
.sort((a: any, b: any) => b.maxMessageLength - a.maxMessageLength)
|
||||||
|
.map((item: any) => ({ title: item.author, value: item.maxMessageLength }));
|
||||||
|
|
||||||
|
const authors = dataGripStore.dataGrip.author.statistic.map((statistic: any) => {
|
||||||
|
const achievements = getAchievementByAuthor(statistic.author);
|
||||||
|
const from = getDate(statistic.firstCommit.date);
|
||||||
|
const to = getDate(statistic.lastCommit.date);
|
||||||
|
return (
|
||||||
|
<div key={statistic.author}>
|
||||||
|
<Title title={statistic.author}/>
|
||||||
|
{`Всего коммитов: ${statistic.commits} `}
|
||||||
|
{`Работал ${statistic.allDaysInProject} дней с ${from} по ${to} `}
|
||||||
|
<PageWrapper>
|
||||||
|
<Achievements list={[
|
||||||
|
...achievements[ACHIEVEMENT_TYPE.GOOD],
|
||||||
|
...achievements[ACHIEVEMENT_TYPE.NORMAL],
|
||||||
|
...achievements[ACHIEVEMENT_TYPE.BAD],
|
||||||
|
]} />
|
||||||
|
</PageWrapper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title title="Скорость закрытия задач"/>
|
||||||
|
<Races tracks={tracks} />
|
||||||
|
<Title title="Максимальная длинна подписи коммита"/>
|
||||||
|
<Tv100And1 rows={maxMessageLength} />
|
||||||
|
<PageWrapper>
|
||||||
|
<div style={{ whiteSpace: 'normal' }} >
|
||||||
|
{extensions}
|
||||||
|
</div>
|
||||||
|
</PageWrapper>
|
||||||
|
{authors}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Top;
|
|
@ -63,9 +63,11 @@ class DataGripStore implements IDataGripStore {
|
||||||
dataGrip.firstLastCommit.minData,
|
dataGrip.firstLastCommit.minData,
|
||||||
dataGrip.firstLastCommit.maxData,
|
dataGrip.firstLastCommit.maxData,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
dataGrip.updateByInitialization();
|
||||||
|
dataGrip.updateByFiles(fileList);
|
||||||
|
achievements.updateByDataGrip(dataGrip.author.statistic);
|
||||||
}
|
}
|
||||||
dataGrip.updateByInitialization();
|
|
||||||
achievements.updateByDataGrip(dataGrip.author.statistic);
|
|
||||||
|
|
||||||
this.dataGrip = null;
|
this.dataGrip = null;
|
||||||
this.dataGrip = dataGrip;
|
this.dataGrip = dataGrip;
|
||||||
|
@ -77,7 +79,7 @@ class DataGripStore implements IDataGripStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChars() { // todo: remove, never use
|
updateChars() { // todo: remove, never use
|
||||||
console.log('s');
|
console.log('need update data TODO');
|
||||||
return;
|
return;
|
||||||
dataGrip.updateByFilters();
|
dataGrip.updateByFilters();
|
||||||
if (!dataGrip.author.list.length) return;
|
if (!dataGrip.author.list.length) return;
|
||||||
|
|
|
@ -128,7 +128,6 @@ class SettingsStore implements ISettingsStore {
|
||||||
halfYear: 10,
|
halfYear: 10,
|
||||||
month: 2,
|
month: 2,
|
||||||
}[type] || 1;
|
}[type] || 1;
|
||||||
|
|
||||||
dataGripStore.updateChars();
|
dataGripStore.updateChars();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|