= ({
+ id,
+ title,
+ price,
+ image
+}) => {
+ return (
+
+

+
+
+ );
+};
+
+export default ShortProductCard;
\ No newline at end of file
diff --git a/src/components/ShortProductCard/img.png b/src/components/ShortProductCard/img.png
new file mode 100644
index 000000000..6a26b28a4
Binary files /dev/null and b/src/components/ShortProductCard/img.png differ
diff --git a/src/components/ShortProductCard/shortproductcard.css b/src/components/ShortProductCard/shortproductcard.css
new file mode 100644
index 000000000..a09cfadd0
--- /dev/null
+++ b/src/components/ShortProductCard/shortproductcard.css
@@ -0,0 +1,47 @@
+.product-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px;
+ border-bottom: 1px solid #ddd;
+ background: #fff;
+ font-family: Arial, sans-serif;
+}
+
+.product-image img {
+ width: 60px;
+ height: 60px;
+ object-fit: cover;
+ border-radius: 5px;
+}
+
+.product-details {
+ flex: 1;
+ padding: 0 15px;
+}
+
+.product-name {
+ font-size: 16px;
+ font-weight: bold;
+ color: #333;
+ margin: 0;
+}
+
+.product-description {
+ font-size: 14px;
+ color: #666;
+ margin: 5px 0 0;
+}
+
+.product-price {
+ font-size: 18px;
+ font-weight: bold;
+ color: #ff671f;
+ min-width: 80px;
+ text-align: right;
+}
+
+.product-action {
+ display: flex;
+ align-items: center;
+}
diff --git a/src/data/products.ts b/src/data/products.ts
new file mode 100644
index 000000000..22398647f
--- /dev/null
+++ b/src/data/products.ts
@@ -0,0 +1,25 @@
+import { Product } from '../types/product';
+
+export const products: Product[] = [
+ {
+ id: 1,
+ title: 'Product 1',
+ description: 'Description for product 1',
+ price: 1000,
+ image: 'https://via.placeholder.com/150'
+ },
+ {
+ id: 2,
+ title: 'Product 2',
+ description: 'Description for product 2',
+ price: 2000,
+ image: 'https://via.placeholder.com/150'
+ },
+ {
+ id: 3,
+ title: 'Product 3',
+ description: 'Description for product 3',
+ price: 3000,
+ image: 'https://via.placeholder.com/150'
+ }
+];
\ No newline at end of file
diff --git a/src/index.html b/src/index.html
index c8a8e9634..d8a60aa40 100644
--- a/src/index.html
+++ b/src/index.html
@@ -1,7 +1,10 @@
+
+
My project
+
diff --git a/src/index.tsx b/src/index.tsx
index 26d2b1437..a311c0fe5 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,11 +1,28 @@
import React from 'react';
-import ReactDOM from 'react-dom/client';
-import './app/index.css';
-import App from './app/App';
+import { createRoot } from 'react-dom/client';
+import { Provider } from 'react-redux';
+import { BrowserRouter } from 'react-router-dom';
+import { App } from './app/App';
+import { store } from './store';
+import { initializeApp } from './store/slices/appSlice';
+import './app/App.css';
+
+const container = document.getElementById('root');
+if (!container) {
+ throw new Error('Root element not found');
+}
+
+const root = createRoot(container);
+
+// Инициализируем приложение
+store.dispatch(initializeApp());
-const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
-
+
+
+
+
+
);
diff --git a/src/pages/BasketPage.tsx b/src/pages/BasketPage.tsx
new file mode 100644
index 000000000..bb56b81da
--- /dev/null
+++ b/src/pages/BasketPage.tsx
@@ -0,0 +1,46 @@
+import React, { useState } from 'react';
+import BasketList from '../components/BasketList/BasketList';
+import '../styles/BasketPage.css';
+import logo from '../components/ItemListContainer/favicon.svg';
+
+const BasketPage: React.FC = () => {
+ const [basket, setBasket] = useState([
+ { id: 1, title: 'Молоко', price: 100, count: 2 },
+ { id: 2, title: 'Хлеб', price: 50, count: 1 },
+ ]);
+
+ const handleRemove = (id: number) => {
+ setBasket(basket.filter(item => item.id !== id));
+ };
+
+ const handleIncrement = (id: number) => {
+ setBasket(basket.map(item =>
+ item.id === id ? { ...item, count: item.count + 1 } : item
+ ));
+ };
+
+ const handleDecrement = (id: number) => {
+ setBasket(basket.map(item =>
+ item.id === id ? { ...item, count: Math.max(0, item.count - 1) } : item
+ ));
+ };
+
+ const total = basket.reduce((sum, item) => sum + item.price * item.count, 0);
+
+ return (
+
+
Корзина
+ ({
+ ...item,
+ onRemove: handleRemove,
+ onIncrement: handleIncrement,
+ onDecrement: handleDecrement
+ }))}
+ />
+ Итого: {total} ₽
+
+ );
+};
+
+export default BasketPage;
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
new file mode 100644
index 000000000..f25b98631
--- /dev/null
+++ b/src/pages/HomePage.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import '../styles/HomePage.css';
+import logo from './banner.svg';
+
+const HomePage: React.FC = () => {
+ return (
+
+
Добро пожаловать в Магазин
+
Лучшие продукты по лучшим ценам. Выбирайте, заказывайте и наслаждайтесь!
+

+
+ );
+};
+
+export default HomePage;
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
new file mode 100644
index 000000000..edf509645
--- /dev/null
+++ b/src/pages/LoginPage.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { useAppDispatch, useAppSelector } from '../store/hooks';
+import { loginRequest } from '../store/slices/authSlice';
+import '../styles/LoginPage.css';
+
+const LoginPage: React.FC = () => {
+ const dispatch = useAppDispatch();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { loading, error } = useAppSelector((state) => state.auth);
+
+ const from = location.state?.from?.pathname || '/';
+
+ const handleLogin = () => {
+ dispatch(loginRequest());
+ navigate(from, { replace: true });
+ };
+
+ return (
+
+
+
Вход в систему
+ {error &&
{error}
}
+
+
+
+ );
+};
+
+export default LoginPage;
\ No newline at end of file
diff --git a/src/pages/ModalPage.css b/src/pages/ModalPage.css
new file mode 100644
index 000000000..eda72923d
--- /dev/null
+++ b/src/pages/ModalPage.css
@@ -0,0 +1,48 @@
+.modalOverlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+}
+
+.modalContent {
+ background-color: white;
+ padding: 2rem;
+ border-radius: 8px;
+ max-width: 500px;
+ width: 90%;
+ position: relative;
+}
+
+.closeButton {
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+ color: #666;
+ transition: color 0.2s ease;
+}
+
+.closeButton:hover {
+ color: #333;
+}
+
+.productTitle {
+ margin-top: 0;
+ margin-bottom: 1rem;
+ color: #333;
+}
+
+.productDescription {
+ color: #666;
+ line-height: 1.5;
+}
\ No newline at end of file
diff --git a/src/pages/ModalPage.tsx b/src/pages/ModalPage.tsx
new file mode 100644
index 000000000..c164195fe
--- /dev/null
+++ b/src/pages/ModalPage.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { products } from '../data/products';
+import { Product } from '../types/product';
+import './ModalPage.css';
+
+type ModalPageParams = {
+ id: string;
+};
+
+const ModalPage: React.FC = () => {
+ const { id } = useParams();
+ const navigate = useNavigate();
+
+ if (!id) {
+ return Ошибка: ID товара не указан
;
+ }
+
+ const product = products.find((p: Product) => p.id === Number(id));
+
+ if (!product) {
+ return Товар не найден
;
+ }
+
+ const handleClose = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ navigate(-1);
+ };
+
+ return (
+
+
e.stopPropagation()}>
+

