фронтенд:
import React from 'react';
import './Button.scss';

const Button = ({ children, onClick, variant = 'primary', className = '', leftIcon, rightIcon, ...props }) => {
  return (
    <button className={`btn btn-${variant} ${className}`} onClick={onClick} {...props}>
      {leftIcon && <span className="button-icon left-icon">{leftIcon}</span>}
      {children}
      {rightIcon && <span className="button-icon right-icon">{rightIcon}</span>}
    </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;
  display: flex;
  align-items: center;
  justify-content: center;

  &-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%;
  }

  .button-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    margin-right: 0.5rem;

    &.right-icon {
      margin-left: 0.5rem;
      margin-right: 0;
    }
  }
}





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;
          }
        }
      }
    }
  }
}






// frontend/src/components/KyrgyzstanMap.jsx

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

// Replace with your actual Mapbox access token
mapboxgl.accessToken = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
const KyrgyzstanMap = () => {
  const mapContainer = useRef(null);
  const map = useRef(null);

  // State to track if the map has loaded
  const [mapLoaded, setMapLoaded] = useState(false);

  // State to store selected feature info
  const [selectedFeature, setSelectedFeature] = useState(null);

  // State to store hovered feature
  const [hoveredFeature, setHoveredFeature] = useState(null);

  // State to control layer visibility
  const [layersVisibility, setLayersVisibility] = useState({
    'admin_boundaries-fill': true,
    'admin_boundaries-outline': true,
    'mountains': true,
    'rivers': true,
    'waterbodies-fill': true,
    'cities-symbol': true,
  });

  useEffect(() => {
    if (map.current) return; // Initialize map only once

    // Initialize the map
    map.current = new mapboxgl.Map({
      container: mapContainer.current,
      style: 'mapbox://styles/mapbox/streets-v12', // Choose your preferred style
      center: [74.7661, 41.2044], // Coordinates of Bishkek
      zoom: 7,
    });

    // Add navigation controls
    map.current.addControl(new mapboxgl.NavigationControl());

    // Handle map load event
    map.current.on('load', () => {
      // Load GeoJSON sources
      const layers = ['admin_boundaries', 'mountains', 'rivers', 'waterbodies', 'cities'];

      layers.forEach(layer => {
        map.current.addSource(layer, {
          type: 'geojson',
          data: `/geo/${layer}`, // Ensure these endpoints serve the correct GeoJSON data
        });
      });

      // Add layers with specific styles

      // Administrative Boundaries
      map.current.addLayer({
        id: 'admin_boundaries-fill',
        type: 'fill',
        source: 'admin_boundaries',
        paint: {
          'fill-color': '#627BC1',
          'fill-opacity': 0.1,
        },
      });

      map.current.addLayer({
        id: 'admin_boundaries-outline',
        type: 'line',
        source: 'admin_boundaries',
        paint: {
          'line-color': '#627BC1',
          'line-width': 2,
        },
      });

      // Mountains
      map.current.addLayer({
        id: 'mountains',
        type: 'circle',
        source: 'mountains',
        paint: {
          'circle-radius': 6,
          'circle-color': '#B8860B',
          'circle-stroke-color': '#FFA500',
          'circle-stroke-width': 2,
        },
      });

      // Rivers
      map.current.addLayer({
        id: 'rivers',
        type: 'line',
        source: 'rivers',
        layout: {
          'line-join': 'round',
          'line-cap': 'round',
        },
        paint: {
          'line-color': '#00BFFF',
          'line-width': 3,
        },
      });

      // Water Bodies
      map.current.addLayer({
        id: 'waterbodies-fill',
        type: 'fill',
        source: 'waterbodies',
        paint: {
          'fill-color': '#1E90FF',
          'fill-opacity': 0.5,
        },
      });

      // Cities
      map.current.addLayer({
        id: 'cities-symbol',
        type: 'symbol',
        source: 'cities',
        layout: {
          'icon-image': 'harbor-15', // Using a built-in icon; customize as needed
          'icon-size': 1.5,
          'text-field': ['get', 'name'],
          'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'],
          'text-offset': [0, 1.25],
          'text-anchor': 'top',
        },
        paint: {
          'text-color': '#000000',
          'text-halo-color': '#FFFFFF',
          'text-halo-width': 1,
        },
      });

      // Indicate that the map has loaded
      setMapLoaded(true);

      // Setup interactivity after layers are added
      setupInteractivity();
    });

    // Cleanup on unmount
    return () => map.current.remove();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    if (!mapLoaded) return; // Ensure the map is loaded before manipulating layers

    // Toggle layer visibility based on state
    Object.keys(layersVisibility).forEach(layer => {
      if (map.current.getLayer(layer)) {
        map.current.setLayoutProperty(
          layer,
          'visibility',
          layersVisibility[layer] ? 'visible' : 'none'
        );
      } else {
        console.warn(`Layer '${layer}' does not exist on the map.`);
      }
    });
  }, [layersVisibility, mapLoaded]);

  const setupInteractivity = () => {
    // Initialize the popup
    const popup = new mapboxgl.Popup({
      closeButton: true,
      closeOnClick: true,
    });

    // Click event to show popup and set selected feature
    map.current.on('click', ['mountains', 'rivers', 'waterbodies-fill', 'cities-symbol'], (e) => {
      const feature = e.features[0];
      const coordinates = e.lngLat;
      let description = '';

      switch (feature.layer.id) {
        case 'mountains':
          description = `<strong>Гора:</strong> ${feature.properties.name}<br/><strong>Высота:</strong> ${feature.properties.height} м`;
          break;
        case 'rivers':
          description = `<strong>Река:</strong> ${feature.properties.name}<br/><strong>Протяженность:</strong> ${feature.properties.length} км`;
          break;
        case 'waterbodies-fill':
          description = `<strong>Водоем:</strong> ${feature.properties.name}<br/><strong>Тип:</strong> ${feature.properties.type}`;
          break;
        case 'cities-symbol':
          description = `<strong>Город:</strong> ${feature.properties.name}<br/><strong>Население:</strong> ${feature.properties.population}`;
          break;
        default:
          description = 'Нет информации';
      }

      // Set popup content and position
      popup.setLngLat(coordinates).setHTML(description).addTo(map.current);

      // Set selected feature for additional functionalities if needed
      setSelectedFeature(feature.properties);
    });

    // Change the cursor to pointer on hover
    map.current.on('mouseenter', ['mountains', 'rivers', 'waterbodies-fill', 'cities-symbol'], () => {
      map.current.getCanvas().style.cursor = 'pointer';
    });

    map.current.on('mouseleave', ['mountains', 'rivers', 'waterbodies-fill', 'cities-symbol'], () => {
      map.current.getCanvas().style.cursor = '';
    });

    // Hover event to show feature info
    map.current.on('mousemove', ['mountains', 'rivers', 'waterbodies-fill', 'cities-symbol'], (e) => {
      const feature = e.features[0];
      setHoveredFeature(feature.properties);
    });

    // Remove hover info when not hovering
    map.current.on('mouseleave', ['mountains', 'rivers', 'waterbodies-fill', 'cities-symbol'], () => {
      setHoveredFeature(null);
    });
  };

  const toggleLayer = (layerName) => {
    setLayersVisibility(prev => ({
      ...prev,
      [layerName]: !prev[layerName],
    }));
  };

  return (
    <div className="kyrgyzstan-map">
      <div ref={mapContainer} className="map-box" />

      {/* Layer Controls */}
      <div className="layer-controls">
        {Object.keys(layersVisibility).map(layer => (
          <div key={layer} className="layer-control">
            <input
              type="checkbox"
              id={`toggle-${layer}`}
              checked={layersVisibility[layer]}
              onChange={() => toggleLayer(layer)}
            />
            <label htmlFor={`toggle-${layer}`}>
              {layer
                .replace('_fill', '')
                .replace('_outline', '')
                .replace('_symbol', '')
                .replace('_', ' ')
                .replace(/\b\w/g, l => l.toUpperCase())}
            </label>
          </div>
        ))}
      </div>

      {/* Selected Feature Info Panel */}
      {selectedFeature && (
        <div className="info-panel">
          <h3>{selectedFeature.name || 'Информация'}</h3>
          <p>{selectedFeature.description || 'Дополнительная информация доступна.'}</p>
          <button onClick={() => setSelectedFeature(null)}>Закрыть</button>
        </div>
      )}

      {/* Hovered Feature Tooltip */}
      {hoveredFeature && (
        <div className="hover-info">
          <p>{hoveredFeature.name}</p>
        </div>
      )}
    </div>
  );
};

