FEATURE: added pols to frontend

This commit is contained in:
Your Name 2026-07-22 14:52:55 +03:00
parent c0391f8e69
commit b5982063c4
6 changed files with 83 additions and 78 deletions

BIN
chat.db

Binary file not shown.

View File

@ -523,7 +523,6 @@
let pollInterval = null;
let selectedGifUrl = null;
let votedPolls = new Set();
let pollsData = [];
function loadVotedPolls() {
try {
@ -711,7 +710,7 @@
if (data.success) {
showToast('Poll created!', 'success');
pollModal.classList.remove('open');
loadPolls();
loadMessages();
} else {
showToast(data.error || 'Failed to create poll', 'error');
}
@ -745,57 +744,6 @@
renderChannels();
messagesEl.innerHTML = '<div class="empty-state">Loading...</div>';
loadMessages();
loadPolls();
}
async function loadPolls() {
try {
const r = await fetch(`/api/polls?channel=${currentChannel}`);
const data = await r.json();
if (data.success) pollsData = data.polls || [];
else pollsData = [];
} catch (e) {
pollsData = [];
}
}
function renderPolls() {
if (!pollsData.length) return '';
let html = '';
pollsData.forEach(poll => {
const isClosed = poll.status === 'closed';
const isExpired = poll.expires_at && Date.now() > poll.expires_at;
const hasVoted = votedPolls.has(poll.id) || poll.options.some(o => o.votes > 0 && false);
const total = poll.total_votes || 0;
let optionsHtml = '';
poll.options.forEach(opt => {
const pct = total > 0 ? Math.round((opt.votes / total) * 100) : 0;
const votedClass = hasVoted ? ' voted' : '';
if (isClosed || isExpired || hasVoted) {
optionsHtml += `<div class="poll-option${votedClass}">
<div class="poll-bar" style="width:${pct}%"></div>
<span class="poll-label">${escapeHtml(opt.label)}</span>
<span class="poll-pct">${pct}%</span>
<span class="poll-votes">${opt.votes}</span>
</div>`;
} else {
optionsHtml += `<div class="poll-option" data-poll-id="${poll.id}" data-opt-id="${opt.id}">
<span class="poll-label">${escapeHtml(opt.label)}</span>
<span class="poll-pct" style="color:#6a8aaa">${opt.votes} votes</span>
</div>`;
}
});
const statusLabel = isClosed || poll.status === 'closed' ? 'Closed' :
isExpired ? 'Expired' : 'Active';
const statusClass = isClosed || isExpired ? 'closed' : 'active';
const creatorLabel = poll.is_anonymous ? 'Anonymous' : escapeHtml(poll.creator_ip);
html += `<div class="poll-card" data-poll-id="${poll.id}">
<div class="poll-question">📊 ${escapeHtml(poll.question)}</div>
<div class="poll-meta">by ${creatorLabel} · ${total} vote${total !== 1 ? 's' : ''} · <span class="poll-status ${statusClass}">${statusLabel}</span></div>
<div class="poll-options">${optionsHtml}</div>
</div>`;
});
return html;
}
async function loadMessages() {
@ -813,28 +761,61 @@
}
function renderMessages(msgs) {
const pollsHtml = renderPolls();
if (msgs.length === 0 && !pollsHtml) {
if (msgs.length === 0) {
messagesEl.innerHTML = '<div class="empty-state">💬 No messages yet. Be the first!</div>';
return;
}
let html = pollsHtml;
let html = '';
msgs.forEach(msg => {
const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
let contentHtml = escapeHtml(msg.content || '');
let gifToShow = msg.gif_url || null;
if (!gifToShow) {
const gifMatch = msg.content.match(/https?:\/\/[^\s]+\.(gif|webp)(\?[^\s]*)?/i);
if (gifMatch) gifToShow = gifMatch[0];
if (msg.poll) {
const poll = msg.poll;
const isClosed = poll.status === 'closed';
const isExpired = poll.expires_at && Date.now() > poll.expires_at;
const hasVoted = votedPolls.has(poll.id);
const total = poll.total_votes || 0;
let optionsHtml = '';
poll.options.forEach(opt => {
const pct = total > 0 ? Math.round((opt.votes / total) * 100) : 0;
const votedClass = hasVoted ? ' voted' : '';
if (isClosed || isExpired || hasVoted) {
optionsHtml += `<div class="poll-option${votedClass}">
<div class="poll-bar" style="width:${pct}%"></div>
<span class="poll-label">${escapeHtml(opt.label)}</span>
<span class="poll-pct">${pct}%</span>
<span class="poll-votes">${opt.votes}</span>
</div>`;
} else {
optionsHtml += `<div class="poll-option" data-poll-id="${poll.id}" data-opt-id="${opt.id}">
<span class="poll-label">${escapeHtml(opt.label)}</span>
<span class="poll-pct" style="color:#6a8aaa">${opt.votes} votes</span>
</div>`;
}
});
const statusLabel = isClosed ? 'Closed' : isExpired ? 'Expired' : 'Active';
const statusClass = isClosed || isExpired ? 'closed' : 'active';
const creatorLabel = poll.is_anonymous ? 'Anonymous' : escapeHtml(poll.creator_ip);
html += `<div class="poll-card" data-poll-id="${poll.id}">
<div class="poll-question">📊 ${escapeHtml(poll.question)}</div>
<div class="poll-meta">by ${creatorLabel} · ${total} vote${total !== 1 ? 's' : ''} · <span class="poll-status ${statusClass}">${statusLabel}</span></div>
<div class="poll-options">${optionsHtml}</div>
</div>`;
} else {
const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
let contentHtml = escapeHtml(msg.content || '');
let gifToShow = msg.gif_url || null;
if (!gifToShow) {
const gifMatch = msg.content.match(/https?:\/\/[^\s]+\.(gif|webp)(\?[^\s]*)?/i);
if (gifMatch) gifToShow = gifMatch[0];
}
if (gifToShow) {
contentHtml += `<img class="inline-gif" src="${escapeHtml(gifToShow)}" alt="gif">`;
}
html += `<div class="message">
<div class="msg-ip">🔑 ${escapeHtml(msg.ip)}</div>
<div class="msg-content">${contentHtml}</div>
<div class="msg-time">${time}</div>
</div>`;
}
if (gifToShow) {
contentHtml += `<img class="inline-gif" src="${escapeHtml(gifToShow)}" alt="gif">`;
}
html += `<div class="message">
<div class="msg-ip">🔑 ${escapeHtml(msg.ip)}</div>
<div class="msg-content">${contentHtml}</div>
<div class="msg-time">${time}</div>
</div>`;
});
messagesEl.innerHTML = html;
attachPollHandlers();
@ -859,10 +840,10 @@
votedPolls.add(pollId);
saveVotedPolls();
showToast('Vote recorded!', 'success');
loadPolls().then(() => renderMessages(window._lastMessages || []));
loadMessages();
} else {
showToast(data.error || 'Failed to vote', 'error');
loadPolls().then(() => renderMessages(window._lastMessages || []));
loadMessages();
}
} catch (e) {
showToast('Network error', 'error');
@ -904,17 +885,13 @@
loadVotedPolls();
renderChannels();
loadMessages();
loadPolls();
sendBtn.addEventListener('click', sendMessage);
inputEl.addEventListener('keydown', e => { if (e.key === 'Enter') sendMessage(); });
// Auto-poll every 3 seconds
// Auto-refresh every 3 seconds
setInterval(() => {
loadMessages();
loadPolls().then(() => {
if (window._lastMessages) renderMessages(window._lastMessages);
});
}, 3000);
</script>
</body>

View File

@ -22,6 +22,7 @@ class Messages(Base):
ip: Mapped[str] = mapped_column(String(64), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False)
gif_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True, default=None)
poll_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, default=None)
timestamp: Mapped[int] = mapped_column(Integer, nullable=False)
class Poll(Base):

View File

@ -18,6 +18,16 @@ class DBfrontend:
self.engine = make_engine(db_cfg)
self.Session = sessionmaker(bind=self.engine)
self.cipher = self._get_cipher(secret, salt)
self._migrate()
def _migrate(self):
with self.Session() as session:
from sqlalchemy import inspect, text
inspector = inspect(session.connection())
columns = [c["name"] for c in inspector.get_columns("messages")]
if "poll_id" not in columns:
session.execute(text("ALTER TABLE messages ADD COLUMN poll_id INTEGER"))
session.commit()
@staticmethod
def _get_cipher(secret: bytes, salt: bytes) -> Fernet:
@ -34,13 +44,14 @@ class DBfrontend:
except Exception:
return "[Decryption failed]"
def add_message(self, channel: str, ip: str, content: str, gif_url: Optional[str] = None) -> int:
def add_message(self, channel: str, ip: str, content: str, gif_url: Optional[str] = None, poll_id: Optional[int] = None) -> int:
with self.Session() as session:
msg = Messages(
channel=channel,
ip=ip,
content=self._encrypt(content),
gif_url=gif_url,
poll_id=poll_id,
timestamp=int(time.time() * 1000),
)
session.add(msg)
@ -57,6 +68,11 @@ class DBfrontend:
)
result = []
for msg in reversed(q.all()):
poll_data = None
if msg.poll_id:
poll = session.get(Poll, msg.poll_id)
if poll and poll.status != "deleted":
poll_data = self._serialize_poll(session, poll)
result.append(
{
"id": msg.id,
@ -64,6 +80,8 @@ class DBfrontend:
"ip": msg.ip,
"content": self._decrypt(msg.content),
"gif_url": msg.gif_url,
"poll_id": msg.poll_id,
"poll": poll_data,
"timestamp": msg.timestamp,
}
)
@ -106,6 +124,15 @@ class DBfrontend:
opt = PollOption(poll_id=poll.id, label=self._encrypt(label), position=i)
session.add(opt)
msg = Messages(
channel=channel,
ip=creator_ip,
content=self._encrypt(question),
poll_id=poll.id,
timestamp=int(time.time() * 1000),
)
session.add(msg)
session.commit()
return poll.id