фронтенд:
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 && {leftIcon}}
{children}
{rightIcon && {rightIcon}}

);
};

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 (



{children &&
{children}
}
{title}

{description}




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


Угадай страну по флагу

{currentQuestion && (
<>
Flag
Континент: {currentQuestion.continent}

{/* Отображаем континент */}

{currentQuestion.options.map((option) => (
<button key={option} onClick={() => handleAnswer(option)} className="option-button">
{option}

))}

{isCorrect === true &&
Правильно!

}
{isCorrect === false &&
Неправильно! Это {currentQuestion.correct.join(", ")}.

}
Ваш счёт: {score}


</>
)}

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



{/* Кнопка мобильного меню /}

☰ {/ Иконка гамбургера */}

    {/* Логотип */}
    <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/KGMap/KyrgyzstanMap.jsx

import React, { useRef, useEffect, useState } from 'react';
import mapboxgl from 'mapbox-gl';
import './KyrgyzstanMap.scss';
import 'mapbox-gl/dist/mapbox-gl.css';
import Header from '../Header/Header'; // Импортируем Header
import Button from '../Button/Button'; // Импортируем Button для возможных кнопок

// Замените на ваш реальный токен доступа Mapbox
mapboxgl.accessToken = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;

const KyrgyzstanMap = ({ user }) => {
const mapContainer = useRef(null);
const map = useRef(null);

// Состояния для управления картой
const [mapLoaded, setMapLoaded] = useState(false);
const [selectedFeature, setSelectedFeature] = useState(null);
const [hoveredFeature, setHoveredFeature] = useState(null);
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; // Инициализируем карту только один раз


// Инициализация карты, центрированной на Кыргызстане
map.current = new mapboxgl.Map({
  container: mapContainer.current,
  style: 'mapbox://styles/mapbox/streets-v12', // Выберите предпочтительный стиль
  center: [74.7661, 41.2044], // Координаты Бишкека
  zoom: 7,
});

// Добавление навигационных контролей
map.current.addControl(new mapboxgl.NavigationControl());

// Обработка события загрузки карты
map.current.on('load', () => {
  // Загрузка GeoJSON источников
  const layers = ['admin_boundaries', 'mountains', 'rivers', 'waterbodies', 'cities'];

  layers.forEach(layer => {
    map.current.addSource(layer, {
      type: 'geojson',
      data: `/geo/${layer}`, // Убедитесь, что эти эндпоинты обслуживают правильные GeoJSON данные
    });
  });

  // Добавление слоев с конкретными стилями

  // Административные границы
  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,
    },
  });

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

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

  // Водоемы
  map.current.addLayer({
    id: 'waterbodies-fill',
    type: 'fill',
    source: 'waterbodies',
    paint: {
      'fill-color': '#1E90FF',
      'fill-opacity': 0.5,
    },
  });

  // Города
  map.current.addLayer({
    id: 'cities-symbol',
    type: 'symbol',
    source: 'cities',
    layout: {
      'icon-image': 'harbor-15', // Использование встроенной иконки; измените по необходимости
      '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,
    },
  });

  // Указываем, что карта загружена
  setMapLoaded(true);

  // Настройка интерактивности после добавления слоев
  setupInteractivity();
});

// Очистка при размонтировании компонента
return () => map.current.remove();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
if (!mapLoaded) return; // Убедиться, что карта загружена перед манипуляциями


// Переключение видимости слоев на основе состояния
Object.keys(layersVisibility).forEach(layer => {
  if (map.current.getLayer(layer)) {
    map.current.setLayoutProperty(
      layer,
      'visibility',
      layersVisibility[layer] ? 'visible' : 'none'
    );
  } else {
    console.warn(`Layer '${layer}' не существует на карте.`);
  }
});
}, [layersVisibility, mapLoaded]);

const setupInteractivity = () => {
// Инициализация всплывающего окна
const popup = new mapboxgl.Popup({
closeButton: true,
closeOnClick: true,
});


// Обработка кликов по слоям
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 = 'Нет информации';
  }

  // Установка содержимого и позиции всплывающего окна
  popup.setLngLat(coordinates).setHTML(description).addTo(map.current);

  // Установка выбранного объекта для дополнительных функций, если необходимо
  setSelectedFeature(feature.properties);
});

// Изменение курсора на указатель при наведении
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 = '';
});

