почему то страница карта мира не прогружается и остается просто пустой если что я правильно указал ключ API

import React from 'react';
import './Button.scss';

const Button = ({ children, onClick, variant = 'primary', className = '', ...props }) => {
  return (
    <button className={`btn btn-${variant} ${className}`} onClick={onClick} {...props}>
      {children}
    </button>
  );
};

export default Button;




@import '../styles/variables.scss'; // Обновите путь, если структура папок отличается

.btn {
  font-family: inherit;
  font-size: 1rem;
  font-weight: bold;
  border: none;
  border-radius: 0.25rem;
  cursor: pointer;
  padding: 0.75rem 1.5rem;
  transition: background-color 0.3s ease, transform 0.2s ease;

  &-primary {
    background-color: $primary-color;
    color: #ffffff;

    &:hover {
      background-color: $primary-hover;
      transform: scale(1.05);
    }

    &:active {
      transform: scale(1);
      background-color: darken($primary-hover, 5%);
    }
  }

  &-secondary {
    background-color: $secondary-color;
    color: #ffffff;

    &:hover {
      background-color: $secondary-hover;
      transform: scale(1.05);
    }

    &:active {
      transform: scale(1);
      background-color: darken($secondary-hover, 5%);
    }
  }

  &-danger {
    background-color: $danger-color;
    color: #ffffff;

    &:hover {
      background-color: $danger-hover;
      transform: scale(1.05);
    }

    &:active {
      transform: scale(1);
      background-color: darken($danger-hover, 5%);
    }
  }

  &-outline {
    background-color: transparent;
    border: 2px solid $primary-color;
    color: $primary-color;

    &:hover {
      background-color: $primary-color;
      color: #ffffff;
      transform: scale(1.05);
    }

    &:active {
      transform: scale(1);
      background-color: darken($primary-color, 5%);
      color: #ffffff;
    }
  }

  &.wider-button {
    width: 100%;
  }
}






import React from 'react';
import './Card.scss';

const Card = ({ title, description, onClick, children }) => {
  return (
    <div className="card" onClick={onClick}>
      <div className="card-content">
        {children && <div className="card-icon">{children}</div>}
        <h2 className="card-title">{title}</h2>
        <p className="card-description">{description}</p>
      </div>
    </div>
  );
};

export default Card;





@import '../styles/variables';

.card {
  background-color: $light-background;
  border-radius: 0.5rem;
  box-shadow: 0 4px 6px $shadow-color, 0 2px 4px rgba(0, 0, 0, 0.06);
  padding: 1.5rem;
  text-align: center;
  transition: transform 0.3s ease;
  cursor: pointer;
  width: 250px;

  &:hover {
    transform: translateY(-5px);
  }

  .card-content {
    display: flex;
    flex-direction: column;
    align-items: center;
  }

  .card-icon {
    font-size: 2rem;
    margin-bottom: 1rem;
  }

  .card-title {
    font-size: 1.5rem;
    color: $heading-color;
    margin-bottom: 0.5rem;
  }

  .card-description {
    color: $text-color;
    font-size: 1rem;
  }
}







import React, { useState, useEffect } from "react";
import "./FlagQuiz.scss";
import countryData from '../flags/countries.json'; // Импортируем данные

const FlagQuiz = () => {
  const [countries, setCountries] = useState([]);
  const [currentQuestion, setCurrentQuestion] = useState(null);
  const [score, setScore] = useState(0);
  const [isCorrect, setIsCorrect] = useState(null);

  // Сортируем данные по русским названиям
  useEffect(() => {
    const sortedCountries = countryData.sort((a, b) => a.name_ru.localeCompare(b.name_ru));
    setCountries(sortedCountries); // Сортированные по русскому названию
    setRandomQuestion(sortedCountries);
  }, []);

  // Функция для загрузки флага через API
  const fetchFlag = async (countryName) => {
    const response = await fetch(`https://restcountries.com/v3.1/name/${countryName}`);
    const data = await response.json();
    if (data && data[0] && data[0].flags) {
      return data[0].flags.svg; // Возвращаем URL флага
    }
    return null;
  };

  // Выбираем случайную страну и загружаем флаг через API
  const setRandomQuestion = async (countries) => {
    const randomCountry = countries[Math.floor(Math.random() * countries.length)];
    const flagUrl = await fetchFlag(randomCountry.name_en); // Загружаем флаг по английскому названию
    const options = generateOptions(randomCountry, countries);

    setCurrentQuestion({
      flag: flagUrl,
      correct: randomCountry.aliases, // Массив вариантов названий
      options,
      continent: randomCountry.continent // Континент
    });
  };

  // Генерируем варианты ответов (3 неверных и 1 правильный)
  const generateOptions = (correctCountry, countries) => {
    const options = [correctCountry.name_ru]; // Добавляем русское название правильного ответа
    while (options.length < 4) {
      const randomOption = countries[Math.floor(Math.random() * countries.length)].name_ru;
      if (!options.includes(randomOption)) {
        options.push(randomOption);
      }
    }
    return options.sort(() => Math.random() - 0.5); // Перемешиваем варианты
  };

  // Обработка ответа пользователя
  const handleAnswer = (answer) => {
    if (currentQuestion.correct.includes(answer)) {
      setScore(score + 1);
      setIsCorrect(true);
    } else {
      setIsCorrect(false);
    }
    setTimeout(() => {
      setIsCorrect(null);
      setRandomQuestion(countries); // Переход к следующему вопросу
    }, 2000);
  };

  return (
    <div className="flag-quiz">
      <h2>Угадай страну по флагу</h2>
      {currentQuestion && (
        <>
          <img src={currentQuestion.flag} alt="Flag" className="flag-image" />
          <p>Континент: {currentQuestion.continent}</p> {/* Отображаем континент */}
          <div className="options">
            {currentQuestion.options.map((option) => (
              <button key={option} onClick={() => handleAnswer(option)} className="option-button">
                {option}
              </button>
            ))}
          </div>
          {isCorrect === true && <p className="correct">Правильно!</p>}
          {isCorrect === false && <p className="incorrect">Неправильно! Это {currentQuestion.correct.join(", ")}.</p>}
          <p className="score">Ваш счёт: {score}</p>
        </>
      )}
    </div>
  );
};

export default FlagQuiz;






@import '../styles/variables';

