Бэкенд:
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from 'src/users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { SessionSerializer } from './session.serializer';

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



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

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

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

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

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

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

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

    return null;
  }
}




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

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

        return request.isAuthenticated();
    }
}



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

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

        await super.logIn(request);

        return result;
    }
}




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

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

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



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

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


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





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

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






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

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

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

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




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

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





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

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

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

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

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




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

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

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







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

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

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

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

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

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

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

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










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







// src/testpages/test.controller.ts

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

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

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

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

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

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

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









// src/testpages/test.model.ts

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

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

  @Column
  title: string;

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

  @Column(DataType.INTEGER)
  timesCompleted: number;

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







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

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








// src/testpages/test.service.ts

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    const questions = test.questions;

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

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

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

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

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

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

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

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

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

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











import { IsNotEmpty } from "class-validator";

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

    @IsNotEmpty()
    readonly password: string;

    @IsNotEmpty()
    readonly email: string;
}









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

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

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

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

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

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

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










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

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

  @Column
  email: string;

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








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

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










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

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

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

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

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

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

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

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

    return user.save();
  }

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

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











// backend/src/app.module.ts

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

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













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

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

  app.use(
    session({
      secret: 'keyword', // Change this to a strong secret in production
      resave: false,
      saveUninitialized: false,
    }),
  );

  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      whitelist: true,
      forbidNonWhitelisted: true,
    }),
  );

  app.use(passport.initialize());
  app.use(passport.session());

  app.enableCors({
    origin: 'http://localhost:3000', // Update if your frontend runs on a different origin
    credentials: true,
  });

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











SQL_DIALECT=mysql
SQL_LOGGING=true
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_USER=root
DATABASE_PASSWORD=1369
DATABASE_NAME=test







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

const Button = ({ children, onClick, variant = 'primary', className = '', leftIcon, rightIcon, ...props }) => {
  return (
    <button className={`btn btn-${variant} ${className}`} onClick={onClick} {...props}>
      {leftIcon && <span className="button-icon left-icon">{leftIcon}</span>}
      {children}
      {rightIcon && <span className="button-icon right-icon">{rightIcon}</span>}
    </button>
  );
};

export default Button;





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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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







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

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

export default Card;





@import '../styles/variables';

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

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

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

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

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

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








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

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

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

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

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

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

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

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

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

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

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

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

export default Header;







@import '../styles/variables';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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











// frontend/src/KGMap/KyrgyzstanMap.jsx

import React, { useState } from 'react';
import { MapContainer, TileLayer, GeoJSON } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import './KyrgyzstanMap.scss';
import Button from '../Button/Button';

import kyrgyzstanData from '../data/kyrgyzstan.json';
import worldData from '../data/world.json';

const KyrgyzstanMap = () => {
  // State variables to manage panel visibility and info
  const [currentStyle, setCurrentStyle] = useState('streets');
  const [isControlPanelOpen, setIsControlPanelOpen] = useState(false);
  const [isInfoPanelOpen, setIsInfoPanelOpen] = useState(false);
  const [kyrgyzstanInfo, setKyrgyzstanInfo] = useState(null);

  const bounds = [
    [34, 64],
    [48, 85],
  ];

  const center = [41.2044, 74.7661];
  const minZoom = 5;
  const maxZoom = 15;

  const handleChangeStyle = (styleId) => {
    setCurrentStyle(styleId);
  };

  const styleMap = {
    streets: {
      url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
      attribution: '© OpenStreetMap contributors',
    },
    satellite: {
      url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
      attribution: '© OpenTopoMap',
    },
    light: {
      url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
      attribution: '© CARTO',
    },
    dark: {
      url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
      attribution: '© CARTO',
    },
    outdoors: {
      url: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
      attribution: '© OpenStreetMap contributors, Tiles style by HOT',
    },
  };

  // Styles for other countries and Kyrgyzstan
  const otherCountriesStyle = {
    fillColor: '#808080',
    fillOpacity: 0.4,
    weight: 0,
    opacity: 0,
  };

  const kyrgyzstanStyle = {
    fillColor: 'transparent',
    fillOpacity: 0,
    weight: 0,
    opacity: 0,
  };

  // Function to handle clicks on Kyrgyzstan
  const onKyrgyzstanClick = () => {
    // Adjust based on your JSON structure
    setKyrgyzstanInfo(kyrgyzstanData.properties);
    setIsInfoPanelOpen(true);
  };

  // Component to render Kyrgyzstan GeoJSON with event handlers
  const KyrgyzstanGeoJSON = () => {
    const onEachFeature = (feature, layer) => {
      if (feature.properties && (feature.properties.ADMIN === 'Kyrgyzstan' || feature.properties.NAME === 'Kyrgyzstan')) {
        layer.on({
          click: onKyrgyzstanClick,
        });
        layer.bindTooltip('Кыргызстан'); // Optional tooltip
      }
    };

    return (
      <GeoJSON
        data={kyrgyzstanData}
        style={kyrgyzstanStyle}
        onEachFeature={onEachFeature}
      />
    );
  };

  return (
    <div className="kyrgyzstan-map-container">
      {/* Top-Left Button: Control Panel Toggle */}
      <Button
        className={`top-left-button ${isControlPanelOpen ? 'active' : ''}`}
        onClick={() => setIsControlPanelOpen(!isControlPanelOpen)}
        variant="outline"
        aria-label="Toggle Control Panel"
      >
        ≡
      </Button>

      {/* Top-Right Button: Info Panel Toggle */}
      <Button
        className={`top-right-button ${isInfoPanelOpen ? 'active' : ''}`}
        onClick={() => setIsInfoPanelOpen(!isInfoPanelOpen)}
        variant="outline"
        aria-label="Toggle Info Panel"
      >
        ℹ️
      </Button>

      {/* Control Panel */}
      <div className={`control-panel ${isControlPanelOpen ? 'open' : ''}`}>
        <h3>Стили карты</h3>
        <div className="style-buttons">
          <Button
            onClick={() => handleChangeStyle('streets')}
            variant="outline"
            size="small"
            className={currentStyle === 'streets' ? 'active' : ''}
          >
            Улицы
          </Button>
          <Button
            onClick={() => handleChangeStyle('satellite')}
            variant="outline"
            size="small"
            className={currentStyle === 'satellite' ? 'active' : ''}
          >
            Спутник
          </Button>
          <Button
            onClick={() => handleChangeStyle('light')}
            variant="outline"
            size="small"
            className={currentStyle === 'light' ? 'active' : ''}
          >
            Светлый
          </Button>
          <Button
            onClick={() => handleChangeStyle('dark')}
            variant="outline"
            size="small"
            className={currentStyle === 'dark' ? 'active' : ''}
          >
            Темный
          </Button>
          <Button
            onClick={() => handleChangeStyle('outdoors')}
            variant="outline"
            size="small"
            className={currentStyle === 'outdoors' ? 'active' : ''}
          >
            На открытом воздухе
          </Button>
        </div>
      </div>

      {/* Info Panel */}
      <div className={`info-panel ${isInfoPanelOpen ? 'open' : ''}`}>
        <h3>Информация о Кыргызстане</h3>
        {kyrgyzstanInfo ? (
          <div>
            {/* Adjust fields based on kyrgyzstan.json structure */}
            <p><strong>Название:</strong> {kyrgyzstanInfo.ADMIN || kyrgyzstanInfo.NAME}</p>
            <p><strong>Столица:</strong> {kyrgyzstanInfo.CAPITAL}</p>
            <p><strong>Население:</strong> {kyrgyzstanInfo.POPULATION}</p>
            {/* Add more fields as necessary */}
          </div>
        ) : (
          <p>Нажмите на территорию Кыргызстана на карте, чтобы получить информацию.</p>
        )}
      </div>

      {/* Map Container */}
      <MapContainer
        center={center}
        zoom={7}
        minZoom={minZoom}
        maxZoom={maxZoom}
        maxBounds={bounds}
        style={{ height: '100vh', width: '100%' }}
        scrollWheelZoom={true}
      >
        {/* Tile Layer based on current style */}
        <TileLayer
          url={styleMap[currentStyle].url}
          attribution={styleMap[currentStyle].attribution}
        />

        {/* Shading for other countries */}
        <GeoJSON
          data={worldData}
          style={otherCountriesStyle}
          filter={(feature) => {
            const countryName = feature.properties.ADMIN || feature.properties.NAME;
            return countryName !== 'Kyrgyzstan';
          }}
        />

        {/* Interactive Kyrgyzstan Layer */}
        <KyrgyzstanGeoJSON />
      </MapContainer>
    </div>
  );
};

export default KyrgyzstanMap;











// frontend/src/KGMap/KyrgyzstanMap.scss

@import '../styles/variables';
@import '../Button/Button.scss'; // Ensure the path is correct

.kyrgyzstan-map-container {
  position: relative;
  height: 100vh;
  width: 100%;

  /* Top-Left Button (Control Panel Toggle) */
  .top-left-button {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 1001; /* Higher than panels to stay on top */
    width: 40px;
    height: 40px;
    padding: 0;
    background-color: rgba(255, 255, 255, 0.9);
    border: 1px solid #ccc;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background-color 0.3s, border-color 0.3s;

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

    &.active {
      border-color: #007bff;
      background-color: #e6f0ff;
    }

    /* Icon styling, assuming using text or icon font */
    font-size: 1.2rem;
    color: #007bff;
  }

  /* Top-Right Button (Info Panel Toggle) */
  .top-right-button {
    position: absolute;
    top: 10px;
    right: 10px;
    z-index: 1001; /* Higher than panels to stay on top */
    width: 40px;
    height: 40px;
    padding: 0;
    background-color: rgba(255, 255, 255, 0.9);
    border: 1px solid #ccc;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background-color 0.3s, border-color 0.3s;

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

    &.active {
      border-color: #007bff;
      background-color: #e6f0ff;
    }

    /* Icon styling, assuming using text or icon font */
    font-size: 1.2rem;
    color: #007bff;
  }

  /* Control Panel Styling */
  .control-panel {
    position: absolute;
    top: 60px; /* Positioned below the toggle button */
    left: 10px;
    background-color: rgba(255, 255, 255, 0.95);
    padding: 1rem;
    border-radius: 0.5rem;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    z-index: 1000; /* Below the toggle buttons */
    max-width: 250px;
    width: 90%;
    transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
    opacity: 0;
    transform: translateY(-20px);
    pointer-events: none;

    &.open {
      opacity: 1;
      transform: translateY(0);
      pointer-events: auto;
    }

    h3 {
      margin-top: 0;
      margin-bottom: 1rem;
      font-size: 1.2rem;
      color: #333;
    }

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

      .btn-outline {
        padding: 0.4rem 0.8rem; /* Reduced padding for smaller buttons */
        font-size: 0.9rem; /* Smaller font size */
        transition: background-color 0.3s, color 0.3s;

        &:hover {
          background-color: #007bff;
          color: #fff;
        }

        &.active {
          background-color: #0056b3;
          border-color: #0056b3;
          color: #fff;
        }
      }
    }
  }

  /* Info Panel Styling */
  .info-panel {
    position: absolute;
    top: 60px; /* Positioned below the toggle button */
    right: 10px;
    background-color: rgba(255, 255, 255, 0.95);
    padding: 1rem;
    border-radius: 0.5rem;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    z-index: 1000; /* Below the toggle buttons */
    max-width: 300px;
    width: 90%;
    transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
    opacity: 0;
    transform: translateY(-20px);
    pointer-events: none;

    &.open {
      opacity: 1;
      transform: translateY(0);
      pointer-events: auto;
    }

    h3 {
      margin-top: 0;
      margin-bottom: 1rem;
      font-size: 1.2rem;
      color: #333;
    }

    p {
      margin: 0.5rem 0;
      color: #555;

      strong {
        color: #333;
      }
    }
  }

  /* Responsive Adjustments */
  @media (max-width: 768px) {
    .control-panel,
    .info-panel {
      max-width: 90%;
      left: 50%;
      right: auto;
      transform: translateX(-50%) translateY(-20px);
      top: 60px;

      &.open {
        transform: translateX(-50%) translateY(0);
      }
    }

    .info-panel {
      right: auto;
      left: 50%;
    }
  }
}










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

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

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

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

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

export default Avatar;









@import '../styles/variables';

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

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





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

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

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

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

export default RegisterPage;







@import '../styles/variables';

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

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

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

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

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

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

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

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

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









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

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

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

    fetchUserData();
  }, [navigate]);

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

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

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

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

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

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

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

  if (!user) {
    return null;
  }

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

export default UserPage;












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

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

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

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

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

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

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

      .avatar-selection {
        width: 100%;

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

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

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

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

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

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

    .info-section {
      flex-grow: 1;

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

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

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

        strong {
          color: $heading-color;
        }
      }

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

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

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

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

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

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

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

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

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

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











import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Header from './Header/Header';
import Modal from './Modal';
import Card from './Card/Card';
import './MainPage.scss';

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

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

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

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

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

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

export default MainPage;








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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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










// src/Modal.jsx

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

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

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

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

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

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

export default Modal;







// src/Modal.scss

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

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

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

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

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

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

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

    button {
      align-self: center;
    }
  }

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

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

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

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

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

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

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

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










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

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

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

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

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

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

export default PublicPage;









@import '../styles/variables';

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

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

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

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

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

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

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

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

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

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

          p {
            color: $text-color;
            font-size: 1rem;
          }
        }
      }
    }
}
Бэкенд:
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from 'src/users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { SessionSerializer } from './session.serializer';

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



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

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

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

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

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

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

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

    return null;
  }
}




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

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

        return request.isAuthenticated();
    }
}



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

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

        await super.logIn(request);

        return result;
    }
}




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

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

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



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

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


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





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

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






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

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

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

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




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

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





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

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

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

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

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




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

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

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







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

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

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

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

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

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

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

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










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







// src/testpages/test.controller.ts

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

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

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

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

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

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

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









// src/testpages/test.model.ts

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

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

  @Column
  title: string;

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

  @Column(DataType.INTEGER)
  timesCompleted: number;

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







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

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








// src/testpages/test.service.ts

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    const questions = test.questions;

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

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

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

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

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

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

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

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

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

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











import { IsNotEmpty } from "class-validator";

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

    @IsNotEmpty()
    readonly password: string;

    @IsNotEmpty()
    readonly email: string;
}









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

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

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

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

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

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

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










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

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

  @Column
  email: string;

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








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

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










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

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

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

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

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

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

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

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

    return user.save();
  }

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

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











// backend/src/app.module.ts

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

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













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

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

  app.use(
    session({
      secret: 'keyword', // Change this to a strong secret in production
      resave: false,
      saveUninitialized: false,
    }),
  );

  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      whitelist: true,
      forbidNonWhitelisted: true,
    }),
  );

  app.use(passport.initialize());
  app.use(passport.session());

  app.enableCors({
    origin: 'http://localhost:3000', // Update if your frontend runs on a different origin
    credentials: true,
  });

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











SQL_DIALECT=mysql
SQL_LOGGING=true
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_USER=root
DATABASE_PASSWORD=1369
DATABASE_NAME=test







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

const Button = ({ children, onClick, variant = 'primary', className = '', leftIcon, rightIcon, ...props }) => {
  return (
    <button className={`btn btn-${variant} ${className}`} onClick={onClick} {...props}>
      {leftIcon && <span className="button-icon left-icon">{leftIcon}</span>}
      {children}
      {rightIcon && <span className="button-icon right-icon">{rightIcon}</span>}
    </button>
  );
};

export default Button;





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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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







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

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

export default Card;





@import '../styles/variables';

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

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

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

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

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

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








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

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

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

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

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

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

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

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

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

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

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

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

export default Header;







@import '../styles/variables';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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











// frontend/src/KGMap/KyrgyzstanMap.jsx

import React, { useState } from 'react';
import { MapContainer, TileLayer, GeoJSON } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import './KyrgyzstanMap.scss';
import Button from '../Button/Button';

import kyrgyzstanData from '../data/kyrgyzstan.json';
import worldData from '../data/world.json';

const KyrgyzstanMap = () => {
  // State variables to manage panel visibility and info
  const [currentStyle, setCurrentStyle] = useState('streets');
  const [isControlPanelOpen, setIsControlPanelOpen] = useState(false);
  const [isInfoPanelOpen, setIsInfoPanelOpen] = useState(false);
  const [kyrgyzstanInfo, setKyrgyzstanInfo] = useState(null);

  const bounds = [
    [34, 64],
    [48, 85],
  ];

  const center = [41.2044, 74.7661];
  const minZoom = 5;
  const maxZoom = 15;

  const handleChangeStyle = (styleId) => {
    setCurrentStyle(styleId);
  };

  const styleMap = {
    streets: {
      url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
      attribution: '© OpenStreetMap contributors',
    },
    satellite: {
      url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
      attribution: '© OpenTopoMap',
    },
    light: {
      url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
      attribution: '© CARTO',
    },
    dark: {
      url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
      attribution: '© CARTO',
    },
    outdoors: {
      url: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
      attribution: '© OpenStreetMap contributors, Tiles style by HOT',
    },
  };

  // Styles for other countries and Kyrgyzstan
  const otherCountriesStyle = {
    fillColor: '#808080',
    fillOpacity: 0.4,
    weight: 0,
    opacity: 0,
  };

  const kyrgyzstanStyle = {
    fillColor: 'transparent',
    fillOpacity: 0,
    weight: 0,
    opacity: 0,
  };

  // Function to handle clicks on Kyrgyzstan
  const onKyrgyzstanClick = () => {
    // Adjust based on your JSON structure
    setKyrgyzstanInfo(kyrgyzstanData.properties);
    setIsInfoPanelOpen(true);
  };

  // Component to render Kyrgyzstan GeoJSON with event handlers
  const KyrgyzstanGeoJSON = () => {
    const onEachFeature = (feature, layer) => {
      if (feature.properties && (feature.properties.ADMIN === 'Kyrgyzstan' || feature.properties.NAME === 'Kyrgyzstan')) {
        layer.on({
          click: onKyrgyzstanClick,
        });
        layer.bindTooltip('Кыргызстан'); // Optional tooltip
      }
    };

    return (
      <GeoJSON
        data={kyrgyzstanData}
        style={kyrgyzstanStyle}
        onEachFeature={onEachFeature}
      />
    );
  };

  return (
    <div className="kyrgyzstan-map-container">
      {/* Top-Left Button: Control Panel Toggle */}
      <Button
        className={`top-left-button ${isControlPanelOpen ? 'active' : ''}`}
        onClick={() => setIsControlPanelOpen(!isControlPanelOpen)}
        variant="outline"
        aria-label="Toggle Control Panel"
      >
        ≡
      </Button>

      {/* Top-Right Button: Info Panel Toggle */}
      <Button
        className={`top-right-button ${isInfoPanelOpen ? 'active' : ''}`}
        onClick={() => setIsInfoPanelOpen(!isInfoPanelOpen)}
        variant="outline"
        aria-label="Toggle Info Panel"
      >
        ℹ️
      </Button>

      {/* Control Panel */}
      <div className={`control-panel ${isControlPanelOpen ? 'open' : ''}`}>
        <h3>Стили карты</h3>
        <div className="style-buttons">
          <Button
            onClick={() => handleChangeStyle('streets')}
            variant="outline"
            size="small"
            className={currentStyle === 'streets' ? 'active' : ''}
          >
            Улицы
          </Button>
          <Button
            onClick={() => handleChangeStyle('satellite')}
            variant="outline"
            size="small"
            className={currentStyle === 'satellite' ? 'active' : ''}
          >
            Спутник
          </Button>
          <Button
            onClick={() => handleChangeStyle('light')}
            variant="outline"
            size="small"
            className={currentStyle === 'light' ? 'active' : ''}
          >
            Светлый
          </Button>
          <Button
            onClick={() => handleChangeStyle('dark')}
            variant="outline"
            size="small"
            className={currentStyle === 'dark' ? 'active' : ''}
          >
            Темный
          </Button>
          <Button
            onClick={() => handleChangeStyle('outdoors')}
            variant="outline"
            size="small"
            className={currentStyle === 'outdoors' ? 'active' : ''}
          >
            На открытом воздухе
          </Button>
        </div>
      </div>

      {/* Info Panel */}
      <div className={`info-panel ${isInfoPanelOpen ? 'open' : ''}`}>
        <h3>Информация о Кыргызстане</h3>
        {kyrgyzstanInfo ? (
          <div>
            {/* Adjust fields based on kyrgyzstan.json structure */}
            <p><strong>Название:</strong> {kyrgyzstanInfo.ADMIN || kyrgyzstanInfo.NAME}</p>
            <p><strong>Столица:</strong> {kyrgyzstanInfo.CAPITAL}</p>
            <p><strong>Население:</strong> {kyrgyzstanInfo.POPULATION}</p>
            {/* Add more fields as necessary */}
          </div>
        ) : (
          <p>Нажмите на территорию Кыргызстана на карте, чтобы получить информацию.</p>
        )}
      </div>

      {/* Map Container */}
      <MapContainer
        center={center}
        zoom={7}
        minZoom={minZoom}
        maxZoom={maxZoom}
        maxBounds={bounds}
        style={{ height: '100vh', width: '100%' }}
        scrollWheelZoom={true}
      >
        {/* Tile Layer based on current style */}
        <TileLayer
          url={styleMap[currentStyle].url}
          attribution={styleMap[currentStyle].attribution}
        />

        {/* Shading for other countries */}
        <GeoJSON
          data={worldData}
          style={otherCountriesStyle}
          filter={(feature) => {
            const countryName = feature.properties.ADMIN || feature.properties.NAME;
            return countryName !== 'Kyrgyzstan';
          }}
        />

        {/* Interactive Kyrgyzstan Layer */}
        <KyrgyzstanGeoJSON />
      </MapContainer>
    </div>
  );
};

export default KyrgyzstanMap;











// frontend/src/KGMap/KyrgyzstanMap.scss

@import '../styles/variables';
@import '../Button/Button.scss'; // Ensure the path is correct

.kyrgyzstan-map-container {
  position: relative;
  height: 100vh;
  width: 100%;

  /* Top-Left Button (Control Panel Toggle) */
  .top-left-button {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 1001; /* Higher than panels to stay on top */
    width: 40px;
    height: 40px;
    padding: 0;
    background-color: rgba(255, 255, 255, 0.9);
    border: 1px solid #ccc;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background-color 0.3s, border-color 0.3s;

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

    &.active {
      border-color: #007bff;
      background-color: #e6f0ff;
    }

    /* Icon styling, assuming using text or icon font */
    font-size: 1.2rem;
    color: #007bff;
  }

  /* Top-Right Button (Info Panel Toggle) */
  .top-right-button {
    position: absolute;
    top: 10px;
    right: 10px;
    z-index: 1001; /* Higher than panels to stay on top */
    width: 40px;
    height: 40px;
    padding: 0;
    background-color: rgba(255, 255, 255, 0.9);
    border: 1px solid #ccc;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background-color 0.3s, border-color 0.3s;

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

    &.active {
      border-color: #007bff;
      background-color: #e6f0ff;
    }

    /* Icon styling, assuming using text or icon font */
    font-size: 1.2rem;
    color: #007bff;
  }

  /* Control Panel Styling */
  .control-panel {
    position: absolute;
    top: 60px; /* Positioned below the toggle button */
    left: 10px;
    background-color: rgba(255, 255, 255, 0.95);
    padding: 1rem;
    border-radius: 0.5rem;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    z-index: 1000; /* Below the toggle buttons */
    max-width: 250px;
    width: 90%;
    transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
    opacity: 0;
    transform: translateY(-20px);
    pointer-events: none;

    &.open {
      opacity: 1;
      transform: translateY(0);
      pointer-events: auto;
    }

    h3 {
      margin-top: 0;
      margin-bottom: 1rem;
      font-size: 1.2rem;
      color: #333;
    }

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

      .btn-outline {
        padding: 0.4rem 0.8rem; /* Reduced padding for smaller buttons */
        font-size: 0.9rem; /* Smaller font size */
        transition: background-color 0.3s, color 0.3s;

        &:hover {
          background-color: #007bff;
          color: #fff;
        }

        &.active {
          background-color: #0056b3;
          border-color: #0056b3;
          color: #fff;
        }
      }
    }
  }

  /* Info Panel Styling */
  .info-panel {
    position: absolute;
    top: 60px; /* Positioned below the toggle button */
    right: 10px;
    background-color: rgba(255, 255, 255, 0.95);
    padding: 1rem;
    border-radius: 0.5rem;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    z-index: 1000; /* Below the toggle buttons */
    max-width: 300px;
    width: 90%;
    transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
    opacity: 0;
    transform: translateY(-20px);
    pointer-events: none;

    &.open {
      opacity: 1;
      transform: translateY(0);
      pointer-events: auto;
    }

    h3 {
      margin-top: 0;
      margin-bottom: 1rem;
      font-size: 1.2rem;
      color: #333;
    }

    p {
      margin: 0.5rem 0;
      color: #555;

      strong {
        color: #333;
      }
    }
  }

  /* Responsive Adjustments */
  @media (max-width: 768px) {
    .control-panel,
    .info-panel {
      max-width: 90%;
      left: 50%;
      right: auto;
      transform: translateX(-50%) translateY(-20px);
      top: 60px;

      &.open {
        transform: translateX(-50%) translateY(0);
      }
    }

    .info-panel {
      right: auto;
      left: 50%;
    }
  }
}










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

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

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

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

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