export default KyrgyzstanMap;





// frontend/src/components/KyrgyzstanMap.scss

.kyrgyzstan-map {
  position: relative;
  width: 100%;
  height: 100vh;
  background-color: #f0f0f0;
  z-index: 1; // Добавлено

  .map-box {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 100%;
    height: 100%;
    border: 2px solid red;
    background-color: #e0e0e0;
    z-index: 1;
  }

  
    /* Layer Controls Styling */
    .layer-controls {
      position: absolute;
      top: 10px;
      right: 10px;
      background-color: rgba(255, 255, 255, 0.95);
      padding: 1rem;
      border-radius: 0.5rem;
      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
      z-index: 2;
      display: flex;
      flex-direction: column;
      gap: 0.5rem;
  
      .layer-control {
        display: flex;
        align-items: center;
        gap: 0.5rem;
  
        input[type="checkbox"] {
          width: 16px;
          height: 16px;
        }
  
        label {
          font-size: 0.875rem;
          color: #1f2937;
        }
      }
    }
  
    /* Info Panel Styling */
    .info-panel {
      position: absolute;
      bottom: 20px;
      left: 20px;
      background-color: rgba(255, 255, 255, 0.95);
      padding: 1rem;
      border-radius: 0.5rem;
      max-width: 300px;
      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
      z-index: 1;
  
      h3 {
        margin-bottom: 0.5rem;
        color: #1f2937;
      }
  
      p {
        color: #374151;
        font-size: 0.9rem;
        margin-bottom: 0.5rem;
      }
  
      button {
        background-color: #e53e3e; // Red color for the close button
        color: #ffffff;
        border: none;
        padding: 0.5rem 1rem;
        border-radius: 0.25rem;
        cursor: pointer;
        transition: background-color 0.3s ease;
        
        &:hover {
          background-color: #c53030;
        }
      }
    }
  
    /* Hover Info Styling */
    .hover-info {
      position: absolute;
      top: 10px;
      left: 50%;
      transform: translateX(-50%);
      background-color: rgba(255, 255, 255, 0.8);
      padding: 0.5rem 1rem;
      border-radius: 0.25rem;
      font-size: 0.9rem;
      color: #1f2937;
      pointer-events: none;
      z-index: 1;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
    }
  
    /* Responsive Design */
    @media (max-width: 768px) {
      .layer-controls {
        top: auto;
        bottom: 10px;
        right: 10px;
        flex-direction: row;
        flex-wrap: wrap;
        gap: 0.25rem;
      }
  
      .info-panel {
        max-width: 90%;
        left: 50%;
        transform: translateX(-50%);
      }
  
      .hover-info {
        font-size: 0.8rem;
      }
    }
  }







