Общественное молодежное движение Псковской области «ЛИГА МОЛОДЕЖИ» совместно с Фондом «Собор попечителей памяти Великой Победы» приглашает молодежь для участия в Третьем молодежном патриотическом медиафоруме «Рассказываем о подвиге», который состоится с 11.00 до 13.00 5 декабря 2024 года в актовом зале Псковского областного колледжа искусств имени Н.А. Римского-Корсакова по адресу: г. Псков, ул. Воеводы Шуйского, д. 2.
Форум посвящен 15-й годовщине Указа Президента РФ от 05.12.2009 N 1387 "О ПРИСВОЕНИИ Г. ПСКОВУ ПОЧЕТНОГО ЗВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ "ГОРОД ВОИНСКОЙ СЛАВЫ" и Международному дню добровольцев.
На площадке Форума состоится презентация молодежного СМИ МАИ ЛИГА.ОНЛАЙН. Все участники мероприятия получат сувенирную продукцию проекта. Участники мероприятия, зарегистрированные через площадку DOBRO.RU по ссылке, как волонтеры мероприятия, получать волонтерские часы.
На мероприятие приглашены представители региональной и муниципальной власти, участники СВО и СМИ.
Форум посвящен 15-й годовщине Указа Президента РФ от 05.12.2009 N 1387 "О ПРИСВОЕНИИ Г. ПСКОВУ ПОЧЕТНОГО ЗВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ "ГОРОД ВОИНСКОЙ СЛАВЫ" и Международному дню добровольцев.
На площадке Форума состоится презентация молодежного СМИ МАИ ЛИГА.ОНЛАЙН. Все участники мероприятия получат сувенирную продукцию проекта. Участники мероприятия, зарегистрированные через площадку DOBRO.RU по ссылке, как волонтеры мероприятия, получать волонтерские часы.
На мероприятие приглашены представители региональной и муниципальной власти, участники СВО и СМИ.
<!-- Начало кода формы голосования для Tilda с сохранением на сервер -->
<div class="poll-container" id="pollContainer">
<div class="poll-header">
<h3 class="poll-question">Вы поддерживаете изменения на нашем сайте?</h3>
<div class="poll-stats">
<span id="totalVotes">0</span> голосов
</div>
</div>
<div class="poll-options">
<div class="poll-option">
<input type="radio" id="option1" name="poll" value="yes">
<label for="option1" class="poll-label">
<span class="option-text">Да</span>
<span class="option-bar">
<span class="option-fill" data-width="0"></span>
</span>
<span class="option-percent">0%</span>
</label>
</div>
<div class="poll-option">
<input type="radio" id="option2" name="poll" value="no">
<label for="option2" class="poll-label">
<span class="option-text">Нет</span>
<span class="option-bar">
<span class="option-fill" data-width="0"></span>
</span>
<span class="option-percent">0%</span>
</label>
</div>
<div class="poll-option">
<input type="radio" id="option3" name="poll" value="neutral">
<label for="option3" class="poll-label">
<span class="option-text">Всё равно</span>
<span class="option-bar">
<span class="option-fill" data-width="0"></span>
</span>
<span class="option-percent">0%</span>
</label>
</div>
</div>
<div class="poll-actions">
<button class="poll-btn" id="voteBtn">Проголосовать</button>
<div class="poll-message" id="pollMessage"></div>
<div class="poll-loading" id="pollLoading" style="display: none;">
<div class="loading-spinner"></div>
<span>Отправка данных...</span>
</div>
</div>
<div class="poll-footer">
<div class="votes-left">Осталось голосов: <span id="votesLeft">3</span></div>
</div>
</div>
<style>
/* Стили для контейнера опроса */
.poll-container {
background: #ffffff;
border: 1px solid #e8ecef;
border-radius: 12px;
padding: 24px;
margin: 20px 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
transition: all 0.3s ease;
}
.poll-container:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
/* Заголовок опроса */
.poll-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.poll-question {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #2d3748;
line-height: 1.4;
}
.poll-stats {
font-size: 14px;
color: #718096;
background: #f7fafc;
padding: 4px 12px;
border-radius: 20px;
}
/* Варианты ответов */
.poll-options {
margin-bottom: 20px;
}
.poll-option {
margin-bottom: 12px;
}
.poll-option input[type="radio"] {
display: none;
}
.poll-label {
display: flex;
align-items: center;
padding: 12px 16px;
background: #f8f9fa;
border: 2px solid transparent;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.poll-label:hover {
background: #e9ecef;
border-color: #dee2e6;
}
.poll-option input[type="radio"]:checked + .poll-label {
background: #e3f2fd;
border-color: #2196f3;
}
.option-text {
flex: 0 0 100px;
font-weight: 500;
color: #4a5568;
}
.option-bar {
flex: 1;
height: 8px;
background: #e2e8f0;
border-radius: 4px;
margin: 0 16px;
overflow: hidden;
position: relative;
}
.option-fill {
display: block;
height: 100%;
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
border-radius: 4px;
width: 0%;
transition: width 1s ease-in-out;
}
.option-percent {
flex: 0 0 40px;
text-align: right;
font-weight: 600;
color: #4a5568;
font-size: 14px;
}
/* Кнопки и действия */
.poll-actions {
text-align: center;
}
.poll-btn {
background: #2196f3;
color: white;
border: none;
padding: 12px 32px;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.poll-btn:hover {
background: #1976d2;
transform: translateY(-1px);
}
.poll-btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.poll-message {
margin-top: 12px;
font-size: 14px;
min-height: 20px;
}
.poll-message.success {
color: #10b981;
}
.poll-message.error {
color: #ef4444;
}
/* Индикатор загрузки */
.poll-loading {
display: flex;
align-items: center;
justify-content: center;
margin-top: 12px;
font-size: 14px;
color: #666;
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #2196f3;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Футер */
.poll-footer {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
text-align: center;
font-size: 14px;
color: #718096;
}
/* Адаптивность */
@media (max-width: 480px) {
.poll-container {
padding: 16px;
}
.poll-header {
flex-direction: column;
align-items: flex-start;
}
.poll-stats {
margin-top: 8px;
align-self: flex-end;
}
.poll-label {
flex-direction: column;
align-items: flex-start;
}
.option-bar {
width: 100%;
margin: 8px 0;
}
.option-percent {
align-self: flex-end;
}
}
</style>
<script>
// Конфигурация для отправки на сервер
const SERVER_CONFIG = {
// === НАСТРОЙКИ GOOGLE FORMS ===
useGoogleForms: true, // true - использовать Google Forms, false - использовать другой сервер
googleFormId: 'YOUR_GOOGLE_FORM_ID_HERE', // Замените на ID вашей Google Forms
questionField: 'entry.123456789', // Замените на ID поля вопроса
// === НАСТРОЙКИ ДЛЯ СВОЕГО СЕРВЕРА ===
serverUrl: 'https://your-server.com/api/vote', // URL вашего сервера
method: 'POST', // Метод запроса
// === ОБЩИЕ НАСТРОЙКИ ===
enableServerSync: true, // Включить синхронизацию с сервером
retryAttempts: 3 // Количество попыток повторной отправки при ошибке
};
document.addEventListener('DOMContentLoaded', function() {
const pollContainer = document.getElementById('pollContainer');
const voteBtn = document.getElementById('voteBtn');
const pollMessage = document.getElementById('pollMessage');
const pollLoading = document.getElementById('pollLoading');
const votesLeftEl = document.getElementById('votesLeft');
const totalVotesEl = document.getElementById('totalVotes');
// Ключи для localStorage
const STORAGE_KEYS = {
VOTES: 'website_poll_votes',
USER_VOTES: 'website_poll_user_votes',
RESULTS: 'website_poll_results',
PENDING_SYNC: 'website_poll_pending_sync'
};
// Инициализация опроса
function initializePoll() {
let results = JSON.parse(localStorage.getItem(STORAGE_KEYS.RESULTS)) || {
yes: 0,
no: 0,
neutral: 0
};
let userVotes = parseInt(localStorage.getItem(STORAGE_KEYS.USER_VOTES)) || 0;
updateResults(results);
updateVotesLeft(userVotes);
// Проверяем pending sync при загрузке
checkPendingSync();
return { results, userVotes };
}
// Проверка ожидающих синхронизаций
function checkPendingSync() {
if (!SERVER_CONFIG.enableServerSync) return;
const pendingSync = JSON.parse(localStorage.getItem(STORAGE_KEYS.PENDING_SYNC)) || [];
if (pendingSync.length > 0) {
console.log('Найдены голоса, ожидающие синхронизации:', pendingSync.length);
// Можно добавить автоматическую повторную отправку
}
}
// Обновление результатов на экране
function updateResults(results) {
const totalVotes = results.yes + results.no + results.neutral;
totalVotesEl.textContent = totalVotes;
updateOptionResult('yes', results.yes, totalVotes);
updateOptionResult('no', results.no, totalVotes);
updateOptionResult('neutral', results.neutral, totalVotes);
}
function updateOptionResult(option, votes, totalVotes) {
const percent = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;
const optionElement = document.querySelector(`input[value="${option}"]`);
if (optionElement) {
const label = optionElement.nextElementSibling;
const percentEl = label.querySelector('.option-percent');
const fillEl = label.querySelector('.option-fill');
percentEl.textContent = percent + '%';
fillEl.style.width = percent + '%';
fillEl.setAttribute('data-width', percent);
}
}
function updateVotesLeft(userVotes) {
const votesLeft = Math.max(0, 3 - userVotes);
votesLeftEl.textContent = votesLeft;
if (votesLeft === 0) {
voteBtn.disabled = true;
voteBtn.textContent = 'Лимит голосов исчерпан';
showMessage('Вы исчерпали лимит голосов (3 голоса)', 'error');
} else {
voteBtn.disabled = false;
voteBtn.textContent = 'Проголосовать';
}
}
function showMessage(text, type) {
pollMessage.textContent = text;
pollMessage.className = 'poll-message ' + (type || '');
setTimeout(() => {
pollMessage.textContent = '';
pollMessage.className = 'poll-message';
}, 5000);
}
function showLoading(show) {
pollLoading.style.display = show ? 'flex' : 'none';
voteBtn.disabled = show;
}
// ===== ФУНКЦИИ ДЛЯ ОТПРАВКИ НА СЕРВЕР =====
// Отправка в Google Forms
async function sendToGoogleForms(voteData) {
if (!SERVER_CONFIG.useGoogleForms || !SERVER_CONFIG.googleFormId) {
throw new Error('Google Forms не настроен');
}
const formData = new FormData();
formData.append(SERVER_CONFIG.questionField, voteData.answer);
formData.append('submit', 'Submit');
const response = await fetch(
`https://docs.google.com/forms/d/e/${SERVER_CONFIG.googleFormId}/formResponse`,
{
method: 'POST',
body: formData,
mode: 'no-cors'
}
);
// В режиме no-cors мы не можем проверить ответ, поэтому считаем успешным
return { success: true };
}
// Отправка на кастомный сервер
async function sendToCustomServer(voteData) {
const response = await fetch(SERVER_CONFIG.serverUrl, {
method: SERVER_CONFIG.method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
question: "Вы поддерживаете изменения на нашем сайте?",
answer: voteData.answer,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
})
});
if (!response.ok) {
throw new Error(`Ошибка сервера: ${response.status}`);
}
return await response.json();
}
// Основная функция отправки
async function sendVoteToServer(voteData) {
if (!SERVER_CONFIG.enableServerSync) {
return { success: true, skipped: true };
}
try {
let result;
if (SERVER_CONFIG.useGoogleForms) {
result = await sendToGoogleForms(voteData);
} else {
result = await sendToCustomServer(voteData);
}
// Удаляем из pending sync при успешной отправке
removeFromPendingSync(voteData);
return result;
} catch (error) {
// Сохраняем в pending sync для повторной отправки
addToPendingSync(voteData);
throw error;
}
}
// Работа с pending sync
function addToPendingSync(voteData) {
const pending = JSON.parse(localStorage.getItem(STORAGE_KEYS.PENDING_SYNC)) || [];
pending.push({
...voteData,
timestamp: new Date().toISOString(),
retryCount: 0
});
localStorage.setItem(STORAGE_KEYS.PENDING_SYNC, JSON.stringify(pending));
}
function removeFromPendingSync(voteData) {
const pending = JSON.parse(localStorage.getItem(STORAGE_KEYS.PENDING_SYNC)) || [];
const updated = pending.filter(item =>
!(item.answer === voteData.answer && item.timestamp === voteData.timestamp)
);
localStorage.setItem(STORAGE_KEYS.PENDING_SYNC, JSON.stringify(updated));
}
// Обработчик голосования
voteBtn.addEventListener('click', async function() {
const selectedOption = document.querySelector('input[name="poll"]:checked');
if (!selectedOption) {
showMessage('Пожалуйста, выберите вариант ответа', 'error');
return;
}
const { results, userVotes } = initializePoll();
if (userVotes >= 3) {
showMessage('Вы исчерпали лимит голосов', 'error');
return;
}
// Локальное обновление
results[selectedOption.value]++;
localStorage.setItem(STORAGE_KEYS.RESULTS, JSON.stringify(results));
localStorage.setItem(STORAGE_KEYS.USER_VOTES, userVotes + 1);
updateResults(results);
updateVotesLeft(userVotes + 1);
// Подготовка данных для отправки
const voteData = {
answer: selectedOption.value,
answerText: getAnswerText(selectedOption.value),
timestamp: new Date().toISOString()
};
// Отправка на сервер
showLoading(true);
try {
await sendVoteToServer(voteData);
showMessage('Спасибо за ваш голос! Данные сохранены.', 'success');
} catch (error) {
console.error('Ошибка отправки:', error);
showMessage('Голос учтен локально. Ошибка связи с сервером.', 'error');
} finally {
showLoading(false);
selectedOption.checked = false;
setTimeout(animateBars, 100);
}
});
function getAnswerText(value) {
const texts = {
yes: 'Да',
no: 'Нет',
neutral: 'Всё равно'
};
return texts[value] || value;
}
function animateBars() {
const fillElements = document.querySelectorAll('.option-fill');
fillElements.forEach(fill => {
const targetWidth = fill.getAttribute('data-width');
fill.style.width = targetWidth + '%';
});
}
// Инициализация
initializePoll();
});
</script>
<!-- Конец кода -->