export default Avatar;









@import '../styles/variables';

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

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





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

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

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

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

export default RegisterPage;







@import '../styles/variables';

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

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

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

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

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

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

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

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

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









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

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

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

    fetchUserData();
  }, [navigate]);

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

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

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

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

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

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

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

  if (!user) {
    return null;
  }

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

export default UserPage;












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

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

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

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

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

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

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

      .avatar-selection {
        width: 100%;

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

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

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

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

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

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

    .info-section {
      flex-grow: 1;

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

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

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

        strong {
          color: $heading-color;
        }
      }

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

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

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

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

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

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

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

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

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

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











import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Header from './Header/Header';
import Modal from './Modal';
import Card from './Card/Card';
import './MainPage.scss';

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

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

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

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

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

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

export default MainPage;








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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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










// src/Modal.jsx

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

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

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

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

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

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

export default Modal;







// src/Modal.scss

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

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

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

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

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

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

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

    button {
      align-self: center;
    }
  }

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

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

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

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

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

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

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

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










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

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

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

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

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

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

export default PublicPage;









@import '../styles/variables';

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

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

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

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

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

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

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

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

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

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

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






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

вот код игры которую я хочу взять за основу
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="tags" content="geography, geo, map, quiz, game" />
    <meta name="description" content="Geography Quiz Games" />
    <meta name="author" content="Owen Kuhn" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <meta name="theme-color" content="#0A0A0A" />
    <title>Geotective</title>

    <link rel="dns-prefetch" href="https://server.arcgisonline.com">


    <link href="style.css" rel="stylesheet" />
    <script defer src="script.js"></script>

    <link defer rel="manifest" href="manifest.json">
    <link defer rel="shortcut icon" type="image/x-icon" href="res/favicon.png" />
    <link defer rel="apple-touch-icon" href="res/favicon_maskable.png">
    <link defer rel="stylesheet" href="res/lib/leaflet.css"/>
    <script defer src="res/lib/leaflet.js" ></script>
    <script defer src="res/lib/smooth-zoom.js" ></script>
    <script defer src="res/lib/fuzzysort-min.js"></script>
