@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -0,0 +1,5 @@
+
+
+
+
+
@@ -0,0 +1,4 @@
+
+
+
+
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+ Timeless - Luxury Watches
+
+
+ Для работы приложения необходимо включить JavaScript.
+
+
+
@@ -0,0 +1,41 @@
+
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { FOOTER_MENU_LINKS, SOCIAL_LINKS } from './constants';
+import styles from './Footer.module.css';
+
+const Footer = () => {
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default Footer;
\ No newline at end of file
@@ -0,0 +1,72 @@
+.line {
+ width: 100%;
+ height: 1px;
+ background-color: #ddd;
+ margin: 40px 0 20px 0;
+}
+
+.footer {
+ display: flex;
+ justify-content: space-around;
+ align-items: flex-start;
+ padding: 40px 50px;
+ background-color: #f9f9f9;
+}
+
+.footerLogo .logo {
+ width: 120px;
+}
+
+.footerText ul {
+ list-style: none;
+ padding: 0;
+}
+
+.footerText ul li {
+ margin-bottom: 10px;
+}
+
+.footerText ul li a {
+ text-decoration: none;
+ color: #333;
+ font-size: 14px;
+ transition: color 0.3s;
+}
+
+.footerText ul li a:hover {
+ color: #007bff;
+}
+
+.social {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.social a {
+ text-decoration: none;
+ color: #333;
+ font-size: 14px;
+ font-weight: 600;
+ transition: color 0.3s;
+}
+
+.social a:hover {
+ color: #007bff;
+}
+
+@media (max-width: 768px) {
+ .footer {
+ flex-direction: column;
+ gap: 30px;
+ padding: 30px 20px;
+ }
+
+ .footerLogo {
+ text-align: center;
+ }
+
+ .social {
+ align-items: center;
+ }
+}
@@ -0,0 +1,14 @@
+export const FOOTER_MENU_LINKS = [
+ { link: '/about', title: 'О нас' },
+ { link: '/policy', title: 'Политика и Конфиденциальность' },
+ { link: '/faq', title: 'FAQ' },
+ { link: '/faq', title: 'Заказы и доставка' },
+ { link: '/faq', title: 'Возврат' },
+];
+
+export const SOCIAL_LINKS = [
+ { href: 'https://www.instagram.com', title: 'INSTAGRAM' },
+ { href: 'https://vk.com', title: 'VK' },
+ { href: 'https://www.youtube.com', title: 'YOUTUBE' },
+ { href: 'https://telegram.org', title: 'TELEGRAM' },
+];
\ No newline at end of file
@@ -0,0 +1,152 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { Link, useLocation, useNavigate } from 'react-router-dom';
+import { useBasket } from '../../../context/BasketContext';
+import { useUser } from '../../../context/UserContext';
+import { useToast } from '../../../context/ToastContext';
+import styles from './Navigation.module.css';
+import { products } from '../../../utils/products';
+
+const Navigation = () => {
+ // const location = useLocation();
+ const navigate = useNavigate();
+ const { getTotalItems, addToBasket } = useBasket();
+ const { isAuthenticated, user } = useUser();
+ const { showToast } = useToast();
+ const totalItems = getTotalItems();
+
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchResults, setSearchResults] = useState([]);
+ const [showResults, setShowResults] = useState(false);
+ const searchRef = useRef(null);
+ const searchTimeoutRef = useRef(null);
+
+ const performSearch = (query) => {
+ if (query.trim() === '') {
+ setSearchResults([]);
+ setShowResults(false);
+ return;
+ }
+
+ const filtered = products.filter(product =>
+ product.name.toLowerCase().includes(query.toLowerCase()) ||
+ product.brand.toLowerCase().includes(query.toLowerCase())
+ );
+
+ setSearchResults(filtered);
+ setShowResults(true);
+ };
+
+ const handleSearch = (query) => {
+ setSearchQuery(query);
+
+ if (searchTimeoutRef.current) {
+ clearTimeout(searchTimeoutRef.current);
+ }
+
+ searchTimeoutRef.current = setTimeout(() => {
+ performSearch(query);
+ }, 500);
+ };
+
+ const handleSelectProduct = (product) => {
+ setShowResults(false);
+ setSearchQuery('');
+ addToBasket(product);
+ showToast('Товар добавлен в корзину!', 'success');
+ };
+
+ const handleProfileClick = (e) => {
+ if (!isAuthenticated) {
+ e.preventDefault();
+ navigate('/registration');
+ }
+ };
+
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (searchRef.current && !searchRef.current.contains(event.target)) {
+ setShowResults(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ useEffect(() => {
+ return () => {
+ if (searchTimeoutRef.current) {
+ clearTimeout(searchTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ return (
+ <>
+
+
+ Timeless
+
+
+
+
handleSearch(e.target.value)}
+ onFocus={() => searchQuery && setShowResults(true)}
+ />
+
+ {showResults && (
+
+ {searchResults.length > 0 ? (
+ searchResults.map(product => (
+
handleSelectProduct(product)}
+ >
+
+
+
+
+
{product.name}
+
{product.brand}
+
+
+ ${product.price.toLocaleString()}
+
+
+ ))
+ ) : (
+
+ Товары не найдены
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+ {totalItems > 0 &&
{totalItems} }
+
+
+
+ {!isAuthenticated && (
+
+ Регистрация
+
+ )}
+
+ >
+ );
+};
+
+export default Navigation;
\ No newline at end of file
@@ -0,0 +1,256 @@
+
+ .cap {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px 50px;
+ background-color: #fff;
+ border-bottom: 1px solid #eee;
+ position: sticky;
+ top: 0;
+ z-index: 999;
+}
+
+.brand a {
+ text-decoration: none;
+ color: #000;
+ font-size: 24px;
+ font-weight: 700;
+}
+
+.inputContainer {
+ position: relative;
+ flex: 1;
+ max-width: 400px;
+ margin: 0 30px;
+}
+
+.input {
+ width: 500px;
+ padding: 10px 15px;
+ border: 1px solid #000000;
+ border-radius: 5px;
+ outline: none;
+ font-size: 14px;
+}
+
+.input:focus {
+ border-color: #6759ca;
+ box-shadow: 0 0 0 2px rgba(103, 89, 202, 0.1);
+}
+
+.icons {
+ display: flex;
+ gap: 80px;
+ align-items: center;
+ position: relative;
+ right: 15px;
+}
+
+.icon {
+ cursor: pointer;
+ transition: opacity 0.3s;
+}
+
+.icon:hover {
+ opacity: 0.7;
+}
+
+.badge {
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ background-color: #ff4444;
+ color: white;
+ border-radius: 50%;
+ padding: 2px 6px;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.registration {
+ margin-left: 20px;
+}
+
+.registration a {
+ text-decoration: none;
+ color: #000;
+ font-size: 14px;
+ font-weight: 500;
+ padding: 8px 16px;
+ border: 1px solid #000;
+ border-radius: 5px;
+ transition: all 0.3s;
+}
+
+.registration a:hover {
+ background-color: #000;
+ color: #fff;
+}
+
+.active {
+ font-weight: bold;
+ color: #007bff;
+}
+
+
+.searchResults {
+ position: absolute;
+ top: calc(100% + 8px);
+ left: 0px;
+ right: -100px;
+ background: #fff;
+ border-radius: 12px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+ max-height: 400px;
+ overflow-y: auto;
+ z-index: 1000;
+}
+
+.searchItem {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ padding: 12px 16px;
+ cursor: pointer;
+ border-bottom: 1px solid #f0f0f0;
+ transition: background 0.2s;
+}
+
+.searchItem:hover {
+ background: #f8f7ff;
+}
+
+.searchItem:last-child {
+ border-bottom: none;
+ border-radius: 0 0 12px 12px;
+}
+
+.searchItem:first-child {
+ border-radius: 12px 12px 0 0;
+}
+
+.searchItemImage {
+ width: 45px;
+ height: 45px;
+ background: linear-gradient(135deg, #7c6fd6 0%, #6759ca 100%);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ overflow: hidden;
+ padding: 5px;
+}
+
+.searchItemImage img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+
+.searchItemInfo {
+ flex: 1;
+ min-width: 0;
+}
+
+.searchItemName {
+ font-size: 15px;
+ font-weight: 600;
+ margin-bottom: 3px;
+ color: #1a1a1a;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.searchItemBrand {
+ font-size: 13px;
+ color: #666;
+}
+
+.searchItemPrice {
+ font-size: 17px;
+ font-weight: 700;
+ color: #6759ca;
+ white-space: nowrap;
+}
+
+.noResults {
+ padding: 30px 20px;
+ text-align: center;
+ color: #999;
+ font-size: 14px;
+}
+
+@media (max-width: 768px) {
+ .cap {
+ flex-wrap: wrap;
+ padding: 15px 20px;
+ }
+
+ .inputContainer {
+ order: 3;
+ width: 100%;
+ max-width: 100%;
+ margin: 10px 0 0 0;
+ }
+}
+.notification {
+ position: fixed;
+ top: 30%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 9999;
+ animation: scaleIn 0.3s ease-in-out;
+}
+
+.notificationContent {
+ background: #2d2d3d;
+ padding: 30px 50px;
+ border-radius: 5px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+ text-align: center;
+ color: white;
+ min-width: 350px;
+}
+
+.notificationContent h3 {
+ margin: 0 0 15px 0;
+ font-size: 20px;
+ font-weight: 600;
+}
+
+.notificationContent p {
+ margin: 0 0 25px 0;
+ font-size: 16px;
+ color: #e0e0e0;
+}
+
+.notificationBtn {
+ padding: 12px 50px;
+ background: #e8e8ff;
+ color: #2d2d3d;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 16px;
+ font-weight: 500;
+ transition: all 0.3s;
+}
+
+.notificationBtn:hover {
+ background: #fff;
+ transform: scale(1.05);
+}
+
+@keyframes scaleIn {
+ from {
+ transform: translate(-50%, -50%) scale(0.7);
+ opacity: 0;
+ }
+ to {
+ transform: translate(-50%, -50%) scale(1);
+ opacity: 1;
+ }
+}
\ No newline at end of file
@@ -0,0 +1,41 @@
+
+
+import React from 'react';
+import { useBasket } from '../../../context/BasketContext';
+import { useToast } from '../../../context/ToastContext';
+import styles from './ProductCard.module.css';
+
+const ProductCard = ({ product }) => {
+ const { addToBasket } = useBasket();
+ const { showToast } = useToast();
+
+ const handleAddToBasket = () => {
+ addToBasket(product);
+ showToast('Часы добавлены в корзину!', 'success');
+ };
+
+ return (
+
+
+
+
+
{product.brand}
+
+
{product.description}
+
+
${product.price.toLocaleString()}
+
+ Добавить в корзину
+
+
+
+ );
+};
+
+export default ProductCard;
@@ -0,0 +1,88 @@
+.card {
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ transition: transform 0.3s, box-shadow 0.3s;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
+
+}
+
+.image {
+ width: 150px;
+ height: 250px;
+ object-fit: cover;
+
+}
+
+.info {
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
+
+.name {
+ font-size: 20px;
+ font-weight: 600;
+ margin-bottom: 10px;
+}
+
+.name p {
+ margin: 0;
+}
+
+.brand {
+ margin: 5px 0;
+ color: #666;
+ font-size: 14px;
+}
+
+.description {
+ margin: 10px 0;
+ min-height: 60px;
+ flex: 1;
+}
+
+.description p {
+ font-size: 13px;
+ color: #888;
+ line-height: 1.4;
+ margin: 0;
+}
+
+.price {
+ font-size: 24px;
+ font-weight: 700;
+ color: #667eea;
+ margin: 15px 0;
+}
+
+.button {
+ width: 100%;
+ padding: 12px;
+ background-color: #667eea;
+ color: white;
+ border: none;
+ border-radius: 5px;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.3s;
+}
+
+.button:hover {
+ background-color: #5568d3;
+}
+
+.button:active {
+ transform: scale(0.98);
+}
@@ -0,0 +1,63 @@
+import React, { useEffect } from 'react';
+import styles from './Toast.module.css';
+
+const Toast = ({
+ message,
+ type = 'success',
+ isVisible,
+ onClose,
+ duration = 3000,
+ product = null
+}) => {
+ useEffect(() => {
+ if (isVisible && duration > 0) {
+ const timer = setTimeout(() => {
+ onClose();
+ }, duration);
+
+ return () => clearTimeout(timer);
+ }
+ }, [isVisible, duration, onClose]);
+
+ if (!isVisible) return null;
+
+ const getIcon = () => {
+ switch (type) {
+ case 'success':
+ return '✓';
+ case 'error':
+ return '✕';
+ case 'warning':
+ return '⚠';
+ case 'info':
+ return 'ℹ';
+ default:
+ return '✓';
+ }
+ };
+
+ return (
+
+
+
{getIcon()}
+
+
{message}
+ {product && (
+
+ {product.name} - ${product.price.toLocaleString()}
+
+ )}
+
+
+ ×
+
+
+
+ );
+};
+
+export default Toast;
\ No newline at end of file
@@ -0,0 +1,126 @@
+.toast {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 9999;
+ animation: slideIn 0.3s ease-out;
+ max-width: 400px;
+ min-width: 300px;
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateX(400px);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+.toastContent {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 16px 20px;
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ border-left: 4px solid;
+ transition: box-shadow 0.3s;
+}
+
+.toastContent:hover {
+ box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
+}
+
+.success .toastContent {
+ border-left-color: #667eea;
+}
+
+.error .toastContent {
+ border-left-color: #ef4444;
+}
+
+.warning .toastContent {
+ border-left-color: #f59e0b;
+}
+
+.info .toastContent {
+ border-left-color: #667eea;
+}
+
+.toastIcon {
+ font-size: 24px;
+ font-weight: 600;
+ flex-shrink: 0;
+}
+
+.success .toastIcon {
+ color: #667eea;
+}
+
+.error .toastIcon {
+ color: #ef4444;
+}
+
+.warning .toastIcon {
+ color: #f59e0b;
+}
+
+.info .toastIcon {
+ color: #667eea;
+}
+
+.toastMessage {
+ flex: 1;
+ font-size: 14px;
+ color: #333;
+}
+
+.toastMessage p {
+ margin: 0;
+ line-height: 1.4;
+ font-weight: 600;
+}
+
+.productDetails {
+ font-size: 13px;
+ color: #666;
+ margin-top: 4px !important;
+ font-weight: 400;
+}
+
+.closeButton {
+ background: none;
+ border: none;
+ font-size: 24px;
+ color: #888;
+ cursor: pointer;
+ padding: 0;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ transition: color 0.3s, transform 0.3s;
+}
+
+.closeButton:hover {
+ color: #667eea;
+}
+
+.closeButton:active {
+ transform: scale(0.98);
+}
+
+@media (max-width: 768px) {
+ .toast {
+ right: 10px;
+ left: 10px;
+ max-width: none;
+ min-width: auto;
+ }
+}
\ No newline at end of file
@@ -0,0 +1,54 @@
+
+import React from 'react';
+import styles from './About.module.css';
+
+const About = () => {
+ return (
+
+
+
О нас
+
+
+
+
+
Timeless — искусство вечного времени
+
+ Компания Timeless создаёт элегантные наручные часы, сочетающие в себе безупречный дизайн,
+ высокое качество и точность. Мы верим, что время — это не просто цифры на циферблате,
+ а отражение стиля, статуса и индивидуальности.
+
+
+
Наша философия
+
+
+ Традиции и инновации — в каждой модели гармонично сочетаются классические
+ элементы и современные технологии.
+
+
+ Долговечность — мы используем только премиальные материалы, чтобы часы служили
+ вам десятилетиями.
+
+
+ Уникальный стиль — от минималистичных моделей до сложных механизмов с автоподзаводом.
+
+
+
+
Почему выбирают Timeless?
+
+ Швейцарское качество — точность и надёжность в каждой детали.
+ Ручная сборка — внимание к мелочам, которое делает часы произведением искусства.
+ Эксклюзивные коллекции — ограниченные серии для истинных ценителей.
+
+
+
+ Timeless — не просто часы, это наследие, которое вы передадите следующим поколениям.
+
+
+
+
+
+
+ );
+};
+
+export default About;
@@ -0,0 +1,116 @@
+.container {
+ min-height: 70vh;
+ padding: 40px 50px;
+}
+
+.main {
+ max-width: 900px;
+ margin: 0 auto;
+}
+
+.title {
+ text-align: center;
+ font-size: 42px;
+ margin-bottom: 40px;
+ color: #333;
+}
+
+.userInfoPanel {
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ padding: 40px;
+}
+
+.userInfoContent {
+ line-height: 1.8;
+}
+
+.content h2 {
+ font-size: 28px;
+ margin-bottom: 20px;
+ color: #667eea;
+}
+
+.content h3 {
+ font-size: 22px;
+ margin-top: 30px;
+ margin-bottom: 15px;
+ color: #333;
+}
+
+.content p {
+ margin-bottom: 20px;
+ color: #555;
+ font-size: 16px;
+}
+
+.content ul {
+ margin: 20px 0;
+ padding-left: 20px;
+}
+
+.content ul li {
+ margin-bottom: 15px;
+ color: #555;
+ font-size: 16px;
+}
+
+.checkList {
+ list-style: none !important;
+ padding-left: 0 !important;
+}
+
+.checkList li {
+ padding-left: 35px;
+ position: relative;
+ margin-bottom: 15px;
+}
+
+.checkList li::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 2px;
+ width: 20px;
+ height: 20px;
+ background: #667eea;
+ border-radius: 50%;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E");
+ background-size: 14px;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.conclusion {
+ margin-top: 30px;
+ font-size: 18px;
+ font-weight: 600;
+ text-align: center;
+ color: #667eea;
+ padding: 20px;
+ background: #f0f4ff;
+ border-radius: 5px;
+}
+
+@media (max-width: 768px) {
+ .container {
+ padding: 20px;
+ }
+
+ .userInfoPanel {
+ padding: 30px 20px;
+ }
+
+ .title {
+ font-size: 32px;
+ }
+
+ .content h2 {
+ font-size: 24px;
+ }
+
+ .content h3 {
+ font-size: 20px;
+ }
+}
\ No newline at end of file
@@ -0,0 +1,211 @@
+// import React, { useState } from 'react';
+// import { useBasket } from '../../../context/BasketContext';
+// import { useUser } from '../../../context/UserContext';
+// import BasketItem from './BasketItem';
+// import styles from './Basket.module.css';
+
+// const Basket = () => {
+// const { basketItems, updateQuantityBasket, removeFromBasket, getTotalPriceBasket, clearBasket } = useBasket();
+// const { user } = useUser();
+// const [orderProcessing, setOrderProcessing] = useState(false);
+// const [showNotification, setShowNotification] = useState(false);
+
+// const handleCheckout = async () => {
+// try {
+// setOrderProcessing(true);
+
+// await new Promise(resolve => setTimeout(resolve, 1000));
+
+// setShowNotification(true);
+
+// setTimeout(() => {
+// setShowNotification(false);
+// }, 3000);
+
+// clearBasket();
+
+// } catch (error) {
+// alert(`Ошибка при оформлении заказа: ${error.message}`);
+// } finally {
+// setOrderProcessing(false);
+// }
+// };
+
+// if (basketItems.length === 0) {
+// return (
+//
+//
+//
Корзина
+//
+//
+//
Ваша корзина пуста
+//
+//
+//
+// );
+// }
+
+// const totalPrice = getTotalPriceBasket();
+
+// return (
+//
+//
+//
+//
+//
Корзина
+
+// {user && (
+//
+//
+//
+//
+//
{user.fullName || user.email || 'Имя пользователя'}
+//
+//
+// Пункт выдачи:
+// Ул. Московская 231к3
+//
+//
+//
+// )}
+//
+
+//
+//
+// {basketItems.map(item => (
+//
+// ))}
+//
+//
+//
Итого: ${totalPrice.toLocaleString()}
+//
+// {orderProcessing ? 'Обработка...' : 'Оформить заказ'}
+//
+//
+//
+
+// {showNotification && (
+//
+//
+//
Заказ успешно оформлен!
+//
Ваш заказ на сумму ${totalPrice.toLocaleString()} принят в обработку
+//
setShowNotification(false)}
+// >
+// OK
+//
+//
+//
+// )}
+//
+// );
+// };
+
+// export default Basket;
+import React, { useState } from 'react';
+import { useBasket } from '../../../context/BasketContext';
+import { useUser } from '../../../context/UserContext';
+import BasketItem from './BasketItem';
+import { useToast } from '../../../context/ToastContext';
+import styles from './Basket.module.css';
+
+const Basket = () => {
+ const { basketItems, updateQuantityBasket, removeFromBasket, getTotalPriceBasket, clearBasket } = useBasket();
+ const { user } = useUser();
+ const { showToast } = useToast();
+ const [orderProcessing, setOrderProcessing] = useState(false);
+
+ const handleCheckout = async () => {
+ try {
+ setOrderProcessing(true);
+
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ const totalPrice = getTotalPriceBasket();
+ showToast(`Заказ успешно оформлен! Сумма: $${totalPrice.toLocaleString()}`, 'success');
+
+ clearBasket();
+
+ } catch (error) {
+ showToast(`Ошибка при оформлении заказа: ${error.message}`, 'error');
+ } finally {
+ setOrderProcessing(false);
+ }
+ };
+
+ if (basketItems.length === 0) {
+ return (
+
+
+
Корзина
+
+
+
Ваша корзина пуста
+
+
+
+ );
+ }
+
+ const totalPrice = getTotalPriceBasket();
+
+ return (
+
+
+
+
+
Корзина
+
+ {user && (
+
+
+
+
+
{user.fullName || user.email || 'Имя пользователя'}
+
+
+ Пункт выдачи:
+ Ул. Московская 231к3
+
+
+
+ )}
+
+
+
+
+ {basketItems.map(item => (
+
+ ))}
+
+
+
Итого: ${totalPrice.toLocaleString()}
+
+ {orderProcessing ? 'Обработка...' : 'Оформить заказ'}
+
+
+
+
+ );
+};
+
+export default Basket;
\ No newline at end of file
@@ -0,0 +1,205 @@
+.container {
+ min-height: 70vh;
+ padding: 20px 50px;
+}
+
+.favoritesHeader {
+ text-align: center;
+ margin-bottom: 20px;
+}
+
+.iconLove {
+ opacity: 0.3;
+}
+
+.main {
+ margin-bottom: 30px;
+}
+
+.title {
+ text-align: center;
+ font-size: 36px;
+ margin-bottom: 30px;
+}
+
+.userInfoPanel {
+ background: #f9f9f9;
+ border-radius: 10px;
+ padding: 20px;
+ margin-bottom: 30px;
+}
+
+.userInfoContent {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.userProfile {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+}
+
+.username {
+ font-weight: 600;
+ font-size: 18px;
+}
+
+.pickupPoint {
+ text-align: right;
+ color: #666;
+ font-size: 14px;
+}
+
+.emptyBasket {
+ text-align: center;
+ padding: 60px 20px;
+}
+
+.emptyBasket p {
+ margin-top: 20px;
+ font-size: 18px;
+ color: #666;
+}
+
+.basketMain {
+ padding: 20px 0;
+}
+
+.basketGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.basketItem {
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ display: flex;
+ align-items:center ;
+}
+
+.itemImage {
+ width: 130px;
+ height: 200px;
+ object-fit: cover;
+}
+
+.itemInfo {
+ padding: 20px;
+}
+
+.itemInfo h3 {
+ margin: 0 0 10px 0;
+ font-size: 18px;
+}
+
+.itemInfo p {
+ margin: 5px 0;
+ color: #666;
+}
+
+.price {
+ font-size: 20px;
+ font-weight: 700;
+ color: #667eea;
+ margin: 10px 0 !important;
+}
+
+.quantityControl {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ margin: 15px 0;
+}
+
+.quantityControl button {
+ width: 30px;
+ height: 30px;
+ border: 1px solid #ddd;
+ background: white;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 18px;
+ transition: all 0.3s;
+}
+
+.quantityControl button:hover {
+ background: #667eea;
+ color: white;
+ border-color: #667eea;
+}
+
+.quantityControl span {
+ font-size: 16px;
+ font-weight: 600;
+ min-width: 30px;
+ text-align: center;
+}
+
+.removeBtn {
+ width: 100%;
+ padding: 10px;
+ background: #ff4444;
+ color: white;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background 0.3s;
+}
+
+.removeBtn:hover {
+ background: #cc0000;
+}
+
+.totalSection {
+ text-align: center;
+ padding: 30px;
+ background: #f9f9f9;
+ border-radius: 10px;
+}
+
+.totalSection h2 {
+ margin-bottom: 20px;
+ color: #667eea;
+}
+
+.checkoutBtn {
+ padding: 15px 40px;
+ background: #667eea;
+ color: white;
+ border: none;
+ border-radius: 5px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.3s;
+}
+
+.checkoutBtn:hover {
+ background: #5568d3;
+}
+
+@media (max-width: 768px) {
+ .container {
+ padding: 20px;
+ }
+
+ .basketGrid {
+ grid-template-columns: 1fr;
+ }
+
+ .userInfoContent {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .pickupPoint {
+ text-align: center;
+ }
+}
@@ -0,0 +1,25 @@
+import React from 'react';
+import styles from './Basket.module.css';
+
+const BasketItem = ({ item, onUpdateQuantity, onRemove }) => {
+ return (
+
+
+
+
{item.name}
+
{item.brand}
+
${item.price.toLocaleString()}
+
+ onUpdateQuantity(item.id, item.quantity - 1)}>-
+ {item.quantity}
+ onUpdateQuantity(item.id, item.quantity + 1)}>+
+
+
onRemove(item.id)}>
+ Удалить
+
+
+
+ );
+};
+
+export default BasketItem;
\ No newline at end of file
@@ -0,0 +1,31 @@
+
+
+import React from 'react';
+import { faqData } from './dataFaq';
+import styles from './FAQ.module.css';
+
+const FAQ = () => {
+ return (
+
+
+
FAQ
+
+
+ {faqData.map((section, idx) => (
+
+
{section.category}
+ {section.questions.map((item, qIdx) => (
+
+
Q: {item.q}
+
A: {item.a}
+
+ ))}
+
+ ))}
+
+
+
+ );
+};
+
+export default FAQ;
@@ -0,0 +1,82 @@
+.container {
+ min-height: 70vh;
+ padding: 40px 50px;
+}
+
+.main {
+ max-width: 900px;
+ margin: 0 auto;
+}
+
+.title {
+ text-align: center;
+ font-size: 42px;
+ margin-bottom: 40px;
+ color: #333;
+}
+
+.faqPanel {
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ padding: 40px;
+}
+
+.faqSection {
+ margin-bottom: 40px;
+}
+
+.faqSection:last-child {
+ margin-bottom: 0;
+}
+
+.faqSection h2 {
+ font-size: 24px;
+ color: #667eea;
+ margin-bottom: 20px;
+ padding-bottom: 10px;
+ border-bottom: 2px solid #667eea;
+}
+
+.faqItem {
+ margin-bottom: 25px;
+ padding: 20px;
+ background: #f9f9f9;
+ border-radius: 5px;
+}
+
+.faqItem:last-child {
+ margin-bottom: 0;
+}
+
+.faqItem h3 {
+ font-size: 16px;
+ color: #333;
+ margin-bottom: 10px;
+ font-weight: 600;
+}
+
+.faqItem p {
+ font-size: 15px;
+ color: #555;
+ line-height: 1.6;
+ margin: 0;
+}
+
+@media (max-width: 768px) {
+ .container {
+ padding: 20px;
+ }
+
+ .faqPanel {
+ padding: 30px 20px;
+ }
+
+ .title {
+ font-size: 32px;
+ }
+
+ .faqSection h2 {
+ font-size: 20px;
+ }
+}
@@ -0,0 +1,71 @@
+export const faqData = [
+ {
+ category: '1. О продуктах',
+ questions: [
+ {
+ q: 'Какие материалы используются в часах Timeless?',
+ a: 'Мы используем высококачественную нержавеющую сталь 316L, сапфировые стёкла, итальянскую кожу и швейцарские механизмы.'
+ },
+ {
+ q: 'Есть ли водозащита?',
+ a: 'Да, большинство моделей имеют водонепроницаемость от 50м (5ATM) до 300м (дайверские часы).'
+ },
+ {
+ q: 'Где производятся часы?',
+ a: 'Дизайн разрабатывается в Европе, сборка осуществляется на швейцарских и японских мануфактурах.'
+ }
+ ]
+ },
+ {
+ category: '2. Заказ и доставка',
+ questions: [
+ {
+ q: 'Как оформить заказ?',
+ a: 'Выберите модель → добавьте в корзину → укажите адрес → оплатите удобным способом (карта, Apple/Google Pay, криптовалюта).'
+ },
+ {
+ q: 'Какие способы доставки доступны?',
+ a: 'Курьерская доставка (1–3 дня). Самовывоз из фирменных бутиков. Международная доставка (DHL/FedEx, сроки уточняйте).'
+ },
+ {
+ q: 'Можно ли изменить/отменить заказ?',
+ a: 'Да, в течение 24 часов после оформления (напишите нам на support@timeless.com).'
+ }
+ ]
+ },
+ {
+ category: '3. Оплата',
+ questions: [
+ {
+ q: 'Какие платежи принимаются?',
+ a: 'VISA, Mastercard, PayPal, банковские переводы, BTC/ETH (для VIP-клиентов).'
+ },
+ {
+ q: 'Безопасна ли оплата на сайте?',
+ a: 'Да, мы используем шифрование SSL и PCI DSS-совместимые платёжные шлюзы.'
+ }
+ ]
+ },
+ {
+ category: '4. Гарантия и возврат',
+ questions: [
+ {
+ q: 'Есть ли гарантия?',
+ a: 'Да, 2 года на механизмы и 1 год на аксессуары (брак, заводские дефекты).'
+ },
+ {
+ q: 'Как вернуть часы?',
+ a: 'При наличии чека и целой упаковки — в течение 14 дней. Подробнее в разделе «Возврат».'
+ }
+ ]
+ },
+ {
+ category: '5. Уход за часами',
+ questions: [
+ {
+ q: 'Как чистить часы?',
+ a: 'Кожаный ремешок: сухая мягкая ткань. Корпус из стали: мыльный раствор (без погружения!). Избегайте магнитов, ударов и резких перепадов температур.'
+ }
+ ]
+ }
+];
\ No newline at end of file
@@ -0,0 +1,32 @@
+import React from 'react';
+import { products } from '../../../utils/products';
+import ProductCard from '../../common/ProductCard/ProductCard';
+import styles from './Home.module.css';
+
+const Home = () => {
+ return (
+
+
+
+
Продукция
+
+ {products.map(product => (
+
+ ))}
+
+
+
+ );
+};
+
+export default Home;
@@ -0,0 +1,51 @@
+.homePage {
+ min-height: 80vh;
+}
+
+.header {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ padding: 80px 50px;
+ text-align: center;
+}
+
+.slogan h1 {
+ font-size: 48px;
+ font-weight: 700;
+ line-height: 1.3;
+ margin: 0;
+}
+
+.main {
+ padding: 40px 50px;
+}
+
+.main h4 {
+ font-size: 32px;
+ margin-bottom: 30px;
+ text-align: center;
+}
+
+.items {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 30px;
+}
+
+@media (max-width: 768px) {
+ .header {
+ padding: 40px 20px;
+ }
+
+ .slogan h1 {
+ font-size: 32px;
+ }
+
+ .main {
+ padding: 30px 20px;
+ }
+
+ .items {
+ grid-template-columns: 1fr;
+ }
+}
@@ -0,0 +1,101 @@
+import React, { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { useNavigate } from 'react-router-dom';
+import { useUser } from '../../../context/UserContext';
+import { useToast } from '../../../context/ToastContext';
+import styles from './Login.module.css';
+
+const Login = () => {
+ const { register: registerForm, handleSubmit, formState: { errors } } = useForm();
+ const { login } = useUser();
+ const { showToast } = useToast();
+ const navigate = useNavigate();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const onSubmit = (data) => {
+ setIsSubmitting(true);
+
+ const savedUser = localStorage.getItem('user');
+
+ if (!savedUser) {
+ showToast('Пользователь не найден. Пожалуйста, зарегистрируйтесь.', 'error');
+ setIsSubmitting(false);
+ return;
+ }
+
+ const user = JSON.parse(savedUser);
+
+ if (user.phone === data.phone && user.password === data.password) {
+ const result = login(user);
+
+ if (result.success) {
+ showToast('Вход выполнен успешно!', 'success');
+ setTimeout(() => navigate('/profile'), 1500);
+ } else {
+ showToast(result.error || 'Ошибка входа', 'error');
+ setIsSubmitting(false);
+ }
+ } else {
+ showToast('Неверный телефон или пароль', 'error');
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default Login;
\ No newline at end of file
@@ -0,0 +1,116 @@
+.loginPage {
+ min-height: 70vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 40px 20px;
+}
+
+.main {
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ padding: 40px;
+ max-width: 500px;
+ width: 100%;
+}
+
+.main h4 {
+ font-size: 32px;
+ text-align: center;
+ margin-bottom: 30px;
+ color: #333;
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.formGroup {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+
+.input {
+ width: 100%;
+ padding: 15px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.3s;
+ box-sizing: border-box;
+}
+
+.input:focus {
+ border-color: #667eea;
+}
+
+.input.error {
+ border-color: #ff4444;
+}
+
+.errorText {
+ color: #ff4444;
+ font-size: 12px;
+ margin-top: 5px;
+}
+
+.saveButton {
+ width: 100%;
+ padding: 15px;
+ background: #667eea;
+ color: white;
+ border: none;
+ border-radius: 5px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.3s;
+ margin-top: 10px;
+}
+
+.saveButton:hover {
+ background: #5568d3;
+}
+
+.saveButton:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+}
+
+.registerLink {
+ text-align: center;
+ margin-top: 20px;
+ color: #666;
+ font-size: 14px;
+}
+
+.registerButton {
+ background: none;
+ border: none;
+ color: #667eea;
+ font-weight: 600;
+ cursor: pointer;
+ text-decoration: underline;
+ padding: 0;
+ margin-left: 5px;
+ transition: color 0.3s;
+}
+
+.registerButton:hover {
+ color: #5568d3;
+}
+
+@media (max-width: 768px) {
+ .main {
+ padding: 30px 20px;
+ }
+
+ .main h4 {
+ font-size: 24px;
+ }
+}
\ No newline at end of file
@@ -0,0 +1,86 @@
+import React from 'react';
+import styles from './Policy.module.css';
+
+const Policy = () => {
+ return (
+
+
+
Политика и Конфиденциальность
+
+
+
+
1. Сбор информации
+
+ Мы собираем информацию, которую вы предоставляете при регистрации на сайте, оформлении заказа
+ или подписке на рассылку. Это может включать ваше имя, адрес электронной почты, номер телефона,
+ адрес доставки и платежную информацию.
+
+
+
+
+
2. Использование информации
+
Собранная информация используется для:
+
+ Обработки и выполнения ваших заказов
+ Улучшения качества обслуживания клиентов
+ Отправки периодических email-рассылок о новых продуктах и специальных предложениях
+ Персонализации вашего опыта использования сайта
+
+
+
+
+
3. Защита информации
+
+ Мы применяем различные меры безопасности для защиты вашей персональной информации. Ваши личные
+ данные хранятся в защищенных сетях и доступны только ограниченному числу лиц, имеющих специальные
+ права доступа к таким системам и обязанных сохранять конфиденциальность информации.
+
+
+
+
+
4. Использование файлов cookie
+
+ Мы используем файлы cookie для улучшения работы сайта и персонализации вашего опыта. Cookie помогают
+ нам запомнить ваши предпочтения и понять, как вы используете наш сайт.
+
+
+
+
+
5. Раскрытие информации третьим лицам
+
+ Мы не продаем, не обмениваем и не передаем вашу личную информацию третьим лицам, за исключением
+ доверенных партнеров, которые помогают нам в управлении сайтом, ведении бизнеса или обслуживании вас,
+ при условии, что эти стороны согласны сохранять конфиденциальность этой информации.
+
+
+
+
+
6. Ваши права
+
Вы имеете право:
+
+ Запросить доступ к вашим персональным данным
+ Запросить исправление неточных данных
+ Запросить удаление ваших данных
+ Отказаться от получения маркетинговых материалов
+
+
+
+
+
7. Контактная информация
+
+ Если у вас есть вопросы относительно нашей политики конфиденциальности, вы можете связаться с нами:
+
+
Email: privacy@timeless.com
+
Телефон: +7 (800) 555-35-35
+
+
+
+ Последнее обновление: 24 декабря 2025 г.
+
+
+
+
+ );
+};
+
+export default Policy;
@@ -0,0 +1,98 @@
+.container {
+ min-height: 70vh;
+ padding: 40px 50px;
+}
+
+.main {
+ max-width: 900px;
+ margin: 0 auto;
+}
+
+.title {
+ text-align: center;
+ font-size: 42px;
+ margin-bottom: 40px;
+ color: #333;
+}
+
+.policyPanel {
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ padding: 40px;
+}
+
+.policySection {
+ margin-bottom: 35px;
+}
+
+.policySection:last-of-type {
+ margin-bottom: 20px;
+}
+
+.policySection h2 {
+ font-size: 22px;
+ color: #667eea;
+ margin-bottom: 15px;
+}
+
+.policySection p {
+ font-size: 16px;
+ color: #555;
+ line-height: 1.7;
+ margin-bottom: 15px;
+}
+
+.policySection ul {
+ margin: 15px 0;
+ padding-left: 30px;
+}
+
+.policySection ul li {
+ margin-bottom: 10px;
+ color: #555;
+ font-size: 16px;
+ line-height: 1.6;
+}
+
+.policySection a {
+ color: #667eea;
+ text-decoration: none;
+}
+
+.policySection a:hover {
+ text-decoration: underline;
+}
+
+.updateDate {
+ text-align: center;
+ font-style: italic;
+ color: #888;
+ font-size: 14px;
+ margin-top: 30px;
+ padding-top: 20px;
+ border-top: 1px solid #eee;
+}
+
+@media (max-width: 768px) {
+ .container {
+ padding: 20px;
+ }
+
+ .policyPanel {
+ padding: 30px 20px;
+ }
+
+ .title {
+ font-size: 32px;
+ }
+
+ .policySection h2 {
+ font-size: 20px;
+ }
+
+ .policySection p,
+ .policySection ul li {
+ font-size: 15px;
+ }
+}
@@ -0,0 +1,29 @@
+import React from 'react';
+import styles from './Profile.module.css';
+
+const GenderRadio = ({ register, error }) => {
+ const genderOptions = [
+ { value: 'male', label: 'М' },
+ { value: 'female', label: 'Ж' }
+ ];
+
+ return (
+ <>
+
+ {genderOptions.map(option => (
+
+
+ {option.label}
+
+ ))}
+
+ {error && {error.message} }
+ >
+ );
+};
+
+export default GenderRadio;
\ No newline at end of file
@@ -0,0 +1,136 @@
+
+import React from 'react';
+import { useForm } from 'react-hook-form';
+import { useUser } from '../../../context/UserContext';
+import { useNavigate } from 'react-router-dom';
+import { useToast } from '../../../context/ToastContext';
+import GenderRadio from './GenderRadio';
+import { VALIDATORS } from './profileValidators';
+import styles from './Profile.module.css';
+
+const Profile = () => {
+ const { user, updateUser, isAuthenticated, logout } = useUser();
+ const navigate = useNavigate();
+ const { showToast } = useToast();
+ const { register, handleSubmit, formState: { errors } } = useForm({
+ defaultValues: {
+ firstName: user?.firstName || '',
+ lastName: user?.lastName || '',
+ gender: user?.gender || '',
+ cardNumber: user?.cardNumber || '',
+ cardExpiry: user?.cardExpiry || '',
+ cardCVV: user?.cardCVV || ''
+ }
+ });
+
+ React.useEffect(() => {
+ if (!isAuthenticated) {
+ navigate('/registration');
+ }
+ }, [isAuthenticated, navigate]);
+
+ const onSubmit = (data) => {
+ updateUser(data);
+ showToast('Профиль обновлен!', 'success');
+ };
+
+ const handleLogout = () => {
+ logout();
+ showToast('Вы вышли из аккаунта', 'success');
+ setTimeout(() => navigate('/'), 500);
+ };
+
+ if (!isAuthenticated) {
+ return null;
+ }
+
+ return (
+
+
+
+
Профиль
+
+
+
+
+
+
+
+
+ Выйти
+
+
+
+ );
+};
+
+export default Profile;
\ No newline at end of file
@@ -0,0 +1,192 @@
+.profilePage {
+ min-height: 70vh;
+ padding: 40px 50px;
+}
+
+.main {
+ max-width: 1200px;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 40px;
+}
+
+.profileInfo {
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ padding: 40px;
+ text-align: center;
+}
+
+.profileInfo h3 {
+ font-size: 32px;
+ margin-bottom: 20px;
+}
+
+.icon {
+ margin-bottom: 30px;
+}
+
+.form {
+ width: 100%;
+}
+
+.inputContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ max-width: 400px;
+ margin: 0 auto;
+}
+
+.formGroup {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+
+.input {
+ width: 100%;
+ padding: 15px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.3s;
+ box-sizing: border-box;
+}
+
+.input:focus {
+ border-color: #667eea;
+}
+
+.input.error {
+ border-color: #ff4444;
+}
+
+.errorText {
+ color: #ff4444;
+ font-size: 12px;
+ text-align: left;
+}
+
+.genderGroup {
+ display: flex;
+ gap: 30px;
+ justify-content: center;
+}
+
+.genderGroup label {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+}
+
+.genderGroup input[type="radio"] {
+ width: 20px;
+ height: 20px;
+ cursor: pointer;
+}
+
+.saveButton {
+ width: 100%;
+ padding: 15px;
+ background: #667eea;
+ color: white;
+ border: none;
+ border-radius: 5px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.3s;
+}
+
+.saveButton:hover {
+ background: #5568d3;
+}
+
+.paymentInfo {
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ padding: 40px;
+}
+
+.cart h4 {
+ font-size: 24px;
+ margin-bottom: 20px;
+ text-align: center;
+}
+
+.inputCart1 {
+ width: 425px;
+ padding: 15px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ font-size: 14px;
+ outline: none;
+ margin-bottom: 15px;
+ transition: border-color 0.3s;
+}
+
+.inputCart1:focus {
+ border-color: #667eea;
+}
+
+.cardDetails {
+ display: flex;
+ gap: 15px;
+}
+
+.inputCart2 {
+ width: 100%;
+ padding: 15px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.3s;
+}
+
+.inputCart2:focus {
+ border-color: #667eea;
+}
+
+.inputCart2.error,
+.inputCart1.error {
+ border-color: #ff4444;
+}
+
+.logoutButton {
+ padding: 15px 40px;
+ background: #ff4444;
+ color: white;
+ border: none;
+ border-radius: 5px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.3s;
+ align-self: center;
+}
+
+.logoutButton:hover {
+ background: #cc0000;
+}
+
+@media (max-width: 768px) {
+ .profilePage {
+ padding: 20px;
+ }
+
+ .profileInfo,
+ .paymentInfo {
+ padding: 30px 20px;
+ }
+
+ .cardDetails {
+ flex-direction: column;
+ }
+}
@@ -0,0 +1,14 @@
+import React from 'react';
+import styles from './Toast.module.css';
+
+export const ToastContainer = ({ toasts }) => {
+ return (
+
+ {toasts.map(toast => (
+
+ {toast.message}
+
+ ))}
+
+ );
+};
\ No newline at end of file
@@ -0,0 +1,58 @@
+.toastContainer {
+ position: fixed;
+ top: 40px;
+ right: 50px;
+ z-index: 9999;
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.toast {
+ padding: 20px 30px;
+ border-radius: 10px;
+ color: white;
+ font-size: 16px;
+ font-weight: 600;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ animation: slideIn 0.3s ease-out;
+ min-width: 300px;
+ text-align: center;
+}
+
+.success {
+ background: #667eea;
+}
+
+.error {
+ background: #ff4444;
+}
+
+.info {
+ background: #5568d3;
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateX(400px);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+@media (max-width: 768px) {
+ .toastContainer {
+ top: 20px;
+ right: 20px;
+ left: 20px;
+ }
+
+ .toast {
+ padding: 15px 20px;
+ font-size: 14px;
+ min-width: auto;
+ }
+}
\ No newline at end of file
@@ -0,0 +1,36 @@
+export const VALIDATORS = {
+ firstName: {
+ required: 'Имя обязательно',
+ minLength: { value: 2, message: 'Минимум 2 символа' }
+ },
+
+ lastName: {
+ required: 'Фамилия обязательна',
+ minLength: { value: 2, message: 'Минимум 2 символа' }
+ },
+
+ gender: {
+ required: 'Выберите пол'
+ },
+
+ cardNumber: {
+ pattern: {
+ value: /^[0-9\s]{16,19}$/,
+ message: 'Некорректный номер карты'
+ }
+ },
+
+ cardExpiry: {
+ pattern: {
+ value: /^(0[1-9]|1[0-2])\/[0-9]{2}$/,
+ message: 'Формат: MM/YY'
+ }
+ },
+
+ cardCVV: {
+ pattern: {
+ value: /^[0-9]{3}$/,
+ message: '3 цифры'
+ }
+ }
+};
\ No newline at end of file
@@ -0,0 +1,117 @@
+
+import React, { useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { useNavigate } from 'react-router-dom';
+import { useUser } from '../../../context/UserContext';
+import { useToast } from '../../../context/ToastContext';
+import { VALIDATION_RULES, TOAST_MESSAGES } from './validationRules';
+import styles from './Registration.module.css';
+
+const Registration = () => {
+ const { register: registerForm, handleSubmit, formState: { errors }, watch } = useForm();
+ const { register } = useUser();
+ const { showToast } = useToast();
+ const navigate = useNavigate();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const onSubmit = (data) => {
+ setIsSubmitting(true);
+
+ const userData = {
+ fullName: data.fullName,
+ email: data.email,
+ phone: data.phone,
+ password: data.password
+ };
+
+ const result = register(userData);
+
+ if (result.success) {
+ showToast(TOAST_MESSAGES.registrationSuccess, 'success');
+ setTimeout(() => navigate('/profile'), 1500);
+ } else {
+ showToast(result.error || TOAST_MESSAGES.registrationError, 'error');
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default Registration;
\ No newline at end of file
@@ -0,0 +1,115 @@
+.registrationPage {
+ min-height: 70vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 40px 20px;
+}
+
+.main {
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ padding: 40px;
+ max-width: 500px;
+ width: 100%;
+}
+
+.main h4 {
+ font-size: 32px;
+ text-align: center;
+ margin-bottom: 30px;
+ color: #333;
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.formGroup {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+
+.input {
+ width: 100%;
+ padding: 15px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.3s;
+ box-sizing: border-box;
+}
+
+.input:focus {
+ border-color: #667eea;
+}
+
+.input.error {
+ border-color: #ff4444;
+}
+
+.errorText {
+ color: #ff4444;
+ font-size: 12px;
+ margin-top: 5px;
+}
+
+.saveButton {
+ width: 100%;
+ padding: 15px;
+ background: #667eea;
+ color: white;
+ border: none;
+ border-radius: 5px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.3s;
+ margin-top: 10px;
+}
+
+.saveButton:hover {
+ background: #5568d3;
+}
+
+.saveButton:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+}
+
+@media (max-width: 768px) {
+ .main {
+ padding: 30px 20px;
+ }
+
+ .main h4 {
+ font-size: 24px;
+ }
+}
+.loginLink {
+ text-align: center;
+ margin-top: 20px;
+ color: #666;
+ font-size: 14px;
+}
+
+.loginButton {
+ background: none;
+ border: none;
+ color: #667eea;
+ font-weight: 600;
+ cursor: pointer;
+ text-decoration: underline;
+ padding: 0;
+ margin-left: 5px;
+ transition: color 0.3s;
+}
+
+.loginButton:hover {
+ color: #5568d3;
+}
@@ -0,0 +1,92 @@
+export const VALIDATION_RULES = {
+ fullName: {
+ required: 'Полное имя обязательно',
+ minLength: {
+ value: 2,
+ message: 'Минимум 2 символа'
+ },
+ validate: (value) => {
+ const trimmed = value.trim();
+ if (trimmed.length < 2) {
+ return 'Минимум 2 символа';
+ }
+ return true;
+ }
+ },
+
+ email: {
+ required: 'Email обязателен',
+ pattern: {
+ value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
+ message: 'Некорректный email адрес'
+ }
+ },
+
+ phone: {
+ required: 'Телефон обязателен',
+ pattern: {
+ value: /^(\+7|8)?[\s\-]?\(?[489][0-9]{2}\)?[\s\-]?[0-9]{3}[\s\-]?[0-9]{2}[\s\-]?[0-9]{2}$/,
+ message: 'Некорректный номер телефона'
+ }
+ },
+
+ password: {
+ required: 'Пароль обязателен',
+ minLength: {
+ value: 6,
+ message: 'Минимум 6 символов'
+ }
+ },
+
+ confirmPassword: (watchPassword) => ({
+ required: 'Подтверждение пароля обязательно',
+ validate: (value) => value === watchPassword || 'Пароли не совпадают'
+ }),
+
+ address: {
+ required: 'Адрес обязателен',
+ minLength: {
+ value: 5,
+ message: 'Минимум 5 символов'
+ }
+ },
+
+ city: {
+ required: 'Город обязателен',
+ minLength: {
+ value: 2,
+ message: 'Минимум 2 символа'
+ }
+ },
+
+ postalCode: {
+ required: 'Почтовый индекс обязателен',
+ pattern: {
+ value: /^\d{6}$/,
+ message: 'Индекс должен содержать 6 цифр'
+ }
+ },
+
+ optionalEmail: {
+ pattern: {
+ value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
+ message: 'Некорректный email адрес'
+ }
+ }
+};
+
+export const TOAST_MESSAGES = {
+
+ registrationSuccess: 'Регистрация успешна!',
+ loginSuccess: 'Вход выполнен успешно!',
+ profileUpdateSuccess: 'Профиль успешно обновлен!',
+ logoutSuccess: 'Вы вышли из системы',
+
+ userNotFound: 'Пользователь не найден. Пожалуйста, зарегистрируйтесь.',
+ invalidCredentials: 'Неверный телефон или пароль',
+ registrationError: 'Ошибка регистрации',
+ loginError: 'Ошибка входа',
+ profileUpdateError: 'Ошибка обновления профиля',
+
+ unknownError: 'Произошла неизвестная ошибка'
+};
\ No newline at end of file
@@ -0,0 +1,80 @@
+import React, { createContext, useContext, useState, useEffect } from 'react';
+
+const BasketContext = createContext();
+
+export const useBasket = () => {
+ const context = useContext(BasketContext);
+ if (!context) {
+ throw new Error('useBasket must be used within BasketProvider');
+ }
+ return context;
+};
+
+export const BasketProvider = ({ children }) => {
+ const [basketItems, setBasketItems] = useState(() => {
+ const saved = localStorage.getItem('basket');
+ return saved ? JSON.parse(saved) : [];
+ });
+
+ useEffect(() => {
+ localStorage.setItem('basket', JSON.stringify(basketItems));
+ }, [basketItems]);
+
+ const addToBasket = (product) => {
+ setBasketItems(prev => {
+ const existing = prev.find(item => item.id === product.id);
+ if (existing) {
+ return prev.map(item =>
+ item.id === product.id
+ ? { ...item, quantity: item.quantity + 1 }
+ : item
+ );
+ }
+ return [...prev, { ...product, quantity: 1 }];
+ });
+ };
+
+ const removeFromBasket = (productId) => {
+ setBasketItems(prev => prev.filter(item => item.id !== productId));
+ };
+
+ const updateQuantityBasket = (productId, quantity) => {
+ if (quantity <= 0) {
+ removeFromBasket(productId);
+ return;
+ }
+ setBasketItems(prev =>
+ prev.map(item =>
+ item.id === productId ? { ...item, quantity } : item
+ )
+ );
+ };
+
+ const clearBasket = () => {
+ setBasketItems([]);
+ };
+
+ const getTotalPriceBasket = () => {
+ return basketItems.reduce((total, item) => total + (item.price * item.quantity), 0);
+ };
+
+ const getTotalItems = () => {
+ return basketItems.reduce((total, item) => total + item.quantity, 0);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
@@ -0,0 +1,30 @@
+import React, { createContext, useContext, useState } from 'react';
+
+const ToastContext = createContext();
+
+export const useToast = () => {
+ const context = useContext(ToastContext);
+ if (!context) {
+ throw new Error('useToast must be used within ToastProvider');
+ }
+ return context;
+};
+
+export const ToastProvider = ({ children }) => {
+ const [toasts, setToasts] = useState([]);
+
+ const showToast = (message, type = 'success') => {
+ const id = Date.now();
+ setToasts(prev => [...prev, { id, message, type }]);
+
+ setTimeout(() => {
+ setToasts(prev => prev.filter(toast => toast.id !== id));
+ }, 3000);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
@@ -0,0 +1,174 @@
+
+
+import React, { createContext, useContext, useState, useEffect } from 'react';
+import { v4 as uuidv4 } from 'uuid';
+
+const UserContext = createContext();
+
+export const useUser = () => {
+ const context = useContext(UserContext);
+ if (!context) {
+ throw new Error('useUser must be used within UserProvider');
+ }
+ return context;
+};
+
+const isValidEmail = (email) => {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+};
+
+const isValidPassword = (password) => {
+ return password && password.length >= 6;
+};
+
+const validateRegistrationData = (userData) => {
+ const errors = [];
+
+ if (!userData || typeof userData !== 'object') {
+ throw new Error('userData должен быть объектом');
+ }
+
+ if (!userData.email || !isValidEmail(userData.email)) {
+ errors.push('Некорректный email адрес');
+ }
+
+ if (!userData.password || !isValidPassword(userData.password)) {
+ errors.push('Пароль должен содержать минимум 6 символов');
+ }
+
+ if (!userData.fullName || userData.fullName.trim().length < 2) {
+ errors.push('Имя должно содержать минимум 2 символа');
+ }
+
+ if (errors.length > 0) {
+ throw new Error(errors.join('; '));
+ }
+
+ return true;
+};
+
+const validateLoginData = (userData) => {
+ const errors = [];
+
+ if (!userData || typeof userData !== 'object') {
+ throw new Error('userData должен быть объектом');
+ }
+
+ if (!userData.email || !isValidEmail(userData.email)) {
+ errors.push('Некорректный email адрес');
+ }
+
+ if (!userData.password) {
+ errors.push('Пароль обязателен');
+ }
+
+ if (errors.length > 0) {
+ throw new Error(errors.join('; '));
+ }
+
+ return true;
+};
+
+const validateUpdateData = (updates) => {
+ if (!updates || typeof updates !== 'object') {
+ throw new Error('updates должен быть объектом');
+ }
+
+ if (updates.email && !isValidEmail(updates.email)) {
+ throw new Error('Некорректный email адрес');
+ }
+
+ if (updates.password && !isValidPassword(updates.password)) {
+ throw new Error('Пароль должен содержать минимум 6 символов');
+ }
+
+ if (updates.fullName !== undefined && (!updates.fullName || updates.fullName.trim().length < 2)) {
+ throw new Error('Имя должно содержать минимум 2 символа');
+ }
+
+ return true;
+};
+
+export const UserProvider = ({ children }) => {
+ const [user, setUser] = useState(() => {
+ try {
+ const saved = localStorage.getItem('user');
+ return saved ? JSON.parse(saved) : null;
+ } catch (error) {
+ return null;
+ }
+ });
+
+ const [isAuthenticated, setIsAuthenticated] = useState(!!user);
+
+ useEffect(() => {
+ try {
+ if (user) {
+ localStorage.setItem('user', JSON.stringify(user));
+ setIsAuthenticated(true);
+ } else {
+ localStorage.removeItem('user');
+ setIsAuthenticated(false);
+ }
+ } catch (error) {
+
+ }
+ }, [user]);
+
+ const login = (userData) => {
+ try {
+ validateLoginData(userData);
+ setUser(userData);
+ return { success: true };
+ } catch (error) {
+ return { success: false, error: error.message };
+ }
+ };
+
+ const logout = () => {
+ setUser(null);
+ };
+
+ const updateUser = (updates) => {
+ try {
+ validateUpdateData(updates);
+ setUser(prev => prev ? { ...prev, ...updates } : null);
+ return { success: true };
+ } catch (error) {
+ return { success: false, error: error.message };
+ }
+ };
+
+ const register = (userData) => {
+ try {
+ validateRegistrationData(userData);
+
+ const newUser = {
+ ...userData,
+ id: uuidv4(),
+ createdAt: new Date().toISOString()
+ };
+
+ setUser(newUser);
+ return { success: true, user: newUser };
+ } catch (error) {
+ return { success: false, error: error.message };
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
@@ -0,0 +1,83 @@
+export const products = [
+ {
+ id: 1,
+ image: "Collection Moon Phase.svg",
+ name: "Collection Moon Phase",
+ brand: "Longines Elegant",
+ description: "- это швейцарский часовой бренд, входящий в Swatch Group...",
+ price: 10000,
+ link: "collection-moon-phase"
+ },
+ {
+ id: 2,
+ image: "Conquest.svg",
+ name: "Conquest",
+ brand: "Longines",
+ description: "- это швейцарский часовой бренд с богатой историей, входящий...",
+ price: 15500,
+ link: "conquest"
+ },
+ {
+ id: 3,
+ image: "Defy Classic.svg",
+ name: "Defy Classic",
+ brand: "Zenith",
+ description: "- это швейцарский бренд, известный своими высокоточными...",
+ price: 11500,
+ link: "defy-classic"
+ },
+ {
+ id: 4,
+ image: "Hampden.svg",
+ name: "Hampden",
+ brand: "Hampden",
+ description: "- это бренд, возрожденный в наше время...",
+ price: 14500,
+ link: "hampden"
+ },
+ {
+ id: 5,
+ image: "Marvin.svg",
+ name: "Marvin",
+ brand: "Marvin",
+ description: "- это швейцарский часовой бренд с богатой историей, основанный...",
+ price: 25500,
+ link: "marvin"
+ },
+ {
+ id: 6,
+ image: "Oyster Perpetual.svg",
+ name: "Oyster Perpetual",
+ brand: "Rolex",
+ description: "- это классическая модель Rolex, известная своей...",
+ price: 10500,
+ link: "oyster-perpetual"
+ },
+ {
+ id: 7,
+ image: "Paul Hewitt.svg",
+ name: "Paul Hewitt",
+ brand: "Sailor Line",
+ description: "- это немецкий бренд, известный своими часами и аксессуарами...",
+ price: 15500,
+ link: "paul-hewitt"
+ },
+ {
+ id: 8,
+ image: "Sky-Dweller.svg",
+ name: "Sky-Dweller",
+ brand: "Rolex",
+ description: "- это один из самых сложных и техничных...",
+ price: 25500,
+ link: "sky-dweller"
+ },
+ {
+ id: 9,
+ image: "Speedmaster.svg",
+ name: "Speedmaster",
+ brand: "Omega",
+ description: "- это швейцарский бренд класса люкс...",
+ price: 19500,
+ link: "speedmaster"
+ }
+];
@@ -0,0 +1,37 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.App {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+.main-content {
+ flex: 1;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
+
+a {
+ text-decoration: none;
+ color: inherit;
+}
+
+button {
+ font-family: inherit;
+}
@@ -0,0 +1,58 @@
+
+import React from 'react';
+import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
+import { BasketProvider } from './context/BasketContext';
+import { UserProvider } from './context/UserContext';
+import { ToastProvider, useToast } from './context/ToastContext';
+import { ToastContainer } from './components/pages/Profile/Toast';
+import Navigation from './components/common/Navigation/Navigation';
+import Footer from './components/common/Footer/Footer';
+import Home from './components/pages/Home/Home';
+import About from './components/pages/About/About';
+import Basket from './components/pages/Basket/Basket';
+import FAQ from './components/pages/FAQ/FAQ';
+import Profile from './components/pages/Profile/Profile';
+import Registration from './components/pages/Registration/Registration';
+import Login from './components/pages/Login/Login';
+import Policy from './components/pages/Policy/Policy';
+import './App.css';
+
+function AppContent() {
+ const { toasts } = useToast();
+
+ return (
+
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+
+ );
+}
+
+function App() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
@@ -0,0 +1,13 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
@@ -0,0 +1,11 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import App from './App';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
@@ -0,0 +1,23 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
@@ -0,0 +1,38 @@
+{
+ "name": "my-app",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "react": "^19.2.3",
+ "react-dom": "^19.2.3",
+ "react-hook-form": "^7.49.2",
+ "react-router-dom": "^6.21.1",
+ "react-scripts": "5.0.1",
+ "uuid": "^13.0.0",
+ "web-vitals": "^2.1.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}