This commit is contained in:
Evdokia
2025-04-30 14:11:50 +03:00
parent 7f2ae842d2
commit fbff8f9c9c
18 changed files with 447 additions and 341 deletions

View File

@@ -1,11 +1,45 @@
/* Основные стили приложения */
.App {
display: flex;
flex-direction: column;
min-height: 100vh;
}
background: #fff; /* Добавляем белый фон */
}
main {
/* Стили для главного контента */
main {
flex: 1;
padding-bottom: 40px;
padding: 20px 0 40px;
max-width: 1200px; /* Ограничиваем ширину контента */
width: 100%;
margin: 0 auto; /* Центрируем контент */
}
/* Плавная прокрутка */
html {
scroll-behavior: smooth;
}
/* Стили для секций */
section {
padding: 60px 0;
scroll-margin-top: 80px; /* Учитываем высоту шапки */
}
/* Отступы между секциями */
section:not(:last-child) {
margin-bottom: 40px;
border-bottom: 1px solid #f0f0f0;
}
/* Для адаптивности */
@media (max-width: 768px) {
main {
padding: 15px 20px 30px;
}
section {
padding: 40px 0;
scroll-margin-top: 70px;
}
}

View File

@@ -14,24 +14,34 @@ function App() {
<Router>
<div className="App">
<Header />
<main>
<Routes>
<Route path="/" element={
<>
<h1 style={{ textAlign: 'center', marginTop: '20px' }}>
<h1 className="main-title">
Добро пожаловать в SkinCare Advisor!
</h1>
<section style={{ maxWidth: '700px', margin: '40px auto', padding: '0 20px' }}>
<section id="quiz" className="section-container">
<SkinTypeQuiz />
</section>
<section id="products" className="section-container">
<PopularProducts />
</section>
<section id="reviews" className="section-container">
<Reviews />
</section>
</>
} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Routes>
</main>
<Footer />
</div>
</Router>

View File

@@ -75,7 +75,6 @@
width: 100%;
}
/* Адаптивность */
@media (max-width: 768px) {
.footer-content {
flex-direction: column;
@@ -93,16 +92,3 @@
}
}
/* Фиксированный футер (опционально) */
/*
.App {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.site-footer {
margin-top: auto;
}
*/

View File

@@ -23,7 +23,7 @@ function Footer() {
<div className="copyright">
&copy; {new Date().getFullYear()} SkinCare Advisor. Все права защищены.
</div>
</div> {/*JS выражение, которое подставляет текущий год*/}
</footer>
);
}

View File

