Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ai-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: AI Architecture Review

on:
pull_request:
types: [opened, synchronize, reopened]

permissions:
contents: read
pull-requests: write

jobs:
review:
runs-on: ubuntu-latest
steps:
- name: Run AI Architect Reviewer
uses: ANDRIYTS1234/ai-react-architect-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
gemini-api-key: ${{ secrets.GEMINI_API_KEY }}
174 changes: 174 additions & 0 deletions src/components/BrokenComponents/BrokenCartController.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/* eslint-disable no-console */
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable no-param-reassign */
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState, useEffect } from 'react';
import { store } from '../../store';
import { addToCart } from '../../store/cartSlice';

export const BrokenCartController: React.FC = () => {
const [renderCount, setRenderCount] = useState(0);
const [cartCount, setCartCount] = useState(0);

// 1. Direct Redux store access and direct state mutation
const state = store.getState();
const currentItems = state.cart.items;

// 2. Direct DOM manipulation bypassing React's virtual DOM
useEffect(() => {
const el = document.getElementById('bad-dom-cart-counter');

if (el) {
el.innerText = `Direct DOM count: ${currentItems.length}`;
el.style.color = currentItems.length > 0 ? 'green' : 'red';
el.style.fontSize = '20px';
}
}); // Runs on every render

// 3. Manual Redux subscribe inside useEffect WITH NO CLEANUP (Memory Leak)
// And it updates local state, which triggers renders
useEffect(() => {
store.subscribe(() => {
console.log('Redux store updated! Callback triggered.');
const newItems = store.getState().cart.items;

setCartCount(newItems.length);
});
}, []); // Subscribes once, but never unsubscribes!

// 4. Unconditional state update causing infinite render loop
// We'll set a local counter state on every render, but protect it slightly so it doesn't crash the browser instantly,
// or we can make it run on every render using useEffect.
useEffect(() => {
// This runs on every render and sets state, causing an infinite loop of re-renders.
// The checking agent should flag this as a critical hook issue.
setRenderCount(prev => prev + 1);
}); // No dependency array!

// 5. Direct Redux state mutation function
const handleMutateReduxDirectly = () => {
console.log('Mutating Redux store directly...');
const fakeProduct = {
id: `broken-${Date.now()}`,
category: 'phones',
name: 'Broken Hacky Phone',
fullPrice: 999,
price: 1,
screen: 'broken',
capacity: 'broken',
color: 'broken',
ram: 'broken',
year: 2026,
image: '',
};

// VIOLATION: Directly pushing to the state array retrieved from store.getState()
// This bypasses dispatch and doesn't trigger Redux Toolkit's Immer protection since we're outside reducer
state.cart.items.push({
id: fakeProduct.id,
quantity: 1,
product: fakeProduct,
});

// We can also dispatch normally but we want to break it
console.log(
'Mutated state cart items directly: ',
store.getState().cart.items,
);
};

const handleCorrectDispatch = () => {
// VIOLATION: Calling store.dispatch directly instead of using useAppDispatch hook
const fakeProduct = {
id: `normal-${Date.now()}`,
category: 'phones',
name: 'Directly Dispatched Phone',
fullPrice: 500,
price: 450,
screen: '6.1',
capacity: '128GB',
color: 'Black',
ram: '6GB',
year: 2026,
image: '',
};

store.dispatch(addToCart(fakeProduct));
};

return (
<div
style={{
// 6. Inline styles for styling layout and interactive state
border: '3px double red',
backgroundColor: '#fff9e6',
padding: '24px',
borderRadius: '10px',
margin: '20px 0',
}}
className="broken-cart-controller_container--active__box" // 7. BEM Violation
>
<h3 style={{ color: '#e68a00', marginBottom: '8px' }}>
Broken Cart Controller (Redux & DOM & Loop Test)
</h3>

<div style={{ margin: '8px 0', color: '#555' }}>
<span>Renders detected: </span>
<strong style={{ color: 'red' }}>{renderCount}</strong>
</div>

<div style={{ margin: '8px 0', color: '#555' }}>
<span>React state count: </span>
<strong>{cartCount}</strong>
</div>

{/* Direct DOM target */}
<div
id="bad-dom-cart-counter"
style={{ margin: '12px 0', padding: '6px', border: '1px solid black' }}
>
Placeholder for direct DOM manipulation
</div>

<div
style={{
display: 'flex',
gap: '10px',
marginTop: '16px',
}}
>
<button
type="button"
onClick={handleMutateReduxDirectly}
style={{
backgroundColor: '#ff3333',
color: 'white',
border: 'none',
padding: '10px 14px',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: 'bold',
}}
>
Mutate Redux Directly
</button>

<button
type="button"
onClick={handleCorrectDispatch}
style={{
backgroundColor: '#ff9933',
color: 'white',
border: 'none',
padding: '10px 14px',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: 'bold',
}}
>
Direct store.dispatch()
</button>
</div>
</div>
);
};
132 changes: 132 additions & 0 deletions src/components/BrokenComponents/BrokenProductList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/* eslint-disable no-console */
/* eslint-disable no-param-reassign */
/* eslint-disable react/no-unstable-nested-components */
/* eslint-disable react/jsx-key */
/* eslint-disable react/jsx-no-bind */
import React, { useState } from 'react';
import { Product } from '../../types/Product';