// Обработка наведения для отображения информации
map.current.on('mousemove', ['mountains', 'rivers', 'waterbodies-fill', 'cities-symbol'], (e) => {
  const feature = e.features[0];
  setHoveredFeature(feature.properties);
});

// Удаление информации при покидании области наведения
map.current.on('mouseleave', ['mountains', 'rivers', 'waterbodies-fill', 'cities-symbol'], () => {
  setHoveredFeature(null);
});
};

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

return (


{/* Интеграция Header */}

  <main className="map-main">
    <div ref={mapContainer} className="map-box" />

    {/* Панель управления слоями */}
    <div className="layer-controls">
      <h3>Управление слоями</h3>
      {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>

    {/* Панель информации о выбранном объекте */}
    {selectedFeature && (
      <div className="info-panel">
        <h3>Информация об объекте</h3>
        <p><strong>Название:</strong> {selectedFeature.name}</p>
        {selectedFeature.height && <p><strong>Высота:</strong> {selectedFeature.height} м</p>}
        {selectedFeature.length && <p><strong>Протяженность:</strong> {selectedFeature.length} км</p>}
        {selectedFeature.type && <p><strong>Тип:</strong> {selectedFeature.type}</p>}
        {selectedFeature.population && <p><strong>Население:</strong> {selectedFeature.population}</p>}
        <Button variant="danger" onClick={() => setSelectedFeature(null)}>Закрыть</Button>
      </div>
    )}

    {/* Всплывающая информация при наведении */}
    {hoveredFeature && (
      <div className="hover-info">
        <p>{hoveredFeature.name}</p>
      </div>
    )}
  </main>
</div>
);
};

export default KyrgyzstanMap;

// frontend/src/KGMap/KyrgyzstanMap.scss

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

.kyrgyzstan-map {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
background-color: $background-color;

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


.map-box {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
}

/* Панель управления слоями */
.layer-controls {
  position: absolute;
  top: 10px;
  left: 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;

  h3 {
    margin-bottom: 0.5rem;
    color: $heading-color;
    text-align: center;
  }

  .layer-control {
    display: flex;
    align-items: center;
    gap: 0.5rem;

    input[type="checkbox"] {
      width: 16px;
      height: 16px;
    }

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

/* Панель информации о выбранном объекте */
.info-panel {
  position: absolute;
  bottom: 20px;
  left: 20px;
  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;
  max-width: 300px;

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

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

  .btn-danger {
    width: 100%;
  }
}

/* Всплывающая информация при наведении */
.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: 2;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

/* Адаптивные стили */
@media (max-width: 768px) {
  .layer-controls {
    top: auto;
    bottom: 10px;
    left: 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 (







{is3D ? '2D Карта' : '3D Глобус'}


{isPanelOpen ? 'Закрыть Панель' : 'Открыть Панель'}


{isPanelOpen && (


{searchQuery}

{suggestions.length > 0 && (

{suggestions.map((s) => (
<li key={s.id} onClick={() => selectCountry(s)}>
{s.place_name}

))}

)}


Стили карты

<button onClick={() => addLayer('streets')}>Улицы
<button onClick={() => addLayer('satellite')}>Спутник
<button onClick={() => addLayer('light')}>Светлый
<button onClick={() => addLayer('dark')}>Темный
<button onClick={() => addLayer('outdoors')}>На открытом воздухе
<button onClick={() => addLayer('navigation-day')}>Навигация (день)
<button onClick={() => addLayer('navigation-night')}>Навигация (ночь)
<button onClick={() => addLayer('satellite-streets')}>Спутник + Улицы

{countryInfo && (

{countryInfo.place_name}

Координаты: {countryInfo.center.join(', ')}



)}

)}


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





Создать новый тест

{error &&
{error}

}
<input
type="text"
placeholder="Название теста"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>

Выберите шаблоны вопросов (от 1 до 10):

{templates.length > 0 ? (

{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">
{template.description} ({template.type === 'multiple_choice' ? 'Выбор ответа' : 'Краткий ответ'})

<button type="button" className="info-button" onClick={() => openTemplateInfo(template)}>
ℹ️


))}

) : (
Загрузка шаблонов...


)}

Создать тест


{showTemplateInfo && currentTemplateInfo && (


Информация о шаблоне

Тип: {currentTemplateInfo.type === 'multiple_choice' ? 'Выбор ответа' : 'Краткий ответ'}


Описание: {currentTemplateInfo.description}


Вопрос: {currentTemplateInfo.question}


{currentTemplateInfo.type === 'multiple_choice' ? (
<>
Варианты ответов:



{currentTemplateInfo.options.map((option, idx) => (
{idx + 1}. {option}

))}

Правильный ответ: {currentTemplateInfo.options[currentTemplateInfo.correctOption]}


</>
) : (
<>
Правильные ответы: {currentTemplateInfo.correctAnswers.join(', ')}


Баллы за правильный ответ: {currentTemplateInfo.pointsPerAnswer}


</>
)}


)}

);
};
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-filled"
style={{ width: ${progress}% }}
>


);
};
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-content"
onClick={(e) => e.stopPropagation()}
>