// src/Map/MapPage.jsx

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


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;

    const mapOptions = {
      container: mapContainer.current,
      style: 'mapbox://styles/mapbox/satellite-streets-v12', // Стиль карты
      center: [0, 0], // Начальная позиция [долгота, широта]
      zoom: 1, // Начальный зум
      projection: is3D ? 'globe' : 'mercator', // 3D глобус или плоская карта
    };

    try {
      map.current = new mapboxgl.Map(mapOptions);
      map.current.addControl(new mapboxgl.NavigationControl(), 'top-right');

      if (is3D) {
        map.current.on('style.load', () => {
          map.current.setFog({}); // Установка атмосферы
        });
      }
    } catch (error) {
      console.error('Ошибка при инициализации карты:', error);
    }
  }, [is3D]);

  // Инициализация карты при монтировании и при изменении is3D
  useEffect(() => {
    const removeMap = () => {
      if (map.current) {
        map.current.remove();
        map.current = null;
      }
    };

    initializeMap();

    // Очищаем карту при размонтировании или изменении is3D
    return () => {
      removeMap();
    };
  }, [initializeMap]);

  // Переключение между 2D и 3D
  const toggle3D = () => {
    setIs3D((prev) => !prev);
  };

  // Переключение панели управления
  const togglePanel = () => {
    setIsPanelOpen((prev) => !prev);
  };

  // Поиск стран через Mapbox Geocoding API
  const searchCountry = async (query) => {
    if (!query) {
      setSuggestions([]);
      return;
    }
    try {
      const response = await fetch(
        `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
          query
        )}.json?types=country&autocomplete=true&limit=5&access_token=${mapboxgl.accessToken}`
      );
      const data = await response.json();
      if (data.features) {
        setSuggestions(data.features);
      } else {
        setSuggestions([]);
      }
    } catch (error) {
      console.error('Ошибка при поиске страны:', error);
    }
  };

  // Обработка изменения ввода поиска
  const handleSearchChange = (e) => {
    const value = e.target.value;
    setSearchQuery(value);
    searchCountry(value);
  };

  // Выбор страны из подсказок
  const selectCountry = (country) => {
    const [lng, lat] = country.center;
    if (map.current) {
      map.current.flyTo({
        center: [lng, lat],
        zoom: 5,
        essential: true, // Обязательная анимация
      });
  
      if (country.bbox) {
        map.current.fitBounds(country.bbox, { padding: 50 });
      }
    }

    setCountryInfo(country);
    setSuggestions([]);
  };

  // Смена стиля карты
  const addLayer = (layerStyle) => {
    const styleMap = {
      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 (styleMap[layerStyle] && map.current) {
      map.current.setStyle(styleMap[layerStyle]);

      if (is3D) {
        map.current.once('style.load', () => {
          map.current.setFog({});
        });
      }
    }
  };

  // Обновляем карту при изменении is3D
  useEffect(() => {
    // Если карта уже инициализирована, обновляем ее стиль
    if (map.current) {
      map.current.remove();
      map.current = null;
      initializeMap();
    }
  }, [is3D, initializeMap]);

  return (
    <div className="map-page">
      <Header user={user} title="Карта мира" />
      <main className="map-main">
        <div ref={mapContainer} className="map-container" />
        <div className="controls">
          <button onClick={toggle3D} className="toggle-button">
            {is3D ? '2D Карта' : '3D Глобус'}
          </button>
          <button onClick={togglePanel} className="toggle-button">
            {isPanelOpen ? 'Закрыть Панель' : 'Открыть Панель'}
          </button>
        </div>
        {isPanelOpen && (
          <div className="control-panel">
            <div className="search-section">
              <input
                type="text"
                value={searchQuery}
                onChange={handleSearchChange}
                placeholder="Поиск страны..."
              />
              {suggestions.length > 0 && (
                <ul className="suggestions">
                  {suggestions.map((s) => (
                    <li key={s.id} onClick={() => selectCountry(s)}>
                      {s.place_name}
                    </li>
                  ))}
                </ul>
              )}
            </div>
            <div className="layer-section">
              <h3>Стили карты</h3>
              <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>
            {countryInfo && (
              <div className="country-info">
                <h4>{countryInfo.place_name}</h4>
                <p>Координаты: {countryInfo.center.join(', ')}</p>
              </div>
            )}
          </div>
        )}
      </main>
    </div>
  );
};

export default MapPage;







// src/Map/MapPage.scss

@import '../styles/variables.scss';
@import '../Button/Button.scss'; // Предполагается, что Button.scss содержит необходимые миксины

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

  .map-main {
    position: relative;
    flex-grow: 1;

    .map-container {
      position: absolute;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      height: 100%;
      width: 100%;
    }

    .controls {
      position: absolute;
      top: 10px;
      left: 10px;
      display: flex;
      flex-direction: column;
      gap: 0.5rem;
      z-index: 1;

      .toggle-button {
        @include button-primary;
        padding: 0.5rem 1rem;
        font-size: 0.875rem;
      }
    }

    .control-panel {
      position: absolute;
      top: 60px;
      left: 10px;
      background-color: $light-background;
      border-radius: 0.5rem;
      padding: 1rem;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      z-index: 1;
      width: 250px;
      max-height: 80vh;
      overflow-y: auto;

      .search-section {
        margin-bottom: 1rem;

        input {
          width: 100%;
          padding: 0.5rem;
          border: 1px solid #d1d5db;
          border-radius: 0.375rem;
          font-size: 1rem;
          box-sizing: border-box;
        }

        .suggestions {
          list-style: none;
          margin-top: 0.5rem;
          max-height: 150px;
          overflow-y: auto;
          padding: 0;
          background-color: #ffffff;
          border: 1px solid #d1d5db;
          border-radius: 0.375rem;

          li {
            padding: 0.5rem;
            cursor: pointer;
            &:hover {
              background-color: lighten($background-color, 10%);
            }
          }
        }
      }

      .layer-section {
        margin-bottom: 1rem;

        h3 {
          font-size: 1.125rem;
          margin-bottom: 0.5rem;
          color: $heading-color;
        }

        button {
          @include button-secondary;
          display: block;
          width: 100%;
          margin-bottom: 0.5rem;
          font-size: 0.875rem;
        }
      }

      .country-info {
        margin-top: 1rem;

        h4 {
          font-size: 1rem;
          margin-bottom: 0.25rem;
          color: $heading-color;
        }

        p {
          font-size: 0.875rem;
          color: $text-color;
        }
      }
    }
  }
}






global.scss:
@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;
}






variables.scss:
// Цветовая палитра
$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);
}







// src/TestPages/CreateTestPage.jsx

import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import Button from '../Button/Button';
import './CreateTestPage.scss';
import Header from '../Header/Header';
import Modal from '../Modal'; // Родительский компонент для модальных окон

