From 1fcc5ed70d525910687c9b7c046252cf12cd581b Mon Sep 17 00:00:00 2001
From: Primakov Alexandr Alexandrovich <primakov.pro@yandex.ru>
Date: Tue, 11 Mar 2025 23:50:50 +0300
Subject: [PATCH] init Questionnaire

---
 server/index.js                               |    1 +
 server/models/questionnaire.js                |   60 +
 server/routers/questioneer/index.js           |  421 ++++
 server/routers/questioneer/public/admin.html  |   70 +
 server/routers/questioneer/public/create.html |  129 ++
 server/routers/questioneer/public/edit.html   |  146 ++
 server/routers/questioneer/public/index.html  |   42 +
 server/routers/questioneer/public/poll.html   |   45 +
 .../questioneer/public/static/css/style.css   | 1830 +++++++++++++++++
 .../questioneer/public/static/js/admin.js     |  294 +++
 .../questioneer/public/static/js/common.js    |  236 +++
 .../questioneer/public/static/js/create.js    |  343 +++
 .../questioneer/public/static/js/edit.js      |  332 +++
 .../questioneer/public/static/js/index.js     |   67 +
 .../questioneer/public/static/js/poll.js      | 1069 ++++++++++
 15 files changed, 5085 insertions(+)
 create mode 100644 server/models/questionnaire.js
 create mode 100644 server/routers/questioneer/index.js
 create mode 100644 server/routers/questioneer/public/admin.html
 create mode 100644 server/routers/questioneer/public/create.html
 create mode 100644 server/routers/questioneer/public/edit.html
 create mode 100644 server/routers/questioneer/public/index.html
 create mode 100644 server/routers/questioneer/public/poll.html
 create mode 100644 server/routers/questioneer/public/static/css/style.css
 create mode 100644 server/routers/questioneer/public/static/js/admin.js
 create mode 100644 server/routers/questioneer/public/static/js/common.js
 create mode 100644 server/routers/questioneer/public/static/js/create.js
 create mode 100644 server/routers/questioneer/public/static/js/edit.js
 create mode 100644 server/routers/questioneer/public/static/js/index.js
 create mode 100644 server/routers/questioneer/public/static/js/poll.js

diff --git a/server/index.js b/server/index.js
index 1e81309..2a179cd 100644
--- a/server/index.js
+++ b/server/index.js
@@ -90,6 +90,7 @@ app.use("/dhs-testing", require("./routers/dhs-testing"))
 app.use("/gamehub", require("./routers/gamehub")) 
 app.use("/esc", require("./routers/esc"))
 app.use('/connectme', require('./routers/connectme'))
+app.use('/questioneer', require('./routers/questioneer'))
 
 app.use(require("./error"))
 
