Создание виджета голосования на PHP и JS

Разработка готового виджета голосования на PHP, JavaScript и CSS

В сегодняшней статье мы детально разберем реализацию красивого и функционального виджета для голосования, который работает без перезагрузки страницы. Этот пример отлично демонстрирует, как заставить взаимодействовать фронтенд и бэкенд, создавая современный пользовательский опыт.

Создание готового виджета голосования на PHP, JavaScript и CSS

Это интерактивный виджет голосования, который позволяет пользователям отвечать на вопрос “Как вам наш новый дизайн?” вариантами “Нравится” или “Не нравится”. Ключевые особенности:

  • Голосование без перезагрузки страницы (технология AJAX)
  • Яркий, современный UI с анимациями и эффектами
  • Автоматическое обновление результатов каждые 3 секунды (опрос сервера – polling)
  • Защита от повторного голосования с использованием localStorage и PHP-сессий
  • Адаптивный дизайн, работающий на любых устройствах
  • Ведение лога всех голосов

Теперь давайте посмотрим на код и разберем каждый файл.

Архитектура проекта

Проект состоит из нескольких ключевых файлов:

  1. get_results.php – Скрипт для отдачи текущих результатов.
  2. index.html – Структура и разметка виджета.
  3. results.txt – Текстовый файл, выступающий в роли простой базы данных.
  4. style.css – Стили, анимации и адаптивный дизайн.
  5. vote.php – Скрипт на серверной стороне для обработки голоса.
  6. voting.js – Вся клиентская логика на JavaScript.
  7. voting_log.txt – Лог-файл для записи истории голосований.

📄 get_results.php           

<?php
// Устанавливаем заголовок для возврата JSON
header('Content-Type: application/json');
// Разрешаем CORS (доступ с любого домена)
header('Access-Control-Allow-Origin: *');

// Файл для хранения результатов в текстовом формате
$resultsFile = 'results.txt';

// Функция инициализации результатов (значения по умолчанию)
function initResults() {
    return ['yes' => 0, 'no' => 0];
}

// Функция чтения результатов из файла
function readResults() {
    global $resultsFile; // Используем глобальную переменную
    $results = initResults(); // Инициализируем массив результатов
    
    // Проверяем существование файла
    if (file_exists($resultsFile)) {
        // Читаем содержимое файла
        $content = file_get_contents($resultsFile);
        // Разбиваем на строки
        $lines = explode("\n", trim($content));
        
        // Обрабатываем каждую строку
        foreach ($lines as $line) {
            // Проверяем, содержит ли строка знак равенства
            if (strpos($line, '=') !== false) {
                // Разбиваем строку на ключ и значение
                list($key, $value) = explode('=', $line, 2);
                $key = trim($key); // Убираем пробелы
                $value = trim($value);
                
                // Если ключ существует в массиве результатов
                if (isset($results[$key])) {
                    // Приводим значение к integer
                    $results[$key] = (int)$value;
                }
            }
        }
    }
    
    return $results; // Возвращаем результаты
}

// Читаем текущие результаты
$results = readResults();

// Возвращаем JSON ответ
echo json_encode([
    'success' => true, // Флаг успешного выполнения
    'results' => $results, // Массив с результатами
    'last_update' => date('Y-m-d H:i:s') // Текущая дата/время
]);
?>

📄 index.html            

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Голосование | Современный виджет</title>
    <!-- Подключаем Font Awesome для иконок -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <!-- Подключаем шрифт Inter -->
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
    <!-- Подключаем CSS стили -->
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <div class="voting-widget">
            <!-- Заголовок -->
            <div class="widget-header">
                <div class="header-icon">
                    <i class="fas fa-vote-yea"></i> <!-- Иконка голосования -->
                </div>
                <h1 class="widget-title">Ваше мнение важно!</h1>
                <p class="widget-subtitle">Как вам наш новый дизайн?</p>
            </div>

            <!-- Кнопки голосования -->
            <div class="voting-actions">
                <button class="vote-btn yes-btn" onclick="vote('yes')">
                    <span class="btn-icon">
                        <i class="fas fa-thumbs-up"></i> <!-- Иконка "нравится" -->
                    </span>
                    <span class="btn-text">Нравится</span>
                    <span class="btn-ripple"></span> <!-- Эффект ripple -->
                </button>
                
                <button class="vote-btn no-btn" onclick="vote('no')">
                    <span class="btn-icon">
                        <i class="fas fa-thumbs-down"></i> <!-- Иконка "не нравится" -->
                    </span>
                    <span class="btn-text">Не нравится</span>
                    <span class="btn-ripple"></span> <!-- Эффект ripple -->
                </button>
            </div>

            <!-- Статус голосования (скрыт по умолчанию) -->
            <div class="vote-status" id="vote-status">
                <i class="fas fa-check-circle"></i> <!-- Иконка галочки -->
                <span>Вы уже проголосовали</span>
            </div>

            <!-- Результаты -->
            <div class="results-container">
                <div class="results-header">
                    <h3>Результаты голосования</h3>
                    <div class="live-badge">
                        <span class="live-dot"></span> <!-- Анимированная точка -->
                        LIVE <!-- Бейдж "в прямом эфире" -->
                    </div>
                </div>

                <div class="results-grid">
                    <!-- Прогресс бар для "За" -->
                    <div class="result-item">
                        <div class="result-label">
                            <span class="label-icon">
                                <i class="fas fa-thumbs-up"></i>
                            </span>
                            <span class="label-text">Нравится</span>
                            <span class="label-percentage" id="yes-percentage">0%</span>
                        </div>
                        <div class="progress-bar">
                            <div class="progress-fill yes-fill" id="yes-progress">
                                <div class="progress-shine"></div> <!-- Эффект блеска -->
                            </div>
                        </div>
                        <div class="result-count" id="yes-count">0 голосов</div>
                    </div>

                    <!-- Прогресс бар для "Против" -->
                    <div class="result-item">
                        <div class="result-label">
                            <span class="label-icon">
                                <i class="fas fa-thumbs-down"></i>
                            </span>
                            <span class="label-text">Не нравится</span>
                            <span class="label-percentage" id="no-percentage">0%</span>
                        </div>
                        <div class="progress-bar">
                            <div class="progress-fill no-fill" id="no-progress">
                                <div class="progress-shine"></div> <!-- Эффект блеска -->
                            </div>
                        </div>
                        <div class="result-count" id="no-count">0 голосов</div>
                    </div>
                </div>

                <!-- Общая статистика -->
                <div class="total-stats">
                    <div class="stat-item">
                        <i class="fas fa-users"></i> <!-- Иконка пользователей -->
                        <span>Всего голосов: <strong id="total-votes">0</strong></span>
                    </div>
                    <div class="stat-item">
                        <i class="fas fa-sync-alt"></i> <!-- Иконка обновления -->
                        <span>Обновлено: <strong id="last-update">только что</strong></span>
                    </div>
                </div>
            </div>
        </div>

        <!-- Футер -->
        <div class="widget-footer">
            <p>© 2024 Голосование • Обновляется в реальном времени</p>
        </div>
    </div>

    <!-- Подключаем JavaScript -->
    <script src="voting.js"></script>
</body>
</html>

📄 results.txt          

yes=2        # Количество голосов "За"
no=0         # Количество голосов "Против"
last_update=2025-09-17 17:27:29  # Время последнего обновления

📄 style.css         

/* Сброс стандартных стилей */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box; /* Учитываем padding и border в ширине */
}

/* Основные стили body */
body {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); /* Градиентный фон */
    min-height: 100vh; /* Минимальная высота = высоте viewport */
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    line-height: 1.6;
}

/* Контейнер */
.container {
    width: 100%;
    max-width: 500px; /* Максимальная ширина */
}

/* Основной виджет голосования */
.voting-widget {
    background: rgba(255, 255, 255, 0.95); /* Полупрозрачный белый */
    backdrop-filter: blur(20px); /* Размытие фона */
    border-radius: 24px; /* Скругленные углы */
    padding: 32px;
    box-shadow: 
        0 20px 40px rgba(0, 0, 0, 0.1), /* Тень */
        0 0 0 1px rgba(255, 255, 255, 0.1); /* Белая обводка */
    border: 1px solid rgba(255, 255, 255, 0.2); /* Полупрозрачная граница */
}

/* Заголовок виджета */
.widget-header {
    text-align: center;
    margin-bottom: 32px;
}

/* Иконка в заголовке */
.header-icon {
    width: 80px;
    height: 80px;
    margin: 0 auto 16px; /* Центрирование */
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); /* Градиент */
    border-radius: 50%; /* Круглая форма */
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 32px;
    color: white;
    box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3); /* Тень */
}

/* Заголовок */
.widget-title {
    font-size: 28px;
    font-weight: 700;
    color: #1a1a1a;
    margin-bottom: 8px;
}