✕

{children}


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


Загрузка теста...



);
}
if (!test) {
return (


Тест не найден.



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

return (






{test.questions.map((q, index) => (
<button
key={index}
className={nav-button ${index === currentQuestionIndex ? 'active' : ''}}
onClick={() => goToQuestion(index)}
>
{index + 1}

))}


Вопрос {currentQuestionIndex + 1} из {test.questions.length}

{currentQuestion.question}


{currentQuestion.type === 'multiple_choice' ? (

{currentQuestion.options.map((option, idx) => (

<input
type="radio"
id={option-${idx}}
name={question-${currentQuestionIndex}}
value={idx}
checked={userAnswers[currentQuestionIndex] === idx}
onChange={() => handleOptionSelect(idx)}
/>
<label htmlFor={option-${idx}}>{option}

))}

) : (

<input
type="text"
value={userAnswers[currentQuestionIndex] || ''}
onChange={handleInputChange}
placeholder="Введите ваш ответ"
/>

)}


{currentQuestionIndex > 0 && (
<Button variant="secondary" onClick={() => goToQuestion(currentQuestionIndex - 1)}>
Назад

)}
{currentQuestionIndex < test.questions.length - 1 && (
<Button variant="primary" onClick={() => goToQuestion(currentQuestionIndex + 1)}>
Далее

)}
{currentQuestionIndex === test.questions.length - 1 && (

Завершить тест

)}

{errorMessage &&
{errorMessage}

}

{showResult && result && (
<ResultModal onClose={() => navigate('/tests')}>

Результаты теста

{result.success ? 'Поздравляем! Вы успешно прошли тест.' : 'К сожалению, ваши ответы не полностью верны. Попробуйте снова.'}


Ваш счёт: {result.totalScore} из {result.maxScore} ({result.percentageScore})



Подробности:


{result.detailedResults.map((res, idx) => (

Вопрос {idx + 1}: {res.question}

Ваш ответ: {res.type === 'multiple_choice' ? (res.userAnswer !== null ? test.questions[idx].options[res.userAnswer] : 'Нет ответа') : res.userAnswer}

{res.isCorrect ? 'Правильно!' : 'Неправильно.'}

Баллы: {res.earnedPoints} из {res.maxPoints}

))}


<Button variant="primary" onClick={() => navigate('/tests')} className="close-result-button">Закрыть


)}

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





{userTests.length === 0 ? (

Создать свой тест


Создать


) : (
<>
{userTests.map(test => (

{test.title}

Количество вопросов: {test.questions.length}



<Link to={/take-test/${test.id}}>
Пройти тест



))}
{/* Allow creating more tests if less than 10 exists */}
{userTests.length < 10 && (

Создать свой тест


Создать


)}
</>
)}



);
};
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() : '?'}

);
}

return (
{alt}
);
};

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 (



Регистрация

{error &&
{error}

}

<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 onClick={() => navigate('/')} variant="outline" className="login-link">
Уже есть аккаунт? Войти



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




<Avatar src={/avatars/${user.avatarId}.png} alt={user.username} className="user-avatar" />

Выберите аватар:


{[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)}
/>
))}

{isAvatarChanged && (
<Button
variant="secondary"
onClick={handleSaveAvatar}
className="save-avatar-button"
leftIcon={}
>
Сохранить аватар

)}



Профиль

