@@ -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 + + + +
+ + @@ -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 ( + <> + + + ); +}; + +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.name} +
+
+

{product.name}

+
+

{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 ( +//
+//
+//

Корзина

+//
+// empty basket +//

Ваша корзина пуста

+//
+//
+//
+// ); +// } + +// const totalPrice = getTotalPriceBasket(); + +// return ( +//
+//
+//
+//
+//

Корзина

+ +// {user && ( +//
+//
+//
+// profile +//
{user.fullName || user.email || 'Имя пользователя'}
+//
+//
+// Пункт выдачи:
+// Ул. Московская 231к3 +//
+//
+//
+// )} +//
+ +//
+//
+// {basketItems.map(item => ( +// +// ))} +//
+//
+//

Итого: ${totalPrice.toLocaleString()}

+// +//
+//
+ +// {showNotification && ( +//
+//
+//

Заказ успешно оформлен!

+//

Ваш заказ на сумму ${totalPrice.toLocaleString()} принят в обработку

+// +//
+//
+// )} +//
+// ); +// }; + +// 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 ( +
+
+

Корзина

+
+ empty basket +

Ваша корзина пуста

+
+
+
+ ); + } + + const totalPrice = getTotalPriceBasket(); + + return ( +
+
+
+
+

Корзина

+ + {user && ( +
+
+
+ profile +
{user.fullName || user.email || 'Имя пользователя'}
+
+
+ Пункт выдачи:
+ Ул. Московская 231к3 +
+
+
+ )} +
+ +
+
+ {basketItems.map(item => ( + + ))} +
+
+

Итого: ${totalPrice.toLocaleString()}

+ +
+
+
+ ); +}; + +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.name}

+

{item.brand}

+

${item.price.toLocaleString()}

+
+ + {item.quantity} + +
+ +
+
+ ); +}; + +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 ( +
+
+

Вход

+
+
+ + {errors.phone && {errors.phone.message}} +
+ +
+ + {errors.password && {errors.password.message}} +
+ + + +
+ Нет аккаунта? + +
+
+
+
+ ); +}; + +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 => ( + + ))} +
+ {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 ( +
+
+
+

Профиль

+ profile + +
+
+
+ + {errors.firstName && {errors.firstName.message}} +
+ +
+ + {errors.lastName && {errors.lastName.message}} +
+ + + + +
+
+
+ +
+
+

Ваши реквизиты

+
+
+ + {errors.cardNumber && {errors.cardNumber.message}} +
+ +
+
+ + {errors.cardExpiry && {errors.cardExpiry.message}} +
+ +
+ + {errors.cardCVV && {errors.cardCVV.message}} +
+
+
+
+
+ + +
+
+ ); +}; + +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 ( +
+
+

Регистрация

+
+
+ + {errors.fullName && {errors.fullName.message}} +
+ +
+ + {errors.email && {errors.email.message}} +
+ +
+ + {errors.phone && {errors.phone.message}} +
+ +
+ + {errors.password && {errors.password.message}} +
+ +
+ + {errors.confirmPassword && {errors.confirmPassword.message}} +
+ + + +
+ Уже есть аккаунт? + +
+
+
+
+ ); +}; + +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" + ] + } +}