В сегодняшней статье мы детально разберем реализацию красивого и функционального виджета для голосования, который работает без перезагрузки страницы. Этот пример отлично демонстрирует, как заставить взаимодействовать фронтенд и бэкенд, создавая современный пользовательский опыт.
Создание готового виджета голосования на PHP, JavaScript и CSS
Это интерактивный виджет голосования, который позволяет пользователям отвечать на вопрос “Как вам наш новый дизайн?” вариантами “Нравится” или “Не нравится”. Ключевые особенности:
- Голосование без перезагрузки страницы (технология AJAX)
- Яркий, современный UI с анимациями и эффектами
- Автоматическое обновление результатов каждые 3 секунды (опрос сервера – polling)
- Защита от повторного голосования с использованием
localStorageи PHP-сессий - Адаптивный дизайн, работающий на любых устройствах
- Ведение лога всех голосов
Теперь давайте посмотрим на код и разберем каждый файл.
Архитектура проекта
Проект состоит из нескольких ключевых файлов:
get_results.php– Скрипт для отдачи текущих результатов.index.html– Структура и разметка виджета.results.txt– Текстовый файл, выступающий в роли простой базы данных.style.css– Стили, анимации и адаптивный дизайн.vote.php– Скрипт на серверной стороне для обработки голоса.voting.js– Вся клиентская логика на JavaScript.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 # Второе голосование
Общий анализ проекта
Это современная система голосования с следующими особенностями:
- Frontend: Красивый, адаптивный интерфейс с анимациями и эффектами
- Backend: Простая PHP-логика для обработки голосов
- Хранение данных: Текстовые файлы вместо базы данных
- Безопасность: Проверка сессий для предотвращения многократного голосования
- Логирование: Запись всех голосов с IP адресами
- Real-time: Автоматическое обновление результатов каждые 3 секунды
- Адаптивность: Поддержка мобильных устройств и темной темы
Проект демонстрирует хорошие практики веб-разработки с акцентом на пользовательский опыт.