</head>
<body>
    <div id="clickCover"></div>
    <div id="home" class="screen centered visible" style="display: grid;">
        <div class="centeredContent">
            <div id="logo">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#7ac2ed" d="M482.38 261.17c0 74.593-36.047 140.766-91.667 182.067l-79.725 27.681-15.553-6.52L53.07 362.844a224.812 224.812 0 01-20.224-60.032l52.12-141.376L230.983 48.542l9.142-7.07 50.669-4.25a224.876 224.876 0 0166.492 21.281L481.6 251.78l.71 6.3.06 2.12c0 .31.01.64.01.97z"/><path fill="#68a6dc" d="M482.38 261.17c0 74.593-36.047 140.766-91.667 182.067l-79.725 27.681-15.553-6.52c86.826-26.761 149.928-107.634 149.928-203.228 0-117.435-95.218-212.638-212.67-212.638-.57 0-1.14 0-1.71.01l9.142-7.07 50.669-4.25a224.876 224.876 0 0166.492 21.281L481.6 251.78l.71 6.3.06 2.12c0 .31.01.64.01.97z"/><path fill="#d1e16f" d="M290.794 37.221c-8.922 4.9-16.563 9.62-23.084 14.18-63.172 44.082-22.614 72.923-38.917 103.994-20.344 38.762-99.128 14.28-118.372 29.321-25.675 20.061 0 102.244-77.574 118.095a227.684 227.684 0 01-3.821-41.642c0-69.963 31.706-132.525 81.535-174.097 39.317-32.811 89.917-52.552 145.137-52.552 11.932.001 23.654.921 35.096 2.701z"/><g fill="#d0edf5"><path d="M40.453 496.999H7.597a7.5 7.5 0 100 15h32.857a7.5 7.5 0 10-.001-15zM143.472 496.999H75.609a7.5 7.5 0 100 15h67.863a7.5 7.5 0 100-15zM27.6 479.999h62.862a7.5 7.5 0 100-15H27.6a7.5 7.5 0 100 15zM415.049 496.999h-62.862a7.5 7.5 0 100 15h62.862a7.5 7.5 0 100-15zM504.499 464.998h-92.868a7.5 7.5 0 100 15h92.868a7.5 7.5 0 100-15zM408.981 7.5c0-4.142-3.359-7.5-7.501-7.5h-32.857a7.5 7.5 0 100 15h32.857a7.5 7.5 0 007.501-7.5zM504.499 0h-67.863a7.5 7.5 0 100 15h67.863a7.5 7.5 0 100-15zM461.491 32.001h-62.862a7.5 7.5 0 100 15h62.862a7.5 7.5 0 100-15z"/></g><path fill="#d1e16f" d="M296.258 321.916c-7.141 16.339-58.722 12.608-71.332 0s-16.475-64.498 0-71.321c23.3-9.65 81.431 48.214 71.332 71.321zM390.713 443.237c-37.707 28.001-84.426 44.572-135.015 44.572-55.17 0-105.73-19.701-145.027-52.462a227.517 227.517 0 01-57.601-72.503c89.307 0 34.106-60.032 75.514-60.032 55.33 0 48.189 89.813 78.415 105.414 29.856 15.421 52.16-16.881 94.608-16.881 32.246 0 32.596 28.101 54.59 42.952 7.891 5.32 18.573 8.94 34.516 8.94z"/><path fill="#e6f8fc" d="M482.37 260.2c-.57-18.061-17.283-10.28-37.317-10.57-14.703-.21-31.196-4.78-44.408-27.091-31.096-52.522-105.43-13.541-105.43-78.403 0-36.061 47.099-6.51 59.241-57.322 1.8-7.52 2.831-16.801 2.831-28.311C431.2 95.604 482.01 171.947 482.37 260.2z"/><path fill="#9ccc4b" d="M232.693 48.532c-45.448 0-87.576 14.261-122.133 38.542 39.317-32.811 89.917-52.552 145.137-52.552 11.932 0 23.654.92 35.096 2.7-8.922 4.9-16.563 9.62-23.084 14.18a213.76 213.76 0 00-35.016-2.87zM356.196 434.297c7.891 5.32 18.573 8.94 34.516 8.94-37.707 28.001-84.426 44.572-135.015 44.572-55.17 0-105.73-19.701-145.027-52.462 34.536 24.241 76.614 38.461 122.023 38.461 46.049.001 88.687-14.64 123.503-39.511z"/><path fill="#d0edf5" d="M482.37 260.2c-.57-18.061-17.283-10.28-37.317-10.57-3.601-67.383-38.587-126.435-90.597-162.816 1.8-7.52 2.831-16.801 2.831-28.311C431.2 95.604 482.01 171.947 482.37 260.2z"/><path fill="#e6f8fc" d="M192.666 79.013c0 13.851-11.242 25.091-25.095 25.091H34.206C15.313 104.104 0 88.784 0 69.893c0-18.881 15.313-34.201 34.206-34.201 7.871 0 15.123 2.66 20.904 7.13C59.471 18.481 80.755 0 106.35 0a51.892 51.892 0 0131.106 10.3c12.712 9.49 20.954 24.661 20.954 41.752 0 1.22-.04 2.44-.12 3.64a25.091 25.091 0 019.282-1.77c13.852 0 25.094 11.231 25.094 25.091z"/><path fill="#d0edf5" d="M192.666 79.013c0 13.851-11.242 25.091-25.095 25.091H34.206C15.313 104.104 0 88.784 0 69.893c0 9.39 11.612 17.851 20.994 17.851h74.204c27.675-.27 50.019-22.791 50.019-50.522 0-9.9-2.84-19.131-7.761-26.921 12.712 9.49 20.954 24.661 20.954 41.752 0 1.22-.04 2.44-.12 3.64a25.091 25.091 0 019.282-1.77c13.852-.001 25.094 11.23 25.094 25.09z"/><g><path fill="#e6f8fc" d="M512 370.358c0 10.588-8.594 19.18-19.183 19.18H390.868c-14.443 0-26.148-11.712-26.148-26.152 0-14.433 11.706-26.145 26.148-26.145 6.017 0 11.56 2.034 15.98 5.451 3.334-18.607 19.604-32.734 39.169-32.734a39.665 39.665 0 0123.778 7.874c9.718 7.255 16.018 18.852 16.018 31.916 0 .933-.031 1.865-.092 2.783a19.185 19.185 0 017.095-1.353c10.59 0 19.184 8.585 19.184 19.18z"/><path fill="#d0edf5" d="M512 370.358c0 10.588-8.594 19.18-19.183 19.18H390.868c-14.443 0-26.148-11.712-26.148-26.152 0 7.178 8.877 13.646 16.048 13.646h56.724c21.156-.206 38.236-17.422 38.236-38.621 0-7.568-2.171-14.624-5.933-20.579 9.718 7.255 16.018 18.852 16.018 31.916 0 .933-.031 1.865-.092 2.783a19.185 19.185 0 017.095-1.353c10.59 0 19.184 8.585 19.184 19.18z"/></g></svg>
            </div>
            <h1 class="title">Geotective</h1>
            <h2 class="subtitle">Geography Quiz</h2>
            <br>
            <div class="titlebtn"><div onclick="toScreen('newGame');">New Game</div></div>
            <div class="titlebtn" id="resumeGameBtn" style="display:none;"><div onclick="resumeGame();">Resume Game</div></div>
            <div class="titlebtn"><div onclick="loadExploreMap();">Explore</div></div>
            <div class="titlebtn"><div onclick="toScreen('settings');">Settings</div></div>
            <div class="titlebtn"><div onclick="toScreen('attributions');">Attributions</div></div>
            <br><br>
        </div>
    </div>
  
   <div id="settings" class="screen paddedContent">
        <div class="title">Settings</div>
        <br><br>
        
        <div class="inputWrap checkbox">
            <input type="checkbox" id="setting_audio" oninput="onSettingsInput(this);" />
            <label for="setting_audio">Sound</label>
        </div>
        <br>
        <div class="inputWrap checkbox">
            <input type="checkbox" id="setting_speak" oninput="onSettingsInput(this);" />
            <label for="setting_speak">Pronounce Answers</label>
        </div>
        
        <br><br>
        <div class="titlebtn"><div onclick="toScreen('home');">Back to Home</div></div>
        <br><br>
  </div>
  
  <div id="attributions" class="screen paddedContent">
        <div class="title">Attributions</div>
        <br><br>
        <p xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/"><a property="dct:title" rel="cc:attributionURL" href="https://github.com/OwenK2/Interactive-Map-Quiz">Interactive-Map-Quiz</a> by <a rel="cc:attributionURL dct:creator" property="cc:attributionName" href="https://github.com/OwenK2">Owen Kuhn</a> is licensed under <a href="http://creativecommons.org/licenses/by-nc-sa/4.0/?ref=chooser-v1" target="_blank" rel="license noopener noreferrer" style="display:inline-block;">CC BY-NC-SA 4.0<img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/nc.svg?ref=chooser-v1"><img style="height:22px!important;margin-left:3px;vertical-align:text-bottom;" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg?ref=chooser-v1"></a></p>
        <br><br>
        <p>Icons made by <a rel="noreferrer" href="https://www.freepik.com" title="Freepik" target="_blank">Freepik</a> from <a href="https://www.flaticon.com/" rel="noreferrer" title="Flaticon" target="_blank">www.flaticon.com</a></p>
        <br>
        <p>Map service thanks to <a href="https://leafletjs.com/" title="Leaflet" rel="noreferrer" target="_blank">Leaflet</a></p>
        <br>
        <p>"Satelite" Map tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community</p>
        <br>
        <p>"Correct" sound effect by <a href="https://freesound.org/people/Eponn/" rel="noreferrer" target="_blank">Eponn</a></p>
        <br>
        <p>"Incorrect" sound effect by <a href="https://freesound.org/people/timgormly/" rel="noreferrer"target="_blank">Tim Gormly</a></p>
        <br><br>
        <div class="titlebtn"><div onclick="toScreen('home');">Back to Home</div></div>
        <br><br>
  </div>
  
    <div id="newGame" class="screen paddedContent">
        <div class="centeredContent">
            <div class="title">New Game</div>
            <br>
            <label class="newGameLabel" style="text-align: center">Game Mode</label>
            <div class="gameModes">
                <input id="gameMode_1" value="1" type="radio" name="gameMode" oninput="onGameModeChange(this)" class="hidden" checked />
                <label for="gameMode_1"><div>
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M316 30h-17.563C292.24 12.539 275.556 0 256 0c-19.556 0-36.24 12.539-42.437 30H196c-24.814 0-45 20.186-45 45 0 8.291 6.709 15 15 15h180c8.291 0 15-6.709 15-15 0-24.814-20.186-45-45-45z"/><path d="M406 60h-16.518A74.989 74.989 0 01391 75c0 24.814-20.186 45-45 45H166c-24.814 0-45-20.186-45-45 0-5.136.529-10.151 1.518-15H106c-24.814 0-45 20.186-45 45v362c0 24.814 20.186 45 45 45h300c24.814 0 45-20.186 45-45V105c0-24.814-20.186-45-45-45zM211 420h-60c-8.291 0-15-6.709-15-15s6.709-15 15-15h60c8.291 0 15 6.709 15 15s-6.709 15-15 15zm0-105h-60c-8.291 0-15-6.709-15-15s6.709-15 15-15h60c8.291 0 15 6.709 15 15s-6.709 15-15 15zm0-105h-60c-8.291 0-15-6.709-15-15s6.709-15 15-15h60c8.291 0 15 6.709 15 15s-6.709 15-15 15zm145.605 214.395c5.859 5.859 5.859 15.352 0 21.211A14.952 14.952 0 01346 450.001a14.95 14.95 0 01-10.605-4.395L316 426.211l-19.395 19.395A14.952 14.952 0 01286 450.001a14.95 14.95 0 01-10.605-4.395c-5.859-5.859-5.859-15.352 0-21.211L294.789 405l-19.395-19.395c-5.859-5.859-5.859-15.352 0-21.211s15.352-5.859 21.211 0L316 383.789l19.395-19.395c5.859-5.859 15.352-5.859 21.211 0s5.859 15.352 0 21.211L337.211 405l19.394 19.395zm15-143.79l-45 45A14.952 14.952 0 01316 330a14.95 14.95 0 01-10.605-4.395l-30-30c-5.859-5.859-5.859-15.352 0-21.211s15.352-5.859 21.211 0L316 293.789l34.395-34.395c5.859-5.859 15.352-5.859 21.211 0 5.859 5.859 5.859 15.352-.001 21.211zm0-90l-45 45A14.952 14.952 0 01316 240a14.95 14.95 0 01-10.605-4.395l-30-30c-5.859-5.859-5.859-15.352 0-21.211s15.352-5.859 21.211 0L316 203.789l34.395-34.395c5.859-5.859 15.352-5.859 21.211 0s5.859 15.352-.001 21.211z"/></svg>
                    <span>Multiple Choice</span>
                </div></label>
                <input id="gameMode_2" value="2" type="radio" name="gameMode" oninput="onGameModeChange(this)" class="hidden" />
                <label for="gameMode_2"><div>
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="-128 0 512 512"><path d="M234.668 469.332H192c-23.531 0-42.668-19.133-42.668-42.664V85.332c0-23.531 19.137-42.664 42.668-42.664h42.668c11.773 0 21.332-9.54 21.332-21.336S246.441 0 234.668 0H192c-25.602 0-48.34 11.562-64 29.484C112.34 11.562 89.602 0 64 0H21.332C9.559 0 0 9.535 0 21.332s9.559 21.336 21.332 21.336H64c23.531 0 42.668 19.133 42.668 42.664v341.336c0 23.531-19.137 42.664-42.668 42.664H21.332C9.559 469.332 0 478.872 0 490.668S9.559 512 21.332 512H64c25.602 0 48.34-11.563 64-29.484C143.66 500.437 166.398 512 192 512h42.668c11.773 0 21.332-9.535 21.332-21.332s-9.559-21.336-21.332-21.336zm0 0"/></svg>
                    <span>Free<br>Response</span>
                </div></label>
                <input id="gameMode_3" value="3" type="radio" name="gameMode" oninput="onGameModeChange(this)" class="hidden" />
                <label for="gameMode_3"><div>
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="m19.98 28.05 7.04-3.21V3.95l-7.03 3.21v20.89zm-7.96-3.21L19 27.73V6.84l-6.98-2.89v20.89zM3.98 7.16v20.89l7.04-2.9V4.28l-7.04 2.9z"/></svg>
                    <span>Map Fill</span>
                </div></label>
            </div>
            <div id="newGameErr">No countries match your game settings, please re-configure</div>
            <div id="regionCheckboxes" class="inputWrap">
                <label style="text-align: center">Region</label>
                <div class="inputWrap checkbox" style="padding-bottom: 10px;">
                    <input id="regionAll" type="checkbox" checked oninput="regionSettingUpdate(this);"/>
                    <label for="regionAll">All Regions</label>
                </div>
                <div class="inputWrap checkbox">
                    <input id="regionAfrica" type="checkbox" checked oninput="regionSettingUpdate(this);"/>
                    <label for="regionAfrica">Africa</label>
                </div>
                <div class="inputWrap checkbox">
                    <input id="regionAmericas" type="checkbox" checked oninput="regionSettingUpdate(this);"/>
                    <label for="regionAmericas">Americas</label>
                </div>
                <div class="inputWrap checkbox">
                    <input id="regionAsia" type="checkbox" checked oninput="regionSettingUpdate(this);"/>
                    <label for="regionAsia">Asia</label>
                </div>
                <div class="inputWrap checkbox">
                    <input id="regionEurope" type="checkbox" checked oninput="regionSettingUpdate(this);"/>
                    <label for="regionEurope">Europe</label>
                </div>
                <div class="inputWrap checkbox">
                    <input id="regionOceania" type="checkbox" checked oninput="regionSettingUpdate(this);"/>
                    <label for="regionOceania">Oceania</label>
                </div>
                <div class="inputWrap checkbox">
                    <input id="regionPolar" type="checkbox" checked oninput="regionSettingUpdate(this);"/>
                    <label for="regionPolar">Polar</label>
                </div>
            </div>
            <br>
            <div class="inputWrap checkbox">
                <input id="isRecognized" type="checkbox" />
                <label for="isRecognized">Recognized Countries Only</label>
            </div>
            <br>
            <div class="inputWrap checkbox">
                <input id="includeIslands" type="checkbox" />
                <label for="includeIslands">Include Tiny Islands</label>
            </div>
            <div id="settingGroup_1" class="settingGroup visible">
                <div class="inputWrap">
                    <label style="text-align: center;">Number of Options</label>
                    <div>
                        <input id="mc_numQuestions2" type="radio" name="mc_numQuestions" value="2" class="hidden" />
                        <label for="mc_numQuestions2" class="radioLabel">2</label>
                        <input id="mc_numQuestions3" type="radio" name="mc_numQuestions" value="3" class="hidden" />
                        <label for="mc_numQuestions3" class="radioLabel">3</label>
                        <input id="mc_numQuestions4" type="radio" name="mc_numQuestions" value="4" class="hidden" checked />
                        <label for="mc_numQuestions4" class="radioLabel">4</label>
                        <input id="mc_numQuestions5" type="radio" name="mc_numQuestions" value="5" class="hidden" />
                        <label for="mc_numQuestions5" class="radioLabel">5</label>
                        <input id="mc_numQuestions6" type="radio" name="mc_numQuestions" value="6" class="hidden" />
                        <label for="mc_numQuestions6" class="radioLabel">6</label>
                    </div>
                </div>
                <br><br>
                <div class="halfhalf">
                    <div>
                        <label>Question</label>
                        <div>
                            <input id="mc_question_1" type="radio" name="mc_question" value="map" oninput="onQuestionRadioChange(this)" class="hidden" checked />
                            <label for="mc_question_1" class="radioLabel">Map</label>
                        </div>
                        <div>
                            <input id="mc_question_2" type="radio" name="mc_question" value="flag" oninput="onQuestionRadioChange(this)" class="hidden" />
                            <label for="mc_question_2" class="radioLabel">Flag</label>
                        </div>
                        <div>
                            <input id="mc_question_3" type="radio" name="mc_question" value="name" oninput="onQuestionRadioChange(this)" class="hidden" />
                            <label for="mc_question_3" class="radioLabel">Country Name</label>
                        </div>
                        <div>
                            <input id="mc_question_4" type="radio" name="mc_question" value="capital" oninput="onQuestionRadioChange(this)" class="hidden" />
                            <label for="mc_question_4" class="radioLabel">Capital</label>
                        </div>
                        <div>
                            <input id="mc_question_5" type="radio" name="mc_question" value="shape" oninput="onQuestionRadioChange(this)" class="hidden" />
                            <label for="mc_question_5" class="radioLabel">Shape</label>
                        </div>
                    </div>
                    <div>
                        <label>Answer</label>
                        <div>
                            <input id="mc_answer_2" type="radio" name="mc_answer" value="flag" class="hidden" />
                            <label for="mc_answer_2" class="radioLabel">Flag</label>
                        </div>
                        <div>
                            <input id="mc_answer_3" type="radio" name="mc_answer" value="name" class="hidden" checked />
                            <label for="mc_answer_3" class="radioLabel">Country Name</label>
                        </div>
                        <div>
                            <input id="mc_answer_4" type="radio" name="mc_answer" value="capital" class="hidden" />
                            <label for="mc_answer_4" class="radioLabel">Capital</label>
                        </div>
                    </div>
                </div>
            </div>
            
            <div id="settingGroup_2" class="settingGroup">
                <div class="inputWrap checkbox">
                    <input id="fr_autocomplete" type="checkbox" checked />
                    <label for="fr_autocomplete">Autocomplete</label>
                </div>
                <br><br>
                <div class="halfhalf">
                    <div>
                        <label>Question</label>
                        <div>
                            <input id="fr_question_1" type="radio" name="fr_question" value="map" oninput="onQuestionRadioChange(this)" class="hidden" checked />
                            <label for="fr_question_1" class="radioLabel">Map</label>
                        </div>
                        <div>
                            <input id="fr_question_2" type="radio" name="fr_question" value="flag" oninput="onQuestionRadioChange(this)" class="hidden" />
                            <label for="fr_question_2" class="radioLabel">Flag</label>
                        </div>
                        <div>
                            <input id="fr_question_3" type="radio" name="fr_question" value="name" oninput="onQuestionRadioChange(this)" class="hidden" />
                            <label for="fr_question_3" class="radioLabel">Country Name</label>
                        </div>
                        <div>
                            <input id="fr_question_4" type="radio" name="fr_question" value="capital" oninput="onQuestionRadioChange(this)" class="hidden" />
                            <label for="fr_question_4" class="radioLabel">Capital</label>
                        </div>
                        <div>
                            <input id="fr_question_5" type="radio" name="fr_question" value="shape" oninput="onQuestionRadioChange(this)" class="hidden" />
                            <label for="fr_question_5" class="radioLabel">Shape</label>
                        </div>
                    </div>
                    <div>
                        <label>Answer</label>
                        <div>
                            <input id="fr_answer_3" type="radio" name="fr_answer" value="name" class="hidden" checked />
                            <label for="fr_answer_3" class="radioLabel">Country Name</label>
                        </div>
                        <div>
                            <input id="fr_answer_4" type="radio" name="fr_answer" value="capital" class="hidden" />
                            <label for="fr_answer_4" class="radioLabel">Capital</label>
                        </div>
                    </div>
                </div>
            </div>

            <div id="settingGroup_3" class="settingGroup">
                <div class="inputWrap checkbox">
                    <input id="fm_timer" type="checkbox" checked oninput="fmTimerInput(this)" />
                    <label for="fm_timer">Timer</label>
                </div> 
                <br>
                <div id="timeInput" class="inputWrap">
                    <label for="fm_time">Time (mins)</label>
                    <input id="fm_time" type="text" inputmode="numeric" spellcheck="false" autocomplete="off" autocapitalize="none" oninput="filterPosInt(this)" />
                </div>
                <div class="halfhalf">
                    <div>
                        <label>Question</label>
                        <div>
                            <input id="mf_question_1" type="radio" name="mf_question" value="map" oninput="onQuestionRadioChange(this)" class="hidden" checked />
                            <label for="mf_question_1" class="radioLabel">Map</label>
                        </div>
                    </div>
                    <div>
                        <label>Answer</label>
                        <div>
                            <input id="mf_answer_3" type="radio" name="mf_answer" value="name" class="hidden" checked />
                            <label for="mf_answer_3" class="radioLabel">Country Name</label>
                        </div>
                        <div>
                            <input id="mf_answer_4" type="radio" name="mf_answer" value="capital" class="hidden" />
                            <label for="mf_answer_4" class="radioLabel">Capital</label>
                        </div>
                    </div>
                </div>
            </div>
            
            <br><br>
            <div class="titlebtn"><div onclick="newGame()">Start Game</div></div>
            <div class="titlebtn"><div onclick="toScreen('home');">Back to Home</div></div>
            <br><br>
        </div>
    </div>
  
    <div id="game" class="screen">
        <svg id="fs_correct" class="feedbackSymbol" xmlns="http://www.w3.org/2000/svg" viewBox="0 -46 417.813 417"><path d="M159.988 318.582c-3.988 4.012-9.43 6.25-15.082 6.25s-11.094-2.238-15.082-6.25L9.375 198.113c-12.5-12.5-12.5-32.77 0-45.246l15.082-15.086c12.504-12.5 32.75-12.5 45.25 0l75.2 75.203L348.104 9.781c12.504-12.5 32.77-12.5 45.25 0l15.082 15.086c12.5 12.5 12.5 32.766 0 45.246zm0 0"/></svg>
        <svg id="fs_incorrect" class="feedbackSymbol" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 249.499 249.499"><path d="M7.079 214.851l25.905 26.276c9.536 9.674 25.106 9.782 34.777.252l56.559-55.761 55.739 56.548c9.542 9.674 25.112 9.782 34.78.246l26.265-25.887c9.674-9.536 9.788-25.106.246-34.786l-55.742-56.547 56.565-55.754c9.667-9.536 9.787-25.106.239-34.786L216.52 8.375c-9.541-9.667-25.111-9.782-34.779-.252l-56.568 55.761-55.74-56.553C59.891-2.337 44.32-2.451 34.65 7.079L8.388 32.971c-9.674 9.542-9.791 25.106-.252 34.786l55.745 56.553-56.55 55.767c-9.674 9.53-9.791 25.106-.252 34.774z"/></svg>
        <div class="gameBtns">
            <div class="floatingBtn" onclick="toScreen('inGameMenu');"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M139.6 0H46.5A46.6 46.6 0 0 0 0 46.5v418.9C0 491.1 20.9 512 46.5 512h93.1c25.7 0 46.5-20.9 46.5-46.5v-419C186.2 20.9 165.3 0 139.6 0zm325.9 0h-93.1c-25.7 0-46.5 20.9-46.5 46.5v418.9c0 25.7 20.9 46.5 46.5 46.5h93.1c25.7 0 46.5-20.9 46.5-46.5V46.5A46.6 46.6 0 0 0 465.5 0z"/></svg></div>
            <div id="revealCountriesBtn" class="floatingBtn" style="display: none;" onclick="identifyRemainingCountries();"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M479.55 265.75a27.28 27.28 0 0 1 0-19.52l19.47-51.1a27.4 27.4 0 0 0-10.63-32.72l-45.8-29.9a27.44 27.44 0 0 1-11.47-15.8l-14.3-52.8A27.43 27.43 0 0 0 389 43.67l-54.63 2.73a27.47 27.47 0 0 1-18.57-6.03L273.2 6.08a27.4 27.4 0 0 0-34.4 0l-42.6 34.3a27.48 27.48 0 0 1-18.57 6.03L123 43.68a27.43 27.43 0 0 0-27.83 20.23L80.9 116.7a27.48 27.48 0 0 1-11.48 15.8l-45.8 29.9a27.41 27.41 0 0 0-10.63 32.71l19.48 51.12c2.4 6.29 2.4 13.24 0 19.52l-19.48 51.12a27.42 27.42 0 0 0 10.63 32.7l45.8 29.92a27.35 27.35 0 0 1 11.48 15.79l14.28 52.8A27.42 27.42 0 0 0 123 468.3l54.63-2.72a27.38 27.38 0 0 1 18.57 6.02l42.6 34.32a27.4 27.4 0 0 0 34.4 0l42.6-34.32a27.37 27.37 0 0 1 18.56-6.02l54.63 2.72a27.42 27.42 0 0 0 27.83-20.23l14.3-52.8a27.3 27.3 0 0 1 11.47-15.78l45.8-29.91A27.4 27.4 0 0 0 499 316.88l-19.46-51.13zM288.78 369.13a18.94 18.94 0 0 1-18.93 18.94h-27.7a18.95 18.95 0 0 1-18.94-18.94V340.2a18.95 18.95 0 0 1 18.94-18.94h27.7c10.45 0 18.93 8.49 18.93 18.94v28.92zm0-100.18a18.93 18.93 0 0 1-18.93 18.93h-27.7a18.94 18.94 0 0 1-18.94-18.93V142.88a18.94 18.94 0 0 1 18.94-18.94h27.7a18.93 18.93 0 0 1 18.93 18.94v126.07z"/></svg></div>
        </div>
        <div id="gameTimer"><div id="gameTimerValue"></div><span id="gameTimerStat"></span></div>
        <div id="map" class="map"></div>
        <div id="answerBank">
            <div id="fr_answer">
                <div id="fr_suggestions"></div>
                <input id="fr_answer_input" type="text" inputmode="search" spellcheck="false" autocomplete="off" autocapitalize="none" placeholder="Your Answer" oninput="onFRInput(this)" onkeydown="onFRKeyDown(event, this)" onfocus="onFRFocus(this)" onblur="onFRBlur(this)" />
            </div>
        </div>
    </div>
    <div id="inGameMenu" class="screen paddedContent">
        <div class="gameBtns">
            <div id="inGameMenuBtn_back" class="floatingBtn red smaller" onclick="toScreen('game');"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 329.26933 329"><path d="m194.800781 164.769531 128.210938-128.214843c8.34375-8.339844 8.34375-21.824219 0-30.164063-8.339844-8.339844-21.824219-8.339844-30.164063 0l-128.214844 128.214844-128.210937-128.214844c-8.34375-8.339844-21.824219-8.339844-30.164063 0-8.34375 8.339844-8.34375 21.824219 0 30.164063l128.210938 128.214843-128.210938 128.214844c-8.34375 8.339844-8.34375 21.824219 0 30.164063 4.15625 4.160156 9.621094 6.25 15.082032 6.25 5.460937 0 10.921875-2.089844 15.082031-6.25l128.210937-128.214844 128.214844 128.214844c4.160156 4.160156 9.621094 6.25 15.082032 6.25 5.460937 0 10.921874-2.089844 15.082031-6.25 8.34375-8.339844 8.34375-21.824219 0-30.164063zm0 0"></path></svg></div>
        </div>
        <div class="title">Menu</div>
        <div id="menuStat"><div>0%</div><div>0 / 0</div></div>
        <br>
        <div id="shareBtn" onclick="shareScore(event);"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12v5a3 3 0 0 0 3 3h8a3 3 0 0 0 3-3v-5m-7 4V4m0 0L8 8m4-4 4 4"/></svg><div>Share</div></div>
        <br>
        <div id="menuSummary"></div>
        <br><br>
        <div id="inGameMenuBtn_resume" class="titlebtn"><div onclick="resumeGame();">Resume</div></div>
        <br>
        <div id="inGameMenuBtn_new" class="titlebtn"><div onclick="if(game.ended || confirm('Are you sure you want to restart the game?'))newGame(true)">Replay</div></div>
        <div class="titlebtn"><div onclick="toScreen('home');">Home Screen</div></div>
        <br><br>
    </div>
    <div id="explore" class="screen">
        <div id="exploreSearch" onclick="toggleExploreSearch()">
            <div onclick="event.stopPropagation();">
                <div id="searchBarWrapper">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M10 .5C4.75329.5.5 4.75329.5 10c0 5.2467 4.25329 9.5 9.5 9.5 2.082 0 4.0076-.6698 5.5731-1.8056l4.7198 4.7198c.3905.3905 1.0237.3905 1.4142 0l.7071-.7071c.3905-.3905.3905-1.0237 0-1.4142l-4.7198-4.7198C18.8302 14.0076 19.5 12.082 19.5 10c0-5.24671-4.2533-9.5-9.5-9.5ZM3.5 10c0-3.58985 2.91015-6.5 6.5-6.5 3.5899 0 6.5 2.91015 6.5 6.5 0 3.5899-2.9101 6.5-6.5 6.5-3.58985 0-6.5-2.9101-6.5-6.5Z" clip-rule="evenodd"/></svg>
                    <input id="countrySearch" type="text" inputmode="search" spellcheck="false" autocomplete="off" autocapitalize="none" placeholder="Search for a country" oninput="onExploreSearchInput(this);" onkeydown="onExploreSearchKeyDown(event, this);" />
                </div>
                <div id="exploreSearchSuggestions"></div>
            </div>
        </div>
        <div id="exploreMenu">
            <div><svg onclick="hideExploreDetails();deleteExploreMap();toScreen('home');" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52" xml:space="preserve"><path d="M48.6 23H15.4c-.9 0-1.3-1.1-.7-1.7l9.6-9.6c.6-.6.6-1.5 0-2.1l-2.2-2.2c-.6-.6-1.5-.6-2.1 0L2.5 25c-.6.6-.6 1.5 0 2.1L20 44.6c.6.6 1.5.6 2.1 0l2.1-2.1c.6-.6.6-1.5 0-2.1l-9.6-9.6c-.6-.7-.2-1.8.7-1.8h33.2c.8 0 1.5-.6 1.5-1.4v-3c0-.8-.6-1.6-1.4-1.6z"/></svg></div>
            <div><svg onclick="toggleExploreSearch();" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M10 .5C4.75329.5.5 4.75329.5 10c0 5.2467 4.25329 9.5 9.5 9.5 2.082 0 4.0076-.6698 5.5731-1.8056l4.7198 4.7198c.3905.3905 1.0237.3905 1.4142 0l.7071-.7071c.3905-.3905.3905-1.0237 0-1.4142l-4.7198-4.7198C18.8302 14.0076 19.5 12.082 19.5 10c0-5.24671-4.2533-9.5-9.5-9.5ZM3.5 10c0-3.58985 2.91015-6.5 6.5-6.5 3.5899 0 6.5 2.91015 6.5 6.5 0 3.5899-2.9101 6.5-6.5 6.5-3.58985 0-6.5-2.9101-6.5-6.5Z" clip-rule="evenodd"/></svg></div>
        </div>
        <div id="exploreMap"></div>
        <div id="exploreDetails">
            <div id="resizeTarget" onmousedown="resizeExploreDetails(event, this)" ontouchstart="resizeExploreDetails(event, this)"></div>
            <svg onclick="hideExploreDetails();" class="closeExploreDetails" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.49 2 2 6.49 2 12s4.49 10 10 10 10-4.49 10-10S17.51 2 12 2Zm3.36 12.3c.29.29.29.77 0 1.06a.74.74 0 0 1-.53.22.74.74 0 0 1-.53-.22l-2.3-2.3-2.3 2.3a.74.74 0 0 1-.53.22.74.74 0 0 1-.53-.22.75.75 0 0 1 0-1.06l2.3-2.3-2.3-2.3a.75.75 0 0 1 0-1.06.75.75 0 0 1 1.06 0l2.3 2.3 2.3-2.3a.75.75 0 0 1 1.06 0c.29.29.29.77 0 1.06l-2.3 2.3 2.3 2.3Z" /></svg>
            <div class="exploreTitle">
                <img id="exploreFlag" height="64" style="display: none;" />
                <div style="min-width: 150px">
                    <div id="exploreCountryName">Explore</div>
                    <div id="exploreCountrySubtitle">
                        <svg id="exploreRecognized" title="Recognized Country" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21.6 9.8a4.6 4.6 0 0 1-.4-.8 4 4 0 0 1-.2-1 4.2 4.2 0 0 0-.6-2 4.3 4.3 0 0 0-2-1.4 4.8 4.8 0 0 1-.8-.4 5.1 5.1 0 0 1-.6-.7 4.2 4.2 0 0 0-1.8-1.4 4.2 4.2 0 0 0-2.2.1 4.2 4.2 0 0 1-2 0 4.2 4.2 0 0 0-2.2 0A4.2 4.2 0 0 0 7 3.4a5.1 5.1 0 0 1-.7.7 4.8 4.8 0 0 1-.8.4A4.3 4.3 0 0 0 3.6 6 4.2 4.2 0 0 0 3 8.1a4 4 0 0 1-.2.9 4.6 4.6 0 0 1-.4.8 4.3 4.3 0 0 0-.8 2.2 4.3 4.3 0 0 0 .8 2.2 4 4 0 0 1 .4.8 4.1 4.1 0 0 1 .2 1 4.2 4.2 0 0 0 .6 2 4.3 4.3 0 0 0 1.9 1.4 4.8 4.8 0 0 1 .8.4 5.1 5.1 0 0 1 .7.7 4.1 4.1 0 0 0 1.8 1.4 3 3 0 0 0 .9 0 6.7 6.7 0 0 0 1.3 0 4 4 0 0 1 2 0 4.3 4.3 0 0 0 2.2 0 4.1 4.1 0 0 0 1.8-1.4 5.1 5.1 0 0 1 .6-.7 4.8 4.8 0 0 1 .9-.4 4.3 4.3 0 0 0 1.9-1.3 4.2 4.2 0 0 0 .6-2.2 4.1 4.1 0 0 1 .1-1 4.6 4.6 0 0 1 .5-.7 4.3 4.3 0 0 0 .8-2.2 4.3 4.3 0 0 0-.8-2.2Zm-4.9 1-5 5a1 1 0 0 1-1.4 0l-3-3a1 1 0 1 1 1.4-1.5l2.3 2.3 4.3-4.3a1 1 0 0 1 1.4 1.4Z"/></svg>
                        <span id="exploreCountryCodes">Click a country to get information</span>
                    </div>
                </div>
            </div>
            <div id="exploreFacts" style="display: none">
                <div id="exploreFactGrid" >
                    <div>
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><path d="M7 0c-.3 0-.5.2-.5.5v4C3.9 4.7 3 6.1 3 7h8c0-1-1-2.3-3.5-2.5v-4C7.5.2 7.2 0 7 0zm1 0v3h4l-2-1.5L12 0H8zM1 8v1h1v4H1l-1 1h14l-1-1h-1V9h1V8H1zm3 1h1v4H4V9zm2 0h2v4H6V9zm3 0h1v4H9V9z"/></svg>
                        <div>
                            <span>Capital</span>
                            <span id="exploreDetailCapital"></span>
                        </div>
                    </div>
                    <div id="exploreDetailSovereigntyWrapper">
                        <img id="exploreDetailSovereigntyImg" />
                        <div>
                            <span>Sovereignty</span>
                            <span id="exploreDetailSovereignty"></span>
                        </div>
                    </div>
                    <div>
                        <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 24 24"><path d="M12 2a7 7 0 0 0-7 7c0 6 7 13 7 13s7-7.1 7-13a7 7 0 0 0-7-7zm0 9.5c-1.4 0-2.5-1.1-2.5-2.5s1.1-2.5 2.5-2.5 2.5 1.1 2.5 2.5-1.1 2.5-2.5 2.5z"/><path fill="none" d="M0 0h24v24H0z"/></svg>
                        <div>
                            <span>Region</span>
                            <span id="exploreDetailRegion"></span>
                        </div>
                    </div>
                    <div>
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill-rule="evenodd" d="M5.2 14a2.2 2.2 0 0 1-.2-1c0-1.4.7-2.8 2-3.7A6.3 6.3 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.2z"/><path d="M4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/></svg>
                        <div>
                            <span>Population</span>
                            <span id="exploreDetailPopulation"></span>
                        </div>
                    </div>
                    <div>
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M160 288h-56a8 8 0 0 1-8-8v-16a8 8 0 0 1 8-8h56v-64h-56a8 8 0 0 1-8-8v-16a8 8 0 0 1 8-8h56V96h-56a8 8 0 0 1-8-8V72a8 8 0 0 1 8-8h56V32a32 32 0 0 0-32-32H32A32 32 0 0 0 0 32v448c0 2.8 1 5.2 1.6 7.8L160 329.4V288zm320 64h-32v56a8 8 0 0 1-8 8h-16a8 8 0 0 1-8-8v-56h-64v56a8 8 0 0 1-8 8h-16a8 8 0 0 1-8-8v-56h-64v56a8 8 0 0 1-8 8h-16a8 8 0 0 1-8-8v-56h-41.4L24.2 510.4c2.6.7 5 1.6 7.8 1.6h448a32 32 0 0 0 32-32v-96a32 32 0 0 0-32-32z"/></svg>
                        <div>
                            <span>Area (km<sup>2</sup>)</span>
                            <span id="exploreDetailArea"></span>
                        </div>
                    </div>
                    <div>
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M11 2a1 1 0 0 1 2 0v2a8 8 0 0 1 7 7h2a1 1 0 0 1 0 2h-2a8 8 0 0 1-7 7v2a1 1 0 0 1-2 0v-2a8 8 0 0 1-7-7H2a1 1 0 0 1 0-2h2a8 8 0 0 1 7-7V2zm7 10a6 6 0 1 0-12 0 6 6 0 0 0 12 0zm-3 0a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" clip-rule="evenodd"/></svg>
                        <div>
                            <span>Center</span>
                            <span id="exploreDetailCenter"></span>
                        </div>
                    </div>
                </div>
                <div id="bordersWrapper" class="exploreSection">
                    <div class="exploreSectionTitle" onclick="this.classList.toggle('visible');">
                        <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M512 273.2V242h-24v-28l-46.3-109.1-46.3 109v28h-26.3v-28l-46.3-109-46.3 109v28h-25.4v-28l-46.3-109-46.3 109v28h-25.4v-28l-46.3-109-46.3 109v28H0v31.4h40.5v35H0v31.3h40.5v67.7h92.6v-67.7h25.4v67.7h92.6v-67.7h25.4v67.7h92.6v-67.7h26.3v67.7H488v-67.7h24V308h-24v-34.9h24zm-353.5 35h-25.4v-35h25.4v35zm118 0h-25.4v-35h25.4v35zm119 0H369v-35h26.3v35z"/></svg>
                        <span>Borders</span>
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.5 0 32 32"><path d="m18.8 11.4-7.9 10c-.7.8-1.9.8-2.6 0l-8-10c-.7-1-.3-1.6.9-1.6H18c1.2 0 1.6.7.8 1.6z"/></svg>
                    </div>
                    <div class="exploreSectionContent">
                        <div id="borderCountries"></div>
                    </div>
                </div>
                <div id="territoriesWrapper" class="exploreSection">
                    <div class="exploreSectionTitle" onclick="this.classList.toggle('visible');">
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="none" d="M0 0h20v20H0z"/><path d="M10 2s3 2 7 2c0 11-7 14-7 14S3 15 3 4c4 0 7-2 7-2zm0 8h5s1-1 1-5c0 0-5-1-6-2v7H5c1 4 5 7 5 7v-7z"/></svg>
                        <span>Territories</span>
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.5 0 32 32"><path d="m18.8 11.4-7.9 10c-.7.8-1.9.8-2.6 0l-8-10c-.7-1-.3-1.6.9-1.6H18c1.2 0 1.6.7.8 1.6z"/></svg>
                    </div>
                    <div class="exploreSectionContent">
                        <div id="territoryCountries"></div>
                    </div>
                </div>
                <div class="exploreSection">
                    <div class="exploreSectionTitle" onclick="this.classList.toggle('visible');">
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><path d="M9 4v17h9v6h5V4H9zm3.9 4h2v2h-2V8zm4 0h2v2h-2V8zM34 8v38h6v-4h2v4h6V8H34zm-21.1 4h2v2h-2v-2zm4 0h2v2h-2v-2zM42 12h2v2h-2v-2zm-4 0h2v2h-2v-2zm0 4h2v2h-2v-2zm4 0h2v2h-2v-2zm-4 4h2v2h-2v-2zm4 0h2v2h-2v-2zM2 23v23h6v-4h2v4h6V23H2zm36 1h2v2h-2v-2zm4 0h2v2h-2v-2zM6 26h2v2H6v-2zm4 0h2v2h-2v-2zm28 2h2v2h-2v-2zm4 0h2v2h-2v-2zm-24 1v17h6v-4h2v4h6V29H18zM6 30h2v2H6v-2zm4 0h2v2h-2v-2zm28 2h2v2h-2v-2zm4 0h2v2h-2v-2zm-20 0h2v2h-2v-2zm4 0h2v2h-2v-2zM6 34h2v2H6v-2zm4 0h2v2h-2v-2zm28 2h2v2h-2v-2zm4 0h2v2h-2v-2zm-16 0h2v2h-2v-2zm-4 0h2v2h-2v-2z"/></svg>
                        <span>Most Populated Cities</span>
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.5 0 32 32"><path d="m18.8 11.4-7.9 10c-.7.8-1.9.8-2.6 0l-8-10c-.7-1-.3-1.6.9-1.6H18c1.2 0 1.6.7.8 1.6z"/></svg>
                    </div>
                    <div id="explorePopulatedCities" class="exploreSectionContent"></div>
                </div>
                <div class="exploreSection">
                    <div class="exploreSectionTitle" onclick="this.classList.toggle('visible');">
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="m29 19.6-9.4 9.3a2 2 0 0 1-2.9 0L5.3 17.5 2.5 8.7l2-2 4 4a2.5 2.5 0 0 0 4.3 2 2.5 2.5 0 0 0-2.1-4.2l-4-4 2-2 8.8 2.8 11.4 11.4c.8.8.8 2 0 2.9zm-17.6-8.3c-.2.2-.6.2-.8 0L2.8 3.5a.5.5 0 1 1 .7-.8l7.9 8c.2.1.2.4 0 .6z"/></svg>
                        <span>Alternative Names</span>
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.5 0 32 32"><path d="m18.8 11.4-7.9 10c-.7.8-1.9.8-2.6 0l-8-10c-.7-1-.3-1.6.9-1.6H18c1.2 0 1.6.7.8 1.6z"/></svg>
                    </div>
                    <div id="exploreAlternativeNames" class="exploreSectionContent"></div>
                </div>
                <div class="exploreSection">
                    <div class="exploreSectionTitle" onclick="this.classList.toggle('visible');">
                        <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 24 24"><path d="M12 24C5.4 24 0 18.6 0 12S5.4 0 12 0s12 5.4 12 12-5.4 12-12 12zm-2.5-7c.6 3.1 1.7 5 2.5 5s1.9-1.9 2.5-5h-5zm7.1 0a17 17 0 0 1-1.4 4.5c2.3-.8 4.3-2.4 5.5-4.5h-4.1zM3.3 17c1.2 2.1 3.2 3.7 5.5 4.5A17 17 0 0 1 7.4 17H3.3zm13.6-2h4.7a10 10 0 0 0-.1-6h-4.7c.2 1 .2 2 .2 3l-.1 3zm-7.7 0h5.7a26.6 26.6 0 0 0 0-6H9.2a26.6 26.6 0 0 0 0 6zm-6.7 0h4.7l-.1-3 .1-3H2.5c-.3.9-.5 2-.5 3s.2 2.1.5 3zm14.1-8h4.1a10.2 10.2 0 0 0-5.5-4.5A17 17 0 0 1 16.6 7zM9.5 7h5.1c-.6-3.1-1.7-5-2.5-5s-2 1.9-2.6 5zM3.3 7h4.1a17 17 0 0 1 1.4-4.5C6.5 3.3 4.6 4.9 3.3 7z"/></svg>
                        <span>Languages</span>
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="-6.5 0 32 32"><path d="m18.8 11.4-7.9 10c-.7.8-1.9.8-2.6 0l-8-10c-.7-1-.3-1.6.9-1.6H18c1.2 0 1.6.7.8 1.6z"/></svg>
                    </div>
                    <div id="exploreDetailsLanguages" class="exploreSectionContent"></div>
                </div>
            </div>
        </div>
    </div>
    <div id="loading" class="screen centered">
        <div class="title loaderText">LOADING</div>
        <div class="loader"></div>
    </div>
</body>
</html>