diff --git a/server/models/questionnaire.js b/server/models/questionnaire.js
new file mode 100644
index 0000000..e08928e
--- /dev/null
+++ b/server/models/questionnaire.js
@@ -0,0 +1,60 @@
+const mongoose = require('mongoose');
+
+// Типы вопросов
+const QUESTION_TYPES = {
+  SINGLE_CHOICE: 'single_choice', // Один вариант
+  MULTIPLE_CHOICE: 'multiple_choice', // Несколько вариантов
+  TEXT: 'text', // Текстовый ответ
+  RATING: 'rating', // Оценка по шкале
+  TAG_CLOUD: 'tag_cloud' // Облако тегов
+};
+
+// Типы отображения
+const DISPLAY_TYPES = {
+  DEFAULT: 'default',
+  TAG_CLOUD: 'tag_cloud',
+  VOTING: 'voting',
+  POLL: 'poll'
+};
+
+// Схема варианта ответа
+const optionSchema = new mongoose.Schema({
+  text: { type: String, required: true },
+  count: { type: Number, default: 0 } // счетчик голосов
+});
+
+// Схема вопроса
+const questionSchema = new mongoose.Schema({
+  text: { type: String, required: true },
+  type: { 
+    type: String, 
+    enum: Object.values(QUESTION_TYPES),
+    required: true 
+  },
+  options: [optionSchema],
+  required: { type: Boolean, default: false }
+});
+
+// Схема опроса
+const questionnaireSchema = new mongoose.Schema({
+  title: { type: String, required: true },
+  description: { type: String },
+  questions: [questionSchema],
+  displayType: { 
+    type: String, 
+    enum: Object.values(DISPLAY_TYPES),
+    default: DISPLAY_TYPES.DEFAULT 
+  },
+  createdAt: { type: Date, default: Date.now },
+  updatedAt: { type: Date, default: Date.now },
+  adminLink: { type: String, required: true }, // ссылка для редактирования
+  publicLink: { type: String, required: true } // ссылка для голосования
+});
+
+const Questionnaire = mongoose.model('Questionnaire', questionnaireSchema);
+
+module.exports = {
+  Questionnaire,
+  QUESTION_TYPES,
+  DISPLAY_TYPES
+}; 
\ No newline at end of file
diff --git a/server/routers/questioneer/index.js b/server/routers/questioneer/index.js
new file mode 100644
index 0000000..c031883
--- /dev/null
+++ b/server/routers/questioneer/index.js
@@ -0,0 +1,421 @@
+const express = require('express')
+const { Router } = require("express")
+const router = Router()
+const crypto = require('crypto')
+const path = require('path')
+const { getDB } = require('../../utils/mongo')
+const mongoose = require('mongoose')
+
+// Используем одно определение модели
+const Questionnaire = (() => {
+  // Если модель уже существует, используем её
+  if (mongoose.models.Questionnaire) {
+    return mongoose.models.Questionnaire;
+  }
+  
+  // Иначе создаем новую модель
+  const questionnaireSchema = new mongoose.Schema({
+    title: { type: String, required: true },
+    description: { type: String },
+    questions: [{
+      text: { type: String, required: true },
+      type: { 
+        type: String, 
+        enum: ['single_choice', 'multiple_choice', 'text', 'rating', 'tag_cloud', 'scale'],
+        required: true 
+      },
+      required: { type: Boolean, default: false },
+      options: [{
+        text: { type: String, required: true },
+        count: { type: Number, default: 0 }
+      }],
+      scaleMin: { type: Number },
+      scaleMax: { type: Number },
+      scaleMinLabel: { type: String },
+      scaleMaxLabel: { type: String },
+      answers: [{ type: String }],
+      scaleValues: [{ type: Number }],
+      tags: [{ 
+        text: { type: String },
+        count: { type: Number, default: 1 }
+      }]
+    }],
+    displayType: { 
+      type: String, 
+      enum: ['default', 'tag_cloud', 'voting', 'poll', 'step_by_step'],
+      default: 'step_by_step' 
+    },
+    createdAt: { type: Date, default: Date.now },
+    updatedAt: { type: Date, default: Date.now },
+    adminLink: { type: String, required: true },
+    publicLink: { type: String, required: true }
+  });
+  
+  return mongoose.model('Questionnaire', questionnaireSchema);
+})();
+
+// Middleware для парсинга JSON
+router.use(express.json());
+
+// Обслуживание статичных файлов - проверяем правильность пути
+router.use('/static', express.static(path.join(__dirname, 'public', 'static')));
+
+// Получить главную страницу
+router.get("/", (req, res) => {
+  res.sendFile(path.join(__dirname, 'public/index.html'))
+})
+
+// Страница создания нового опроса
+router.get("/create", (req, res) => {
+  res.sendFile(path.join(__dirname, 'public/create.html'))
+})
+
+// Страница редактирования опроса
+router.get("/edit/:adminLink", (req, res) => {
+  res.sendFile(path.join(__dirname, 'public/edit.html'))
+})
+
+// Страница администрирования опроса
+router.get("/admin/:adminLink", (req, res) => {
+  res.sendFile(path.join(__dirname, 'public/admin.html'))
+})
+
+// Страница голосования
+router.get("/poll/:publicLink", (req, res) => {
+  res.sendFile(path.join(__dirname, 'public/poll.html'))
+})
+
+// API для работы с опросами
+
+// Создать новый опрос
+router.post("/api/questionnaires", async (req, res) => {
+  try {
+    // Проверка наличия нужных полей
+    const { title, questions } = req.body;
+    
+    if (!title || !Array.isArray(questions) || questions.length === 0) {
+      return res.json({ success: false, error: 'Необходимо указать название и хотя бы один вопрос' });
+    }
+    
+    // Создаем уникальные ссылки
+    const adminLink = crypto.randomBytes(6).toString('hex');
+    const publicLink = crypto.randomBytes(6).toString('hex');
+    
+    // Устанавливаем тип отображения step_by_step, если не указан
+    if (!req.body.displayType) {
+      req.body.displayType = 'step_by_step';
+    }
+    
+    // Создаем новый опросник
+    const questionnaire = new Questionnaire({
+      ...req.body,
+      adminLink,
+      publicLink
+    });
+    
+    await questionnaire.save();
+    
+    res.json({
+      success: true,
+      data: {
+        adminLink,
+        publicLink
+      }
+    });
+  } catch (error) {
+    console.error('Error creating questionnaire:', error);
+    res.json({ success: false, error: error.message });
+  }
+});
+
+// Получить все опросы
+router.get("/api/questionnaires", async (req, res) => {
+  try {
+    const questionnaires = await Questionnaire.find({}, {
+      title: 1,
+      description: 1,
+      createdAt: 1,
+      updatedAt: 1,
+      _id: 1,
+      adminLink: 1,
+      publicLink: 1
+    }).sort({ createdAt: -1 })
+    
+    res.status(200).json({
+      success: true,
+      data: questionnaires
+    })
+  } catch (error) {
+    console.error('Error fetching questionnaires:', error)
+    res.status(500).json({ 
+      success: false, 
+      error: 'Failed to fetch questionnaires' 
+    })
+  }
+})
+
+// Получить опрос по ID для админа
+router.get("/api/questionnaires/admin/:adminLink", async (req, res) => {
+  try {
+    const { adminLink } = req.params
+    const questionnaire = await Questionnaire.findOne({ adminLink })
+    
+    if (!questionnaire) {
+      return res.status(404).json({
+        success: false,
+        error: 'Questionnaire not found'
+      })
+    }
+    
+    res.status(200).json({
+      success: true,
+      data: questionnaire
+    })
+  } catch (error) {
+    console.error('Error fetching questionnaire:', error)
+    res.status(500).json({ 
+      success: false, 
+      error: 'Failed to fetch questionnaire' 
+    })
+  }
+})
+
+// Получить опрос по публичной ссылке (для голосования)
+router.get("/api/questionnaires/public/:publicLink", async (req, res) => {
+  try {
+    const { publicLink } = req.params
+    const questionnaire = await Questionnaire.findOne({ publicLink })
+    
+    if (!questionnaire) {
+      return res.status(404).json({
+        success: false,
+        error: 'Questionnaire not found'
+      })
+    }
+    
+    res.status(200).json({
+      success: true,
+      data: questionnaire
+    })
+  } catch (error) {
+    console.error('Error fetching questionnaire:', error)
+    res.status(500).json({ 
+      success: false, 
+      error: 'Failed to fetch questionnaire' 
+    })
+  }
+})
+
+// Обновить опрос
+router.put("/api/questionnaires/:adminLink", async (req, res) => {
+  try {
+    const { adminLink } = req.params
+    const { title, description, questions, displayType } = req.body
+    
+    const updatedQuestionnaire = await Questionnaire.findOneAndUpdate(
+      { adminLink },
+      { 
+        title, 
+        description, 
+        questions, 
+        displayType,
+        updatedAt: Date.now() 
+      },
+      { new: true }
+    )
+    
+    if (!updatedQuestionnaire) {
+      return res.status(404).json({
+        success: false,
+        error: 'Questionnaire not found'
+      })
+    }
+    
+    res.status(200).json({
+      success: true,
+      data: updatedQuestionnaire
+    })
+  } catch (error) {
+    console.error('Error updating questionnaire:', error)
+    res.status(500).json({ 
+      success: false, 
+      error: 'Failed to update questionnaire' 
+    })
+  }
+})
+
+// Удалить опрос
+router.delete("/api/questionnaires/:adminLink", async (req, res) => {
+  try {
+    const { adminLink } = req.params
+    
+    const deletedQuestionnaire = await Questionnaire.findOneAndDelete({ adminLink })
+    
+    if (!deletedQuestionnaire) {
+      return res.status(404).json({
+        success: false,
+        error: 'Questionnaire not found'
+      })
+    }
+    
+    res.status(200).json({
+      success: true,
+      message: 'Questionnaire deleted successfully'
+    })
+  } catch (error) {
+    console.error('Error deleting questionnaire:', error)
+    res.status(500).json({ 
+      success: false, 
+      error: 'Failed to delete questionnaire' 
+    })
+  }
+})
+
+// Голосование в опросе
+router.post("/api/vote/:publicLink", async (req, res) => {
+  try {
+    const { publicLink } = req.params
+    const { answers } = req.body
+    
+    const questionnaire = await Questionnaire.findOne({ publicLink })
+    
+    if (!questionnaire) {
+      return res.status(404).json({
+        success: false,
+        error: 'Questionnaire not found'
+      })
+    }
+    
+    // Обновить счетчики голосов
+    answers.forEach(answer => {
+      const { questionIndex, optionIndices, textAnswer, scaleValue, tagTexts } = answer
+      
+      // Обработка одиночного и множественного выбора
+      if (Array.isArray(optionIndices)) {
+        // Для множественного выбора
+        optionIndices.forEach(optionIndex => {
+          if (questionnaire.questions[questionIndex] && 
+              questionnaire.questions[questionIndex].options[optionIndex]) {
+            questionnaire.questions[questionIndex].options[optionIndex].count += 1
+          }
+        })
+      } else if (typeof optionIndices === 'number') {
+        // Для единичного выбора
+        if (questionnaire.questions[questionIndex] && 
+            questionnaire.questions[questionIndex].options[optionIndices]) {
+          questionnaire.questions[questionIndex].options[optionIndices].count += 1
+        }
+      }
+      
+      // Сохраняем текстовые ответы
+      if (textAnswer && questionnaire.questions[questionIndex]) {
+        if (!questionnaire.questions[questionIndex].answers) {
+          questionnaire.questions[questionIndex].answers = [];
+        }
+        questionnaire.questions[questionIndex].answers.push(textAnswer);
+      }
+      
+      // Сохраняем ответы шкалы оценки
+      if (scaleValue !== undefined && questionnaire.questions[questionIndex]) {
+        if (!questionnaire.questions[questionIndex].scaleValues) {
+          questionnaire.questions[questionIndex].scaleValues = [];
+        }
+        questionnaire.questions[questionIndex].scaleValues.push(scaleValue);
+      }
+      
+      // Сохраняем теги
+      if (Array.isArray(tagTexts) && tagTexts.length > 0 && questionnaire.questions[questionIndex]) {
+        if (!questionnaire.questions[questionIndex].tags) {
+          questionnaire.questions[questionIndex].tags = [];
+        }
+        
+        tagTexts.forEach(tagText => {
+          const existingTag = questionnaire.questions[questionIndex].tags.find(t => t.text === tagText);
+          if (existingTag) {
+            existingTag.count += 1;
+          } else {
+            questionnaire.questions[questionIndex].tags.push({ text: tagText, count: 1 });
+          }
+        });
+      }
+    })
+    
+    await questionnaire.save()
+    
+    res.status(200).json({
+      success: true,
+      message: 'Vote registered successfully'
+    })
+  } catch (error) {
+    console.error('Error registering vote:', error)
+    res.status(500).json({ 
+      success: false, 
+      error: 'Failed to register vote' 
+    })
+  }
+})
+
+// Получить результаты опроса по публичной ссылке
+router.get("/api/results/:publicLink", async (req, res) => {
+  try {
+    const { publicLink } = req.params;
+    const questionnaire = await Questionnaire.findOne({ publicLink });
+    
+    if (!questionnaire) {
+      return res.status(404).json({
+        success: false,
+        error: 'Questionnaire not found'
+      });
+    }
+    
+    // Формируем результаты для отправки
+    const results = {
+      title: questionnaire.title,
+      description: questionnaire.description,
+      questions: questionnaire.questions.map(question => {
+        const result = {
+          text: question.text,
+          type: question.type
+        };
+        
+        // Добавляем варианты ответов, если они есть
+        if (question.options && question.options.length > 0) {
+          result.options = question.options;
+        }
+        
+        // Добавляем текстовые ответы, если они есть
+        if (question.answers && question.answers.length > 0) {
+          result.answers = question.answers;
+        }
+        
+        // Добавляем результаты шкалы, если они есть
+        if (question.scaleValues && question.scaleValues.length > 0) {
+          result.scaleValues = question.scaleValues;
+          
+          // Считаем среднее значение
+          result.scaleAverage = question.scaleValues.reduce((a, b) => a + b, 0) / question.scaleValues.length;
+        }
+        
+        // Добавляем теги, если они есть
+        if (question.tags && question.tags.length > 0) {
+          result.tags = question.tags;
+        }
+        
+        return result;
+      })
+    };
+    
+    res.status(200).json({
+      success: true,
+      data: results
+    });
+  } catch (error) {
+    console.error('Error fetching poll results:', error);
+    res.status(500).json({ 
+      success: false, 
+      error: 'Failed to fetch poll results' 
+    });
+  }
+});
+
+module.exports = router
diff --git a/server/routers/questioneer/public/admin.html b/server/routers/questioneer/public/admin.html
new file mode 100644
index 0000000..5928fc2
--- /dev/null
+++ b/server/routers/questioneer/public/admin.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Управление опросом</title>
+  <link rel="stylesheet" href="/questioneer/static/css/style.css">
+</head>
+<body>
+  <!-- Навигационная шапка -->
+  <header class="nav-header">
+    <div class="nav-container">
+      <a href="/questioneer" class="nav-logo">Анонимные опросы</a>
+      <nav class="nav-menu">
+        <a href="/questioneer" class="nav-link">Главная</a>
+        <a href="/questioneer/create" class="nav-link">Создать опрос</a>
+      </nav>
+    </div>
+  </header>
+
+  <div class="container">
+    <h1>Управление опросом</h1>
+    
+    <div id="loading">Загрузка опроса...</div>
+    
+    <div id="questionnaire-container" style="display: none;">
+      <div class="questionnaire-header">
+        <h2 id="questionnaire-title"></h2>
+        <p id="questionnaire-description"></p>
+      </div>
+      
+      <div class="questionnaire-links">
+        <div class="link-group">
+          <h3>Ссылка для голосования:</h3>
+          <div class="link-input-group">
+            <input type="text" id="public-link" readonly>
+            <button class="btn btn-small" id="copy-public-link">Копировать</button>
+            <button class="btn btn-small" id="show-qr-code">QR-код</button>
+          </div>
+        </div>
+        
+        <div class="link-group">
+          <h3>Административная ссылка:</h3>
+          <div class="link-input-group">
+            <input type="text" id="admin-link" readonly>
+            <button class="btn btn-small" id="copy-admin-link">Копировать</button>
+          </div>
+        </div>
+      </div>
+      
+      <div class="questionnaire-stats">
+        <h3>Статистика ответов</h3>
+        <div id="stats-container"></div>
+      </div>
+      
+      <div class="questionnaire-actions">
+        <button id="edit-questionnaire" class="btn">Редактировать опрос</button>
+        <button id="delete-questionnaire" class="btn btn-danger">Удалить опрос</button>
+      </div>
+    </div>
+  </div>
+  
+  <script src="https://code.jquery.com/jquery-3.7.1.min.js" 
+          integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
+          crossorigin="anonymous"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js"></script>
+  <script src="/questioneer/static/js/common.js"></script>
+  <script src="/questioneer/static/js/admin.js"></script>
+</body>
+</html> 
\ No newline at end of file
diff --git a/server/routers/questioneer/public/create.html b/server/routers/questioneer/public/create.html
new file mode 100644
index 0000000..61cddc9
--- /dev/null
+++ b/server/routers/questioneer/public/create.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Создание нового опроса</title>
+  <link rel="stylesheet" href="/questioneer/static/css/style.css">
+</head>
+<body>
+  <div class="container">
+    <h1>Создание нового опроса</h1>
+    
+    <div class="form-container">
+      <form id="create-questionnaire-form">
+        <div class="form-group">
+          <label for="title">Название опроса *</label>
+          <input type="text" id="title" name="title" required>
+        </div>
+        
+        <div class="form-group">
+          <label for="description">Описание опроса</label>
+          <textarea id="description" name="description"></textarea>
+        </div>
+        
+        <div class="form-group" style="display: none;">
+          <label for="display-type">Тип отображения</label>
+          <select id="display-type" name="display-type">
+            <option value="step_by_step">Пошаговый</option>
+          </select>
+        </div>
+        
+        <div class="questions-container">
+          <h2>Вопросы</h2>
+          <div id="questions-list"></div>
+          
+          <button type="button" id="add-question" class="btn btn-small">Добавить вопрос</button>
+        </div>
+        
+        <div class="form-actions">
+          <a href="/questioneer" class="btn btn-secondary">Отмена</a>
+          <button type="submit" class="btn btn-primary">Создать опрос</button>
+        </div>
+      </form>
+    </div>
+  </div>
+  
+  <!-- Шаблон для вопроса -->
+  <template id="question-template">
+    <div class="question-item" data-index="{{index}}">
+      <div class="question-header">
+        <h3>Вопрос {{number}}</h3>
+        <button type="button" class="btn-icon delete-question">
+          <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
+            <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/>
+            <path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/>
+          </svg>
+        </button>
+      </div>
+      
+      <div class="form-group">
+        <label for="question-text-{{index}}">Текст вопроса *</label>
+        <input type="text" id="question-text-{{index}}" class="question-text" name="questions[{{index}}][text]" required>
+      </div>
+      
+      <div class="form-group">
+        <label for="question-type-{{index}}">Тип вопроса *</label>
+        <select id="question-type-{{index}}" class="question-type-select" name="questions[{{index}}][type]" required>
+          <option value="single_choice">Одиночный выбор</option>
+          <option value="multiple_choice">Множественный выбор</option>
+          <option value="text">Текстовый ответ</option>
+          <option value="scale">Шкала оценки</option>
+          <option value="tag_cloud">Облако тегов</option>
+        </select>
+      </div>
+      
+      <div class="form-group">
+        <label>
+          <input type="checkbox" name="questions[{{index}}][required]" value="true">
+          Обязательный вопрос
+        </label>
+      </div>
+      
+      <div class="options-container" id="options-container-{{index}}">
+        <h4>Варианты ответа</h4>
+        <div class="options-list" id="options-list-{{index}}"></div>
+        <button type="button" class="btn btn-small add-option" data-question-index="{{index}}">Добавить вариант</button>
+      </div>
+      
+      <div class="scale-container" id="scale-container-{{index}}" style="display: none;">
+        <h4>Настройки шкалы</h4>
+        <div class="form-group">
+          <label for="scale-min-{{index}}">Минимальное значение</label>
+          <input type="number" id="scale-min-{{index}}" class="scale-min" name="questions[{{index}}][scaleMin]" value="0" min="0">
+        </div>
+        <div class="form-group">
+          <label for="scale-max-{{index}}">Максимальное значение</label>
+          <input type="number" id="scale-max-{{index}}" class="scale-max" name="questions[{{index}}][scaleMax]" value="10" min="1" max="20">
+        </div>
+        <div class="form-group">
+          <label for="scale-min-label-{{index}}">Подпись минимального значения</label>
+          <input type="text" id="scale-min-label-{{index}}" class="scale-min-label" name="questions[{{index}}][scaleMinLabel]" value="Минимум">
+        </div>
+        <div class="form-group">
+          <label for="scale-max-label-{{index}}">Подпись максимального значения</label>
+          <input type="text" id="scale-max-label-{{index}}" class="scale-max-label" name="questions[{{index}}][scaleMaxLabel]" value="Максимум">
+        </div>
+      </div>
+    </div>
+  </template>
+  
+  <!-- Шаблон для варианта ответа -->
+  <template id="option-template">
+    <div class="option-item" data-index="{{optionIndex}}">
+      <input type="text" name="questions[{{questionIndex}}][options][{{optionIndex}}][text]" placeholder="Вариант ответа">
+      <button type="button" class="btn-icon delete-option">
+        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
+          <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
+        </svg>
+      </button>
+    </div>
+  </template>
+  
+  <script src="https://code.jquery.com/jquery-3.7.1.min.js" 
+          integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
+          crossorigin="anonymous"></script>
+  <script src="/questioneer/static/js/common.js"></script>
+  <script src="/questioneer/static/js/create.js"></script>
+</body>
+</html> 
\ No newline at end of file
diff --git a/server/routers/questioneer/public/edit.html b/server/routers/questioneer/public/edit.html
new file mode 100644
index 0000000..a67890f
--- /dev/null
+++ b/server/routers/questioneer/public/edit.html
@@ -0,0 +1,146 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Редактирование опроса</title>
+  <link rel="stylesheet" href="/questioneer/static/css/style.css">
+</head>
+<body>
+  <div class="container">
+    <h1>Редактирование опроса</h1>
+    
+    <div id="loading">Загрузка опроса...</div>
+    
+    <div class="form-container" id="edit-form-container" style="display: none;">
+      <form id="edit-questionnaire-form">
+        <div class="form-group">
+          <label for="title">Название опроса:</label>
+          <input type="text" id="title" name="title" required>
+        </div>
+        
+        <div class="form-group">
+          <label for="description">Описание:</label>
+          <textarea id="description" name="description" rows="3"></textarea>
+        </div>
+        
+        <div class="form-group">
+          <label for="display-type">Тип отображения:</label>
+          <select id="display-type" name="displayType">
+            <option value="default">Обычный</option>
+            <option value="tag_cloud">Облако тегов</option>
+            <option value="voting">Голосование</option>
+            <option value="poll">Опрос</option>
+          </select>
+        </div>
+        
+        <div class="questions-container">
+          <h2>Вопросы</h2>
+          <div id="questions-list"></div>
+          
+          <button type="button" id="add-question" class="btn btn-small">Добавить вопрос</button>
+        </div>
+        
+        <div class="form-actions">
+          <div class="link-group">
+            <h3>Ссылки:</h3>
+            <div class="link-input-group">
+              <div>
+                <label for="public-link">Ссылка для голосования:</label>
+                <input type="text" id="public-link" readonly>
+                <button type="button" class="btn btn-small" id="copy-public-link">Копировать</button>
+                <button type="button" class="btn btn-small" id="show-qr-code">QR-код</button>
+              </div>
+              <div>
+                <label for="admin-link">Административная ссылка:</label>
+                <input type="text" id="admin-link" readonly>
+                <button type="button" class="btn btn-small" id="copy-admin-link">Копировать</button>
+              </div>
+            </div>
+          </div>
+          
+          <div class="form-buttons">
+            <a href="#" id="back-to-admin" class="btn btn-secondary">Вернуться</a>
+            <button type="submit" class="btn btn-primary">Сохранить изменения</button>
+          </div>
+        </div>
+      </form>
+    </div>
+  </div>
+  
+  <!-- Шаблон для вопроса -->
+  <template id="question-template">
+    <div class="question-item" data-index="{{index}}">
+      <div class="question-header">
+        <h3>Вопрос {{number}}</h3>
+        <button type="button" class="btn-icon delete-question">
+          <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
+            <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
+          </svg>
+        </button>
+      </div>
+      
+      <div class="form-group">
+        <label for="question-text-{{index}}">Текст вопроса:</label>
+        <input type="text" id="question-text-{{index}}" name="questions[{{index}}][text]" required>
+      </div>
+      
+      <div class="form-group">
+        <label for="question-type-{{index}}">Тип вопроса:</label>
+        <select id="question-type-{{index}}" name="questions[{{index}}][type]" class="question-type-select">
+          <option value="single_choice">Один вариант</option>
+          <option value="multiple_choice">Несколько вариантов</option>
+          <option value="text">Текстовый ответ</option>
+          <option value="rating">Оценка</option>
+          <option value="scale">Шкала оценки</option>
+          <option value="tag_cloud">Облако тегов</option>
+        </select>
+      </div>
+      
+      <div class="form-group">
+        <label>
+          <input type="checkbox" name="questions[{{index}}][required]" value="true">
+          Обязательный вопрос
+        </label>
+      </div>
+      
+      <div class="options-container" id="options-container-{{index}}">
+        <h4>Варианты ответа</h4>
+        <div class="options-list" id="options-list-{{index}}"></div>
+        <button type="button" class="btn btn-small add-option" data-question-index="{{index}}">Добавить вариант</button>
+      </div>
+      
+      <div class="scale-container" id="scale-container-{{index}}" style="display: none;">
+        <h4>Настройки шкалы</h4>
+        <div class="form-group">
+          <label for="scale-max-{{index}}">Максимальное значение:</label>
+          <select id="scale-max-{{index}}" name="questions[{{index}}][scaleMax]">
+            <option value="5">5</option>
+            <option value="10" selected>10</option>
+            <option value="20">20</option>
+          </select>
+        </div>
+      </div>
+    </div>
+  </template>
+  
+  <!-- Шаблон для варианта ответа -->
+  <template id="option-template">
+    <div class="option-item" data-index="{{optionIndex}}">
+      <input type="text" name="questions[{{questionIndex}}][options][{{optionIndex}}][text]" placeholder="Вариант ответа">
+      <button type="button" class="btn-icon delete-option">
+        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
+          <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
+        </svg>
+      </button>
+    </div>
+  </template>
+  
+  <script src="https://code.jquery.com/jquery-3.7.1.min.js" 
+          integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
+          crossorigin="anonymous"></script>
+  <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js"></script>
+  <script src="/questioneer/static/js/common.js"></script>
+  <script src="/questioneer/static/js/edit.js"></script>
+</body>
+</html> 
\ No newline at end of file
diff --git a/server/routers/questioneer/public/index.html b/server/routers/questioneer/public/index.html
new file mode 100644
index 0000000..ea8b6ef
--- /dev/null
+++ b/server/routers/questioneer/public/index.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Анонимные опросы</title>
+  <link rel="stylesheet" href="/questioneer/static/css/style.css">
+</head>
+<body>
+  <!-- Навигационная шапка -->
+  <header class="nav-header">
+    <div class="nav-container">
+      <a href="/questioneer" class="nav-logo">Анонимные опросы</a>
+      <nav class="nav-menu">
+        <a href="/questioneer" class="nav-link active">Главная</a>
+        <a href="/questioneer/create" class="nav-link">Создать опрос</a>
+      </nav>
+    </div>
+  </header>
+  
+  <div class="container">
+    <h1>Сервис анонимных опросов</h1>
+    
+    <div class="main-buttons">
+      <a href="/questioneer/create" class="btn">Создать новый опрос</a>
+    </div>
+    
+    <div class="questionnaires-list">
+      <h2>Ваши опросы</h2>
+      <div id="questionnaires-container">
+        <p>Загрузка опросов...</p>
+      </div>
+    </div>
+  </div>
+  
+  <script src="https://code.jquery.com/jquery-3.7.1.min.js" 
+          integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
+          crossorigin="anonymous"></script>
+  <script src="/questioneer/static/js/common.js"></script>
+  <script src="/questioneer/static/js/index.js"></script>
+</body>
+</html> 
\ No newline at end of file
diff --git a/server/routers/questioneer/public/poll.html b/server/routers/questioneer/public/poll.html
new file mode 100644
index 0000000..0aa8463
--- /dev/null
+++ b/server/routers/questioneer/public/poll.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Участие в опросе</title>
+  <link rel="stylesheet" href="/questioneer/static/css/style.css">
+</head>
+<body>
+  <div class="container">
+    <div id="loading">Загрузка опроса...</div>
+    
+    <div id="questionnaire-container" style="display: none;">
+      <div class="questionnaire-header">
+        <h1 id="questionnaire-title"></h1>
+        <p id="questionnaire-description"></p>
+      </div>
+      
+      <form id="poll-form">
+        <div id="questions-container"></div>
+        
+        <div class="form-actions">
+          <button type="submit" class="btn btn-primary">Отправить ответы</button>
+        </div>
+      </form>
+      
+      <div id="results-container" style="display: none;">
+        <h2>Спасибо за участие!</h2>
+        <p>Ваши ответы были успешно отправлены.</p>
+        
+        <div class="poll-results">
+          <h3>Текущие результаты:</h3>
+          <div id="poll-results-container"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+  
+  <script src="https://code.jquery.com/jquery-3.7.1.min.js" 
+          integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
+          crossorigin="anonymous"></script>
+  <script src="/questioneer/static/js/common.js"></script>
+  <script src="/questioneer/static/js/poll.js"></script>
+</body>
+</html> 
\ No newline at end of file
diff --git a/server/routers/questioneer/public/static/css/style.css b/server/routers/questioneer/public/static/css/style.css
new file mode 100644
index 0000000..5dc4bcf
--- /dev/null
+++ b/server/routers/questioneer/public/static/css/style.css
@@ -0,0 +1,1830 @@
+/* Темный стиль */
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  margin: 0;
+  padding: 0;
+  color: #e0e0e0;
+  background-color: #1e1e1e;
+  line-height: 1.6;
+}
+
+.container {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+h1, h2, h3, h4, h5, h6 {
+  margin-top: 0;
+  margin-bottom: 0.5rem;
+  font-weight: 500;
+  line-height: 1.2;
+}
+
+h1 {
+  font-size: 2.5rem;
+  margin-bottom: 1.5rem;
+  color: #61dafb;
+}
+
+h2 {
+  font-size: 2rem;
+  color: #64b5f6;
+}
+
+h3 {
+  font-size: 1.5rem;
+  color: #81c784;
+}
+
+p {
+  margin-top: 0;
+  margin-bottom: 1rem;
+}
+
+/* Кнопки */
+.btn {
+  display: inline-block;
+  font-weight: 400;
+  text-align: center;
+  vertical-align: middle;
+  user-select: none;
+  padding: 0.5rem 1rem;
+  font-size: 1rem;
+  line-height: 1.5;
+  border-radius: 0.25rem;
+  text-decoration: none;
+  cursor: pointer;
+  background-color: #2196f3;
+  color: white;
+  border: 1px solid transparent;
+  transition: all 0.15s ease-in-out;
+}
+
+.btn:hover {
+  background-color: #1976d2;
+}
+
+.btn-primary {
+  background-color: #4caf50;
+}
+
+.btn-primary:hover {
+  background-color: #388e3c;
+}
+
+.btn-secondary {
+  background-color: #757575;
+}
+
+.btn-secondary:hover {
+  background-color: #616161;
+}
+
+.btn-danger {
+  background-color: #f44336;
+}
+
+.btn-danger:hover {
+  background-color: #d32f2f;
+}
+
+.btn-small {
+  padding: 0.25rem 0.5rem;
+  font-size: 0.875rem;
+}
+
+.btn-icon {
+  background: none;
+  border: none;
+  cursor: pointer;
+  font-size: 1rem;
+  padding: 0;
+  margin: 0 5px;
+  color: #e0e0e0;
+}
+
+/* Формы */
+.form-container {
+  background-color: #2d2d2d;
+  padding: 20px;
+  border-radius: 5px;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+  margin-bottom: 20px;
+}
+
+.form-group {
+  margin-bottom: 15px;
+}
+
+.form-group label {
+  display: block;
+  margin-bottom: 5px;
+  font-weight: 500;
+  color: #bbdefb;
+}
+
+input[type="text"],
+input[type="email"],
+input[type="password"],
+input[type="number"],
+textarea,
+select {
+  display: block;
+  width: 100%;
+  padding: 0.5rem 0.75rem;
+  font-size: 1rem;
+  line-height: 1.5;
+  color: #e0e0e0;
+  background-color: #424242;
+  background-clip: padding-box;
+  border: 1px solid #616161;
+  border-radius: 0.25rem;
+  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+  box-sizing: border-box;
+}
+
+input:focus,
+textarea:focus,
+select:focus {
+  color: #e0e0e0;
+  background-color: #424242;
+  border-color: #64b5f6;
+  outline: 0;
+  box-shadow: 0 0 0 0.2rem rgba(33, 150, 243, 0.25);
+}
+
+.form-actions {
+  margin-top: 20px;
+  display: flex;
+  justify-content: space-between;
+}
+
+/* Список опросов */
+.questionnaires-list {
+  margin-top: 30px;
+}
+
+.questionnaire-item {
+  background-color: #2d2d2d;
+  padding: 20px;
+  border-radius: 5px;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+  margin-bottom: 20px;
+}
+
+.questionnaire-links {
+  display: flex;
+  gap: 10px;
+  margin-top: 15px;
+}
+
+/* Вопросы */
+.question-item {
+  background-color: #2d2d2d;
+  padding: 20px;
+  border-radius: 5px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+  margin-bottom: 20px;
+}
+
+.question-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+}
+
+.options-container {
+  margin-top: 10px;
+}
+
+.options-list {
+  margin-bottom: 10px;
+}
+
+.option-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+/* Стили для варианта ответа */
+.option-item input[type="text"] {
+  flex: 1;
+  margin-right: 10px;
+}
+
+.radio-option label,
+.checkbox-option label {
+  margin-left: 10px;
+  cursor: pointer;
+}
+
+/* Звездный рейтинг */
+.rating-container {
+  display: flex;
+  gap: 10px;
+  margin-top: 10px;
+}
+
+.rating-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.rating-item label {
+  cursor: pointer;
+  padding: 8px 12px;
+  background-color: #424242;
+  border-radius: 4px;
+  transition: all 0.2s;
+}
+
+.rating-item input {
+  display: none;
+}
+
+.rating-item input:checked + label {
+  background-color: #2196f3;
+  color: white;
+}
+
+/* Облако тегов */
+.tag-cloud-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  margin-top: 10px;
+}
+
+.tag-item {
+  padding: 8px 15px;
+  background-color: #424242;
+  border-radius: 20px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.tag-item:hover {
+  background-color: #616161;
+}
+
+.tag-item.selected {
+  background-color: #2196f3;
+  color: white;
+}
+
+/* Результаты голосования */
+.results-visualization {
+  margin-top: 15px;
+}
+
+.result-bar-container {
+  margin-bottom: 15px;
+}
+
+.result-label {
+  margin-bottom: 5px;
+  font-weight: 500;
+  color: #bbdefb;
+}
+
+.result-bar {
+  height: 20px;
+  background-color: #424242;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.result-bar-fill {
+  height: 100%;
+  background-color: #2196f3;
+  border-radius: 4px;
+  transition: width 0.5s;
+}
+
+.result-percent {
+  margin-top: 5px;
+  font-size: 0.875rem;
+  color: #9e9e9e;
+}
+
+.results-tag-cloud {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 15px;
+  margin-top: 15px;
+  justify-content: center;
+}
+
+.result-tag {
+  padding: 5px 10px;
+  background-color: #2196f3;
+  color: white;
+  border-radius: 20px;
+  display: inline-block;
+}
+
+/* Обязательные поля */
+.required-mark {
+  color: #f44336;
+}
+
+/* Состояния загрузки и ошибки */
+#loading {
+  text-align: center;
+  padding: 20px;
+  font-size: 1.2rem;
+  color: #9e9e9e;
+}
+
+.error {
+  color: #f44336;
+  padding: 10px;
+  background-color: rgba(244, 67, 54, 0.2);
+  border-radius: 4px;
+}
+
+/* Ссылки */
+.link-group {
+  margin-bottom: 15px;
+}
+
+.link-input-group {
+  display: flex;
+  gap: 10px;
+}
+
+.link-input-group input {
+  flex: 1;
+}
+
+/* Текстовая область для ввода ответа */
+.textarea-container {
+  width: 100%;
+  margin-top: 10px;
+}
+
+.text-answer {
+  width: 100%;
+  min-height: 100px;
+  resize: vertical;
+}
+
+/* Таблица статистики */
+.stats-table {
+  width: 100%;
+  border-collapse: collapse;
+  margin-top: 10px;
+  color: #e0e0e0;
+}
+
+.stats-table th, 
+.stats-table td {
+  padding: 8px;
+  text-align: left;
+  border-bottom: 1px solid #424242;
+}
+
+.stats-table th {
+  background-color: #383838;
+  font-weight: 500;
+  color: #bbdefb;
+}
+
+.stats-table .total-row {
+  font-weight: bold;
+  background-color: #424242;
+}
+
+/* Стили для question-stats */
+.question-stats {
+  background-color: #2d2d2d;
+  padding: 15px;
+  border-radius: 5px;
+  margin-bottom: 20px;
+  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
+}
+
+/* Модальные окна */
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.7);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+  opacity: 0;
+  visibility: hidden;
+  transition: opacity 0.3s, visibility 0.3s;
+}
+
+.modal-overlay.active {
+  opacity: 1;
+  visibility: visible;
+}
+
+.modal {
+  background-color: #2d2d2d;
+  border-radius: 5px;
+  box-shadow: 0 3px 15px rgba(0, 0, 0, 0.3);
+  width: 90%;
+  max-width: 500px;
+  padding: 20px;
+  position: relative;
+  transform: translateY(-20px);
+  transition: transform 0.3s;
+  max-height: 90vh;
+  overflow-y: auto;
+}
+
+.modal-overlay.active .modal {
+  transform: translateY(0);
+}
+
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+  padding-bottom: 10px;
+  border-bottom: 1px solid #444;
+}
+
+.modal-header h3 {
+  margin: 0;
+  color: #64b5f6;
+}
+
+.modal-close {
+  background: none;
+  border: none;
+  color: #e0e0e0;
+  font-size: 1.5rem;
+  cursor: pointer;
+  padding: 0;
+  line-height: 1;
+}
+
+.modal-body {
+  margin-bottom: 20px;
+}
+
+.modal-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+  padding-top: 10px;
+  border-top: 1px solid #444;
+}
+
+/* QR код */
+.qr-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 15px;
+}
+
+.qr-code {
+  margin-bottom: 15px;
+  background-color: #fff;
+  padding: 15px;
+  border-radius: 8px;
+}
+
+.qr-link-container {
+  display: flex;
+  width: 100%;
+  max-width: 500px;
+  margin-top: 10px;
+}
+
+.qr-link-input {
+  flex-grow: 1;
+  padding: 10px;
+  border: 1px solid #444;
+  border-radius: 4px 0 0 4px;
+  background-color: #333;
+  color: #fff;
+}
+
+.btn-copy-link {
+  padding: 10px 15px;
+  background-color: #64b5f6;
+  color: #fff;
+  border: none;
+  border-radius: 0 4px 4px 0;
+  cursor: pointer;
+  transition: background-color 0.3s;
+}
+
+.btn-copy-link:hover {
+  background-color: #90caf9;
+}
+
+.btn-copy-link.copied {
+  background-color: #4caf50;
+}
+
+/* Шкала оценки */
+.scale-container {
+  margin-top: 20px;
+  width: 100%;
+}
+
+.scale-labels {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 10px;
+}
+
+.scale-label-min,
+.scale-label-max {
+  font-weight: 500;
+  color: #bbdefb;
+}
+
+.scale-values {
+  display: flex;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  gap: 5px;
+}
+
+.scale-item {
+  text-align: center;
+}
+
+.scale-item input {
+  display: none;
+}
+
+.scale-item label {
+  display: block;
+  width: 40px;
+  height: 40px;
+  line-height: 40px;
+  text-align: center;
+  background-color: #424242;
+  border-radius: 50%;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.scale-item input:checked + label {
+  background-color: #2196f3;
+  color: white;
+}
+
+.scale-item label:hover {
+  background-color: #616161;
+}
+
+/* Пошаговый опрос */
+.step-navigation {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 20px;
+  padding-top: 15px;
+  border-top: 1px solid #444;
+}
+
+#question-counter {
+  font-size: 1rem;
+  color: #bbdefb;
+}
+
+/* Улучшения для облака тегов */
+.tag-input-container {
+  width: 100%;
+}
+
+.tag-input {
+  width: 100%;
+  padding: 10px;
+  margin-bottom: 10px;
+  background-color: #424242;
+  border: 1px solid #616161;
+  border-radius: 4px;
+  color: #e0e0e0;
+}
+
+.tag-items {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  margin-top: 10px;
+}
+
+.tag-item {
+  position: relative;
+  padding: 8px 15px;
+  background-color: #424242;
+  border-radius: 20px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.tag-item.selected {
+  background-color: #2196f3;
+  color: white;
+}
+
+.tag-remove {
+  margin-left: 5px;
+  font-size: 1.2rem;
+  cursor: pointer;
+}
+
+/* Стили для результатов опроса */
+.question-result {
+  background-color: #2d2d2d;
+  padding: 15px;
+  border-radius: 5px;
+  margin-bottom: 20px;
+  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
+}
+
+.text-answers {
+  margin-top: 10px;
+}
+
+.text-answer {
+  background-color: #424242;
+  padding: 10px;
+  border-radius: 4px;
+  margin-bottom: 10px;
+}
+
+/* Улучшения для радио и чекбоксов */
+.radio-options-container,
+.checkbox-options-container {
+  margin-top: 10px;
+}
+
+.radio-option,
+.checkbox-option {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.radio-option input,
+.checkbox-option input {
+  margin-right: 10px;
+}
+
+.radio-option label,
+.checkbox-option label {
+  cursor: pointer;
+}
+
+/* Стили для вопросов с ошибками */
+.question-item.error {
+  border-left: 4px solid var(--color-error);
+  box-shadow: 0 0 10px rgba(220, 53, 69, 0.3);
+  padding-left: 16px;
+  transition: all 0.3s ease;
+}
+
+.question-item.error .question-title {
+  color: var(--color-error);
+}
+
+/* Анимация для ошибок */
+.shake {
+  animation: shake 0.6s cubic-bezier(.36,.07,.19,.97) both;
+}
+
+@keyframes shake {
+  10%, 90% {
+    transform: translate3d(-1px, 0, 0);
+  }
+  
+  20%, 80% {
+    transform: translate3d(2px, 0, 0);
+  }
+  
+  30%, 50%, 70% {
+    transform: translate3d(-4px, 0, 0);
+  }
+  
+  40%, 60% {
+    transform: translate3d(4px, 0, 0);
+  }
+}
+
+/* Стили для выхода вопроса */
+.question-item.exit {
+  opacity: 0;
+  transform: translateX(-30px);
+  transition: opacity 0.3s ease, transform 0.3s ease;
+  pointer-events: none;
+}
+
+/* Стили для плавного входа вопроса */
+.question-item.enter {
+  opacity: 0;
+  transform: translateX(30px);
+}
+
+.question-item.active {
+  opacity: 1;
+  transform: translateX(0);
+  transition: opacity 0.5s ease, transform 0.5s ease;
+}
+
+/* Анимации для элементов результатов */
+.results-title {
+  animation: slideDown 0.8s ease forwards;
+}
+
+.scale-average {
+  margin-bottom: 15px;
+  font-size: 1.2em;
+}
+
+.scale-average .highlight {
+  color: var(--color-primary);
+  font-size: 1.5em;
+  animation: pulse 2s infinite;
+}
+
+.result-bar-container {
+  margin-bottom: 10px;
+}
+
+.result-label {
+  margin-bottom: 5px;
+  font-weight: 500;
+}
+
+.result-bar {
+  height: 20px;
+  background-color: rgba(0, 0, 0, 0.1);
+  border-radius: 4px;
+  overflow: hidden;
+  position: relative;
+}
+
+.result-bar-fill {
+  height: 100%;
+  background-color: var(--color-primary);
+  transition: width 1s ease-out;
+  border-radius: 4px;
+}
+
+.result-percent {
+  margin-top: 3px;
+  text-align: right;
+  font-size: 0.9em;
+  color: var(--color-muted);
+}
+
+.text-answer {
+  background-color: rgba(0, 0, 0, 0.05);
+  padding: 10px 15px;
+  border-radius: 4px;
+  margin-bottom: 10px;
+  transition: opacity 0.5s ease, transform 0.5s ease;
+}
+
+.results-tag-cloud {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: center;
+  margin: 20px 0;
+}
+
+.result-tag {
+  background-color: rgba(var(--primary-rgb), 0.1);
+  color: var(--color-primary);
+  padding: 8px 15px;
+  border-radius: 20px;
+  margin: 5px;
+  display: inline-block;
+  transition: all 0.5s ease;
+}
+
+@keyframes slideDown {
+  from { 
+    opacity: 0; 
+    transform: translateY(-20px);
+  }
+  to { 
+    opacity: 1; 
+    transform: translateY(0);
+  }
+}
+
+@keyframes slideIn {
+  from { 
+    opacity: 0; 
+    transform: translateX(30px);
+  }
+  to { 
+    opacity: 1; 
+    transform: translateX(0);
+  }
+}
+
+@keyframes pulse {
+  0% {
+    transform: scale(1);
+  }
+  50% {
+    transform: scale(1.05);
+  }
+  100% {
+    transform: scale(1);
+  }
+}
+
+.question-result {
+  transition: all 0.5s ease;
+  padding: 15px;
+  margin-bottom: 20px;
+  border-radius: 8px;
+  background-color: rgba(0, 0, 0, 0.03);
+}
+
+/* Улучшенная анимация для счетчика вопросов */
+.question-counter {
+  transition: all 0.3s ease;
+  animation: fadeIn 0.5s;
+}
+
+.question-counter.update {
+  animation: pulse 0.5s;
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+/* Анимации для кнопок навигации */
+.nav-btn:hover {
+  transform: translateY(-3px);
+  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+  transition: all 0.3s ease;
+}
+
+.nav-btn:active {
+  transform: translateY(-1px);
+  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+}
+
+/* Улучшенные стили для загрузки */
+#loading {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  margin: 50px 0;
+}
+
+.loading-spinner {
+  width: 50px;
+  height: 50px;
+  border: 5px solid rgba(0, 0, 0, 0.1);
+  border-radius: 50%;
+  border-top: 5px solid var(--color-primary);
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+.loading-text {
+  margin-top: 15px;
+  animation: pulse 1.5s infinite;
+}
+
+/* Анимации */
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+@keyframes slideInUp {
+  from {
+    transform: translateY(20px);
+    opacity: 0;
+  }
+  to {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+@keyframes slideInRight {
+  from {
+    transform: translateX(20px);
+    opacity: 0;
+  }
+  to {
+    transform: translateX(0);
+    opacity: 1;
+  }
+}
+
+@keyframes pulse {
+  0% { transform: scale(1); }
+  50% { transform: scale(1.05); }
+  100% { transform: scale(1); }
+}
+
+@keyframes shake {
+  0%, 100% { transform: translateX(0); }
+  10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
+  20%, 40%, 60%, 80% { transform: translateX(5px); }
+}
+
+@keyframes rotate {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+/* Применение анимаций */
+#questionnaire-container {
+  animation: fadeIn 0.6s ease-out;
+}
+
+.question-item {
+  animation: slideInUp 0.5s ease-out;
+  transition: all 0.3s ease;
+}
+
+.btn {
+  transition: all 0.2s ease-in-out;
+}
+
+.btn:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+}
+
+.btn:active {
+  transform: translateY(1px);
+}
+
+.btn-primary {
+  animation: pulse 2s infinite;
+}
+
+.question-item.error {
+  animation: shake 0.5s ease-in-out;
+}
+
+.radio-option label:hover,
+.checkbox-option label:hover {
+  transform: translateX(3px);
+  transition: transform 0.2s ease;
+}
+
+.tag-item {
+  transition: all 0.2s ease;
+}
+
+.tag-item:hover {
+  transform: scale(1.05);
+}
+
+.tag-item.selected {
+  animation: pulse 1s;
+}
+
+.scale-item label:hover {
+  transform: scale(1.1);
+  transition: transform 0.2s ease;
+}
+
+.scale-item input:checked + label {
+  animation: pulse 0.5s;
+}
+
+/* Анимированный лоадер */
+#loading {
+  position: relative;
+  padding-left: 30px;
+}
+
+#loading:before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 50%;
+  margin-top: -10px;
+  width: 20px;
+  height: 20px;
+  border: 3px solid #2196f3;
+  border-top-color: transparent;
+  border-radius: 50%;
+  animation: rotate 1s linear infinite;
+}
+
+/* Анимированные переходы между вопросами */
+.step-navigation button {
+  transition: all 0.3s ease;
+}
+
+.question-item.active {
+  animation: slideInRight 0.4s ease-out;
+}
+
+.question-item.exit {
+  animation: fadeIn 0.4s ease-out reverse;
+}
+
+/* Анимация для результатов */
+.result-bar-fill {
+  transition: width 1.5s ease-out;
+  animation: slideInRight 1.5s ease-out;
+}
+
+.results-container {
+  animation: fadeIn 1s ease-out;
+}
+
+.question-result {
+  animation: slideInUp 0.5s ease-out;
+  animation-fill-mode: both;
+}
+
+.question-result:nth-child(1) { animation-delay: 0.1s; }
+.question-result:nth-child(2) { animation-delay: 0.2s; }
+.question-result:nth-child(3) { animation-delay: 0.3s; }
+.question-result:nth-child(4) { animation-delay: 0.4s; }
+.question-result:nth-child(5) { animation-delay: 0.5s; }
+.question-result:nth-child(6) { animation-delay: 0.6s; }
+.question-result:nth-child(7) { animation-delay: 0.7s; }
+.question-result:nth-child(8) { animation-delay: 0.8s; }
+.question-result:nth-child(9) { animation-delay: 0.9s; }
+.question-result:nth-child(10) { animation-delay: 1s; }
+
+/* Другие улучшения стилей */
+.textarea-container textarea {
+  transition: height 0.3s ease;
+}
+
+.textarea-container textarea:focus {
+  height: 120px;
+}
+
+/* Анимация для модальных окон */
+.modal-overlay.active .modal {
+  animation: slideInUp 0.3s ease-out;
+}
+
+/* Анимация для кнопки добавления вопроса */
+#add-question {
+  transition: background-color 0.3s ease, transform 0.2s ease;
+}
+
+#add-question:hover {
+  transform: translateY(-2px);
+}
+
+#add-question:active {
+  transform: translateY(1px);
+}
+
+/* Анимация иконок */
+.btn-icon svg {
+  transition: transform 0.3s ease;
+}
+
+.btn-icon:hover svg {
+  transform: rotate(90deg);
+}
+
+/* Анимация для обратной связи */
+@keyframes success-animation {
+  0% { background-color: transparent; }
+  30% { background-color: rgba(76, 175, 80, 0.2); }
+  100% { background-color: transparent; }
+}
+
+.success-feedback {
+  animation: success-animation 1.5s ease;
+}
+
+/* Анимированные переключатели */
+input[type="checkbox"], input[type="radio"] {
+  transition: all 0.2s ease;
+}
+
+/* Дополнительные плавные переходы для всех элементов */
+* {
+  transition-property: background-color, border-color, color, box-shadow;
+  transition-duration: 0.2s;
+  transition-timing-function: ease;
+}
+
+/* Стили для приветствия и благодарности */
+.welcome-animation,
+.thank-you-animation,
+.already-completed {
+  max-width: 800px;
+  margin: 50px auto;
+  padding: 30px;
+  text-align: center;
+  background-color: #2d2d2d;
+  border-radius: 8px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+  transition: all 0.5s ease;
+}
+
+.welcome-icon,
+.thank-you-icon,
+.completed-icon {
+  margin-bottom: 20px;
+}
+
+.welcome-icon svg,
+.thank-you-icon svg,
+.completed-icon svg {
+  width: 80px;
+  height: 80px;
+  color: var(--color-primary, #2196f3);
+  opacity: 0.8;
+}
+
+.welcome-title,
+.thank-you-title,
+.completed-title {
+  font-size: 2rem;
+  margin-bottom: 15px;
+  color: var(--color-primary, #2196f3);
+}
+
+.welcome-description,
+.thank-you-description,
+.completed-description {
+  font-size: 1.1rem;
+  line-height: 1.6;
+  margin-bottom: 30px;
+  color: #e0e0e0;
+}
+
+.welcome-start-btn,
+.view-results-btn {
+  margin-top: 20px;
+  font-size: 1.1rem;
+  padding: 12px 30px;
+  border-radius: 30px;
+  animation: pulse 2s infinite;
+}
+
+.start-again-btn {
+  margin-top: 20px;
+  margin-left: 10px;
+  font-size: 1.1rem;
+  padding: 12px 30px;
+  border-radius: 30px;
+}
+
+/* Анимации для приветствия и благодарности */
+@keyframes fadeInUp {
+  from {
+    opacity: 0;
+    transform: translateY(30px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.welcome-icon svg,
+.thank-you-icon svg,
+.completed-icon svg {
+  animation: pulse 3s infinite;
+}
+
+/* CSS переменные для цветов */
+:root {
+  --color-primary: #2196f3;
+  --color-primary-dark: #1976d2;
+  --color-primary-light: #64b5f6;
+  --color-secondary: #757575;
+  --color-secondary-dark: #616161;
+  --color-success: #4caf50;
+  --color-success-dark: #388e3c;
+  --color-error: #f44336;
+  --color-error-dark: #d32f2f;
+  --color-muted: #9e9e9e;
+  --primary-rgb: 33, 150, 243;
+  --color-bg-dark: #1e1e1e;
+  --color-bg-card: #2d2d2d;
+  --color-bg-input: #424242;
+}
+
+/* Стили для навигационных кнопок */
+.nav-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 10px 20px;
+  border-radius: 30px;
+  transition: all 0.3s ease;
+}
+
+.nav-btn svg {
+  margin: 0 5px;
+}
+
+.nav-btn:hover {
+  transform: translateY(-3px);
+  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
+}
+
+/* Стили для статистики в админке */
+.question-stats {
+  margin-bottom: 30px;
+  padding: 20px;
+  background-color: #2d2d2d;
+  border-radius: 8px;
+  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
+}
+
+.question-stats h3 {
+  margin-top: 0;
+  margin-bottom: 15px;
+  color: #90caf9;
+  border-bottom: 1px solid #444;
+  padding-bottom: 10px;
+}
+
+.no-stats, .no-votes {
+  padding: 15px;
+  background-color: #383838;
+  border-radius: 5px;
+  text-align: center;
+  color: #aaa;
+}
+
+.stats-table {
+  width: 100%;
+  border-collapse: collapse;
+  margin-bottom: 15px;
+}
+
+.stats-table th, .stats-table td {
+  padding: 10px;
+  text-align: left;
+  border-bottom: 1px solid #444;
+}
+
+.stats-table th {
+  background-color: #383838;
+  color: #90caf9;
+}
+
+.bar-container {
+  width: 100%;
+  height: 20px;
+  background-color: #383838;
+  border-radius: 3px;
+  overflow: hidden;
+}
+
+.bar {
+  height: 100%;
+  background-color: #64b5f6;
+  border-radius: 3px;
+  transition: width 0.5s ease-in-out;
+}
+
+.total-votes {
+  text-align: right;
+  font-style: italic;
+  color: #aaa;
+  margin-top: 10px;
+}
+
+/* Стили для облака тегов в статистике */
+.tag-cloud-stats {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  padding: 15px;
+  background-color: #383838;
+  border-radius: 5px;
+}
+
+.tag-item {
+  display: inline-block;
+  padding: 5px 10px;
+  background-color: #444;
+  border-radius: 15px;
+  margin-right: 8px;
+  margin-bottom: 8px;
+  color: #90caf9;
+  transition: transform 0.2s ease;
+}
+
+.tag-item:hover {
+  transform: scale(1.05);
+}
+
+/* Стили для шкалы и рейтинга */
+.scale-stats {
+  padding: 15px;
+  background-color: #383838;
+  border-radius: 5px;
+}
+
+.stat-item {
+  margin-bottom: 10px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.stat-label {
+  font-weight: bold;
+  color: #aaa;
+}
+
+.stat-value {
+  font-size: 1.1em;
+  color: #90caf9;
+}
+
+/* Навигация */
+.nav-header {
+  background-color: #212121;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+  padding: 15px 0;
+  margin-bottom: 30px;
+}
+
+.nav-container {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 0 20px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.nav-logo {
+  font-size: 1.3rem;
+  font-weight: 600;
+  color: #61dafb;
+  text-decoration: none;
+}
+
+.nav-menu {
+  display: flex;
+  gap: 20px;
+}
+
+.nav-link {
+  color: #e0e0e0;
+  text-decoration: none;
+  padding: 8px 12px;
+  border-radius: 4px;
+  transition: background-color 0.2s;
+}
+
+.nav-link:hover {
+  background-color: #424242;
+  color: #61dafb;
+}
+
+.nav-link.active {
+  background-color: #424242;
+  color: #61dafb;
+}
+
+/* Формы - улучшенные чекбоксы и радиокнопки */
+.form-container {
+  background-color: #2d2d2d;
+  padding: 20px;
+  border-radius: 5px;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+  margin-bottom: 20px;
+}
+
+.form-group {
+  margin-bottom: 15px;
+}
+
+.form-group label {
+  display: block;
+  margin-bottom: 5px;
+  font-weight: 500;
+  color: #bbdefb;
+}
+
+/* Улучшенные стили для чекбоксов и радиокнопок */
+.radio-option,
+.checkbox-option {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12px;
+  position: relative;
+}
+
+.radio-option input[type="radio"],
+.checkbox-option input[type="checkbox"] {
+  position: absolute;
+  opacity: 0;
+  cursor: pointer;
+  height: 0;
+  width: 0;
+}
+
+.radio-option label,
+.checkbox-option label {
+  position: relative;
+  padding-left: 40px;
+  cursor: pointer;
+  display: block;
+  font-size: 1rem;
+  user-select: none;
+}
+
+.radio-option label:before,
+.checkbox-option label:before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 24px;
+  height: 24px;
+  border: 2px solid #616161;
+  background-color: #424242;
+  transition: all 0.3s;
+}
+
+.radio-option label:before {
+  border-radius: 50%;
+}
+
+.checkbox-option label:before {
+  border-radius: 4px;
+}
+
+.radio-option input[type="radio"]:checked ~ label:before {
+  background-color: #2196f3;
+  border-color: #2196f3;
+}
+
+.checkbox-option input[type="checkbox"]:checked ~ label:before {
+  background-color: #4caf50;
+  border-color: #4caf50;
+}
+
+.radio-option label:after {
+  content: '';
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  background: white;
+  border-radius: 50%;
+  top: 7px;
+  left: 7px;
+  transition: all 0.2s;
+  opacity: 0;
+  transform: scale(0);
+}
+
+.checkbox-option label:after {
+  content: '';
+  position: absolute;
+  left: 9px;
+  top: 5px;
+  width: 6px;
+  height: 12px;
+  border: solid white;
+  border-width: 0 2px 2px 0;
+  transform: rotate(45deg) scale(0);
+  opacity: 0;
+  transition: all 0.2s;
+}
+
+.radio-option input[type="radio"]:checked ~ label:after {
+  opacity: 1;
+  transform: scale(1);
+}
+
+.checkbox-option input[type="checkbox"]:checked ~ label:after {
+  opacity: 1;
+  transform: rotate(45deg) scale(1);
+}
+
+.radio-option:hover label:before,
+.checkbox-option:hover label:before {
+  border-color: #90caf9;
+}
+
+/* Модальные окна с прогресс-баром */
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.7);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+  opacity: 0;
+  visibility: hidden;
+  transition: opacity 0.3s, visibility 0.3s;
+}
+
+.modal-overlay.active {
+  opacity: 1;
+  visibility: visible;
+}
+
+.modal {
+  background-color: #2d2d2d;
+  border-radius: 5px;
+  box-shadow: 0 3px 15px rgba(0, 0, 0, 0.3);
+  width: 90%;
+  max-width: 500px;
+  padding: 20px;
+  position: relative;
+  transform: translateY(-20px);
+  transition: transform 0.3s;
+  max-height: 90vh;
+  overflow-y: auto;
+}
+
+.modal-overlay.active .modal {
+  transform: translateY(0);
+}
+
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 15px;
+  padding-bottom: 10px;
+  border-bottom: 1px solid #444;
+}
+
+.modal-header h3 {
+  margin: 0;
+  color: #64b5f6;
+}
+
+.modal-close {
+  background: none;
+  border: none;
+  color: #e0e0e0;
+  font-size: 1.5rem;
+  cursor: pointer;
+  padding: 0;
+  line-height: 1;
+}
+
+.modal-body {
+  margin-bottom: 20px;
+}
+
+.modal-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+  padding-top: 10px;
+  border-top: 1px solid #444;
+}
+
+/* Прогресс-бар для модальных окон */
+.modal-progress {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  height: 5px;
+  background-color: #2196f3;
+  width: 0;
+  transition: width 2s linear;
+}
+
+.modal-progress.active {
+  width: 100%;
+}
+
+/* Анимации для опроса */
+.question-item {
+  opacity: 1;
+  transform: translateY(0);
+  transition: opacity 0.5s, transform 0.5s;
+}
+
+.question-item.enter {
+  opacity: 0;
+  transform: translateY(20px);
+}
+
+.question-item.active {
+  opacity: 1;
+  transform: translateY(0);
+}
+
+#question-counter {
+  transition: opacity 0.3s, transform 0.3s;
+}
+
+#question-counter.update {
+  opacity: 0;
+  transform: translateY(-10px);
+}
+
+/* Анимации для благодарности и приветствия */
+.welcome-animation,
+.thank-you-animation,
+.already-completed {
+  background-color: #2d2d2d;
+  padding: 30px;
+  border-radius: 8px;
+  text-align: center;
+  margin: 50px auto;
+  max-width: 600px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+  transition: opacity 0.5s, transform 0.5s;
+}
+
+.welcome-icon,
+.thank-you-icon,
+.completed-icon {
+  margin-bottom: 20px;
+  color: #4caf50;
+}
+
+.welcome-title,
+.thank-you-title,
+.completed-title {
+  color: #64b5f6;
+  margin-bottom: 15px;
+}
+
+.welcome-description,
+.thank-you-description,
+.completed-description {
+  color: #e0e0e0;
+  margin-bottom: 25px;
+}
+
+.welcome-start-btn,
+.view-results-btn {
+  margin: 10px;
+}
+
+/* Анимации для ошибок */
+.shake-animation {
+  animation: shake 0.5s;
+}
+
+@keyframes shake {
+  0%, 100% { transform: translateX(0); }
+  10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
+  20%, 40%, 60%, 80% { transform: translateX(5px); }
+}
+
+/* Улучшенные стили для чекбоксов и радиокнопок */
+.radio-option,
+.checkbox-option {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12px;
+  position: relative;
+}
+
+.radio-option input[type="radio"],
+.checkbox-option input[type="checkbox"] {
+  position: absolute;
+  opacity: 0;
+  cursor: pointer;
+  height: 0;
+  width: 0;
+}
+
+.radio-option label,
+.checkbox-option label {
+  position: relative;
+  padding-left: 40px;
+  cursor: pointer;
+  display: block;
+  font-size: 1rem;
+  user-select: none;
+}
+
+.radio-option label:before,
+.checkbox-option label:before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 24px;
+  height: 24px;
+  border: 2px solid #616161;
+  background-color: #424242;
+  transition: all 0.3s;
+}
+
+.radio-option label:before {
+  border-radius: 50%;
+}
+
+.checkbox-option label:before {
+  border-radius: 4px;
+}
+
+.radio-option input[type="radio"]:checked ~ label:before {
+  background-color: #2196f3;
+  border-color: #2196f3;
+}
+
+.checkbox-option input[type="checkbox"]:checked ~ label:before {
+  background-color: #4caf50;
+  border-color: #4caf50;
+}
+
+.radio-option label:after {
+  content: '';
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  background: white;
+  border-radius: 50%;
+  top: 7px;
+  left: 7px;
+  transition: all 0.2s;
+  opacity: 0;
+  transform: scale(0);
+}
+
+.checkbox-option label:after {
+  content: '';
+  position: absolute;
+  left: 9px;
+  top: 5px;
+  width: 6px;
+  height: 12px;
+  border: solid white;
+  border-width: 0 2px 2px 0;
+  transform: rotate(45deg) scale(0);
+  opacity: 0;
+  transition: all 0.2s;
+}
+
+.radio-option input[type="radio"]:checked ~ label:after {
+  opacity: 1;
+  transform: scale(1);
+}
+
+.checkbox-option input[type="checkbox"]:checked ~ label:after {
+  opacity: 1;
+  transform: rotate(45deg) scale(1);
+}
+
+.radio-option:hover label:before,
+.checkbox-option:hover label:before {
+  border-color: #90caf9;
+}
+
+/* Стили для текстовых ответов в админке */
+.text-answers-list {
+  margin-top: 15px;
+}
+
+.text-answer-item {
+  background-color: #383838;
+  padding: 12px 15px;
+  border-radius: 5px;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: flex-start;
+}
+
+.answer-number {
+  font-weight: bold;
+  color: #64b5f6;
+  margin-right: 10px;
+  flex-shrink: 0;
+}
+
+.answer-text {
+  flex-grow: 1;
+  white-space: pre-wrap;
+  word-break: break-word;
+} 
\ No newline at end of file
diff --git a/server/routers/questioneer/public/static/js/admin.js b/server/routers/questioneer/public/static/js/admin.js
new file mode 100644
index 0000000..2adf316
--- /dev/null
+++ b/server/routers/questioneer/public/static/js/admin.js
@@ -0,0 +1,294 @@
+/* global $, window, document, showAlert, showConfirm, showQRCodeModal */
+$(document).ready(function() {
+  const adminLink = window.location.pathname.split('/').pop();
+  let questionnaireData = null;
+
+  // Получаем базовый путь API (для работы и с /questioneer, и с /ms/questioneer)
+  const getApiPath = () => {
+    const pathParts = window.location.pathname.split('/');
+    // Убираем последние две части пути (admin/:adminLink)
+    pathParts.pop();
+    pathParts.pop();
+    return pathParts.join('/') + '/api';
+  };
+
+  // Загрузка данных опроса
+  const loadQuestionnaire = () => {
+    $.ajax({
+      url: `${getApiPath()}/questionnaires/admin/${adminLink}`,
+      method: 'GET',
+      success: function(result) {
+        if (result.success) {
+          questionnaireData = result.data;
+          renderQuestionnaire();
+        } else {
+          $('#loading').text(`Ошибка: ${result.error}`);
+        }
+      },
+      error: function(error) {
+        console.error('Error loading questionnaire:', error);
+        $('#loading').text('Не удалось загрузить опрос. Пожалуйста, попробуйте позже.');
+      }
+    });
+  };
+
+  // Отображение данных опроса
+  const renderQuestionnaire = () => {
+    // Заполняем основные данные
+    $('#questionnaire-title').text(questionnaireData.title);
+    $('#questionnaire-description').text(questionnaireData.description || 'Нет описания');
+    
+    // Формируем ссылки
+    const baseUrl = window.location.origin;
+    const baseQuestionnairePath = window.location.pathname.split('/admin')[0];
+    const publicUrl = `${baseUrl}${baseQuestionnairePath}/poll/${questionnaireData.publicLink}`;
+    const adminUrl = `${baseUrl}${baseQuestionnairePath}/admin/${questionnaireData.adminLink}`;
+    
+    $('#public-link').val(publicUrl);
+    $('#admin-link').val(adminUrl);
+    
+    // Отображаем статистику
+    renderStats(questionnaireData.questions);
+    
+    // Показываем контейнер с данными
+    $('#loading').hide();
+    $('#questionnaire-container').show();
+  };
+
+  // Отображение статистики опроса
+  const renderStats = (questions) => {
+    const $statsContainer = $('#stats-container');
+    $statsContainer.empty();
+    
+    // Проверяем, есть ли ответы
+    let hasAnyResponses = false;
+    
+    // Проверяем наличие ответов для каждого типа вопросов
+    for (const question of questions) {
+      if (question.type === 'single' || question.type === 'multiple') {
+        if (question.options && question.options.some(option => option.votes && option.votes > 0)) {
+          hasAnyResponses = true;
+          break;
+        }
+      } else if (question.type === 'tagcloud') {
+        if (question.tags && question.tags.some(tag => tag.count && tag.count > 0)) {
+          hasAnyResponses = true;
+          break;
+        }
+      } else if (question.type === 'scale' || question.type === 'rating') {
+        if (question.responses && question.responses.length > 0) {
+          hasAnyResponses = true;
+          break;
+        }
+      } else if (question.type === 'text') {
+        if (question.textAnswers && question.textAnswers.length > 0) {
+          hasAnyResponses = true;
+          break;
+        }
+      }
+    }
+    
+    if (!hasAnyResponses) {
+      $statsContainer.html('<div class="no-stats">Пока нет ответов на опрос</div>');
+      return;
+    }
+    
+    // Для каждого вопроса создаем блок статистики
+    questions.forEach((question, index) => {
+      const $questionStats = $('<div>', { class: 'question-stats' });
+      const $questionTitle = $('<h3>', { text: `${index + 1}. ${question.text}` });
+      $questionStats.append($questionTitle);
+      
+      // В зависимости от типа вопроса отображаем разную статистику
+      if (question.type === 'single' || question.type === 'multiple') {
+        // Для вопросов с выбором вариантов
+        const totalVotes = question.options.reduce((sum, option) => sum + (option.votes || 0), 0);
+        
+        if (totalVotes === 0) {
+          $questionStats.append($('<div>', { class: 'no-votes', text: 'Нет голосов' }));
+        } else {
+          const $table = $('<table>', { class: 'stats-table' });
+          const $thead = $('<thead>').append(
+            $('<tr>').append(
+              $('<th>', { text: 'Вариант' }),
+              $('<th>', { text: 'Голоса' }),
+              $('<th>', { text: '%' }),
+              $('<th>', { text: 'Визуализация' })
+            )
+          );
+          
+          const $tbody = $('<tbody>');
+          
+          question.options.forEach(option => {
+            const votes = option.votes || 0;
+            const percent = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;
+            
+            const $tr = $('<tr>').append(
+              $('<td>', { text: option.text }),
+              $('<td>', { text: votes }),
+              $('<td>', { text: `${percent}%` }),
+              $('<td>').append(
+                $('<div>', { class: 'bar-container' }).append(
+                  $('<div>', { 
+                    class: 'bar',
+                    css: { width: `${percent}%` }
+                  })
+                )
+              )
+            );
+            
+            $tbody.append($tr);
+          });
+          
+          $table.append($thead, $tbody);
+          $questionStats.append($table);
+          $questionStats.append($('<div>', { class: 'total-votes', text: `Всего голосов: ${totalVotes}` }));
+        }
+      } else if (question.type === 'tagcloud') {
+        // Для облака тегов
+        if (!question.tags || question.tags.length === 0 || !question.tags.some(tag => tag.count > 0)) {
+          $questionStats.append($('<div>', { class: 'no-votes', text: 'Нет выбранных тегов' }));
+        } else {
+          const $tagCloud = $('<div>', { class: 'tag-cloud-stats' });
+          
+          // Находим максимальное количество для масштабирования
+          const maxCount = Math.max(...question.tags.map(tag => tag.count || 0));
+          
+          // Сортируем теги по популярности
+          const sortedTags = [...question.tags].sort((a, b) => (b.count || 0) - (a.count || 0));
+          
+          sortedTags.forEach(tag => {
+            if (tag.count && tag.count > 0) {
+              const fontSize = maxCount > 0 ? 1 + (tag.count / maxCount) * 1.5 : 1; // от 1em до 2.5em
+              
+              $tagCloud.append(
+                $('<span>', {
+                  class: 'tag-item',
+                  text: `${tag.text} (${tag.count})`,
+                  css: { fontSize: `${fontSize}em` }
+                })
+              );
+            }
+          });
+          
+          $questionStats.append($tagCloud);
+        }
+      } else if (question.type === 'scale' || question.type === 'rating') {
+        // Для шкалы и рейтинга
+        if (!question.responses || question.responses.length === 0) {
+          $questionStats.append($('<div>', { class: 'no-votes', text: 'Нет оценок' }));
+        } else {
+          const values = question.responses;
+          const sum = values.reduce((a, b) => a + b, 0);
+          const avg = sum / values.length;
+          const min = Math.min(...values);
+          const max = Math.max(...values);
+          
+          const $scaleStats = $('<div>', { class: 'scale-stats' });
+          
+          $scaleStats.append(
+            $('<div>', { class: 'stat-item' }).append(
+              $('<span>', { class: 'stat-label', text: 'Среднее значение:' }),
+              $('<span>', { class: 'stat-value', text: avg.toFixed(1) })
+            ),
+            $('<div>', { class: 'stat-item' }).append(
+              $('<span>', { class: 'stat-label', text: 'Минимум:' }),
+              $('<span>', { class: 'stat-value', text: min })
+            ),
+            $('<div>', { class: 'stat-item' }).append(
+              $('<span>', { class: 'stat-label', text: 'Максимум:' }),
+              $('<span>', { class: 'stat-value', text: max })
+            ),
+            $('<div>', { class: 'stat-item' }).append(
+              $('<span>', { class: 'stat-label', text: 'Количество оценок:' }),
+              $('<span>', { class: 'stat-value', text: values.length })
+            )
+          );
+          
+          $questionStats.append($scaleStats);
+        }
+      } else if (question.type === 'text') {
+        // Для текстовых ответов
+        if (!question.textAnswers || question.textAnswers.length === 0) {
+          $questionStats.append($('<div>', { class: 'no-votes', text: 'Нет текстовых ответов' }));
+        } else {
+          const $textAnswers = $('<div>', { class: 'text-answers-list' });
+          
+          question.textAnswers.forEach((answer, i) => {
+            $textAnswers.append(
+              $('<div>', { class: 'text-answer-item' }).append(
+                $('<div>', { class: 'answer-number', text: `#${i + 1}` }),
+                $('<div>', { class: 'answer-text', text: answer })
+              )
+            );
+          });
+          
+          $questionStats.append($textAnswers);
+        }
+      }
+      
+      $statsContainer.append($questionStats);
+    });
+  };
+
+  // Копирование ссылок
+  $('#copy-public-link').on('click', function() {
+    $('#public-link').select();
+    document.execCommand('copy');
+    showAlert('Ссылка для голосования скопирована в буфер обмена', 'Копирование', null, true);
+  });
+  
+  $('#copy-admin-link').on('click', function() {
+    $('#admin-link').select();
+    document.execCommand('copy');
+    showAlert('Административная ссылка скопирована в буфер обмена', 'Копирование', null, true);
+  });
+  
+  // Отображение QR-кода
+  $('#show-qr-code').on('click', function() {
+    const publicUrl = $('#public-link').val();
+    showQRCodeModal(publicUrl, 'QR-код для голосования');
+  });
+  
+  // Редактирование опроса
+  $('#edit-questionnaire').on('click', function() {
+    const basePath = window.location.pathname.split('/admin')[0];
+    window.location.href = `${basePath}/edit/${adminLink}`;
+  });
+  
+  // Удаление опроса
+  $('#delete-questionnaire').on('click', function() {
+    showConfirm('Вы уверены, что хотите удалить опрос? Все ответы будут удалены безвозвратно.', function(confirmed) {
+      if (confirmed) {
+        deleteQuestionnaire();
+      }
+    }, 'Удаление опроса');
+  });
+  
+  // Функция удаления опроса
+  const deleteQuestionnaire = () => {
+    $.ajax({
+      url: `${getApiPath()}/questionnaires/admin/${adminLink}`,
+      method: 'DELETE',
+      success: function(result) {
+        if (result.success) {
+          showAlert('Опрос успешно удален', 'Удаление опроса', function() {
+            window.location.href = window.location.pathname.split('/admin')[0];
+          }, true);
+        } else {
+          showAlert(`Ошибка при удалении опроса: ${result.error}`, 'Ошибка');
+        }
+      },
+      error: function(error) {
+        console.error('Error deleting questionnaire:', error);
+        showAlert('Не удалось удалить опрос. Пожалуйста, попробуйте позже.', 'Ошибка');
+      }
+    });
+  };
+  
+  // Инициализация
+  loadQuestionnaire();
+  
+  // Обновление данных каждые 10 секунд
+  setInterval(loadQuestionnaire, 10000);
+}); 
\ No newline at end of file
diff --git a/server/routers/questioneer/public/static/js/common.js b/server/routers/questioneer/public/static/js/common.js
new file mode 100644
index 0000000..7007de8
--- /dev/null
+++ b/server/routers/questioneer/public/static/js/common.js
@@ -0,0 +1,236 @@
+/* global $, document */
+
+// Функция для создания модального окна
+function createModal(options) {
+  // Если модальное окно уже существует, удаляем его
+  $('.modal-overlay').remove();
+  
+  // Опции по умолчанию
+  const defaultOptions = {
+    title: 'Сообщение',
+    content: '',
+    closeText: 'Закрыть',
+    onClose: null,
+    showCancel: false,
+    cancelText: 'Отмена',
+    confirmText: 'Подтвердить',
+    onConfirm: null,
+    onCancel: null,
+    size: 'normal', // 'normal', 'large', 'small'
+    customClass: '',
+    autoClose: false, // Автоматическое закрытие по таймеру
+    autoCloseTime: 2000 // Время до автоматического закрытия (2 секунды)
+  };
+  
+  // Объединяем пользовательские опции с опциями по умолчанию
+  const settings = $.extend({}, defaultOptions, options);
+  
+  // Создаем структуру модального окна
+  const $modalOverlay = $('<div>', { class: 'modal-overlay' });
+  const $modal = $('<div>', { class: `modal ${settings.customClass}` });
+  
+  // Устанавливаем ширину в зависимости от размера
+  if (settings.size === 'large') {
+    $modal.css('max-width', '700px');
+  } else if (settings.size === 'small') {
+    $modal.css('max-width', '400px');
+  }
+  
+  // Создаем заголовок
+  const $modalHeader = $('<div>', { class: 'modal-header' });
+  const $modalTitle = $('<h3>', { text: settings.title });
+  const $modalClose = $('<button>', { 
+    class: 'modal-close',
+    html: '&times;',
+    click: function() {
+      closeModal();
+      if (typeof settings.onClose === 'function') {
+        settings.onClose();
+      }
+    }
+  });
+  
+  $modalHeader.append($modalTitle, $modalClose);
+  
+  // Создаем тело
+  const $modalBody = $('<div>', { class: 'modal-body' });
+  if (typeof settings.content === 'string') {
+    $modalBody.html(settings.content);
+  } else {
+    $modalBody.append(settings.content);
+  }
+  
+  // Создаем футер
+  const $modalFooter = $('<div>', { class: 'modal-footer' });
+  
+  // Если нужно показать кнопку отмены
+  if (settings.showCancel) {
+    const $cancelButton = $('<button>', { 
+      class: 'btn btn-secondary',
+      text: settings.cancelText,
+      click: function() {
+        closeModal();
+        if (typeof settings.onCancel === 'function') {
+          settings.onCancel();
+        }
+      }
+    });
+    $modalFooter.append($cancelButton);
+  }
+  
+  // Кнопка подтверждения/закрытия
+  const $confirmButton = $('<button>', { 
+    class: settings.showCancel ? 'btn btn-primary' : 'btn',
+    text: settings.showCancel ? settings.confirmText : settings.closeText,
+    click: function() {
+      closeModal();
+      if (settings.showCancel && typeof settings.onConfirm === 'function') {
+        settings.onConfirm();
+      } else if (!settings.showCancel && typeof settings.onClose === 'function') {
+        settings.onClose();
+      }
+    }
+  });
+  $modalFooter.append($confirmButton);
+  
+  // Добавляем прогресс-бар, если включено автоматическое закрытие
+  if (settings.autoClose) {
+    const $progressBar = $('<div>', { class: 'modal-progress' });
+    $modal.append($progressBar);
+  }
+  
+  // Собираем модальное окно
+  $modal.append($modalHeader, $modalBody, $modalFooter);
+  $modalOverlay.append($modal);
+  
+  // Добавляем модальное окно в DOM
+  $('body').append($modalOverlay);
+  
+  // Закрытие по клику на фоне
+  $modalOverlay.on('click', function(e) {
+    if (e.target === this) {
+      closeModal();
+      if (typeof settings.onClose === 'function') {
+        settings.onClose();
+      }
+    }
+  });
+  
+  // Функция закрытия модального окна
+  function closeModal() {
+    $modalOverlay.removeClass('active');
+    setTimeout(function() {
+      $modalOverlay.remove();
+    }, 300);
+  }
+  
+  // Активируем модальное окно
+  setTimeout(function() {
+    $modalOverlay.addClass('active');
+    
+    // Активируем прогресс-бар и запускаем таймер закрытия, если включено автоматическое закрытие
+    if (settings.autoClose) {
+      const $progressBar = $modal.find('.modal-progress');
+      
+      setTimeout(() => {
+        $progressBar.addClass('active');
+      }, 50);
+      
+      setTimeout(() => {
+        closeModal();
+        if (typeof settings.onClose === 'function') {
+          settings.onClose();
+        }
+      }, settings.autoCloseTime);
+    }
+  }, 10);
+  
+  // Возвращаем объект модального окна
+  return {
+    $modal: $modal,
+    $overlay: $modalOverlay,
+    close: closeModal
+  };
+}
+
+// Функция для отображения модального окна с сообщением (замена alert)
+function showAlert(message, title, callback, autoClose = false) {
+  return createModal({
+    title: title || 'Сообщение',
+    content: message,
+    onClose: callback,
+    autoClose: autoClose,
+    autoCloseTime: 2000
+  });
+}
+
+// Функция для отображения модального окна с подтверждением (замена confirm)
+function showConfirm(message, callback, title) {
+  return createModal({
+    title: title || 'Подтверждение',
+    content: message,
+    showCancel: true,
+    onConfirm: function() {
+      if (typeof callback === 'function') {
+        callback(true);
+      }
+    },
+    onCancel: function() {
+      if (typeof callback === 'function') {
+        callback(false);
+      }
+    }
+  });
+}
+
+// Функция для генерации QR-кода
+function generateQRCode(data, size) {
+  const typeNumber = 0; // Автоматическое определение
+  const errorCorrectionLevel = 'L'; // Низкий уровень коррекции ошибок
+  const qr = qrcode(typeNumber, errorCorrectionLevel);
+  qr.addData(data);
+  qr.make();
+  return qr.createImgTag(size || 8, 0);
+}
+
+// Функция для отображения QR-кода в модальном окне
+function showQRCodeModal(url, title) {
+  const qrCode = generateQRCode(url);
+  const content = `
+    <div class="qr-container">
+      <div class="qr-code">
+        ${qrCode}
+      </div>
+      <div class="qr-link-container">
+        <input type="text" class="qr-link-input" value="${url}" readonly>
+        <button class="btn btn-copy-link">Копировать</button>
+      </div>
+    </div>
+  `;
+  
+  const modal = createModal({
+    title: title || 'QR-код для доступа',
+    content: content,
+    size: 'large'
+  });
+  
+  // Добавляем обработчик для кнопки копирования
+  modal.$modal.find('.btn-copy-link').on('click', function() {
+    const input = modal.$modal.find('.qr-link-input');
+    input.select();
+    document.execCommand('copy');
+    
+    // Показываем уведомление о копировании
+    const $button = $(this);
+    const originalText = $button.text();
+    $button.text('Скопировано!');
+    $button.addClass('copied');
+    
+    setTimeout(function() {
+      $button.text(originalText);
+      $button.removeClass('copied');
+    }, 1500);
+  });
+  
+  return modal;
+} 
\ No newline at end of file
diff --git a/server/routers/questioneer/public/static/js/create.js b/server/routers/questioneer/public/static/js/create.js
new file mode 100644
index 0000000..2597e7c
--- /dev/null
+++ b/server/routers/questioneer/public/static/js/create.js
@@ -0,0 +1,343 @@
+/* global $, window, document, alert, showAlert, showConfirm */
+$(document).ready(function() {
+  const form = $('#create-questionnaire-form');
+  const questionsList = $('#questions-list');
+  const addQuestionBtn = $('#add-question');
+  
+  let questionCount = 0;
+  
+  // Получаем базовый путь API (для работы и с /questioneer, и с /ms/questioneer)
+  const getApiPath = () => {
+    const pathParts = window.location.pathname.split('/');
+    // Убираем последнюю часть пути (create)
+    pathParts.pop();
+    return pathParts.join('/') + '/api';
+  };
+  
+  // Добавление нового вопроса
+  addQuestionBtn.on('click', function() {
+    addQuestion();
+  });
+  
+  // Обработка отправки формы
+  form.on('submit', function(e) {
+    e.preventDefault();
+    saveQuestionnaire();
+  });
+  
+  // Делегирование событий для динамических элементов
+  questionsList.on('click', '.delete-question', function() {
+    // Удаление вопроса
+    const questionItem = $(this).closest('.question-item');
+    showConfirm('Вы уверены, что хотите удалить этот вопрос?', function(confirmed) {
+      if (confirmed) {
+        questionItem.remove();
+        renumberQuestions();
+        // Вызываем функцию обновления атрибутов required
+        updateRequiredAttributes();
+      }
+    });
+  });
+  
+  questionsList.on('click', '.add-option', function() {
+    // Добавление варианта ответа
+    const questionIndex = $(this).data('question-index');
+    addOption(questionIndex);
+  });
+  
+  questionsList.on('click', '.delete-option', function() {
+    // Удаление варианта ответа
+    $(this).closest('.option-item').remove();
+    // Вызываем функцию обновления атрибутов required
+    updateRequiredAttributes();
+  });
+  
+  // Делегирование для изменения типа вопроса
+  questionsList.on('change', '.question-type-select', function() {
+    const questionItem = $(this).closest('.question-item');
+    const questionIndex = questionItem.data('index');
+    const optionsContainer = $(`#options-container-${questionIndex}`);
+    const scaleContainer = $(`#scale-container-${questionIndex}`);
+    
+    // Скрыть/показать варианты ответа в зависимости от типа вопроса
+    const questionType = $(this).val();
+    if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
+      optionsContainer.show();
+      scaleContainer.hide();
+      
+      // Если нет вариантов, добавляем два
+      const optionsList = $(`#options-list-${questionIndex}`);
+      if (optionsList.children().length === 0) {
+        addOption(questionIndex);
+        addOption(questionIndex);
+      }
+      
+      // Включаем required для полей ввода вариантов
+      optionsList.find('input[type="text"]').prop('required', true);
+    } else if (questionType === 'scale') {
+      optionsContainer.hide();
+      scaleContainer.show();
+      // Отключаем required для скрытых полей
+      $(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
+    } else {
+      optionsContainer.hide();
+      scaleContainer.hide();
+      // Отключаем required для скрытых полей
+      $(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
+    }
+    
+    // Вызываем функцию обновления атрибутов required
+    updateRequiredAttributes();
+  });
+  
+  // Функция для добавления нового вопроса
+  function addQuestion() {
+    const template = $('#question-template').html();
+    const index = questionCount++;
+    
+    // Заменяем плейсхолдеры в шаблоне
+    let questionHtml = template
+      .replace(/\{\{index\}\}/g, index)
+      .replace(/\{\{number\}\}/g, index + 1);
+    
+    questionsList.append(questionHtml);
+    
+    // Показываем/скрываем контейнер вариантов в зависимости от типа вопроса
+    const questionType = $(`#question-type-${index}`).val();
+    if (!['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
+      $(`#options-container-${index}`).hide();
+      // Отключаем required для скрытых полей
+      $(`#options-list-${index}`).find('input[type="text"]').prop('required', false);
+    } else {
+      // Добавляем пару начальных вариантов ответа
+      addOption(index);
+      addOption(index);
+    }
+    
+    if (questionType === 'scale') {
+      $(`#scale-container-${index}`).show();
+    } else {
+      $(`#scale-container-${index}`).hide();
+    }
+    
+    // Вызываем функцию обновления атрибутов required
+    updateRequiredAttributes();
+  }
+  
+  // Функция для добавления варианта ответа
+  function addOption(questionIndex) {
+    const optionsList = $(`#options-list-${questionIndex}`);
+    const template = $('#option-template').html();
+    
+    const optionIndex = optionsList.children().length;
+    
+    // Заменяем плейсхолдеры в шаблоне
+    let optionHtml = template
+      .replace(/\{\{questionIndex\}\}/g, questionIndex)
+      .replace(/\{\{optionIndex\}\}/g, optionIndex);
+    
+    optionsList.append(optionHtml);
+    
+    // Проверяем, видим ли контейнер опций
+    const optionsContainer = $(`#options-container-${questionIndex}`);
+    if (optionsContainer.is(':hidden')) {
+      // Если контейнер скрыт, отключаем required у полей ввода
+      optionsList.find('input[type="text"]').prop('required', false);
+    }
+    
+    // Вызываем функцию обновления атрибутов required
+    updateRequiredAttributes();
+  }
+  
+  // Перенумерация вопросов
+  function renumberQuestions() {
+    $('.question-item').each(function(index) {
+      $(this).find('h3').text(`Вопрос ${index + 1}`);
+    });
+  }
+  
+  // Функция для обновления нумерации вопросов
+  function updateQuestionNumbers() {
+    $('.question-item').each(function(index) {
+      $(this).find('h3').text(`Вопрос ${index + 1}`);
+    });
+  }
+  
+  // Сохранение опроса
+  function saveQuestionnaire() {
+    const questionnaire = {
+      title: $('#title').val(),
+      description: $('#description').val(),
+      displayType: 'step_by_step', // Всегда устанавливаем пошаговый режим
+      questions: []
+    };
+    
+    // Собираем данные о вопросах
+    $('.question-item').each(function() {
+      const index = $(this).data('index');
+      const questionType = $(`#question-type-${index}`).val();
+      
+      const question = {
+        text: $(`#question-text-${index}`).val(),
+        type: questionType,
+        required: $(`input[name="questions[${index}][required]"]`).is(':checked'),
+        options: []
+      };
+      
+      // Добавляем настройки шкалы если нужно
+      if (questionType === 'scale') {
+        question.scaleMin = parseInt($(`#scale-min-${index}`).val()) || 0;
+        question.scaleMax = parseInt($(`#scale-max-${index}`).val()) || 10;
+        question.scaleMinLabel = $(`#scale-min-label-${index}`).val() || 'Минимум';
+        question.scaleMaxLabel = $(`#scale-max-label-${index}`).val() || 'Максимум';
+      }
+      
+      // Собираем варианты ответа если это не текстовый вопрос или шкала
+      if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
+        $(`#options-list-${index} .option-item`).each(function() {
+          const optionText = $(this).find('input[type="text"]').val();
+          
+          if (optionText) {
+            question.options.push({
+              text: optionText,
+              count: 0
+            });
+          }
+        });
+      }
+      
+      questionnaire.questions.push(question);
+    });
+    
+    // Отправка на сервер
+    $.ajax({
+      url: `${getApiPath()}/questionnaires`,
+      method: 'POST',
+      contentType: 'application/json',
+      data: JSON.stringify(questionnaire),
+      success: function(result) {
+        if (result.success) {
+          // Перенаправление на страницу администрирования опроса
+          const basePath = window.location.pathname.split('/create')[0];
+          window.location.href = `${basePath}/admin/${result.data.adminLink}`;
+        } else {
+          showAlert(`Ошибка при создании опроса: ${result.error}`, 'Ошибка');
+        }
+      },
+      error: function(error) {
+        console.error('Error creating questionnaire:', error);
+        showAlert('Не удалось создать опрос. Пожалуйста, попробуйте позже.', 'Ошибка');
+      }
+    });
+  }
+  
+  // Функция для обновления атрибута required в зависимости от видимости полей
+  function updateRequiredAttributes() {
+    // Для полей вопросов
+    $('.question-item').each(function() {
+      const questionType = $(this).find('.question-type-select').val();
+      const textInput = $(this).find('.question-text');
+      const optionsContainer = $(this).find('.options-container');
+      
+      // Обновляем required для текстового поля вопроса
+      if (textInput.is(':visible')) {
+        textInput.prop('required', true);
+      } else {
+        textInput.prop('required', false);
+      }
+      
+      // Обновляем required для полей опций
+      if (questionType === 'single_choice' || questionType === 'multiple_choice') {
+        optionsContainer.find('input[type="text"]').each(function() {
+          if ($(this).is(':visible')) {
+            $(this).prop('required', true);
+          } else {
+            $(this).prop('required', false);
+          }
+        });
+      } else {
+        optionsContainer.find('input[type="text"]').prop('required', false);
+      }
+      
+      // Для шкалы оценки
+      if (questionType === 'scale') {
+        const minInput = $(this).find('.scale-min');
+        const maxInput = $(this).find('.scale-max');
+        const minLabelInput = $(this).find('.scale-min-label');
+        const maxLabelInput = $(this).find('.scale-max-label');
+        
+        if (minInput.is(':visible')) minInput.prop('required', true);
+        else minInput.prop('required', false);
+        
+        if (maxInput.is(':visible')) maxInput.prop('required', true);
+        else maxInput.prop('required', false);
+        
+        if (minLabelInput.is(':visible')) minLabelInput.prop('required', true);
+        else minLabelInput.prop('required', false);
+        
+        if (maxLabelInput.is(':visible')) maxLabelInput.prop('required', true);
+        else maxLabelInput.prop('required', false);
+      }
+    });
+    
+    // Для основных полей формы
+    const titleInput = $('#title');
+    const descriptionInput = $('#description');
+    
+    if (titleInput.is(':visible')) titleInput.prop('required', true);
+    else titleInput.prop('required', false);
+    
+    if (descriptionInput.is(':visible')) descriptionInput.prop('required', false); // Описание не обязательно
+  }
+  
+  // Инициализация с одним вопросом
+  addQuestion();
+  
+  // Обработчик отправки формы
+  $('#create-questionnaire-form').on('submit', function(e) {
+    // Обновляем атрибуты required перед отправкой
+    updateRequiredAttributes();
+    
+    // Проверяем валидность формы
+    if (!this.checkValidity()) {
+      e.preventDefault();
+      e.stopPropagation();
+      
+      // Находим первый невалидный элемент и прокручиваем к нему
+      const firstInvalid = $(this).find(':invalid').first();
+      if (firstInvalid.length) {
+        $('html, body').animate({
+          scrollTop: firstInvalid.offset().top - 100
+        }, 500);
+        
+        // Добавляем класс ошибки к родительскому элементу вопроса
+        firstInvalid.closest('.question-item').addClass('error');
+        setTimeout(() => {
+          firstInvalid.closest('.question-item').removeClass('error');
+        }, 3000);
+      }
+    }
+    
+    $(this).addClass('was-validated');
+  });
+  
+  // Инициализируем атрибуты required
+  updateRequiredAttributes();
+});
+
+// Обработчик удаления вопроса
+$(document).on('click', '.remove-question', function() {
+  $(this).closest('.question-item').remove();
+  updateQuestionNumbers();
+  
+  // Вызываем функцию обновления атрибутов required
+  updateRequiredAttributes();
+});
+
+// Обработчик удаления опции
+$(document).on('click', '.remove-option', function() {
+  $(this).closest('.option-item').remove();
+  
+  // Вызываем функцию обновления атрибутов required
+  updateRequiredAttributes();
+}); 
\ No newline at end of file
diff --git a/server/routers/questioneer/public/static/js/edit.js b/server/routers/questioneer/public/static/js/edit.js
new file mode 100644
index 0000000..90120f6
--- /dev/null
+++ b/server/routers/questioneer/public/static/js/edit.js
@@ -0,0 +1,332 @@
+/* global $, window, document, showAlert, showConfirm, showQRCodeModal */
+$(document).ready(function() {
+  const form = $('#edit-questionnaire-form');
+  const questionsList = $('#questions-list');
+  const addQuestionBtn = $('#add-question');
+  const adminLink = window.location.pathname.split('/').pop();
+  
+  let questionCount = 0;
+  let questionnaireData = null;
+  
+  // Получаем базовый путь API
+  const getApiPath = () => {
+    const pathParts = window.location.pathname.split('/');
+    // Убираем последние две части пути (edit/:adminLink)
+    pathParts.pop();
+    pathParts.pop();
+    return pathParts.join('/') + '/api';
+  };
+  
+  // Загрузка данных опроса
+  const loadQuestionnaire = () => {
+    $.ajax({
+      url: `${getApiPath()}/questionnaires/admin/${adminLink}`,
+      method: 'GET',
+      success: function(result) {
+        if (result.success) {
+          questionnaireData = result.data;
+          fillFormData();
+          $('#loading').hide();
+          $('#edit-form-container').show();
+        } else {
+          $('#loading').text(`Ошибка: ${result.error}`);
+        }
+      },
+      error: function(error) {
+        console.error('Error loading questionnaire:', error);
+        $('#loading').text('Не удалось загрузить опрос. Пожалуйста, попробуйте позже.');
+      }
+    });
+  };
+  
+  // Заполнение формы данными опроса
+  const fillFormData = () => {
+    // Заполняем основные данные
+    $('#title').val(questionnaireData.title);
+    $('#description').val(questionnaireData.description || '');
+    $('#display-type').val(questionnaireData.displayType);
+    
+    // Формируем ссылки
+    const baseUrl = window.location.origin;
+    const baseQuestionnairePath = window.location.pathname.split('/edit')[0];
+    const publicUrl = `${baseUrl}${baseQuestionnairePath}/poll/${questionnaireData.publicLink}`;
+    const adminUrl = `${baseUrl}${baseQuestionnairePath}/admin/${questionnaireData.adminLink}`;
+    
+    $('#public-link').val(publicUrl);
+    $('#admin-link').val(adminUrl);
+    
+    // Добавляем вопросы
+    questionsList.empty();
+    
+    if (questionnaireData.questions && questionnaireData.questions.length > 0) {
+      questionnaireData.questions.forEach((question, index) => {
+        addQuestion(question);
+      });
+    } else {
+      // Если нет вопросов, добавляем пустой
+      addQuestion();
+    }
+    
+    renumberQuestions();
+  };
+  
+  // Добавление нового вопроса
+  addQuestionBtn.on('click', function() {
+    addQuestion();
+    renumberQuestions();
+  });
+  
+  // Обработка отправки формы
+  form.on('submit', function(e) {
+    e.preventDefault();
+    saveQuestionnaire();
+  });
+  
+  // Делегирование событий для динамических элементов
+  questionsList.on('click', '.delete-question', function() {
+    // Удаление вопроса
+    const questionItem = $(this).closest('.question-item');
+    questionItem.remove();
+    renumberQuestions();
+  });
+  
+  questionsList.on('click', '.add-option', function() {
+    // Добавление варианта ответа
+    const questionIndex = $(this).data('question-index');
+    addOption(questionIndex);
+  });
+  
+  questionsList.on('click', '.delete-option', function() {
+    // Удаление варианта ответа
+    $(this).closest('.option-item').remove();
+  });
+  
+  // Делегирование для изменения типа вопроса
+  questionsList.on('change', '.question-type-select', function() {
+    const questionItem = $(this).closest('.question-item');
+    const questionIndex = questionItem.data('index');
+    const optionsContainer = $(`#options-container-${questionIndex}`);
+    const scaleContainer = $(`#scale-container-${questionIndex}`);
+    
+    // Показываем/скрываем контейнеры в зависимости от типа вопроса
+    const questionType = $(this).val();
+    if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
+      optionsContainer.show();
+      scaleContainer.hide();
+      
+      // Если нет вариантов, добавляем два
+      const optionsList = $(`#options-list-${questionIndex}`);
+      if (optionsList.children().length === 0) {
+        addOption(questionIndex);
+        addOption(questionIndex);
+      }
+      
+      // Включаем required для полей ввода вариантов
+      optionsList.find('input[type="text"]').prop('required', true);
+    } else if (questionType === 'scale') {
+      optionsContainer.hide();
+      scaleContainer.show();
+      // Отключаем required для скрытых полей
+      $(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
+    } else {
+      optionsContainer.hide();
+      scaleContainer.hide();
+      // Отключаем required для скрытых полей
+      $(`#options-list-${questionIndex}`).find('input[type="text"]').prop('required', false);
+    }
+  });
+  
+  // Копирование ссылок
+  $('#copy-public-link').on('click', function() {
+    $('#public-link').select();
+    document.execCommand('copy');
+    showAlert('Ссылка для голосования скопирована в буфер обмена', 'Копирование');
+  });
+  
+  $('#copy-admin-link').on('click', function() {
+    $('#admin-link').select();
+    document.execCommand('copy');
+    showAlert('Административная ссылка скопирована в буфер обмена', 'Копирование');
+  });
+  
+  // Отображение QR-кода
+  $('#show-qr-code').on('click', function() {
+    const publicUrl = $('#public-link').val();
+    showQRCodeModal(publicUrl, 'QR-код для голосования');
+  });
+  
+  // Возврат к админке
+  $('#back-to-admin').on('click', function(e) {
+    e.preventDefault();
+    const basePath = window.location.pathname.split('/edit')[0];
+    window.location.href = `${basePath}/admin/${adminLink}`;
+  });
+  
+  // Функция для добавления нового вопроса
+  function addQuestion(questionData) {
+    const template = $('#question-template').html();
+    const index = questionCount++;
+    
+    // Заменяем плейсхолдеры в шаблоне
+    let questionHtml = template
+      .replace(/\{\{index\}\}/g, index)
+      .replace(/\{\{number\}\}/g, index + 1);
+    
+    questionsList.append(questionHtml);
+    
+    // Если есть данные вопроса - заполняем поля
+    if (questionData) {
+      $(`#question-text-${index}`).val(questionData.text);
+      $(`#question-type-${index}`).val(questionData.type);
+      
+      if (questionData.required) {
+        $(`input[name="questions[${index}][required]"]`).prop('checked', true);
+      }
+      
+      // Добавляем варианты ответа если они есть
+      if (questionData.options && questionData.options.length > 0) {
+        questionData.options.forEach(option => {
+          addOption(index, option.text);
+        });
+      }
+      
+      // Заполняем настройки шкалы если нужно
+      if (questionData.scaleMax) {
+        $(`#scale-max-${index}`).val(questionData.scaleMax);
+      }
+    }
+    
+    // Показываем/скрываем контейнеры в зависимости от типа вопроса
+    const questionType = $(`#question-type-${index}`).val();
+    if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
+      $(`#options-container-${index}`).show();
+      $(`#scale-container-${index}`).hide();
+      
+      // Если нет вариантов и не загружены данные, добавляем два
+      if (!questionData && $(`#options-list-${index}`).children().length === 0) {
+        addOption(index);
+        addOption(index);
+      }
+    } else if (questionType === 'scale') {
+      $(`#options-container-${index}`).hide();
+      $(`#scale-container-${index}`).show();
+    } else {
+      $(`#options-container-${index}`).hide();
+      $(`#scale-container-${index}`).hide();
+    }
+  }
+  
+  // Функция для добавления варианта ответа
+  function addOption(questionIndex, optionText) {
+    const optionsList = $(`#options-list-${questionIndex}`);
+    const template = $('#option-template').html();
+    
+    const optionIndex = optionsList.children().length;
+    
+    // Заменяем плейсхолдеры в шаблоне
+    let optionHtml = template
+      .replace(/\{\{questionIndex\}\}/g, questionIndex)
+      .replace(/\{\{optionIndex\}\}/g, optionIndex);
+    
+    optionsList.append(optionHtml);
+    
+    // Если есть текст варианта - устанавливаем его
+    if (optionText) {
+      optionsList.children().last().find('input[type="text"]').val(optionText);
+    }
+    
+    // Проверяем, видим ли контейнер опций
+    const optionsContainer = $(`#options-container-${questionIndex}`);
+    if (optionsContainer.is(':hidden')) {
+      // Если контейнер скрыт, отключаем required у полей ввода
+      optionsList.find('input[type="text"]').prop('required', false);
+    }
+  }
+  
+  // Перенумерация вопросов
+  function renumberQuestions() {
+    $('.question-item').each(function(index) {
+      $(this).find('h3').text(`Вопрос ${index + 1}`);
+    });
+  }
+  
+  // Сохранение опроса
+  function saveQuestionnaire() {
+    const questionnaire = {
+      title: $('#title').val(),
+      description: $('#description').val(),
+      displayType: $('#display-type').val(),
+      questions: []
+    };
+    
+    // Собираем данные о вопросах
+    $('.question-item').each(function() {
+      const index = $(this).data('index');
+      const questionType = $(`#question-type-${index}`).val();
+      
+      const question = {
+        text: $(`#question-text-${index}`).val(),
+        type: questionType,
+        required: $(`input[name="questions[${index}][required]"]`).is(':checked'),
+        options: []
+      };
+      
+      // Добавляем настройки шкалы если нужно
+      if (questionType === 'scale') {
+        question.scaleMax = parseInt($(`#scale-max-${index}`).val());
+      }
+      
+      // Собираем варианты ответа если это не текстовый вопрос или оценка
+      if (['single_choice', 'multiple_choice', 'tag_cloud'].includes(questionType)) {
+        $(`#options-list-${index} .option-item`).each(function() {
+          const optionText = $(this).find('input[type="text"]').val();
+          
+          if (optionText) {
+            // Сохраняем количество голосов из старых данных
+            let count = 0;
+            const optionIndex = $(this).data('index');
+            
+            if (questionnaireData && 
+                questionnaireData.questions[index] && 
+                questionnaireData.questions[index].options && 
+                questionnaireData.questions[index].options[optionIndex]) {
+              count = questionnaireData.questions[index].options[optionIndex].count || 0;
+            }
+            
+            question.options.push({
+              text: optionText,
+              count: count
+            });
+          }
+        });
+      }
+      
+      questionnaire.questions.push(question);
+    });
+    
+    // Отправка на сервер
+    $.ajax({
+      url: `${getApiPath()}/questionnaires/${adminLink}`,
+      method: 'PUT',
+      contentType: 'application/json',
+      data: JSON.stringify(questionnaire),
+      success: function(result) {
+        if (result.success) {
+          showAlert('Опрос успешно обновлен', 'Успешно', function() {
+            const basePath = window.location.pathname.split('/edit')[0];
+            window.location.href = `${basePath}/admin/${adminLink}`;
+          });
+        } else {
+          showAlert(`Ошибка при обновлении опроса: ${result.error}`, 'Ошибка');
+        }
+      },
+      error: function(error) {
+        console.error('Error updating questionnaire:', error);
+        showAlert('Не удалось обновить опрос. Пожалуйста, попробуйте позже.', 'Ошибка');
+      }
+    });
+  }
+  
+  // Инициализация
+  loadQuestionnaire();
+}); 
\ No newline at end of file
diff --git a/server/routers/questioneer/public/static/js/index.js b/server/routers/questioneer/public/static/js/index.js
new file mode 100644
index 0000000..68f970d
--- /dev/null
+++ b/server/routers/questioneer/public/static/js/index.js
@@ -0,0 +1,67 @@
+/* global $, window, document */
+$(document).ready(function() {
+  // Функция для получения базового пути API
+  const getApiPath = () => {
+    // Извлекаем базовый путь из URL страницы
+    const pathParts = window.location.pathname.split('/');
+    // Если последний сегмент пустой (из-за /) - удаляем его
+    if (pathParts[pathParts.length - 1] === '') {
+      pathParts.pop();
+    }
+    
+    // Путь до корня приложения
+    return pathParts.join('/') + '/api';
+  };
+  
+  // Функция для загрузки списка опросов
+  const loadQuestionnaires = () => {
+    $.ajax({
+      url: getApiPath() + '/questionnaires',
+      method: 'GET',
+      success: function(result) {
+        if (result.success) {
+          renderQuestionnaires(result.data);
+        } else {
+          $('#questionnaires-container').html(`<p class="error">Ошибка: ${result.error}</p>`);
+        }
+      },
+      error: function(error) {
+        console.error('Error loading questionnaires:', error);
+        $('#questionnaires-container').html('<p class="error">Не удалось загрузить опросы. Пожалуйста, попробуйте позже.</p>');
+      }
+    });
+  };
+  
+  // Функция для отображения списка опросов
+  const renderQuestionnaires = (questionnaires) => {
+    if (!questionnaires || questionnaires.length === 0) {
+      $('#questionnaires-container').html('<p>У вас еще нет созданных опросов.</p>');
+      return;
+    }
+    
+    // Получаем базовый путь (для работы и с /questioneer, и с /ms/questioneer)
+    const basePath = window.location.pathname.endsWith('/') 
+      ? window.location.pathname 
+      : window.location.pathname + '/';
+    
+    const questionnairesHTML = questionnaires.map(q => `
+      <div class="questionnaire-item">
+        <h3>${q.title}</h3>
+        <p>${q.description || 'Нет описания'}</p>
+        <p>Создан: ${new Date(q.createdAt).toLocaleString()}</p>
+        <div class="questionnaire-links">
+          <a href="${basePath}admin/${q.adminLink}" class="btn btn-small">Редактировать</a>
+          <a href="${basePath}poll/${q.publicLink}" class="btn btn-small btn-primary" target="_blank">Смотреть как участник</a>
+        </div>
+      </div>
+    `).join('');
+    
+    $('#questionnaires-container').html(questionnairesHTML);
+  };
+  
+  // Инициализация страницы
+  loadQuestionnaires();
+  
+  // Обновление данных каждые 30 секунд
+  setInterval(loadQuestionnaires, 30000);
+}); 
\ No newline at end of file
diff --git a/server/routers/questioneer/public/static/js/poll.js b/server/routers/questioneer/public/static/js/poll.js
new file mode 100644
index 0000000..d7e78f1
--- /dev/null
+++ b/server/routers/questioneer/public/static/js/poll.js
@@ -0,0 +1,1069 @@
+/* global $, window, document, showAlert */
+$(document).ready(function() {
+  const publicLink = window.location.pathname.split('/').pop();
+  let questionnaireData = null;
+  
+  // Элементы DOM
+  const loadingEl = $('#loading');
+  const containerEl = $('#questionnaire-container');
+  const titleEl = $('#questionnaire-title');
+  const descriptionEl = $('#questionnaire-description');
+  const questionsContainerEl = $('#questions-container');
+  const formEl = $('#poll-form');
+  const resultsContainerEl = $('#results-container');
+  const pollResultsContainerEl = $('#poll-results-container');
+  
+  // Элементы навигации для пошаговых опросов
+  const navigationControlsEl = $('#navigation-controls');
+  const prevButtonEl = $('#prev-question');
+  const nextButtonEl = $('#next-question');
+  const questionCounterEl = $('#question-counter');
+  const submitButtonEl = $('#submit-button');
+  
+  // Для пошаговых опросов
+  let currentQuestionIndex = 0;
+  
+  // Проверка доступности localStorage
+  const isLocalStorageAvailable = () => {
+    try {
+      const testKey = 'test';
+      window.localStorage.setItem(testKey, testKey);
+      window.localStorage.removeItem(testKey);
+      return true;
+    } catch (e) {
+      return false;
+    }
+  };
+  
+  // Ключ для localStorage
+  const getLocalStorageKey = () => `questionnaire_${publicLink}_completed`;
+  
+  // Проверка на повторное прохождение опроса
+  const checkIfAlreadyCompleted = () => {
+    if (!isLocalStorageAvailable()) return false;
+    return window.localStorage.getItem(getLocalStorageKey()) === 'true';
+  };
+  
+  // Сохранение информации о прохождении опроса
+  const markAsCompleted = () => {
+    if (!isLocalStorageAvailable()) return;
+    window.localStorage.setItem(getLocalStorageKey(), 'true');
+  };
+  
+  // Получаем базовый путь API
+  const getApiPath = () => {
+    const pathParts = window.location.pathname.split('/');
+    // Убираем последние две части пути (poll/:publicLink)
+    pathParts.pop();
+    pathParts.pop();
+    return pathParts.join('/') + '/api';
+  };
+  
+  // Загрузка данных опроса
+  const loadQuestionnaire = () => {
+    $.ajax({
+      url: `${getApiPath()}/questionnaires/public/${publicLink}`,
+      method: 'GET',
+      success: function(result) {
+        if (result.success) {
+          questionnaireData = result.data;
+          
+          // Проверяем, проходил ли пользователь уже этот опрос
+          if (checkIfAlreadyCompleted()) {
+            showAlreadyCompletedMessage();
+          } else {
+            showWelcomeAnimation();
+          }
+        } else {
+          loadingEl.text(`Ошибка: ${result.error}`);
+        }
+      },
+      error: function(error) {
+        console.error('Error loading questionnaire:', error);
+        loadingEl.text('Не удалось загрузить опрос. Пожалуйста, попробуйте позже.');
+      }
+    });
+  };
+  
+  // Показываем анимацию приветствия
+  const showWelcomeAnimation = () => {
+    // Скрываем индикатор загрузки
+    loadingEl.hide();
+    
+    // Создаем элемент приветствия
+    const $welcomeEl = $('<div>', {
+      id: 'welcome-animation',
+      class: 'welcome-animation',
+      css: {
+        opacity: 0,
+        transform: 'translateY(20px)'
+      }
+    });
+    
+    const $welcomeIcon = $('<div>', {
+      class: 'welcome-icon',
+      html: '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" viewBox="0 0 16 16"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M4.285 9.567a.5.5 0 0 1 .683.183A3.498 3.498 0 0 0 8 11.5a3.498 3.498 0 0 0 3.032-1.75.5.5 0 1 1 .866.5A4.498 4.498 0 0 1 8 12.5a4.498 4.498 0 0 1-3.898-2.25.5.5 0 0 1 .183-.683zM7 6.5C7 7.328 6.552 8 6 8s-1-.672-1-1.5S5.448 5 6 5s1 .672 1 1.5zm4 0c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5z"/></svg>'
+    });
+    
+    const $welcomeTitle = $('<h2>', {
+      text: `Добро пожаловать в опрос "${questionnaireData.title}"`,
+      class: 'welcome-title'
+    });
+    
+    const $welcomeDescription = $('<p>', {
+      text: questionnaireData.description || 'Пожалуйста, ответьте на следующие вопросы:',
+      class: 'welcome-description'
+    });
+    
+    const $startButton = $('<button>', {
+      type: 'button',
+      text: 'Начать опрос',
+      class: 'btn btn-primary welcome-start-btn'
+    });
+    
+    $startButton.on('click', function() {
+      $welcomeEl.css({
+        opacity: 0,
+        transform: 'translateY(-20px)'
+      });
+      
+      setTimeout(() => {
+        $welcomeEl.remove();
+        renderQuestionnaire();
+      }, 500);
+    });
+    
+    $welcomeEl.append($welcomeIcon, $welcomeTitle, $welcomeDescription, $startButton);
+    
+    // Добавляем приветствие перед контейнером опроса
+    containerEl.before($welcomeEl);
+    
+    // Анимируем появление
+    setTimeout(() => {
+      $welcomeEl.css({
+        opacity: 1,
+        transform: 'translateY(0)'
+      });
+    }, 100);
+  };
+  
+  // Показываем сообщение о том, что опрос уже пройден
+  const showAlreadyCompletedMessage = () => {
+    loadingEl.hide();
+    
+    const $completedEl = $('<div>', {
+      class: 'already-completed',
+      css: {
+        opacity: 0,
+        transform: 'translateY(20px)'
+      }
+    });
+    
+    const $completedIcon = $('<div>', {
+      class: 'completed-icon',
+      html: '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" viewBox="0 0 16 16"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/></svg>'
+    });
+    
+    const $completedTitle = $('<h2>', {
+      text: 'Вы уже прошли этот опрос',
+      class: 'completed-title'
+    });
+    
+    const $completedDescription = $('<p>', {
+      text: 'Спасибо за ваше участие. Вы уже отправили свои ответы на этот опрос.',
+      class: 'completed-description'
+    });
+    
+    const $viewResultsButton = $('<button>', {
+      type: 'button',
+      text: 'Посмотреть результаты',
+      class: 'btn btn-primary view-results-btn'
+    });
+    
+    const $startAgainButton = $('<button>', {
+      type: 'button',
+      text: 'Пройти снова',
+      class: 'btn btn-secondary start-again-btn'
+    });
+    
+    $viewResultsButton.on('click', function() {
+      $completedEl.remove();
+      containerEl.show();
+      formEl.hide();
+      resultsContainerEl.show();
+      loadPollResults();
+    });
+    
+    $startAgainButton.on('click', function() {
+      if (isLocalStorageAvailable()) {
+        window.localStorage.removeItem(getLocalStorageKey());
+      }
+      $completedEl.remove();
+      showWelcomeAnimation();
+    });
+    
+    $completedEl.append($completedIcon, $completedTitle, $completedDescription, $viewResultsButton, $startAgainButton);
+    
+    containerEl.before($completedEl);
+    
+    setTimeout(() => {
+      $completedEl.css({
+        opacity: 1,
+        transform: 'translateY(0)'
+      });
+    }, 100);
+  };
+  
+  // Отображение опроса
+  const renderQuestionnaire = () => {
+    titleEl.text(questionnaireData.title);
+    descriptionEl.text(questionnaireData.description || '');
+    
+    // Удаляем кнопку "Отправить ответы", так как есть кнопка "Отправить" в навигации
+    formEl.find('.form-actions').remove();
+    
+    // Добавляем навигацию для пошагового опроса
+    formEl.append(`
+      <div class="step-navigation">
+        <button type="button" id="prev-question" class="btn btn-secondary nav-btn">
+          <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
+            <path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
+          </svg>
+          Назад
+        </button>
+        <span id="question-counter">Вопрос 1 из ${questionnaireData.questions.length}</span>
+        <button type="button" id="next-question" class="btn nav-btn">
+          Далее
+          <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
+            <path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
+          </svg>
+        </button>
+      </div>
+    `);
+    
+    // Обработчики для навигации
+    $('#prev-question').on('click', prevQuestion);
+    $('#next-question').on('click', nextQuestion);
+    
+    // Показываем только первый вопрос
+    currentQuestionIndex = 0;
+    renderCurrentQuestion();
+    
+    // Показываем контейнер с данными
+    containerEl.show();
+  };
+  
+  // Отображение текущего вопроса в пошаговом режиме
+  const renderCurrentQuestion = () => {
+    // Очищаем контейнер вопросов
+    questionsContainerEl.empty();
+    
+    // Получаем текущий вопрос
+    const currentQuestion = questionnaireData.questions[currentQuestionIndex];
+    
+    // Отображаем вопрос
+    renderQuestion(questionsContainerEl, currentQuestion, currentQuestionIndex);
+    
+    // Добавляем классы для анимации
+    const $currentQuestionEl = questionsContainerEl.find(`.question-item[data-index="${currentQuestionIndex}"]`);
+    $currentQuestionEl.addClass('enter');
+    
+    // Запускаем анимацию появления после небольшой задержки
+    setTimeout(() => {
+      $currentQuestionEl.removeClass('enter').addClass('active');
+    }, 50);
+    
+    // Обновляем счетчик вопросов
+    questionCounterEl.addClass('update');
+    questionCounterEl.text(`Вопрос ${currentQuestionIndex + 1} из ${questionnaireData.questions.length}`);
+    
+    // Снимаем класс анимации счетчика
+    setTimeout(() => {
+      questionCounterEl.removeClass('update');
+    }, 500);
+    
+    // Управляем состоянием кнопок навигации
+    prevButtonEl.prop('disabled', currentQuestionIndex === 0);
+    nextButtonEl.text(
+      currentQuestionIndex === questionnaireData.questions.length - 1 
+        ? 'Отправить' 
+        : 'Далее'
+    );
+  };
+  
+  // Переход к предыдущему вопросу
+  const prevQuestion = function() {
+    if (currentQuestionIndex > 0) {
+      currentQuestionIndex--;
+      renderCurrentQuestion();
+    }
+  };
+  
+  // Переход к следующему вопросу или завершение опроса
+  const nextQuestion = function() {
+    const currentQuestion = questionnaireData.questions[currentQuestionIndex];
+    
+    // Проверяем, что на текущий вопрос есть ответ, если он обязательный
+    if (currentQuestion.required && !checkQuestionAnswer(currentQuestion, currentQuestionIndex)) {
+      // Добавляем класс ошибки к текущему вопросу
+      const $currentQuestionEl = questionsContainerEl.find(`.question-item[data-index="${currentQuestionIndex}"]`);
+      $currentQuestionEl.addClass('error shake');
+      
+      // Удаляем класс ошибки после окончания анимации
+      setTimeout(() => {
+        $currentQuestionEl.removeClass('shake');
+      }, 500);
+      
+      return;
+    }
+    
+    // Если есть еще вопросы, переходим к следующему
+    if (currentQuestionIndex < questionnaireData.questions.length - 1) {
+      // Анимируем текущий вопрос перед переходом к следующему
+      questionsContainerEl.find(`.question-item[data-index="${currentQuestionIndex}"]`).addClass('exit');
+      
+      // Увеличиваем индекс текущего вопроса
+      currentQuestionIndex++;
+      
+      // Рендерим новый вопрос после небольшой задержки
+      setTimeout(() => {
+        renderCurrentQuestion();
+      }, 200);
+    } else {
+      // Анимируем контейнер перед отправкой формы
+      questionsContainerEl.find('.question-item').addClass('exit');
+      
+      // Последний вопрос, отправляем форму
+      setTimeout(() => {
+        // Отключаем атрибут required у невидимых полей перед отправкой
+        $('input[required]:not(:visible), textarea[required]:not(:visible)').prop('required', false);
+        
+        submitForm();
+      }, 300);
+    }
+  };
+  
+  // Проверка наличия ответа на вопрос
+  const checkQuestionAnswer = (question, questionIndex) => {
+    switch (question.type) {
+      case 'single_choice':
+        return $(`.question-item[data-index="${questionIndex}"] input[name="question_${questionIndex}"]:checked`).length > 0;
+        
+      case 'multiple_choice':
+        return $(`.question-item[data-index="${questionIndex}"] input[type="checkbox"]:checked:not(.multiple-choice-validation)`).length > 0;
+        
+      case 'text':
+        return $(`.question-item[data-index="${questionIndex}"] textarea`).val().trim() !== '';
+        
+      case 'scale':
+        return $(`.question-item[data-index="${questionIndex}"] input[name="question_${questionIndex}"]:checked`).length > 0;
+        
+      case 'tag_cloud':
+        return $(`.question-item[data-index="${questionIndex}"] .tag-item.selected`).length > 0;
+        
+      default:
+        return true;
+    }
+  };
+  
+  // Отображение всех вопросов (стандартный режим)
+  const renderQuestions = () => {
+    questionsContainerEl.empty();
+    
+    $.each(questionnaireData.questions, function(questionIndex, question) {
+      renderQuestion(questionsContainerEl, question, questionIndex);
+    });
+  };
+  
+  // Отображение одного вопроса
+  const renderQuestion = (container, question, questionIndex) => {
+    const $questionEl = $('<div>', {
+      class: 'question-item',
+      'data-index': questionIndex
+    });
+    
+    const $questionTitle = $('<h3>', {
+      text: question.text,
+      class: 'question-title'
+    });
+    
+    if (question.required) {
+      const $requiredMark = $('<span>', {
+        class: 'required-mark',
+        text: ' *'
+      });
+      $questionTitle.append($requiredMark);
+    }
+    
+    $questionEl.append($questionTitle);
+    
+    // Отображение вариантов ответа в зависимости от типа вопроса
+    switch (question.type) {
+      case 'single_choice':
+        renderSingleChoice($questionEl, question, questionIndex);
+        break;
+      case 'multiple_choice':
+        renderMultipleChoice($questionEl, question, questionIndex);
+        break;
+      case 'text':
+        renderTextAnswer($questionEl, question, questionIndex);
+        break;
+      case 'scale':
+        renderScale($questionEl, question, questionIndex);
+        break;
+      case 'rating': // Обратная совместимость для рейтинга
+        renderScale($questionEl, { ...question, scaleMax: 5 }, questionIndex);
+        break;
+      case 'tag_cloud':
+        renderTagCloud($questionEl, question, questionIndex);
+        break;
+    }
+    
+    container.append($questionEl);
+  };
+  
+  // Отображение вопроса с одиночным выбором
+  const renderSingleChoice = ($container, question, questionIndex) => {
+    const $optionsContainer = $('<div>', {
+      class: 'radio-options-container'
+    });
+    
+    $.each(question.options, function(optionIndex, option) {
+      const $optionContainer = $('<div>', {
+        class: 'radio-option'
+      });
+      
+      const $optionInput = $('<input>', {
+        type: 'radio',
+        id: `question_${questionIndex}_option_${optionIndex}`,
+        name: `question_${questionIndex}`,
+        value: optionIndex,
+        required: question.required
+      });
+      
+      const $optionLabel = $('<label>', {
+        for: `question_${questionIndex}_option_${optionIndex}`,
+        text: option.text
+      });
+      
+      $optionContainer.append($optionInput, $optionLabel);
+      $optionsContainer.append($optionContainer);
+    });
+    
+    $container.append($optionsContainer);
+  };
+  
+  // Отображение вопроса с множественным выбором
+  const renderMultipleChoice = ($container, question, questionIndex) => {
+    const $optionsContainer = $('<div>', {
+      class: 'checkbox-options-container'
+    });
+    
+    // Скрытое поле для проверки заполнения обязательного вопроса с чекбоксами
+    if (question.required) {
+      const $validationCheckbox = $('<input>', {
+        type: 'checkbox',
+        class: 'multiple-choice-validation',
+        required: true,
+        name: `question_${questionIndex}_validation`,
+        style: 'display: none;',
+        id: `question_${questionIndex}_validation`
+      });
+      
+      $optionsContainer.append($validationCheckbox);
+    }
+    
+    $.each(question.options, function(optionIndex, option) {
+      const $optionContainer = $('<div>', {
+        class: 'checkbox-option'
+      });
+      
+      const $optionInput = $('<input>', {
+        type: 'checkbox',
+        id: `question_${questionIndex}_option_${optionIndex}`,
+        name: `question_${questionIndex}_option_${optionIndex}`,
+        value: 'true'
+      });
+      
+      const $optionLabel = $('<label>', {
+        for: `question_${questionIndex}_option_${optionIndex}`,
+        text: option.text
+      });
+      
+      // Обработчик изменения значения чекбокса
+      if (question.required) {
+        $optionInput.on('change', function() {
+          const anyChecked = $optionsContainer.find('input[type="checkbox"]:checked:not(.multiple-choice-validation)').length > 0;
+          $(`#question_${questionIndex}_validation`).prop('checked', anyChecked);
+        });
+      }
+      
+      $optionContainer.append($optionInput, $optionLabel);
+      $optionsContainer.append($optionContainer);
+    });
+    
+    $container.append($optionsContainer);
+  };
+  
+  // Отображение текстового вопроса
+  const renderTextAnswer = ($container, question, questionIndex) => {
+    const $textareaContainer = $('<div>', {
+      class: 'textarea-container'
+    });
+    
+    const $textarea = $('<textarea>', {
+      id: `question_${questionIndex}_text`,
+      name: `question_${questionIndex}_text`,
+      rows: 4,
+      placeholder: 'Введите ваш ответ здесь...',
+      required: question.required
+    });
+    
+    $textareaContainer.append($textarea);
+    $container.append($textareaContainer);
+  };
+  
+  // Отображение вопроса с шкалой оценки
+  const renderScale = ($container, question, questionIndex) => {
+    const minValue = question.scaleMin || 0;
+    const maxValue = question.scaleMax || 10;
+    const minLabel = question.scaleMinLabel || 'Минимум';
+    const maxLabel = question.scaleMaxLabel || 'Максимум';
+    
+    const $scaleContainer = $('<div>', {
+      class: 'scale-container'
+    });
+    
+    const $scaleLabels = $('<div>', {
+      class: 'scale-labels'
+    });
+    
+    $scaleLabels.append(
+      $('<span>', { class: 'scale-label-min', text: minLabel }),
+      $('<span>', { class: 'scale-label-max', text: maxLabel })
+    );
+    
+    const $scaleValues = $('<div>', {
+      class: 'scale-values'
+    });
+    
+    // Создаем кнопки для шкалы от min до max
+    for (let i = minValue; i <= maxValue; i++) {
+      const $scaleItem = $('<div>', {
+        class: 'scale-item'
+      });
+      
+      const $scaleInput = $('<input>', {
+        type: 'radio',
+        id: `question_${questionIndex}_scale_${i}`,
+        name: `question_${questionIndex}`,
+        value: i,
+        required: question.required
+      });
+      
+      const $scaleLabel = $('<label>', {
+        for: `question_${questionIndex}_scale_${i}`,
+        text: i
+      });
+      
+      $scaleItem.append($scaleInput, $scaleLabel);
+      $scaleValues.append($scaleItem);
+    }
+    
+    $scaleContainer.append($scaleLabels, $scaleValues);
+    $container.append($scaleContainer);
+  };
+  
+  // Отображение вопроса с облаком тегов
+  const renderTagCloud = ($container, question, questionIndex) => {
+    const $tagCloudContainer = $('<div>', {
+      class: 'tag-cloud-container'
+    });
+    
+    if (question.options && question.options.length > 0) {
+      // Предопределенное облако тегов
+      $.each(question.options, function(optionIndex, option) {
+        if (!option || !option.text) return; // Пропускаем пустые или невалидные опции
+        
+        const $tagItem = $('<div>', {
+          class: 'tag-item',
+          text: option.text,
+          'data-index': optionIndex,
+          click: function() {
+            $(this).toggleClass('selected');
+            updateTagValidation();
+          }
+        });
+        
+        $tagCloudContainer.append($tagItem);
+      });
+    } else {
+      // Пользовательское облако тегов
+      const $tagInputContainer = $('<div>', {
+        class: 'tag-input-container'
+      });
+      
+      const $tagInput = $('<input>', {
+        type: 'text',
+        placeholder: 'Введите теги, разделенные пробелом, и нажмите Enter',
+        class: 'tag-input'
+      });
+      
+      const $tagItems = $('<div>', {
+        class: 'tag-items'
+      });
+      
+      // Обработчик ввода тегов
+      $tagInput.on('keypress', function(e) {
+        if (e.which === 13) { // Enter
+          e.preventDefault();
+          
+          const inputText = $(this).val().trim();
+          if (inputText) {
+            // Разбиваем ввод на отдельные теги
+            const tags = inputText.split(/\s+/);
+            
+            // Добавляем каждый тег как отдельный элемент
+            tags.forEach(tagText => {
+              if (tagText) {
+                const $tagItem = $('<div>', {
+                  class: 'tag-item selected',
+                  text: tagText
+                });
+                
+                const $tagRemove = $('<span>', {
+                  class: 'tag-remove',
+                  html: '&times;',
+                  click: function(e) {
+                    e.stopPropagation();
+                    $(this).parent().remove();
+                    updateTagValidation();
+                  }
+                });
+                
+                $tagItem.append($tagRemove);
+                $tagItems.append($tagItem);
+              }
+            });
+            
+            $(this).val('');
+            updateTagValidation();
+          }
+        }
+      });
+      
+      $tagInputContainer.append($tagInput, $tagItems);
+      $tagCloudContainer.append($tagInputContainer);
+    }
+    
+    // Скрытое поле для валидации
+    if (question.required) {
+      const $validationInput = $('<input>', {
+        type: 'text',
+        required: true,
+        style: 'display: none;',
+        id: `question_${questionIndex}_tag_validation`
+      });
+      
+      $tagCloudContainer.append($validationInput);
+    }
+    
+    $container.append($tagCloudContainer);
+    
+    // Функция для обновления валидации тегов
+    function updateTagValidation() {
+      if (question.required) {
+        const hasSelectedTags = $tagCloudContainer.find('.tag-item.selected').length > 0;
+        $(`#question_${questionIndex}_tag_validation`).val(hasSelectedTags ? 'valid' : '');
+      }
+    }
+  };
+  
+  // Показываем анимацию благодарности
+  const showThankYouAnimation = () => {
+    // Скрываем форму
+    formEl.hide();
+    
+    // Создаем анимацию благодарности
+    const $thankYouEl = $('<div>', {
+      class: 'thank-you-animation',
+      css: {
+        opacity: 0,
+        transform: 'scale(0.9)'
+      }
+    });
+    
+    const $thankYouIcon = $('<div>', {
+      class: 'thank-you-icon',
+      html: '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" fill="currentColor" viewBox="0 0 16 16"><path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/><path d="M10.97 4.97a.235.235 0 0 0-.02.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05z"/></svg>'
+    });
+    
+    const $thankYouTitle = $('<h2>', {
+      text: 'Спасибо за участие!',
+      class: 'thank-you-title'
+    });
+    
+    const $thankYouDescription = $('<p>', {
+      text: 'Ваши ответы были успешно записаны. Мы ценим ваше мнение!',
+      class: 'thank-you-description'
+    });
+    
+    $thankYouEl.append($thankYouIcon, $thankYouTitle, $thankYouDescription);
+    
+    // Добавляем анимацию перед результатами
+    resultsContainerEl.prepend($thankYouEl);
+    
+    // Показываем контейнер с результатами
+    resultsContainerEl.show();
+    
+    // Анимируем появление
+    setTimeout(() => {
+      $thankYouEl.css({
+        opacity: 1,
+        transform: 'scale(1)'
+      });
+      
+      // Загружаем результаты через некоторое время
+      setTimeout(() => {
+        loadPollResults();
+      }, 1000);
+    }, 100);
+  };
+  
+  // Отправка формы
+  const submitForm = function() {
+    // Отключаем атрибут required у невидимых полей перед отправкой
+    $('input[required]:not(:visible), textarea[required]:not(:visible)').prop('required', false);
+    
+    // Собираем ответы
+    const answers = [];
+    
+    $.each(questionnaireData.questions, function(questionIndex, question) {
+      const $questionEl = $(`.question-item[data-index="${questionIndex}"]`);
+      
+      let optionIndices;
+      let selectedOption;
+      let selectedOptions;
+      let selectedTags;
+      let textAnswer;
+      let scaleValue;
+      
+      switch (question.type) {
+        case 'single_choice':
+          selectedOption = $questionEl.find(`input[name="question_${questionIndex}"]:checked`);
+          if (selectedOption.length) {
+            answers.push({
+              questionIndex,
+              optionIndices: [parseInt(selectedOption.val())]
+            });
+          }
+          break;
+          
+        case 'multiple_choice':
+          selectedOptions = $questionEl.find('input[type="checkbox"]:checked:not(.multiple-choice-validation)');
+          if (selectedOptions.length) {
+            optionIndices = $.map(selectedOptions, function(option) {
+              const nameParts = $(option).attr('name').split('_');
+              return parseInt(nameParts[nameParts.length - 1]);
+            });
+            answers.push({
+              questionIndex,
+              optionIndices
+            });
+          }
+          break;
+          
+        case 'text':
+          textAnswer = $questionEl.find('textarea').val().trim();
+          if (textAnswer) {
+            answers.push({
+              questionIndex,
+              textAnswer
+            });
+          }
+          break;
+          
+        case 'scale':
+        case 'rating': // Обратная совместимость
+          selectedOption = $questionEl.find(`input[name="question_${questionIndex}"]:checked`);
+          if (selectedOption.length) {
+            answers.push({
+              questionIndex,
+              scaleValue: parseInt(selectedOption.val())
+            });
+          }
+          break;
+          
+        case 'tag_cloud':
+          selectedTags = $questionEl.find('.tag-item.selected');
+          if (selectedTags.length) {
+            const tagTexts = $.map(selectedTags, function(tag) {
+              return $(tag).text().replace('×', '').trim();
+            });
+            answers.push({
+              questionIndex,
+              tagTexts
+            });
+          }
+          break;
+      }
+    });
+    
+    // Отправляем ответы на сервер
+    $.ajax({
+      url: `${getApiPath()}/vote/${publicLink}`,
+      method: 'POST',
+      contentType: 'application/json',
+      data: JSON.stringify({ answers }),
+      success: function(result) {
+        if (result.success) {
+          // Сохраняем информацию о прохождении опроса
+          markAsCompleted();
+          
+          // Показываем анимацию благодарности вместо сразу скрытия формы
+          showThankYouAnimation();
+        } else {
+          showAlert(`Ошибка: ${result.error}`, 'Ошибка');
+        }
+      },
+      error: function(error) {
+        console.error('Error submitting poll:', error);
+        showAlert('Не удалось отправить ответы. Пожалуйста, попробуйте позже.', 'Ошибка');
+      }
+    });
+  };
+  
+  // Загрузка результатов опроса
+  const loadPollResults = () => {
+    $.ajax({
+      url: `${getApiPath()}/results/${publicLink}`,
+      method: 'GET',
+      success: function(result) {
+        if (result.success) {
+          renderPollResults(result.data);
+        } else {
+          pollResultsContainerEl.html(`<p class="error">Ошибка: ${result.error}</p>`);
+        }
+      },
+      error: function(error) {
+        console.error('Error loading poll results:', error);
+        pollResultsContainerEl.html('<p class="error">Не удалось загрузить результаты. Пожалуйста, попробуйте позже.</p>');
+      }
+    });
+  };
+  
+  // Отображение результатов опроса
+  const renderPollResults = (data) => {
+    pollResultsContainerEl.empty();
+    
+    // Добавляем заголовок с анимацией
+    const $resultsTitle = $('<h2>', {
+      text: 'Результаты опроса',
+      class: 'results-title'
+    });
+    
+    pollResultsContainerEl.append($resultsTitle);
+    
+    // Отображаем результаты с отложенной анимацией
+    data.questions.forEach((question, index) => {
+      setTimeout(() => {
+        const $questionResult = $('<div>', {
+          class: 'question-result',
+          css: { opacity: 0, transform: 'translateY(20px)' }
+        });
+        
+        const $questionTitle = $('<h3>', {
+          text: question.text,
+          class: 'question-title'
+        });
+        
+        $questionResult.append($questionTitle);
+        
+        // Отображаем результаты в зависимости от типа вопроса
+        switch (question.type) {
+          case 'single_choice':
+          case 'multiple_choice':
+            if (question.options && question.options.length > 0) {
+              // Находим общее количество голосов
+              const totalVotes = question.options.reduce((sum, option) => sum + (option.count || 0), 0);
+              
+              if (totalVotes > 0) {
+                question.options.forEach((option) => {
+                  const percent = totalVotes > 0 ? Math.round((option.count || 0) * 100 / totalVotes) : 0;
+                  
+                  const $optionResult = $('<div>', {
+                    class: 'result-bar-container'
+                  });
+                  
+                  const $optionLabel = $('<div>', {
+                    class: 'result-label',
+                    text: option.text
+                  });
+                  
+                  const $resultBar = $('<div>', {
+                    class: 'result-bar'
+                  });
+                  
+                  const $resultBarFill = $('<div>', {
+                    class: 'result-bar-fill',
+                    css: { width: '0%' }
+                  });
+                  
+                  const $resultPercent = $('<div>', {
+                    class: 'result-percent',
+                    text: `${percent}% (${option.count || 0} голосов)`
+                  });
+                  
+                  $resultBar.append($resultBarFill);
+                  $optionResult.append($optionLabel, $resultBar, $resultPercent);
+                  $questionResult.append($optionResult);
+                  
+                  // Анимируем заполнение полосы после добавления в DOM
+                  setTimeout(() => {
+                    $resultBarFill.css('width', `${percent}%`);
+                  }, 100);
+                });
+              } else {
+                $questionResult.append($('<p>', {
+                  text: 'Пока нет голосов для этого вопроса',
+                  class: 'no-results'
+                }));
+              }
+            }
+            break;
+            
+          case 'tag_cloud':
+            if (question.tags && question.tags.length > 0) {
+              const $tagCloud = $('<div>', {
+                class: 'results-tag-cloud'
+              });
+              
+              // Находим максимальное количество для масштабирования
+              const maxCount = Math.max(...question.tags.map(tag => tag.count || 1));
+              
+              question.tags.forEach((tag, tagIndex) => {
+                // Масштабируем размер тега от 1 до 3 в зависимости от частоты
+                const scale = maxCount > 1 ? 1 + (tag.count / maxCount) * 2 : 1;
+                
+                const $tag = $('<div>', {
+                  class: 'result-tag',
+                  text: tag.text,
+                  css: { 
+                    opacity: 0,
+                    transform: 'scale(0.5)',
+                    fontSize: `${scale}em`
+                  }
+                });
+                
+                $tagCloud.append($tag);
+                
+                // Анимируем появление тега с задержкой
+                setTimeout(() => {
+                  $tag.css({
+                    opacity: 1,
+                    transform: 'scale(1)'
+                  });
+                }, tagIndex * 100);
+              });
+              
+              $questionResult.append($tagCloud);
+            } else {
+              $questionResult.append($('<p>', {
+                text: 'Пока нет тегов для этого вопроса',
+                class: 'no-results'
+              }));
+            }
+            break;
+            
+          case 'scale':
+          case 'rating':
+            if (question.scaleValues && question.scaleValues.length > 0) {
+              const average = question.scaleAverage || 
+                (question.scaleValues.reduce((sum, val) => sum + val, 0) / question.scaleValues.length);
+              
+              const min = Math.min(...question.scaleValues);
+              const max = Math.max(...question.scaleValues);
+              
+              const $scaleAverage = $('<div>', {
+                class: 'scale-average',
+                html: `Средняя оценка: <span class="highlight">${average.toFixed(1)}</span> (мин: ${min}, макс: ${max})`
+              });
+              
+              $questionResult.append($scaleAverage);
+            } else {
+              $questionResult.append($('<p>', {
+                text: 'Пока нет оценок для этого вопроса',
+                class: 'no-results'
+              }));
+            }
+            break;
+            
+          case 'text':
+            if (question.answers && question.answers.length > 0) {
+              const $answersContainer = $('<div>', {
+                class: 'text-answers-container'
+              });
+              
+              question.answers.forEach((answer, answerIndex) => {
+                const $textAnswer = $('<div>', {
+                  class: 'text-answer',
+                  text: answer,
+                  css: { 
+                    opacity: 0,
+                    transform: 'translateX(20px)'
+                  }
+                });
+                
+                $answersContainer.append($textAnswer);
+                
+                // Анимируем появление ответа с задержкой
+                setTimeout(() => {
+                  $textAnswer.css({
+                    opacity: 1,
+                    transform: 'translateX(0)'
+                  });
+                }, answerIndex * 200);
+              });
+              
+              $questionResult.append($answersContainer);
+            } else {
+              $questionResult.append($('<p>', {
+                text: 'Пока нет текстовых ответов для этого вопроса',
+                class: 'no-results'
+              }));
+            }
+            break;
+        }
+        
+        pollResultsContainerEl.append($questionResult);
+        
+        // Анимируем появление блока результатов
+        setTimeout(() => {
+          $questionResult.css({
+            opacity: 1,
+            transform: 'translateY(0)'
+          });
+        }, 100);
+      }, index * 300); // Задержка между вопросами
+    });
+    
+    // Если нет данных для отображения
+    if (!data.questions || data.questions.length === 0) {
+      pollResultsContainerEl.append($('<p>', {
+        text: 'Нет данных для отображения',
+        class: 'no-results'
+      }));
+    }
+  };
+  
+  // Обработка отправки формы
+  formEl.on('submit', function(e) {
+    e.preventDefault();
+    
+    // Всегда используем навигацию через кнопки в пошаговом режиме
+    return false;
+  });
+  
+  // Инициализация
+  loadQuestionnaire();
+}); 
\ No newline at end of file