/* Подзаголовок */
.widget-subtitle {
    font-size: 16px;
    color: #666;
    font-weight: 400;
}

/* Контейнер кнопок голосования */
.voting-actions {
    display: grid;
    grid-template-columns: 1fr 1fr; /* Две колонки */
    gap: 16px; /* Расстояние между кнопками */
    margin-bottom: 20px;
}

/* Стили кнопок голосования */
.vote-btn {
    position: relative;
    padding: 20px;
    border: none;
    border-radius: 16px;
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s ease; /* Плавные переходы */
    overflow: hidden; /* Скрываем выходящее за границы */
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px; /* Расстояние между элементами */
    min-height: 80px;
}

/* Стили кнопки "Да" */
.yes-btn {
    background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); /* Зеленый градиент */
    color: white;
    box-shadow: 0 8px 20px rgba(76, 175, 80, 0.3); /* Тень */
}

/* Стили кнопки "Нет" */
.no-btn {
    background: linear-gradient(135deg, #f44336 0%, #da190b 100%); /* Красный градиент */
    color: white;
    box-shadow: 0 8px 20px rgba(244, 67, 54, 0.3); /* Тень */
}

/* Эффект при наведении на кнопку */
.vote-btn:hover:not(:disabled) {
    transform: translateY(-2px); /* Поднимаем кнопку */
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.2); /* Увеличиваем тень */
}

/* Эффект при нажатии на кнопку */
.vote-btn:active:not(:disabled) {
    transform: translateY(0); /* Возвращаем на место */
}

/* Стили отключенной кнопки */
.vote-btn:disabled {
    opacity: 0.6;
    cursor: not-allowed; /* Курсор "недоступно" */
    transform: none;
}

/* Иконка в кнопке */
.btn-icon {
    font-size: 20px;
}

/* Эффект ripple (волны) */
.btn-ripple {
    position: absolute;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.3);
    transform: scale(0);
    animation: ripple 0.6s linear; /* Анимация ripple */
}

/* Анимация ripple эффекта */
@keyframes ripple {
    to {
        transform: scale(4);
        opacity: 0;
    }
}

/* Статус голосования */
.vote-status {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 15px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border-radius: 12px;
    margin-bottom: 20px;
    opacity: 0; /* Скрыт по умолчанию */
    transform: translateY(-10px); /* Сдвинут вверх */
    transition: all 0.3s ease; /* Плавное появление */
}

/* Класс для показа статуса */
.vote-status.show {
    opacity: 1;
    transform: translateY(0);
}

/* Иконка в статусе */
.vote-status i {
    font-size: 18px;
}

/* Контейнер результатов */
.results-container {
    background: rgba(248, 249, 250, 0.8); /* Полупрозрачный фон */
    border-radius: 16px;
    padding: 24px;
}

/* Заголовок результатов */
.results-header {
    display: flex;
    justify-content: space-between; /* Распределение по ширине */
    align-items: center;
    margin-bottom: 20px;
}

/* Заголовок h3 */
.results-header h3 {
    font-size: 18px;
    font-weight: 600;
    color: #1a1a1a;
}

/* Бейдж "LIVE" */
.live-badge {
    display: flex;
    align-items: center;
    gap: 6px;
    background: #dc3545; /* Красный фон */
    color: white;
    padding: 6px 12px;
    border-radius: 20px; /* Скругленные углы */
    font-size: 12px;
    font-weight: 600;
}

/* Анимированная точка в бейдже */
.live-dot {
    width: 8px;
    height: 8px;
    background: white;
    border-radius: 50%;
    animation: pulse 2s infinite; /* Анимация пульсации */
}

/* Анимация пульсации */
@keyframes pulse {
    0% { opacity: 1; }
    50% { opacity: 0.5; }
    100% { opacity: 1; }
}

/* Элемент результата */
.result-item {
    margin-bottom: 24px;
}

/* Метка результата */
.result-label {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 12px;
    font-weight: 500;
}

/* Иконка в метке */
.label-icon {
    font-size: 16px;
}

/* Процентное значение */
.label-percentage {
    margin-left: auto; /* Выравнивание вправо */
    font-weight: 700;
    color: #1a1a1a;
}

/* Прогресс-бар */
.progress-bar {
    width: 100%;
    height: 12px;
    background: rgba(0, 0, 0, 0.1); /* Фон прогресс-бара */
    border-radius: 10px;
    overflow: hidden; /* Скрываем выходящее за границы */
    position: relative;
}