.flag-quiz {
  background-color: $light-background;
  border-radius: 0.75rem;
  padding: 2.5rem;
  box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
  max-width: 500px;
  margin: 0 auto;
  text-align: center;
  font-family: $font-family;

  h2 {
    font-size: 1.5rem;
    color: $heading-color;
    margin-bottom: 1.875rem;
  }

  .flag-image {
    width: 200px;
    border: 3px solid lighten($background-color, 10%);
    border-radius: 0.75rem;
    margin-bottom: 1.25rem;
    @include transition(transform 0.3s ease);

    &:hover {
      transform: scale(1.05);
    }
  }

  .continent {
    font-size: 1rem;
    color: $text-color;
    margin-bottom: 1.25rem;
  }

  .options {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    justify-content: center;

    .option-button {
      @include button-style($primary-color, #ffffff, $primary-hover);
      flex: 1 1 45%;
      font-size: 1rem;
    }
  }

  .correct {
    color: $secondary-color;
    font-size: 1.125rem;
    font-weight: bold;
    margin-top: 1.25rem;
  }

  .incorrect {
    color: $danger-color;
    font-size: 1.125rem;
    font-weight: bold;
    margin-top: 1.25rem;
  }

  .score {
    font-size: 1.125rem;
    color: $text-color;
    margin-top: 1.25rem;
    font-weight: bold;
  }
}





import React, { useState, useRef, useEffect } from "react";
import { Link, useNavigate } from 'react-router-dom';
import './Header.scss';
import Avatar from '../User/Avatar';
import Button from '../Button/Button';

const Header = ({ user, onLoginClick }) => {
  const [showWorldSubmenu, setShowWorldSubmenu] = useState(false);
  const [showKyrgyzstanSubmenu, setShowKyrgyzstanSubmenu] = useState(false);
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
  const [openSubmenu, setOpenSubmenu] = useState(null); // для контроля открытого подменю в мобильном меню
  const headerRef = useRef(null);
  const navigate = useNavigate();

  // Закрытие мобильного меню при клике вне его
  useEffect(() => {
    const handleClickOutside = (event) => {
      if (headerRef.current && !headerRef.current.contains(event.target)) {
        setIsMobileMenuOpen(false);
        setOpenSubmenu(null);
      }
    };
    if (isMobileMenuOpen) {
      document.addEventListener("mousedown", handleClickOutside);
    }
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [isMobileMenuOpen]);

  const toggleWorldSubmenu = () => {
    if (window.innerWidth >= 768) { // Предполагая, что 768px — это брейкпоинт
      setShowWorldSubmenu(!showWorldSubmenu);
    }
  };
  
  const toggleKyrgyzstanSubmenu = () => {
    if (window.innerWidth >= 768) {
      setShowKyrgyzstanSubmenu(!showKyrgyzstanSubmenu);
    }
  };

  const handleMenuToggle = () => {
    setIsMobileMenuOpen(!isMobileMenuOpen);
  };

  const handleSubmenuToggle = (menu) => {
    setOpenSubmenu(prev => (prev === menu ? null : menu));
  };

  const handleProfileClick = () => {
    navigate('/user'); // Переход на страницу профиля пользователя
    setIsMobileMenuOpen(false);
  };

  return (
    <header ref={headerRef} className="main-header">
      <div className="header-content">
        {/* Кнопка мобильного меню */}
        <button className="nav-button" onClick={handleMenuToggle}>
          &#9776; {/* Иконка гамбургера */}
        </button>

        {/* Логотип */}
        <Link to="/" className="logo">
          Название сайта
        </Link>

        {/* Навигационные ссылки для десктопа */}
        <nav className={`nav-links ${isMobileMenuOpen ? 'mobile-open' : ''}`}>
          {/* Навигационные элементы для десктопа и мобильного */}
          <div 
            className="nav-item desktop-nav-item" 
            onMouseEnter={() => window.innerWidth >= 768 && setShowWorldSubmenu(true)} 
            onMouseLeave={() => window.innerWidth >= 768 && setShowWorldSubmenu(false)}
          >
            <Link to="#" onClick={() => window.innerWidth < 768 && handleSubmenuToggle('world')}>
              Мир
              <span className="arrow-down">&#9662;</span>
            </Link>
            {/* Подменю для 'Мир' */}
            <div className={`submenu ${showWorldSubmenu ? 'visible' : ''} ${openSubmenu === 'world' ? 'active' : ''}`}>
              <Link to="/map">Карта мира</Link>
              <Link to="/tests">Тесты</Link>
              {/* Добавьте другие ссылки для "Мир", если необходимо */}
            </div>
          </div>
          
          <div 
            className="nav-item desktop-nav-item" 
            onMouseEnter={() => window.innerWidth >= 768 && setShowKyrgyzstanSubmenu(true)} 
            onMouseLeave={() => window.innerWidth >= 768 && setShowKyrgyzstanSubmenu(false)}
          >
            <Link to="#" onClick={() => window.innerWidth < 768 && handleSubmenuToggle('kyrgyzstan')}>
              Кыргызстан
              <span className="arrow-down">&#9662;</span>
            </Link>
            {/* Подменю для 'Кыргызстан' */}
            <div className={`submenu ${showKyrgyzstanSubmenu ? 'visible' : ''} ${openSubmenu === 'kyrgyzstan' ? 'active' : ''}`}>
              <Link to="/kyrgyzstan/map">Карта Кыргызстана</Link>
              <Link to="/kyrgyzstan/tests">Тесты по Кыргызстану</Link>
              {/* Добавьте другие ссылки для "Кыргызстан", если необходимо */}
            </div>
          </div>
        </nav>

        {/* Информация о пользователе или кнопка входа */}
        <div className="user-section">
          {user ? (
            <div className="user-info">
              <Link to="/user" className="user-link">
                <Avatar src={`/avatars/${user.avatarId}.png`} alt={user.username} className="user-avatar" />
                <div className="user-details">
                  <span className="username">{user.username}</span>
                  <span className="user-nickname">{`@${user.username.toLowerCase()}`}</span>
                </div>
              </Link>
            </div>
          ) : (
            <Button variant="primary" onClick={onLoginClick} className="login-button">Вход</Button>
          )}
        </div>
      </div>

      {/* Мобильное навигационное меню */}
      {isMobileMenuOpen && (
        <nav className="mobile-nav">
          <div className="mobile-nav-item">
            <button onClick={() => handleSubmenuToggle('world')} className="mobile-nav-link">
              Мир
              <span className="arrow-down">&#9662;</span>
            </button>
            {openSubmenu === 'world' && (
              <div className="mobile-submenu">
                <Link to="/map" onClick={() => setIsMobileMenuOpen(false)}>Карта мира</Link>
                <Link to="/tests" onClick={() => setIsMobileMenuOpen(false)}>Тесты</Link>
                {/* Добавьте другие ссылки для "Мир", если необходимо */}
              </div>
            )}
          </div>
          
          <div className="mobile-nav-item">
            <button onClick={() => handleSubmenuToggle('kyrgyzstan')} className="mobile-nav-link">
              Кыргызстан
              <span className="arrow-down">&#9662;</span>
            </button>
            {openSubmenu === 'kyrgyzstan' && (
              <div className="mobile-submenu">
                <Link to="/kyrgyzstan/map" onClick={() => setIsMobileMenuOpen(false)}>Карта Кыргызстана</Link>
                <Link to="/kyrgyzstan/tests" onClick={() => setIsMobileMenuOpen(false)}>Тесты по Кыргызстану</Link>
                {/* Добавьте другие ссылки для "Кыргызстан", если необходимо */}
              </div>
            )}
          </div>
        </nav>
      )}
    </header>
  );
};

export default Header;






@import '../styles/variables';

.main-header {
  background-color: $light-background;
  box-shadow: 0 1px 3px $shadow-color, 0 1px 2px rgba(0, 0, 0, 0.06);
  position: relative;

  .header-content {
    max-width: 1280px;
    margin: 0 auto;
    padding: $spacer;
    display: flex;
    justify-content: space-between;
    align-items: center;
    position: relative;
    z-index: 1000;

    .nav-button {
      display: none; // Скрыто на десктопе
      background: none;
      border: none;
      font-size: 1.5rem;
      cursor: pointer;
      color: $text-color;

      @media (max-width: 767px) {
        display: block; // Показывается на мобильных устройствах
      }
    }

    .logo {
      font-size: 1.5rem;
      font-weight: bold;
      color: $primary-color;
      text-decoration: none;
      @include transition(color 0.3s ease);

      &:hover {
        color: $primary-hover;
      }

      @media (max-width: 767px) {
        position: absolute;
        left: 50%;
        transform: translateX(-50%);
      }
    }

    .nav-links {
      display: flex;
      gap: 1rem;

      @media (max-width: 767px) {
        display: none; // Скрыто на мобильных устройствах, управляется через мобильное меню
      }

      .nav-item {
        position: relative;
        font-size: 1rem;

        a {
          color: $text-color;
          text-decoration: none;
          font-size: 1rem;
          padding: 0.5rem 0;
          @include transition(color 0.3s ease);

          &:hover {
            color: $primary-color;
          }
        }

        .arrow-down {
          margin-left: 0.25rem;
          font-size: 0.75rem;
        }

        .submenu {
          position: absolute;
          top: 100%;
          left: 0;
          background-color: $light-background;
          box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
          border-radius: 0.25rem;
          padding: $spacer;
          display: none;
          flex-direction: column;

          a {
            padding: 0.5rem 1rem;
            color: $text-color;
            @include transition(background-color 0.3s ease, color 0.3s ease);

            &:hover {
              background-color: lighten($background-color, 10%);
            }
          }
        }

        &.desktop-nav-item:hover .submenu {
          display: flex;
        }

        &.desktop-nav-item .submenu.visible {
          display: flex;
        }
      }
    }

    .user-section {
      display: flex;
      align-items: center;

      .user-info {
        display: flex;
        align-items: center;

        .user-link {
          display: flex;
          align-items: center;
          color: $primary-color;
          text-decoration: none;
          font-weight: bold;
          @include transition(color 0.3s ease);

          &:hover {
            color: $primary-hover;
          }

          .user-avatar {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            margin-right: 0.5rem;
            object-fit: cover;
            border: 2px solid $secondary-color;
          }

          .user-details {
            display: flex;
            flex-direction: column;

            .username {
              font-size: 0.875rem;
              color: $heading-color;
            }

            .user-nickname {
              font-size: 0.75rem;
              color: $text-color;
            }
          }
        }
      }

      .login-button {
        @include button-style($primary-color, #ffffff, $primary-hover);
      }
    }
  }

  /* Мобильные навигационные стили */
  .mobile-nav {
    position: absolute;
    top: 100%;
    left: 0;
    width: 100%;
    background-color: $light-background;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    flex-direction: column;
    padding: $spacer;
    display: flex;
    z-index: 999;

    .mobile-nav-item {
      margin-bottom: 1rem;

      .mobile-nav-link {
        display: flex;
        justify-content: space-between;
        align-items: center;
        width: 100%;
        background: none;
        border: none;
        font-size: 1rem;
        font-weight: bold;
        cursor: pointer;
        color: $text-color;
        padding: 0.5rem 0;
        @include transition(color 0.3s ease);

        &:hover {
          color: $primary-color;
        }

        .arrow-down {
          font-size: 0.75rem;
        }
      }

      .mobile-submenu {
        display: flex;
        flex-direction: column;
        gap: 0.5rem;
        padding-left: 1rem;
        margin-top: 0.5rem;

        a {
          color: $text-color;
          text-decoration: none;
          font-size: 0.875rem;

          &:hover {
            color: $primary-color;
          }
        }
      }
    }
  }
}





import React, { useState, useEffect, useRef, useCallback } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import './MapPage.scss';
import Header from '../Header/Header';

mapboxgl.accessToken = '';

const MapPage = ({ user }) => {
  const [is3D, setIs3D] = useState(false);
  const [isPanelOpen, setIsPanelOpen] = useState(false);
  const [searchQuery, setSearchQuery] = useState('');
  const [suggestions, setSuggestions] = useState([]); // Подсказки для поиска
  const [countryInfo, setCountryInfo] = useState(null); // Информация о выбранной стране
  const mapContainer = useRef(null);
  const map = useRef(null);

  // Инициализация карты
  const initializeMap = useCallback(() => {
    if (map.current) return;
    map.current = new mapboxgl.Map({
      container: mapContainer.current,
      style: 'mapbox://styles/mapbox/satellite-streets-v12',
      center: [0, 0],
      zoom: 1,
      projection: is3D ? 'globe' : 'mercator',
    });
    map.current.addControl(new mapboxgl.NavigationControl(), 'top-right');
  }, [is3D]);

  useEffect(() => {
    initializeMap();
    return () => {
      if (map.current) {
        map.current.remove();
        map.current = null;
      }
    };
  }, [initializeMap]);

  // Поиск стран через Geocoding API с автодополнением
  const searchCountry = (query) => {
    fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${query}.json?autocomplete=true&access_token=${mapboxgl.accessToken}`)
      .then((response) => response.json())
      .then((data) => {
        if (data.features && data.features.length > 0) {
          setSuggestions(data.features); // Выводим подсказки
        } else {
          setSuggestions([]);
        }
      })
      .catch((error) => console.error('Error fetching country data:', error));
  };

  // Выбор страны и перемещение на карту
  const selectCountry = (country) => {
    const [longitude, latitude] = country.center;
    const bbox = country.bbox || null; // Используем bounding box для увеличения на объект
  
    if (bbox) {
      map.current.fitBounds(bbox, { padding: 20, maxZoom: 9, duration: 1500 });
    } else {
      map.current.flyTo({
        center: [longitude, latitude],
        zoom: 5,
        essential: true,
      });
    }
  
    setCountryInfo(country);
    setSuggestions([]);
  };

  // Накладывание слоев (добавим все стили Mapbox)
  const addLayer = (layerId) => {
    const layers = {
      'streets': 'mapbox://styles/mapbox/streets-v12',
      'satellite': 'mapbox://styles/mapbox/satellite-v9',
      'light': 'mapbox://styles/mapbox/light-v10',
      'dark': 'mapbox://styles/mapbox/dark-v10',
      'outdoors': 'mapbox://styles/mapbox/outdoors-v12',
      'navigation-day': 'mapbox://styles/mapbox/navigation-day-v1',
      'navigation-night': 'mapbox://styles/mapbox/navigation-night-v1',
      'satellite-streets': 'mapbox://styles/mapbox/satellite-streets-v12',
    };
    if (layers[layerId]) {
      map.current.setStyle(layers[layerId]);
    }
  };

  return (
    <div className="map-page">
      <Header user={user} title="Карта мира" />
      <main>
        <div ref={mapContainer} className={`map-container ${is3D ? 'is-3d' : ''}`} />
        <button onClick={() => setIs3D(!is3D)} className="toggle-view-button">
          {is3D ? '2D Map' : '3D Globe'}
        </button>
        <button onClick={() => setIsPanelOpen(!isPanelOpen)} className="toggle-panel-button">
          {isPanelOpen ? 'Close Panel' : 'Open Panel'}
        </button>
        {isPanelOpen && (
          <div className="control-panel">
            <input
              type="text"
              value={searchQuery}
              onChange={(e) => {
                setSearchQuery(e.target.value);
                searchCountry(e.target.value); // Запрос подсказок по мере ввода
              }}
              placeholder="Введите страну..."
            />
            {suggestions.length > 0 && (
              <ul className="suggestions-list">
                {suggestions.map((suggestion) => (
                  <li key={suggestion.id} onClick={() => selectCountry(suggestion)}>
                    {suggestion.place_name}
                  </li>
                ))}
              </ul>
            )}
            {countryInfo && (
              <div className="country-info">
                <h4>{countryInfo.place_name}</h4>
              </div>
            )}
            <div className="layer-controls">
              <button onClick={() => addLayer('streets')}>Улицы</button>
              <button onClick={() => addLayer('satellite')}>Спутник</button>
              <button onClick={() => addLayer('light')}>Светлая тема</button>
              <button onClick={() => addLayer('dark')}>Темная тема</button>
              <button onClick={() => addLayer('outdoors')}>На улице</button>
              <button onClick={() => addLayer('navigation-day')}>Навигация (день)</button>
              <button onClick={() => addLayer('navigation-night')}>Навигация (ночь)</button>
              <button onClick={() => addLayer('satellite-streets')}>Спутник + Улицы</button>
            </div>
          </div>
        )}
      </main>
    </div>
  );
};

export default MapPage;






@import '../styles/variables.scss';
@import '../Button/Button.scss';

.map-page {
  height: 100vh;
  display: flex;
  flex-direction: column;

  .map-container {
    flex-grow: 1;
    width: 100%;
    transition: background-color 0.5s ease;

    &.is-3d {
      background-color: rgb(11, 11, 25);
    }
  }

  .toggle-view-button,
  .toggle-panel-button {
    @include button-secondary;
  }

  .control-panel {
    background-color: $light-background;
    border: 1px solid #d1d5db;
    border-radius: 0.5rem;
    padding: 1rem;
    box-shadow: 0 4px 6px $shadow-color;
    z-index: 100;

    input[type="text"] {
      width: 100%;
      padding: 0.5rem;
      border: 1px solid #d1d5db;
      border-radius: 0.375rem;
      margin-bottom: 0.5rem;
    }

    .suggestions-list {
      list-style: none;
      max-height: 150px;
      overflow-y: auto;
      margin-bottom: 0.5rem;

      li {
        padding: 0.5rem;
        border-bottom: 1px solid #e5e7eb;
        cursor: pointer;
        @include transition(background-color 0.3s ease);

        &:hover {
          background-color: lighten($background-color, 10%);
        }
      }
    }

    .country-info {
      margin-bottom: 0.5rem;
      font-weight: bold;
      color: $heading-color;
    }

    .layer-controls {
      display: flex;
      flex-wrap: wrap;
      gap: 0.5rem;

      button {
        @include button-primary;
        flex: 1 1 48%;
      }
    }
  }

  .toggle-buttons {
    @include flex-center;
    gap: 1rem;
    padding: $spacer;
    background-color: $light-background;
    box-shadow: 0 -1px 3px $shadow-color;

    .toggle-view-button,
    .toggle-panel-button {
      @include button-secondary;
    }
  }
}






@import 'variables';

// Сброс стилей и базовая настройка
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body, html, #root {
  height: 100%;
  font-family: $font-family;
  background-color: $background-color;
  color: $text-color;
}

a {
  text-decoration: none;
  color: inherit;
}

button {
  font-family: inherit;
}

h1, h2, h3, h4, h5, h6 {
  color: $heading-color;
}

p {
  line-height: 1.6;
}




// Цветовая палитра
$primary-color: #1E3A8A;          // Глубокий синий
$primary-hover: #2563EB;          // Вариант основного цвета при наведении
$secondary-color: #14B8A6;        // Бирюзовый
$secondary-hover: #0D9488;        // Вариант вторичного цвета при наведении
$accent-color: #F97316;           // Оранжевый
$accent-hover: #EA580C;           // Вариант акцентного цвета при наведении

// Цвета состояния
$success-color: #10B981;          // Зеленый
$danger-color: #EF4444;           // Красный
$danger-hover: #DC2626;           // Вариант красного при наведении
$warning-color: #F59E0B;          // Янтарный
$info-color: #0EA5E9;             // Небесно-голубой

// Фоновые цвета
$background-color: #F9FAFB;       // Светло-серый
$light-background: #FFFFFF;       // Белый

// Цвета текста
$text-color: #374151;             // Темно-серый
$heading-color: #1F2937;          // Глубокий серый

// Тень
$shadow-color: rgba(0, 0, 0, 0.1);

// Шрифты
$font-family: 'Arial', sans-serif;

// Размеры шрифтов
$font-size-base: 16px;
$heading-font-size: 1.5rem;
$subheading-font-size: 1.25rem;

// Отступы
$spacer: 1rem;

// Миксины
@mixin transition($properties...) {
  transition: $properties;
}

@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

@mixin button-style($bg-color, $text-color, $hover-bg-color) {
  background-color: $bg-color;
  color: $text-color;
  border: none;
  border-radius: 0.25rem;
  padding: 0.5rem 1rem;
  cursor: pointer;
  font-weight: bold;
  font-size: 1rem;
  @include transition(background-color 0.3s ease, transform 0.2s);
  
  &:hover {
    background-color: $hover-bg-color;
    transform: scale(1.05);
  }
  
  &:active {
    transform: scale(1);
  }
  
  &.wider-button {
    width: 100%;
  }
}

// Новые миксины для кнопок
@mixin button-primary {
  @include button-style($primary-color, #ffffff, $primary-hover);
}

@mixin button-secondary {
  @include button-style($secondary-color, #ffffff, $secondary-hover);
}

@mixin button-danger {
  @include button-style($danger-color, #ffffff, $danger-hover);
}






import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import './CreateTestPage.scss';
import Header from '../Header/Header';

const CreateTestPage = ({ user }) => {
  const navigate = useNavigate();
  const [userTests, setUserTests] = useState([]);
  const [newTest, setNewTest] = useState({
    title: '',
    questions: [{
      question: '',
      answers: ['', '', '', ''],
      correctAnswer: null
    }]
  });
  const [isSubmitting, setIsSubmitting] = useState(false);

  useEffect(() => {
    fetchUserTests();
  }, []);

  const fetchUserTests = async () => {
    try {
      const response = await fetch('http://localhost:3001/tests/my-tests', {
        credentials: 'include',
      });
      if (response.ok) {
        const data = await response.json();
        setUserTests(data.tests);
      } else if (response.status === 401) {
        navigate('/');
      }
    } catch (error) {
      console.error('Ошибка при получении тестов пользователя:', error);
    }
  };

  const handleCreateTest = async (e) => {
    e.preventDefault();
    if (!newTest.title.trim() || newTest.questions.some(q => !q.question.trim() || q.answers.some(a => !a.trim()) || q.correctAnswer === null)) {
      alert('Пожалуйста, заполните все поля и выберите правильный ответ для каждого вопроса');
      return;
    }

    setIsSubmitting(true);
    try {
      const response = await fetch('http://localhost:3001/tests/create', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newTest),
        credentials: 'include',
      });
      if (response.ok) {
        navigate('/tests');
      } else {
        const result = await response.json();
        alert(result.message);
      }
    } catch (error) {
      console.error('Ошибка при создании теста:', error);
    } finally {
      setIsSubmitting(false);
    }
  };

  const addQuestion = () => {
    if (newTest.questions.length < 10) {
      setNewTest({
        ...newTest,
        questions: [...newTest.questions, { question: '', answers: ['', '', '', ''], correctAnswer: null }],
      });
    }
  };

  const removeQuestion = (index) => {
    const updatedQuestions = newTest.questions.filter((_, i) => i !== index);
    setNewTest({ ...newTest, questions: updatedQuestions });
  };

  const updateQuestion = (index, field, value) => {
    const updatedQuestions = newTest.questions.map((q, i) => {
      if (i === index) {
        return { ...q, [field]: value };
      }
      return q;
    });
    setNewTest({ ...newTest, questions: updatedQuestions });
  };

  const updateAnswer = (questionIndex, answerIndex, value) => {
    const updatedQuestions = newTest.questions.map((q, i) => {
      if (i === questionIndex) {
        const updatedAnswers = q.answers.map((a, j) => (j === answerIndex ? value : a));
        return { ...q, answers: updatedAnswers };
      }
      return q;
    });
    setNewTest({ ...newTest, questions: updatedQuestions });
  };

  const setCorrectAnswer = (questionIndex, answerIndex) => {
    const updatedQuestions = newTest.questions.map((q, i) => {
      if (i === questionIndex) {
        return { ...q, correctAnswer: answerIndex };
      }
      return q;
    });
    setNewTest({ ...newTest, questions: updatedQuestions });
  };

  if (userTests.length >= 10) {
    return (
      <div className="create-test-page">
        <header>
          <div className="header-content">
            <h1>Создание теста</h1>
            <Link to="/tests" className="back-button">Назад к тестам</Link>
          </div>
        </header>
        <main>
          <p>Вы достигли максимального количества тестов (10). Удалите существующий тест, чтобы создать новый.</p>
        </main>
      </div>
    );
  }

  return (
    <div className="create-test-page">
      <Header user={user} title="Название страницы" />
      <main>
        <form onSubmit={handleCreateTest}>
          <input
            type="text"
            placeholder="Название теста"
            value={newTest.title}
            onChange={(e) => setNewTest({ ...newTest, title: e.target.value })}
            required
          />
          {newTest.questions.map((q, qi) => (
            <div key={qi} className="question-container">
              <input
                type="text"
                placeholder={`Вопрос ${qi + 1}`}
                value={q.question}
                onChange={(e) => updateQuestion(qi, 'question', e.target.value)}
                required
              />
              {q.answers.map((a, ai) => (
                <div key={ai} className="answer-container">
                  <input
                    type="text"
                    placeholder={`Ответ ${ai + 1}`}
                    value={a}
                    onChange={(e) => updateAnswer(qi, ai, e.target.value)}
                    required
                  />
                  <input
                    type="radio"
                    name={`correct-answer-${qi}`}
                    checked={q.correctAnswer === ai}
                    onChange={() => setCorrectAnswer(qi, ai)}
                    required
                  />
                  <label>Правильный</label>
                </div>
              ))}
              {newTest.questions.length > 1 && (
                <button type="button" onClick={() => removeQuestion(qi)}>Удалить вопрос</button>
              )}
            </div>
          ))}
          {newTest.questions.length < 10 && (
            <button type="button" onClick={addQuestion}>Добавить вопрос</button>
          )}
          <button type="submit" disabled={isSubmitting}>
            {isSubmitting ? 'Создание...' : 'Создать тест'}
          </button>
        </form>
      </main>
    </div>
  );
};

export default CreateTestPage;






.create-test-page {
    min-height: 100vh;
    background-color: #f3f4f6;
  
    header {
      background-color: #ffffff;
      box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
  
      .header-content {
        max-width: 1280px;
        margin: 0 auto;
        padding: 1rem;
        display: flex;
        justify-content: space-between;
        align-items: center;
  
        h1 {
          font-size: 1.875rem;
          font-weight: bold;
          color: #111827;
        }
  
        .back-button {
          padding: 0.5rem 1rem;
          background-color: #3b82f6;
          color: #ffffff;
          border-radius: 0.25rem;
          text-decoration: none;
          font-weight: bold;
          transition: background-color 0.3s ease;
  
          &:hover {
            background-color: #2563eb;
          }
        }
      }
    }
  
    main {
      max-width: 1280px;
      margin: 0 auto;
      padding: 2rem;
  
      .tests-container {
        display: flex;
        justify-content: center;
        gap: 2rem;
        flex-wrap: wrap;
  
        .test-card, .create-test-card {
          background-color: #ffffff;
          border-radius: 0.5rem;
          box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
          padding: 1.5rem;
          width: 300px;
          text-align: center;
          transition: transform 0.3s ease;
  
          &:hover {
            transform: translateY(-5px);
          }
  
          h2 {
            font-size: 1.5rem;
            font-weight: bold;
            margin-bottom: 0.5rem;
            color: #1f2937;
          }
  
          p {
            color: #4b5563;
            margin-bottom: 1rem;
          }
  
          .card-button {
            display: inline-block;
            padding: 0.5rem 1rem;
            background-color: #10b981;
            color: #ffffff;
            text-decoration: none;
            border-radius: 0.25rem;
            font-weight: bold;
            transition: background-color 0.3s ease;
  
            &:hover {
              background-color: #059669;
            }
          }
        }
  
        .create-test-card {
          cursor: pointer;
          background-color: #e5e7eb;
  
          &:hover {
            background-color: #d1d5db;
          }
        }
      }
  
      .create-test-form {
        background-color: #ffffff;
        border-radius: 0.5rem;
        box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
        padding: 2rem;
        margin-top: 2rem;
  
        h2 {
          font-size: 1.5rem;
          font-weight: bold;
          margin-bottom: 1rem;
          color: #1f2937;
        }
  
        form {
          display: flex;
          flex-direction: column;
          gap: 1rem;
  
          input[type="text"] {
            padding: 0.5rem;
            border: 1px solid #d1d5db;
            border-radius: 0.25rem;
          }
  
          .question-block {
            border: 1px solid #e5e7eb;
            border-radius: 0.25rem;
            padding: 1rem;
  
            .answer-block {
              display: flex;
              align-items: center;
              gap: 0.5rem;
              margin-top: 0.5rem;
            }
          }
  
          button {
            padding: 0.5rem 1rem;
            background-color: #3b82f6;
            color: #ffffff;
            border: none;
            border-radius: 0.25rem;
            font-weight: bold;
            cursor: pointer;
            transition: background-color 0.3s ease;
  
            &:hover {
              background-color: #2563eb;
            }
  
            &[type="button"] {
              background-color: #6b7280;
  
              &:hover {
                background-color: #4b5563;
              }
            }
          }
        }
      }
    }
  }






import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import './TakeTestPage.scss';
import Header from '../Header/Header';

const TakeTestPage = ({ user }) => {
  const { id } = useParams();
  const navigate = useNavigate();
  const [test, setTest] = useState(null);
  const [userAnswers, setUserAnswers] = useState([]);
  const [isSubmitted, setIsSubmitted] = useState(false);
  const [result, setResult] = useState(null);
  const [errorMessage, setErrorMessage] = useState('');

  useEffect(() => {
    const fetchTest = async () => {
      try {
        const response = await fetch(`http://localhost:3001/tests/${id}`, {
          credentials: 'include',
        });
        if (response.ok) {
          const testData = await response.json();
          
          // Убедимся, что поле questions является массивом
          if (typeof testData.questions === 'string') {
            testData.questions = JSON.parse(testData.questions);
          }

          setTest(testData);
          setUserAnswers(new Array(testData.questions.length).fill(null)); // Инициализируем массив ответов
        } else if (response.status === 401) {
          navigate('/');
        }
      } catch (error) {
        console.error('Error fetching test:', error);
      }
    };

    fetchTest();
  }, [id, navigate]);

  const handleAnswerChange = (questionIndex, answerIndex) => {
    const updatedAnswers = [...userAnswers];
    updatedAnswers[questionIndex] = answerIndex;
    setUserAnswers(updatedAnswers);
  };

  const handleSubmitTest = async () => {
  if (userAnswers.includes(null)) {
    setErrorMessage('Пожалуйста, ответьте на все вопросы');
    return;
  }

  try {
    const response = await fetch(`http://localhost:3001/tests/${id}/submit`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ answers: userAnswers }), // Отправляем массив ответов
      credentials: 'include',
    });

    if (!response.ok) {
      const errorData = await response.json();
      setErrorMessage(errorData.message || 'Ошибка при отправке теста');
      return;
    }

    const resultData = await response.json();
    setResult(resultData);
    setIsSubmitted(true);
  } catch (error) {
    setErrorMessage('Ошибка при отправке теста');
    console.error('Error submitting test:', error);
  }
};




  if (!test) {
    return <div className="take-test-page loading">Загрузка...</div>;
  }

  return (
    <div className="take-test-page">
      <Header user={user} title="Название страницы" />
      <main>
        {!isSubmitted ? (
          <div className="test-content">
            {errorMessage && <p className="error-message">{errorMessage}</p>}
            {test.questions.map((question, index) => (
              <div key={index} className="question-block">
                <p className="question">{question.question}</p>
                <div className="answers">
                  {question.answers.map((answer, answerIndex) => (
                    <label key={answerIndex} className="answer-option">
                      <input
                        type="radio"
                        name={`question-${index}`}
                        checked={userAnswers[index] === answerIndex}
                        onChange={() => handleAnswerChange(index, answerIndex)}
                      />
                      <span>{answer}</span>
                    </label>
                  ))}
                </div>
              </div>
            ))}
            <button onClick={handleSubmitTest} className="submit-button">Отправить тест</button>
          </div>
        ) : (
          <div className="result-content">
            <h2>Результаты теста</h2>
            <p>{result.success ? 'Поздравляем! Вы успешно прошли тест.' : 'К сожалению, есть ошибки. Попробуйте еще раз.'}</p>
            <Link to="/tests" className="back-to-tests">Вернуться к тестам</Link>
          </div>
        )}
      </main>
    </div>
  );
};

