feat: align React online-board start page styling with Angular version

- Updated feature card grid from CSS Grid to Flexbox (2x2 layout with 50% width)
- Added background image and title icon SVGs for visual parity
- Changed card title colors from dark gray to blue (#0066cc) matching Angular links
- Fixed padding and spacing to match Angular (50px sections, 65px icon offset)
- Added data-testid attributes for E2E testing
- Created comprehensive visual design alignment report documenting changes
This commit is contained in:
gnezim
2026-04-06 02:26:51 +03:00
parent f6c126a804
commit 1a3256147a
62 changed files with 2738 additions and 9 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+32
View File
@@ -0,0 +1,32 @@
report({
"testSuite": "BackstopJS",
"tests": [
{
"pair": {
"reference": "../bitmaps_reference/aeroflot_flights_Start_Page_0_document_0_desktop.png",
"test": "../bitmaps_test_react/20260406-022103/aeroflot_flights_Start_Page_0_document_0_desktop.png",
"selector": "document",
"fileName": "aeroflot_flights_Start_Page_0_document_0_desktop.png",
"label": "Start Page",
"requireSameDimensions": true,
"misMatchThreshold": 0,
"url": "http://localhost:3001/ru-ru/onlineboard",
"expect": 0,
"viewportLabel": "desktop",
"diff": {
"isSameDimensions": true,
"dimensionDifference": {
"width": 0,
"height": 0
},
"rawMisMatchPercentage": 54.57237654320988,
"misMatchPercentage": "54.57",
"analysisTime": 52
},
"diffImage": "../bitmaps_test_react/20260406-022103/failed_diff_aeroflot_flights_Start_Page_0_document_0_desktop.png"
},
"status": "fail"
}
],
"id": "aeroflot_flights"
});
File diff suppressed because one or more lines are too long
+340
View File
@@ -0,0 +1,340 @@
'use strict';
const noop = function (){};
let LCS_DIFF_ARRAY_METHOD = undefined;
// debugger
if (typeof require !== 'undefined') {
LCS_DIFF_ARRAY_METHOD = require('diff').diffArrays;
} else {
try {
LCS_DIFF_ARRAY_METHOD = JsDiff.diffArrays;
} catch(err) {
console.error(err);
}
}
const rowSpread = 1;
const spread = 50; // range of adjacent pixels to aggregate when calculating diff
const IS_ADDED_WORD = '0_255_0_255';
const IS_REMOVED_WORD = '255_0_0_255';
const IS_ADDED_AND_REMOVED_WORD = '0_255_255_255';
const IS_SAME_WORD = '';
const OPACITY = '40'; // 0-255 range
/**
* Applies Longest-Common-Subsequence-Diff algorithm to imageData formatted arrays
*
* @param {Uint8ClampedArray} [reference] baseline image
* @param {Uint8ClampedArray} [test] test image
*
* @returns {Uint8ClampedArray} diff image
*
*/
if (typeof module !== 'undefined') {
module.exports = diverged;
}
function diverged(reference, test, h, w) {
console.time("diverged_total_time");
const spread = Math.floor(h / 80); //override
console.log('spread:', spread);
console.time("imgDataToWords");
const img1wordArr = imgDataToWords(reference);
const img2wordArr = imgDataToWords(test);
console.timeEnd("imgDataToWords");
console.time("imgDataWordArrToColsAndRows");
let cols_rows_ref = imgDataWordArrToColsAndRows(img1wordArr, h, w);
let cols_rows_test = imgDataWordArrToColsAndRows(img2wordArr, h, w);
console.timeEnd("imgDataWordArrToColsAndRows");
console.time("groupAdjacent");
const columnRef = groupAdjacent(cols_rows_ref.columns, spread, h, w);
const columnTest = groupAdjacent(cols_rows_test.columns, spread, h, w);
console.timeEnd("groupAdjacent");
console.time("columnDiffRaw");
const columnDiffRaw = diffArr(columnRef, columnTest, h, w);
console.timeEnd("columnDiffRaw");
console.time("reduceColumnDiffRaw");
const reducedColumnDiff = reduceColumnDiffRaw(columnDiffRaw, h, w);
console.timeEnd("reduceColumnDiffRaw");
// console.log("reducedColumnDiff>>>", reducedColumnDiff);
console.time("unGroupAdjacent");
const expandedColumns = ungroupAdjacent(reducedColumnDiff, spread, cols_rows_test.columns, h, w);
console.timeEnd("unGroupAdjacent");
console.time("columnWordDataToImgDataFormatAsWords");
const convertedColumnDiffImgData = columnWordDataToImgDataFormatAsWords(expandedColumns, h, w);
console.timeEnd("columnWordDataToImgDataFormatAsWords");
// console.log("convertedColumnDiffImgData>>>", convertedColumnDiffImgData);
console.time("imgDataWordsToClampedImgData");
const imgDataArr = convertImgDataWordsToClampedImgData(convertedColumnDiffImgData);
console.timeEnd("imgDataWordsToClampedImgData");
// console.log("imgDataArr>>>", imgDataArr);
console.timeEnd("diverged_total_time");
return imgDataArr;
}
/**
* ========= HELPERS ========
*/
function columnWordDataToImgDataFormatAsWords(columns, h, w) {
const imgDataWordsLength = w * h;
let convertedArr = new Array(imgDataWordsLength);
for (var i = 0; i < imgDataWordsLength; i++) {
const {column, depth} = serialToColumnMap(i, h, w);
convertedArr[i] = columns[column][depth];
}
return convertedArr;
}
function convertImgDataWordsToClampedImgData(wordsArr) {
let convertedArr = new Uint8ClampedArray(wordsArr.length * 4);
for (var i = 0; i < wordsArr.length; i++) {
const convertedOffset = i * 4;
const segments = wordsArr[i].split('_');
convertedArr[convertedOffset] = segments[0];
convertedArr[convertedOffset+1] = segments[1];
convertedArr[convertedOffset+2] = segments[2];
convertedArr[convertedOffset+3] = segments[3];
}
return convertedArr;
}
function reduceColumnDiffRaw(columnDiffs, h, w) {
let reducedColumns = new Array(columnDiffs.length);
for (let columnIndex = 0; columnIndex < columnDiffs.length; columnIndex++) {
const columnDiff = columnDiffs[columnIndex];
let resultColumn = new Array();
let removedCounter = 0;
let resultClass = '';
let segment = [];
let debug = false;
for (let depthIndex = 0; depthIndex < columnDiff.length; depthIndex++) {
let segmentLength = 0;
// Categorize the current segment
if (columnDiff[depthIndex].removed) {
segmentLength = columnDiff[depthIndex].count;
removedCounter += segmentLength;
resultClass = IS_REMOVED_WORD;
} else {
if (columnDiff[depthIndex].added) {
if (removedCounter) {
resultClass = IS_ADDED_AND_REMOVED_WORD;
} else {
resultClass = IS_ADDED_WORD;
}
} else {
resultClass = IS_SAME_WORD;
}
segmentLength = columnDiff[depthIndex].count;
if (removedCounter > 0) {
if (segmentLength > removedCounter) {
segmentLength -= removedCounter;
removedCounter = 0;
} else {
removedCounter -= segmentLength;
segmentLength = 0;
}
}
}
// Limit segmentLength to total length of column
if (!segmentLength) {
continue;
} else {
segmentLength = Math.min(segmentLength, h - resultColumn.length);
}
const printSampleMap = false;
if (!printSampleMap || resultClass !== IS_SAME_WORD){
segment = new Array(segmentLength).fill(resultClass);
} else {
// reduced resolution image
segment = columnDiff[depthIndex].value.slice(0,segmentLength).map((value, i) => {
if (/|/.test(value)) {
return value.split('|')[0];
}
return value;
});
}
resultColumn = resultColumn.concat(segment);
if (resultColumn.length > h) {
console.log('WARNING -- this value is out of bounds!')
}
}
reducedColumns[columnIndex] = resultColumn;
}
return reducedColumns;
}
function diffArr(refArr, testArr, h, w) {
let rawResultArr = [];
for (let i = 0; i < refArr.length; i++) {
rawResultArr.push(LCS_DIFF_ARRAY_METHOD(refArr[i], testArr[i]));
}
return rawResultArr;
}
function groupAdjacent(columns, spread, h, w) {
if (!spread) {
return columns;
}
/**
* [getAdjacentArrayBounds retuns existing adjacent lower and upper column bounds]
* @param {[int]} pointer [current index]
* @param {[int]} spread [distance from index]
* @param {[int]} length [total length]
* @return {[array]} [0] lower bound, [1] upper bound
*/
function getAdjacentArrayBounds(pointer, spread, length) {
return [
// Math.max(0, pointer - spread),
Math.max(0, pointer),
Math.min(length - 1, pointer + spread)
]
}
function getInterpolatedSequence(beginning, end) {
const interpolated = [];
for (let step = beginning; step <= end; step++) {
interpolated.push(step);
}
return interpolated;
}
function getCompositeColumnDepthValues(columns, sequence, depth) {
return sequence.reduce((acc, column) => {
return acc.concat(columns[column][depth]);
}, [])
}
function getCompositeRowIndexValues(groupedColumns, sequence, column) {
return sequence.reduce((acc, depth) => {
return acc.concat(groupedColumns[column][depth]);
}, [])
}
const groupedColumns = new Array();
let columnPointer = 0;
while (columnPointer < w) {
const adjacentColumnBounds = getAdjacentArrayBounds(columnPointer, spread, w);
const interpolatedColumns = getInterpolatedSequence(...adjacentColumnBounds);
const columnComposite = new Array();
for (var depth = 0; depth < h; depth++) {
columnComposite[depth] = getCompositeColumnDepthValues(columns, interpolatedColumns, depth).join('|');
}
groupedColumns.push(columnComposite);
columnPointer += spread;
}
const groupedRows = new Array();
if (rowSpread > 1) {
for (var index = 0; index < groupedColumns.length; index++) {
const rowComposite = new Array();
let depthPointer = 0;
while (depthPointer < h) {
const adjacentRowBounds = getAdjacentArrayBounds(depthPointer, rowSpread, h);
const interpolatedRows = getInterpolatedSequence(...adjacentRowBounds);
rowComposite.push(getCompositeRowIndexValues(groupedColumns, interpolatedRows, index).join(','));
depthPointer += rowSpread;
}
groupedRows[index] = rowComposite;
}
}
return groupedRows.length ? groupedRows : groupedColumns ;
}
function ungroupAdjacent(grouped, spread, columnUnderlay, h, w) {
if (!spread) {
return grouped;
}
function mapUngroupedColumnIndexToGroupedIndex(index, spread) {
return Math.floor(index / spread);
}
// expand columns
const ungrouped = new Array(w);
for (let index = 0; index < w; index++) {
if (!ungrouped[index]) {
ungrouped[index] = new Array(h);
}
const groupedIndexMap = mapUngroupedColumnIndexToGroupedIndex(index, spread);
for (let depth = 0; depth < h; depth++) {
const groupedDepthMap = rowSpread > 1 ? mapUngroupedColumnIndexToGroupedIndex(depth, rowSpread) : depth;
const value = grouped[groupedIndexMap][groupedDepthMap].split('|')[0];
ungrouped[index][depth] = value ? value : columnUnderlay[index][depth].replace(/\d+$/, OPACITY);
}
}
return ungrouped
}
function imgDataWordArrToColsAndRows(arr, h, w) {
let columns = new Array(w);
let rows = new Array(h);
for (var i = 0; i < arr.length; i++) {
const word = arr[i];
var {column, depth} = serialToColumnMap(i, h, w);
if (!columns[column]) {
columns[column] = new Array(h);
}
columns[column][depth] = word;
var {row, index} = serialToRowMap(i, h, w);
if (!rows[row]) {
rows[row] = new Array(w);
}
rows[row][index] = word;
}
return {columns, rows}
}
function serialToColumnMap(index, h, w) {
return {
column: index % w,
depth: Math.floor(index / w)
}
}
function serialToRowMap(index, h, w) {
return {
row: Math.floor(index / w),
index: index % w
}
}
function imgDataToWords(arr) {
let result = [];
for (let i = 0; i < arr.length-1; i += 4) {
result.push(`${arr[i]}_${arr[i+1]}_${arr[i+2]}_${arr[i+3]}`)
}
return result;
}
+6
View File
@@ -0,0 +1,6 @@
importScripts('diff.js');
importScripts('diverged.js');
self.addEventListener('message', function(e) {
self.postMessage(diverged(...e.data.divergedInput));
self.close();
}, false);
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Disable Cache -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>BackstopJS Report</title>
<style>
@font-face {
font-family: 'latoregular';
src: url('./assets/fonts/lato-regular-webfont.woff2') format('woff2'),
url('./assets/fonts/lato-regular-webfont.woff') format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'latobold';
src: url('./assets/fonts/lato-bold-webfont.woff2') format('woff2'),
url('./assets/fonts/lato-bold-webfont.woff') format('woff');
font-weight: 700;
font-style: normal;
}
.ReactModal__Body--open {
overflow: hidden;
}
.ReactModal__Body--open .header {
display: none;
}
</style>
</head>
<body style="background-color: #E2E7EA">
<div id="root">
</div>
<script>
function report (report) { // eslint-disable-line no-unused-vars
window.tests = report;
}
</script>
<script src="config.js"></script>
<script src="index_bundle.js"></script>
</body>
</html>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,59 @@
/*!
Copyright (c) 2015 Jed Watson.
Based on code that is Copyright 2013-2015, Facebook, Inc.
All rights reserved.
*/
/*!
* Adapted from jQuery UI core
*
* http://jqueryui.com
*
* Copyright 2014 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/category/ui-core/
*/
/*! 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.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.
*/
/**
* @license React
* use-sync-external-store-with-selector.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.
*/