script.js:
let map, canvasLayer, exploreMap, highlighted = [];
let game = {}, settings = {};
let geoJSON, geometry = {}, topCities, exploreGeometry = {}, countries = [], subregions = {}, regions = {}, nameToCode = {}, autocompleteCountries = [], capitalToCode = {}, autocompleteCapitals = [], countryStyle = {};
let aliases, countryData, timerInterval, alertIcon;
let exploreCountry;
const transitionTime = 1000;

const exploreDetailsBreakPoint = 1000;
const exploreDetailsHeightBreakPoint = 900;

const tinyIslands = ["ABW","AIA","ALA","ASM","ATF","ATG","BES","BHR","BHS","BLM","BMU","BRB","BRN","BVT","CCK","COK","COM","CPV","CUW","CXR","CYM","DMA","FJI","FLK","FRO","FSM","GGY","GLP","GRD","GUM","HMD","IMN","IOT","JEY","JAM","KIR","KNA","LCA","MAF","MDV","MHL","MLT","MNP","MSR","MTQ","MUS","MYT","NFK","NIU","NRU","PCN","PLW","PYF","REU","SGS","SHN","SJM","SLB","SPM","STP","SXM","SYC","TCA","TKL","TLS","TON","TTO","TUV","VCT","VGB","VIR","VUT","WLF","WSM"];
let extendedNames = {"ALAND":["ALA"],"SAMOA":["ASM"],"ANTIGUA":["ATG"],"BARBUDA":["ATG"],"BENGAL":["BGD"],"BIM":["BRB"],"GUDIJA":["BLR"],"DAHOMEY":["BEN"],"BONAIRE":["BES"],"EUSTATIUS":["BES"],"SABA":["BES"],"BOSNIA":["BIH"],"HERZEGOVINA":["BIH"],"BRASIL":["BRA"],"BRITISHINDIAN":["IOT"],"VIRGIN":["VGB","VIR"],"BRITISHVIRGIN":["VGB"],"UKVIRGIN":["VGB"],"USVIRGIN":["VIR"],"UNITEDSTATESVIRGIN":["VIR"],"DARUSSALAM":["BRN"],"BOURKINAFASSO":["BFA"],"KAMPUCHEA":["KHM"],"KHMER":["KHM"],"TCHAD":["TCD"],"CHILLI":["CHL"],"CHILI":["CHL"],"KEELING":["CCK"],"KOMORI":["COM"],"BRAZZAVILLE":["COG"],"DRC":["COD"],"KINSHASA":["COD"],"HRVATSKA":["HRV"],"CZECH":["CZE"],"BOHEMIA":["CZE"],"DANMARK":["DNK"],"DR":["DOM"],"DOMINICAN":["DOM"],"DOMINICANA":["DOM"],"REPUBLICADOMINICANA":["DOM"],"KIMI":["EGY"],"GUINEAEQUATORIAL":["GNQ"],"EESTI":["EST"],"ABYSSINIA":["ETH"],"MALVINAS":["FLK"],"SUOMI":["FIN"],"GUYANE":["GUF"],"POLYNESIA":["PYF"],"FRENCHSOUTHERN":["ATF"],"IVERIA":["GEO"],"DEUTSCHLAND":["DEU"],"GDR":["DEU"],"BRD":["DEU"],"GOLDCOAST":["GHA"],"HELLADA":["GRC"],"HELLAS":["GRC"],"BISSAU":["GIN"],"HEARD":["HMD"],"MCDONALD":["HMD"],"VATICAN":["VAT"],"BHARAT":["IND"],"NUSANTARA":["IDN"],"DUTCHEASTINDIES":["IDN"],"INDUNESIA":["IDN"],"IVORYCOAST":["CIV"],"EIRE":["IRL"],"ZION":["ISR"],"ITALIANA":["ITA"],"ITALIA":["ITA"],"XAMAYCA":["JAM"],"NIPPON":["JPN"],"CHANNEL":["JEY","GGY"],"KYRGYZ":["KGZ"],"LAO":["LAO"],"LATVIJA":["LVA"],"MACEDONIA":["MKD"],"MOLDAVIA":["MDA"],"BURMA":["MMR"],"MARIANA":["MNP"],"NORGE":["NOR"],"FILIPINAS":["PHL"],"POLSKA":["POL"],"RF":["RUS"],"SOVIETUNION":["RUS"],"RUSSIANFEDERATION":["RUS"],"RUANDA":["RWA"],"HELENA":["SHN"],"ASCENSION":["SHN"],"TRISTANDACUNHA":["SHN"],"KITTS":["KNA"],"NEVIS":["KNA"],"MARTIN":["MAF","SXM"],"PIERRE":["SPM"],"MIQUELON":["SPM"],"VINCENT":["VCT"],"GRENADINES":["VCT"],"SAOTOME":["STP"],"PRINCIPE":["STP"],"SOUTHGEORGIA":["SGS"],"SANDWHICH":["SGS"],"HISPANIA":["ESP"],"ESPANA":["ESP"],"EASTTIMOR":["TLS"],"TIMOR":["TLS"],"TRINIDAD":["TTO"],"TOBAGO":["TTO"],"CAICOS":["TCA"],"UAE":["ARE"],"UK":["GBR"],"BRITAIN":["GBR"],"GREATBRITAIN":["GBR"],"USA":["USA"],"AMERICA":["USA"],"WALLIS":["WLF"],"FUTUNA":["WLF"],"SAHRAWI":["ESH"],"MOROCCO":["ESH"],"CABOVERDE":["CPV"],"MACAU":["MAC"]};
let extendedCapitals = {};

const defaultGame = {
    mode: 1,
    ended: true,
    modeData: {q: 'map', a: 'name', numQuestions: 4, autocomplete: true, maxTime: 900},
    includeIslands: false,
    recognizedOnly: true,
    maxZoom: 16,
    regions: ['Africa', 'Americas', 'Asia', 'Europe', 'Oceania', 'Polar'],
    countryPool: [],
    answered: 0,
    correct: 0,
    history: [],
    isPaused: false,
    timerValue: 0,
    timer: false,
};
const defaultSettings = {
    audio: true,
    speak: false,
};
const defaultCountryStyle = {
    weight: 2,
    color: "black",
    fill: false,
    fillColor: "var(--accent)",
    fillOpacity: .3,
    interactive: false
};
const defaultHighlightedCountryStyle = {
    weight: 3,
    fillOpacity: .3,
    color: "var(--accent)",
    fill: true
};
const defaultHiddenCountryStyle = {
    stroke: false,
    fillOpacity: .2,
    fill: true
};
const exploreCountryStyle = {
    weight: 2,
    color: "black",
    fill: true,
    fillOpacity: 0,
    interactive: true,
    bubblingMouseEvents: false,
    opacity: 1,
};
const exploreCountryHoveredStyle = {
    weight: 2,
    fillOpacity: .2,
    color: "black",
    fillColor: "var(--accent3)",
};
const exploreCountryHighlightedStyle = {
    weight: 2,
    fillOpacity: .4,
    color: "var(--accent)",
    fillColor: "var(--accent)",
};
const autocompleteOptions = {
    limit: 5,
    allowTypo: true,
    threshold: -10000,
};
const defaultMapOptions = {
    attributionControl: false,
    zoomControl: false,
    boxZoom: false,
    zoomDelta: .5,
    zoomSnap: .5,
    bounceAtZoomLimits: false,
    maxBoundsViscosity: .5,
    keyboard: false,
    doubleClickZoom: true,
    touchZoom: true,
    scrollWheelZoom: false, // disable original zoom function
    smoothWheelZoom: true,  // enable smooth zoom 
    smoothSensitivity: 1,   // zoom speed. default is 1
};
const exploreMapOptions = {
    attributionControl: false,
    zoomControl: false,
    boxZoom: true,
    zoomDelta: .5,
    zoomSnap: .5,
    maxBounds: [[-90, -180],[90, 180]],
    minZoom: 1,
    bounceAtZoomLimits: false,
    maxBoundsViscosity: .5,
    keyboard: true,
    doubleClickZoom: true,
    touchZoom: true,
    scrollWheelZoom: false, // disable original zoom function
    smoothWheelZoom: true,  // enable smooth zoom 
    smoothSensitivity: 1,   // zoom speed. default is 1
};
const centerLatLng = [43.06888777,-15.46875];
const centerZoom = 3;