export default TakeTestPage;





.take-test-page {
    min-height: 100vh;
    background-color: #f3f4f6;
  
    &.loading {
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 1.5rem;
      color: #4b5563;
    }
  
    header {
      background-color: #ffffff;
      box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
  
      .header-content {
        max-width: 1280px;
        margin: 0 auto;
        padding: 1rem;
        display: flex;
        justify-content: space-between;
        align-items: center;
  
        h1 {
          font-size: 1.875rem;
          font-weight: bold;
          color: #111827;
        }
  
        .back-button {
          padding: 0.5rem 1rem;
          background-color: #3b82f6;
          color: #ffffff;
          border-radius: 0.25rem;
          text-decoration: none;
          font-weight: bold;
          transition: background-color 0.3s ease;
  
          &:hover {
            background-color: #2563eb;
          }
        }
      }
    }
  
    .review-question {
      margin-top: 1rem;
      padding: 1rem;
      border: 1px solid #ccc;
      border-radius: 0.5rem;
    
      &.correct {
        background-color: #d1fae5; /* зеленый фон для правильных ответов */
      }
    
      &.incorrect {
        background-color: #fee2e2; /* красный фон для неправильных ответов */
      }
    }
    
    main {
      max-width: 800px;
      margin: 2rem auto;
      padding: 2rem;
      background-color: #ffffff;
      border-radius: 0.5rem;
      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  
      .test-content {
        .question-block {
          margin-bottom: 2rem;
  
          .question {
            font-size: 1.25rem;
            font-weight: bold;
            margin-bottom: 1rem;
            color: #1f2937;
          }
  
          .answers {
            display: flex;
            flex-direction: column;
            gap: 0.5rem;
  
            .answer-option {
              display: flex;
              align-items: center;
              cursor: pointer;
  
              input[type="radio"] {
                margin-right: 0.5rem;
              }
  
              span {
                font-size: 1rem;
                color: #4b5563;
              }
            }
          }
        }
  
        .submit-button {
          width: 100%;
          padding: 0.75rem;
          background-color: #10b981;
          color: #ffffff;
          border: none;
          border-radius: 0.25rem;
          font-size: 1rem;
          font-weight: bold;
          cursor: pointer;
          transition: background-color 0.3s ease;
  
          &:hover {
            background-color: #059669;
          }
        }
      }
  
      .result-content {
        text-align: center;
  
        h2 {
          font-size: 1.5rem;
          font-weight: bold;
          margin-bottom: 1rem;
          color: #1f2937;
        }
  
        p {
          font-size: 1.125rem;
          margin-bottom: 1.5rem;
          color: #4b5563;
        }
  
        .back-to-tests {
          display: inline-block;
          padding: 0.75rem 1.5rem;
          background-color: #3b82f6;
          color: #ffffff;
          text-decoration: none;
          border-radius: 0.25rem;
          font-weight: bold;
          transition: background-color 0.3s ease;
  
          &:hover {
            background-color: #2563eb;
          }
        }
      }
    }
  }