const CreateTestPage = ({ user }) => {
  const navigate = useNavigate();
  const [title, setTitle] = useState('');
  const [templates, setTemplates] = useState([]);
  const [selectedTemplateIds, setSelectedTemplateIds] = useState([]);
  const [error, setError] = useState('');
  const [showTemplateInfo, setShowTemplateInfo] = useState(false);
  const [currentTemplateInfo, setCurrentTemplateInfo] = useState(null);

  useEffect(() => {
    // Fetch available templates from the server
    const fetchTemplates = async () => {
      try {
        const response = await fetch('http://localhost:3001/tests/templates', {
          credentials: 'include'
        });
        if (response.ok) {
          const data = await response.json();
          if (Array.isArray(data.templates)) {
            setTemplates(data.templates);
          } else {
            console.error('Unexpected format for templates:', data);
            setTemplates([]);
          }
        } else {
          console.error('Не удалось получить шаблоны:', response.status);
          setTemplates([]);
        }
      } catch (err) {
        console.error('Ошибка при получении шаблонов:', err);
        setTemplates([]);
      }
    };

    fetchTemplates();
  }, []);

  const handleTemplateSelect = (templateId) => {
    if (selectedTemplateIds.includes(templateId)) {
      setSelectedTemplateIds(selectedTemplateIds.filter(id => id !== templateId));
    } else {
      if (selectedTemplateIds.length >= 10) {
        alert('Максимум 10 вопросов в тесте.');
        return;
      }
      setSelectedTemplateIds([...selectedTemplateIds, templateId]);
    }
  };

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

    if (!title.trim()) {
      setError('Пожалуйста, введите название теста.');
      return;
    }

    if (selectedTemplateIds.length < 1 || selectedTemplateIds.length > 10) {
      setError('Тест должен содержать от 1 до 10 вопросов.');
      return;
    }

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

      const result = await response.json();

      if (response.ok) {
        alert('Тест успешно создан!');
        navigate('/tests');
      } else {
        setError(result.message || 'Ошибка при создании теста');
      }
    } catch (error) {
      console.error('Ошибка при создании теста:', error);
      setError('Ошибка при создании теста. Попробуйте снова.');
    }
  };

  const openTemplateInfo = (template) => {
    setCurrentTemplateInfo(template);
    setShowTemplateInfo(true);
  };

  const closeTemplateInfo = () => {
    setShowTemplateInfo(false);
    setCurrentTemplateInfo(null);
  };

  return (
    <div className="create-test-page">
      <Header user={user} title="Создание теста" />
      <main>
        <form onSubmit={handleSubmit} className="create-test-form">
          <h2>Создать новый тест</h2>
          {error && <p className="error-message">{error}</p>}
          <input
            type="text"
            placeholder="Название теста"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            required
          />
          <div className="templates-section">
            <h3>Выберите шаблоны вопросов (от 1 до 10):</h3>
            {templates.length > 0 ? (
              <div className="templates-list">
                {templates.map(template => (
                  <div key={template.id} className={`template-item ${selectedTemplateIds.includes(template.id) ? 'selected' : ''}`}>
                    <input
                      type="checkbox"
                      id={`template-${template.id}`}
                      checked={selectedTemplateIds.includes(template.id)}
                      onChange={() => handleTemplateSelect(template.id)}
                    />
                    <label htmlFor={`template-${template.id}`} className="template-label">
                      <span className="template-description">{template.description} ({template.type === 'multiple_choice' ? 'Выбор ответа' : 'Краткий ответ'})</span>
                    </label>
                    <button type="button" className="info-button" onClick={() => openTemplateInfo(template)}>
                      ℹ️
                    </button>
                  </div>
                ))}
              </div>
            ) : (
              <p>Загрузка шаблонов...</p>
            )}
          </div>
          <Button type="submit" variant="primary" className="submit-button">Создать тест</Button>
        </form>
      </main>
      {showTemplateInfo && currentTemplateInfo && (
        <Modal onClose={closeTemplateInfo}>
          <div className="template-info">
            <h2>Информация о шаблоне</h2>
            <p><strong>Тип:</strong> {currentTemplateInfo.type === 'multiple_choice' ? 'Выбор ответа' : 'Краткий ответ'}</p>
            <p><strong>Описание:</strong> {currentTemplateInfo.description}</p>
            <p><strong>Вопрос:</strong> {currentTemplateInfo.question}</p>
            {currentTemplateInfo.type === 'multiple_choice' ? (
              <>
                <p><strong>Варианты ответов:</strong></p>
                <ul>
                  {currentTemplateInfo.options.map((option, idx) => (
                    <li key={idx}>{idx + 1}. {option}</li>
                  ))}
                </ul>
                <p><strong>Правильный ответ:</strong> {currentTemplateInfo.options[currentTemplateInfo.correctOption]}</p>
              </>
            ) : (
              <>
                <p><strong>Правильные ответы:</strong> {currentTemplateInfo.correctAnswers.join(', ')}</p>
                <p><strong>Баллы за правильный ответ:</strong> {currentTemplateInfo.pointsPerAnswer}</p>
              </>
            )}
          </div>
        </Modal>
      )}
    </div>
  );
};

export default CreateTestPage;






// src/TestPages/CreateTestPage.scss

@import '../styles/variables.scss';
@import '../Button/Button.scss'; // Предполагается, что Button.scss содержит необходимые миксины