//Event Handlers
window.addEventListener('load', function() {
    if('serviceWorker' in navigator) {
        navigator.serviceWorker.register('serviceWorker.js', {scope: '.'})
        .then(function (registration) {}, function (err) {
            console.error('PWA: ServiceWorker registration failed: ', err);
        });
    }
  
    settings = defaultSettings;
    setSettingElements();

    onResize();
    if(window.visualViewport) {
        window.visualViewport.addEventListener('resize', onResize);
    }
    else {
        window.addEventListener('resize', onResize);
    }
    window.addEventListener('orientationchange', onResize);
    window.addEventListener('mousemove', onMouseMove);
    window.addEventListener('mouseup', onMouseUp);
    window.addEventListener('touchmove', onMouseMove);
    window.addEventListener('touchend', onMouseUp);
    window.addEventListener('touchcancel', onMouseUp);
    window.addEventListener('keydown', function(event) {
        if(event.key === 'Escape' && !game.ended && document.getElementById('inGameMenu').classList.contains('visible')) {
            resumeGame();
        }
        else if(event.key === 'Escape' && !game.ended && document.getElementById('game').classList.contains('visible')) {
            toScreen('inGameMenu');
        }
        else if(event.key === 'Escape' && (game.ended || game.isPaused)) {
            toScreen('home');
        }
        if(event.key === '/' && exploreMap) {
            toggleExploreSearch();
        }
    });

    alertIcon = L.divIcon({
        html: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M479.55 265.75a27.28 27.28 0 0 1 0-19.52l19.47-51.1a27.4 27.4 0 0 0-10.63-32.72l-45.8-29.9a27.44 27.44 0 0 1-11.47-15.8l-14.3-52.8A27.43 27.43 0 0 0 389 43.67l-54.63 2.73a27.47 27.47 0 0 1-18.57-6.03L273.2 6.08a27.4 27.4 0 0 0-34.4 0l-42.6 34.3a27.48 27.48 0 0 1-18.57 6.03L123 43.68a27.43 27.43 0 0 0-27.83 20.23L80.9 116.7a27.48 27.48 0 0 1-11.48 15.8l-45.8 29.9a27.41 27.41 0 0 0-10.63 32.71l19.48 51.12c2.4 6.29 2.4 13.24 0 19.52l-19.48 51.12a27.42 27.42 0 0 0 10.63 32.7l45.8 29.92a27.35 27.35 0 0 1 11.48 15.79l14.28 52.8A27.42 27.42 0 0 0 123 468.3l54.63-2.72a27.38 27.38 0 0 1 18.57 6.02l42.6 34.32a27.4 27.4 0 0 0 34.4 0l42.6-34.32a27.37 27.37 0 0 1 18.56-6.02l54.63 2.72a27.42 27.42 0 0 0 27.83-20.23l14.3-52.8a27.3 27.3 0 0 1 11.47-15.78l45.8-29.91A27.4 27.4 0 0 0 499 316.88l-19.46-51.13zM288.78 369.13a18.94 18.94 0 0 1-18.93 18.94h-27.7a18.95 18.95 0 0 1-18.94-18.94V340.2a18.95 18.95 0 0 1 18.94-18.94h27.7c10.45 0 18.93 8.49 18.93 18.94v28.92zm0-100.18a18.93 18.93 0 0 1-18.93 18.93h-27.7a18.94 18.94 0 0 1-18.94-18.93V142.88a18.94 18.94 0 0 1 18.94-18.94h27.7a18.93 18.93 0 0 1 18.93 18.94v126.07z"/></svg>',
        bgPos: [12, 12],
        className: 'countryMarker',
        iconSize: [24, 24]
    });
    
    // Look for share link
    const params = new Proxy(new URLSearchParams(window.location.search), {
        get: (searchParams, prop) => searchParams.get(prop),
    });
    if(params.game) {
        var g = urlToGame(params.game);
        if(g) {
            toScreen('newGame');
            game = g;
        }
        else {
            alert("Invalid share url");
        }
        this.history.replaceState(null, null, window.location.href.replace(window.location.search, '').replace(/\/*$/, '/'));
    }
    else {
        game = Object.assign({}, defaultGame);
    }
    setUIFromGame(game);
});
var lastWidth;
function onResize(event) {
    document.body.scrollTop = document.documentElement.scrollTop = 0;
    if(window.visualViewport) {
        document.documentElement.style.height = document.body.style.height = window.visualViewport.height + "px";
    }
    else {
        document.documentElement.style.height = document.body.style.height = window.innerHeight + "px";
    }
    if(map) {map.invalidateSize();}
    if(exploreMap) {exploreMap.invalidateSize();}
    if(lastWidth) {
        if(document.body.offsetWidth <= exploreDetailsBreakPoint && lastWidth > exploreDetailsBreakPoint) {
            document.getElementById('explore').style.gridTemplateRows = 'auto ' + (document.body.offsetHeight < exploreDetailsHeightBreakPoint ? '50' : '40') + '%';
            document.getElementById('explore').style.gridTemplateColumns = '1fr';
        }
        else if(document.body.offsetWidth > exploreDetailsBreakPoint && lastWidth <= exploreDetailsBreakPoint) {
            document.getElementById('explore').style.gridTemplateColumns = 'auto 350px';
            document.getElementById('explore').style.gridTemplateRows = '1fr';
        }
    }
    lastWidth = document.body.offsetWidth;
}

//General UI
function toScreen(screen) {
    var e = document.getElementById(screen);
    if(e) {
        document.querySelectorAll('.screen.visible').forEach(function(e) {
            e.classList.remove('visible');
        });

        // Special cases
        if(screen === 'home') {
            document.getElementById('resumeGameBtn').style.display = game.ended ? 'none' : 'block';
        }
        else if(screen === 'game') {
            game.isPaused = false;
        }
        else if(screen === 'inGameMenu') {
            setSummaryData();
            game.isPaused = true;
        }
        e.classList.add('visible');
    }
}

//Game Setup
function onGameModeChange(input) {
    var e = document.getElementById('settingGroup_'+input.value);
    if(e) {
        document.querySelectorAll('.settingGroup.visible').forEach(function(e) {
            e.classList.remove('visible'); 
        });
        e.classList.add('visible');
    }
}
function onQuestionRadioChange(elem) {
    var checkAnother = false;
    var answerElems = document.getElementsByName(elem.name.split('_')[0] + '_answer');

    for(var i = 0;i < answerElems.length;++i) {
        if(elem.id.split('_')[2] === answerElems[i].id.split('_')[2]) {
            checkAnother = answerElems[i].checked;
            answerElems[i].disabled = true;
        }
        else {answerElems[i].disabled = false;}
    }
    if(checkAnother) {
        for(var i = 0;i < answerElems.length;++i) {
            if(!answerElems[i].disabled) {
                answerElems[i].checked = true;
                return;
            }
        }
    }
}
function regionSettingUpdate(elemChanged) {
    var checkboxes = document.querySelectorAll('#regionCheckboxes input:not(#regionAll)');
    var allCheckbox = document.getElementById('regionAll');
    if(elemChanged === allCheckbox) {
        for(var i = 0; i < checkboxes.length; ++i) {
            checkboxes[i].checked = allCheckbox.checked;
        }
    }
    else {
        var allChecked = true;
        for(var i = 0; i < checkboxes.length; ++i) {
            if(!checkboxes[i].checked) {allChecked = false; break;}
        }
        allCheckbox.checked = allChecked;
    }
}
function setRegionSetting(regions) {
    var checkboxes = document.querySelectorAll('#regionCheckboxes input:not(#regionAll)');
    for(var i = 0; i < checkboxes.length; ++i) {
        checkboxes[i].checked = regions.indexOf(checkboxes[i].id.substring(6)) > -1;
    }
    regionSettingUpdate();
}
function getRegionSetting() {
    var checkboxes = document.querySelectorAll('#regionCheckboxes input:not(#regionAll)');
    var regions = [];
    for(var i = 0; i < checkboxes.length; ++i) {
        if(checkboxes[i].checked) {
            regions.push(checkboxes[i].id.substring(6));
        }
    }
    return regions;
}
function setUIFromGame(g) {
    setRegionSetting(g.regions);
    document.getElementById('includeIslands').checked = g.includeIslands;
    document.getElementById('isRecognized').checked = g.recognizedOnly;
    document.getElementById('fr_autocomplete').checked = g.modeData.autocomplete;
    document.getElementById('resumeGameBtn').style.display = g.ended ? 'none' : 'block';
    document.getElementById('fm_timer').checked = g.timer;
    document.getElementById('fm_time').value = Math.round(g.modeData.maxTime / 60);
    fmTimerInput(document.getElementById('fm_timer'));
    setRadioIfExists('gameMode', g.mode);
    setRadioIfExists('mc_numQuestions', g.modeData.numQuestions);
    setRadioIfExists('mc_question', g.modeData.q);
    setRadioIfExists('mc_answer', g.modeData.a);
    setRadioIfExists('fr_question', g.modeData.q);
    setRadioIfExists('fr_answer', g.modeData.a);
    setRadioIfExists('mf_question', g.modeData.q);
    setRadioIfExists('mf_answer', g.modeData.a);
    setSummaryData();
}
function setRadioIfExists(name, val) {
    if(val === undefined) {return false;}
    var e = document.querySelector('input[name="' + name + '"][value="' + val + '"]');
    if(e) {
        e.checked = true; 
        if(e.oninput instanceof Function) {
            e.oninput(e); 
        }
        return true;}
    return false;
}


//Settings
function setSettingElements() {
    Object.keys(settings).forEach(function(key) {
        var e = document.getElementById('setting_' + key);
        if(e) {
            if(e.type === 'checkbox') {e.checked = settings[key];}
            else {e.value = settings[key];}
        }
    });
}
function onSettingsInput(elem) {
    var key = elem.id.split('_')[1] || null;
    if(key in settings) {
        if(elem.type === 'checkbox') {
            settings[key] = elem.checked;
        }
        else {
            settings[key] = elem.value;
        }
    }
}



//Game Functions
function newGame(replay=false) {
    toScreen('loading');

    //Clean Up DOM
    deleteMap();
    clearInterval(timerInterval);
    document.querySelectorAll('.mc_answers').forEach(function(e) {e.remove();});
    document.getElementById('fr_answer').classList.remove('visible');
    document.getElementById('fr_answer_input').spellcheck = "false";
    document.getElementById('fr_suggestions').innerHTML = '';
    document.getElementById('newGameErr').style.display = 'none';
    document.getElementById('gameTimerStat').textContent = '';
    document.getElementById('revealCountriesBtn').style.display = 'none';

    // Lazy load json
    if(!countryData) {
        loadJSON('countryInfo.json', function(res) {
            setupDataStructs(res);
            setupGameObject(replay);
        });
    }
    else {setupGameObject(replay);}
}
function resumeGame() {
    toScreen('game');
    if(game.mode === 2 || game.mode === 3) {
        document.getElementById('fr_answer_input').focus();
    }
}
function setupGameObject(replay = false) {
    game.history = [];
    game.correct = 0;
    game.answered = 0;
    game.ended = false;
    game.isPaused = false;
    game.timerValue = 0;

    // If replay keep the actual game settings
    if(!replay) {
        game.modeData = {};
        game.mode = parseInt(document.querySelector('[name="gameMode"]:checked').value);
        game.regions = getRegionSetting();
        game.includeIslands = document.getElementById('includeIslands').checked;
        game.recognizedOnly = document.getElementById('isRecognized').checked;
    }

    // Game Mode Specific Settings
    document.getElementById('answerBank').classList.remove('floating');
    game.timer = false;
    if(game.mode === 1) {
        if(!replay) {
            game.modeData = {
                q: document.querySelector('[name="mc_question"]:checked').value,
                a: document.querySelector('[name="mc_answer"]:checked').value,
                numQuestions: document.querySelector('[name="mc_numQuestions"]:checked').value,
            };
        }
        document.getElementById('answerBank').classList.add('floating');
    }
    else if(game.mode === 2) {
        if(!replay) {
            game.modeData = {
                q: document.querySelector('[name="fr_question"]:checked').value,
                a: document.querySelector('[name="fr_answer"]:checked').value,
                autocomplete: document.getElementById('fr_autocomplete').checked,
            };
        }
        document.getElementById('fr_answer').classList.add('visible');
        document.getElementById('fr_answer_input').focus();
    }
    else if(game.mode === 3) {
        if(!replay) {
            var timer = document.getElementById('fm_timer').checked;
            var maxTime = parseInt(document.getElementById('fm_time').value) * 60 || 0;
            game.modeData = {
                q: 'map',
                a: document.querySelector('[name="mf_answer"]:checked').value,
                autocomplete: false,
                maxTime: timer ? maxTime : Infinity,
            };
            game.timer = timer;
            game.timerValue = timer ? maxTime : 0;
        }
        document.getElementById('fr_answer').classList.add('visible');
        document.getElementById('fr_answer_input').spellcheck = "true";
        document.getElementById('fr_answer_input').focus();
    }

    // Create countryPool
    game.countryPool = [];
    game.regions.forEach(function(r) {
        regions[r].forEach(function(c) {
            if(!game.includeIslands && tinyIslands.indexOf(c) > -1) {return;}
            else if(game.recognizedOnly && countryData[c].sovereignty !== undefined) {return;}
            else if(!hasRequirement(c)) {return;}
            else {game.countryPool.push(c);}
        });
    });

    // Game settings resulted in no countries
    if(game.countryPool.length === 0) {
        endGame('newGame');
        document.getElementById('newGame').scrollTop = 0
        document.getElementById('c').textContent = 'No countries match your game settings, please re-configure'
        document.getElementById('newGameErr').style.display = 'block';
        return;
    }
    else if(game.timer && game.modeData.maxTime && game.modeData.maxTime <= 0) {
        endGame('newGame');
        document.getElementById('newGame').scrollTop = 0
        document.getElementById('c').textContent = 'The timer cannot be 0 or negative';
        document.getElementById('newGameErr').style.display = 'block';
        return;
    }

    if(game.modeData.q === 'map') {
        loadMap(game.mode === 3 ? exploreMapOptions : {
            dragging: false,
            smoothWheelZoom: "center",  // enable smooth zoom 
            smoothSensitivity: 1,   // zoom speed. default is 1
            doubleClickZoom: "center",
            touchZoom: "center",
        });
    }
    else {
        makeQuestion(null, true);
        toScreen('game');
    }
}
function makeQuestion(c, isFirst) {
    if(!c && game.countryPool.length === 0) {endGame();return;}
    var attempts = 0;
    while(attempts < 10 && (!c || !(c in countryData) || !hasRequirement(c))) {
        c = game.countryPool[Math.floor(Math.random() * game.countryPool.length)];
        ++attempts;
    }
    if(attempts >= 10) {
        endGame();
        return;
    }
    
    game.lastAnswer = game.answer;
    game.answer = c;
    removeItem(game.countryPool, c);
    
    setTimeout(function() {
        fadeOutQuestion(isFirst);
        switch(game.modeData.q) {
            case 'map':
                unHighlight();
                highlight(geometry[c]);
                if(game.recognizedOnly) {callOnDownstream(c, function(id) {highlight(geometry[id]);});}
                disableControls(600);
                map.flyTo(countryData[c].center, calculateZoomLevel(c), {duration: transitionTime/1000});
                break;
            case 'flag':
                fadeInQuestion('<img onerror="makeQuestion();" src="res/flags/'+c+'.svg" />');
                break;
            case 'name':
                resizeQuestionText(fadeInQuestion(countryData[c].name));
                break;
            case 'capital':
                resizeQuestionText(fadeInQuestion(countryData[c].capital));
                break;
            case 'shape':
                fadeInQuestion('<img class="countryShapeQuestion" onerror="makeQuestion();" src="https://worldle.teuteuf.fr/images/countries/'+countryData[c].a2.toLowerCase()+'/vector.svg" />');
                break;
        }
    }, (game.modeData.q === 'map' || isFirst) ? 0 : transitionTime-250);
    if(game.mode === 1) {setMCAnswers(isFirst);}
}
var fsSymbolTimeout;
function submitAnswer(answer) {
    var shouldMakeSound = true;
    clearTimeout(fsSymbolTimeout);
    ++game.answered;
    game.history.push([game.answer, game.answer === answer]);
    
    if(game.mode === 1) {
        document.querySelectorAll('.mc_a').forEach(function(e) {
            if(e.dataset.value === game.answer) {
                e.classList.add('correct');
                if(settings.speak && e.textContent.trim() !== '') {
                    speak(e.textContent);
                    shouldMakeSound = false;
                }
            }
            else {e.remove();}
        });
    }
    else if(game.mode === 2) {
        setFRInput('');
        document.getElementById('fr_suggestions').innerHTML = '';
        var e = document.createElement('div');
        var ansTxt = (game.modeData.a === 'name' ? countryData[game.answer].name : countryData[game.answer].capital);
        e.className = 'mc_answers';
        e.innerHTML = '<div class="mc_a correct">' + ansTxt + '</div>';
        if(settings.speak) {
            speak(ansTxt);
            shouldMakeSound = false;
        }
        document.getElementById('answerBank').insertBefore(e, document.getElementById('fr_answer'));
        setTimeout(function() {e.classList.add('visible');}, 5);
        setTimeout(function() {
            e.classList.remove('visible');
            setTimeout(function() {e.remove();}, 500);
        }, transitionTime);
    }
    if(answer === game.answer) {
        document.getElementById('fs_correct').classList.add('visible');
        if(shouldMakeSound) {playSound('res/audio/correct.mp3', .1);}
        ++game.correct;
    }
    else {
        document.getElementById('fs_incorrect').classList.add('visible');
        if(shouldMakeSound) {playSound('res/audio/incorrect.mp3', .6);}
    }
    fsSymbolTimeout = setTimeout(function() {
        var e = document.querySelector('.feedbackSymbol.visible');
        if(e) {e.classList.remove('visible');}
    }, transitionTime);
    makeQuestion();
    updateGameTimerStat();
}
function submitFillOutAnswer(input) {
    if(input.value.trim() === '') {return;}
    var ids = game.modeData.a === 'name' ? name2codeEx(input.value) : capital2codeEx(input.value);
    if(ids.length > 0) {
        var oneCorrect = false;
        var errs = [];
        ids.forEach(id => {
            if(game.countryPool.indexOf(id) > -1) {
                ++game.answered;
                ++game.correct;
                oneCorrect = true;
                game.history.push([id, true]);
                removeItem(game.countryPool, id);
                hidden(geometry[id]);
                if(game.recognizedOnly) {callOnDownstream(id, function(c) {hidden(geometry[c]);});}
                if(id in mapMarkers) {mapMarkers[id].remove();delete mapMarkers[id];}
            }
            else if(game.history.find(x => x[0] === id)) {
                errs.push(countryData[id][game.modeData.a] + " has already been entered");
            }
            else if(id in countryData) {
                errs.push(countryData[id][game.modeData.a] + " was not included via game settings");
            }
        });
        if(oneCorrect) {
            playSound('res/audio/correct.mp3', .1);
            input.value = '';
            setTimeout(function() {input.value = '';}, 100);
            updateGameTimerStat();
        }
        else if(errs.length > 0) {
            var prevVal = input.value;
            setTimeout(function() {
                if(input.value === prevVal) {
                    errs.forEach(err => {
                        var exists = document.evaluate('//span[text()="'+err+'"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
                        if(exists) {return;}
                        var elem = document.createElement('span');
                        elem.className = 'err';
                        elem.textContent = err;
                        document.getElementById('fr_suggestions').appendChild(elem);
                        setTimeout(function() {elem.remove();}, 4000);
                    });
                }
            }, 500);
        }
        if(game.countryPool.length === 0) {endGame();}
        document.getElementById('revealCountriesBtn').style.display = game.countryPool.length < 25 ? 'block' : 'none';
    }
}
function handleTimer(isFirst) {
    if(game.isPaused) {return;}
    var isFinale = false;
    if(game.timer) {
        if(game.timerValue <= 0) {endGame();return;}
        --game.timerValue;
        isFinale = game.timerValue <= 10;
    }
    else if(!isFirst) {
        ++game.timerValue;
    }
    document.getElementById('gameTimerValue').textContent = Math.floor(game.timerValue / 60).toString() + ':' + (game.timerValue % 60).toString().padStart(2, '0');
    document.getElementById('gameTimer').className = isFinale ? 'finale' : '';
}
function updateGameTimerStat() {
    if(game.mode === 3) {
        document.getElementById('gameTimerStat').innerHTML = '<b>' + game.correct + '</b> / ' + (game.countryPool.length + game.answered);
    }
    else {
        var grade = Math.round(game.correct / game.answered * 100) || 0;
        var ranking = grade > 75 ? 'good' : (grade > 50 ? 'mid' : 'bad');
        document.getElementById('gameTimerStat').innerHTML = game.answered + '/' + (game.countryPool.length + game.answered) + ' <b class="'+ranking+'">' + (grade) + '%</b>';
    }
}
var mapMarkers = {};
var mapMarkerTimeout;
function identifyRemainingCountries() {
    clearMapMarkers();
    game.countryPool.forEach(x => {
        var marker = L.marker(countryData[x].center, {icon: alertIcon, pane: "markers"});
        mapMarkers[x] = marker;
        marker.addTo(map);
    });
    var group = new L.featureGroup(Object.values(mapMarkers));
    map.flyToBounds(group.getBounds(), {padding: [15, 15]});
    mapMarkerTimeout = setTimeout(clearMapMarkers, 5000);
}
function clearMapMarkers() {
    clearTimeout(mapMarkerTimeout);
    Object.values(mapMarkers).forEach(x => x.remove());
    mapMarkers = {};
}

var qVisibilityTimeout;
function setMCAnswers(isFirst) {
    clearTimeout(qVisibilityTimeout);
    var e = document.createElement('div');
    e.className = 'mc_answers';
    document.getElementById('answerBank').appendChild(e);
    var picked = [game.answer];
    
    //First try other countries from subregion/bordering
    var p = 0, pools = [
        countryData[game.answer].borders.shuffle().slice(0, Math.min(game.modeData.numQuestions/2, countryData[game.answer].borders.length)) || [],
        subregions[countryData[game.answer].subregion].slice() || [],
        regions[countryData[game.answer].region].slice() || [],
        countries.slice() || [],
    ];
    removeItem(pools, game.answer, true);
    while(picked.length < game.modeData.numQuestions) {
        while(pools[p].length === 0) {++p;}
        var choice = pools[p][Math.floor(Math.random() * pools[p].length)];
        if(hasRequirement(choice)) picked.push(choice);
        removeItem(pools, choice, true);
    }
    
    picked.shuffle();
    var fg;
    for(var i = 0;i < picked.length;++i) {
        switch(game.modeData.a) {
            case 'flag':
                e.innerHTML += '<div data-value="'+picked[i]+'" onclick="submitAnswer(\''+picked[i]+'\');" class="mc_a flag"><img src="res/flags/'+picked[i]+'.svg" /></div>';
                break;
            case 'name':
                e.innerHTML += '<div data-value="'+picked[i]+'" onclick="submitAnswer(\''+picked[i]+'\');" class="mc_a">'+countryData[picked[i]].name+'</div>';
                break;
            case 'capital':
                e.innerHTML += '<div data-value="'+picked[i]+'" onclick="submitAnswer(\''+picked[i]+'\');" class="mc_a">'+countryData[picked[i]].capital+'</div>';
                break;
        }
    }
    qVisibilityTimeout = setTimeout(function() {
        document.querySelectorAll('.mc_answers').forEach(function(elem) {
            if(elem === e) {elem.classList.add('visible');}
            else {elem.classList.remove('visible');setTimeout(function(){elem.remove()}, 500);}
        })
    }, isFirst ? 0 : transitionTime);
}
function fmTimerInput(input) {
    document.getElementById('timeInput').style.display = input.checked ? 'inline-block' : 'none';
}
function onFRKeyDown(event, input) {
    if(event.key === 'Enter' && input.value.trim() !== '') {
        event.preventDefault();
        if(game.mode === 3) {submitFillOutAnswer(input);return;}
        var id = game.modeData.a === 'name' ? name2code(input.value) : capital2code(input.value);
        if(!id && game.modeData.autocomplete) {
            var e = document.querySelector('#fr_suggestions > div.selected');
            if(e) {
                e.click();
                setTimeout(function() {submitAnswer(game.modeData.a === 'name' ? name2code(input.value) : capital2code(input.value))}, 100);
                return;
            }
        } 
        submitAnswer(id);
    }
    else if(event.key === 'Tab' && game.modeData.autocomplete) {
        event.preventDefault();
        if(game.mode === 3) {submitFillOutAnswer(input);return;}
        var e = document.querySelector('#fr_suggestions > div.selected');
        if(e) {
            e.click();
        }
    }
    else if(event.key === 'ArrowUp') {
        event.preventDefault();
        var e = document.querySelector('#fr_suggestions > div.selected');
        if(e && e.nextElementSibling) {
            e.classList.remove('selected');
            e.nextElementSibling.classList.add('selected');
        } 
    }
    else if(event.key === 'ArrowDown') {
        event.preventDefault();
        var e = document.querySelector('#fr_suggestions > div.selected');
        if(e && e.previousElementSibling) {
            e.classList.remove('selected');
            e.previousElementSibling.classList.add('selected');
        }  
    }
}
var autocompletePromise;
function onFRInput(input) {
    if(game.mode === 3) {submitFillOutAnswer(input);return;}
    var val = normalizeName(input.value);
    document.getElementById('fr_suggestions').innerHTML = '';
    if(game.modeData.autocomplete && val !== '') {
        if(autocompletePromise) {autocompletePromise.cancel();}
        autocompletePromise = fuzzysort.goAsync(val,  game.modeData.a === 'name' ? autocompleteCountries : autocompleteCapitals, autocompleteOptions);
        autocompletePromise.then(results => {
            for(var i = 0;i < results.length;++i) {
                var res = Object.assign({}, results[i], {target: countryData[(game.modeData.a === 'name' ? nameToCode[results[i].target] : capitalToCode[results[i].target])][game.modeData.a]});
                document.getElementById('fr_suggestions').innerHTML += '<div ' + (i === 0 ? 'class="selected" ' : '') + 'onclick="setFRInput(this.textContent)">' + fuzzysort.highlight(res) + '</div>';
            }
        });
    }
}
function onFRFocus(input) {};
function onFRBlur(input) {
    if(!game.ended && !game.isPaused) {input.focus();}
};
function setFRInput(val) {
    document.getElementById('fr_answer_input').value = val;
    document.getElementById('fr_answer_input').focus();
    document.getElementById('fr_answer_input').setSelectionRange(val.length, val.length);
    onFRInput(document.getElementById('fr_answer_input'));
}
function fadeInQuestion(content) {
    var q = document.createElement('div');
    q.className = 'question';
    q.innerHTML = content;
    document.getElementById('game').appendChild(q);
    setTimeout(function() {q.classList.add('visible');}, 100);
    return q;
}
function resizeQuestionText(elem) {
    var textSize = 5;
    while(textSize > .1 && elem.scrollWidth > elem.offsetWidth) {
        textSize -= .1;
        elem.style.fontSize = textSize + "em";
    }

    // Add buffer for padding
    if(textSize > .5 && textSize < 5) {textSize -= .3;}
    elem.style.fontSize = textSize + "em";
}
function fadeOutQuestion(isFirst) {
    var q = document.getElementById('game').querySelector('.question.visible');
    if(q) {
        q.classList.remove('visible');
        setTimeout(function() {q.remove();}, isFirst ? 0 : transitionTime);
    }
}

function setSummaryData() {
    var grade = game.mode !== 3 ? (Math.round(game.correct / game.answered * 100) || 0) :  (Math.round(game.correct / (game.countryPool.length + game.answered) * 100) || 0);
    var ranking = grade > 75 ? 'good' : (grade > 50 ? 'mid' : 'bad');
    var timerVal = Math.floor(game.timerValue / 60).toString() + ':' + (game.timerValue % 60).toString().padStart(2, '0');
    if(game.timer) {timerVal += '/' + Math.floor(game.modeData.maxTime / 60).toString() + ':' + (game.modeData.maxTime % 60).toString().padStart(2, '0');}
    document.getElementById('menuStat').innerHTML = '<div class="'+ranking+'">'+grade+'%</div><div>' + game.correct + '/' + (game.mode !== 3 ? game.answered : (game.countryPool.length + game.answered)) + ' · ' + timerVal + '</div>';
    document.getElementById('menuSummary').innerHTML = '';
    for(var i = 0;i < game.history.length;++i) {
        if(game.history[i][1]) {
            document.getElementById('menuSummary').innerHTML += '<div><svg style="fill: var(--accent);" xmlns="http://www.w3.org/2000/svg" viewBox="0 -46 417.813 417"><path d="M159.988 318.582c-3.988 4.012-9.43 6.25-15.082 6.25s-11.094-2.238-15.082-6.25L9.375 198.113c-12.5-12.5-12.5-32.77 0-45.246l15.082-15.086c12.504-12.5 32.75-12.5 45.25 0l75.2 75.203L348.104 9.781c12.504-12.5 32.77-12.5 45.25 0l15.082 15.086c12.5 12.5 12.5 32.766 0 45.246zm0 0"/></svg>'+countryData[game.history[i][0]].name+'</div>';
        }
        else {
            document.getElementById('menuSummary').innerHTML += '<div><svg style="fill: var(--error);" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 249.499 249.499"><path d="M7.079 214.851l25.905 26.276c9.536 9.674 25.106 9.782 34.777.252l56.559-55.761 55.739 56.548c9.542 9.674 25.112 9.782 34.78.246l26.265-25.887c9.674-9.536 9.788-25.106.246-34.786l-55.742-56.547 56.565-55.754c9.667-9.536 9.787-25.106.239-34.786L216.52 8.375c-9.541-9.667-25.111-9.782-34.779-.252l-56.568 55.761-55.74-56.553C59.891-2.337 44.32-2.451 34.65 7.079L8.388 32.971c-9.674 9.542-9.791 25.106-.252 34.786l55.745 56.553-56.55 55.767c-9.674 9.53-9.791 25.106-.252 34.774z"/></svg>'+countryData[game.history[i][0]].name+'</div>';
        }
    }
    if(game.ended) {
        document.querySelector('#inGameMenuBtn_new > div').textContent = 'Replay';
        document.getElementById('inGameMenuBtn_resume').style.display = 'none';
        document.getElementById('inGameMenuBtn_back').style.display = 'none';
        document.getElementById('shareBtn').style.display = 'inline-block';
    }
    else {
        document.querySelector('#inGameMenuBtn_new > div').textContent = 'Restart';
        document.getElementById('inGameMenuBtn_resume').style.display = 'block';  
        document.getElementById('inGameMenuBtn_back').style.display = 'block';
        document.getElementById('shareBtn').style.display = 'none';
    }
}

function endGame(screen = 'inGameMenu') {
    game.ended = true;
    if(game.mode === 3) {
        game.countryPool.forEach(id => {game.history.push([id, false])});
    }
    clearTimeout(timerInterval);
    setTimeout(function() {
        toScreen(screen);
    }, transitionTime);
    document.getElementById('fr_answer_input').blur();
}


//Helper Functions
function loadJSON(fileName, callback) {
    var xhr = new XMLHttpRequest();
    xhr.addEventListener('load', function(evt) {
        if(callback instanceof Function) {callback(xhr.response);}
    });
    xhr.responseType = 'json';
    xhr.open('GET', 'res/data/' + fileName, true);
    xhr.send();
}
function setupDataStructs(res) {
    countryData = res;
    countries = Object.keys(countryData);
    countries.forEach(function(id) {
        if(!(id in countryData) || !countryData[id].name) {console.log(id); return;}
        if(!subregions[countryData[id].subregion]) {subregions[countryData[id].subregion] = [];}
        if(!regions[countryData[id].region]) {regions[countryData[id].region] = [];}
        subregions[countryData[id].subregion].push(id);
        regions[countryData[id].region].push(id);

        var normalizedName = normalizeName(countryData[id].name);
        nameToCode[normalizedName] = id;
        autocompleteCountries.push(fuzzysort.prepare(normalizedName));
        
        var normalizedCapital = normalizeName(countryData[id].capital);
        if(normalizedCapital) {
            capitalToCode[normalizedCapital] = id;
            autocompleteCapitals.push(fuzzysort.prepare(normalizedCapital));
        }
    });
    // Generate even more aliases for extended names
    Object.keys(nameToCode).forEach(x => {
        var name = normalizeNameExtended(x);
        if(name && name !== x) {
            if(name in extendedNames) {
                if(extendedNames[name].indexOf(nameToCode[x]) === -1) {
                    extendedNames[name].push(nameToCode[x]);
                }
            }
            else {
                extendedNames[name] = [nameToCode[x]];
            }   
        }
    });
    Object.keys(capitalToCode).forEach(x => {
        var name = normalizeNameExtended(x);
        if(name && name !== x) {
            if(name in extendedCapitals) {
                if(extendedCapitals[name].indexOf(capitalToCode[x]) === -1) {
                    extendedCapitals[name].push(capitalToCode[x]);
                }
            }
            else {
                extendedCapitals[name] = [capitalToCode[x]];
            }   
        }
    });
}
var mapLoadedParts = 0;
function loadMap(options = {}, countryOptions = {}, baseMap = true) {
    //Clear Up Old Map/data if exists
    deleteMap();
    
    //Make Map
    options = Object.assign({}, defaultMapOptions, options);
    map = L.map('map', options);
    map.on('load', checkLoad);
    map.setView(centerLatLng, centerZoom);
    map.createPane('markers');
    
    //Set BASEMAP
    if(baseMap) {
        var bm = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'}).addTo(map);
    }

    // Lazy load geoJSON
    if(!geoJSON) {
        loadJSON('countries.geojson', function(res) {
            geoJSON = res;
            setGeoJSON(countryOptions);
        });
    }
    else {setGeoJSON(countryOptions);}
    
}
function checkLoad() {
    if(++mapLoadedParts >= 2) {
        if(game.mode !== 3) {
            makeQuestion(null, true);
        }
        else {
            map.fitBounds(L.latLngBounds(map.geoCenters), {padding: [15, 15]});
        }
        clearInterval(timerInterval);
        handleTimer(true);
        timerInterval = setInterval(handleTimer, 1000);
        toScreen('game');
    }
}
function setGeoJSON(countryOptions) {
    countryStyle = Object.assign({}, defaultCountryStyle, countryOptions);
    map.geoCenters = [];
    L.geoJSON(geoJSON, {
        style: countryStyle,
        onEachFeature: function(feature, layer) {
            geometry[feature.id] = layer;
            if(game.mode === 3) {
                if(game.countryPool.indexOf(feature.id) === -1)  {
                    if(!game.recognizedOnly || game.countryPool.indexOf(countryData[feature.id].sovereignty) === -1) {
                        hidden(layer);
                    }
                }
                else {
                    map.geoCenters.push(countryData[feature.id].center);
                }
            }
        }
    }).addTo(map);
    checkLoad();
}
function deleteMap() {
    geometry = {};
    if(map) {
        map.remove();
        map = null;
        mapLoadedParts = 0;
    }
}
function removeItem(arr, value, deep = false) {
    if(deep) {
        for(var j = 0;j < arr.length;++j) {
            var i = arr[j].indexOf(value);
            if(i > -1) {arr[j].splice(i, 1);}    
        }
    }
    else {
        var i = arr.indexOf(value);
        if(i > -1) {arr.splice(i, 1);}
    }
}
function unHighlight() {
    for(var i = 0;i < highlighted.length;++i) {
        highlighted[i].setStyle(countryStyle);
    }
    highlighted = [];
}
function highlight(elem, options = defaultHighlightedCountryStyle) {
    elem.setStyle(options);
    elem.bringToFront();
    highlighted.push(elem);
}
function hidden(elem, options = defaultHiddenCountryStyle) {
    elem.setStyle(options);
    elem.bringToBack();
    highlighted.push(elem);
}
function callOnDownstream(id, func) {
    Object.keys(countryData).forEach(c => {
        if(countryData[c].sovereignty && countryData[c].sovereignty === id) {
            func(c);
        }
    });
}

var disableControlsTimeout;
function disableControls(timeout) {
    clearTimeout(disableControlsTimeout);
    
    var dragging = map.dragging.enabled();
    var keyboard = map.keyboard.enabled();
    var touchZoom = map.touchZoom.enabled();
    var doubleClickZoom = map.doubleClickZoom.enabled();
    var scrollWheelZoom = map.scrollWheelZoom.enabled();
    
    map.dragging.disable();
    map.keyboard.disable();
    map.touchZoom.disable();
    map.doubleClickZoom.disable();
    map.scrollWheelZoom.disable();
    
    disableControlsTimeout = setTimeout(function() {
        if(dragging) {map.dragging.enable();}
        if(keyboard) {map.keyboard.enable();}
        if(touchZoom) {map.touchZoom.enable();}
        if(doubleClickZoom) {map.doubleClickZoom.enable();}
        if(scrollWheelZoom) {map.scrollWheelZoom.enable();}
    }, timeout);
}
Array.prototype.shuffle = function() {
  for (let i = this.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1));
    [this[i], this[j]] = [this[j], this[i]];
  }
  return this;
}

function hasRequirement(country) {
    switch(game.modeData.a) {
        case 'map':
            return country in geometry;
        case 'flag':
            return true;
        case 'name':
            return countryData[country].name;
        case 'capital':
            return countryData[country].capital;
    }
}
var aCtx;
var soundCache = {};
function playSound(file, volume = 1) {
    if(!aCtx) {aCtx = new (window.AudioContext || window.webkitAudioContext)();}
    if(settings.audio === true) {
        if(!(file in soundCache)) {
            var xhr = new XMLHttpRequest();
            xhr.open('GET', file, true);
            xhr.responseType = "arraybuffer";
            xhr.onload = function() {
                aCtx.decodeAudioData(xhr.response, function(buffer) {
                    soundCache[file] = buffer;
                    playSound(file, volume);
                }, console.error);
            }
            xhr.onerror = console.error;
            xhr.send();
        }
        else {
            var gn = aCtx.createGain();
            gn.gain.value = volume;
            gn.connect(aCtx.destination);
            var source = aCtx.createBufferSource();
            source.buffer = soundCache[file];
            source.volume = volume;
            source.connect(gn);
            if(!source.start) {source.start = source.noteOn;}
            source.start();
            source.onended = function() {
                source.disconnect(gn);
                gn.disconnect(aCtx.destination);
                delete source;
                delete gn;
            }
        }
    }
}

function unlockAudio() {
    if(!aCtx) {aCtx = new (window.AudioContext || window.webkitAudioContext)();}
    var buffer = aCtx.createBuffer(1, 1, 22050); // 1/10th of a second of silence
    var source = aCtx.createBufferSource();
    source.buffer = buffer;
    source.connect(aCtx.destination);
    source.onended = function() {
        source.disconnect(aCtx.destination);
        window.removeEventListener("mousedown", unlockAudio);
        delete source;
    }
    source.start();
}

window.addEventListener('mousedown', unlockAudio);

function calculateZoomLevel(c) {
    switch(c) {
        case 'RUS': return 2.5; break;
        case 'CHN': return 4; break;
        case 'USA': return 3.5; break;
        case 'CAN': return 3.5; break;
        case 'BRA': return 4; break;
        case 'ATA': return 2.3; break;
        default: return 4.5;
    }
}




// Explore map
var exploreMapLoadedParts = 0;
function loadExploreMap(baseMap = true) {
    // Show loading screen
    toScreen('loading');

    //Clear Up Old Map/data if exists
    deleteExploreMap();

    // Lazy load data
    if(!countryData) {
        loadJSON('countryInfo.json', function(res) {
            setupDataStructs(res);
            checkExploreLoad();
        });
    }
    else {checkExploreLoad();}
    
    //Make Map
    exploreMap = L.map('exploreMap', exploreMapOptions);
    exploreMap.on('load', checkExploreLoad);
    exploreMap.setView(centerLatLng, centerZoom);
    
    //Set BASEMAPs
    if(baseMap) {
        var base = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community', noWrap: true, bounds: [[-90, -180],[90, 180]]}).addTo(exploreMap);
        var pane = exploreMap.createPane('labels');
        pane.style.zIndex = 650;
        pane.style.pointerEvents = 'none';
        var labels = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/dark_only_labels/{z}/{x}/{y}{r}.png', {attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>', subdomains: 'abcd', maxZoom: 20, pane: 'labels', noWrap: true,   bounds: [[-90, -180],[90, 180]]}).addTo(exploreMap);
    }

    if(!geoJSON) {
        loadJSON('countries.geojson', function(res) {
            geoJSON = res;
            setExploreGeoJSON();
        });
    }
    else {setExploreGeoJSON();}
}
function setExploreGeoJSON() {
    L.geoJSON(geoJSON, {
        style: exploreCountryStyle,
        onEachFeature: function(feature, layer) {
            exploreGeometry[feature.id] = layer;
            layer.setStyle(exploreCountryStyle);
            layer.on('mouseover', function(evt) {
                if(this.feature.id !== exploreCountry) {
                    this.setStyle(exploreCountryHoveredStyle);
                }
            });
            layer.on('mouseout', function(evt) {
                if(this.feature.id !== exploreCountry) {
                    this.setStyle(exploreCountryStyle);
                    this.bringToFront();
                    if(exploreCountry) {exploreGeometry[exploreCountry].bringToFront();}
                }
            });
            layer.on('click', function(evt) {
                selectExploreCountry(this.feature.id);
            });
        }
    }).addTo(exploreMap);
    checkExploreLoad();
}
function checkExploreLoad() {
    if(++exploreMapLoadedParts >= 3) {
        toScreen('explore');
    }
}
function deleteExploreMap() {
    exploreMapLoadedParts = 0;
    exploreGeometry = {};
    if(exploreMap) {
        exploreMap.remove();
        exploreMap = null;
    }
}
function selectExploreCountry(id) {
    if(exploreCountry) {exploreGeometry[exploreCountry].setStyle(exploreCountryStyle);}
    exploreGeometry[id].setStyle(exploreCountryHighlightedStyle);
    exploreGeometry[id].bringToFront();
    exploreCountry = id;
    setExploreDetails(id);
}
function hideExploreDetails() {
    if(exploreMap) {
        exploreMap.getContainer().style.width = "100vw";
        exploreMap.getContainer().style.height = "100vh";
        exploreMap.invalidateSize();
        document.getElementById('exploreDetails').style.display = 'none';
    }
}
function setExploreDetails(id) {
    if(exploreMap.getContainer().style.width !== "100%") {
        exploreMap.getContainer().style.width = "100%";
        exploreMap.getContainer().style.height = "100%";
        exploreMap.invalidateSize();
        document.getElementById('exploreDetails').style.display = 'flex';
    }
    document.getElementById('exploreFlag').style.display = 'block';
    document.getElementById('exploreFacts').style.display = 'block';
    document.getElementById('exploreFlag').src = 'res/flags/' + id + '.svg';
    document.getElementById('exploreCountryName').textContent = countryData[id].name;
    document.getElementById('exploreRecognized').style.display =  'block';
    document.getElementById('exploreDetailSovereigntyWrapper').style.display = 'none';
    document.getElementById('exploreCountryCodes').innerHTML = '<span title="Alpha-3 Code">' + id + '</span> · <span title="Alpha-2 Code">' + countryData[id].a2 + '</span>';
    document.getElementById('exploreDetailCapital').textContent = countryData[id].capital || '-';
    document.getElementById('exploreDetailRegion').innerHTML = countryData[id].subregion || '-';
    document.getElementById('exploreDetailPopulation').textContent = (countryData[id].population || '-').toLocaleString();
    document.getElementById('exploreDetailArea').innerHTML = (countryData[id].area || '-').toLocaleString();
    document.getElementById('exploreDetailCenter').innerHTML = countryData[id].center ? (countryData[id].center[1].toFixed(2) || '-') + '&#176;, ' + (countryData[id].center[1].toFixed(2) || '-') + '&#176;' : '-';
    
    if ('speechSynthesis' in window) {
        document.getElementById("exploreCountryCodes").innerHTML += ' · <svg onclick="speak(\'' + countryData[id].name + '\');" class="listen" title="Pronounce country name" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.36 19.36a1 1 0 0 1-.7-.29 1 1 0 0 1 0-1.41 8 8 0 0 0 0-11.32 1 1 0 0 1 1.41-1.41 10 10 0 0 1 0 14.14 1 1 0 0 1-.71.29Z" /><path d="M15.54 16.54a1 1 0 0 1-.71-.3 1 1 0 0 1 0-1.41 4 4 0 0 0 0-5.66 1 1 0 0 1 1.41-1.41 6 6 0 0 1 0 8.48 1 1 0 0 1-.7.3ZM11.38 4.08a1 1 0 0 0-1.09.21L6.59 8H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h2.59l3.7 3.71A1 1 0 0 0 11 20a.84.84 0 0 0 .38-.08A1 1 0 0 0 12 19V5a1 1 0 0 0-.62-.92Z"/></svg>';
        document.getElementById('exploreDetailCapital').innerHTML += ' <svg onclick="speak(\'' + countryData[id].capital + '\');" class="listen" title="Pronounce capital" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.36 19.36a1 1 0 0 1-.7-.29 1 1 0 0 1 0-1.41 8 8 0 0 0 0-11.32 1 1 0 0 1 1.41-1.41 10 10 0 0 1 0 14.14 1 1 0 0 1-.71.29Z" /><path d="M15.54 16.54a1 1 0 0 1-.71-.3 1 1 0 0 1 0-1.41 4 4 0 0 0 0-5.66 1 1 0 0 1 1.41-1.41 6 6 0 0 1 0 8.48 1 1 0 0 1-.7.3ZM11.38 4.08a1 1 0 0 0-1.09.21L6.59 8H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h2.59l3.7 3.71A1 1 0 0 0 11 20a.84.84 0 0 0 .38-.08A1 1 0 0 0 12 19V5a1 1 0 0 0-.62-.92Z"/></svg>';
    }

    if(countryData[id].sovereignty !== undefined) {
        document.getElementById('exploreRecognized').style.display =  'none';
        var sid = countryData[id].sovereignty.toUpperCase();
        if(sid in countryData) {
            document.getElementById('exploreDetailSovereigntyWrapper').style.display = 'flex';
            document.getElementById('exploreDetailSovereigntyImg').src = 'res/flags/' + sid + '.svg';
            document.getElementById('exploreDetailSovereigntyImg').onclick = function() {selectExploreCountry(sid);};
            document.getElementById('exploreDetailSovereignty').textContent = countryData[sid].name;
        }
    }

    // borders
    if(countryData[id].borders.length === 0) {
        document.getElementById('bordersWrapper').style.display = 'none';
    }
    else {
        document.getElementById('bordersWrapper').style.display = 'block';
        document.getElementById('borderCountries').innerHTML = '';
        countryData[id].borders.forEach((c) => {
            document.getElementById('borderCountries').innerHTML += '<div data-id="' + c + '" onclick="selectExploreCountry(this.dataset.id)" ><img src="res/flags/' + c + '.svg" /> ' + countryData[c].name + '</div>';
        }); 
    }

    // territories
    var territories = Object.keys(countryData).filter(x => countryData[x].sovereignty === id);
    if(territories.length === 0) {
        document.getElementById('territoriesWrapper').style.display = 'none';
    }
    else {
        document.getElementById('territoriesWrapper').style.display = 'block';
        document.getElementById('territoryCountries').innerHTML = '';
        territories.forEach((c) => {
            document.getElementById('territoryCountries').innerHTML += '<div data-id="' + c + '" onclick="selectExploreCountry(this.dataset.id)" ><img src="res/flags/' + c + '.svg" /> ' + countryData[c].name + '</div>';
        }); 
    }

    // cities
    if(!topCities) {
        loadJSON('cities.json', function(res) {
            topCities = res;
            setTopCities(id)
        });
    }
    else {setTopCities(id);}

    // languages
    var langHTML = '';
    if(countryData[id].languages.length == 0) {
        langHTML = '<span class="noData">No data</span>';
    }
    else {
        langHTML = '<ul>';
        countryData[id].languages.forEach((l) => {
            langHTML += '<li>' + l + '</li>';
        });
        langHTML += '</ul>';
    }
    document.getElementById('exploreDetailsLanguages').innerHTML = langHTML;

    if(!aliases) {
        loadJSON('countryAliases.json', function(res) {aliases=res;setExploreAliases(id);});
    }
    else {setExploreAliases(id);}
}
function setTopCities(id) {
    var citiesHTML = '';
    var a2 = countryData[id].a2.toLowerCase();
    if(!(a2 in topCities) || topCities[a2].length === 0) {
        citiesHTML = '<span class="noData">No data</span>';
    }
    else {
        citiesHTML = '<table class="subtleTable"><thead><tr><th>#</th><th>City</th><th>Population</th></tr></thead><tbody>';
        topCities[a2].forEach(function(city, x) {
            citiesHTML += '<tr><td>' + (x+1) + '</td><td>' + city[0] + '</td><td>' + (city[1] ? city[1].toLocaleString() : 'Unknown') + '</td></tr>';
        });
        citiesHTML += '</tbody></table>';
    }
    document.getElementById('explorePopulatedCities').innerHTML = citiesHTML;
}
function setExploreAliases(id) {
    var aliasesHTML = '';
    var names = id in aliases ? aliases[id].slice() : [];
    if(countryData[id].nativeName && countryData[id].nativeName !== countryData[id].name) {
        names.push([countryData[id].nativeName, 'native name']);
    }
    if(names.length == 0) {
        aliasesHTML = '<span class="noData">No alternative names</span>';
    }
    else {
        aliasesHTML = '<ul>';
        names.forEach((x) => {
            aliasesHTML += '<li>' + x[0] + ' <d>(' + x[1] + ')</d></li>';
        });
        aliasesHTML += '</ul>';
    }
    document.getElementById('exploreAlternativeNames').innerHTML = aliasesHTML;
}
var exploreResizeDetails = {isResizing: false};
function resizeExploreDetails(event, elem) {
    event.stopPropagation();
    event.preventDefault();
    var mx, my;
    if(event.touches && event.touches.length > 0) {
        mx = event.touches[0].clientX;
        my = event.touches[0].clientY;
    }
    else {
        mx = event.clientX;
        my = event.clientY;
    }
    var bb = elem.getBoundingClientRect();
    elem.classList.add('active');
    exploreResizeDetails.isResizing = true;
    exploreResizeDetails.ox = mx - (bb.x + bb.width/2);
    exploreResizeDetails.oy = my - Math.floor(bb.y + 11);
    exploreResizeDetails.isVertical = document.body.offsetWidth <= exploreDetailsBreakPoint;
    exploreResizeDetails.elem = elem;
    document.getElementById('clickCover').style.display = 'block';
    onMouseMove(event);
}
function onMouseMove(event) {
    if(exploreResizeDetails.isResizing) {
        event.stopPropagation();
        event.preventDefault();
        var mx, my;
        if(event.touches && event.touches.length > 0) {
            mx = event.touches[0].clientX;
            my = event.touches[0].clientY;
        }
        else {
            mx = event.clientX;
            my = event.clientY;
        }
        var parent = document.getElementById('explore');
        if(exploreResizeDetails.isVertical) {
            if(document.body.offsetHeight - my < 10) {onMouseUp();hideExploreDetails();return;}
            var y = (document.body.offsetHeight - my + exploreResizeDetails.oy);
            if(y < 108) {y = 108;}
            if(y > document.body.offsetHeight - 106) {y = document.body.offsetHeight - 106;}
            parent.style.gridTemplateRows = 'auto ' + y + 'px';
            parent.style.gridTemplateColumns = '1fr';
        }
        else {
            if(document.body.offsetWidth - mx < 10) {onMouseUp();hideExploreDetails();return;}
            var x = (document.body.offsetWidth - mx + exploreResizeDetails.ox);
            if(x < 250) {x = 250;}
            if(x > document.body.offsetWidth/2) {x = document.body.offsetWidth/2;}
            parent.style.gridTemplateColumns = 'auto ' + x + 'px';
            parent.style.gridTemplateRows = '1fr';
        }
    }
}
function onMouseUp() {
    if(exploreResizeDetails.isResizing) {
        exploreResizeDetails.isResizing = false;
        if(exploreMap) {exploreMap.invalidateSize();}
        document.getElementById('clickCover').style.display = 'none';
        exploreResizeDetails.elem.classList.remove('active');
    }
}
function toggleExploreSearch() {
    document.getElementById('exploreSearch').classList.toggle('visible');
    if(document.getElementById('exploreSearch').classList.contains('visible')) {
        document.getElementById('countrySearch').value = '';
        document.getElementById('countrySearch').focus();
        document.getElementById('searchBarWrapper').classList.remove('hasResults');
    }
}
function filterSearch(input) {
    var cursor = input.selectionStart;
    var len = input.value.length;
    input.value = input.value.replace(/[\/]/gi, '');
    var d = len - input.value.length;
	input.setSelectionRange(cursor-d+1, cursor-d);
}
function filterPosInt(input) {
    var cursor = input.selectionStart;
    var len = input.value.length;
    input.value = input.value.replace(/[^0-9]/gi, '');
    var d = len - input.value.length;
	input.setSelectionRange(cursor-d+1, cursor-d);
}
function onExploreSearchInput(input) {
    filterSearch(input);
    var val = normalizeName(input.value);
    document.getElementById('exploreSearchSuggestions').innerHTML = '';
    document.getElementById('searchBarWrapper').classList.remove('hasResults');
    if(val !== '') {
        if(autocompletePromise) {autocompletePromise.cancel();}
        autocompletePromise = fuzzysort.goAsync(val, autocompleteCountries, autocompleteOptions);
        autocompletePromise.then(results => {
            if(results.length > 0) {document.getElementById('searchBarWrapper').classList.add('hasResults');}
            for(var i = 0;i < results.length;++i) {
                var res = Object.assign({}, results[i], {target: countryData[nameToCode[results[i].target]].name});
                document.getElementById('exploreSearchSuggestions').innerHTML += '<div ' + (i === 0 ? 'class="active" ' : '') + 'onclick="chooseSearchCountry(this.textContent)">' + fuzzysort.highlight(res) + '</div>';
            }
        });
    }
}
function onExploreSearchKeyDown(event, input) {
    if(event.key === 'Enter' && input.value.trim() !== '') {
        event.preventDefault();
        if(input.value.toUpperCase() in countryData) {chooseSearchCountry(input.value);return;}
        var e = document.querySelector('#exploreSearchSuggestions > div.active');
        if(e) {e.click();}
        input.blur();
    }
    else if(event.key === 'Tab') {
        event.preventDefault();
        if(input.value.toUpperCase() in countryData) {chooseSearchCountry(input.value);return;}
        var e = document.querySelector('#exploreSearchSuggestions > div.active');
        if(e) {e.click();}
        input.blur();
    }
    else if(event.key === 'ArrowUp') {
        event.preventDefault();
        var e = document.querySelector('#exploreSearchSuggestions > div.active');
        if(e && e.previousElementSibling) {
            e.classList.remove('active');
            e.previousElementSibling.classList.add('active');
        } 
    }
    else if(event.key === 'ArrowDown') {
        event.preventDefault();
        var e = document.querySelector('#exploreSearchSuggestions > div.active');
        if(e && e.nextElementSibling) {
            e.classList.remove('active');
            e.nextElementSibling.classList.add('active');
        }  
    }
    else if(event.key === 'Escape') {
        event.preventDefault();
        toggleExploreSearch();
        input.blur();
    }
}
function chooseSearchCountry(name) {
    name = name.toUpperCase();
    var id = name in countryData ? name : name2code(name);
    if(id && id in countryData) {
        exploreMap.flyTo(countryData[id].center, calculateZoomLevel(id), {duration: transitionTime/1000});
        toggleExploreSearch();
        selectExploreCountry(id);
    }
}
function name2code(name) {
    name = normalizeName(name);
    if(name in nameToCode) {
        return nameToCode[name];
    }
    return null;
}
function name2codeEx(name) {
    var results = [];
    var normalName = normalizeName(name);
    var normalNameEx = normalizeNameExtended(name);
    if(normalNameEx in extendedNames) {results.push(...extendedNames[normalNameEx]);}
    if(normalName in nameToCode) {results.push(nameToCode[normalName]);}
    return [...new Set(results)];
}
function capital2code(name) {
    name = normalizeName(name);
    if(name in capitalToCode) {return capitalToCode[name];}
    return null;
}
function capital2codeEx(name) {
    var results = [];
    var normalName = normalizeName(name);
    var normalNameEx = normalizeNameExtended(name);
    if(normalNameEx in extendedCapitals) {results.push(...extendedCapitals[normalNameEx]);}
    if(normalName in capitalToCode) {results.push(capitalToCode[normalName]);}
    return [...new Set(results)];
}
function normalizeNameExtended(name) {
    return name.normalize('NFKD')
    .replace(/\bislands?\b/gi, '') // replace island(s)
    .replace(/\bilas?\b/gi, '') // replace ila(s)
    .replace(/\(.*\)/, '') // replace anything in parenthesses
    .replace(/^.*\bof\b/gi, '') // replace all words up to "of" including of
    .replace(/\b(the|saint|sint|san|republic)\b/gi, '') // replace some specific words
    .replace(/[^a-z]/gi, '') // delete everything except a-z (delete spaces)
    .toUpperCase(); 
}
function normalizeName(name) {
    return name.normalize('NFKD')
    .trim()
    .replace(/\s/gi, ' ')
    .replace(/\-/gi, ' ')
    .replace(/[^a-z ]/gi, '') // delete everything except a-z and spaces
    .toUpperCase(); 
}
function speak(text) {
    if (!('speechSynthesis' in window)) {
       alert('Your browser does not support speech synthesis');
       return;
    }
    var msg = new SpeechSynthesisUtterance(text);
    window.speechSynthesis.speak(msg);
}
function shareScore(event) {
    var grade = game.mode !== 3 ? (Math.round(game.correct / game.answered * 100) || 0) :  (Math.round(game.correct / (game.countryPool.length + game.answered) * 100) || 0);
    var timerVal = Math.floor(game.timerValue / 60).toString() + ':' + (game.timerValue % 60).toString().padStart(2, '0');
    if(game.timer) {timerVal += '/' + Math.floor(game.modeData.maxTime / 60).toString() + ':' + (game.modeData.maxTime % 60).toString().padStart(2, '0');}
    var txt = modeToStr() + ' · ' + grade + '% · ' + game.correct + '/' + (game.mode !== 3 ? game.answered : (game.countryPool.length + game.answered)) + ' · ' + timerVal;
    var url = window.location.href.replace(window.location.search, '').replace(/\/+$/, '/') + '?game=' + gameToUrl();
    if(navigator.canShare) {
        navigator.share({
            title: "Geotective",
            text: txt + '\n' + url,
        }).then(() => {createFadeMsg(event.clientX, event.clientY, "SHARED");}).catch(err => {
            console.error("Failed to share", err);
        });
    }
    else {
        navigator.clipboard.writeText(txt + "\n" + url).then(
            () => {
              createFadeMsg(event.clientX, event.clientY, "COPIED");
            },
            (err) => {
              console.error("Failed to copy score", err);
            }
          );  
    }
}
function createFadeMsg(x, y, msg) {
    var e = document.createElement('div');
    e.className = 'fadeMsg';
    e.textContent = msg;
    setTimeout(function() {
        e.remove();
    }, 2000);
    document.body.appendChild(e);
    e.style.left = (x) + "px";
    e.style.top = (y - e.offsetHeight) + "px";
}
function modeToStr() {
    switch(game.mode) {
        case 1: return "Multiple Choice"; break;
        case 2: return "Free Response"; break;
        case 3: return "Fill Out"; break;
        default: return ""; break;
    }
}
function gameToUrl() {
    return encodeURIComponent(
        game.mode.toString() +
        (game.recognizedOnly ? '1' : '0') +
        (game.includeIslands ? '1' : '0') +
        (game.timer ? '1' : '0') +
        (game.regions.indexOf('Africa') > -1 ? '1' : '0') +
        (game.regions.indexOf('Americas') > -1 ? '1' : '0') +
        (game.regions.indexOf('Asia') > -1 ? '1' : '0') +
        (game.regions.indexOf('Europe') > -1 ? '1' : '0') +
        (game.regions.indexOf('Oceania') > -1 ? '1' : '0') +
        (game.regions.indexOf('Polar') > -1 ? '1' : '0') +
        (game.modeData.autocomplete ? '1' : '0') +
        qToNum(game.modeData.q).toString() +
        aToNum(game.modeData.a).toString() +
        (game.modeData.numQuestions || 4).toString() + 
        (game.modeData.maxTime === Infinity ? (game.modeData.maxTime || 900) : 1).toString()
    );
}
function urlToGame(data) {
    var obj = Object.assign({}, defaultGame);
    data = decodeURIComponent(data);
    if(data.length < 15) {console.error('Data length less than 15'); return false;}
    try {
        // Mode
        var mode = parseInt(data[0]);
        if(mode < 1 || mode > 3) {console.error('Invalid mode: ' + data[0]);return false;}
        obj.mode = mode;

        // Flags
        obj.recognizedOnly = data[1] === '1';
        obj.includeIslands = data[2] === '1';
        obj.timer = data[3] === '1';
        obj.modeData.autocomplete = data[10] === '1';

        //Regions
        obj.regions = [];
        if(data[4] === '1') obj.regions.push('Africa');
        if(data[5] === '1') obj.regions.push('Americas');
        if(data[6] === '1') obj.regions.push('Asia');
        if(data[7] === '1') obj.regions.push('Europe');
        if(data[8] === '1') obj.regions.push('Oceania');
        if(data[9] === '1') obj.regions.push('Polar');

        // Question type
        var q = numToQ(parseInt(data[11]));
        if(!q) {console.error('Invalid q: ' + data[11]);return false;}
        obj.modeData.q = q;

        // Answer type
        var a = numToA(parseInt(data[12]));
        if(!a) {console.error('Invalid a: ' + data[12]);return false;}
        obj.modeData.a = a;

        // Num Questions
        var numQuestions = parseInt(data[13])
        if(numQuestions < 2 || numQuestions > 6) {console.error('Invalid num questions: ' + data[13]);return false;}
        obj.modeData.numQuestions = numQuestions;

        // Max time
        var maxTime = parseInt(data.substring(14))
        if(maxTime <= 0) {console.error('Invalid maxTime: ' + data.substring(14));return false;}
        obj.modeData.maxTime = maxTime;
    } 
    catch(e) {
        return false;
    }
    return obj;
}
const questionTypes = ['map', 'flag', 'name', 'capital', 'shape'];
function qToNum(q) {return questionTypes.indexOf(q);}
function numToQ(i) {if(i < 0 || i > questionTypes.length-1) {return null;} return questionTypes[i];}
const answerTypes = ['flag', 'name', 'capital'];
function aToNum(q) {return answerTypes.indexOf(q);}
function numToA(i) {if(i < 0 || i > answerTypes.length-1) {return null;} return answerTypes[i];}






serviceWorker.js:
var staticCacheName = "pwa-v" + new Date().getTime();
var filesToCache = [
    '.htaccess',
    'manifest.json',
    'index.html',
    'style.css',
    'script.js',
    'res/favicon.png',
    'res/favicon_maskable.png',
    'res/audio/correct.mp3',
    'res/audio/incorrect.mp3',
    'res/lib/fuzzysort-min.js',
    'res/lib/leaflet.js',
    'res/lib/leaflet.css',
    'res/lib/leaflet.js.map',
    'res/lib/smooth-zoom.js',
    'res/data/cities.json',
    'res/data/countries.geojson',
    'res/data/countryAliases.json',
    'res/data/countryInfo.json',
    'res/fonts/FiraCode-VariableFont_wght.ttf',
    'res/flags/AFG.svg',
    'res/flags/ALA.svg',
    'res/flags/ALB.svg',
    'res/flags/DZA.svg',
    'res/flags/ASM.svg',
    'res/flags/AND.svg',
    'res/flags/AGO.svg',
    'res/flags/AIA.svg',
    'res/flags/ATA.svg',
    'res/flags/ATG.svg',
    'res/flags/ARG.svg',
    'res/flags/ARM.svg',
    'res/flags/ABW.svg',
    'res/flags/AUS.svg',
    'res/flags/AUT.svg',
    'res/flags/AZE.svg',
    'res/flags/BHS.svg',
    'res/flags/BHR.svg',
    'res/flags/BGD.svg',
    'res/flags/BRB.svg',
    'res/flags/BLR.svg',
    'res/flags/BEL.svg',
    'res/flags/BLZ.svg',
    'res/flags/BEN.svg',
    'res/flags/BMU.svg',
    'res/flags/BTN.svg',
    'res/flags/BOL.svg',
    'res/flags/BES.svg',
    'res/flags/BIH.svg',
    'res/flags/BWA.svg',
    'res/flags/BVT.svg',
    'res/flags/BRA.svg',
    'res/flags/IOT.svg',
    'res/flags/VGB.svg',
    'res/flags/VIR.svg',
    'res/flags/BRN.svg',
    'res/flags/BGR.svg',
    'res/flags/BFA.svg',
    'res/flags/BDI.svg',
    'res/flags/KHM.svg',
    'res/flags/CMR.svg',
    'res/flags/CAN.svg',
    'res/flags/CPV.svg',
    'res/flags/CYM.svg',
    'res/flags/CAF.svg',
    'res/flags/TCD.svg',
    'res/flags/CHL.svg',
    'res/flags/CHN.svg',
    'res/flags/CXR.svg',
    'res/flags/CCK.svg',
    'res/flags/COL.svg',
    'res/flags/COM.svg',
    'res/flags/COG.svg',
    'res/flags/COD.svg',
    'res/flags/COK.svg',
    'res/flags/CRI.svg',
    'res/flags/HRV.svg',
    'res/flags/CUB.svg',
    'res/flags/CUW.svg',
    'res/flags/CYP.svg',
    'res/flags/CZE.svg',
    'res/flags/DNK.svg',
    'res/flags/DJI.svg',
    'res/flags/DMA.svg',
    'res/flags/DOM.svg',
    'res/flags/ECU.svg',
    'res/flags/EGY.svg',
    'res/flags/SLV.svg',
    'res/flags/GNQ.svg',
    'res/flags/ERI.svg',
    'res/flags/EST.svg',
    'res/flags/ETH.svg',
    'res/flags/FLK.svg',
    'res/flags/FRO.svg',
    'res/flags/FJI.svg',
    'res/flags/FIN.svg',
    'res/flags/FRA.svg',
    'res/flags/GUF.svg',
    'res/flags/PYF.svg',
    'res/flags/ATF.svg',
    'res/flags/GAB.svg',
    'res/flags/GMB.svg',
    'res/flags/GEO.svg',
    'res/flags/DEU.svg',
    'res/flags/GHA.svg',
    'res/flags/GIB.svg',
    'res/flags/GRC.svg',
    'res/flags/GRL.svg',
    'res/flags/GRD.svg',
    'res/flags/GLP.svg',
    'res/flags/GUM.svg',
    'res/flags/GTM.svg',
    'res/flags/GGY.svg',
    'res/flags/GIN.svg',
    'res/flags/GNB.svg',
    'res/flags/GUY.svg',
    'res/flags/HTI.svg',
    'res/flags/HMD.svg',
    'res/flags/VAT.svg',
    'res/flags/HND.svg',
    'res/flags/HKG.svg',
    'res/flags/HUN.svg',
    'res/flags/ISL.svg',
    'res/flags/IND.svg',
    'res/flags/IDN.svg',
    'res/flags/CIV.svg',
    'res/flags/IRN.svg',
    'res/flags/IRQ.svg',
    'res/flags/IRL.svg',
    'res/flags/IMN.svg',
    'res/flags/ISR.svg',
    'res/flags/ITA.svg',
    'res/flags/JAM.svg',
    'res/flags/JPN.svg',
    'res/flags/JEY.svg',
    'res/flags/JOR.svg',
    'res/flags/KAZ.svg',
    'res/flags/KEN.svg',
    'res/flags/KIR.svg',
    'res/flags/KWT.svg',
    'res/flags/KGZ.svg',
    'res/flags/LAO.svg',
    'res/flags/LVA.svg',
    'res/flags/LBN.svg',
    'res/flags/LSO.svg',
    'res/flags/LBR.svg',
    'res/flags/LBY.svg',
    'res/flags/LIE.svg',
    'res/flags/LTU.svg',
    'res/flags/LUX.svg',
    'res/flags/MAC.svg',
    'res/flags/MKD.svg',
    'res/flags/MDG.svg',
    'res/flags/MWI.svg',
    'res/flags/MYS.svg',
    'res/flags/MDV.svg',
    'res/flags/MLI.svg',
    'res/flags/MLT.svg',
    'res/flags/MHL.svg',
    'res/flags/MTQ.svg',
    'res/flags/MRT.svg',
    'res/flags/MUS.svg',
    'res/flags/MYT.svg',
    'res/flags/MEX.svg',
    'res/flags/FSM.svg',
    'res/flags/MDA.svg',
    'res/flags/MCO.svg',
    'res/flags/MNG.svg',
    'res/flags/MNE.svg',
    'res/flags/MSR.svg',
    'res/flags/MAR.svg',
    'res/flags/MOZ.svg',
    'res/flags/MMR.svg',
    'res/flags/NAM.svg',
    'res/flags/NRU.svg',
    'res/flags/NPL.svg',
    'res/flags/NLD.svg',
    'res/flags/NCL.svg',
    'res/flags/NZL.svg',
    'res/flags/NIC.svg',
    'res/flags/NER.svg',
    'res/flags/NGA.svg',
    'res/flags/NIU.svg',
    'res/flags/NFK.svg',
    'res/flags/PRK.svg',
    'res/flags/MNP.svg',
    'res/flags/NOR.svg',
    'res/flags/OMN.svg',
    'res/flags/PAK.svg',
    'res/flags/PLW.svg',
    'res/flags/PSE.svg',
    'res/flags/PAN.svg',
    'res/flags/PNG.svg',
    'res/flags/PRY.svg',
    'res/flags/PER.svg',
    'res/flags/PHL.svg',
    'res/flags/PCN.svg',
    'res/flags/POL.svg',
    'res/flags/PRT.svg',
    'res/flags/PRI.svg',
    'res/flags/QAT.svg',
    'res/flags/XKS.svg',
    'res/flags/REU.svg',
    'res/flags/ROU.svg',
    'res/flags/RUS.svg',
    'res/flags/RWA.svg',
    'res/flags/BLM.svg',
    'res/flags/SHN.svg',
    'res/flags/KNA.svg',
    'res/flags/LCA.svg',
    'res/flags/MAF.svg',
    'res/flags/SPM.svg',
    'res/flags/VCT.svg',
    'res/flags/WSM.svg',
    'res/flags/SMR.svg',
    'res/flags/STP.svg',
    'res/flags/SAU.svg',
    'res/flags/SEN.svg',
    'res/flags/SRB.svg',
    'res/flags/SYC.svg',
    'res/flags/SLE.svg',
    'res/flags/SGP.svg',
    'res/flags/SXM.svg',
    'res/flags/SVK.svg',
    'res/flags/SVN.svg',
    'res/flags/SLB.svg',
    'res/flags/SOM.svg',
    'res/flags/ZAF.svg',
    'res/flags/SGS.svg',
    'res/flags/KOR.svg',
    'res/flags/SSD.svg',
    'res/flags/ESP.svg',
    'res/flags/LKA.svg',
    'res/flags/SDN.svg',
    'res/flags/SUR.svg',
    'res/flags/SWZ.svg',
    'res/flags/SWE.svg',
    'res/flags/CHE.svg',
    'res/flags/SYR.svg',
    'res/flags/TWN.svg',
    'res/flags/TJK.svg',
    'res/flags/TZA.svg',
    'res/flags/THA.svg',
    'res/flags/TLS.svg',
    'res/flags/TGO.svg',
    'res/flags/TKL.svg',
    'res/flags/TON.svg',
    'res/flags/TTO.svg',
    'res/flags/TUN.svg',
    'res/flags/TUR.svg',
    'res/flags/TKM.svg',
    'res/flags/TCA.svg',
    'res/flags/TUV.svg',
    'res/flags/UGA.svg',
    'res/flags/UKR.svg',
    'res/flags/ARE.svg',
    'res/flags/GBR.svg',
    'res/flags/USA.svg',
    'res/flags/URY.svg',
    'res/flags/UZB.svg',
    'res/flags/VUT.svg',
    'res/flags/VEN.svg',
    'res/flags/VNM.svg',
    'res/flags/WLF.svg',
    'res/flags/ESH.svg',
    'res/flags/YEM.svg',
    'res/flags/ZMB.svg',
    'res/flags/ZWE.svg',
];

// Cache on install
self.addEventListener("install", event => {
    this.skipWaiting();
    event.waitUntil(
        caches.open(staticCacheName)
            .then(cache => {
                return cache.addAll(filesToCache);
            })
    )
});

// Clear cache on activate
self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames
                    .filter(cacheName => (cacheName.startsWith("pwa-")))
                    .filter(cacheName => (cacheName !== staticCacheName))
                    .map(cacheName => caches.delete(cacheName))
            );
        })
    );
});

// Serve from Cache
self.addEventListener("fetch", event => {
    event.respondWith(
        caches.match(event.request)
            .then(response => {
                return response || fetch(event.request);
            })
            .catch(() => {
                return caches.match('index.html');
            })
    )
});










style.css:
@font-face {
	font-family: 'Fira Code';
	src: url('res/fonts/FiraCode-VariableFont_wght.ttf');
}

:root {
    --background: #0A0A0A;
    --background_darker: black;
    --highlight: rgba(255, 255, 255, .05);
    --highlight_bright: rgba(255, 255, 255, .1);
    --selected: rgb(170, 217, 76, .3);
    --border: #3F4145;
    --border_light: #5E6166;
    --text: #FEFEFE;
    --dull: #999999;
    --text_bright: #FFFFFF;
    --accent: #AAD94C;
    --accent2: #D6A76B;
    --accent3: #3498DB;
    --error: #D95757;
    --warning: #EDBC5A;
    --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
    --monospace: "Fira Code", Inconsolata, "SFMono-Regular", "Consolas", "Menlo", monospace;
}

* {
    -webkit-font-smoothing: subpixel-antialiased;
    overscroll-behavior-y: none;
    overscroll-behavior-x: none;
    -webkit-tap-highlight-color: transparent;
}

html, body {
    height: 100%;
    width: 100%;
    position: relative;
    top: 0;
    left: 0;
    background: var(--background);
    overflow: hidden;
}

body {
    margin: 0;
    padding: 0;
    color: var(--text);
    font-family: var(--font);
    letter-spacing: 1px;
}

.screen {
    height: 100%;
    width: 100%;
    position: absolute;
    top: 0;
    left: 0;
    background: var(--background);
    pointer-events: none;
    opacity: 0;
    overflow-x: hidden;
    overflow-y: auto;
    z-index: -1;
    transition: opacity .5s;
}
#game {
    overflow: hidden;
    display: grid;
    grid-template-rows: 1fr auto;
}
.centered {
    display: grid;
    align-items: center;
    justify-content: center;
}
.centeredContent {
    width: 100%;
    text-align: center;
    max-width: 700px;
    display: inline-block;
}
.screen.visible {
    opacity: 1;
    z-index: 0;
    pointer-events: inherit;
}
.screen.paddedContent {
    text-align: center;
    padding: 20px;
    box-sizing: border-box;
}
p {
    width: 100%;
    max-width: 500px;
    font-size: .8em;
    margin: 10px 0;
    display: inline-block;
}
p b {
    color: var(--text_bright);
    font-weight: bold;
}
p a {
    text-decoration: none;
    color: var(--accent);
    position: relative;
}
p a:before {
    content: "";
    height: 0;
    width: 0;
    position: absolute;
    bottom: 0;
    left: 50%;
    -webkit-transform: translate(-50%);
    -moz-transform: translate(-50%);
    -o-transform: translate(-50%);
    -ms-transform: translate(-50%);
    transform: translate(-50%);
    border-bottom: 1px solid;
    transition: width .3s;
}
p a:hover:before {width: 100%;}
.hidden {
    height: 0;
    width: 0;
    display: none;
    opacity: 0;
    pointer-events: none;
}

.title {
    font-family: var(--font);
    color: var(--text_bright);
    font-weight: 700;
    font-size: 3em;
    margin: 10px 0;
    -webkit-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    -ms-user-select: none;
    user-select: none;
}
.subtitle {
    font-family: var(--monospace);
    color: var(--dull);
    font-weight: 400;
    font-size: .9em;
    margin: -5px 0 0 10px;
    -webkit-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    -ms-user-select: none;
    user-select: none;
}


#logo {
    height: 128px;
    width: 128px;
    position: relative;
    display: inline-block;
    margin: 10px 0;
    padding: 10px 0;
    -webkit-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    -ms-user-select: none;
    user-select: none;
}
#logo > svg {
    height: 100%;
    width: 100%;
    -webkit-animation: hover 3s ease-in-out infinite;
    -moz-animation: hover 3s ease-in-out infinite;
    -o-animation: hover 3s ease-in-out infinite;
    -ms-animation: hover 3s ease-in-out infinite;
    animation: hover 3s ease-in-out infinite;
    will-change: transform;
    transform: translateZ(0);
}
#logo:after {
    content: "";
    width: 80%;
    height: 20px;
    position: absolute;
    top: 100%;
    left: 50%;
    background: radial-gradient(rgba(0,0,0,.3) 0%, transparent 100%);
    border-radius: 50%;
    -webkit-transform: translateX(-50%);
    -moz-transform: translateX(-50%);
    -o-transform: translateX(-50%);
    -ms-transform: translateX(-50%);
    transform: translateX(-50%);
    -webkit-animation: shadow 3s ease-in-out infinite;
    -moz-animation: shadow 3s ease-in-out infinite;
    -o-animation: shadow 3s ease-in-out infinite;
    -ms-animation: shadow 3s ease-in-out infinite;
    animation: shadow 3s ease-in-out infinite;
    will-change: transform;
}

.titlebtn {
    margin: 5px;
    font-size: .9em;
    font-family: var(--monospace);
    text-transform: uppercase;
    letter-spacing: 2px;
    color: var(--text_bright);
    -webkit-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    -ms-user-select: none;
    user-select: none;
}
.titlebtn > div {
    position: relative;
    display: inline-block;
    padding: 15px 30px;
    cursor: pointer;
}
.titlebtn > div:after {
    content: "";
    position: absolute;
    height: 75%;
    width: 90%;
    left: -100%;
    top: 12.5%;
    background: var(--accent);
    opacity: 0;
    transition: opacity .3s ease-in, transform .3s, left .3s;
    z-index: -1;
}
.titlebtn > div:hover:after {
    opacity: .7;
    -webkit-transform: skew(-20deg);
    -moz-transform: skew(-20deg);
    -ms-transform: skew(-20deg);
    -o-transform: skew(-20deg);
    transform: skew(-20deg);
    left: 5%;
}
.halfhalf {
    display: grid;
    grid: 1fr / 1fr 1fr;
    text-align: center;
}
.halfhalf > div > div {
    margin: 10px 0;
}
.halfhalf label:first-child {
    text-align: center !important;
}

.radioLabel {
    padding: 7px 12px;
    margin: 5px;
    border-radius: 4px;
    box-sizing: border-box;
    display: inline-block;
    background: var(--highlight);
    font-family: var(--monospace);
    letter-spacing: 1px;
    text-transform: uppercase;
    font-size: .8em;
    border: 1px solid var(--border_light);
    transition: background .3s;
    -webkit-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    -ms-user-select: none;
    user-select: none;
    cursor: pointer;
}
.radioLabel:hover {
    background: var(--highlight_bright);
}
input:disabled + .radioLabel {
    background: var(--highlight) !important;
    opacity: .5;
    cursor: default;
}
input:checked + .radioLabel {
    background: var(--selected);
}


.inputWrap {
    width: 95%;
    max-width: 300px;
    margin: 20px 0;
    display: inline-block;
    vertical-align: middle;
    position: relative;
}
.inputWrap.checkbox {
    margin: 10px 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.inputWrap > label, .halfhalf > div > label, .newGameLabel {
    font-size: .8em;
    margin: 0 0 1px 5px;
    font-family: var(--monospace);
    text-align: left;
    text-transform: uppercase;
    color: var(--dull);
    display: block;
    -webkit-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    -ms-user-select: none;
    user-select: none;
    z-index: 1;
}
.inputWrap input, .inputWrap select, .inputWrap textarea {
    width: 100%;
    padding: 10px;
    box-sizing: border-box;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 4px;
    color: var(--text_bright);
    font-family: var(--font);
    font-size: 1em;
    outline: none;
    resize: none;
    transition: border-color .3s, opacity .3s;
}
.inputWrap input:disabled, .inputWrap select:disabled, .inputWrap textarea:disabled {
    opacity: .5;
}
.inputWrap input:focus, .inputWrap select:focus, .inputWrap textarea:focus {
    border-color: var(--accent);
}
.inputWrap select:focus {
    border-radius: 4px 4px 0 0;
}
.inputWrap input[type="checkbox"] {
    width: 1em;
    height: 1em;
    margin: 0;
    cursor: pointer;
}
.inputWrap input[type="checkbox"] + label {
    display: inline-block;
    margin: 0;
    padding: 5px 10px;
    cursor: pointer;
    color: var(--text_bright);
    transition: color .3s;
}
.inputWrap input[type="checkbox"]:checked + label {
    color: var(--accent);
}
.inputWrap input[type="checkbox"]:disabled + label {
    color: var(--dull);
    cursor: default;
}
.inputWrap select {
    padding-right: 44px;
    -webkit-appearance: none;
    appearance: none;
}
.inputWrap option {background-color: var(--background);color: var(--text);}
.inputWrap option:checked {color: var(--text_bright);}
.inputWrap select + svg {
    height: 10px;
    width: 10px;
    position: absolute;
    top: calc(50% - 5px);
    right: 10px;
    fill: var(--text_bright);
    pointer-events: none;
}
#regionCheckboxes {
    width: auto;
    display: inline-block;
    text-align: left;
}
#regionCheckboxes > .inputWrap {
    display: flex;
    width: auto;
    margin: 0;
    justify-content: left;
}

.gameModes {
    width: 90%;
    display: inline-block;
    text-align: center;
    -webkit-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    -ms-user-select: none;
    user-select: none;
}
.gameModes > label {
    width: 120px;
    margin: 10px;
    border: 1px solid var(--border_light);
    box-sizing: border-box;
    position: relative;
    display: inline-block;
    border-radius: 4px;
    background: var(--highlight);
    transition: background .3s;
    cursor: pointer;
}
.gameModes > label:hover {
    background: var(--highlight_bright);
}
.gameModes > input:checked + label {
    background: var(--selected);
}
.gameModes > label > div {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    padding: 15px;
    box-sizing: border-box;
}
.gameModes > label svg {
    width: 60%;
    fill: var(--text_bright);
}
.gameModes > label span {
    margin-top: 10px;
    display: block;
    font-size: .7em;
    letter-spacing: 0px;
    font-family: var(--monospace);
    text-transform: uppercase;
}
.gameModes > label:after {
    content: "";
    display: block;
    padding-bottom: 100%;
}
.settingGroup {display: none;}
.settingGroup.visible {display: block;}
#newGameErr {
    display: none;
    color: var(--error);
    font-family: var(--monospace);
    letter-spacing: 1px;
    font-size: .8em;
    margin: 20px 0;
}

/* Game Elements */
.map {
    height: 100%;
    width: 100%;
    background: var(--background) !important;
    -webkit-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    -ms-user-select: none;
    user-select: none;
    touch-action: none;
    perspective: 800px;
    z-index: 0;
}
.countryMarker {
    background: var(--background);
    border-radius: 50%;
    box-shadow: 0 0 10px 3px rgba(0,0,0,.4);
    padding: 10px;
}
.countryMarker svg {
    height: 100%;
    width: 100%;
    fill: var(--error);
    transform-origin: center center;
    -webkit-animation: countryMarker 3s ease-in-out infinite;
    -moz-animation: countryMarker 3s ease-in-out infinite;
    -ms-animation: countryMarker 3s ease-in-out infinite;
    -o-animation: countryMarker 3s ease-in-out infinite;
    animation: countryMarker 3s ease-in-out infinite;
}
.question {
    width: 100%;
    height: 100%;
    padding: 3% 5%;
    box-sizing: border-box;
    position: absolute;
    bottom: 0;
    left: 0;
    font-size: 5em;
    font-family: var(--font);
    color: var(--text_bright);
    display: flex;
    align-items: center;
    text-align: center;
    justify-content: center;
    opacity: 0;
    pointer-events: none;
    transition: opacity 1s;
}
.question > img {
    max-width: 90%;
    max-height: 90%;
    border-radius: 3px;
    box-shadow: 0 0 5px 0 rgba(0,0,0,.2);
    border: 5px solid white;
}
.question.visible {
    opacity: 1;
}
.question.small {
    height: auto;
    color: var(--background_darker);
    text-shadow: 0 0 10px rgba(255,255, 255, .5);
    align-items: flex-end;
}
.question.small > img {
    height: 100px;
    opacity: .85;
}
.countryShapeQuestion {
    border-radius: 0 !important;
    border: 0 !important;
    box-shadow: none !important;
    -webkit-filter: invert();
    -moz-filter: invert();
    -ms-filter: invert();
    -o-filter: invert();
    filter: invert();
}
#answerBank {
    width: 100%;
    text-align: center;
    z-index: 1;
}
#answerBank.floating {
    position: absolute;
    bottom: 25px;
    left: 0;
}
#fr_answer {
    width: 100%;
    box-shadow: 0 -4px 10px rgba(0,0,0,.5);
    display: none;
    position: relative;
}
#fr_answer.visible {
    display: block;
}
#fr_suggestions {
    width: 100%;
    position: absolute;
    left: 0;
    bottom: 100%;
    z-index: 1;
    display: flex;
    flex-direction: column-reverse;
}
#fr_suggestions > div {
    width: 100%;
    box-sizing: border-box;
    padding: 10px;
    font-family: var(--monospace);
    font-size: .8em;
    letter-spacing: 1px;
    text-transform: capitalize;
    position: relative;
    color: var(--text);
    text-align: left;
    transition: color .3s;
    cursor: pointer;
}
#fr_suggestions > div:hover, #fr_suggestions > div.selected {
    color: var(--text_bright);
}
#fr_suggestions > div b {
    color: var(--accent) !important;
}
#fr_suggestions > div:before, #fr_suggestions > .err:before {
    content: "";
    position: absolute;
    top: 0; left: 0; right: 0; bottom: 0;
    background: var(--background);
    opacity: .85;
    z-index: -1;
}
#fr_suggestions > div:after {
    content: "";
    position: absolute;
    top: 0; left: 0; right: 0; bottom: 0;
    background: var(--accent);
    opacity: 0;
    transition: opacity .3s;
    z-index: -1;
}
#fr_suggestions > div:hover:after {
    opacity: .08;
}
#fr_suggestions > div.selected:after {
    opacity: .25 !important;
}
#fr_suggestions > .err {
    width: 100%;
    box-sizing: border-box;
    padding: 10px;
    color: var(--error);
    font-family: var(--monospace);
    font-size: .8em;
    letter-spacing: 1px;
    position: relative;
    text-align: left;
}
#fr_answer_input {
    width: 100%;
    height: 48px;
    box-sizing: border-box;
    padding: 12px 10px;
    font-size: 1.2em;
    font-family: var(--monospace);
    color: var(--text_bright);
    background: var(--background);
    border: 0;
    margin: 0;
    border-top: 1px solid var(--border);
    border-radius: 0;
    text-transform: capitalize;
    -webkit-appearance: none;
    appearance: none;
    outline: none;
}
.mc_answers {
    width: 100%;
    text-align: center;
    opacity: 0;
    position: relative;
    pointer-events: none;
    transition: opacity .5s;
}
.mc_answers.visible {
    opacity: 1;
    pointer-events: inherit;
}
.mc_a {
    padding: 12px 20px;
    margin: 15px;
    position: relative;
    box-sizing: border-box;
    color: var(--background);
    font-family: var(--font);
    font-size: 1em;
    display: inline-block;
    -webkit-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    -ms-user-select: none;
    user-select: none;
    outline: none;
    transition: color .3s, background .3s, opacity .3s, font-size .3s;
    cursor: pointer;
}
.mc_a.correct {
    position: absolute;
    -webkit-transform: translateX(-50%);
    -moz-transform: translateX(-50%);
    -ms-transform: translateX(-50%);
    -o-transform: translateX(-50%);
    transform: translateX(-50%);
    left: 50%;
    bottom: 15px;
    pointer-events: none;
    font-size: 1.5em;
}
.mc_a:before {
    content: "";
    height: 100%;
    width: 100%;
    position: absolute;
    top: 0;
    left: 0;
    background: var(--accent2);
    -webkit-transform: skew(-20deg);
    -moz-transform: skew(-20deg);
    -ms-transform: skew(-20deg);
    -o-transform: skew(-20deg);
    transform: skew(-20deg);
    opacity: .8;
    box-shadow: 0 0 5px 0 rgba(0,0,0,.5);
    transition: opacity .3s;
    z-index: -1;
}
.mc_a.correct:before {
    background: var(--accent);
}
.mc_a.incorrect:before {
    background: var(--error);
}
.mc_a:hover {
    color: var(--background_darker);
}
.mc_a:hover:before {
    opacity: 1;
}
.mc_a.flag {
    padding: 0;
    border: 2px solid var(black);
    box-shadow: 0 0 0 2px white;
    border-radius: 3px;
    transition: border .3s, height .3s, width .3s;
    overflow: hidden;
}
.mc_a.flag.correct {
    border: 4px solid var(--accent);
    box-shadow: 0 0 5px rgba(0,0,0,.2);
}
.mc_a.flag.incorrect {
    display: none;
}
.mc_a.flag > img {
    height: 64px;
    display: block;
}
.mc_a.flag.correct > img {
    height: 100px;
}
.mc_a.flag:before {
    opacity: 0;
}