import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import './TestsPage.scss';
import Header from '../Header/Header';

const TestsPage = ({ user }) => {
  const [userTests, setUserTests] = useState([]);
  const navigate = useNavigate();

  useEffect(() => {
    fetchUserTests();
  }, []);

  const fetchUserTests = async () => {
    try {
      const response = await fetch('http://localhost:3001/tests/my-tests', { credentials: 'include' });
      if (response.ok) {
        const data = await response.json();
        setUserTests(data.tests);
      } else if (response.status === 401) {
        navigate('/');
      } else {
        console.error('Ошибка при получении тестов:', response.status);
      }
    } catch (error) {
      console.error('Ошибка при запросе тестов:', error);
    }
  };

  return (
    <div className="tests-page">
      <Header user={user} title="Выбор тестов" />
      <main>
        <div className="tests-container">
          {userTests.length === 0 ? (
            <div className="test-card">
              <h2>Создать свой тест</h2>
              <Link to="/create-test" className="card-button">Создать</Link>
            </div>
          ) : (
            <>
              {userTests.map(test => (
                <div className="test-card" key={test.id}>
                  <h2>{test.title}</h2>
                  <p>{test.questions.length} вопросов</p>
                  <Link to={`/take-test/${test.id}`} className="card-button">Пройти тест</Link>
                </div>
              ))}
              {userTests.length < 10 && (
                <div className="test-card">
                  <h2>Создать свой тест</h2>
                  <Link to="/create-test" className="card-button">Создать</Link>
                </div>
              )}
            </>
          )}
        </div>
      </main>
    </div>
  );
};

