This commit is contained in:
bakhirev 2024-08-14 00:56:49 +03:00
parent 5d890b4848
commit 72d3c59f30
18 changed files with 250 additions and 145 deletions

View file

@ -5,23 +5,24 @@ import Banner from 'ts/components/Banner';
import style from '../index.module.scss';
interface ICardWithBannerProps {
long?: boolean;
size?: 's' | 'm' | 'l';
}
function CardWithBanner({
long = false,
}: ICardWithBannerProps): React.ReactElement | null {
const className = long
? style.card_with_icon_long
: style.card_with_icon;
function CardWithBanner({ size }: ICardWithBannerProps): React.ReactElement | null {
const className = [
style.card_with_icon,
style.card_with_icon_banner,
];
if (size === 's') className.push(style.card_with_icon_small);
if (size === 'l') className.push(style.card_with_icon_long);
return (
<Banner className={`${className} ${style.card_with_icon_banner}`} />
<Banner className={className.join(' ')} />
);
}
CardWithBanner.defaultProps = {
long: false,
size: 'm',
};
export default CardWithBanner;

View file

@ -15,6 +15,7 @@ function Scoring({
total,
}: IScoringProps): React.ReactElement | null {
const { t } = useTranslation();
if (!value) return null;
return (

View file

@ -1,14 +1,13 @@
@import 'src/styles/variables';
.card_with_icon,
.card_with_icon_long {
.card_with_icon {
position: relative;
display: inline-block;
width: calc(50% - 12px);
min-height: 270px;
margin: 0 24px 24px 0;
padding: 16px;
margin: 0 var(--space-xxl) var(--space-xxl) 0;
padding: var(--space-l);
vertical-align: top;
text-decoration: none;
@ -35,10 +34,13 @@
&_value {
font-weight: bold;
font-size: 28px;
display: block;
margin: 0 0 4px 0;
margin: 0 0 var(--space-xxs) 0;
padding: 0;
text-align: center;
color: var(--color-11);
}
@ -46,19 +48,22 @@
&_description,
&_scoring {
font-weight: 100;
display: block;
margin: 0 auto;
padding: 0;
line-height: 1.3;
text-align: center;
text-decoration: none;
color: var(--color-black);
}
&_title {
font-size: var(--font-m);
line-height: 14px;
margin: 0 0 4px 0;
margin: 0 0 var(--space-xxs) 0;
}
&_description,
@ -68,12 +73,6 @@
color: var(--color-grey);
}
&_banner {
font-size: 32px;
padding: 0;
line-height: 270px;
}
&_scoring {
position: absolute;
bottom: -11px;
@ -82,6 +81,7 @@
display: inline-block;
padding: var(--space-xxxs) var(--space-xs);
white-space: nowrap;
text-align: center;
@ -89,11 +89,31 @@
border: 1px solid var(--color-border);
background-color: var(--color-white);
}
&_small {
min-height: auto;
padding: var(--space-l) 0;
}
&_long {
width: 100%;
margin: 0 0 var(--space-xxl) 0;
}
&_banner {
font-size: 32px;
padding: 0;
line-height: 270px;
}
}
.card_with_icon_long {
width: 100%;
margin: 0 0 24px 0;
.card_with_icon_small {
.card_with_icon_value {
font-size: var(--font-m);
font-weight: 100;
margin-bottom: var(--space-m);
color: var(--color-button-2);
}
}
.card_with_icon:nth-child(2n+2) {
@ -101,10 +121,9 @@
}
@media (max-width: 900px) {
.card_with_icon,
.card_with_icon_long {
.card_with_icon {
min-height: 220px;
padding: 16px 0;
padding: var(--space-l) 0;
&_title {
margin: 0;
@ -117,8 +136,7 @@
}
@media (max-width: 650px) {
.card_with_icon,
.card_with_icon_long {
.card_with_icon {
min-height: 190px;
padding: 32px 0;
@ -139,10 +157,9 @@
}
@media print {
.card_with_icon,
.card_with_icon_long {
.card_with_icon {
min-height: 220px;
padding: 16px 0;
padding: var(--space-l) 0;
&_icon {
height: 60px;
@ -155,7 +172,7 @@
&_title {
font-size: var(--font-s);
margin: 0 0 8px 0;
margin: 0 0 var(--space-s) 0;
}
&_description {

View file

@ -10,9 +10,8 @@ interface ICardWithIconProps {
description?: string;
value: number | string | null;
suffix?: string;
color?: string;
icon?: string;
long?: boolean;
size?: 's' | 'm' | 'l';
scoring?: IScoringProps;
}
@ -21,43 +20,47 @@ function CardWithIcon({
description,
value,
suffix,
color,
icon,
long = false,
size,
scoring,
}: ICardWithIconProps): React.ReactElement | null {
const { t } = useTranslation();
if (!value && value !== 0) return null;
const className = [style.card_with_icon];
if (size === 's') className.push(style.card_with_icon_small);
if (size === 'l') className.push(style.card_with_icon_long);
return (
<figure className={long
? style.card_with_icon_long
: style.card_with_icon}>
<figure className={className.join(' ')}>
{icon && (
<img
className={style.card_with_icon_icon}
src={icon}
/>
)}
<p
className={style.card_with_icon_value}
style={{ color: color || '' }}
>
<p className={style.card_with_icon_value}>
{value}
{suffix || ''}
</p>
<h4 className={style.card_with_icon_title}>
{t(title || '')}
</h4>
<figcaption className={style.card_with_icon_description}>
{t(description || '')}
</figcaption>
<Scoring
title={scoring?.title}
value={scoring?.value}
total={scoring?.total}
/>
{scoring ? (
<Scoring
title={scoring?.title}
value={scoring?.value}
total={scoring?.total}
/>
) : null}
</figure>
);
}
@ -65,9 +68,8 @@ function CardWithIcon({
CardWithIcon.defaultProps = {
description: '',
suffix: '',
color: undefined,
icon: undefined,
long: false,
size: 'm',
scoring: undefined,
};

View file

@ -1,16 +1,21 @@
import React, { useEffect } from 'react';
import { observer } from 'mobx-react-lite';
import Logo from 'ts/pages/PageWrapper/components/sidebar/Logo';
import globalScroll from 'ts/helpers/globalScroll';
import splashScreenStore from './store';
import style from './index.module.scss';
import progress from './progress.module.scss';
function SplashScreen(): React.ReactElement | null {
const SplashScreen = observer((): React.ReactElement | null => {
useEffect(() => {
globalScroll.off(5400);
}, []);
if (!splashScreenStore.isOpen) return;
setTimeout(() => {
splashScreenStore.hide();
}, 5400);
}, [splashScreenStore.isOpen]);
if (!splashScreenStore.isOpen) return null;
return (
<div className={style.splash_screen}>
@ -20,6 +25,6 @@ function SplashScreen(): React.ReactElement | null {
</div>
</div>
);
}
});
export default SplashScreen;

View file

@ -0,0 +1,27 @@
import { observable, action, makeObservable } from 'mobx';
import globalScroll from 'ts/helpers/globalScroll';
class SplashScreenStore {
isOpen: boolean = false;
constructor() {
makeObservable(this, {
isOpen: observable,
show: action,
hide: action,
});
}
show() {
this.isOpen = true;
globalScroll.off(5400);
}
hide() {
this.isOpen = false;
}
}
const splashScreen = new SplashScreenStore();
export default splashScreen;

View file

@ -26,16 +26,20 @@ export default class FileBuilderLineStat {
file.removedLines += fileChange.removedLines;
file.changedLines += fileChange.changedLines;
file.addedLinesByAuthor[commit.author] = file.addedLinesByAuthor[commit.author]
? (file.addedLinesByAuthor[commit.author] + fileChange.addedLines)
// TODO: performance
const addedLinesByAuthor = file.addedLinesByAuthor[commit.author];
file.addedLinesByAuthor[commit.author] = addedLinesByAuthor
? (addedLinesByAuthor + fileChange.addedLines)
: fileChange.addedLines;
file.removedLinesByAuthor[commit.author] = file.removedLinesByAuthor[commit.author]
? (file.removedLinesByAuthor[commit.author] + fileChange.removedLines)
const removedLinesByAuthor = file.removedLinesByAuthor[commit.author];
file.removedLinesByAuthor[commit.author] = removedLinesByAuthor
? (removedLinesByAuthor + fileChange.removedLines)
: fileChange.removedLines;
file.changedLinesByAuthor[commit.author] = file.changedLinesByAuthor[commit.author]
? (file.changedLinesByAuthor[commit.author] + fileChange.changedLines)
const changedLinesByAuthor = file.changedLinesByAuthor[commit.author];
file.changedLinesByAuthor[commit.author] = changedLinesByAuthor
? (changedLinesByAuthor + fileChange.changedLines)
: fileChange.changedLines;
}

View file

@ -11,6 +11,8 @@ const MASTER_BRANCH = {
let prevDate = new Date();
let refTimestampTime = {};
export default function getCommitInfo(
logString: string,
refEmailAuthor: IHashMap<string>,
@ -18,7 +20,7 @@ export default function getCommitInfo(
// "2021-02-09T12:59:17+03:00>Frolov Ivan>frolov@mail.ru>profile"
const parts = logString.split('>');
const sourceDate = parts.shift() || '';
const sourceDate = parts[0] || '';
let date = new Date(sourceDate);
if (isNaN(date.getDay())) {
console.log(`PARSE ERROR: Date parse error for: "${logString}"`);
@ -26,11 +28,14 @@ export default function getCommitInfo(
}
prevDate = date;
const day = date.getDay() - 1;
const timestamp = sourceDate.split('T')[0];
const timestamp = sourceDate.substring(0, 10); // split('T')[0];
if (!refTimestampTime[timestamp]) {
refTimestampTime[timestamp] = (new Date(timestamp)).getTime();
}
let author = parts.shift()?.replace(/[._]/gm, ' ') || '';
let email = parts.shift() || '';
if (!(/@/gim).test(email)) email = '';
let author = parts[1]?.replace(/[._]/gm, ' ') || '';
let email = parts[2] || '';
if (email.indexOf('@') === -1) email = '';
const authorID = author.replace(/\s|\t/gm, '');
if (authorID && refEmailAuthor[authorID] && refEmailAuthor[authorID] !== author) {
@ -52,7 +57,8 @@ export default function getCommitInfo(
refEmailAuthor[author] = email;
refEmailAuthor[authorID] = author;
const message = parts.join('>');
// performance
const message = logString.substring(parts[0]?.length + parts[1]?.length + parts[2]?.length + 3);
const commonInfo: any = {
date: sourceDate,
@ -64,7 +70,7 @@ export default function getCommitInfo(
year: date.getUTCFullYear(),
week: 0,
timestamp,
milliseconds: (new Date(timestamp)).getTime(),
milliseconds: refTimestampTime[timestamp],
author,
email,
@ -99,7 +105,7 @@ export default function getCommitInfo(
task = getTask(branch);
} else if (isBitbucketPR) {
commitType = COMMIT_TYPE.PR_BITBUCKET;
const messageParts = message.substring(14, Infinity).split(':');
const messageParts = message.substring(14).split(':');
prId = messageParts.shift();
task = getTask(messageParts.join(':'));
} else if (isAutoMerge) {

View file

@ -19,7 +19,7 @@ function getFilePath(path: string): string[] {
return [oldPath, newPath];
}
// "38 9 src/app.css" -> [38, 9, 9, 'src/app.css']
// "38 9 src/app.css" -> [38, 9, 'src/app.css']
export function getNumStatInfo(message: string) {
let [addedRaw, removedRaw, path] = message.split('\t');
@ -50,16 +50,17 @@ export function getNumStatInfo(message: string) {
}
// ":000000 100644 000000000 fc44b0a37 A public/logo192.png" -> ['A', 'public/logo192.png']
export function getRawInfo(message: string) {
const action = message[35];
const path = message.split('\t')[1];
return { path, action };
return {
action:message[35],
path: message.substring(37),
};
}
// "src/AppGit.css" -> { id: 'src/appgit.css', path: 'src/AppGit.css' }
export function getInfoFromPath(path: string): IFileChange {
const [oldPath, newPath] = getFilePath(path);
const id = oldPath.toLowerCase();
const id = oldPath.toLowerCase(); // TODO: performance
const newId = newPath?.toLowerCase();
return {

View file

@ -47,7 +47,7 @@ function getMenu(navigate: Function): any[] {
confirm.open({
title: 'Вы уверены что хотите выйти?',
}).then(() => {
dataGripStore.setCommits([]);
dataGripStore.asyncSetCommits([]);
navigate('/');
});
},

View file

@ -82,7 +82,7 @@ const Money = observer(({ user }: IPersonCommonProps): React.ReactElement => {
) : (
<div>
<CardWithIcon
long
size="l"
value={taskNumber
? getShortMoney(statistic.moneyWorked / taskNumber, 0)
: null}
@ -91,7 +91,7 @@ const Money = observer(({ user }: IPersonCommonProps): React.ReactElement => {
description="page.person.money.tasks.description"
/>
<CardWithIcon
long
size="l"
value={taskNumber
? getShortMoney(statistic.moneyWorked / statistic.commits, 0)
: null}

View file

@ -65,14 +65,14 @@ const Speed = observer(({ user }: IPersonCommonProps): React.ReactElement => {
<Title title="page.person.speed.max"/>
<div>
<CardWithIcon
long
size="l"
value={byTimestamp.tasksByTimestampCounter.max}
icon="./assets/cards/tasks.png"
title="page.person.speed.tasks.title"
description="page.person.speed.tasks.description"
/>
<CardWithIcon
long
size="l"
value={byTimestamp.commitsByTimestampCounter.max}
icon="./assets/cards/commits.png"
title="page.person.speed.maxCommits.title"

View file

@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import achievementByAuthor from 'ts/helpers/achievement/byCompetition';
import ACHIEVEMENT_TYPE from 'ts/helpers/achievement/constants/type';
import { getDate } from 'ts/helpers/formatter';
import CardWithIcon from 'ts/components/CardWithIcon';
import CardWithBanner from 'ts/components/CardWithIcon/components/Banner';
@ -48,6 +49,20 @@ const Total = observer(({ user }: IPersonCommonProps): React.ReactElement => {
<PageColumn>
<Title title="page.person.total.title"/>
<div>
{false && (
<>
<CardWithIcon
size="s"
value={getDate(statistic.firstCommit.timestamp)}
title="page.team.tasks.from"
/>
<CardWithIcon
size="s"
value={getDate(statistic.lastCommit.timestamp)}
title="page.team.tasks.to"
/>
</>
)}
<CardWithIcon
value={statistic.daysWorked}
icon="./assets/cards/work_days.png"
@ -88,7 +103,7 @@ const Total = observer(({ user }: IPersonCommonProps): React.ReactElement => {
total: scoringTotal.commits,
}}
/>
<CardWithBanner long />
<CardWithBanner size="l" />
</div>
{false && <Title title="page.person.character.title"/>}
{false && <Character user={statistic} />}

View file

@ -54,7 +54,7 @@ const Total = observer((): React.ReactElement => {
description="page.team.total.employment.description"
/>
<CardWithIcon
long
size="l"
value={workSpeed}
icon="./assets/cards/tasks_month.png"
title="page.team.total.workSpeed.title"
@ -99,7 +99,7 @@ const Total = observer((): React.ReactElement => {
description="page.team.total.weekendPayment.description"
/>
<CardWithIcon
long
size="l"
value={getShortMoney(moneySpeed)}
icon="./assets/cards/money_month.png"
title="page.team.total.moneySpeed.title"

View file

@ -2,6 +2,7 @@ import React from 'react';
import Console from 'ts/components/Console';
import Description from 'ts/components/Description';
import splashScreenStore from 'ts/components/SplashScreen/store';
import {
getStringFromFileList,
getStringsForParser,
@ -60,7 +61,8 @@ function Welcome() {
const files = Array.from(event.target.files);
const text = await getStringFromFileList(files);
const report = getStringsForParser(text);
dataGripStore.setCommits(report);
splashScreenStore.show();
dataGripStore.asyncSetCommits(report);
}}
/>
</label>

View file

@ -1,8 +1,9 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { Route, Routes } from 'react-router-dom';
import { observer } from 'mobx-react-lite';
import dataGripStore, { DataParseStatusEnum } from 'ts/store/DataGrip';
import dataGripStore from 'ts/store/DataGrip';
import viewNameStore, { ViewNameEnum } from 'ts/store/ViewName';
import DropZone from 'ts/components/DropZone';
import Sponsor from 'ts/components/Sponsor';
import SplashScreen from 'ts/components/SplashScreen';
@ -16,11 +17,7 @@ import Welcome from './Welcome/index';
import Settings from './Settings/index';
import DebugPage from './Debug/index';
interface IViewWithChartsProps {
showSplashScreen: boolean;
}
function ViewWithCharts({ showSplashScreen }: IViewWithChartsProps) {
function ViewWithCharts() {
return (
<>
<Sponsor />
@ -75,7 +72,6 @@ function ViewWithCharts({ showSplashScreen }: IViewWithChartsProps) {
)}
/>
</Routes>
{showSplashScreen && <SplashScreen />}
</>
);
}
@ -94,36 +90,38 @@ function ViewWithWelcome() {
}
const Main = observer(() => {
const [showSplashScreen, setShowSplashScreen] = useState<boolean>(true);
const status = dataGripStore.status;
const view = viewNameStore.view;
useEffect(() => {
// @ts-ignore
dataGripStore.setCommits(window?.report || []);
const list = window?.report || [];
if (list?.length) {
dataGripStore.asyncSetCommits(list);
} else {
viewNameStore.toggle(ViewNameEnum.WELCOME);
}
}, []);
useEffect(() => {
if (status !== DataParseStatusEnum.DONE || window.location.hash) return;
if (view !== ViewNameEnum.INFO || window.location.hash) return;
window.location.hash = '#/team/total';
}, [status]);
}, [view]);
if (status === DataParseStatusEnum.PROCESSING) return null;
if (view === ViewNameEnum.EMPTY) return null;
return (
<>
{status === DataParseStatusEnum.DONE && (
<ViewWithCharts showSplashScreen={showSplashScreen} />
)}
{status === DataParseStatusEnum.WAITING && (
{view === ViewNameEnum.WELCOME && (
<ViewWithWelcome />
)}
{view === ViewNameEnum.INFO && (
<ViewWithCharts />
)}
<SplashScreen />
<DropZone
onChange={(type: string, data: any[]) => {
setShowSplashScreen(false);
if (type === 'dump') dataGripStore.setCommits(data);
setTimeout(() => {
setShowSplashScreen(true);
});
if (type !== 'dump') return;
dataGripStore.asyncSetCommits(data);
}}
/>
</>

View file

@ -1,4 +1,4 @@
import { makeObservable, observable, action } from 'mobx';
import { action, makeObservable, observable } from 'mobx';
import ICommit, { ISystemCommit } from 'ts/interfaces/Commit';
@ -9,27 +9,16 @@ import Parser from 'ts/helpers/Parser';
import getTitle from 'ts/helpers/Title';
import { setDefaultValues } from 'ts/pages/Settings/helpers/getEmptySettings';
import splashScreenStore from 'ts/components/SplashScreen/store';
import { applicationHasCustom } from 'ts/helpers/RPC';
import Depersonalized from 'ts/helpers/Depersonalized';
import filtersInHeaderStore from './FiltersInHeader';
import viewNameStore, { ViewNameEnum } from './ViewName';
export enum DataParseStatusEnum {
WAITING = 'waiting',
PROCESSING = 'processing',
DONE = 'done',
}
const PROCESSING_DELAY = 300;
interface IDataGripStore {
commits: ICommit[];
dataGrip: any;
fileGrip: any;
status: DataParseStatusEnum;
isDepersonalized: boolean;
setCommits: (log?: string[]) => void;
}
class DataGripStore implements IDataGripStore {
class DataGripStore {
commits: any[] = [];
dataGrip: any = null;
@ -40,55 +29,66 @@ class DataGripStore implements IDataGripStore {
isDepersonalized: boolean = false;
status: DataParseStatusEnum = DataParseStatusEnum.PROCESSING;
constructor() {
makeObservable(this, {
commits: observable,
dataGrip: observable,
fileGrip: observable,
hash: observable,
isDepersonalized: observable,
status: observable,
setCommits: action,
asyncSetCommits: action,
processingStep01: action,
processingStep03: action,
depersonalized: action,
updateStatistic: action,
});
}
setCommits(dump?: string[]) {
asyncSetCommits(dump?: string[]) {
if (!dump?.length) return;
splashScreenStore.show();
setTimeout(() => this.processingStep01(dump), PROCESSING_DELAY);
}
processingStep01(dump?: string[]) {
dataGrip.clear();
fileGrip.clear();
const commits = Parser(dump || []);
if (!commits.length) {
splashScreenStore.hide();
return;
}
setTimeout(() => this.processingStep02(commits), PROCESSING_DELAY);
}
processingStep02(commits: (ICommit | ISystemCommit)[]) {
commits.sort((a, b) => a.milliseconds - b.milliseconds);
commits.forEach((commit: ICommit | ISystemCommit) => {
dataGrip.addCommit(commit);
fileGrip.addCommit(commit);
});
fileGrip.updateTotalInfo();
setTimeout(() => this.processingStep03(commits), PROCESSING_DELAY);
}
processingStep03(commits: (ICommit | ISystemCommit)[]) {
fileGrip.updateTotalInfo();
this.commits = commits;
this.status = this.commits.length
? DataParseStatusEnum.DONE
: DataParseStatusEnum.WAITING;
filtersInHeaderStore.updateByCommits(
dataGrip.firstLastCommit.minData,
dataGrip.firstLastCommit.maxData,
);
setDefaultValues(dataGrip.firstLastCommit.minData, dataGrip.firstLastCommit.maxData);
if (this.status === DataParseStatusEnum.DONE) {
filtersInHeaderStore.updateByCommits(
dataGrip.firstLastCommit.minData,
dataGrip.firstLastCommit.maxData,
);
setDefaultValues(dataGrip.firstLastCommit.minData, dataGrip.firstLastCommit.maxData);
dataGrip.updateTotalInfo();
achievements.updateByGrip(dataGrip, fileGrip);
}
dataGrip.updateTotalInfo();
achievements.updateByGrip(dataGrip, fileGrip);
viewNameStore.toggle(ViewNameEnum.INFO);
this.#updateRender();
console.dir(this.dataGrip);
console.dir(this.fileGrip);
if (!applicationHasCustom.title) {
document.title = getTitle(this.dataGrip, this.fileGrip, this.commits);
}

26
src/ts/store/ViewName.tsx Normal file
View file

@ -0,0 +1,26 @@
import { observable, action, makeObservable } from 'mobx';
export enum ViewNameEnum {
EMPTY = 'empty',
WELCOME = 'welcome',
INFO = 'info',
}
class ViewNameStore {
view: ViewNameEnum = ViewNameEnum.EMPTY;
constructor() {
makeObservable(this, {
view: observable,
toggle: action,
});
}
toggle(view: ViewNameEnum) {
this.view = view;
}
}
const viewNameStore = new ViewNameStore();
export default viewNameStore;