.feedbackSymbol {
    max-width: 256px;
    max-height: 256px;
    width: 90%;
    height: 90%;
    position: absolute;
    top: 50%;
    left: 50%;
    -webkit-transform: translate(-50%, -50%);
    -moz-transform: translate(-50%, -50%);
    -o-transform: translate(-50%, -50%);
    -ms-transform: translate(-50%, -50%);
    transform: translate(-50%, -50%);
    opacity: 0;
    pointer-events: none;
    transition: opacity .2s;
    z-index: 1;
}
.feedbackSymbol.visible {
    opacity: 1;
}
#fs_correct {
    fill: var(--accent);
}
#fs_incorrect {
    fill: var(--error);
}
.gameBtns {
    position: fixed;
    top: 15px;
    right: 15px;
    display: flex;
    flex-direction: column;
    z-index: 2;
}
.floatingBtn {
    height: 32px;
    width: 32px;
    position: relative;
    padding: 12px;
    margin-bottom: 10px;
    border-radius: 50%;
    box-shadow: 0 0 5px 0 rgba(0,0,0,.5);
    cursor: pointer;
    outline: none;
    overflow: hidden;
    backdrop-filter: blur(2px);
    z-index: 2;
}
.floatingBtn:before, #gameTimer:before {
    content: "";
    height: 100%;
    width: 100%;
    position: absolute;
    top: 0;
    left: 0;
    background: var(--background);
    opacity: .8;
    z-index: -1;
}
.floatingBtn.red:before {
    background: var(--error);
}
.floatingBtn > svg {
    width: 100%;
    fill: var(--text);
}
.floatingBtn.smaller {
    width: 24px;
    height: 24px;
}
#gameTimer {
    display: inline-block;
    padding: 15px;
    box-sizing: border-box;
    position: fixed;
    top: 0;
    left: 0;
    color: var(--text);
    border-bottom-right-radius: 5px;
    box-shadow: 0 0 5px 0 rgba(0,0,0,.5);
    -moz-pointer-events: none;
    -ms-pointer-events: none;
    -o-pointer-events: none;
    -webkit-pointer-events: none;
    pointer-events: none;
    border-right: 1px solid rgba(0,0,0, .3);
    border-bottom: 1px solid rgba(0,0,0, .3);
    font-family: var(--monospace);
    overflow: hidden;
    backdrop-filter: blur(2px);
    z-index: 2;
}
#gameTimer div {
    font-size: 2em;
}
#gameTimer.finale div {
    color: var(--error);
}
#gameTimer span {
    display: block;
    text-align: center;
    letter-spacing: 0;
    font-size: .8em;
    color: var(--dull);
}
#gameTimer span b {
    color: var(--accent);
    font-weight: normal;
}
#gameTimer span b.mid {
    color: var(--warning);
}
#gameTimer span b.bad {
    color: var(--error);
}
#menuStat {
    font-family: var(--font);
    color: var(--text_bright);
}
#menuStat > div:first-child {
    font-size: 4em;
    color: var(--accent);
}
#menuStat > div.mid:first-child {
    color: var(--warning);
}
#menuStat > div.bad:first-child {
    color: var(--error);
}
#menuStat > div:last-child {
    font-size: 1em;
    color: var(--dull);
    font-family: var(--monospace);
    letter-spacing: 1px;
}
#shareBtn {
    display: inline-block;
    padding: 5px;
    font-size: .8em;
    font-family: var(--monospace);
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--dull);
    stroke: var(--text);
    -webkit-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    -ms-user-select: none;
    user-select: none;
    cursor: pointer;
    text-align: center;
    transition: stroke .3s, color .3s;
}
#shareBtn > svg {
    display: inline-block;
    height: 24px;
    width: 24px;
    stroke: inherit;
    fill: transparent;
}
#shareBtn:hover {
    stroke: var(--accent);
    color: var(--accent);
}
#menuSummary {
    box-sizing: border-box;
    display: inline-grid;
    grid-template-columns: auto auto auto auto;
    padding: 30px 40px 50px 40px;
}
#menuSummary > div {
    text-align: left;
    display: inline-grid;
    justify-content: left;
    grid-template-columns: auto auto;
    padding: 10px 20px;
    font-size: .8em;
    align-items: center;
    font-family: var(--font);
}
#menuSummary > div > svg {
    height: 1em;
    width: 1em;
    margin-right: 8px;
}