export default TestsPage;




.tests-page {
    min-height: 100vh;
    background-color: #f3f4f6;
  
    header {
      background-color: #ffffff;
      box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
  
      .header-content {
        max-width: 1280px;
        margin: 0 auto;
        padding: 1rem;
        display: flex;
        justify-content: space-between;
        align-items: center;
  
        h1 {
          font-size: 1.875rem;
          font-weight: bold;
          color: #111827;
        }
  
        .home-button {
          padding: 0.5rem 1rem;
          background-color: #3b82f6;
          color: #ffffff;
          border-radius: 0.25rem;
          text-decoration: none;
          font-weight: bold;
          transition: background-color 0.3s ease;
  
          &:hover {
            background-color: #2563eb;
          }
        }
      }
    }
  
    main {
      max-width: 1280px;
      margin: 0 auto;
      padding: 2rem;
  
      .tests-container {
        display: flex;
        justify-content: center;
        gap: 2rem;
        flex-wrap: wrap;
  
        .test-card {
          background-color: #ffffff;
          border-radius: 0.5rem;
          box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
          padding: 1.5rem;
          width: 300px;
          text-align: center;
          transition: transform 0.3s ease;
  
          &:hover {
            transform: translateY(-5px);
          }
  
          h2 {
            font-size: 1.5rem;
            font-weight: bold;
            margin-bottom: 0.5rem;
            color: #1f2937;
          }
  
          p {
            color: #4b5563;
            margin-bottom: 1rem;
          }
  
          .card-button {
            display: inline-block;
            padding: 0.5rem 1rem;
            background-color: #10b981;
            color: #ffffff;
            text-decoration: none;
            border-radius: 0.25rem;
            font-weight: bold;
            transition: background-color 0.3s ease;
  
            &:hover {
              background-color: #059669;
            }
          }
        }
      }
    }
  }







import React, { useState } from 'react';
import './Avatar.scss';

const Avatar = ({ src, alt, className }) => {
  const [error, setError] = useState(false);

  const handleError = () => {
    setError(true);
  };

  if (error || !src) {
    return (
      <div className={`${className} avatar-placeholder`}>
        {alt ? alt.charAt(0).toUpperCase() : '?'}
      </div>
    );
  }

  return (
    <img
      src={src}
      alt={alt}
      className={className}
      onError={handleError}
    />
  );
};

export default Avatar;





@import '../styles/variables';

.avatar-placeholder {
  width: 120px;
  height: 120px;
  border-radius: 50%;
  background-color: #d1d5db;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #ffffff;
  font-weight: bold;
  font-size: 2rem;
  transition: background-color 0.3s ease, transform 0.3s ease;

  &:hover {
    background-color: lighten(#d1d5db, 10%);
    transform: scale(1.05);
  }
}






import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Button from '../Button/Button';
import './RegisterPage.scss';

const RegisterPage = ({ onLogin }) => {
  const navigate = useNavigate();
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    try {
      const response = await fetch('http://localhost:3001/users/signup', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password, email }),
        credentials: 'include',
      });
  
      if (response.ok) {
        const userData = await response.json();
        // После успешной регистрации выполняем вход
        const loginResponse = await fetch('http://localhost:3001/users/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ username, password }),
          credentials: 'include',
        });
  
        if (loginResponse.ok) {
          const loginData = await loginResponse.json();
          onLogin(loginData.user);
          navigate('/');
        } else {
          setError('Registration successful, but login failed. Please try logging in manually.');
        }
      } else {
        const errorData = await response.json();
        setError(errorData.message || 'Registration failed');
      }
    } catch (error) {
      setError('An error occurred. Please try again.');
    }
  };

  return (
    <div className="register-page">
      <div className="register-form">
        <h1>Регистрация</h1>
        {error && <p className="error-message">{error}</p>}
        <form onSubmit={handleSubmit}>
          <input
            type="text"
            placeholder="Имя пользователя"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
            required
          />
          <input
            type="password"
            placeholder="Пароль"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
          />
          <input
            type="email"
            placeholder="Email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
          />
          <Button type="submit" variant="primary" className="wider-button">Зарегистрироваться</Button>
        </form>
        <Button onClick={() => navigate('/')} variant="outline" className="login-link">
          Уже есть аккаунт? Войти
        </Button>
      </div>
    </div>
  );
};

export default RegisterPage;





@import '../styles/variables';

.register-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  background-color: $background-color;

  .register-form {
    background-color: $light-background;
    padding: 2rem;
    border-radius: 0.5rem;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    width: 24rem;

    h1 {
      font-size: 1.5rem;
      font-weight: bold;
      margin-bottom: 1.5rem;
      color: $heading-color;
    }

    .error-message {
      color: $danger-color;
      margin-bottom: 1rem;
      text-align: center;
    }

    form {
      display: flex;
      flex-direction: column;
      gap: 1rem;

      input {
        padding: 0.75rem;
        border: 1px solid #d1d5db;
        border-radius: 0.375rem;
        font-size: 1rem;
      }

      button {
        @include button-style($primary-color, #ffffff, $primary-hover);
        font-size: 1rem;
      }
    }

    .login-link {
      margin-top: 1rem;
      text-align: center;
      color: $primary-color;
      cursor: pointer;
      @include transition(color 0.3s ease);

      &:hover {
        text-decoration: underline;
        color: $primary-hover;
      }
    }
  }
}







import React, { useState, useEffect } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import './UserPage.scss';
import Avatar from '../User/Avatar';
import Button from '../Button/Button';
import { FaEdit, FaSave, FaTimes, FaSignOutAlt, FaHome } from 'react-icons/fa'; // Импорт иконок

const UserPage = ({ onLogout, onUpdateAvatar }) => {
  const navigate = useNavigate();
  const [user, setUser] = useState(null);
  const [selectedAvatar, setSelectedAvatar] = useState(null);
  const [isAvatarChanged, setIsAvatarChanged] = useState(false);
  const [isEditing, setIsEditing] = useState(false);
  const [username, setUsername] = useState('');
  const [email, setEmail] = useState('');

  useEffect(() => {
    const fetchUserData = async () => {
      try {
        const response = await fetch('http://localhost:3001/users/login-check', {
          credentials: 'include',
        });
        if (response.ok) {
          const userData = await response.json();
          setUser(userData);
          setSelectedAvatar(userData.avatarId);
          setUsername(userData.username);
          setEmail(userData.email);
        } else {
          navigate('/');
        }
      } catch (error) {
        console.error('Error fetching user data:', error);
        navigate('/');
      }
    };

    fetchUserData();
  }, [navigate]);

  const handleLogout = async () => {
    await onLogout();
    navigate('/');
  };

  const handleAvatarChange = (avatarId) => {
    setSelectedAvatar(avatarId);
    setIsAvatarChanged(true);
  };

  const handleSaveAvatar = async () => {
    try {
      const response = await fetch(`http://localhost:3001/users/${user.userId}/avatar`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ avatarId: selectedAvatar }),
        credentials: 'include',
      });

      if (response.ok) {
        const updatedUser = { ...user, avatarId: selectedAvatar };
        setUser(updatedUser);
        onUpdateAvatar(selectedAvatar);
        setIsAvatarChanged(false);
      } else {
        console.error('Failed to update avatar');
      }
    } catch (error) {
      console.error('Error updating avatar:', error);
    }
  };

  const handleEditProfile = () => {
    setIsEditing(true);
  };

  const handleSaveProfile = async () => {
    // Здесь можно добавить логику для обновления профиля пользователя
    // Например, отправить новые данные на сервер и обновить состояние `user`
    setIsEditing(false);
    // Пример обновления локального состояния
    setUser({ ...user, username, email });
    // Предполагается, что onUpdateProfile передаст обновленные данные из родительского компонента
    // onUpdateProfile({ username, email });
  };

  const handleCancelEdit = () => {
    setIsEditing(false);
    setUsername(user.username);
    setEmail(user.email);
  };

  if (!user) {
    return null;
  }

  return (
    <div className="user-page">
      <div className="profile-card">
        <div className="avatar-section">
          <Avatar src={`/avatars/${user.avatarId}.png`} alt={user.username} className="user-avatar" />
          <div className="avatar-selection">
            <h3>Выберите аватар:</h3>
            <div className="avatar-options">
              {[1, 2, 3, 4, 5].map((avatarId) => (
                <img
                  key={avatarId}
                  src={`/avatars/${avatarId}.png`}
                  alt={`Avatar ${avatarId}`}
                  className={`avatar-option ${selectedAvatar === avatarId ? 'selected' : ''}`}
                  onClick={() => handleAvatarChange(avatarId)}
                />
              ))}
            </div>
            {isAvatarChanged && (
              <Button
                variant="secondary"
                onClick={handleSaveAvatar}
                className="save-avatar-button"
                leftIcon={<FaSave />}
              >
                Сохранить аватар
              </Button>
            )}
          </div>
        </div>
        <div className="info-section">
          <h2>Профиль</h2>
          {isEditing ? (
            <>
              <div className="input-group">
                <label htmlFor="username">Имя пользователя:</label>
                <input
                  type="text"
                  id="username"
                  value={username}
                  onChange={(e) => setUsername(e.target.value)}
                  required
                />
              </div>
              <div className="input-group">
                <label htmlFor="email">Email:</label>
                <input
                  type="email"
                  id="email"
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  required
                />
              </div>
              <div className="button-group">
                <Button
                  variant="secondary"
                  onClick={handleSaveProfile}
                  className="save-button"
                  leftIcon={<FaSave />}
                >
                  Сохранить
                </Button>
                <Button
                  variant="danger"
                  onClick={handleCancelEdit}
                  className="cancel-button"
                  leftIcon={<FaTimes />}
                >
                  Отмена
                </Button>
              </div>
            </>
          ) : (
            <>
              <p><strong>Имя пользователя:</strong> {user.username}</p>
              <p><strong>Email:</strong> {user.email}</p>
              <div className="button-group">
                <Button
                  variant="primary"
                  onClick={handleEditProfile}
                  className="edit-button"
                  leftIcon={<FaEdit />}
                >
                  Редактировать профиль
                </Button>
                <Button
                  variant="danger"
                  onClick={handleLogout}
                  className="logout-button"
                  leftIcon={<FaSignOutAlt />}
                >
                  Выйти
                </Button>
              </div>
            </>
          )}
          <Link to="/" className="home-button">
            <Button variant="primary" leftIcon={<FaHome />}>
              На главную
            </Button>
          </Link>
        </div>
      </div>
    </div>
  );
};

