Files
whetstone_DSL/example_projects/twitter_clone_local/public/app.js

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}`;
});