{isEditing ? (
<>

Имя пользователя:
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>


Email:
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>


<Button
variant="secondary"
onClick={handleSaveProfile}
className="save-button"
leftIcon={}
>
Сохранить

<Button
variant="danger"
onClick={handleCancelEdit}
className="cancel-button"
leftIcon={}
>
Отмена


</>
) : (
<>
Имя пользователя: {user.username}


Email: {user.email}



<Button
variant="primary"
onClick={handleEditProfile}
className="edit-button"
leftIcon={}
>
Редактировать профиль

<Button
variant="danger"
onClick={handleLogout}
className="logout-button"
leftIcon={}
>
Выйти


</>
)}

<Button variant="primary" leftIcon={}>
На главную





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


<Route
path="/"
element={}
/>
<Route
path="/register"
element={}
/>
<Route
path="/user"
element={user ? : }
/>
<Route
path="/tests"
element={user ? : }
/>
<Route
path="/create-test"
element={user ? : }
/>
<Route
path="/take-test/:id"
element={user ? : }
/>
<Route
path="/map"
element={user ? : }
/>
<Route
path="/flag-quiz"
element={}
/>
<Route
path="/kyrgyzstan/map"
element={user ? : }
/>
<Route path="*" element={} />


);
}

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 (





Добро пожаловать на образовательную платформу!

{user ? 'Выберите интересующий вас раздел:' : 'Авторизуйтесь, чтобы получить доступ ко всем функциям:'}




{cards.map((card) => (
<Card key={card.id} title={card.title} description={card.description} onClick={() => handleCardClick(card)}>
{card.icon}

))}


{showLoginModal && (
<Modal
onClose={() => setShowLoginModal(false)}
onLogin={handleLoginSuccess}
/>
)}

);
};
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-content" onClick={(e) => e.stopPropagation()}>
✕
Авторизация

{error &&
{error}

}

<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
/>
Войти


Еще нет аккаунта? Зарегистрируйтесь





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




Название сайта
Вход



Добро пожаловать!

Чтобы получить доступ к полному функционалу, авторизуйтесь.