#explore {
    display: grid;
    grid-template-columns: 1fr 350px;
    grid-template-rows: 1fr;
}
#exploreMap {
    width: 100vw;
    height: 100vh;
    background: var(--background) !important;
    -webkit-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    -ms-user-select: none;
    user-select: none;
    touch-action: none;
    z-index: 0;
}
#exploreDetails {
    height: 100vh;
    width: 100%;
    box-sizing: border-box;
    position: relative;
    display: none;
    flex-direction: column;
    background: var(--background);
    box-shadow: 0 0px 15px 10px rgba(0,0,0,.2);
    border-left: 1px solid var(--border);
    z-index: 1;
}
#resizeTarget {
    height: 100%;
    width: 11px;
    position: absolute;
    top: 0;
    left: -6px;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    -o-user-select: none;
    user-select: none;
    cursor: col-resize;
}
#resizeTarget:before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    background: transparent;
    z-index: 2;
    transition: background .3s;
}
#resizeTarget.active:before  {
    background: var(--highlight_bright);
}
#clickCover {
    height: 100vh;
    width: 100vw;
    position: fixed;
    top: 0;
    left: 0;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    -o-user-select: none;
    user-select: none;
    display: none;
    cursor: col-resize;
    z-index: 10000;
}
.closeExploreDetails {
    position: absolute;
    top: 10px;
    right: 10px;
    height: 24px;
    width: 24px;
    cursor: pointer;
    -webkit-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    -ms-user-select: none;
    user-select: none;
    fill: var(--dull);
    transition: fill .3s;
}
.closeExploreDetails:hover, .closeExploreDetails:active {
    fill: var(--error);
}
.exploreTitle {
    display: flex;
    flex-wrap: wrap;
    width: 100%;
    padding: 20px 20px 0 20px;
    box-sizing: border-box;
}
#exploreFlag {
    margin: 0 20px 20px 0;
    padding: 0;
    border: 2px solid white;
    border-radius: 3px;
}
#exploreCountryName {
    font-size: 2em;
    font-weight: bold;
    display: inline-block;
}
#exploreCountrySubtitle {
    color: var(--dull);
    font-family: monospace;
    letter-spacing: 1px;
    text-transform: uppercase;
    margin-bottom: 10px;
    display: flex;
    align-items: center;
}
#exploreCountrySubtitle > svg {
    height: 1em;
    width: 1em;
    fill: var(--accent3);
    margin-right: 5px;
}
#exploreCountryCodes {
    display: flex;
    grid-gap: 5px;
    align-items: center;
    flex-wrap: wrap;
}
.listen {
    height: 1.5em;
    width: 1.5em;
    fill: var(--dull);
    display: inline-block;
    cursor: pointer;
    transition: fill .3s;
}
.listen:hover, .listen:active {
    fill: var(--accent);
}
#exploreFacts {
    flex: 1;
    width: 100%;
    padding: 0 20px 20px 20px;
    box-sizing: border-box;
    overflow-y: auto;
    overflow-x: hidden;
}
#exploreFactGrid > div {
    display: flex;
    align-items: center;
    margin: 20px 0;
}
#exploreFactGrid > div > svg {
    height: 28px;
    width: 28px;
    fill: var(--text);
    margin-right: 10px;
}
#exploreFactGrid img {
    width: 32px;
    margin-right: 10px;
    border: 2px solid white;
    border-radius: 3px;
}
#exploreFactGrid span {
    font-family: var(--monospace);
    letter-spacing: 1px;
    font-size: .8em;
}
#exploreFactGrid span > d {
    color: var(--dull);
}
#exploreFactGrid > div > div > span:first-child {
    display: block;
    text-transform: uppercase;
    letter-spacing: 1px;
    color: var(--dull)
}
#exploreFactGrid > div > div > span:last-child {
    display: flex;
    grid-gap: 5px;
    align-items: center;
}
.exploreSection {
    margin: 20px 0;
}
.exploreSectionTitle {
    padding: 10px 36px 10px 0;
    width: 100%;
    box-sizing: border-box;
    position: relative;
    font-family: var(--monospace);
    letter-spacing: 1px;
    text-transform: uppercase;
    border-bottom: 1px dashed var(--dull);
    fill: var(--text);
    transition: color .3s, fill .3s, stroke .3s;
    display: flex;
    align-items: center;
    -webkit-user-select: none;
    user-select: none;
    cursor: pointer;
}
.exploreSectionTitle > svg:last-child {
    height: 16px;
    width: 16px;
    position: absolute;
    right: 10px;
    top: calc(50% - 8px);
    fill: inherit;
    -webkit-transform: rotate(90deg);
    -moz-transform: rotate(90deg);
    -ms-transform: rotate(90deg);
    -o-transform: rotate(90deg);
    transform: rotate(90deg);
    transition: transform .3s;
}
.exploreSectionTitle.visible > svg:last-child {
    -webkit-transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    transform: rotate(0deg); 
}
.exploreSectionTitle:hover, .exploreSectionTitle:active, .exploreSectionTitle.visible {
    color: var(--accent);
    fill: var(--accent);
}
.exploreSectionTitle > svg:first-child {
    height: 24px;
    width: 24px;
    fill: inherit;
    display: inline-block;
    margin-right: 10px;
}
.exploreSectionContent {
    display: none;
    margin-top: 10px;
}
.exploreSectionTitle.visible + .exploreSectionContent {
    display: block;
}
#exploreDetailSovereigntyImg {
    cursor: pointer;
}

