110 lines
3.3 KiB
JavaScript
110 lines
3.3 KiB
JavaScript
'use strict';
|
|
|
|
const tweetsEl = document.getElementById('tweets');
|
|
const refreshBtn = document.getElementById('refresh');
|
|
const tweetForm = document.getElementById('tweet-form');
|
|
const statusEl = document.getElementById('status');
|
|
const template = document.getElementById('tweet-template');
|
|
|
|
function fmtTime(iso) {
|
|
const d = new Date(iso);
|
|
return d.toLocaleString();
|
|
}
|
|
|
|
async function api(path, options = {}) {
|
|
const res = await fetch(path, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
...options
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data.error || `http_${res.status}`);
|
|
return data;
|
|
}
|
|
|
|
function renderTweets(tweets) {
|
|
tweetsEl.innerHTML = '';
|
|
for (const tweet of tweets) {
|
|
const node = template.content.firstElementChild.cloneNode(true);
|
|
node.dataset.tweetId = String(tweet.id);
|
|
node.querySelector('.author').textContent = `@${tweet.author}`;
|
|
node.querySelector('.time').textContent = fmtTime(tweet.createdAt);
|
|
node.querySelector('.tweet-text').textContent = tweet.text;
|
|
node.querySelector('.likes').textContent = `${tweet.likes} likes`;
|
|
|
|
const likeBtn = node.querySelector('.like-btn');
|
|
likeBtn.addEventListener('click', async () => {
|
|
try {
|
|
await api(`/api/tweets/${tweet.id}/like`, { method: 'POST' });
|
|
await loadTweets();
|
|
} catch (err) {
|
|
statusEl.textContent = `Like failed: ${err.message}`;
|
|
}
|
|
});
|
|
|
|
const repliesEl = node.querySelector('.replies');
|
|
for (const r of tweet.replies || []) {
|
|
const li = document.createElement('li');
|
|
li.textContent = `@${r.author}: ${r.text}`;
|
|
repliesEl.appendChild(li);
|
|
}
|
|
|
|
const replyForm = node.querySelector('.reply-form');
|
|
replyForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const author = replyForm.querySelector('.reply-author').value.trim();
|
|
const text = replyForm.querySelector('.reply-text').value.trim();
|
|
if (!author || !text) return;
|
|
try {
|
|
await api(`/api/tweets/${tweet.id}/reply`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ author, text })
|
|
});
|
|
await loadTweets();
|
|
} catch (err) {
|
|
statusEl.textContent = `Reply failed: ${err.message}`;
|
|
}
|
|
});
|
|
|
|
tweetsEl.appendChild(node);
|
|
}
|
|
}
|
|
|
|
async function loadTweets() {
|
|
const data = await api('/api/tweets');
|
|
renderTweets(data.tweets || []);
|
|
}
|
|
|
|
refreshBtn.addEventListener('click', async () => {
|
|
statusEl.textContent = '';
|
|
try {
|
|
await loadTweets();
|
|
} catch (err) {
|
|
statusEl.textContent = `Refresh failed: ${err.message}`;
|
|
}
|
|
});
|
|
|
|
tweetForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const form = new FormData(tweetForm);
|
|
const author = String(form.get('author') || '').trim();
|
|
const text = String(form.get('text') || '').trim();
|
|
if (!author || !text) return;
|
|
|
|
statusEl.textContent = 'Posting...';
|
|
try {
|
|
await api('/api/tweets', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ author, text })
|
|
});
|
|
tweetForm.reset();
|
|
statusEl.textContent = 'Tweet posted.';
|
|
await loadTweets();
|
|
} catch (err) {
|
|
statusEl.textContent = `Post failed: ${err.message}`;
|
|
}
|
|
});
|
|
|
|
loadTweets().catch(err => {
|
|
statusEl.textContent = `Initial load failed: ${err.message}`;
|
|
});
|