mirror of
https://github.com/bakhirev/assayo.git
synced 2024-11-16 08:11:40 +00:00
update
This commit is contained in:
parent
05a3874efd
commit
d8b49a579b
|
@ -29,10 +29,11 @@ class SponsorStore {
|
|||
const ONE_MINUTE = 60 * 1000;
|
||||
setInterval(() => {
|
||||
if (this.type) return;
|
||||
this.type = Math.random() > 0.5
|
||||
? MODAL_TYPE.MONEY
|
||||
: MODAL_TYPE.SHARE;
|
||||
}, 10 * ONE_MINUTE);
|
||||
this.type = MODAL_TYPE.SHARE;
|
||||
// Math.random() > 0.5
|
||||
// ? MODAL_TYPE.MONEY
|
||||
// : MODAL_TYPE.SHARE;
|
||||
}, 7 * ONE_MINUTE);
|
||||
}
|
||||
|
||||
open() {
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
|
||||
import PageWrapper from 'ts/components/Page/wrapper';
|
||||
import getOptions from 'ts/components/LineChart/helpers/getOptions';
|
||||
|
@ -25,18 +22,23 @@ function getTimeZoneChart(authors: any[]) {
|
|||
increment(acc, author.lastCommit.timezone.replace(':', '.'));
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const options = getOptions({
|
||||
order: Object.keys(details).sort(),
|
||||
limit: 5,
|
||||
suffix: 'page.team.country.chart.item',
|
||||
});
|
||||
|
||||
return [options, details];
|
||||
}
|
||||
|
||||
const PieCharts = observer((): React.ReactElement | null => {
|
||||
const authors = dataGripStore.dataGrip.author.statistic;
|
||||
const rows = dataGripStore.dataGrip.country.statistic;
|
||||
const [countryOptions, countryDetails] = getCountryChart(rows);
|
||||
interface PieChartsProps {
|
||||
authors: any[];
|
||||
countries: any[];
|
||||
}
|
||||
|
||||
function PieCharts({ authors, countries }: PieChartsProps): React.ReactElement | null {
|
||||
const [countryOptions, countryDetails] = getCountryChart(countries);
|
||||
const [timezoneOptions, timezoneDetails] = getTimeZoneChart(authors);
|
||||
|
||||
return (
|
||||
|
@ -57,6 +59,6 @@ const PieCharts = observer((): React.ReactElement | null => {
|
|||
</PageColumn>
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default PieCharts;
|
||||
|
|
68
src/ts/pages/Team/components/Country/components/Filters.tsx
Normal file
68
src/ts/pages/Team/components/Country/components/Filters.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
|
||||
import SelectWithButtons from 'ts/components/UiKit/components/SelectWithButtons';
|
||||
import UiKitCheckbox from 'ts/components/UiKit/components/Checkbox';
|
||||
import Title from 'ts/components/Title';
|
||||
|
||||
import IFilters from '../interfaces/Filters';
|
||||
import { getOptions } from '../helpers';
|
||||
|
||||
import style from '../styles/index.module.scss';
|
||||
|
||||
interface FiltersProps {
|
||||
filters: IFilters,
|
||||
onChange: Function;
|
||||
}
|
||||
|
||||
const Filters = observer(({
|
||||
filters,
|
||||
onChange,
|
||||
}: FiltersProps) => {
|
||||
const companies = dataGripStore.dataGrip.company.statistic;
|
||||
const companyOptions = useMemo(() => getOptions(companies), companies);
|
||||
const update = (property: string, value: any) => {
|
||||
onChange({
|
||||
...filters,
|
||||
[property]: value,
|
||||
hash: Math.random(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title title="common.filters"/>
|
||||
<div className={style.team_country_filter}>
|
||||
<SelectWithButtons
|
||||
title="page.team.tree.filters.author"
|
||||
className={style.team_country_filter_select}
|
||||
value={filters.company}
|
||||
options={companyOptions}
|
||||
onChange={(company: string) => update('company', company)}
|
||||
/>
|
||||
<UiKitCheckbox
|
||||
title="page.team.country.filters.active"
|
||||
className={style.team_country_filter_checkbox}
|
||||
value={filters.isActive}
|
||||
onChange={() => update('isActive', !filters.isActive)}
|
||||
/>
|
||||
<UiKitCheckbox
|
||||
title="page.team.country.filters.dismissed"
|
||||
className={style.team_country_filter_checkbox}
|
||||
value={filters.isDismissed}
|
||||
onChange={() => update('isDismissed', !filters.isDismissed)}
|
||||
/>
|
||||
<UiKitCheckbox
|
||||
title="page.team.country.filters.staff"
|
||||
className={style.team_country_filter_checkbox}
|
||||
value={filters.isStaff}
|
||||
onChange={() => update('isStaff', !filters.isStaff)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Filters;
|
|
@ -1,77 +1,20 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
|
||||
import dataGripStore from 'ts/store/DataGrip';
|
||||
|
||||
import SelectWithButtons from 'ts/components/UiKit/components/SelectWithButtons';
|
||||
import UiKitCheckbox from 'ts/components/UiKit/components/Checkbox';
|
||||
import TimeZoneMap from 'ts/components/TimeZoneMap';
|
||||
import PageWrapper from 'ts/components/Page/Box';
|
||||
import Title from 'ts/components/Title';
|
||||
import { t } from 'ts/helpers/Localization';
|
||||
|
||||
import style from '../../../styles/country.module.scss';
|
||||
|
||||
function getOptions(companies: any[]) {
|
||||
const options = companies.map((item: any) => ({ id: item.company, title: item.company }));
|
||||
return [
|
||||
{ id: '', title: t('page.common.filter.allUsers') },
|
||||
{ id: Math.random(), title: 'Unknown' },
|
||||
...options,
|
||||
];
|
||||
interface CustomMapProps {
|
||||
authors: any[];
|
||||
}
|
||||
|
||||
const CustomMap = observer(() => {
|
||||
const companies = dataGripStore.dataGrip.company.statistic;
|
||||
const companyOptions = useMemo(() => getOptions(companies), companies);
|
||||
|
||||
const [company, setCompany] = useState<string>('');
|
||||
const [isStaff, setIsStaff] = useState<boolean>(true);
|
||||
const [isActive, setIsActive] = useState<boolean>(true);
|
||||
const [isDismissed, setIsDismissed] = useState<boolean>(true);
|
||||
|
||||
const authors = dataGripStore.dataGrip.author.statistic
|
||||
.filter((author: any) => {
|
||||
if (company && author.lastCompany !== company) return false;
|
||||
if (!isStaff && author.isStaff) return false;
|
||||
if (!isActive && !author.isDismissed && !author.isStaff) return false;
|
||||
if (!isDismissed && author.isDismissed && !author.isStaff) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
function CustomMap({ authors }: CustomMapProps) {
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Title title="page.team.country.byTimezone"/>
|
||||
<TimeZoneMap authors={authors}/>
|
||||
<div className={style.team_country_filter}>
|
||||
<UiKitCheckbox
|
||||
title="page.team.country.filters.active"
|
||||
className={style.team_country_filter_checkbox}
|
||||
value={isActive}
|
||||
onChange={() => setIsActive(!isActive)}
|
||||
/>
|
||||
<UiKitCheckbox
|
||||
title="page.team.country.filters.dismissed"
|
||||
className={style.team_country_filter_checkbox}
|
||||
value={isDismissed}
|
||||
onChange={() => setIsDismissed(!isDismissed)}
|
||||
/>
|
||||
<UiKitCheckbox
|
||||
title="page.team.country.filters.staff"
|
||||
className={style.team_country_filter_checkbox}
|
||||
value={isStaff}
|
||||
onChange={() => setIsStaff(!isStaff)}
|
||||
/>
|
||||
<SelectWithButtons
|
||||
title="page.team.tree.filters.author"
|
||||
className={style.team_country_filter_select}
|
||||
value={company}
|
||||
options={companyOptions}
|
||||
onChange={(id: string) => setCompany(id)}
|
||||
/>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default CustomMap;
|
||||
|
|
48
src/ts/pages/Team/components/Country/helpers/index.ts
Normal file
48
src/ts/pages/Team/components/Country/helpers/index.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { t } from 'ts/helpers/Localization';
|
||||
|
||||
import IFilters from '../interfaces/Filters';
|
||||
|
||||
export function getOptions(companies: any[]) {
|
||||
const options = companies.map((item: any) => ({ id: item.company, title: item.company }));
|
||||
return [
|
||||
{ id: '', title: t('page.common.filter.allUsers') },
|
||||
{ id: Math.random(), title: 'Unknown' },
|
||||
...options,
|
||||
];
|
||||
}
|
||||
|
||||
export function getDefaultFilters(): IFilters {
|
||||
return {
|
||||
hash: Math.random(),
|
||||
isActive: true,
|
||||
isDismissed: true,
|
||||
isStaff: true,
|
||||
company: '',
|
||||
};
|
||||
}
|
||||
|
||||
export function getFilterForAuthors(filters: IFilters) {
|
||||
return (author: any) => {
|
||||
if (filters.company && author.lastCompany !== filters.company) return false;
|
||||
if (!filters.isStaff && author.isStaff) return false;
|
||||
if (!filters.isActive && !author.isDismissed && !author.isStaff) return false;
|
||||
if (!filters.isDismissed && author.isDismissed && !author.isStaff) return false;
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export function getCountryByAuthors(authors: any[]) {
|
||||
const isCorrectAuthor = authors.reduce((acc: any, item: any) => {
|
||||
acc.set(item.author, true);
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
return (item: any) => {
|
||||
const employments = item.employments
|
||||
.filter((name: string) => isCorrectAuthor.has(name));
|
||||
|
||||
return employments.length
|
||||
? ({ ...item, employments })
|
||||
: null;
|
||||
};
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import ISort from 'ts/interfaces/Sort';
|
||||
|
@ -15,28 +15,56 @@ import Countries from './components/Countries';
|
|||
import CountryCharts from './components/Charts';
|
||||
import fullScreen from 'ts/store/FullScreen';
|
||||
|
||||
import CustomMap from './components/Map';
|
||||
import IFilters from './interfaces/Filters';
|
||||
import Filters from './components/Filters';
|
||||
import Travel from './components/Travel';
|
||||
import CustomMap from './components/Map';
|
||||
|
||||
import { getCountryByAuthors, getDefaultFilters, getFilterForAuthors } from './helpers';
|
||||
|
||||
const Country = observer(({
|
||||
mode,
|
||||
}: ICommonPageProps): React.ReactElement | null => {
|
||||
const authors = dataGripStore.dataGrip.author.statistic;
|
||||
const countryRows = dataGripStore.dataGrip.country.statistic;
|
||||
const travel = authors.filter((dot: any) => dot?.country?.length)
|
||||
.sort((a: any, b: any) => b?.country?.length - a?.country?.length);
|
||||
const [filters, setFilters] = useState<IFilters>(getDefaultFilters());
|
||||
|
||||
const dataGripAuthors = dataGripStore.dataGrip.author.statistic;
|
||||
const authors = useMemo(() => (
|
||||
dataGripAuthors.filter(getFilterForAuthors(filters))
|
||||
), [dataGripAuthors, filters.hash]);
|
||||
|
||||
const dataGripCountries = dataGripStore.dataGrip.country.statistic;
|
||||
const countries = useMemo(() => (
|
||||
dataGripCountries.map(getCountryByAuthors(authors)).filter((v: any) => v)
|
||||
), [dataGripCountries, filters.hash]);
|
||||
|
||||
const travel = useMemo(() => (
|
||||
authors
|
||||
.filter((dot: any) => dot?.country?.length)
|
||||
.sort((a: any, b: any) => b?.country?.length - a?.country?.length)
|
||||
), [authors, filters.hash]);
|
||||
|
||||
const canShowByCountries = (!fullScreen.isOpen || fullScreen.mode === 'countries');
|
||||
const canShowByTravel = (!fullScreen.isOpen || fullScreen.mode === 'travel') && travel.length;
|
||||
|
||||
if (!countryRows?.length) {
|
||||
if (!countries?.length) {
|
||||
return mode !== 'print' ? (<NothingFound/>) : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!fullScreen.isOpen && <CustomMap />}
|
||||
{!fullScreen.isOpen && <CountryCharts />}
|
||||
{!fullScreen.isOpen && (
|
||||
<>
|
||||
<Filters
|
||||
filters={filters}
|
||||
onChange={setFilters}
|
||||
/>
|
||||
<CustomMap authors={authors} />
|
||||
<CountryCharts
|
||||
authors={authors}
|
||||
countries={countries}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{canShowByCountries ? (
|
||||
<>
|
||||
|
@ -44,13 +72,13 @@ const Country = observer(({
|
|||
<DataLoader
|
||||
to="response"
|
||||
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
|
||||
content: countryRows, pagination, sort, mode,
|
||||
content: countries, pagination, sort, mode,
|
||||
})}
|
||||
watch={`${mode}${dataGripStore.hash}`}
|
||||
watch={`${mode}${dataGripStore.hash}${filters.hash}`}
|
||||
>
|
||||
<Countries
|
||||
mode={mode}
|
||||
rowsForExcel={countryRows}
|
||||
rowsForExcel={countries}
|
||||
/>
|
||||
<Pagination/>
|
||||
</DataLoader>
|
||||
|
@ -65,11 +93,11 @@ const Country = observer(({
|
|||
loader={(pagination?: IPaginationRequest, sort?: ISort[]) => getFakeLoader({
|
||||
content: travel, pagination, sort, mode,
|
||||
})}
|
||||
watch={`${mode}${dataGripStore.hash}`}
|
||||
watch={`${mode}${dataGripStore.hash}${filters.hash}`}
|
||||
>
|
||||
<Travel
|
||||
mode={mode}
|
||||
rowsForExcel={countryRows}
|
||||
rowsForExcel={countries}
|
||||
/>
|
||||
<Pagination/>
|
||||
</DataLoader>
|
||||
|
|
10
src/ts/pages/Team/components/Country/interfaces/Filters.ts
Normal file
10
src/ts/pages/Team/components/Country/interfaces/Filters.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
interface IFilters {
|
||||
company: string;
|
||||
isStaff: boolean;
|
||||
isActive: boolean;
|
||||
isDismissed: boolean;
|
||||
|
||||
hash: number;
|
||||
}
|
||||
|
||||
export default IFilters;
|
|
@ -0,0 +1,16 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.team_country_filter {
|
||||
margin: 0 0 var(--space-xxl);
|
||||
|
||||
&_checkbox,
|
||||
&_select {
|
||||
display: inline-block;
|
||||
margin-right: var(--space-xxl);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&_select {
|
||||
min-width: 350px;
|
||||
}
|
||||
}
|
|
@ -287,7 +287,7 @@ will be marked as a jump in "deleted" and "added" lines.
|
|||
§ page.person.week.workDay: weekdays
|
||||
§ page.person.week.weekends: weekends
|
||||
§ page.sponsor.title: Please, support this project
|
||||
§ page.sponsor.share.description: Tell about our [project|https://github.com/bakhirev/assayo] on social networks! You can share [article|https://habr.com/ru/articles/763342/], [post|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] or make a video review.
|
||||
§ page.sponsor.share.description: Tell about our [project|https://github.com/bakhirev/assayo] on social networks! You can share [article|https://habr.com/ru/articles/763342/], [post|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] or [video|https://www.youtube.com/watch?v=jwCp_-bhrCQ].
|
||||
§ page.sponsor.share.button: Copy the link
|
||||
§ page.sponsor.money.description: We will be glad if you support us with any amount! All funds will be used for the further development of the project.
|
||||
§ page.sponsor.money.qr: One-time payment (only Russia)
|
||||
|
|
|
@ -289,7 +289,7 @@ will be marked as a jump in "deleted" and "added" lines.
|
|||
§ page.person.week.workDay: weekdays
|
||||
§ page.person.week.weekends: weekends
|
||||
§ page.sponsor.title: Please, support this project
|
||||
§ page.sponsor.share.description: Tell about our [project|https://github.com/bakhirev/assayo] on social networks! You can share [article|https://habr.com/ru/articles/763342/], [post|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] or make a video review.
|
||||
§ page.sponsor.share.description: Tell about our [project|https://github.com/bakhirev/assayo] on social networks! You can share [article|https://habr.com/ru/articles/763342/], [post|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] or [video|https://www.youtube.com/watch?v=jwCp_-bhrCQ].
|
||||
§ page.sponsor.share.button: Copy the link
|
||||
§ page.sponsor.money.description: We will be glad if you support us with any amount! All funds will be used for the further development of the project.
|
||||
§ page.sponsor.money.qr: One-time payment (only Russia)
|
||||
|
|
|
@ -287,7 +287,7 @@ git puede Mostrar una pequeña cantidad de cambios en las estadísticas, y el re
|
|||
§ page.person.week.workDay: entresemana
|
||||
§ page.person.week.weekends: día de descanso
|
||||
§ page.sponsor.title: Please, support this project
|
||||
§ page.sponsor.share.description: Tell about our [project|https://github.com/bakhirev/assayo] on social networks! You can share [article|https://habr.com/ru/articles/763342/], [post|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] or make a video review.
|
||||
§ page.sponsor.share.description: Tell about our [project|https://github.com/bakhirev/assayo] on social networks! You can share [article|https://habr.com/ru/articles/763342/], [post|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] or [video|https://www.youtube.com/watch?v=jwCp_-bhrCQ].
|
||||
§ page.sponsor.share.button: Copy the link
|
||||
§ page.sponsor.money.description: We will be glad if you support us with any amount! All funds will be used for the further development of the project.
|
||||
§ page.sponsor.money.qr: One-time payment (only Russia)
|
||||
|
|
|
@ -286,7 +286,7 @@ export default `
|
|||
§ page.person.week.workDay: jours de semaine
|
||||
§ page.person.week.weekends: congés
|
||||
§ page.sponsor.title: Please, support this project
|
||||
§ page.sponsor.share.description: Tell about our [project|https://github.com/bakhirev/assayo] on social networks! You can share [article|https://habr.com/ru/articles/763342/], [post|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] or make a video review.
|
||||
§ page.sponsor.share.description: Tell about our [project|https://github.com/bakhirev/assayo] on social networks! You can share [article|https://habr.com/ru/articles/763342/], [post|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] or [video|https://www.youtube.com/watch?v=jwCp_-bhrCQ].
|
||||
§ page.sponsor.share.button: Copy the link
|
||||
§ page.sponsor.money.description: We will be glad if you support us with any amount! All funds will be used for the further development of the project.
|
||||
§ page.sponsor.money.qr: One-time payment (only Russia)
|
||||
|
|
|
@ -289,7 +289,7 @@ will be marked as a jump in "deleted" and "added" lines.
|
|||
§ page.person.week.workDay: weekdays
|
||||
§ page.person.week.weekends: weekends
|
||||
§ page.sponsor.title: Please, support this project
|
||||
§ page.sponsor.share.description: Tell about our [project|https://github.com/bakhirev/assayo] on social networks! You can share [article|https://habr.com/ru/articles/763342/], [post|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] or make a video review.
|
||||
§ page.sponsor.share.description: Tell about our [project|https://github.com/bakhirev/assayo] on social networks! You can share [article|https://habr.com/ru/articles/763342/], [post|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] or [video|https://www.youtube.com/watch?v=jwCp_-bhrCQ].
|
||||
§ page.sponsor.share.button: Copy the link
|
||||
§ page.sponsor.money.description: We will be glad if you support us with any amount! All funds will be used for the further development of the project.
|
||||
§ page.sponsor.money.qr: One-time payment (only Russia)
|
||||
|
|
|
@ -289,7 +289,7 @@ will be marked as a jump in "deleted" and "added" lines.
|
|||
§ page.person.week.workDay: weekdays
|
||||
§ page.person.week.weekends: weekends
|
||||
§ page.sponsor.title: Please, support this project
|
||||
§ page.sponsor.share.description: Tell about our [project|https://github.com/bakhirev/assayo] on social networks! You can share [article|https://habr.com/ru/articles/763342/], [post|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] or make a video review.
|
||||
§ page.sponsor.share.description: Tell about our [project|https://github.com/bakhirev/assayo] on social networks! You can share [article|https://habr.com/ru/articles/763342/], [post|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] or [video|https://www.youtube.com/watch?v=jwCp_-bhrCQ].
|
||||
§ page.sponsor.share.button: Copy the link
|
||||
§ page.sponsor.money.description: We will be glad if you support us with any amount! All funds will be used for the further development of the project.
|
||||
§ page.sponsor.money.qr: One-time payment (only Russia)
|
||||
|
|
|
@ -289,7 +289,7 @@ git может показать малое количество изменени
|
|||
§ page.person.week.workDay: будни
|
||||
§ page.person.week.weekends: выходные
|
||||
§ page.sponsor.title: Поддержите проект
|
||||
§ page.sponsor.share.description: Расскажите о нашем [проекте|https://github.com/bakhirev/assayo] в соцсетях! Можно поделиться [статьей|https://habr.com/ru/articles/763342/], [постом|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] или сделать видео-обзор.
|
||||
§ page.sponsor.share.description: Расскажите о нашем [проекте|https://github.com/bakhirev/assayo] в соцсетях! Можно поделиться [статьей|https://habr.com/ru/articles/763342/], [постом|https://www.reddit.com/r/ITManagers/comments/1e5k291/the_visualization_and_analysis_of_git_commit/] или [видео|https://www.youtube.com/watch?v=jwCp_-bhrCQ].
|
||||
§ page.sponsor.share.button: Копировать ссылку
|
||||
§ page.sponsor.money.description: Мы будем рады, если вы поддержите нас любой суммой! Все средства пойдут на дальнейшее развитие проекта.
|
||||
§ page.sponsor.money.qr: Разовый платёж (СБП)
|
||||
|
|
Loading…
Reference in a new issue