export default UserPage;






@import '../styles/variables.scss';
@import '../Button/Button.scss'; // Обновите путь, если структура папок отличается

.user-page {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background-color: $background-color;
  padding: $spacer;

  .profile-card {
    background-color: $light-background;
    border-radius: 0.75rem;
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: 700px;
    padding: 2rem;

    @media (min-width: 768px) {
      flex-direction: row;
    }

    .avatar-section {
      display: flex;
      flex-direction: column;
      align-items: center;
      margin-bottom: 2rem;

      @media (min-width: 768px) {
        margin-bottom: 0;
        margin-right: 2rem;
      }

      .user-avatar {
        width: 120px;
        height: 120px;
        border-radius: 50%;
        object-fit: cover;
        margin-bottom: 1rem;
        border: 4px solid $primary-color;
      }

      .avatar-selection {
        width: 100%;

        h3 {
          font-size: 1.25rem;
          margin-bottom: 1rem;
          color: $heading-color;
          text-align: center;
        }

        .avatar-options {
          display: flex;
          gap: 0.75rem;
          justify-content: center;
          flex-wrap: wrap;
          margin-bottom: 1rem;

          .avatar-option {
            width: 60px;
            height: 60px;
            border-radius: 50%;
            cursor: pointer;
            border: 2px solid transparent;
            @include transition(transform 0.3s ease, border 0.3s ease);

            &:hover {
              transform: scale(1.1);
            }

            &.selected {
              border: 2px solid $primary-color;
            }
          }
        }

        .save-avatar-button {
          @include button-secondary;
          width: 100%;
          margin-top: 0.5rem;
        }
      }
    }

    .info-section {
      flex-grow: 1;

      h2 {
        font-size: 1.75rem;
        margin-bottom: 1.5rem;
        color: $heading-color;
        text-align: center;

        @media (min-width: 768px) {
          text-align: left;
        }
      }

      p {
        font-size: 1rem;
        margin-bottom: 1rem;
        color: $text-color;

        strong {
          color: $heading-color;
        }
      }

      .input-group {
        display: flex;
        flex-direction: column;
        margin-bottom: 1rem;

        label {
          font-size: 0.95rem;
          margin-bottom: 0.5rem;
          color: $heading-color;
        }

        input {
          padding: 0.75rem;
          border: 1px solid #d1d5db;
          border-radius: 0.375rem;
          font-size: 1rem;
          @include transition(border-color 0.3s ease);

          &:focus {
            border-color: $primary-color;
            outline: none;
          }
        }
      }

      .button-group {
        display: flex;
        flex-wrap: wrap;
        gap: 0.5rem;
        margin-bottom: 1.5rem;

        .edit-button {
          @include button-primary;
          flex: 1 1 48%;
        }

        .save-button {
          @include button-secondary;
          flex: 1 1 48%;
        }

        .cancel-button {
          @include button-danger;
          flex: 1 1 48%;
        }

        .logout-button {
          @include button-danger;
          flex: 1 1 48%;
        }
      }

      .home-button {
        @include button-primary;
        width: 100%;
        text-align: center;
      }
    }
  }
}





import React, { useState, useEffect } from 'react';
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
import MainPage from './MainPage';
import RegisterPage from './User/RegisterPage';
import UserPage from './User/UserPage';
import TestsPage from './TestPages/TestsPage';
import CreateTestPage from './TestPages/CreateTestPage';
import TakeTestPage from './TestPages/TakeTestPage';
import MapPage from './Map/MapPage';
import FlagQuizPage from './FlagQuiz/FlagQuiz';

function App() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const checkLoginStatus = async () => {
      try {
        const response = await fetch('http://localhost:3001/users/login-check', {
          credentials: 'include',
        });
        if (response.ok) {
          const userData = await response.json();
          setUser(userData);
        }
      } catch (error) {
        console.error('Error checking login status:', error);
      }
    };

    checkLoginStatus();
  }, []);

  const handleLogin = (userData) => {
    setUser(userData);
  };

  const handleLogout = async () => {
    try {
      await fetch('http://localhost:3001/users/logout', {
        method: 'GET',
        credentials: 'include',
      });
      setUser(null);
    } catch (error) {
      console.error('Error logging out:', error);
    }
  };

  const handleUpdateAvatar = (avatarId) => {
    setUser(prevUser => ({ ...prevUser, avatarId }));
  };

  return (
    <Router>
      <Routes>
        <Route 
          path="/" 
          element={<MainPage user={user} onLogin={handleLogin} />} 
        />
        <Route 
          path="/register" 
          element={<RegisterPage onLogin={handleLogin} />} 
        />
        <Route 
          path="/user" 
          element={user ? <UserPage user={user} onLogout={handleLogout} onUpdateAvatar={handleUpdateAvatar} /> : <Navigate to="/" />} 
        />
        <Route 
          path="/tests" 
          element={user ? <TestsPage user={user} /> : <Navigate to="/" />} 
        />
        <Route 
          path="/create-test" 
          element={user ? <CreateTestPage user={user} /> : <Navigate to="/" />} 
        />
        <Route 
          path="/take-test/:id" 
          element={user ? <TakeTestPage user={user} /> : <Navigate to="/" />} 
        />
        <Route 
          path="/map" 
          element={user ? <MapPage user={user} /> : <Navigate to="/" />} 
        />
        <Route 
          path="/flag-quiz" 
          element={<FlagQuizPage />} 
        />
        {/* Добавьте другие маршруты по необходимости */}
        <Route path="*" element={<Navigate to="/" />} />
      </Routes>
    </Router>
  );
}

export default App;





import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Header from './Header/Header';
import Modal from './Modal';
import Card from './Card/Card'; // Создадим универсальный компонент Card
import './MainPage.scss';

const MainPage = ({ user, onLogin }) => {
  const [showLoginModal, setShowLoginModal] = useState(false);
  const [selectedCard, setSelectedCard] = useState(null);
  const navigate = useNavigate();

  const cards = [
    { id: 1, title: 'Карта мира', description: 'Изучайте мир, путешествуя по интерактивной карте', path: '/map', icon: '🌎' },
    { id: 2, title: 'Тесты', description: 'Проверьте свои знания в различных областях', path: '/tests', icon: '📝' },
    { id: 3, title: 'Географический квест', description: 'Путешествуйте по миру и изучайте географию', path: '/game/geo-quest', icon: '🗺️' },
  ];

  const handleCardClick = (card) => {
    if (user) {
      navigate(card.path);
    } else {
      setSelectedCard(card);
      setShowLoginModal(true);
    }
  };

  const handleLoginClick = () => {
    setShowLoginModal(true);
  };

  const handleLoginSuccess = (userData) => {
    onLogin(userData);
    setShowLoginModal(false);
    if (selectedCard) {
      navigate(selectedCard.path);
    }
  };

  return (
    <div className="main-page">
      <Header user={user} onLoginClick={handleLoginClick} />
      <main>
        <section className="hero-section">
          <h1>Добро пожаловать на образовательную платформу!</h1>
          <p>{user ? 'Выберите интересующий вас раздел:' : 'Авторизуйтесь, чтобы получить доступ ко всем функциям:'}</p>
        </section>
        <section className="cards-grid">
          {cards.map((card) => (
            <Card key={card.id} title={card.title} description={card.description} onClick={() => handleCardClick(card)}>
              {card.icon}
            </Card>
          ))}
        </section>
      </main>
      {showLoginModal && (
        <Modal
          onClose={() => setShowLoginModal(false)}
          onLogin={handleLoginSuccess}
        />
      )}
    </div>
  );
};

export default MainPage;