.create-test-page {
  min-height: 100vh;
  background-color: $background-color;
  padding: $spacer;

  main {
    max-width: 800px;
    margin: 0 auto;

    .create-test-form {
      background-color: $light-background;
      padding: 2rem;
      border-radius: 0.5rem;
      box-shadow: 0 4px 6px rgba(0,0,0,0.1);
      display: flex;
      flex-direction: column;
      gap: 1.5rem;

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

      .error-message {
        color: $danger-color;
        text-align: center;
      }

      input[type="text"] {
        padding: 0.75rem;
        border: 1px solid #d1d5db;
        border-radius: 0.375rem;
        font-size: 1rem;
      }

      .templates-section {
        h3 {
          font-size: 1.25rem;
          color: $heading-color;
          margin-bottom: 0.75rem;
        }

        .templates-list {
          display: flex;
          flex-direction: column;
          gap: 0.75rem;

          .template-item {
            display: flex;
            align-items: center;
            gap: 0.5rem;
            padding: 0.5rem;
            border: 1px solid #d1d5db;
            border-radius: 0.375rem;
            transition: background-color 0.3s ease, border-color 0.3s ease;

            &.selected {
              background-color: lighten($primary-color, 40%);
              border-color: $primary-color;
            }

            input[type="checkbox"] {
              margin-right: 0.5rem;
              width: 1rem;
              height: 1rem;
            }

            .template-label {
              flex-grow: 1;
              font-size: 1rem;
              color: $text-color;
            }

            .info-button {
              background: none;
              border: none;
              cursor: pointer;
              font-size: 1rem;
              color: $primary-color;
              transition: color 0.3s ease;

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

      .submit-button {
        align-self: center;
        width: 50%;
        @media (max-width: 480px) {
          width: 100%;
        }
      }
    }

    .template-info {
      display: flex;
      flex-direction: column;
      gap: 1rem;

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

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

      ul {
        list-style: none;
        padding: 0;

        li {
          padding: 0.25rem 0;
          color: $text-color;
        }
      }
    }
  }
}







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

const ProgressBar = ({ progress }) => {
  return (
    <div className="progress-bar">
      <div
        className="progress-bar-filled"
        style={{ width: `${progress}%` }}
      ></div>
    </div>
  );
};

export default ProgressBar;





@import '../styles/variables';

.progress-bar {
  width: 100%;
  height: 10px;
  background-color: lighten($background-color, 10%);
  border-radius: 5px;
  overflow: hidden;
  margin-bottom: 1.5rem;
}

.progress-bar-filled {
  height: 100%;
  background-color: $primary-color;
  transition: width 0.3s ease;
}







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

const ResultModal = ({ children, onClose }) => {
  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div
        className="modal-content"
        onClick={(e) => e.stopPropagation()}
      >
        <button className="close-button" onClick={onClose}>
          ✕
        </button>
        {children}
      </div>
    </div>
  );
};

export default ResultModal;







@import '../styles/variables';

.modal-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: 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: 800px;
  box-shadow: 0 4px 6px rgba(0,0,0,0.1);
  position: relative;
  animation: slideDown 0.3s ease;

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

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

  .result-modal {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;

    h2 {
      font-size: 1.75rem;
      color: $heading-color;
    }

    p {
      font-size: 1.25rem;
      color: $text-color;
    }

    .detailed-results {
      width: 100%;
      max-height: 400px;
      overflow-y: auto;
      margin-bottom: 1.5rem;

      h3 {
        font-size: 1.25rem;
        color: $heading-color;
        margin-bottom: 0.75rem;
      }

      ul {
        list-style: none;
        padding: 0;

        li {
          padding: 1rem;
          border-bottom: 1px solid #d1d5db;
          color: $text-color;

          &:last-child {
            border-bottom: none;
          }

          strong {
            color: $heading-color;
          }
        }
      }
    }

    .close-result-button {
      width: 100%;
    }
  }
}

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

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






// src/TestPages/TakeTestPage.jsx

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

const TakeTestPage = ({ user }) => {
  const { id } = useParams();
  const navigate = useNavigate();
  const [test, setTest] = useState(null);
  const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
  const [userAnswers, setUserAnswers] = useState([]); // Array to store answers
  const [showResult, setShowResult] = useState(false);
  const [result, setResult] = useState(null);
  const [errorMessage, setErrorMessage] = useState('');
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetchTest = async () => {
      try {
        const response = await fetch(`http://localhost:3001/tests/${id}`, {
          credentials: 'include',
        });
        if (response.ok) {
          let testData = await response.json();
          
          // Проверка и парсинг questions
          if (typeof testData.questions === 'string') {
            try {
              testData.questions = JSON.parse(testData.questions);
            } catch (e) {
              console.error('Failed to parse questions:', e);
              testData.questions = [];
            }
          }

          // Проверка, что questions является массивом
          if (!Array.isArray(testData.questions)) {
            console.error('Questions is not an array:', testData.questions);
            testData.questions = [];
          }

          setTest(testData);
          setUserAnswers(Array(testData.questions.length).fill(null)); // Initialize with nulls
          setIsLoading(false);
        } else if (response.status === 401) {
          navigate('/');
        } else {
          console.error('Ошибка при получении теста:', response.status);
          alert('Ошибка при получении теста');
          setIsLoading(false);
        }
      } catch (error) {
        console.error('Error fetching test:', error);
        alert('Ошибка при получении теста');
        setIsLoading(false);
      }
    };

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

  const handleAnswerChange = (value) => {
    const updatedAnswers = [...userAnswers];
    updatedAnswers[currentQuestionIndex] = value;
    setUserAnswers(updatedAnswers);
  };

  const handleOptionSelect = (optionIndex) => {
    handleAnswerChange(optionIndex);
  };

  const handleInputChange = (e) => {
    handleAnswerChange(e.target.value);
  };

  const goToQuestion = (index) => {
    setCurrentQuestionIndex(index);
  };

  const handleFinishTest = async () => {
    // Validate that all questions have been answered
    for (let i = 0; i < userAnswers.length; i++) {
      if (userAnswers[i] === null || userAnswers[i] === '') {
        alert(`Пожалуйста, ответьте на все вопросы. Пропущен вопрос ${i + 1}.`);
        setCurrentQuestionIndex(i);
        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',
      });

      const resultData = await response.json();

      if (response.ok) {
        setResult(resultData);
        setShowResult(true);
      } else {
        setErrorMessage(resultData.message || 'Ошибка при отправке теста');
      }
    } catch (error) {
      setErrorMessage('Ошибка при отправке теста');
      console.error('Error submitting test:', error);
    }
  };

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

  if (!test) {
    return (
      <div className="take-test-page error">
        <p>Тест не найден.</p>
      </div>
    );
  }

  const currentQuestion = test.questions[currentQuestionIndex];
  const progressPercentage = ((currentQuestionIndex + 1) / test.questions.length) * 100;

  return (
    <div className="take-test-page">
      <Header user={user} title={test.title} />
      <main className="test-main">
        <ProgressBar progress={progressPercentage} />
        <div className="question-navigation">
          {test.questions.map((q, index) => (
            <button
              key={index}
              className={`nav-button ${index === currentQuestionIndex ? 'active' : ''}`}
              onClick={() => goToQuestion(index)}
            >
              {index + 1}
            </button>
          ))}
        </div>
        <div className="question-section">
          <h2>Вопрос {currentQuestionIndex + 1} из {test.questions.length}</h2>
          <p className="question-text">{currentQuestion.question}</p>
          {currentQuestion.type === 'multiple_choice' ? (
            <div className="options">
              {currentQuestion.options.map((option, idx) => (
                <div key={idx} className="option-item">
                  <input
                    type="radio"
                    id={`option-${idx}`}
                    name={`question-${currentQuestionIndex}`}
                    value={idx}
                    checked={userAnswers[currentQuestionIndex] === idx}
                    onChange={() => handleOptionSelect(idx)}
                  />
                  <label htmlFor={`option-${idx}`}>{option}</label>
                </div>
              ))}
            </div>
          ) : (
            <div className="short-answer">
              <input
                type="text"
                value={userAnswers[currentQuestionIndex] || ''}
                onChange={handleInputChange}
                placeholder="Введите ваш ответ"
              />
            </div>
          )}
        </div>
        <div className="test-controls">
          {currentQuestionIndex > 0 && (
            <Button variant="secondary" onClick={() => goToQuestion(currentQuestionIndex - 1)}>
              Назад
            </Button>
          )}
          {currentQuestionIndex < test.questions.length - 1 && (
            <Button variant="primary" onClick={() => goToQuestion(currentQuestionIndex + 1)}>
              Далее
            </Button>
          )}
          {currentQuestionIndex === test.questions.length - 1 && (
            <Button variant="danger" onClick={handleFinishTest}>
              Завершить тест
            </Button>
          )}
        </div>
        {errorMessage && <p className="error-message">{errorMessage}</p>}
      </main>
      {showResult && result && (
        <ResultModal onClose={() => navigate('/tests')}>
          <div className="result-modal">
            <h2>Результаты теста</h2>
            <p>{result.success ? 'Поздравляем! Вы успешно прошли тест.' : 'К сожалению, ваши ответы не полностью верны. Попробуйте снова.'}</p>
            <p>Ваш счёт: {result.totalScore} из {result.maxScore} ({result.percentageScore})</p>
            <div className="detailed-results">
              <h3>Подробности:</h3>
              <ul>
                {result.detailedResults.map((res, idx) => (
                  <li key={idx}>
                    <strong>Вопрос {idx + 1}:</strong> {res.question}<br />
                    <strong>Ваш ответ:</strong> {res.type === 'multiple_choice' ? (res.userAnswer !== null ? test.questions[idx].options[res.userAnswer] : 'Нет ответа') : res.userAnswer}<br />
                    <strong>{res.isCorrect ? 'Правильно!' : 'Неправильно.'}</strong><br />
                    <strong>Баллы:</strong> {res.earnedPoints} из {res.maxPoints}
                  </li>
                ))}
              </ul>
            </div>
            <Button variant="primary" onClick={() => navigate('/tests')} className="close-result-button">Закрыть</Button>
          </div>
        </ResultModal>
      )}
    </div>
  );
};

export default TakeTestPage;







@import '../styles/variables.scss';
@import '../Button/Button.scss'; // Предполагается, что Button.scss содержит необходимые миксины

.take-test-page {
  min-height: 100vh;
  background-color: $background-color;
  display: flex;
  flex-direction: column;

  .test-main {
    flex-grow: 1;
    max-width: 800px;
    margin: 2rem auto;
    padding: 1.5rem;
    background-color: $light-background;
    border-radius: 0.5rem;
    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
    display: flex;
    flex-direction: column;
    gap: 1.5rem;

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

      .nav-button {
        width: 2rem;
        height: 2rem;
        border: none;
        border-radius: 50%;
        background-color: #d1d5db;
        color: #374151;
        cursor: pointer;
        font-weight: bold;
        @include transition(background-color 0.3s ease, transform 0.3s ease);

        &.active {
          background-color: $primary-color;
          color: #ffffff;
          transform: scale(1.1);
        }

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

    .question-section {
      display: flex;
      flex-direction: column;
      gap: 1rem;

      h2 {
        font-size: 1.25rem;
        color: $heading-color;
      }

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

      .options {
        display: flex;
        flex-direction: column;
        gap: 0.5rem;

        .option-item {
          display: flex;
          align-items: center;

          input[type="radio"] {
            margin-right: 0.5rem;
          }

          label {
            font-size: 1rem;
            color: $text-color;
            cursor: pointer;
          }
        }
      }

      .short-answer {
        input[type="text"] {
          width: 100%;
          padding: 0.5rem;
          border: 1px solid #d1d5db;
          border-radius: 0.375rem;
          font-size: 1rem;
        }
      }
    }

    .test-controls {
      display: flex;
      justify-content: space-between;
      gap: 1rem;

      button {
        flex: 1;
      }
    }

    .error-message {
      color: $danger-color;
      text-align: center;
    }
  }

  .loading {
    font-size: 1.5rem;
    color: $text-color;
    text-align: center;
    margin-top: 2rem;
  }
}








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

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">
                <Button variant="primary" className="card-button">Создать</Button>
              </Link>
            </div>
          ) : (
            <>
              {userTests.map(test => (
                <div className="test-card" key={test.id}>
                  <h2>{test.title}</h2>
                  <p>Количество вопросов: {test.questions.length}</p>
                  <div className="test-buttons">
                    <Link to={`/take-test/${test.id}`}>
                      <Button variant="primary" className="card-button">Пройти тест</Button>
                    </Link>
                  </div>
                </div>
              ))}
              {/* Allow creating more tests if less than 10 exists */}
              {userTests.length < 10 && (
                <div className="test-card">
                  <h2>Создать свой тест</h2>
                  <Link to="/create-test">
                    <Button variant="primary" className="card-button">Создать</Button>
                  </Link>
                </div>
              )}
            </>
          )}
        </div>
      </main>
    </div>
  );
};