+
{product.title}
+
{product.description}
+
{product.price} ₽
+
+
+
+ );
+};
+
+export default ModalPage;
\ No newline at end of file
diff --git a/src/pages/ProductsPage.tsx b/src/pages/ProductsPage.tsx
new file mode 100644
index 000000000..8a27a75f1
--- /dev/null
+++ b/src/pages/ProductsPage.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+import "../styles/ProductsPage.css";
+import { ItemListDemoButton } from "../components/ItemListContainer/ItemList_Demo_Button";
+
+const ProductsPage: React.FC = () => {
+ return (
+
+
Товары
+
+
+ );
+};
+
+export default ProductsPage;
\ No newline at end of file
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
new file mode 100644
index 000000000..93da490b5
--- /dev/null
+++ b/src/pages/ProfilePage.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { useAppSelector, useAppDispatch } from '../store/hooks';
+import { logout } from '../store/slices/authSlice';
+import { ProtectedRoute } from '../components/ProtectedRoute';
+import '../styles/ProfilePage.css';
+
+export const ProfilePage: React.FC = () => {
+ const dispatch = useAppDispatch();
+ const { data: profile, loading } = useAppSelector((state) => state.profile);
+ const { isAdmin } = useAppSelector((state) => state.auth);
+
+ const handleLogout = () => {
+ dispatch(logout());
+ };
+
+ if (loading) {
+ return Загрузка...
;
+ }
+
+ return (
+
+
+
+
Профиль пользователя
+ {profile && (
+
+

+
+
Имя: {profile.name}
+
Email: {profile.email}
+
Роль: {isAdmin ? 'Администратор' : 'Пользователь'}
+
+
+ )}
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/banner.svg b/src/pages/banner.svg
new file mode 100644
index 000000000..b9a2844db
--- /dev/null
+++ b/src/pages/banner.svg
@@ -0,0 +1,215 @@
+
+
+
diff --git a/src/store/hooks.ts b/src/store/hooks.ts
new file mode 100644
index 000000000..0e4abc53e
--- /dev/null
+++ b/src/store/hooks.ts
@@ -0,0 +1,5 @@
+import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
+import type { RootState, AppDispatch } from './index';
+
+export const useAppDispatch = () => useDispatch();
+export const useAppSelector: TypedUseSelectorHook = useSelector;
\ No newline at end of file
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 000000000..9c7827cbf
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,27 @@
+import { configureStore } from '@reduxjs/toolkit';
+import createSagaMiddleware from 'redux-saga';
+import { rootSaga } from './sagas';
+import authReducer from './slices/authSlice';
+import appReducer from './slices/appSlice';
+import profileReducer from './slices/profileSlice';
+import cartReducer from './slices/cartSlice';
+import productsReducer from './slices/productsSlice';
+
+const sagaMiddleware = createSagaMiddleware();
+
+export const store = configureStore({
+ reducer: {
+ auth: authReducer,
+ app: appReducer,
+ profile: profileReducer,
+ cart: cartReducer,
+ products: productsReducer,
+ },
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware().concat(sagaMiddleware),
+});
+
+sagaMiddleware.run(rootSaga);
+
+export type RootState = ReturnType;
+export type AppDispatch = typeof store.dispatch;
\ No newline at end of file
diff --git a/src/store/sagas/appSaga.ts b/src/store/sagas/appSaga.ts
new file mode 100644
index 000000000..5156d51ad
--- /dev/null
+++ b/src/store/sagas/appSaga.ts
@@ -0,0 +1,29 @@
+import { takeLatest, put, call } from 'redux-saga/effects';
+import { initializeApp } from '../slices/appSlice';
+import { fetchProfileRequest } from '../slices/profileSlice';
+import { fetchProductsRequest } from '../slices/productsSlice';
+
+// Функция для синхронизации токена между вкладками
+function setupTokenSync() {
+ window.addEventListener('storage', (event) => {
+ if (event.key === 'token') {
+ // Перезагружаем страницу для обновления состояния
+ window.location.reload();
+ }
+ });
+}
+
+function* handleInitializeApp() {
+ // Устанавливаем слушатель для синхронизации токена
+ yield call(setupTokenSync);
+
+ // Загружаем профиль, если есть токен
+ yield put(fetchProfileRequest());
+
+ // Загружаем список продуктов
+ yield put(fetchProductsRequest());
+}
+
+export function* watchApp() {
+ yield takeLatest(initializeApp.type, handleInitializeApp);
+}
\ No newline at end of file
diff --git a/src/store/sagas/authSaga.ts b/src/store/sagas/authSaga.ts
new file mode 100644
index 000000000..acc651fdc
--- /dev/null
+++ b/src/store/sagas/authSaga.ts
@@ -0,0 +1,26 @@
+import { takeLatest, put, delay } from 'redux-saga/effects';
+import { loginRequest, loginSuccess, loginFailure, setAdmin } from '../slices/authSlice';
+
+// Имитация API запроса
+function* handleLogin() {
+ try {
+ // Имитируем задержку запроса
+ yield delay(1000);
+
+ // Генерируем фейковый токен
+ const token = `fake-token-${Date.now()}`;
+
+ // Имитируем успешный вход
+ yield put(loginSuccess(token));
+
+ // Случайным образом определяем, является ли пользователь админом
+ yield put(setAdmin(Math.random() > 0.5));
+
+ } catch (error) {
+ yield put(loginFailure(error instanceof Error ? error.message : 'Unknown error'));
+ }
+}
+
+export function* watchAuth() {
+ yield takeLatest(loginRequest.type, handleLogin);
+}
\ No newline at end of file
diff --git a/src/store/sagas/cartSaga.ts b/src/store/sagas/cartSaga.ts
new file mode 100644
index 000000000..752e81c53
--- /dev/null
+++ b/src/store/sagas/cartSaga.ts
@@ -0,0 +1,16 @@
+import { takeLatest, put, select } from 'redux-saga/effects';
+import { addToCart, removeFromCart, updateQuantity } from '../slices/cartSlice';
+import { RootState } from '../index';
+
+// Сохранение корзины в localStorage
+function* handleCartChange() {
+ const cart: RootState['cart'] = yield select((state: RootState) => state.cart);
+ localStorage.setItem('cart', JSON.stringify(cart.items));
+}
+
+export function* watchCart() {
+ yield takeLatest(
+ [addToCart.type, removeFromCart.type, updateQuantity.type],
+ handleCartChange
+ );
+}
\ No newline at end of file
diff --git a/src/store/sagas/index.ts b/src/store/sagas/index.ts
new file mode 100644
index 000000000..1acbfc567
--- /dev/null
+++ b/src/store/sagas/index.ts
@@ -0,0 +1,16 @@
+import { all } from 'redux-saga/effects';
+import { watchAuth } from './authSaga';
+import { watchProfile } from './profileSaga';
+import { watchProducts } from './productsSaga';
+import { watchCart } from './cartSaga';
+import { watchApp } from './appSaga';
+
+export function* rootSaga() {
+ yield all([
+ watchAuth(),
+ watchProfile(),
+ watchProducts(),
+ watchCart(),
+ watchApp(),
+ ]);
+}
\ No newline at end of file
diff --git a/src/store/sagas/productsSaga.ts b/src/store/sagas/productsSaga.ts
new file mode 100644
index 000000000..d4ba0492d
--- /dev/null
+++ b/src/store/sagas/productsSaga.ts
@@ -0,0 +1,48 @@
+import { takeLatest, put, delay } from 'redux-saga/effects';
+import {
+ fetchProductsRequest,
+ fetchProductsSuccess,
+ fetchProductsFailure,
+ Product,
+} from '../slices/productsSlice';
+
+// Фейковые данные продуктов
+const fakeProducts: Product[] = [
+ {
+ id: '1',
+ name: 'Laptop',
+ price: 999.99,
+ description: 'Powerful laptop for work and gaming',
+ image: 'https://via.placeholder.com/200',
+ },
+ {
+ id: '2',
+ name: 'Smartphone',
+ price: 599.99,
+ description: 'Latest smartphone with great camera',
+ image: 'https://via.placeholder.com/200',
+ },
+ {
+ id: '3',
+ name: 'Headphones',
+ price: 99.99,
+ description: 'Wireless headphones with noise cancellation',
+ image: 'https://via.placeholder.com/200',
+ },
+];
+
+// Имитация API запроса
+function* handleFetchProducts() {
+ try {
+ // Имитируем задержку запроса
+ yield delay(1000);
+
+ yield put(fetchProductsSuccess(fakeProducts));
+ } catch (error) {
+ yield put(fetchProductsFailure(error instanceof Error ? error.message : 'Unknown error'));
+ }
+}
+
+export function* watchProducts() {
+ yield takeLatest(fetchProductsRequest.type, handleFetchProducts);
+}
\ No newline at end of file
diff --git a/src/store/sagas/profileSaga.ts b/src/store/sagas/profileSaga.ts
new file mode 100644
index 000000000..379726ba0
--- /dev/null
+++ b/src/store/sagas/profileSaga.ts
@@ -0,0 +1,40 @@
+import { takeLatest, put, delay, select } from 'redux-saga/effects';
+import {
+ fetchProfileRequest,
+ fetchProfileSuccess,
+ fetchProfileFailure,
+ clearProfile,
+} from '../slices/profileSlice';
+import { RootState } from '../index';
+
+// Имитация API запроса
+function* handleFetchProfile() {
+ try {
+ // Проверяем наличие токена
+ const token: string | null = yield select((state: RootState) => state.auth.token);
+
+ if (!token) {
+ yield put(clearProfile());
+ return;
+ }
+
+ // Имитируем задержку запроса
+ yield delay(1000);
+
+ // Генерируем фейковые данные профиля
+ const fakeProfile = {
+ id: '1',
+ name: 'Алексей Королев',
+ email: 'whispersofdew@gmail.com',
+ avatar: 'https://via.placeholder.com/150',
+ };
+
+ yield put(fetchProfileSuccess(fakeProfile));
+ } catch (error) {
+ yield put(fetchProfileFailure(error instanceof Error ? error.message : 'Unknown error'));
+ }
+}
+
+export function* watchProfile() {
+ yield takeLatest(fetchProfileRequest.type, handleFetchProfile);
+}
\ No newline at end of file
diff --git a/src/store/slices/appSlice.ts b/src/store/slices/appSlice.ts
new file mode 100644
index 000000000..70f7edee4
--- /dev/null
+++ b/src/store/slices/appSlice.ts
@@ -0,0 +1,23 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+interface AppState {
+ isInitialized: boolean;
+}
+
+const initialState: AppState = {
+ isInitialized: false,
+};
+
+const appSlice = createSlice({
+ name: 'app',
+ initialState,
+ reducers: {
+ initializeApp: (state) => {
+ state.isInitialized = true;
+ },
+ },
+});
+
+export const { initializeApp } = appSlice.actions;
+
+export default appSlice.reducer;
\ No newline at end of file
diff --git a/src/store/slices/authSlice.ts b/src/store/slices/authSlice.ts
new file mode 100644
index 000000000..947c2caf2
--- /dev/null
+++ b/src/store/slices/authSlice.ts
@@ -0,0 +1,54 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+interface AuthState {
+ token: string | null;
+ isAuthenticated: boolean;
+ isAdmin: boolean;
+ loading: boolean;
+ error: string | null;
+}
+
+const initialState: AuthState = {
+ token: localStorage.getItem('token'),
+ isAuthenticated: !!localStorage.getItem('token'),
+ isAdmin: false,
+ loading: false,
+ error: null,
+};
+
+const authSlice = createSlice({
+ name: 'auth',
+ initialState,
+ reducers: {
+ loginRequest: (state) => {
+ state.loading = true;
+ state.error = null;
+ },
+ loginSuccess: (state, action: PayloadAction) => {
+ state.token = action.payload;
+ state.isAuthenticated = true;
+ state.loading = false;
+ state.error = null;
+ localStorage.setItem('token', action.payload);
+ },
+ loginFailure: (state, action: PayloadAction) => {
+ state.loading = false;
+ state.error = action.payload;
+ },
+ logout: (state) => {
+ state.token = null;
+ state.isAuthenticated = false;
+ state.isAdmin = false;
+ state.loading = false;
+ state.error = null;
+ localStorage.removeItem('token');
+ },
+ setAdmin: (state, action: PayloadAction) => {
+ state.isAdmin = action.payload;
+ },
+ },
+});
+
+export const { loginRequest, loginSuccess, loginFailure, logout, setAdmin } = authSlice.actions;
+
+export default authSlice.reducer;
\ No newline at end of file
diff --git a/src/store/slices/cartSlice.ts b/src/store/slices/cartSlice.ts
new file mode 100644
index 000000000..8f75a1459
--- /dev/null
+++ b/src/store/slices/cartSlice.ts
@@ -0,0 +1,54 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+interface CartItem {
+ id: string;
+ name: string;
+ price: number;
+ quantity: number;
+}
+
+interface CartState {
+ items: CartItem[];
+ loading: boolean;
+ error: string | null;
+}
+
+const initialState: CartState = {
+ items: [],
+ loading: false,
+ error: null,
+};
+
+const cartSlice = createSlice({
+ name: 'cart',
+ initialState,
+ reducers: {
+ addToCart: (state, action: PayloadAction>) => {
+ const existingItem = state.items.find(item => item.id === action.payload.id);
+ if (existingItem) {
+ existingItem.quantity += 1;
+ } else {
+ state.items.push({ ...action.payload, quantity: 1 });
+ }
+ },
+ removeFromCart: (state, action: PayloadAction) => {
+ state.items = state.items.filter(item => item.id !== action.payload);
+ },
+ updateQuantity: (state, action: PayloadAction<{ id: string; quantity: number }>) => {
+ const item = state.items.find(item => item.id === action.payload.id);
+ if (item) {
+ item.quantity = Math.max(0, action.payload.quantity);
+ if (item.quantity === 0) {
+ state.items = state.items.filter(i => i.id !== action.payload.id);
+ }
+ }
+ },
+ clearCart: (state) => {
+ state.items = [];
+ },
+ },
+});
+
+export const { addToCart, removeFromCart, updateQuantity, clearCart } = cartSlice.actions;
+
+export default cartSlice.reducer;
\ No newline at end of file
diff --git a/src/store/slices/productsSlice.ts b/src/store/slices/productsSlice.ts
new file mode 100644
index 000000000..2b04d2642
--- /dev/null
+++ b/src/store/slices/productsSlice.ts
@@ -0,0 +1,64 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+export interface Product {
+ id: string;
+ name: string;
+ price: number;
+ description: string;
+ image: string;
+}
+
+interface ProductsState {
+ items: Product[];
+ loading: boolean;
+ error: string | null;
+}
+
+const initialState: ProductsState = {
+ items: [],
+ loading: false,
+ error: null,
+};
+
+const productsSlice = createSlice({
+ name: 'products',
+ initialState,
+ reducers: {
+ fetchProductsRequest: (state) => {
+ state.loading = true;
+ state.error = null;
+ },
+ fetchProductsSuccess: (state, action: PayloadAction) => {
+ state.items = action.payload;
+ state.loading = false;
+ state.error = null;
+ },
+ fetchProductsFailure: (state, action: PayloadAction) => {
+ state.loading = false;
+ state.error = action.payload;
+ },
+ addProduct: (state, action: PayloadAction) => {
+ state.items.push(action.payload);
+ },
+ updateProduct: (state, action: PayloadAction) => {
+ const index = state.items.findIndex(item => item.id === action.payload.id);
+ if (index !== -1) {
+ state.items[index] = action.payload;
+ }
+ },
+ deleteProduct: (state, action: PayloadAction) => {
+ state.items = state.items.filter(item => item.id !== action.payload);
+ },
+ },
+});
+
+export const {
+ fetchProductsRequest,
+ fetchProductsSuccess,
+ fetchProductsFailure,
+ addProduct,
+ updateProduct,
+ deleteProduct,
+} = productsSlice.actions;
+
+export default productsSlice.reducer;
\ No newline at end of file
diff --git a/src/store/slices/profileSlice.ts b/src/store/slices/profileSlice.ts
new file mode 100644
index 000000000..b41692fe0
--- /dev/null
+++ b/src/store/slices/profileSlice.ts
@@ -0,0 +1,52 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+
+interface ProfileState {
+ data: {
+ id: string;
+ name: string;
+ email: string;
+ avatar: string;
+ } | null;
+ loading: boolean;
+ error: string | null;
+}
+
+const initialState: ProfileState = {
+ data: null,
+ loading: false,
+ error: null,
+};
+
+const profileSlice = createSlice({
+ name: 'profile',
+ initialState,
+ reducers: {
+ fetchProfileRequest: (state) => {
+ state.loading = true;
+ state.error = null;
+ },
+ fetchProfileSuccess: (state, action: PayloadAction) => {
+ state.data = action.payload;
+ state.loading = false;
+ state.error = null;
+ },
+ fetchProfileFailure: (state, action: PayloadAction) => {
+ state.loading = false;
+ state.error = action.payload;
+ },
+ clearProfile: (state) => {
+ state.data = null;
+ state.loading = false;
+ state.error = null;
+ },
+ },
+});
+
+export const {
+ fetchProfileRequest,
+ fetchProfileSuccess,
+ fetchProfileFailure,
+ clearProfile,
+} = profileSlice.actions;
+
+export default profileSlice.reducer;
\ No newline at end of file
diff --git a/src/styles/BasketPage.css b/src/styles/BasketPage.css
new file mode 100644
index 000000000..fbedc9d9b
--- /dev/null
+++ b/src/styles/BasketPage.css
@@ -0,0 +1,3 @@
+.cart-container {
+ padding: 20px;
+}
\ No newline at end of file
diff --git a/src/styles/HomePage.css b/src/styles/HomePage.css
new file mode 100644
index 000000000..b636a62d4
--- /dev/null
+++ b/src/styles/HomePage.css
@@ -0,0 +1,9 @@
+.home-container {
+ text-align: center;
+ padding: 20px;
+}
+.banner {
+ width: 100%;
+ max-height: 300px;
+ object-fit: cover;
+}
\ No newline at end of file
diff --git a/src/styles/LoginPage.css b/src/styles/LoginPage.css
new file mode 100644
index 000000000..bf5a7b813
--- /dev/null
+++ b/src/styles/LoginPage.css
@@ -0,0 +1,45 @@
+.login-page {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ background-color: #f5f5f5;
+}
+
+.login-container {
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ text-align: center;
+}
+
+.login-container h1 {
+ margin-bottom: 1.5rem;
+ color: #333;
+}
+
+.login-container button {
+ background-color: #007bff;
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 1rem;
+ transition: background-color 0.2s;
+}
+
+.login-container button:hover {
+ background-color: #0056b3;
+}
+
+.login-container button:disabled {
+ background-color: #ccc;
+ cursor: not-allowed;
+}
+
+.error {
+ color: #dc3545;
+ margin-bottom: 1rem;
+}
\ No newline at end of file
diff --git a/src/styles/ProductList.css b/src/styles/ProductList.css
new file mode 100644
index 000000000..0b2c29c07
--- /dev/null
+++ b/src/styles/ProductList.css
@@ -0,0 +1,7 @@
+.product-list {
+ list-style: none;
+ padding: 0;
+}
+.product-item {
+ padding: 5px;
+}
\ No newline at end of file
diff --git a/src/styles/ProductsPage.css b/src/styles/ProductsPage.css
new file mode 100644
index 000000000..eec70281b
--- /dev/null
+++ b/src/styles/ProductsPage.css
@@ -0,0 +1,7 @@
+.products-container {
+ padding: 20px;
+}
+.product-list {
+ list-style: none;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/src/styles/ProfilePage.css b/src/styles/ProfilePage.css
new file mode 100644
index 000000000..f93e66aec
--- /dev/null
+++ b/src/styles/ProfilePage.css
@@ -0,0 +1,106 @@
+.profile-page {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ background-color: #f5f5f5;
+}
+
+.profile-container {
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ text-align: center;
+ max-width: 600px;
+ width: 100%;
+}
+
+.profile-info {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 2rem 0;
+}
+
+.avatar {
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ margin-bottom: 1rem;
+}
+
+.details {
+ text-align: left;
+ width: 100%;
+}
+
+.details p {
+ margin: 0.5rem 0;
+ font-size: 1.1rem;
+}
+
+.logout-button {
+ background-color: #dc3545;
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 1rem;
+ transition: background-color 0.2s;
+}
+
+.logout-button:hover {
+ background-color: #c82333;
+}
+
+.profile-title {
+ text-align: center;
+ color: #ff6600;
+ margin-bottom: 20px;
+}
+
+.profile-form {
+ display: flex;
+ flex-direction: column;
+}
+
+.profile-form label {
+ margin-top: 10px;
+ font-weight: bold;
+}
+
+.profile-form input {
+ padding: 8px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 16px;
+ margin-top: 5px;
+}
+
+.profile-form input:focus {
+ border-color: #d71921;
+ outline: none;
+}
+
+.error {
+ color: #d71921;
+ font-size: 14px;
+ margin-top: 5px;
+}
+
+.save-button {
+ margin-top: 20px;
+ padding: 10px;
+ background: #ff6600;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ font-size: 16px;
+ cursor: pointer;
+}
+
+.save-button:hover {
+ background: #e65c00;
+}
\ No newline at end of file
diff --git a/src/types/product.ts b/src/types/product.ts
new file mode 100644
index 000000000..939c7c17e
--- /dev/null
+++ b/src/types/product.ts
@@ -0,0 +1,7 @@
+export interface Product {
+ id: number;
+ title: string;
+ description: string;
+ price: number;
+ image: string;
+}
\ No newline at end of file