@@ -1,111 +1,141 @@
/* Основные стили шапки */
.site-header {
.site-header { /* Основной контейнер шапки сайта */
background: white;
padding: 15px 0;
padding: 15px 0; /* Отступы сверху/снизу */
position: sticky;
top: 0;
z-index: 1000;
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
border-bottom: 1px solid #f0f0f0;
top: 0; /* Привязка к верху */
z-index: 1000; /* Поверх других элементов */
box-shadow: 0 2px 15px rgba(0,0,0,0.07); /* Тень снизу */
border-bottom: 1px solid #f0f0f0; /* Граница снизу */
}
.header-content {
.header-content { /* Контейнер содержимого */
max-width: 1200px;
margin: 0 auto;
margin: 0 auto; /* Центрирование */
display: flex;
align-items: center;
padding: 0 20px;
gap: 30px;
align-items: center; /* Вертикальное выравнивание */
padding: 0 20px; /* Отступы по бокам */
gap: 22px; /* Расстояние между элементами */
}
/* Стили для логотипа */
.logo-link {
text-decoration: none;
.logo-link { /* Ссылка с логотипом */
display: flex;
align-items: center;
transition: transform 0.3s ease;
align-items: center; /* Выравнивание по центру */
text-decoration: none; /* Без подчеркивания */
}
.logo-link:hover {
transform: translateY(-2px);
}
/* Стили для поиска */
.search-container {
.navigation ul { /* Навигационное меню */
display: flex;
flex-grow: 1;
max-width: 600px;
gap: 12px;
gap: 18px; /* Расстояние между пунктами */
list-style: none; /* Убрать маркеры */
margin: 0;
padding: 0;
}
.search-input {
flex: 1;
padding: 12px 25px;
border: 1px solid #e0e0e0;
border-radius: 30px;
background: white;
.navigation a { /* Ссылки меню */
color: #333;
text-decoration: none; /* Без подчеркивания */
font-size: 16px;
transition: all 0.3s;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
font-weight: 500; /* Насыщенность */
padding: 8px 14px;
border-radius: 6px;
transition: background 0.2s, color 0.2s; /* Анимация */
}
.search-input:focus {
border-color: #6C63FF;
box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.1);
outline: none;
.navigation a:hover { /* При наведении */
background: #f3f2ff;
color: #6C63FF;
}
.search-input::placeholder {
.search-container { /* Поисковая строка */
display: flex;
align-items: center;
max-width: 260px;
width: 100%;
margin-left: 30px;
background: #f6f6fa;
border-radius: 22px;
box-shadow: 0 1px 4px rgba(108,99,255,0.05); /* Тень */
padding: 2px 4px;
gap: 6px;
}
.search-input { /* Поле ввода */
flex: 1; /* Заполнение пространства */
padding: 8px 14px;
border: none;
border-radius: 18px;
background: transparent; /* Прозрачный фон */
color: #333;
font-size: 15px;
outline: none; /* Без обводки */
}
.search-input::placeholder { /* Плейсхолдер */
color: #aaa;
}
.search-btn {
padding: 0 30px;
background: #ffffff;
color: rgb(0, 0, 0);
border: none;
border-radius: 30px;
cursor: pointer;
font-size: 16px;
.search-btn { /* Кнопка поиска */
padding: 7px 18px;
background: #fff;
color: #6C63FF;
border: 1px solid #6C63FF;
border-radius: 18px;
font-size: 15px;
font-weight: 600;
transition: all 0.3s;
box-shadow: 0 2px 10px rgba(108, 99, 255, 0.2);
cursor: pointer; /* Курсор-указатель */
transition: background 0.2s, color 0.2s; /* Анимация */
}
.search-btn:hover {
background: #e2e2e2;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(108, 99, 255, 0.3);
.search-btn:hover { /* При наведении */
background: #6C63FF;
color: #fff;
}
/* Стили для кнопок авторизации */
.auth-buttons {
.auth-buttons { /* Кнопки авторизации */
display: flex;
gap: 15px;
gap: 10px;
align-items: center;
margin-left: 30px;
}
.auth-link {
.auth-link { /* Ссылка авторизации */
color: #555;
text-decoration: none;
padding: 10px 20px;
padding: 8px 16px;
border-radius: 8px;
transition: all 0.3s;
transition: background 0.2s, color 0.2s; /* Анимация */
font-weight: 500;
}
.auth-link:hover {
.auth-link:hover { /* При наведении */
color: #6C63FF;
background: rgba(108, 99, 255, 0.05);
background: rgba(108, 99, 255, 0.07);
}
.auth-link.register {
.auth-link.register { /* Кнопка регистрации */
background: #6C63FF;
color: white;
box-shadow: 0 2px 10px rgba(108, 99, 255, 0.2);
box-shadow: 0 2px 10px rgba(108, 99, 255, 0.08); /* Тень */
}
.auth-link.register:hover {
.auth-link.register:hover { /* При наведении */
background: #5a52d4;
box-shadow: 0 4px 12px rgba(108, 99, 255, 0.3);
color: #fff;
}
@media (max-width: 1000px) { /* Адаптивность */
.header-content {
flex-wrap: wrap; /* Перенос строк */
gap: 10px;
}
.navigation, .search-container, .auth-buttons {
margin-left: 0;
}
.navigation ul { /* Меню */
flex-wrap: wrap; /* Перенос пунктов */
gap: 10px;
}
.search-container { /* Поиск */
max-width: 100%; /* Полная ширина */
}
}

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom';
import Logo from './Logo'; // Убедитесь в правильности пути
import Logo from './Logo';
import Navigation from './Navigation';
import './Header.css';
function Header() {
@@ -11,10 +12,12 @@ function Header() {
<Logo />
</Link>
<Navigation />
<div className="search-container">
<input
type="text"
placeholder="Поиск продуктов..."
placeholder="Поиск..."
className="search-input"
/>
<button className="search-btn">Поиск</button>

View File

@@ -1,53 +1,22 @@
.logo {
display: flex;
align-items: center;
gap: 15px;
transition: all 0.3s ease;
}
.logo:hover {
transform: translateY(-2px);
gap: 12px;
}
.logo-image {
height: 40px;
width: auto;
transition: transform 0.3s ease;
}
.logo-image:hover {
transform: scale(1.05) rotate(-5deg);
}
.logo-text {
font-size: 22px;
color: #333; /* Изменили на тёмный цвет */
color: #333;
font-weight: 600;
margin: 0;
letter-spacing: 0.5px;
position: relative;
}
.logo-text::after {
content: "";
position: absolute;
bottom: -5px;
left: 0;
width: 0;
height: 2px;
background: #6C63FF;
transition: width 0.3s ease;
}
.logo:hover .logo-text::after {
width: 100%;
letter-spacing: 0.5px; /* Доб 0.5px между символами */
}
.logo-text span {
color: #6C63FF;
transition: color 0.3s ease;
}
.logo:hover .logo-text span {
color: #5a52d4;
}

View File

@@ -4,11 +4,15 @@ function Navigation() {
return (
<nav className="navigation">
<ul>
<li><a href="/">Помощь</a></li>
<li><a href="/contacts">Контакты</a></li>
<li><a href="/skin-types">Типы кожи</a></li>
<li><a href="/reviews">Отзывы</a></li>
<li><a href="/auth">Вход/Регистрация</a></li>
<li>
<a href="#products">Товары</a>
</li>
<li>
<a href="#reviews">Отзывы</a>
</li>
<li>
<a href="#quiz">Опросник</a>
</li>
</ul>
</nav>
);

View File

@@ -1,77 +1,76 @@
.popular-products {
.popular-products { /* Контейнер секции */
max-width: 1200px;
margin: 40px auto;
margin: 40px auto; /* Центрирование */
padding: 0 20px;
}
}
.popular-products h2 {
.popular-products h2 { /* Заголовок */
text-align: center;
margin-bottom: 40px;
font-size: 28px;
}
}
.products-grid {
.products-grid { /* Сетка товаров */
display: grid;
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
gap: 30px;
}
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); /* Автоподбор колонок */
gap: 30px; /* Расстояние между карточками */
}
.product-card {
.product-card { /* Карточка товара */
border: 1px solid #eee;
border-radius: 12px;
overflow: hidden;
transition: transform 0.3s;
}
overflow: hidden; /* Скрытие выходящего контента */
transition: transform 0.3s; /* Анимация */
}
.product-card:hover {
transform: translateY(-5px);
}
.product-card:hover { /* Эффект при наведении */
transform: translateY(-5px); /* Сдвиг вверх */
}
.product-image {
.product-image { /* Изображение товара */
width: 100%;
height: 200px;
object-fit: cover;
}
object-fit: cover; /* Обрезка изображения */
}
.product-card h3 {
.product-card h3 { /* Название товара */
padding: 15px 15px 0;
margin: 0;
font-size: 18px;
}
}
.product-card p {
.product-card p { /* Описание товара */
padding: 10px 15px;
color: #666;
font-size: 14px;
margin: 0;
}
}
.product-footer {
.product-footer { /* Подвал карточки */
padding: 15px;
display: flex;
justify-content: space-between;
justify-content: space-between; /* Распределение пространства */
align-items: center;
}
}
.price {
.price {
font-weight: bold;
color: #333;
}
}
.details-btn {
.details-btn { /* Кнопка "Подробнее" */
background: #f8f9fa;
border: 1px solid #ddd;
padding: 8px 15px;
border-radius: 20px;
cursor: pointer;
cursor: pointer; /* Курсор-указатель */
transition: all 0.3s;
opacity: 0; /* Скрываем кнопку по умолчанию */
}
opacity: 0;
}
.product-card:hover .details-btn {
.product-card:hover .details-btn { /* Кнопка при наведении */
background: #6C63FF;
color: white;
border-color: #6C63FF;
opacity: 1; /* Показываем кнопку при наведении */
}
opacity: 1; /* Показываем кнопку */
}

View File

@@ -4,6 +4,8 @@ import product1 from './img/product1.jpg';
import product2 from './img/product2.jpg';
import product3 from './img/product3.jpg';
import product4 from './img/product4.jpg';
import product5 from './img/product5.png';
import product6 from './img/product6.png';
const products = [
{
@@ -33,15 +35,31 @@ const products = [
image: product4,
description: "Восстановление во время сна",
price: "1 950 ₽"
},
{
id: 5,
name: "Солнцезащитный крем SPF 50",
image: product5,
description: "Защита от UVA/UVB лучей",
price: "1 250 ₽"
},
{
id: 6,
name: "Пилинг для лица",
image: product6,
description: "Мягкое отшелушивание кожи",
price: "1 780 ₽"
}
];
function PopularProducts() {
function PopularProducts() { /* берет массив products, преобразует каждый товар
в HTML-карточку и выводит их в адаптивную сетку.*/
return (
<section className="popular-products">
<section className="popular-products"> {/* Основная секция */}
<h2>Популярные косметические средства</h2>
<div className="products-grid">
{products.map(product => (
<div className="products-grid"> {/* Контейнер сетки */}
{products.map(product => ( /* Перебираем массив товаров */
<div key={product.id} className="product-card">
<img
src={product.image}

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

View File

@@ -2,47 +2,46 @@
max-width: 800px;
margin: 40px auto;
padding: 0 20px;
}
}
.reviews-section h2 {
.reviews-section h2 {
text-align: center;
margin-bottom: 32px;
font-size: 26px;
}
}
.reviews-list {
.reviews-list {
display: flex;
flex-direction: column;
gap: 24px;
}
}
.review-card {
.review-card {
border: 1px solid #eee;
border-radius: 10px;
background: #fafbfc;
padding: 20px 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
}
}
.review-text {
.review-text {
font-size: 16px;
margin-bottom: 12px;
color: #222;
}
}
.review-meta {
.review-info {
display: flex;
align-items: center;
gap: 16px;
}
}
.review-author {
.review-author {
font-weight: bold;
color: #6C63FF;
}
}
.star-rating {
.star-rating {
color: #FFD700;
font-size: 18px;
}
}

View File

@@ -1,6 +1,7 @@
import React from 'react';
import './Reviews.css';
// Массив с отзывами пользователей
const reviews = [
{
id: 1,
@@ -22,6 +23,7 @@ const reviews = [
}
];
// Компонент для отображения звездного рейтинга
function StarRating({ rating }) {
return (
<span className="star-rating">
@@ -31,15 +33,18 @@ function StarRating({ rating }) {
);
}
// Основной компонент отзывов
function Reviews() {
return (
<section className="reviews-section">
<h2>Отзывы пользователей</h2>
<div className="reviews-list">
<div className="reviews-list">{/* Перебираем массив отзывов */}
{reviews.map(review => (
// Карточка отзыва с уникальным ключом
<div key={review.id} className="review-card">
<p className="review-text">"{review.text}"</p>
<div className="review-meta">
<div className="review-info">
<span className="review-author">{review.author}</span>
<StarRating rating={review.rating} />
</div>
@@ -50,4 +55,5 @@ function Reviews() {
);
}
export default Reviews;

View File

@@ -1,15 +1,17 @@
import React, { useState, useEffect } from 'react';
function Question({ question, answer, onAnswer, isLast, onNext }) {
// Выбранные ответы
const [selected, setSelected] = useState(() => {
return question.type === 'multiple'
? Array.isArray(answer) ? [...answer] : []
: answer || '';
});
// Текст для поля "Другое"
const [otherText, setOtherText] = useState('');
// Инициализация otherText
// Инициализация текста для "Другое"
useEffect(() => {
if (question.type === 'multiple' && Array.isArray(answer)) {
const otherItem = answer.find(item => item?.other);
@@ -17,7 +19,7 @@ function Question({ question, answer, onAnswer, isLast, onNext }) {
}
}, [answer, question.type]);
// Синхронизация с пропсом answer
// Синхронизация с внешними изменениями
useEffect(() => {
if (question.type === 'multiple') {
setSelected(Array.isArray(answer) ? answer : []);
@@ -26,6 +28,7 @@ function Question({ question, answer, onAnswer, isLast, onNext }) {
}
}, [answer, question.type]);
// Обработчик изменения выбора
const handleChange = (option, isOther = false) => {
if (question.type === 'single') {
setSelected(option);
@@ -35,6 +38,7 @@ function Question({ question, answer, onAnswer, isLast, onNext }) {
let newSelected = Array.isArray(selected) ? [...selected] : [];
// Логика для чекбокса "Другое"
if (isOther) {
const existingOther = newSelected.findIndex(item => item?.other);
if (existingOther >= 0) {
@@ -56,6 +60,7 @@ function Question({ question, answer, onAnswer, isLast, onNext }) {
onAnswer(question.id, newSelected);
};
// Обработчик изменения текста для "Другое"
const handleOtherTextChange = (e) => {
const text = e.target.value;
setOtherText(text);
@@ -66,11 +71,13 @@ function Question({ question, answer, onAnswer, isLast, onNext }) {
onAnswer(question.id, newSelected);
};
// Отправка формы
const handleSubmit = (e) => {
e.preventDefault();
onNext();
};
// Нормализация выбранных значений
const safeSelected = question.type === 'multiple'
? (Array.isArray(selected) ? selected : [])
: selected;

View File

@@ -3,6 +3,7 @@ import Question from './Question';
import Result from './Result';
import './SkinTypeQuiz.css';
// Список всех вопросов опросника
const questionsMock = [
{
id: 1,
@@ -49,15 +50,25 @@ const questionsMock = [
];
function SkinTypeQuiz() {
// Текущий индекс вопроса
const [currentIndex, setCurrentIndex] = useState(0);
// Все ответы пользователя
const [answers, setAnswers] = useState({});
// Флаг показа результатов
const [showResult, setShowResult] = useState(false);
// Рекомендации по уходу
const [recommendations, setRecommendations] = useState('');
const questions = questionsMock;
// Обработчик выбора ответа
const handleAnswer = (questionId, answer) => {
const currentQuestion = questions.find(q => q.id === questionId);
// Для чекбоксов: преобразуем в массив если необходимо
const safeAnswer = currentQuestion.type === 'multiple'
? Array.isArray(answer) ? answer : []
: answer;
@@ -65,6 +76,7 @@ function SkinTypeQuiz() {
setAnswers(prev => ({ ...prev, [questionId]: safeAnswer }));
};
// Переход к следующему вопросу или показ результатов
const handleNext = () => {
if (currentIndex + 1 < questions.length) {
setCurrentIndex(currentIndex + 1);
@@ -75,10 +87,12 @@ function SkinTypeQuiz() {
}
};
// Показываем результаты если опрос завершен
if (showResult) {
return <Result recommendations={recommendations} />;
}
// Рендер текущего вопроса
return (
<Question
question={questions[currentIndex]}
@@ -90,8 +104,10 @@ function SkinTypeQuiz() {
);
}
// Генерация рекомендаций на основе ответов
function calculateRecommendations(answers) {
const question3Answer = answers[3]; // ID=3 — вопрос про тип кожи
const question3Answer = answers[3]; // Ответ на вопрос о типе кожи
if (question3Answer === 'Сухая и тянущаяся') {
return 'Рекомендуется использовать увлажняющие средства и средства для чувствительной кожи.';
}

View File

@@ -1,34 +1,42 @@
import React, { useState } from 'react';
import React, { useState } from 'react'; // Хук состояния
import { useNavigate } from 'react-router-dom';
import './AuthForms.css';
function Login() {
// Состояние для хранения данных формы
const [formData, setFormData] = useState({
email: '',
password: ''
});
// Хук для программной навигации
const navigate = useNavigate();
// Обработчик изменений в полях ввода
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
...prev, // Сохранение предыдущих значений
[name]: value // Динамическое обновление поля
}));
};
// Обработчик отправки формы
const handleSubmit = (e) => {
e.preventDefault();
console.log('Вход:', formData);
navigate('/');
e.preventDefault(); // Отмена стандартного поведения
console.log('Вход:', formData); // Логирование данных (в реальном приложении - запрос к API)
navigate('/'); // Перенаправление на главную страницу
};
return (
// Основной контейнер страницы
<div className="auth-page">
<div className="auth-container">
<h2>Вход</h2>
{/* Форма с обработчиком отправки */}
<form onSubmit={handleSubmit}>
{/* Группа полей для email */}
<div className="form-group">
<label>Email</label>
<input
@@ -36,10 +44,11 @@ function Login() {
name="email"
value={formData.email}
onChange={handleChange}
required
required // Обязательное поле
/>
</div>
{/* Группа полей для пароля */}
<div className="form-group">
<label>Пароль</label>
<input
@@ -51,11 +60,13 @@ function Login() {
/>
</div>
{/* Кнопка отправки формы */}
<button type="submit" className="auth-btn">
Войти
</button>
</form>
{/* Ссылка для перехода к регистрации */}
<div className="auth-switch">
<span>Нет аккаунта? <a href="/register">Создать</a></span>
</div>

View File

@@ -1,27 +1,35 @@
import React, { useState } from 'react';
import React, { useState } from 'react'; //для управления состоянием формы
import { useNavigate } from 'react-router-dom';
import './AuthForms.css';
function Register() {
// Состояние для хранения данных формы
const [formData, setFormData] = useState({
name: '',
email: '',
password: ''
});
// Хук для навигации между страницами
const navigate = useNavigate();
// Обработчик изменений в полях ввода
const handleChange = (e) => {
const { name, value } = e.target;
const { name, value } = e.target; // Извлекаем имя поля и значение
setFormData(prev => ({
...prev,
[name]: value
...prev, // Копируем предыдущее состояние
[name]: value // Обновляем конкретное поле
}));
};
// Обработчик отправки формы
const handleSubmit = (e) => {
e.preventDefault();
e.preventDefault(); // Отменяем стандартное поведение формы
// В реальном приложении здесь будет запрос к API
console.log('Регистрация:', formData);
// Перенаправляем на главную страницу
navigate('/');
};
@@ -29,18 +37,21 @@ function Register() {
<div className="auth-page">
<div className="auth-container">
<h2>Регистрация</h2>
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit}> {/* Форма с обработчиком отправки */}
{/* Группа полей для имени */}
<div className="form-group">
<label>Имя</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
required
value={formData.name} // Привязка к состоянию
onChange={handleChange} // Обработчик изменений
required // Обязательное поле
/>
</div>
{/* Группа полей для email */}
<div className="form-group">
<label>Email</label>
<input
@@ -52,6 +63,7 @@ function Register() {
/>
</div>
{/* Группа полей для пароля */}
<div className="form-group">
<label>Пароль</label>
<input
@@ -63,11 +75,13 @@ function Register() {
/>
</div>
{/* Кнопка отправки формы */}
<button type="submit" className="auth-btn">
Зарегистрироваться
</button>
</form>
{/* Ссылка для перехода к авторизации */}
<div className="auth-switch">
<span>Уже есть аккаунт? <a href="/login">Войти</a></span>
</div>