Loading...
;
+ }
+
+ if (!product) {
+ return
+
+
+
navigate(`/${product.category}`)}
+ className={styles.back_button}
+ >
+ {'<'} Back
+
+
+
{product.name}
+
+
+
+
+ {product.images?.map(img => (
+
setMainImage(img)}
+ >
+ {/* ВИКОРИСТОВУЄМО getImageUrl */}
+
+
+ ))}
+
+
+ {/* ВИКОРИСТОВУЄМО getImageUrl */}
+
+
+
+
+
+
+
+ Available colors
+
+ ID:{' '}
+ {product.namespaceId
+ ? Math.floor(Math.random() * 900000) + 100000
+ : '802390'}
+
+
+
+
+ {product.colorsAvailable.map(color => (
+
handleColorChange(color)}
+ />
+ ))}
+
+
+
+
+
Select capacity
+
+ {product.capacityAvailable.map(cap => (
+
handleCapacityChange(cap)}
+ >
+ {cap}
+
+ ))}
+
+
+
+
+
+ ${product.priceDiscount || product.price}
+
+
+ ${product.priceRegular || product.fullPrice}
+
+
+
+
+
+ {isAddedToCart ? 'Added' : 'Add to cart'}
+
+
+ dispatch(toggleFavorite(product as unknown as Product))
+ }
+ >
+ {/* ВИКОРИСТОВУЄМО getImageUrl */}
+
+
+
+
+
+
+ Screen {product.screen}
+
+
+ Resolution {product.resolution}
+
+
+ Processor {product.processor}
+
+
+ RAM {product.ram}
+
+
+
+
+
+
+
+
About
+ {product.description?.map(desc => (
+
+
{desc.title}
+ {desc.text.map((p, index) => (
+
{p}
+ ))}
+
+ ))}
+
+
+
+
Tech specs
+
+
+ Screen {product.screen}
+
+
+ Resolution {product.resolution}
+
+
+ Processor {product.processor}
+
+
+ RAM {product.ram}
+
+
+ Capacity {product.capacity}
+
+
+ Camera {product.camera}
+
+
+ Zoom {product.zoom}
+
+
+ Cell {product.cell?.join(', ') || 'N/A'}
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/modules/ProductDetailsPage/index.ts b/src/modules/ProductDetailsPage/index.ts
new file mode 100644
index 00000000000..6615089e5ec
--- /dev/null
+++ b/src/modules/ProductDetailsPage/index.ts
@@ -0,0 +1 @@
+export * from './ProductDetailsPage';
diff --git a/src/modules/RightsPage/RightsPage.module.scss b/src/modules/RightsPage/RightsPage.module.scss
new file mode 100644
index 00000000000..30fe610f81c
--- /dev/null
+++ b/src/modules/RightsPage/RightsPage.module.scss
@@ -0,0 +1,23 @@
+@import '../../styles/variables';
+
+.page {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 48px 16px;
+ text-align: center;
+ flex: 1;
+
+ &__title {
+ font-size: 32px;
+ font-weight: 800;
+ color: $c-text-primary;
+ margin-bottom: 24px;
+ }
+
+ &__text {
+ font-size: 16px;
+ line-height: 1.5;
+ color: $c-text-secondary;
+ font-weight: 600;
+ }
+}
diff --git a/src/modules/RightsPage/RightsPage.tsx b/src/modules/RightsPage/RightsPage.tsx
new file mode 100644
index 00000000000..e4148444a74
--- /dev/null
+++ b/src/modules/RightsPage/RightsPage.tsx
@@ -0,0 +1,14 @@
+import styles from './RightsPage.module.scss';
+
+export const RightsPage = () => {
+ return (
+
+
Rights
+
+ All rights reserved © Nice Gadgets 2026.
+
+ Terms and conditions apply.
+
+
+ );
+};
diff --git a/src/store/cartSlice.ts b/src/store/cartSlice.ts
new file mode 100644
index 00000000000..48a4c23a9af
--- /dev/null
+++ b/src/store/cartSlice.ts
@@ -0,0 +1,62 @@
+/* eslint-disable no-param-reassign */
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { CartItem } from '../types/CartItem';
+import { Product } from '../types/Product';
+
+export interface CartState {
+ items: CartItem[];
+}
+
+const initialState: CartState = {
+ items: [],
+};
+
+const cartSlice = createSlice({
+ name: 'cart',
+ initialState,
+ reducers: {
+ addToCart: (state, action: PayloadAction
) => {
+ const product = action.payload;
+ const existingItem = state.items.find(
+ cartItem => cartItem.id === product.id,
+ );
+
+ if (existingItem) {
+ return;
+ }
+
+ state.items.push({
+ id: product.id,
+ quantity: 1,
+ product,
+ });
+ },
+
+ removeFromCart: (state, action: PayloadAction) => {
+ state.items = state.items.filter(
+ cartItem => cartItem.id !== action.payload,
+ );
+ },
+
+ updateQuantity: (
+ state,
+ action: PayloadAction<{ id: string; quantity: number }>,
+ ) => {
+ const { id, quantity } = action.payload;
+ const itemToUpdate = state.items.find(cartItem => cartItem.id === id);
+
+ if (itemToUpdate && quantity > 0) {
+ itemToUpdate.quantity = quantity;
+ }
+ },
+
+ clearCart: state => {
+ state.items = [];
+ },
+ },
+});
+
+export const { addToCart, removeFromCart, updateQuantity, clearCart } =
+ cartSlice.actions;
+
+export default cartSlice.reducer;
diff --git a/src/store/favoritesSlice.ts b/src/store/favoritesSlice.ts
new file mode 100644
index 00000000000..cd975207e49
--- /dev/null
+++ b/src/store/favoritesSlice.ts
@@ -0,0 +1,43 @@
+/* eslint-disable no-param-reassign */
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { Product } from '../types/Product';
+
+export interface FavoritesState {
+ items: Product[];
+}
+
+const loadFavoritesFromStorage = (): Product[] => {
+ try {
+ const saved = localStorage.getItem('favorites');
+
+ return saved ? JSON.parse(saved) : [];
+ } catch (e) {
+ return [];
+ }
+};
+
+const initialState: FavoritesState = {
+ items: loadFavoritesFromStorage(),
+};
+
+const favoritesSlice = createSlice({
+ name: 'favorites',
+ initialState,
+ reducers: {
+ toggleFavorite: (state, action: PayloadAction) => {
+ const product = action.payload;
+ const existingIndex = state.items.findIndex(
+ item => item.id === product.id,
+ );
+
+ if (existingIndex >= 0) {
+ state.items.splice(existingIndex, 1);
+ } else {
+ state.items.push(product);
+ }
+ },
+ },
+});
+
+export const { toggleFavorite } = favoritesSlice.actions;
+export default favoritesSlice.reducer;
diff --git a/src/store/hooks.ts b/src/store/hooks.ts
new file mode 100644
index 00000000000..f92270e7bd9
--- /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: () => AppDispatch = useDispatch;
+export const useAppSelector: TypedUseSelectorHook = useSelector;
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 00000000000..4660861fed4
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,20 @@
+import { configureStore } from '@reduxjs/toolkit';
+import cartReducer from './cartSlice';
+import favoritesReducer from './favoritesSlice';
+
+export const store = configureStore({
+ reducer: {
+ cart: cartReducer,
+ favorites: favoritesReducer,
+ },
+});
+
+store.subscribe(() => {
+ const state = store.getState();
+
+ localStorage.setItem('cart', JSON.stringify(state.cart.items));
+ localStorage.setItem('favorites', JSON.stringify(state.favorites.items));
+});
+
+export type RootState = ReturnType;
+export type AppDispatch = typeof store.dispatch;
diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss
new file mode 100644
index 00000000000..b07ea2873db
--- /dev/null
+++ b/src/styles/_mixins.scss
@@ -0,0 +1,13 @@
+@import './variables';
+
+@mixin tablet {
+ @media (min-width: $bp-tablet) {
+ @content;
+ }
+}
+
+@mixin desktop {
+ @media (min-width: $bp-desktop) {
+ @content;
+ }
+}
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
new file mode 100644
index 00000000000..13e1d7f5d13
--- /dev/null
+++ b/src/styles/_variables.scss
@@ -0,0 +1,16 @@
+$c-bg-dark: #0F1121;
+$c-bg-card: #161827;
+$c-bg-elements: #161618;
+$c-text-primary: #fff;
+$c-text-secondary: #75767f;
+$c-accent-purple: #9056ec;
+$c-accent-red: #eb5757;
+$c-border: #323542;
+$c-primary: #905BFF;
+$c-primary-hover: #A378FF;
+$c-selected: #323542;
+$c-border: #323542;
+$font-family: 'Mont', sans-serif;
+$bp-mobile: 320px;
+$bp-tablet: 640px;
+$bp-desktop: 1200px;
diff --git a/src/types/CartItem.ts b/src/types/CartItem.ts
new file mode 100644
index 00000000000..e22f60a5541
--- /dev/null
+++ b/src/types/CartItem.ts
@@ -0,0 +1,7 @@
+import { Product } from './Product';
+
+export interface CartItem {
+ id: string;
+ quantity: number;
+ product: Product;
+}
diff --git a/src/types/Product.ts b/src/types/Product.ts
new file mode 100644
index 00000000000..90032fd2c8b
--- /dev/null
+++ b/src/types/Product.ts
@@ -0,0 +1,37 @@
+export interface Product {
+ id: string;
+ category: string;
+ phoneId?: string;
+ itemId?: string;
+ name: string;
+ fullPrice: number;
+ price: number;
+ priceRegular?: number;
+ priceDiscount?: number;
+ screen: string;
+ capacity: string;
+ color: string;
+ ram: string;
+ year: number;
+ image: string;
+ imageUrl?: string;
+ images?: string[];
+}
+
+export interface ProductDescription {
+ title: string;
+ text: string[];
+}
+
+export interface ProductDetails extends Product {
+ namespaceId: string;
+ capacityAvailable: string[];
+ colorsAvailable: string[];
+ description: ProductDescription[];
+ resolution: string;
+ processor: string;
+ zoom?: string;
+ cell: string[];
+ camera?: string;
+ images: string[];
+}
diff --git a/vite.config.ts b/vite.config.ts
index 5a33944a9b4..4c1c856a931 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -2,6 +2,10 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
-export default defineConfig({
- plugins: [react()],
-})
+export default defineConfig(({ command }) => {
+ return {
+ plugins: [react()],
+ // Якщо ти назвав репозиторій на гітхабі інакше — зміни назву між слешами
+ base: command === 'build' ? '/react_phone-catalog/' : '/',
+ };
+});