export default TestsPage;





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

.tests-page {
  min-height: 100vh;
  background-color: $background-color;

  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: $light-background;
        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;
        display: flex;
        flex-direction: column;
        gap: 1rem;

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

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

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

        .test-buttons {
          display: flex;
          justify-content: center;
          gap: 1rem;
        }

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

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

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






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 FlagQuiz from './FlagQuiz/FlagQuiz';
import KyrgyzstanMap from './KGMap/KyrgyzstanMap';

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={<FlagQuiz />} 
        />
        <Route
          path="/kyrgyzstan/map"
          element={user ? <KyrgyzstanMap /> : <Navigate to="/" />}
        />
        <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';
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: '🗺️' },
    { id: 4, title: 'Флаговой викторина', description: 'Угадывайте страны по их флагам', path: '/flag-quiz', 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;
        }
      }
    }
  }
}






// src/Modal.jsx

import React, { useState } from 'react';
import './Modal.scss';
import Button from './Button/Button';
import { Link } from 'react-router-dom'; // Импортируем Link

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" variant="primary">Войти</Button>
        </form>
        <div className="register-link">
          <p>Еще нет аккаунта? <Link to="/register" onClick={onClose}>Зарегистрируйтесь</Link></p>
        </div>
      </div>
    </div>
  );
};