.main-page {
  min-height: 100vh;
  background-color: #f3f4f6;

  main {
    max-width: 1280px;
    margin: 0 auto;
    padding: 2rem;

    section {
      background-color: #ffffff;
      border-radius: 0.5rem;
      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
      padding: 1.5rem;
      margin-bottom: 2rem;

      h1 {
        font-size: 2rem;
        font-weight: bold;
        text-align: center;
        margin-bottom: 1.5rem;
      }
      h2 {
        font-size: 1.5rem;
        font-weight: bold;
        margin-bottom: 1rem;
        color: #1f2937;
      }
    }
    p {
      text-align: center;
      margin-bottom: 2rem;
    }

    .cards-grid {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
      gap: 1.5rem;

      .card {
        background-color: #ffffff;
        border-radius: 0.5rem;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        padding: 1.5rem;
        text-align: center;
        transition: transform 0.3s ease;
        cursor: pointer;

        &:hover {
          transform: translateY(-5px);
        }

        h2 {
          font-size: 1.5rem;
          font-weight: bold;
          margin-bottom: 0.5rem;
        }

        p {
          color: #6b7280;
        }
      }
    }
    .featured-content {
      .games-grid {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
        gap: 1.5rem;

        .game-card {
          background-color: #f9fafb;
          border-radius: 0.375rem;
          padding: 1rem;
          transition: transform 0.3s ease;

          &:hover {
            transform: translateY(-5px);
          }

          h3 {
            font-size: 1.25rem;
            font-weight: bold;
            margin-bottom: 0.5rem;
            color: #111827;
          }

          p {
            color: #4b5563;
            margin-bottom: 1rem;
          }

          .play-button {
            display: inline-block;
            padding: 0.5rem 1rem;
            background-color: #10b981;
            color: #ffffff;
            text-decoration: none;
            border-radius: 0.25rem;
            font-weight: bold;
            transition: background-color 0.3s ease;

            &:hover {
              background-color: #059669;
            }
          }
        }
      }
    }

    .user-tests, .map-link {
      text-align: center;

      .view-tests-button, .map-button {
        display: inline-block;
        padding: 0.75rem 1.5rem;
        background-color: #3b82f6;
        color: #ffffff;
        text-decoration: none;
        border-radius: 0.25rem;
        font-weight: bold;
        transition: background-color 0.3s ease;

        &:hover {
          background-color: #2563eb;
        }
      }
    }
  }
}





import React, { useState } from 'react';
import './Modal.scss';

const Modal = ({ onClose, onLogin }) => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');

    try {
      const response = await fetch('http://localhost:3001/users/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password }),
        credentials: 'include',
      });

      if (response.ok) {
        const userData = await response.json();
        onLogin(userData.user);
      } else {
        const data = await response.json();
        setError(data.message || 'Неверное имя пользователя или пароль');
      }
    } catch (err) {
      setError('Ошибка соединения. Попробуйте снова.');
    }
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button className="close-button" onClick={onClose}>✕</button>
        <h2>Авторизация</h2>
        {error && <p className="error-message">{error}</p>}
        <form onSubmit={handleSubmit}>
          <input
            type="text"
            placeholder="Имя пользователя"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
            required
          />
          <input
            type="password"
            placeholder="Пароль"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
          />
          <button type="submit">Войти</button>
        </form>
      </div>
    </div>
  );
};

export default Modal;





@import './styles/variables';

.modal-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
  animation: fadeIn 0.3s ease;
}

.modal-content {
  background-color: $light-background;
  border-radius: 0.5rem;
  padding: 2rem;
  width: 90%;
  max-width: 400px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  position: relative;
  animation: slideDown 0.3s ease;

  h2 {
    font-size: 1.5rem;
    margin-bottom: 1.5rem;
    color: $heading-color;
    text-align: center;
  }

  .error-message {
    color: $danger-color;
    margin-bottom: 1rem;
    text-align: center;
  }

  form {
    display: flex;
    flex-direction: column;
    gap: 1rem;

    input {
      padding: 0.75rem;
      border: 1px solid #d1d5db;
      border-radius: 0.375rem;
      font-size: 1rem;
    }

    button {
      @include button-style($primary-color, #ffffff, $primary-hover);
      font-size: 1rem;
    }
  }

  .close-button {
    position: absolute;
    top: 1rem;
    right: 1rem;
    background: none;
    border: none;
    font-size: 1.5rem;
    cursor: pointer;
    color: $text-color;
    @include transition(color 0.3s ease);

    &:hover {
      color: $heading-color;
    }
  }
}

@keyframes fadeIn {
  from { background: rgba(0, 0, 0, 0); }
  to { background: rgba(0, 0, 0, 0.5); }
}

@keyframes slideDown {
  from { transform: translateY(-20px); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}







import React, { useState } from 'react';
import './PublicPage.scss';

const PublicPage = ({ onLoginClick }) => {
  const [showModal, setShowModal] = useState(false);
  const [selectedCard, setSelectedCard] = useState(null);

  const cards = [
    { id: 1, title: 'Карта мира', description: 'Изучайте мир, путешествуя по интерактивной карте' },
    { id: 2, title: 'Тесты', description: 'Проверьте свои знания в различных областях' },
    { id: 3, title: 'Викторина по истории', description: 'Проверьте свои знания истории' },
    { id: 4, title: 'Математический вызов', description: 'Решайте математические задачи на время' },
    { id: 5, title: 'Географический квест', description: 'Путешествуйте по миру и изучайте географию' },
  ];

  const handleCardClick = (card) => {
    setSelectedCard(card);
    setShowModal(true);
  };

  return (
    <div className="public-page">
      <header>
        <div className="header-content">
          <Link to="/" className="logo">Название сайта</Link>
          <button onClick={onLoginClick} className="login-button">Вход</button>
        </div>
      </header>
      <main>
        <h1>Добро пожаловать!</h1>
        <p>Чтобы получить доступ к полному функционалу, авторизуйтесь.</p>
        <div className="card-container">
          {cards.map((card) => (
            <div key={card.id} className="card" onClick={() => handleCardClick(card)}>
              <h2>{card.title}</h2>
              <p>{card.description}</p>
            </div>
          ))}
        </div>
      </main>

      {showModal && (
        <div className="modal-backdrop" onClick={() => setShowModal(false)}>
          <div className="modal-content">
            <h2>{selectedCard.title}</h2>
            <p>{selectedCard.description}</p>
            <button onClick={() => setShowModal(false)}>Закрыть</button>
          </div>
        </div>
      )}
    </div>
  );
};

export default PublicPage;





.public-page {
    min-height: 100vh;
    background-color: #f3f4f6;
    display: flex;
    flex-direction: column;
  
    header {
      background-color: #ffffff;
      padding: 1rem;
      display: flex;
      justify-content: space-between;
      align-items: center;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
      
      .logo {
        font-size: 1.5rem;
        font-weight: bold;
        color: #3b82f6;
      }
    }
  
    main {
      flex-grow: 1;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 2rem;
  
      h1 {
        font-size: 2rem;
        color: #111827;
        margin-bottom: 1rem;
      }
  
      p {
        font-size: 1.125rem;
        color: #4b5563;
        margin-bottom: 2rem;
      }
  
      .card-container {
        display: flex;
        gap: 2rem;
  
        .card {
          background-color: #ffffff;
          border-radius: 0.5rem;
          box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
          padding: 1.5rem;
          text-align: center;
          transition: transform 0.2s;
          cursor: pointer;
  
          &:hover {
            transform: translateY(-5px);
          }
  
          h2 {
            font-size: 1.5rem;
            color: #111827;
          }
  
          p {
            color: #6b7280;
          }
        }
      }
    }
  }
  




import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from 'src/users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { SessionSerializer } from './session.serializer';

@Module({
  imports: [UsersModule, PassportModule.register({session: true})],
  providers: [AuthService, LocalStrategy, SessionSerializer]
})
export class AuthModule {}





import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UsersService } from 'src/users/users.service';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(private readonly usersService: UsersService) {}

  async validateUser(username: string, password: string) {
    const user = await this.usersService.findOne({ where: { username } });

    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const passwordValid = await bcrypt.compare(password, user.password);

    if (!passwordValid) {
      throw new UnauthorizedException('Invalid credentials');
    }

    if (user && passwordValid) {
      return {
        userId: user.id,
        username: user.username,
        email: user.email,
        avatarId: user.avatarId
      };
    }

    return null;
  }
}



import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Observable } from "rxjs";

@Injectable()
export class AuthenticatedGuard implements CanActivate {
    async canActivate(context: ExecutionContext) {
        const request = context.switchToHttp().getRequest();

        return request.isAuthenticated();
    }
}




import { ExecutionContext, Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { Observable } from "rxjs";

@Injectable()
export class LocalAuthGuard extends AuthGuard('local'){
    async canActivate(context: ExecutionContext) {
        const result = await super.canActivate(context) as boolean
        const request = context.switchToHttp().getRequest();

        await super.logIn(request);

        return result;
    }
}







import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(
        username.toLowerCase(), 
        password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}





import { Injectable } from "@nestjs/common";
import { PassportSerializer } from "@nestjs/passport";

@Injectable()
export class SessionSerializer extends PassportSerializer{
    serializeUser(user: any, done: (err: Error, user: any) => void) {
        done(null, user)
    }


    deserializeUser(payload: any, done: (err: Error, user: any) => void) {
        done(null, payload)
    }
}





import { registerAs } from "@nestjs/config";
import { sqlconfig } from "./sql.config";

export const databaseConfig = registerAs('database',  () => ({
    sql: {
        ...sqlconfig(),
    }
}));




import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { SequelizeModuleOptions, SequelizeOptionsFactory } from "@nestjs/sequelize";
import { User } from "src/users/users.model";

@Injectable()
export class SequelizeConfigService implements SequelizeOptionsFactory{
    constructor (private readonly configService: ConfigService) {}

    createSequelizeOptions(connectionName?: string): SequelizeModuleOptions {
        const {
            sql: { dialect,logging,host,password,username,port,database}
        } = this.configService.get('database');

        return{
            username,
            password,
            dialect,
            logging,
            port,
            host,
            database,
            models: [User],
            autoLoadModels: true,
            synchronize: true,
            define: {
                charset: 'utf8',
                collate: 'utf8_general_ci'
            }
        }
    }
}




import { registerAs } from "@nestjs/config";
import { Dialect } from "sequelize";

export const sqlconfig = registerAs('database', ()=> ({
    dialect: <Dialect>process.env.SQL_DIALECT || 'mysql',
    logging: process.env.SQL_LOGGING === 'true' ? true: false,
    host: process.env.DATABASE_HOST,
    port: +process.env.DATABASE_PORT,
    username: process.env.DATABASE_USER,
    password: process.env.DATABASE_PASSWORD,
    database: process.env.DATABASE_NAME,
    autoLoadEntities: true,
    synchronize: true,
}));




import { Controller, Post, Get, Body, UseGuards, Request, Param } from '@nestjs/common';
import { TestService } from './test.service';
import { AuthenticatedGuard } from '../auth/authenticated.guard';

@Controller('tests')
export class TestController {
  constructor(private readonly testService: TestService) {}

  @UseGuards(AuthenticatedGuard)
  @Post('/create')
  async createTest(@Request() req, @Body() body) {
    const { title, questions } = body;
    const userTests = await this.testService.findTestsByUser(req.user.userId);

    if (userTests.length >= 10) {
      return { message: 'Нельзя создать больше 10 тестов' };
    }

    if (questions.length > 10) {
      return { message: 'Тест не может содержать более 10 вопросов' };
    }

    if (questions.some(q => q.answers.length !== 4)) {
      return { message: 'Каждый вопрос должен иметь ровно 4 варианта ответа' };
    }

    return this.testService.createTest(req.user.userId, title, questions);
  }

  @UseGuards(AuthenticatedGuard)
@Get('/my-tests')
async getUserTests(@Request() req) {
  const tests = await this.testService.findTestsByUser(req.user.userId);
  const testCount = await this.testService.countTestsByUser(req.user.userId);
  
  return { tests, testCount };
}


  @Get('/all')
  async getAllTests() {
    return this.testService.findAllTests();
  }

  @Get('/:id')
  async getTest(@Param('id') id: string) {
    return this.testService.findTestById(id);
  }
  
  @UseGuards(AuthenticatedGuard)
@Post('/:id/submit')
async submitTest(@Param('id') id: string, @Body() body: { answers: number[] }) {
  console.log('Received request to submit test:', id);
  console.log('Request body:', JSON.stringify(body, null, 2));
  return this.testService.checkTestAnswers(id, body.answers);
}

  
}





import { Column, Model, Table, ForeignKey, DataType } from 'sequelize-typescript';
import { User } from '../users/users.model';

@Table
export class Test extends Model {
  @ForeignKey(() => User)
  @Column
  userId: number;

  @Column
  title: string;

  // Изменяем тип поля questions, чтобы оно правильно сохраняло массив
  @Column(DataType.JSON) // Сохраняем как JSON (массив)
  questions: any[];

  @Column(DataType.INTEGER)
  timesCompleted: number;

  @Column(DataType.FLOAT)
  averageScore: number;
}





import { Module } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { Test } from './test.model';
import { TestService } from './test.service';
import { TestController } from './test.controller';

@Module({
  imports: [SequelizeModule.forFeature([Test])],
  providers: [TestService],
  controllers: [TestController],
})
export class TestModule {}




import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Test } from './test.model';