#borderCountries, #territoryCountries {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: center;
}
.noData {
    display: block;
    text-align: center;
    color: var(--dull);
    text-transform: uppercase;
    letter-spacing: 1px;
    font-size: .9em;
    font-family: var(--monospace);
    margin: 5px 0;
}
#borderCountries > div, #territoryCountries > div {
    margin: 10px;
    padding: 10px;
    max-width: 100px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border-radius: 4px;
    border: 1px solid transparent;
    cursor: pointer;
    font-family: var(--monospace);
    letter-spacing: 1px;
    text-transform: uppercase;
    font-size: .8em;
    word-wrap: break-word;
    text-align: center;
    transition: background .3s, border .3s;
}
#borderCountries > div:hover, #territoryCountries > div:hover {
    background: var(--highlight);
    border: 1px solid var(--border);
}
#borderCountries > div > img, #territoryCountries > div > img {
    height: 32px;
    border-radius: 3px;
    margin-bottom: 5px;
    border: 2px solid white;
}
ul, ol {
    margin: 0;
    font-family: var(--monospace);
    letter-spacing: 1px;
    font-size: .8em;
}
ul {
    list-style-type: none;
}
ul > li, ol > li {
    text-indent: -10px;   
}
ul > li:before {
    content: "-";
    text-indent: -10px;
    margin-right: 10px;
}
ul d, ol d {
    color: var(--dull);
}
.subtleTable {
    width: 100%;
    border: 0;
    margin: 0;
    padding: 0;
    font-family: var(--monospace);
    letter-spacing: 1px;
    font-size: .8em;
    text-align: left;
    border-collapse: collapse;
}
.subtleTable th:first-child, .subtleTable td:first-child, .subtleTable td:last-child {
    color: var(--dull);
}
.subtleTable td:nth-child(2) {
    text-transform: capitalize;
}
.subtleTable th {
    text-transform: uppercase;
    font-weight: normal;
    border-bottom: 1px solid var(--border);
}
.subtleTable td, .subtleTable th {
    padding: 2px 8px;
    white-space: nowrap;
}
.subtleTable td:last-child, .subtleTable th:last-child  {
    width: 99%;
}

#exploreMenu {
    position: absolute;
    top: 0px;
    left: 0px;
    width: 100%;
    box-sizing: border-box;
    padding: 15px;
    z-index: 3;
    -webkit-pointer-events: none;
    -moz-pointer-events: none;
    -ms-pointer-events: none;
    -o-pointer-events: none;
    pointer-events: none;
}
#exploreMenu > div {
    height: 32px;
    width: 32px;
    padding: 12px;
    border-radius: 50%;
    margin: 10px;
    display: block;
    box-shadow: 0 0 5px 0 rgba(0,0,0,.5);
    cursor: pointer;
    outline: none;
    fill: var(--text);
    -webkit-pointer-events: all;
    -moz-pointer-events: all;
    -ms-pointer-events: all;
    -o-pointer-events: all;
    pointer-events: all;
    backdrop-filter: blur(2px);
    overflow: hidden;
    position: relative;
    z-index: 2;
}
#exploreMenu > div:before {
    content: "";
    height: 100%;
    width: 100%;
    position: absolute;
    top: 0;
    left: 0;
    background: var(--background);
    opacity: .8;
    z-index: -1;
}
#exploreSearch {
    position: absolute;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    padding: 15% 5% 5% 5%;
    background: rgba(0, 0, 0, .2);
    justify-content: center;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    -o-user-select: none;
    user-select: none;
    z-index: 4;
    display: none;
    text-align: center;
}
#exploreSearch.visible {
    display: block;
}
#exploreSearch.visible + div, #exploreSearch.visible + div + div, #exploreSearch.visible + div + div + div {
    -webkit-filter: blur(1px);
    filter: blur(1px);
}
#exploreSearch > div {
    width: 100%;
    max-width: 500px;
    display: inline-block;
    box-shadow: 0 0 15px 10px rgba(0,0,0,.2);
    border: 1px solid var(--border);
    background: var(--background);
    border-radius: 5px;
    overflow: hidden;
}
#searchBarWrapper {
    display: grid;
    grid-template-columns: auto 1fr;
    z-index: 0;
}
#searchBarWrapper > svg {
    padding: 13px;
    height: 16px;
    width: 16px;
    fill: var(--text);
}
#exploreSearch input {
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    font-size: 1em;
    font-family: var(--monospace);
    letter-spacing: 1px;
    color: var(--text);
    border: 0;
    background: transparent;
    outline: none;
}
#exploreSearchSuggestions {
    width: 100%;
    border-top: 0;
    box-sizing: border-box;
    z-index: 2;
} 
#exploreSearchSuggestions > div {
    padding: 10px;
    font-family: var(--monospace);
    color: var(--dull);
    text-align: left;
    letter-spacing: 1px;
    font-size: .8em;
    position: relative;
    cursor: pointer;
    text-transform: capitalize;
    z-index: 0;
}
#exploreSearchSuggestions > div:hover {
    background: var(--highlight);
}
#exploreSearchSuggestions > div.active {
    background: transparent !important;
}
#exploreSearchSuggestions > div.active:before {
    content: '';
    height: 100%;
    width: 100%;
    position: absolute;
    left: 0;
    top: 0;
    -webkit-pointer-events: none;
    -moz-pointer-events: none;
    -ms-pointer-events: none;
    -o-pointer-events: none;
    pointer-events: none;
    background: var(--accent);
    opacity: .2;
    z-index: -1;
}
#exploreSearchSuggestions > div > b {
    color: var(--accent);
    font-weight: 400;
}





.loader {
    height: 64px;
    width: 64px;
    position: fixed;
    top: calc(50% - 90px);
    left: calc(50% - 32px);
    border-radius: 50%;
    border: 2px solid rgba(255, 255, 255, .5);
    border-left-color: var(--accent);
    -webkit-animation: loader 1.5s linear infinite;
    -moz-animation: loader 1.5s linear infinite;
    -o-animation: loader 1.5s linear infinite;
    -ms-animation: loader 1.5s linear infinite;
    animation: loader 1.5s linear infinite;
    z-index: 3;
}
.loaderText {
    margin-top: 58px;
    letter-spacing: 3px;
    font-family: var(--monospace);
    -webkit-animation: loaderText 2s ease-in-out infinite;
    -moz-animation: loaderText 2s ease-in-out infinite;
    -o-animation: loaderText 2s ease-in-out infinite;
    -ms-animation: loaderText 2s ease-in-out infinite;
    animation: loaderText 2s ease-in-out infinite;
}
.fadeMsg {
    display: inline-block;
    padding: 5px 10px;
    position: fixed;
    top: 0;
    left: 0;
    border-radius: 4px;
    font-size: .8em;
    letter-spacing: 1px;
    font-family: var(--monospace);
    background: var(--background);
    color: var(--text);
    box-shadow: 0 0 5px 0 rgba(0,0,0,.2);
    -webkit-pointer-events: none;
    -moz-pointer-events: none;
    -ms-pointer-events: none;
    -o-pointer-events: none;
    pointer-events: none;
    -webkit-animation: fadeMsg 3s ease-in-out 1;
    -mos-animation: fadeMsg 3s ease-in-out 1;
    -ms-animation: fadeMsg 3s ease-in-out 1;
    -o-animation: fadeMsg 3s ease-in-out 1;
    animation: fadeMsg 3s ease-in-out 1;
    -webkit-animation-fill-mode: forwards;
    -moz-animation-fill-mode: forwards;
    -o-animation-fill-mode: forwards;
    -ms-animation-fill-mode: forwards;
    animation-fill-mode: forwards;
    border: 1px solid var(--border);
    background: var(--highlight);
    z-index: 5;
    overflow: hidden;
}
.fadeMsg:before {
    content: "";
    height: 100%;
    width: 100%;
    position: absolute;
    top: 0;
    left: 0;
    background: var(--highlight);
    z-index: -1;
}
@-webkit-keyframes fadeMsg {
    50% {opacity: 1;}
    100% {opacity: 0;-webkit-transform: translate(30px, -30px);-moz-transform: translate(30px, -30px);-ms-transform: translate(30px, -30px);-o-transform: translate(30px, -30px);transform: translate(30px, -30px);}
}
@keyframes fadeMsg {
    50% {opacity: 1;}
    100% {opacity: 0;-webkit-transform: translate(30px, -30px);-moz-transform: translate(30px, -30px);-ms-transform: translate(30px, -30px);-o-transform: translate(30px, -30px);transform: translate(30px, -30px);}
}
@-webkit-keyframes countryMarker {
    100% {-webkit-transform: rotateY(-360deg);-moz-transform: rotateY(-360deg);-ms-transform: rotateY(-360deg);-o-transform: rotateY(-360deg);transform: rotateY(-360deg);}
}
@keyframes countryMarker {
    100% {-webkit-transform: rotateY(-360deg);-moz-transform: rotateY(-360deg);-ms-transform: rotateY(-360deg);-o-transform: rotateY(-360deg);transform: rotateY(-360deg);}
}
@-webkit-keyframes loaderText {
    50% {color: var(--dull);}
}
@keyframes loaderText {
    50% {color: var(--dull);}
}
@-webkit-keyframes loader {
    100% {-moz-transform: rotate(360deg);-o-transform: rotate(360deg);-ms-transform: rotate(360deg);-webkit-transform: rotate(360deg);transform: rotate(360deg);}
}
@keyframes loader {
    100% {-moz-transform: rotate(360deg);-o-transform: rotate(360deg);-ms-transform: rotate(360deg);-webkit-transform: rotate(360deg);transform: rotate(360deg);}
}
@-webkit-keyframes skewhover {
    75% {left: 15%;-webkit-transform: skew(-20deg);-moz-transform: skew(-20deg);-ms-transform: skew(-20deg);-o-transform: skew(-20deg);transform: skew(-20deg);}
    100% {left:5%;-webkit-transform: skew(-20deg);-moz-transform: skew(-20deg);-ms-transform: skew(-20deg);-o-transform: skew(-20deg);transform: skew(-20deg);}
}
@keyframes skewhover {
    75% {left: 15%;-webkit-transform: skew(-20deg);-moz-transform: skew(-20deg);-ms-transform: skew(-20deg);-o-transform: skew(-20deg);transform: skew(-20deg);}
    100% {left:5%;-webkit-transform: skew(-20deg);-moz-transform: skew(-20deg);-ms-transform: skew(-20deg);-o-transform: skew(-20deg);transform: skew(-20deg);}
}
@-webkit-keyframes hover {
    50% {-webkit-transform: translateY(-5%);-moz-transform: translateY(-5%);-o-transform: translateY(-5%);-ms-transform: translateY(-5%);transform: translateY(-5%);}
    100% {-webkit-transform: translateY(0);-moz-transform: translateY(0);-o-transform: translateY(0);-ms-transform: translateY(0);transform: translateY(0);}
}
@keyframes hover {
    50% {-webkit-transform: translateY(-5%);-moz-transform: translateY(-5%);-o-transform: translateY(-5%);-ms-transform: translateY(-5%);transform: translateY(-5%);}
    100% {-webkit-transform: translateY(0);-moz-transform: translateY(0);-o-transform: translateY(0);-ms-transform: translateY(0);transform: translateY(0);}
}
@-webkit-keyframes shadow {
    50% {-webkit-transform: translateX(-50%) scaleX(.9);-moz-transform: translateX(-50%) scaleX(.9);-o-transform: translateX(-50%) scaleX(.9);-ms-transform: translateX(-50%) scaleX(.9);transform: translateX(-50%) scaleX(.9);}
    100% {-webkit-transform: translateX(-50%) scaleX(1);-moz-transform: translateX(-50%) scaleX(1);-o-transform: translateX(-50%) scaleX(1);-ms-transform: translateX(-50%) scaleX(1);transform: translateX(-50%) scaleX(1);}
}
@keyframes shadow {
    50% {-webkit-transform: translateX(-50%) scaleX(.9);-moz-transform: translateX(-50%) scaleX(.9);-o-transform: translateX(-50%) scaleX(.9);-ms-transform: translateX(-50%) scaleX(.9);transform: translateX(-50%) scaleX(.9);}
    100% {-webkit-transform: translateX(-50%) scaleX(1);-moz-transform: translateX(-50%) scaleX(1);-o-transform: translateX(-50%) scaleX(1);-ms-transform: translateX(-50%) scaleX(1);transform: translateX(-50%) scaleX(1);}
}
* {
  scrollbar-width: thin;
  scrollbar-color: dark;
}
::-webkit-scrollbar {
  width: 10px;
  height: 10px;
  background: none; 
}
::-webkit-scrollbar-track {
  background: none;
}
::-webkit-scrollbar-thumb {
  background-color: var(--highlight);
  border-radius: 5px;
  border: 0;
}
::-webkit-scrollbar-thumb:hover, ::-webkit-scrollbar-thumb:active {
  background-color: var(--highlight_bright);
  border: 0;
}
::-webkit-scrollbar-track-thumb {
  background-color: red;
}
::-webkit-scrollbar-corner, ::-webkit-scrollbar-button {
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  display: none;
  border: 0;
}

@media all and (max-width: 1000px) {
    #explore {
        grid-template-rows: auto 40%;
        grid-template-columns: 1fr;
    }
    #exploreDetails {
        height: 100%;
        width: 100vw;
        border-left: 0;
        border-top: 1px solid var(--border);
    }
    #exploreFactGrid {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        justify-content: center;
    }
    #exploreFactGrid > div {
        margin: 20px;
    }
    #exploreMenu {
        display: flex;
    }
    #exploreMenu > svg {
        display: inline-block;
    }
    #resizeTarget {
        width: 100%;
        height: 51px;
        left: 0px;
        top: -11px;
        cursor: default;
    }
    #resizeTarget:before {
        width: 100%;
        height: 11px;
        top: 5px;
        left: 0;
        cursor: row-resize;
    }
    #clickCover {
        cursor: row-resize;
    }
}
@media all and (max-width: 1000px) and (max-height: 900px) {
    #explore {
        grid-template-rows: auto 50%;
        grid-template-columns: 1fr;
    }
}
@media all and (max-width: 800px) {
    #menuSummary {
        grid-template-columns: auto auto auto;
    }
    #menuSummary > div {
        padding: 10px;
    }
}
@media all and (max-width: 500px) {
    #menuSummary {
        grid-template-columns: auto auto;
        padding: 30px 20px 50px 20px;
    }
    #exploreFactGrid {
        display: grid;
        grid: 1fr 1fr 1fr / 1fr 1fr;
    }
    #exploreFactGrid > div {
        margin: 15px;
    }
}
@media all and (max-width: 400px) {
    #menuSummary {
        grid-template-columns: auto;
        padding: 30px 15px 50px 15px;
    }
}






ты должен перенести эту игру и сделать отдельную страницу чтобы на нее можно было перейти и играть в нее напиши полные коды для всех новых нужных классов и все что нужно добавить в существующие, у меня сайт на next js кстати