export default Modal;





// src/Modal.scss

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

.modal-backdrop {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: 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 {
      align-self: center;
    }
  }

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

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

  .register-link {
    margin-top: 1rem;
    text-align: center;

    p {
      font-size: 0.9rem;
      color: $text-color;

      a {
        color: $primary-color;
        text-decoration: none;
        font-weight: bold;
        @include transition(color 0.3s ease);

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

@keyframes fadeIn {
  from { background-color: rgba(0, 0, 0, 0); }
  to { background-color: 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;







@import '../styles/variables';

.public-page {
    min-height: 100vh;
    background-color: $background-color;
    display: flex;
    flex-direction: column;

    header {
      background-color: $light-background;
      padding: 1rem;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    }

    main {
      flex-grow: 1;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 2rem;

      .hero-section {
        text-align: center;
        margin-bottom: 2rem;

        h1 {
          font-size: 2rem;
          color: $heading-color;
          margin-bottom: 1rem;
        }

        p {
          font-size: 1.125rem;
          color: $text-color;
        }
      }

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

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

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

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

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






бэкенд:
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,
}));





// backend/src/geo/geo.controller.ts

import { Controller, Get, Param } from '@nestjs/common';
import { GeoService } from './geo.service';

@Controller('geo')
export class GeoController {
  constructor(private readonly geoService: GeoService) {}

  @Get(':layer')
  async getLayer(@Param('layer') layer: string) {
    return this.geoService.getGeoData(layer);
  }

  @Get('details/:id')
  async getFeatureDetails(@Param('id') id: string) {
    return this.geoService.getFeatureDetails(id);
  }
}






// backend/src/geo/geo.module.ts

import { Module } from '@nestjs/common';
import { GeoService } from './geo.service';
import { GeoController } from './geo.controller';

@Module({
  providers: [GeoService],
  controllers: [GeoController],
})
export class GeoModule {}






// backend/src/geo/geo.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';

@Injectable()
export class GeoService {
  private readonly dataPath = path.join(__dirname, '..', '..', 'frontend', 'public', 'data', 'kyrgyzstan');

  async getGeoData(layer: string): Promise<any> {
    const filePath = path.join(this.dataPath, `${layer}.geojson`);
    if (!fs.existsSync(filePath)) {
      throw new NotFoundException(`Layer '${layer}' not found`);
    }

    const data = fs.readFileSync(filePath, 'utf-8');
    return JSON.parse(data);
  }

  async getFeatureDetails(id: string): Promise<any> {
    const layers = ['admin_boundaries', 'mountains', 'rivers', 'waterbodies', 'cities'];

    for (const layer of layers) {
      const data = await this.getGeoData(layer);
      const feature = data.features.find((f: any) => f.properties.id === id);
      if (feature) {
        return feature.properties;
      }
    }

    throw new NotFoundException(`Feature with id '${id}' not found`);
  }
}





export interface TestResult {
    success: boolean;
    totalScore: number;
    maxScore: number;
    percentageScore: string; // e.g., "70.00%"
    detailedResults: DetailedResult[];
  }
  
  export interface DetailedResult {
    question: string;
    type: string;
    userAnswer: any;
    isCorrect: boolean;
    earnedPoints: number;
    maxPoints: number;
  }







// src/testpages/test.controller.ts

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

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

  // Get available templates
  @Get('/templates')
  async getTemplates() {
    const templates = this.testService.getTemplatesDescriptions();
    console.log('Templates sent to frontend:', templates);
    return { templates };
  }

  // Create a test based on selected templates
  @UseGuards(AuthenticatedGuard)
  @Post('/create')
  async createTest(
    @Request() req,
    @Body() body: { title: string; selectedTemplateIds: string[] },
  ): Promise<{ test?: any; message?: string }> {
    const { title, selectedTemplateIds } = body;
    try {
      const newTest = await this.testService.createTest(req.user.userId, title, selectedTemplateIds);
      console.log('Test created:', newTest);
      return { test: newTest };
    } catch (error) {
      if (error instanceof BadRequestException) {
        return { message: error.message };
      } else {
        console.error('Unexpected error:', error);
        return { message: 'Произошла неизвестная ошибка' };
      }
    }
  }

  // Get user's tests
  @UseGuards(AuthenticatedGuard)
  @Get('/my-tests')
  async getUserTests(@Request() req): Promise<{ tests: any[]; testCount: number }> {
    const tests = await this.testService.findTestsByUser(req.user.userId);
    const testCount = await this.testService.countTestsByUser(req.user.userId);
    console.log(`Fetched ${testCount} tests for user ID ${req.user.userId}`);
    return { tests, testCount };
  }

  // Get a specific test by ID
  @Get('/:id')
  async getTest(@Param('id') id: string) {
    const test = await this.testService.findTestById(id);
    console.log('Fetched test:', test);
    return test;
  }

  // Submit answers to a test
  @UseGuards(AuthenticatedGuard)
  @Post('/:id/submit')
  async submitTest(
    @Param('id') id: string,
    @Body() body: { answers: any[] }
  ): Promise<TestResult> { // Use the TestResult interface
    console.log('Received request to submit test:', id);
    console.log('Request body:', JSON.stringify(body, null, 2));
    try {
      const result = await this.testService.checkTestAnswers(id, body.answers);
      console.log('Test submission result:', result);
      return result;
    } catch (error) {
      if (error instanceof BadRequestException) {
        throw new BadRequestException(error.message);
      } else {
        console.error('Unexpected error:', error);
        throw new BadRequestException('Произошла неизвестная ошибка');
      }
    }
  }
}





// src/testpages/test.model.ts

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;

  @Column(DataType.JSON)
  questions: any[]; // Убедитесь, что questions хранятся как массив

  @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 {}





// src/testpages/test.service.ts

import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Test } from './test.model';
import templatesData from '../data/templates.json'; // Проверьте корректность пути
import { TestResult, DetailedResult } from './response.interface'; // Импортируйте интерфейсы

interface TestTemplate {
  type: string;
  description: string;
  question: string;
  options?: string[];
  correctOption?: number;
  correctAnswers?: string[];
  pointsPerAnswer?: number;
}

interface Templates {
  [key: string]: TestTemplate;
}

@Injectable()
export class TestService {
  private templates: Templates = templatesData; // Инициализируем шаблоны из JSON
  
  constructor(
    @InjectModel(Test)
    private testModel: typeof Test,
  ) {
    console.log('Templates loaded:', this.templates); // Для отладки
  }

  getTemplatesDescriptions() {
    return Object.entries(this.templates).map(([id, template]) => ({
      id,
      description: template.description,
      type: template.type,
    }));
  }

  async createTest(userId: number, title: string, selectedTemplateIds: string[]) {
    if (selectedTemplateIds.length < 1 || selectedTemplateIds.length > 10) {
      throw new BadRequestException('Тест должен содержать от 1 до 10 вопросов.');
    }

    // Проверяем существование всех выбранных шаблонов
    const selectedTemplates: TestTemplate[] = [];
    for (const templateId of selectedTemplateIds) {
      const template = this.templates[templateId];
      if (!template) {
        throw new BadRequestException(`Шаблон с ID ${templateId} не существует.`);
      }
      selectedTemplates.push(template);
    }

    // Создание массива вопросов
    const questions = selectedTemplates.map(template => {
      if (template.type === 'multiple_choice') {
        return {
          type: 'multiple_choice',
          question: template.question,
          options: template.options,
          correctOption: template.correctOption,
          points: 1, // Назначение 1 балла за вопрос с выбором ответа
        };
      } else if (template.type === 'short_answer') {
        return {
          type: 'short_answer',
          question: template.question,
          correctAnswers: template.correctAnswers,
          points: template.pointsPerAnswer || 1, // Баллы могут варьироваться
        };
      } else {
        throw new BadRequestException(`Неизвестный тип шаблона: ${template.type}`);
      }
    });

    // Добавляем логирование для проверки структуры вопросов
    console.log('Creating test with questions:', questions);

    return this.testModel.create({
      userId,
      title,
      questions: questions,
      timesCompleted: 0,
      averageScore: 0,
    });
  }

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

  async findAllTests() {
    const tests = await this.testModel.findAll();
    return tests.map(test => test.toJSON());
  }

  async findTestById(testId: string) {
    const test = await this.testModel.findByPk(testId);
    if (!test) {
      throw new NotFoundException('Тест не найден');
    }
    return test.toJSON();
  }

  async countTestsByUser(userId: number) {
    return this.testModel.count({ where: { userId } });
  }

  async checkTestAnswers(testId: string, userAnswers: any[]): Promise<TestResult> {
    const test = await this.testModel.findByPk(testId);
    if (!test) {
      throw new NotFoundException('Тест не найден');
    }

    const questions = test.questions;

    if (!questions || !Array.isArray(questions)) {
      throw new BadRequestException('Неправильный формат теста');
    }

    if (userAnswers.length !== questions.length) {
      throw new BadRequestException('Некорректное количество ответов');
    }

    let totalScore = 0;
    let maxScore = 0;
    const detailedResults: DetailedResult[] = [];

    for (let i = 0; i < questions.length; i++) {
      const q = questions[i];
      const userAnswer = userAnswers[i];
      let isCorrect = false;
      let earnedPoints = 0;

      if (q.type === 'multiple_choice') {
        if (userAnswer === q.correctOption) {
          isCorrect = true;
          earnedPoints = q.points;
        }
      } else if (q.type === 'short_answer') {
        const normalizedUserAnswer = typeof userAnswer === 'string' ? userAnswer.trim().toLowerCase() : '';
        const isMatch = Array.isArray(q.correctAnswers) && q.correctAnswers.some(ans => ans.toLowerCase() === normalizedUserAnswer);
        if (isMatch) {
          isCorrect = true;
          earnedPoints = q.points;
        }
      }

      totalScore += earnedPoints;
      maxScore += q.points;

      detailedResults.push({
        question: q.question,
        type: q.type,
        userAnswer: userAnswer,
        isCorrect: isCorrect,
        earnedPoints: earnedPoints,
        maxPoints: q.points,
      });
    }

    const rawPercentageScore = maxScore === 0 ? 0 : (totalScore / maxScore) * 100;
    const percentageScoreStr = rawPercentageScore.toFixed(2) + '%';
    const success = rawPercentageScore >= 70; // Проходной балл >= 70%

    // Обновление статистики
    test.timesCompleted += 1;
    test.averageScore = ((test.averageScore * (test.timesCompleted - 1)) + rawPercentageScore) / test.timesCompleted;
    await test.save();

    return {
      success: success,
      totalScore: totalScore,
      maxScore: maxScore,
      percentageScore: percentageScoreStr,
      detailedResults: detailedResults,
    };
  }
}








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;
  }
}






// backend/src/app.module.ts

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

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [databaseConfig],
      isGlobal: true,
    }),
    SequelizeModule.forRootAsync({
      imports: [ConfigModule],
      useClass: SequelizeConfigService,
      inject: [SequelizeConfigService],
    }),
    UsersModule,
    AuthModule,
    TestModule,
    GeoModule, // Add GeoModule here
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}





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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.use(
    session({
      secret: 'keyword', // Change this to a strong secret in production
      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', // Update if your frontend runs on a different origin
    credentials: true,
  });

  await app.listen(3001);
}
bootstrap();






нужно чтобы в странице карта Кыргызстана был такой же header как на других страницах а также панели управления как на странице карта мира только отличие от карты мира должно быть в том что на странице карта кыргызстана отображался только кыргызстан без других стран