@Injectable()
export class TestService {
  constructor(
    @InjectModel(Test)
    private testModel: typeof Test,
  ) {}

  async createTest(userId: number, title: string, questions: any[]) {
    if (!Array.isArray(questions)) {
      throw new BadRequestException('Questions must be an array');
    }
  
    return this.testModel.create({
      userId,
      title,
      questions,  // Сохраняем как массив
      timesCompleted: 0,
      averageScore: 0,
    });
  }
  

  async findTestsByUser(userId: number) {
    const tests = await this.testModel.findAll({ where: { userId } });
    return tests.map(test => ({
      ...test.toJSON(),
      questions: test.questions,
    }));
  }

  async findAllTests() {
    const tests = await this.testModel.findAll();
    return tests.map(test => {
      const testJson = test.toJSON();
  
      // Если поле questions — строка, парсим его
      if (typeof testJson.questions === 'string') {
        testJson.questions = JSON.parse(testJson.questions);
      }
  
      return {
        ...testJson,
        questionsCount: testJson.questions.length, // Передаём правильное количество вопросов
      };
    });
  }
  
  

  async findTestById(testId: string) {
    const test = await this.testModel.findByPk(testId);
    if (!test) {
      throw new NotFoundException('Тест не найден');
    }
  
    // Проверим, что questions является массивом
    if (typeof test.questions === 'string') {
      test.questions = JSON.parse(test.questions);
    }
  
    return test.toJSON();
  }
  

  async countTestsByUser(userId: number) {
    return this.testModel.count({ where: { userId } });
  }
  
  async checkTestAnswers(testId: string, userAnswers: number[]) {
    const test = await this.testModel.findByPk(testId);
    if (!test) {
      throw new NotFoundException('Тест не найден');
    }
  
    // Проверим, что поле questions — это JSON и содержит массив вопросов
    let questions = test.questions;
    if (typeof questions === 'string') {
      try {
        questions = JSON.parse(questions);  // Распарсим, если это строка
      } catch (error) {
        throw new BadRequestException('Ошибка при обработке вопросов теста');
      }
    }
  
    if (!questions || !Array.isArray(questions)) {
      throw new BadRequestException('Вопросы теста отсутствуют или некорректны');
    }
  
    console.log('Количество вопросов:', questions.length);
    console.log('Ответы пользователя:', userAnswers);
  
    if (questions.length !== userAnswers.length) {
      throw new BadRequestException('Некорректное количество ответов');
    }
  
    let score = 0;
    const result = questions.map((q, index) => {
      const isCorrect = q.correctAnswer === userAnswers[index];
      if (isCorrect) score++;
      return isCorrect;
    });
  
    const percentageScore = (score / questions.length) * 100;
  
    // Обновляем статистику по тесту
    test.timesCompleted += 1;
    test.averageScore = ((test.averageScore * (test.timesCompleted - 1)) + percentageScore) / test.timesCompleted;
    await test.save();
  
    return {
      success: result.every(r => r),
      correctAnswers: questions.map(q => q.correctAnswer),
      userScore: score,
      totalQuestions: questions.length,
      percentageScore: percentageScore.toFixed(2),
    };
  }
  
  
  
}






import { IsNotEmpty } from "class-validator";

export class CreateUserDto {
    @IsNotEmpty()
    readonly username: string;

    @IsNotEmpty()
    readonly password: string;

    @IsNotEmpty()
    readonly email: string;
}





import { Body, Controller, Get, Header, HttpCode, HttpStatus, Post, Request, UseGuards, Patch, Param } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { LocalAuthGuard } from 'src/auth/local.auth.guard';
import { AuthenticatedGuard } from 'src/auth/authenticated.guard';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post('/signup')
  @HttpCode(HttpStatus.CREATED)
  @Header('Content-type', 'application/json')
  async createUser(@Body() CreateUserDto: CreateUserDto) {
    const user = await this.usersService.create(CreateUserDto);
    if ('warningMessage' in user) {
      return user;
    }
    return { userId: user.id, username: user.username, email: user.email, avatarId: user.avatarId };
  }

  @Post('/login')
  @UseGuards(LocalAuthGuard)
  @HttpCode(HttpStatus.OK)
  login(@Request() req) {
    return { user: req.user, msg: 'Logged in successfully' };
  }

  @Get('/login-check')
  @UseGuards(AuthenticatedGuard)
  async loginCheck(@Request() req) {
    const user = await this.usersService.findById(req.user.userId);
    return {
      userId: user.id,
      username: user.username,
      email: user.email,
      avatarId: user.avatarId
    };
  }

  @Get('/logout')
  logout(@Request() req) {
    req.session.destroy();
    return { msg: 'Logged out successfully' };
  }

  @Patch('/:id/avatar')
  @UseGuards(AuthenticatedGuard)
  async updateAvatar(@Param('id') id: string, @Body('avatarId') avatarId: number) {
    const user = await this.usersService.updateAvatar(Number(id), avatarId);
    return { avatarId: user.avatarId };
  }
}






import { Column, Model, Table, DataType } from "sequelize-typescript";

@Table
export class User extends Model {
  @Column
  username: string;
  
  @Column
  password: string;

  @Column
  email: string;

  @Column(DataType.INTEGER)
  avatarId: number;
}




import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { SequelizeModule } from '@nestjs/sequelize';
import { User } from './users.model';

@Module({
  imports:[SequelizeModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
  exports: [UsersService]
})
export class UsersModule {}





import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { User } from './users.model';
import { CreateUserDto } from './dto/create-user.dto';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
  constructor(
    @InjectModel(User)
    private userModel: typeof User,
  ) {}

  findOne(filter: {
    where: { id?: string; username?: string; email?: string };
  }): Promise<User> {
    return this.userModel.findOne({ ...filter });
  }

  async create(
    createUserDto: CreateUserDto,
  ): Promise<User | { warningMessage: string }> {
    const user = new User();
    const existingByUserName = await this.findOne({
      where: { username: createUserDto.username },
    });
    const existingByEmail = await this.findOne({
      where: { email: createUserDto.email },
    });

    if (existingByUserName) {
      return { warningMessage: 'Пользователь с таким именем уже существует' };
    }

    if (existingByEmail) {
      return { warningMessage: 'Пользователь с таким электронным адресом уже существует' };
    }

    const hashedPassword = await bcrypt.hash(createUserDto.password, 10);

    user.username = createUserDto.username;
    user.password = hashedPassword;
    user.email = createUserDto.email;
    user.avatarId = 1; // Default avatar ID

    return user.save();
  }

  async updateAvatar(userId: number, avatarId: number): Promise<User> {
    const user = await this.userModel.findByPk(userId);
    if (!user) {
      throw new Error('User not found');
    }
    user.avatarId = avatarId;
    await user.save();
    return user;
  }

  async findById(userId: number): Promise<User> {
    const user = await this.userModel.findByPk(userId);
    if (!user) {
      throw new Error('User not found');
    }
    return user;
  }
}







import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { SequelizeModule } from '@nestjs/sequelize';
import { ConfigModule } from '@nestjs/config';
import { SequelizeConfigService } from './config/sequelizeConfig.service';
import { databaseConfig } from './config/configuration';
import { AuthModule } from './auth/auth.module';
import { TestModule } from './testpages/test.module';

@Module({
  imports: [
    SequelizeModule.forRootAsync({
      imports: [ConfigModule],
      useClass: SequelizeConfigService,
    }),
    ConfigModule.forRoot({
      load: [databaseConfig],
    }),
    UsersModule,
    AuthModule,
    TestModule,
  ],
})
export class AppModule {}





import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as session from 'express-session'
import * as passport from 'passport'
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(session({
    secret: 'keyword',
    resave: false,
    saveUninitialized: false
  }))
  app.useGlobalPipes(new ValidationPipe({
    transform: true,
    whitelist: true,
    forbidNonWhitelisted: true,
  }));
  app.use(passport.initialize());
  app.use(passport.session());
  app.enableCors({
    origin: 'http://localhost:3000',  // Разрешаем запросы с фронтенда
    credentials: true,  // Включаем передачу cookies и сессий
  });
  await app.listen(3001);
}
bootstrap();