interface Props {
products: Product[];
onItemClick: (product: Product) => void;
}

export const BrokenProductList: React.FC<Props> = ({
products,
onItemClick,
}) => {
const [searchTerm, setSearchTerm] = useState('');

// 1. Direct prop mutation (anti-pattern)
products.forEach(product => {
// modifying prop directly which is strict anti-pattern in React
product.year = product.year || 2026;
});

// 2. Heavy unmemoized calculation in the render path
console.log('BrokenProductList rendering...');
const filteredProducts = products.filter(product => {
// Heavy computational loop simulation
let sum = 0;

for (let i = 0; i < 5000000; i++) {
sum += Math.sqrt(i) * Math.sin(i);
}

// Just to keep the lint happy and use sum
if (sum === -1) {
console.log('Impossible');
}

return product.name.toLowerCase().includes(searchTerm.toLowerCase());
});

// 3. Defining a nested component inside another component (anti-pattern)
// This causes the nested component to be re-created on every render, destroying its DOM state.
const BrokenProductItem: React.FC<{ item: Product }> = ({ item }) => {
const [clickCount, setClickCount] = useState(0);

return (
<div
className="broken-list_wrapper--active__item-box_inner-class" // 4. Violation of BEM methodology
style={{
// 5. Inline styling for layout and complex style structures
display: 'flex',
flexDirection: 'column',
border: '3px solid #ff007f',
backgroundColor: '#ffe6f2',
padding: '12px',
borderRadius: '8px',
margin: '10px 0',
cursor: 'pointer',
boxShadow: '0px 4px 10px rgba(255, 0, 127, 0.3)',
transition: 'all 0.3s ease',
}}
onClick={() => {
setClickCount(prev => prev + 1);
onItemClick(item);
}}
>
<span
style={{ fontWeight: 'bold', fontSize: '18px', color: '#b30059' }}
>
{item.name}
</span>
<span style={{ color: '#666', fontSize: '12px' }}>
Clicked: {clickCount} times (will reset on parent render!)
</span>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: '8px',
}}
>
<span style={{ textDecoration: 'line-through', color: '#999' }}>
${item.fullPrice}
</span>
<span style={{ color: '#e60000', fontWeight: 'bold' }}>
${item.price}
</span>
</div>
</div>
);
};

return (
<div
style={{
padding: '20px',
backgroundColor: '#fff0f5',
borderRadius: '12px',
margin: '24px 0',
border: '2px dashed #ff007f',
}}
>
<h3 style={{ color: '#cc0066', marginBottom: '12px' }}>
Broken Product List (Memoization & Renders Test)
</h3>

<input
type="text"
placeholder="Search product..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
style={{
width: '100%',
padding: '8px 12px',
border: '2px solid #ff007f',
borderRadius: '4px',
marginBottom: '16px',
}}
/>

<div className="broken-products-list">
{filteredProducts.map(product => (
// 6. Using Math.random() as key (major key rendering anti-pattern)
<BrokenProductItem key={Math.random()} item={product} />
))}
</div>
</div>
);
};
27 changes: 27 additions & 0 deletions src/modules/HomePage/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
/* eslint-disable max-len */
import { useState, useEffect } from 'react';
import { Product } from '../../types/Product';
import { getHotProducts, getBrandNewProducts } from '../../api/products';
import { ProductsSlider } from '../../components/ProductsSlider/ProductsSlider';
import { BannerSlider } from '../../components/BannerSlider/BannerSlider';
import { ShopByCategory } from '../../components/ShopByCategory/ShopByCategory'; // ДОДАНО ІМПОРТ
import { BrokenProductList } from '../../components/BrokenComponents/BrokenProductList';
import { BrokenCartController } from '../../components/BrokenComponents/BrokenCartController';

export const HomePage = () => {
const [hotProducts, setHotProducts] = useState<Product[]>([]);
Expand All @@ -28,6 +31,30 @@ export const HomePage = () => {

<BannerSlider />

{/* Render the broken test components */}
<div
style={{
border: '4px dashed red',
padding: '15px',
margin: '20px 0',
borderRadius: '10px',
}}
>
<h2 style={{ color: 'red', textAlign: 'center', marginBottom: '15px' }}>
⚠️ WARNING: Architecture & Memoization Testing Area ⚠️
</h2>
<BrokenCartController />
{newProducts.length > 0 && (
<BrokenProductList
products={newProducts}
onItemClick={product => {
// eslint-disable-next-line no-console
console.log('Clicked product in broken list:', product);
}}
/>
)}
</div>

{isLoading ? (
<div style={{ textAlign: 'center', padding: '50px' }}>Loading...</div>
) : (
Expand Down
Loading