/* Заполнение прогресс-бара */
.progress-fill {
    height: 100%;
    border-radius: 10px;
    position: relative;
    transition: width 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); /* Плавное изменение ширины */
    width: 0%; /* Начальная ширина */
}

/* Заполнение для "Да" */
.yes-fill {
    background: linear-gradient(90deg, #4CAF50, #45a049); /* Зеленый градиент */
}

/* Заполнение для "Нет" */
.no-fill {
    background: linear-gradient(90deg, #f44336, #da190b); /* Красный градиент */
}

/* Эффект блеска на прогресс-баре */
.progress-shine {
    position: absolute;
    top: 0;
    left: -100%;
    width: 50%;
    height: 100%;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
    animation: shine 2s infinite; /* Анимация блеска */
}

/* Анимация блеска */
@keyframes shine {
    0% { left: -100%; }
    100% { left: 200%; }
}

/* Счетчик голосов */
.result-count {
    font-size: 14px;
    color: #666;
    margin-top: 8px;
}

/* Общая статистика */
.total-stats {
    display: grid;
    grid-template-columns: 1fr 1fr; /* Две колонки */
    gap: 16px;
    padding-top: 20px;
    border-top: 1px solid rgba(0, 0, 0, 0.1); /* Разделительная линия */
}

/* Элемент статистики */
.stat-item {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 14px;
    color: #666;
}

/* Иконка в статистике */
.stat-item i {
    color: #667eea;
}

/* Футер */
.widget-footer {
    text-align: center;
    margin-top: 20px;
    color: rgba(255, 255, 255, 0.8); /* Полупрозрачный белый */
    font-size: 14px;
}

/* Адаптивность для мобильных устройств */
@media (max-width: 600px) {
    .voting-widget {
        padding: 24px;
        margin: 10px;
    }
    
    .voting-actions {
        grid-template-columns: 1fr; /* Одна колонка на мобильных */
    }
    
    .total-stats {
        grid-template-columns: 1fr; /* Одна колонка на мобильных */
    }
    
    .widget-title {
        font-size: 24px; /* Уменьшаем размер шрифта */
    }
    
    .header-icon {
        width: 60px;
        height: 60px;
        font-size: 24px; /* Уменьшаем иконку */
    }
}

/* Анимация появления виджета */
@keyframes slideIn {
    from {
        opacity: 0;
        transform: translateY(-20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* Применяем анимацию к виджету */
.voting-widget {
    animation: slideIn 0.6s ease;
}

/* Плавные переходы для кнопок */
.vote-btn {
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Плавные переходы для прогресс-баров */
.progress-fill {
    transition: width 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* Поддержка темной темы */
@media (prefers-color-scheme: dark) {
    .voting-widget {
        background: rgba(26, 26, 26, 0.95); /* Темный фон */
        color: white;
    }
    
    .widget-title {
        color: white;
    }
    
    .widget-subtitle {
        color: #ccc; /* Светло-серый текст */
    }
    
    .results-container {
        background: rgba(40, 40, 40, 0.8); /* Темный фон */
    }
    
    .results-header h3 {
        color: white;
    }
    
    .label-percentage {
        color: white;
    }
    
    .result-count {
        color: #ccc; /* Светло-серый текст */
    }
    
    .stat-item {
        color: #ccc; /* Светло-серый текст */
    }
    
    .progress-bar {
        background: rgba(255, 255, 255, 0.1); /* Светлый фон */
    }
}

📄 vote.php        

<?php
// Устанавливаем заголовки для JSON и CORS
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');

// Начинаем сессию для отслеживания голосований
session_start();

// Файлы для хранения данных
$resultsFile = 'results.txt';
$logFile = 'voting_log.txt';

// Функция инициализации результатов
function initResults() {
    return ['yes' => 0, 'no' => 0];
}

// Функция чтения результатов из файла
function readResults() {
    global $resultsFile;
    $results = initResults();
    
    if (file_exists($resultsFile)) {
        $content = file_get_contents($resultsFile);
        $lines = explode("\n", trim($content));
        
        foreach ($lines as $line) {
            if (strpos($line, '=') !== false) {
                list($key, $value) = explode('=', $line, 2);
                $key = trim($key);
                $value = trim($value);
                
                if (isset($results[$key])) {
                    $results[$key] = (int)$value;
                }
            }
        }
    }
    
    return $results;
}

// Функция записи результатов в файл
function writeResults($results) {
    global $resultsFile;
    
    $content = "yes=" . $results['yes'] . "\n";
    $content .= "no=" . $results['no'] . "\n";
    $content .= "last_update=" . date('Y-m-d H:i:s'); // Добавляем время обновления
    
    file_put_contents($resultsFile, $content);
}

// Функция логирования голосований
function logVote($choice, $ip) {
    global $logFile;
    
    $logEntry = date('Y-m-d H:i:s') . " | IP: " . $ip . " | Vote: " . $choice . "\n";
    // Записываем в файл с блокировкой
    file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);
}

// Функция проверки, голосовал ли уже пользователь
function hasUserVoted() {
    return isset($_SESSION['voted']) && $_SESSION['voted'] === true;
}

// Основная логика обработки POST запросов
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Парсим JSON входные данные
    $input = json_decode(file_get_contents('php://input'), true);
    
    // Если JSON не удалось распарсить, пробуем парсить как form-data
    if ($input === null) {
        parse_str(file_get_contents('php://input'), $input);
    }
    
    // Проверяем наличие и валидность голоса
    if (isset($input['vote']) && in_array($input['vote'], ['yes', 'no'])) {
        // Проверяем, не голосовал ли уже пользователь
        if (hasUserVoted()) {
            echo json_encode([
                'success' => false,
                'message' => 'Вы уже голосовали'
            ]);
            exit;
        }
        
        // Читаем текущие результаты
        $results = readResults();
        // Увеличиваем счетчик выбранного варианта
        $results[$input['vote']]++;
        
        // Записываем обновленные результаты
        writeResults($results);
        
        // Логируем голосование с IP адресом
        $ip = $_SERVER['REMOTE_ADDR'];
        logVote($input['vote'], $ip);
        
        // Отмечаем, что пользователь проголосовал
        $_SESSION['voted'] = true;
        
        // Возвращаем успешный ответ
        echo json_encode([
            'success' => true,
            'results' => $results
        ]);
    } else {
        // Неверный запрос
        echo json_encode([
            'success' => false,
            'message' => 'Неверный запрос'
        ]);
    }
} else {
    // Метод не разрешен
    echo json_encode([
        'success' => false,
        'message' => 'Метод не разрешен'
    ]);
}
?>

📄 voting.js          

// Класс для управления виджетом голосования
class VotingWidget {
    constructor() {
        // Проверяем, голосовал ли пользователь ранее (из localStorage)
        this.hasVoted = localStorage.getItem('hasVoted') === 'true';
        this.init(); // Инициализация
        this.startPolling(); // Запуск периодического обновления
    }

    // Инициализация виджета
    init() {
        if (this.hasVoted) {
            this.showVoteStatus(); // Показываем статус, если уже голосовали
            this.disableVoting(); // Отключаем кнопки
        }
        this.loadResults(); // Загружаем результаты
        this.setupRippleEffects(); // Настраиваем эффекты ripple
    }

    // Настройка ripple эффектов для кнопок
    setupRippleEffects() {
        document.querySelectorAll('.vote-btn').forEach(btn => {
            btn.addEventListener('click', function(e) {
                if (this.disabled) return; // Если кнопка отключена, выходим
                
                // Удаляем старый ripple эффект, если есть
                const ripple = this.querySelector('.btn-ripple');
                if (ripple) {
                    ripple.remove();
                }
                
                // Создаем новый ripple эффект
                const newRipple = document.createElement('span');
                newRipple.className = 'btn-ripple';
                
                // Рассчитываем позицию и размер
                const rect = this.getBoundingClientRect();
                const size = Math.max(rect.width, rect.height);
                const x = e.clientX - rect.left - size / 2;
                const y = e.clientY - rect.top - size / 2;
                
                // Устанавливаем стили
                newRipple.style.width = newRipple.style.height = size + 'px';
                newRipple.style.left = x + 'px';
                newRipple.style.top = y + 'px';
                
                // Добавляем эффект на кнопку
                this.appendChild(newRipple);
                
                // Удаляем эффект через 600ms
                setTimeout(() => {
                    if (newRipple.parentNode === this) {
                        this.removeChild(newRipple);
                    }
                }, 600);
            });
        });
    }

    // Функция голосования
    async vote(choice) {
        if (this.hasVoted) return; // Если уже голосовали, выходим

        try {
            // Визуальная обратная связь
            this.disableVoting();
            
            // Отправляем запрос на сервер
            const response = await fetch('vote.php', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                },
                body: `vote=${choice}` // Данные для отправки
            });

            const data = await response.json(); // Парсим ответ

            if (data.success) {
                // Если успешно, обновляем состояние
                this.hasVoted = true;
                localStorage.setItem('hasVoted', 'true'); // Сохраняем в localStorage
                this.showVoteStatus(); // Показываем статус
                this.updateResults(data.results); // Обновляем результаты
                
                // Меняем текст кнопок
                document.querySelectorAll('.btn-text').forEach(btn => {
                    btn.textContent = 'Проголосовано';
                });
            }
        } catch (error) {
            console.error('Error:', error);
            this.enableVoting(); // Включаем кнопки при ошибке
        }
    }

    // Загрузка результатов с сервера
    async loadResults() {
        try {
            const response = await fetch('get_results.php');
            const data = await response.json();
            
            if (data.success) {
                this.updateResults(data.results); // Обновляем результаты
                this.updateLastUpdate(); // Обновляем время
            }
        } catch (error) {
            console.error('Error loading results:', error);
        }
    }

    // Обновление отображения результатов
    updateResults(results) {
        const total = results.yes + results.no; // Общее количество голосов
        const yesPercentage = total > 0 ? Math.round((results.yes / total) * 100) : 0; // Процент "Да"
        const noPercentage = total > 0 ? Math.round((results.no / total) * 100) : 0; // Процент "Нет"

        // Анимируем прогресс бары
        document.getElementById('yes-progress').style.width = yesPercentage + '%';
        document.getElementById('no-progress').style.width = noPercentage + '%';
        
        // Обновляем текстовые значения
        document.getElementById('yes-percentage').textContent = yesPercentage + '%';
        document.getElementById('no-percentage').textContent = noPercentage + '%';
        document.getElementById('yes-count').textContent = this.formatVotes(results.yes);
        document.getElementById('no-count').textContent = this.formatVotes(results.no);
        document.getElementById('total-votes').textContent = this.formatVotes(total);
    }

    // Форматирование чисел (разделители тысяч)
    formatVotes(count) {
        return count.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
    }

    // Обновление времени последнего обновления
    updateLastUpdate() {
        const now = new Date();
        const timeString = now.toLocaleTimeString('ru-RU', {
            hour: '2-digit',
            minute: '2-digit'
        });
        document.getElementById('last-update').textContent = timeString;
    }

    // Показать статус голосования
    showVoteStatus() {
        const status = document.getElementById('vote-status');
        status.classList.add('show'); // Добавляем класс для показа
    }

    // Отключить кнопки голосования
    disableVoting() {
        document.querySelectorAll('.vote-btn').forEach(btn => {
            btn.disabled = true;
            btn.style.opacity = '0.7';
            btn.style.cursor = 'not-allowed';
            btn.style.transform = 'none';
        });
    }

    // Включить кнопки голосования
    enableVoting() {
        document.querySelectorAll('.vote-btn').forEach(btn => {
            btn.disabled = false;
            btn.style.opacity = '1';
            btn.style.cursor = 'pointer';
        });
    }

    // Запуск периодического обновления результатов
    startPolling() {
        // Обновляем результаты каждые 3 секунды
        setInterval(() => {
            this.loadResults();
        }, 3000);
    }
}

// Глобальная функция для вызова из HTML
function vote(choice) {
    if (!window.votingWidget) {
        window.votingWidget = new VotingWidget();
    }
    window.votingWidget.vote(choice);
}

// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
    window.votingWidget = new VotingWidget();
});

📄 voting_log.txt

2025-09-17 17:27:16 | IP: 127.0.0.1 | Vote: yes  # Первое голосование
2025-09-17 17:27:29 | IP: 127.0.0.2 | Vote: yes  # Второе голосование

Общий анализ проекта

Это современная система голосования с следующими особенностями:

  1. Frontend: Красивый, адаптивный интерфейс с анимациями и эффектами
  2. Backend: Простая PHP-логика для обработки голосов
  3. Хранение данных: Текстовые файлы вместо базы данных
  4. Безопасность: Проверка сессий для предотвращения многократного голосования
  5. Логирование: Запись всех голосов с IP адресами
  6. Real-time: Автоматическое обновление результатов каждые 3 секунды
  7. Адаптивность: Поддержка мобильных устройств и темной темы

Проект демонстрирует хорошие практики веб-разработки с акцентом на пользовательский опыт.

Звонок WhatsApp Telegram VK