denisgay/public/index.html
2025-11-12 23:53:05 +07:00

746 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Цитатник Дениса Шулепова</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
}
header {
text-align: center;
color: white;
margin-bottom: 40px;
padding: 30px 20px;
}
h1 {
font-size: 3rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
}
.rainbow-flag {
display: flex;
height: 8px;
margin: 20px 0;
border-radius: 4px;
overflow: hidden;
}
.rainbow-flag div {
flex: 1;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 30px;
}
button {
background: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.2);
}
button:active {
transform: translateY(0);
}
.quote-card {
background: white;
border-radius: 15px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
transition: transform 0.3s;
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.quote-card:hover {
transform: translateY(-5px);
}
.quote-text {
font-size: 1.3rem;
line-height: 1.6;
color: #333;
margin-bottom: 15px;
font-style: italic;
}
.quote-timestamp {
color: #667eea;
font-size: 0.9rem;
font-weight: 600;
}
.reactions-container {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #eee;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.reaction-btn {
background: #f5f5f5;
border: 2px solid transparent;
padding: 8px 15px;
border-radius: 20px;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 5px;
}
.reaction-btn:hover {
background: #667eea;
border-color: #667eea;
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.reaction-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.reaction-count {
font-size: 0.9rem;
font-weight: 600;
min-width: 20px;
text-align: center;
}
.add-reaction-btn {
background: white;
border: 2px dashed #ccc;
padding: 8px 15px;
border-radius: 20px;
font-size: 1.1rem;
cursor: pointer;
transition: all 0.2s;
}
.add-reaction-btn:hover {
border-color: #667eea;
background: #f8f9ff;
}
.emoji-picker-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
animation: fadeIn 0.2s;
padding: 20px;
overflow-y: auto;
}
.emoji-picker-overlay.show {
display: flex;
justify-content: center;
align-items: center;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.emoji-picker {
background: white;
border-radius: 20px;
padding: 25px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
max-width: 400px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
animation: slideUp 0.3s;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.emoji-picker-title {
font-size: 1.3rem;
font-weight: 600;
color: #333;
margin-bottom: 20px;
text-align: center;
}
.emoji-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
margin-bottom: 15px;
}
.emoji-option {
background: #f5f5f5;
border: 2px solid transparent;
padding: 15px;
border-radius: 12px;
font-size: 2rem;
cursor: pointer;
transition: all 0.2s;
text-align: center;
}
.emoji-option:hover {
background: #667eea;
transform: scale(1.15);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.emoji-picker-close {
width: 100%;
background: #f5f5f5;
color: #666;
padding: 12px;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.emoji-picker-close:hover {
background: #e0e0e0;
}
.comments-section {
margin-top: 20px;
padding-top: 20px;
border-top: 2px solid #f0f0f0;
}
.comments-title {
font-size: 1.1rem;
font-weight: 600;
color: #667eea;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 8px;
}
.comment {
background: #f8f9ff;
border-radius: 12px;
padding: 15px;
margin-bottom: 12px;
border-left: 3px solid #667eea;
}
.comment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.comment-author {
font-weight: 600;
color: #667eea;
font-size: 0.95rem;
}
.comment-timestamp {
font-size: 0.8rem;
color: #999;
}
.comment-text {
color: #333;
line-height: 1.5;
font-size: 0.95rem;
margin-bottom: 10px;
}
.comment-reactions {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
margin-top: 8px;
}
.comment-reaction-btn {
background: #fff;
border: 1px solid #e0e0e0;
padding: 4px 10px;
border-radius: 15px;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 4px;
}
.comment-reaction-btn:hover {
background: #667eea;
border-color: #667eea;
color: white;
transform: scale(1.05);
}
.comment-reaction-count {
font-size: 0.8rem;
font-weight: 600;
}
.add-comment-reaction-btn {
background: white;
border: 1px dashed #ccc;
padding: 4px 10px;
border-radius: 15px;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
}
.add-comment-reaction-btn:hover {
border-color: #667eea;
background: #f8f9ff;
}
.comment-form {
margin-top: 15px;
display: flex;
flex-direction: column;
gap: 10px;
}
.comment-input {
padding: 10px 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 0.95rem;
font-family: inherit;
transition: border-color 0.2s;
}
.comment-input:focus {
outline: none;
border-color: #667eea;
}
.comment-textarea {
min-height: 80px;
resize: vertical;
}
.comment-submit {
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
border-radius: 10px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
align-self: flex-end;
}
.comment-submit:hover {
background: #5568d3;
transform: translateY(-1px);
}
.no-comments {
color: #999;
font-style: italic;
font-size: 0.9rem;
text-align: center;
padding: 15px;
}
.loading {
text-align: center;
color: white;
font-size: 1.2rem;
padding: 40px;
}
.no-quotes {
text-align: center;
color: white;
font-size: 1.2rem;
padding: 40px;
background: rgba(255,255,255,0.1);
border-radius: 15px;
}
.spinner {
border: 3px solid rgba(255,255,255,0.3);
border-top: 3px solid white;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🏳️‍🌈 Цитатник Дениса Шулепова</h1>
<div class="rainbow-flag">
<div style="background: #e40303;"></div>
<div style="background: #ff8c00;"></div>
<div style="background: #ffed00;"></div>
<div style="background: #008026;"></div>
<div style="background: #24408e;"></div>
<div style="background: #732982;"></div>
</div>
</header>
<div class="controls">
<button onclick="loadQuotes()">🔄 Обновить</button>
</div>
<div id="quotes-container"></div>
</div>
<div id="emoji-picker-overlay" class="emoji-picker-overlay" onclick="closeEmojiPicker(event)">
<div class="emoji-picker" onclick="event.stopPropagation()">
<div class="emoji-picker-title">Выберите реакцию</div>
<div class="emoji-grid" id="emoji-grid"></div>
<button class="emoji-picker-close" onclick="closeEmojiPicker()">Закрыть</button>
</div>
</div>
<script>
async function loadQuotes() {
const container = document.getElementById('quotes-container');
container.innerHTML = '<div class="loading"><div class="spinner"></div>Загрузка цитат...</div>';
try {
const response = await fetch('/api/quotes');
const quotes = await response.json();
if (quotes.length === 0) {
container.innerHTML = '<div class="no-quotes">Пока нет цитат. Нажмите "Создать новую цитату"!</div>';
return;
}
container.innerHTML = quotes.map((q, index) => {
const date = new Date(q.timestamp);
const formattedDate = date.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
const reactions = q.reactions || {};
const reactionsHtml = Object.entries(reactions).map(([emoji, count]) =>
`<button class="reaction-btn" onclick="addReaction(${index}, '${emoji}')">
<span>${emoji}</span>
<span class="reaction-count">${count}</span>
</button>`
).join('');
const comments = q.comments || [];
const commentsHtml = comments.length > 0
? comments.map(c => {
const commentDate = new Date(c.timestamp);
const commentFormatted = commentDate.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
const commentReactions = c.reactions || {};
const commentReactionsHtml = Object.entries(commentReactions).map(([emoji, count]) =>
`<button class="comment-reaction-btn" onclick="addCommentReaction(${index}, ${c.id}, '${emoji}')">
<span>${emoji}</span>
<span class="comment-reaction-count">${count}</span>
</button>`
).join('');
return `
<div class="comment">
<div class="comment-header">
<span class="comment-author">${escapeHtml(c.author)}</span>
<span class="comment-timestamp">${commentFormatted}</span>
</div>
<div class="comment-text">${escapeHtml(c.text)}</div>
<div class="comment-reactions">
${commentReactionsHtml}
<button class="add-comment-reaction-btn" onclick="showCommentReactionPicker(${index}, ${c.id})"></button>
</div>
</div>
`;
}).join('')
: '<div class="no-comments">Пока нет комментариев. Будьте первым!</div>';
return `
<div class="quote-card">
<img src="photo_2023-10-27_20-55-45.jpg" alt="Денис Шулепов" style="width: 80px; height: 80px; border-radius: 50%; object-fit: cover; float: left; margin-right: 20px; border: 3px solid #667eea;">
<div class="quote-text">"${q.quote}"</div>
<div class="quote-timestamp">📅 ${formattedDate}</div>
<div class="reactions-container">
${reactionsHtml}
<button class="add-reaction-btn" onclick="showReactionPicker(${index})"></button>
</div>
<div class="comments-section">
<div class="comments-title">💬 Комментарии (${comments.length})</div>
${commentsHtml}
<form class="comment-form" onsubmit="addComment(event, ${index})">
<input type="text" class="comment-input" placeholder="Ваше имя" name="author" required maxlength="50">
<textarea class="comment-input comment-textarea" placeholder="Напишите комментарий..." name="text" required maxlength="500"></textarea>
<button type="submit" class="comment-submit">Отправить</button>
</form>
</div>
</div>
`;
}).join('');
} catch (error) {
container.innerHTML = '<div class="no-quotes">❌ Ошибка загрузки цитат</div>';
console.error('Error loading quotes:', error);
}
}
async function generateQuote() {
const container = document.getElementById('quotes-container');
const originalContent = container.innerHTML;
container.innerHTML = '<div class="loading"><div class="spinner"></div>Генерируем новую цитату...</div>';
try {
const response = await fetch('/api/generate', {
method: 'POST'
});
if (response.ok) {
setTimeout(() => loadQuotes(), 1000);
} else {
throw new Error('Failed to generate quote');
}
} catch (error) {
container.innerHTML = originalContent;
alert('❌ Ошибка генерации цитаты. Проверьте настройки API.');
console.error('Error generating quote:', error);
}
}
async function addReaction(index, emoji) {
try {
const response = await fetch(`/api/quotes/${index}/react`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ emoji })
});
if (response.ok) {
loadQuotes();
} else {
throw new Error('Failed to add reaction');
}
} catch (error) {
console.error('Error adding reaction:', error);
alert('❌ Ошибка добавления реакции');
}
}
let currentQuoteIndex = null;
let currentCommentId = null;
let pickerMode = 'quote'; // 'quote' or 'comment'
function showReactionPicker(index) {
currentQuoteIndex = index;
currentCommentId = null;
pickerMode = 'quote';
const emojis = ['❤️', '😍', '🔥', '👏', '🌈', '💅', '✨', '💖', '😂', '👑', '🥰', '💋', '🎉', '⭐', '💜'];
const emojiGrid = document.getElementById('emoji-grid');
emojiGrid.innerHTML = emojis.map(emoji =>
`<button class="emoji-option" onclick="selectEmoji('${emoji}')">${emoji}</button>`
).join('');
document.getElementById('emoji-picker-overlay').classList.add('show');
}
function showCommentReactionPicker(quoteIndex, commentId) {
currentQuoteIndex = quoteIndex;
currentCommentId = commentId;
pickerMode = 'comment';
const emojis = ['❤️', '😍', '🔥', '👏', '🌈', '💅', '✨', '💖', '😂', '👑', '🥰', '💋', '🎉', '⭐', '💜'];
const emojiGrid = document.getElementById('emoji-grid');
emojiGrid.innerHTML = emojis.map(emoji =>
`<button class="emoji-option" onclick="selectEmoji('${emoji}')">${emoji}</button>`
).join('');
document.getElementById('emoji-picker-overlay').classList.add('show');
}
function closeEmojiPicker(event) {
if (event && event.target.id !== 'emoji-picker-overlay') {
return;
}
document.getElementById('emoji-picker-overlay').classList.remove('show');
currentQuoteIndex = null;
currentCommentId = null;
pickerMode = 'quote';
}
function selectEmoji(emoji) {
if (pickerMode === 'quote' && currentQuoteIndex !== null) {
addReaction(currentQuoteIndex, emoji);
} else if (pickerMode === 'comment' && currentQuoteIndex !== null && currentCommentId !== null) {
addCommentReaction(currentQuoteIndex, currentCommentId, emoji);
}
document.getElementById('emoji-picker-overlay').classList.remove('show');
currentQuoteIndex = null;
currentCommentId = null;
pickerMode = 'quote';
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async function addComment(event, index) {
event.preventDefault();
const form = event.target;
const author = form.author.value.trim();
const text = form.text.value.trim();
if (!author || !text) {
alert('Пожалуйста, заполните все поля');
return;
}
try {
const response = await fetch(`/api/quotes/${index}/comment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ author, text })
});
if (response.ok) {
form.reset();
loadQuotes();
} else {
throw new Error('Failed to add comment');
}
} catch (error) {
console.error('Error adding comment:', error);
alert('❌ Ошибка добавления комментария');
}
}
async function addCommentReaction(quoteIndex, commentId, emoji) {
try {
const response = await fetch(`/api/quotes/${quoteIndex}/comment/${commentId}/react`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ emoji })
});
if (response.ok) {
loadQuotes();
} else {
throw new Error('Failed to add comment reaction');
}
} catch (error) {
console.error('Error adding comment reaction:', error);
alert('❌ Ошибка добавления реакции на комментарий');
}
}
loadQuotes();
setInterval(loadQuotes, 60000);
</script>
</body>
</html>