{cards.map((card) => (
<div key={card.id} className="card" onClick={() => handleCardClick(card)}>
{card.title}

{card.description}



))}


  {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 {
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: 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 {
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 {
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 { // 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 {
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 {
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 {
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 {
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();

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

вот что написано на официальном сайте для того чтобы отобразить только одну страну
Style a single country
On this page
Getting started
Create a new style
Step 1: Add a satellite layer
Step 2: Add a mask layer
Step 3: Style the mask layer
Step 4: Hide the mask for one country
Step 5: Add a water layer
Step 6: Add country borders
Step 7: Show more satellite context
Publish your style
Final product
Next steps: Add a data visualization
In this tutorial, you will learn how to style a map with one specific country highlighted apart from other countries. You will use Mapbox Countries, Mapbox Satellite, and Mapbox Streets, three freely available tilesets, with a masking technique to do this.

Masking is a cartographic term that describes hiding features on a map so that the geographic area of interest is the main visual area illustrated, limiting unnecessary visual clutter. In this example, you will be styling The United States and masking all other countries.

Your final map style will look like this:

Screenshot showing a single country styled on a map
Getting started
Before getting started, you will need a few things:

Mapbox account. You will need an account to log in and use the Mapbox Studio style editor. You can create an account at mapbox.com/account.
Create a new style
Log into your Mapbox account and navigate to your Styles page. This is where all your styles are listed.
Click New style to create a new style.
Scroll down and select Blank to create a new style from scratch.
Click Customize Blank. The style editor will automatically open.
circle
circle
circle
arrow-leftarrow-rightrotate
heartmapboxmenu
What is a style?
A style is a set of rules for how Mapbox will draw the map on a page. It includes references to your data, map images (icons, markers, patterns), fonts, and defines how all your data should be styled on your map. For more about styles, see the Styles section of the Mapbox Studio Manual.

Step 1: Add a satellite layer
First, you will add a new raster layer using satellite imagery from Mapbox.

In Studio, click the Layers tab to open the Layers panel.
Click the plus button (+) to add a new layer. This will open a Layer properties panel.
In the Layer properties panel, click None selected to select a source. Then, search for "Mapbox Satellite" and click raster Mapbox Satellite to select that layer as the layer source. This will create a raster layer called mapbox-satellite.
Close the Layer properties panel.
Your map should look like this:

Step 2: Add a mask layer
Next, you will add a new fill layer using country polygons from Mapbox.

Click the plus button (+) to add a new layer.
In the Layer properties panel, click None selected to select a source. Then, search for "Mapbox Countries" and click the polygon country_boundaries layer from the Mapbox Countries v1 tileset.
Close the Layer properties panel.
Your map will look black because the default styling for a fill layer is black:

How to add a layer using the Mapbox Countries tileset in Mapbox Studio
We will adjust the styling in the next step.

Step 3: Style the mask layer
Next, you will style all country polygons except one, to create the mask effect.

In the Layers panel, click your fill country_boundaries layer.
In the Layer properties panel, set the fill color to white (#ffffff).
All countries are now covered with a white color fill.

Step 4: Hide the mask for one country
Next, you will apply a data condition to style one country differently.

Click the Style with data conditions option, search for "name", and click the name property. This will open a panel where you can set a data condition to define which countries you will apply styling rules to. By default, the Data field will use the is operator.
Click the pencil icon (pencil) and enter the value "United States", then click the plus sign (+) to add this value.
In the Fill color section, drag the opacity slider to the left or set the a value to 0 to make the fill color transparent for features with a name property that is "United States".
Your map will look like this:

A map style with all countries except one covered by a solid color in Mapbox Studio
Step 5: Add a water layer
Next, you will add a new fill layer using water data from Mapbox.

Click the plus button (+) to add a new layer.
In the Layer properties panel, click None selected to select a source. Then, search for "water" and click the polygon water layer from the Mapbox Streets v8 tileset.
Click the Style tab and set the fill color to white (#ffffff).
Your map will look like this, with only one country visible:

How to add a water layer and style it with a solid color in Mapbox Studio
Step 6: Add country borders
Next, you will add a new line layer using national border data from Mapbox.

In the Layers panel, click your fill country_boundaries layer.
Click the duplicate icon (duplicate) to create a copy of this layer. The new layer will be named fill country_boundaries copy .
Select the new layer to open the Layer properties panel, and click the Select data tab.
Click the Type property and change the type from fill to line.
The Mapbox Studio data inspector will display the line data like this:

How to add country borders with a line layer in Mapbox Studio
Click the Style tab, then set the Width property to 0.3 and the Color property to gray: #cccccc.
In the Layers panel, drag your fill country_boundaries copy layer to the top of the layer hierarchy, above all other layers.
Your map will look like this, with visible outlines around the masked countries:

How to move a layer to the top of the layer stack in Mapbox Studio
Step 7: Show more satellite context
Next, you will add some black-and-white satellite imagery to give the masked countries some visual context.

In the Layers panel, click your mapbox-satellite layer.
Click the duplicate icon (duplicate) to create another copy of this layer. The new layer will be named mapbox-satellite copy .
Click the Style tab, then set the Opacity property to 0.3 and the Saturation property to 0.1.
In the Layers panel, drag your fill mapbox-satellite copy layer below your water layer but above your country-boundaries layer.
Your final map style will look like this:

View fullscreen demochevron-right
To begin developing with your new map style, continue to Publish your style.

Publish your style
When you have finished editing your map style, publish your changes by clicking Publish in the upper right of the style editor. When you click the publish button, a window will display the difference between the previous and current version of this style. If you're happy with the changes, click Publish. Your style will now be available to share from a variety of tools and applications.

Final product
You've styled a single country with the help of several freely available data sources. Mapbox Studio provides a wide variety of ways to use your new map style. You can use this map directly on your website or in a web or mobile application. Take a look at the Publish style section of the Mapbox Studio Manual to see all the ways you can use your style!

Next steps: Add a data visualization
You can extend the workflow outlined in this tutorial to add and style additional geometries in Mapbox Studio.

Try adding a choropleth map visualization using the data and instructions from our Make a choropleth map tutorial.
We would love to see how designers use boundaries data with different masking techniques. Share your creations on Twitter with the hashtag #BuiltWithMapbox.
Learn more about what you can do with the Mapbox Studio style editor in the Mapbox Studio Manual.

только мне еще более хорошо надо это сделать


Чтобы на странице с картой Кыргызстана отображалась только территория Кыргызстана без других стран, вы можете воспользоваться подходом маскировки, описанным в официальном руководстве Mapbox. Ниже приведены подробные шаги по реализации этого функционала с учётом вашего текущего кода и структуры проекта.