FEATURE: added pols to frontend
This commit is contained in:
parent
c0391f8e69
commit
b5982063c4
71
index.html
71
index.html
|
|
@ -523,7 +523,6 @@
|
||||||
let pollInterval = null;
|
let pollInterval = null;
|
||||||
let selectedGifUrl = null;
|
let selectedGifUrl = null;
|
||||||
let votedPolls = new Set();
|
let votedPolls = new Set();
|
||||||
let pollsData = [];
|
|
||||||
|
|
||||||
function loadVotedPolls() {
|
function loadVotedPolls() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -711,7 +710,7 @@
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showToast('Poll created!', 'success');
|
showToast('Poll created!', 'success');
|
||||||
pollModal.classList.remove('open');
|
pollModal.classList.remove('open');
|
||||||
loadPolls();
|
loadMessages();
|
||||||
} else {
|
} else {
|
||||||
showToast(data.error || 'Failed to create poll', 'error');
|
showToast(data.error || 'Failed to create poll', 'error');
|
||||||
}
|
}
|
||||||
|
|
@ -745,27 +744,34 @@
|
||||||
renderChannels();
|
renderChannels();
|
||||||
messagesEl.innerHTML = '<div class="empty-state">Loading...</div>';
|
messagesEl.innerHTML = '<div class="empty-state">Loading...</div>';
|
||||||
loadMessages();
|
loadMessages();
|
||||||
loadPolls();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPolls() {
|
async function loadMessages() {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`/api/polls?channel=${currentChannel}`);
|
const r = await fetch(`/api/messages?channel=${currentChannel}&limit=50`);
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
if (data.success) pollsData = data.polls || [];
|
if (data.success) {
|
||||||
else pollsData = [];
|
window._lastMessages = data.messages || [];
|
||||||
|
renderMessages(window._lastMessages);
|
||||||
|
}
|
||||||
|
else showToast('Failed to load messages', 'error');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
pollsData = [];
|
showToast('Network error', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPolls() {
|
function renderMessages(msgs) {
|
||||||
if (!pollsData.length) return '';
|
if (msgs.length === 0) {
|
||||||
|
messagesEl.innerHTML = '<div class="empty-state">💬 No messages yet. Be the first!</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
let html = '';
|
let html = '';
|
||||||
pollsData.forEach(poll => {
|
msgs.forEach(msg => {
|
||||||
|
if (msg.poll) {
|
||||||
|
const poll = msg.poll;
|
||||||
const isClosed = poll.status === 'closed';
|
const isClosed = poll.status === 'closed';
|
||||||
const isExpired = poll.expires_at && Date.now() > poll.expires_at;
|
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 hasVoted = votedPolls.has(poll.id);
|
||||||
const total = poll.total_votes || 0;
|
const total = poll.total_votes || 0;
|
||||||
let optionsHtml = '';
|
let optionsHtml = '';
|
||||||
poll.options.forEach(opt => {
|
poll.options.forEach(opt => {
|
||||||
|
|
@ -785,8 +791,7 @@
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const statusLabel = isClosed || poll.status === 'closed' ? 'Closed' :
|
const statusLabel = isClosed ? 'Closed' : isExpired ? 'Expired' : 'Active';
|
||||||
isExpired ? 'Expired' : 'Active';
|
|
||||||
const statusClass = isClosed || isExpired ? 'closed' : 'active';
|
const statusClass = isClosed || isExpired ? 'closed' : 'active';
|
||||||
const creatorLabel = poll.is_anonymous ? 'Anonymous' : escapeHtml(poll.creator_ip);
|
const creatorLabel = poll.is_anonymous ? 'Anonymous' : escapeHtml(poll.creator_ip);
|
||||||
html += `<div class="poll-card" data-poll-id="${poll.id}">
|
html += `<div class="poll-card" data-poll-id="${poll.id}">
|
||||||
|
|
@ -794,32 +799,7 @@
|
||||||
<div class="poll-meta">by ${creatorLabel} · ${total} vote${total !== 1 ? 's' : ''} · <span class="poll-status ${statusClass}">${statusLabel}</span></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 class="poll-options">${optionsHtml}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
} else {
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMessages() {
|
|
||||||
try {
|
|
||||||
const r = await fetch(`/api/messages?channel=${currentChannel}&limit=50`);
|
|
||||||
const data = await r.json();
|
|
||||||
if (data.success) {
|
|
||||||
window._lastMessages = data.messages || [];
|
|
||||||
renderMessages(window._lastMessages);
|
|
||||||
}
|
|
||||||
else showToast('Failed to load messages', 'error');
|
|
||||||
} catch (e) {
|
|
||||||
showToast('Network error', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMessages(msgs) {
|
|
||||||
const pollsHtml = renderPolls();
|
|
||||||
if (msgs.length === 0 && !pollsHtml) {
|
|
||||||
messagesEl.innerHTML = '<div class="empty-state">💬 No messages yet. Be the first!</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let html = pollsHtml;
|
|
||||||
msgs.forEach(msg => {
|
|
||||||
const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
||||||
let contentHtml = escapeHtml(msg.content || '');
|
let contentHtml = escapeHtml(msg.content || '');
|
||||||
let gifToShow = msg.gif_url || null;
|
let gifToShow = msg.gif_url || null;
|
||||||
|
|
@ -835,6 +815,7 @@
|
||||||
<div class="msg-content">${contentHtml}</div>
|
<div class="msg-content">${contentHtml}</div>
|
||||||
<div class="msg-time">${time}</div>
|
<div class="msg-time">${time}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
messagesEl.innerHTML = html;
|
messagesEl.innerHTML = html;
|
||||||
attachPollHandlers();
|
attachPollHandlers();
|
||||||
|
|
@ -859,10 +840,10 @@
|
||||||
votedPolls.add(pollId);
|
votedPolls.add(pollId);
|
||||||
saveVotedPolls();
|
saveVotedPolls();
|
||||||
showToast('Vote recorded!', 'success');
|
showToast('Vote recorded!', 'success');
|
||||||
loadPolls().then(() => renderMessages(window._lastMessages || []));
|
loadMessages();
|
||||||
} else {
|
} else {
|
||||||
showToast(data.error || 'Failed to vote', 'error');
|
showToast(data.error || 'Failed to vote', 'error');
|
||||||
loadPolls().then(() => renderMessages(window._lastMessages || []));
|
loadMessages();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('Network error', 'error');
|
showToast('Network error', 'error');
|
||||||
|
|
@ -904,17 +885,13 @@
|
||||||
loadVotedPolls();
|
loadVotedPolls();
|
||||||
renderChannels();
|
renderChannels();
|
||||||
loadMessages();
|
loadMessages();
|
||||||
loadPolls();
|
|
||||||
|
|
||||||
sendBtn.addEventListener('click', sendMessage);
|
sendBtn.addEventListener('click', sendMessage);
|
||||||
inputEl.addEventListener('keydown', e => { if (e.key === 'Enter') sendMessage(); });
|
inputEl.addEventListener('keydown', e => { if (e.key === 'Enter') sendMessage(); });
|
||||||
|
|
||||||
// Auto-poll every 3 seconds
|
// Auto-refresh every 3 seconds
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
loadMessages();
|
loadMessages();
|
||||||
loadPolls().then(() => {
|
|
||||||
if (window._lastMessages) renderMessages(window._lastMessages);
|
|
||||||
});
|
|
||||||
}, 3000);
|
}, 3000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -22,6 +22,7 @@ class Messages(Base):
|
||||||
ip: Mapped[str] = mapped_column(String(64), nullable=False)
|
ip: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
gif_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True, default=None)
|
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)
|
timestamp: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||||
|
|
||||||
class Poll(Base):
|
class Poll(Base):
|
||||||
|
|
|
||||||
29
models/db.py
29
models/db.py
|
|
@ -18,6 +18,16 @@ class DBfrontend:
|
||||||
self.engine = make_engine(db_cfg)
|
self.engine = make_engine(db_cfg)
|
||||||
self.Session = sessionmaker(bind=self.engine)
|
self.Session = sessionmaker(bind=self.engine)
|
||||||
self.cipher = self._get_cipher(secret, salt)
|
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
|
@staticmethod
|
||||||
def _get_cipher(secret: bytes, salt: bytes) -> Fernet:
|
def _get_cipher(secret: bytes, salt: bytes) -> Fernet:
|
||||||
|
|
@ -34,13 +44,14 @@ class DBfrontend:
|
||||||
except Exception:
|
except Exception:
|
||||||
return "[Decryption failed]"
|
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:
|
with self.Session() as session:
|
||||||
msg = Messages(
|
msg = Messages(
|
||||||
channel=channel,
|
channel=channel,
|
||||||
ip=ip,
|
ip=ip,
|
||||||
content=self._encrypt(content),
|
content=self._encrypt(content),
|
||||||
gif_url=gif_url,
|
gif_url=gif_url,
|
||||||
|
poll_id=poll_id,
|
||||||
timestamp=int(time.time() * 1000),
|
timestamp=int(time.time() * 1000),
|
||||||
)
|
)
|
||||||
session.add(msg)
|
session.add(msg)
|
||||||
|
|
@ -57,6 +68,11 @@ class DBfrontend:
|
||||||
)
|
)
|
||||||
result = []
|
result = []
|
||||||
for msg in reversed(q.all()):
|
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(
|
result.append(
|
||||||
{
|
{
|
||||||
"id": msg.id,
|
"id": msg.id,
|
||||||
|
|
@ -64,6 +80,8 @@ class DBfrontend:
|
||||||
"ip": msg.ip,
|
"ip": msg.ip,
|
||||||
"content": self._decrypt(msg.content),
|
"content": self._decrypt(msg.content),
|
||||||
"gif_url": msg.gif_url,
|
"gif_url": msg.gif_url,
|
||||||
|
"poll_id": msg.poll_id,
|
||||||
|
"poll": poll_data,
|
||||||
"timestamp": msg.timestamp,
|
"timestamp": msg.timestamp,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -106,6 +124,15 @@ class DBfrontend:
|
||||||
opt = PollOption(poll_id=poll.id, label=self._encrypt(label), position=i)
|
opt = PollOption(poll_id=poll.id, label=self._encrypt(label), position=i)
|
||||||
session.add(opt)
|
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()
|
session.commit()
|
||||||
return poll.id
|
return poll.id
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue