Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Hacker News Clone</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
.story:hover .story-title { | |
color: #ff6600; | |
} | |
.upvote:hover { | |
color: #ff6600; | |
transform: scale(1.2); | |
} | |
.comment-count:hover, .comment-link:hover { | |
color: #ff6600; | |
} | |
.fade-in { | |
animation: fadeIn 0.3s ease-in; | |
} | |
.slide-in { | |
animation: slideIn 0.3s ease-out; | |
} | |
@keyframes fadeIn { | |
from { opacity: 0; } | |
to { opacity: 1; } | |
} | |
@keyframes slideIn { | |
from { transform: translateX(20px); opacity: 0; } | |
to { transform: translateX(0); opacity: 1; } | |
} | |
.loading-spinner { | |
border: 3px solid rgba(255, 102, 0, 0.3); | |
border-radius: 50%; | |
border-top: 3px solid #ff6600; | |
width: 20px; | |
height: 20px; | |
animation: spin 1s linear infinite; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
.comment { | |
position: relative; | |
} | |
.comment::before { | |
content: ''; | |
position: absolute; | |
left: -15px; | |
top: 0; | |
bottom: 0; | |
width: 2px; | |
background-color: #e5e7eb; | |
} | |
.comment:hover::before { | |
background-color: #ff6600; | |
} | |
.comment-content { | |
position: relative; | |
} | |
.comment-content::after { | |
content: ''; | |
position: absolute; | |
left: -15px; | |
top: 20px; | |
width: 15px; | |
height: 2px; | |
background-color: #e5e7eb; | |
} | |
.comment:hover .comment-content::after { | |
background-color: #ff6600; | |
} | |
.highlight-new { | |
animation: highlight 2s ease-out; | |
} | |
@keyframes highlight { | |
0% { background-color: rgba(255, 102, 0, 0.1); } | |
100% { background-color: transparent; } | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 font-sans"> | |
<header class="bg-orange-600 py-2 px-4 shadow-md"> | |
<div class="max-w-5xl mx-auto flex items-center"> | |
<div class="flex items-center"> | |
<div class="mr-2"> | |
<i class="fas fa-newspaper text-white text-xl"></i> | |
</div> | |
<h1 class="text-white font-bold text-xl mr-4">Hacker News</h1> | |
</div> | |
<nav class="flex space-x-4 text-sm"> | |
<a href="#" class="text-white hover:underline">new</a> | |
<a href="#" class="text-white hover:underline">past</a> | |
<a href="#" class="text-white hover:underline">comments</a> | |
<a href="#" class="text-white hover:underline">ask</a> | |
<a href="#" class="text-white hover:underline">show</a> | |
<a href="#" class="text-white hover:underline">jobs</a> | |
<a href="#" class="text-white hover:underline">submit</a> | |
</nav> | |
<div class="ml-auto"> | |
<a href="#" class="text-white text-sm hover:underline">login</a> | |
</div> | |
</div> | |
</header> | |
<main class="max-w-5xl mx-auto bg-white shadow-sm mt-4 rounded-md overflow-hidden"> | |
<div id="news-view" class=""> | |
<div class="p-4 border-b border-gray-200"> | |
<div class="flex items-center"> | |
<h2 class="font-semibold text-gray-800">Top Stories</h2> | |
<div class="ml-4 flex space-x-2"> | |
<button id="refresh-btn" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded text-sm flex items-center"> | |
<i class="fas fa-sync-alt mr-1 text-gray-600"></i> Refresh | |
</button> | |
<div class="relative"> | |
<select id="filter-select" class="appearance-none px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded text-sm pr-8"> | |
<option value="top">Top</option> | |
<option value="new">New</option> | |
<option value="best">Best</option> | |
<option value="ask">Ask HN</option> | |
<option value="show">Show HN</option> | |
<option value="jobs">Jobs</option> | |
</select> | |
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"> | |
<i class="fas fa-chevron-down text-xs"></i> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div id="stories-container" class="divide-y divide-gray-100"> | |
<!-- Stories will be loaded here --> | |
<div class="p-4 flex items-center justify-center"> | |
<div class="loading-spinner"></div> | |
</div> | |
</div> | |
<div class="p-4 border-t border-gray-200 text-center"> | |
<button id="load-more" class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded text-sm font-medium"> | |
Load More | |
</button> | |
</div> | |
</div> | |
<div id="comments-view" class="hidden"> | |
<div class="p-4 border-b border-gray-200 flex items-center"> | |
<button id="back-btn" class="mr-4 text-gray-600 hover:text-orange-600"> | |
<i class="fas fa-arrow-left"></i> | |
</button> | |
<h2 class="font-semibold text-gray-800">Comments</h2> | |
</div> | |
<div id="story-header" class="p-4 border-b border-gray-200 bg-gray-50"> | |
<!-- Story details will be loaded here --> | |
</div> | |
<div id="comments-container" class="divide-y divide-gray-100"> | |
<!-- Comments will be loaded here --> | |
</div> | |
<div class="p-4 border-t border-gray-200"> | |
<div class="flex items-start mb-4"> | |
<div class="mr-3 mt-1"> | |
<div class="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center"> | |
<i class="fas fa-user text-gray-500"></i> | |
</div> | |
</div> | |
<div class="flex-1"> | |
<textarea id="comment-input" class="w-full border border-gray-300 rounded p-2 text-sm" rows="3" placeholder="Add your comment..."></textarea> | |
<div class="mt-2 flex justify-end"> | |
<button id="submit-comment" class="px-4 py-1 bg-orange-600 hover:bg-orange-700 text-white rounded text-sm"> | |
Submit | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</main> | |
<footer class="max-w-5xl mx-auto mt-4 py-4 border-t border-gray-200 text-center text-xs text-gray-500"> | |
<div class="mb-2"> | |
<a href="#" class="hover:underline">Guidelines</a> | | |
<a href="#" class="hover:underline">FAQ</a> | | |
<a href="#" class="hover:underline">Lists</a> | | |
<a href="#" class="hover:underline">API</a> | | |
<a href="#" class="hover:underline">Security</a> | | |
<a href="#" class="hover:underline">Legal</a> | | |
<a href="#" class="hover:underline">Apply to YC</a> | | |
<a href="#" class="hover:underline">Contact</a> | |
</div> | |
<div> | |
<form class="inline-flex items-center"> | |
<label for="search" class="mr-2">Search:</label> | |
<input type="text" id="search" class="border border-gray-300 px-2 py-1 rounded text-xs w-64"> | |
<button type="submit" class="ml-2 px-2 py-1 bg-gray-200 hover:bg-gray-300 rounded text-xs"> | |
<i class="fas fa-search"></i> | |
</button> | |
</form> | |
</div> | |
</footer> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
let currentPage = 1; | |
let currentFilter = 'top'; | |
let isLoading = false; | |
let currentStoryId = null; | |
const storiesContainer = document.getElementById('stories-container'); | |
const loadMoreBtn = document.getElementById('load-more'); | |
const refreshBtn = document.getElementById('refresh-btn'); | |
const filterSelect = document.getElementById('filter-select'); | |
const newsView = document.getElementById('news-view'); | |
const commentsView = document.getElementById('comments-view'); | |
const backBtn = document.getElementById('back-btn'); | |
const storyHeader = document.getElementById('story-header'); | |
const commentsContainer = document.getElementById('comments-container'); | |
const commentInput = document.getElementById('comment-input'); | |
const submitComment = document.getElementById('submit-comment'); | |
// Mock data for stories | |
const mockStories = [ | |
{ | |
id: 1, | |
title: 'Rust 1.70.0 Released', | |
url: 'https://blog.rust-lang.org/2023/06/01/Rust-1.70.0.html', | |
score: 287, | |
by: 'steveklabnik', | |
time: Date.now() - 3600000 * 3, | |
descendants: 54, | |
text: 'The Rust team is happy to announce a new version of Rust, 1.70.0. Rust is a programming language empowering everyone to build reliable and efficient software.' | |
}, | |
{ | |
id: 2, | |
title: 'The future of TypeScript is JavaScript', | |
url: 'https://dev.to/this-is-learning/the-future-of-typescript-is-javascript-2o5e', | |
score: 156, | |
by: 'tldrews', | |
time: Date.now() - 3600000 * 5, | |
descendants: 42, | |
text: 'With the new features coming to JavaScript, TypeScript might become less necessary in the future. Here are my thoughts on why.' | |
}, | |
{ | |
id: 3, | |
title: 'Show HN: I built a tool that helps you find remote jobs', | |
url: 'https://remotejobs.com', | |
score: 98, | |
by: 'remoteworker', | |
time: Date.now() - 3600000 * 7, | |
descendants: 23, | |
text: 'After struggling to find good remote jobs myself, I built this tool to aggregate the best remote job listings from across the web. Would love your feedback!' | |
}, | |
{ | |
id: 4, | |
title: 'Ask HN: What books changed the way you think about programming?', | |
url: '', | |
score: 210, | |
by: 'bookworm', | |
time: Date.now() - 3600000 * 9, | |
descendants: 187, | |
text: 'I\'m looking to expand my programming knowledge and would love to hear about books that had a significant impact on how you think about software development.' | |
}, | |
{ | |
id: 5, | |
title: 'The hidden cost of technical debt', | |
url: 'https://medium.com/tech-debt/the-hidden-cost-of-technical-debt-3895e59a9d5e', | |
score: 176, | |
by: 'debtfree', | |
time: Date.now() - 3600000 * 12, | |
descendants: 63, | |
text: 'Technical debt is often discussed, but the hidden costs are rarely quantified. This article explores the real impact of accumulated tech debt on productivity and morale.' | |
} | |
]; | |
// Mock data for comments | |
const mockComments = { | |
1: [ | |
{ | |
id: 101, | |
by: 'rustfan', | |
time: Date.now() - 3600000 * 2, | |
text: 'This is a great release! The performance improvements are significant.', | |
score: 45, | |
kids: [ | |
{ | |
id: 1011, | |
by: 'rustnewbie', | |
time: Date.now() - 3600000 * 1, | |
text: 'I agree! The new error messages are much clearer.', | |
score: 12, | |
kids: [] | |
} | |
] | |
}, | |
{ | |
id: 102, | |
by: 'programmer123', | |
time: Date.now() - 3600000 * 2.5, | |
text: 'I\'ve been waiting for these features. The async improvements are game-changing.', | |
score: 32, | |
kids: [ | |
{ | |
id: 1021, | |
by: 'asyncdev', | |
time: Date.now() - 3600000 * 1.5, | |
text: 'Yes! Finally we can do X without workarounds.', | |
score: 8, | |
kids: [] | |
}, | |
{ | |
id: 1022, | |
by: 'perfexpert', | |
time: Date.now() - 3600000 * 1, | |
text: 'The benchmarks show a 15% improvement in my use case.', | |
score: 5, | |
kids: [] | |
} | |
] | |
} | |
], | |
2: [ | |
{ | |
id: 201, | |
by: 'jsdev', | |
time: Date.now() - 3600000 * 4, | |
text: 'I think TypeScript will still be relevant for large codebases.', | |
score: 28, | |
kids: [ | |
{ | |
id: 2011, | |
by: 'tslover', | |
time: Date.now() - 3600000 * 3, | |
text: 'Exactly! Type safety is crucial for team development.', | |
score: 10, | |
kids: [] | |
} | |
] | |
} | |
], | |
4: [ | |
{ | |
id: 401, | |
by: 'classiccoder', | |
time: Date.now() - 3600000 * 8, | |
text: 'Structure and Interpretation of Computer Programs changed everything for me.', | |
score: 56, | |
kids: [ | |
{ | |
id: 4011, | |
by: 'lisper', | |
time: Date.now() - 3600000 * 7, | |
text: 'Same here! It teaches you to think differently about problems.', | |
score: 22, | |
kids: [] | |
} | |
] | |
}, | |
{ | |
id: 402, | |
by: 'pragmatic', | |
time: Date.now() - 3600000 * 7.5, | |
text: 'Clean Code by Robert Martin is a must-read for any professional developer.', | |
score: 42, | |
kids: [] | |
} | |
] | |
}; | |
// Format time as "X hours ago" | |
function formatTime(timestamp) { | |
const seconds = Math.floor((Date.now() - timestamp) / 1000); | |
let interval = Math.floor(seconds / 31536000); | |
if (interval >= 1) return `${interval} year${interval === 1 ? '' : 's'} ago`; | |
interval = Math.floor(seconds / 2592000); | |
if (interval >= 1) return `${interval} month${interval === 1 ? '' : 's'} ago`; | |
interval = Math.floor(seconds / 86400); | |
if (interval >= 1) return `${interval} day${interval === 1 ? '' : 's'} ago`; | |
interval = Math.floor(seconds / 3600); | |
if (interval >= 1) return `${interval} hour${interval === 1 ? '' : 's'} ago`; | |
interval = Math.floor(seconds / 60); | |
if (interval >= 1) return `${interval} minute${interval === 1 ? '' : 's'} ago`; | |
return `${Math.floor(seconds)} second${seconds === 1 ? '' : 's'} ago`; | |
} | |
// Get domain from URL | |
function getDomain(url) { | |
if (!url) return ''; | |
try { | |
const domain = new URL(url).hostname.replace('www.', ''); | |
return domain; | |
} catch { | |
return ''; | |
} | |
} | |
// Render stories | |
function renderStories(stories) { | |
storiesContainer.innerHTML = ''; | |
stories.forEach(story => { | |
const storyElement = document.createElement('div'); | |
storyElement.className = 'story p-4 hover:bg-gray-50 transition-colors duration-150 fade-in'; | |
storyElement.innerHTML = ` | |
<div class="flex items-start"> | |
<div class="text-gray-500 text-xs mr-2 mt-1 flex flex-col items-center"> | |
<button class="upvote text-gray-400 hover:text-orange-600 transition-all duration-200"> | |
<i class="fas fa-caret-up"></i> | |
</button> | |
<span class="text-gray-700 font-medium">${story.score}</span> | |
</div> | |
<div class="flex-1"> | |
<div class="flex items-baseline flex-wrap"> | |
<a href="${story.url || `#item?id=${story.id}`}" target="_blank" class="story-title text-base font-medium text-gray-900 hover:text-orange-600 mr-1">${story.title}</a> | |
${story.url ? `<span class="text-xs text-gray-500">(${getDomain(story.url)})</span>` : ''} | |
</div> | |
<div class="mt-1 text-xs text-gray-500"> | |
by <a href="#" class="hover:underline">${story.by}</a> ${formatTime(story.time)} | | |
<a href="#" class="comment-link hover:underline ml-1" data-story-id="${story.id}">${story.descendants} comment${story.descendants !== 1 ? 's' : ''}</a> | |
</div> | |
</div> | |
</div> | |
`; | |
storiesContainer.appendChild(storyElement); | |
}); | |
// Add click handlers to comment links | |
document.querySelectorAll('.comment-link').forEach(link => { | |
link.addEventListener('click', function(e) { | |
e.preventDefault(); | |
showComments(parseInt(this.getAttribute('data-story-id'))); | |
}); | |
}); | |
// Add click handlers to upvote buttons | |
document.querySelectorAll('.upvote').forEach(btn => { | |
btn.addEventListener('click', function() { | |
const scoreElement = this.nextElementSibling; | |
if (!this.classList.contains('text-orange-600')) { | |
this.classList.add('text-orange-600'); | |
scoreElement.textContent = parseInt(scoreElement.textContent) + 1; | |
} else { | |
this.classList.remove('text-orange-600'); | |
scoreElement.textContent = parseInt(scoreElement.textContent) - 1; | |
} | |
}); | |
}); | |
} | |
// Render comments recursively | |
function renderComments(comments, level = 0) { | |
let html = ''; | |
comments.forEach(comment => { | |
html += ` | |
<div class="comment pl-${level > 0 ? level * 4 : 4} py-3 fade-in highlight-new" data-comment-id="${comment.id}"> | |
<div class="comment-content pl-2"> | |
<div class="text-xs text-gray-500 mb-1"> | |
<button class="upvote text-gray-400 hover:text-orange-600 transition-all duration-200 mr-1"> | |
<i class="fas fa-caret-up"></i> | |
</button> | |
<a href="#" class="hover:underline font-medium">${comment.by}</a> ${formatTime(comment.time)} | | |
<span class="text-gray-700">${comment.score} point${comment.score !== 1 ? 's' : ''}</span> | |
</div> | |
<div class="text-sm text-gray-800 mb-2"> | |
${comment.text} | |
</div> | |
<div class="text-xs"> | |
<a href="#" class="text-gray-500 hover:underline reply-link" data-comment-id="${comment.id}">reply</a> | |
</div> | |
</div> | |
</div> | |
`; | |
if (comment.kids && comment.kids.length > 0) { | |
html += renderComments(comment.kids, level + 1); | |
} | |
}); | |
return html; | |
} | |
// Show comments for a story | |
function showComments(storyId) { | |
currentStoryId = storyId; | |
newsView.classList.add('hidden'); | |
commentsView.classList.remove('hidden'); | |
// Find the story | |
const story = mockStories.find(s => s.id === storyId); | |
if (!story) return; | |
// Render story header | |
storyHeader.innerHTML = ` | |
<div class="flex items-start"> | |
<div class="text-gray-500 text-xs mr-2 mt-1 flex flex-col items-center"> | |
<button class="upvote text-gray-400 hover:text-orange-600 transition-all duration-200"> | |
<i class="fas fa-caret-up"></i> | |
</button> | |
<span class="text-gray-700 font-medium">${story.score}</span> | |
</div> | |
<div class="flex-1"> | |
<div class="flex items-baseline flex-wrap"> | |
<a href="${story.url || `#item?id=${story.id}`}" target="_blank" class="text-base font-medium text-gray-900 hover:text-orange-600 mr-1">${story.title}</a> | |
${story.url ? `<span class="text-xs text-gray-500">(${getDomain(story.url)})</span>` : ''} | |
</div> | |
<div class="mt-1 text-xs text-gray-500"> | |
by <a href="#" class="hover:underline">${story.by}</a> ${formatTime(story.time)} | |
</div> | |
${story.text ? `<div class="mt-2 text-sm text-gray-800">${story.text}</div>` : ''} | |
</div> | |
</div> | |
`; | |
// Add upvote handler | |
storyHeader.querySelector('.upvote').addEventListener('click', function() { | |
const scoreElement = this.nextElementSibling; | |
if (!this.classList.contains('text-orange-600')) { | |
this.classList.add('text-orange-600'); | |
scoreElement.textContent = parseInt(scoreElement.textContent) + 1; | |
} else { | |
this.classList.remove('text-orange-600'); | |
scoreElement.textContent = parseInt(scoreElement.textContent) - 1; | |
} | |
}); | |
// Load and render comments | |
commentsContainer.innerHTML = '<div class="p-4 flex items-center justify-center"><div class="loading-spinner"></div></div>'; | |
setTimeout(() => { | |
const comments = mockComments[storyId] || []; | |
if (comments.length === 0) { | |
commentsContainer.innerHTML = ` | |
<div class="p-4 text-center text-gray-500"> | |
No comments yet. Be the first to comment! | |
</div> | |
`; | |
} else { | |
commentsContainer.innerHTML = renderComments(comments); | |
// Add upvote handlers | |
commentsContainer.querySelectorAll('.upvote').forEach(btn => { | |
btn.addEventListener('click', function() { | |
const scoreElement = this.nextElementSibling.nextElementSibling; | |
if (!this.classList.contains('text-orange-600')) { | |
this.classList.add('text-orange-600'); | |
const currentScore = parseInt(scoreElement.textContent); | |
scoreElement.textContent = (currentScore || 0) + 1 + ' point' + ((currentScore + 1) !== 1 ? 's' : ''); | |
} else { | |
this.classList.remove('text-orange-600'); | |
const currentScore = parseInt(scoreElement.textContent); | |
scoreElement.textContent = (currentScore - 1) + ' point' + ((currentScore - 1) !== 1 ? 's' : ''); | |
} | |
}); | |
}); | |
// Add reply handlers | |
commentsContainer.querySelectorAll('.reply-link').forEach(link => { | |
link.addEventListener('click', function(e) { | |
e.preventDefault(); | |
const commentId = parseInt(this.getAttribute('data-comment-id')); | |
commentInput.focus(); | |
commentInput.placeholder = `Replying to comment #${commentId}...`; | |
}); | |
}); | |
} | |
}, 800); | |
} | |
// Simulate loading stories from API | |
function loadStories(page = 1, filter = 'top') { | |
isLoading = true; | |
// Show loading spinner | |
storiesContainer.innerHTML = ` | |
<div class="p-4 flex items-center justify-center"> | |
<div class="loading-spinner"></div> | |
</div> | |
`; | |
// Simulate API delay | |
setTimeout(() => { | |
// Filter mock stories based on selection | |
let filteredStories = [...mockStories]; | |
if (filter === 'new') { | |
filteredStories.sort((a, b) => b.time - a.time); | |
} else if (filter === 'best') { | |
filteredStories.sort((a, b) => b.score - a.score); | |
} else if (filter === 'ask') { | |
filteredStories = filteredStories.filter(story => story.title.startsWith('Ask HN:')); | |
} else if (filter === 'show') { | |
filteredStories = filteredStories.filter(story => story.title.startsWith('Show HN:')); | |
} else if (filter === 'jobs') { | |
filteredStories = filteredStories.filter(story => story.title.includes('hiring') || story.title.includes('job')); | |
} | |
// Pagination simulation | |
const startIdx = (page - 1) * 5; | |
const endIdx = startIdx + 5; | |
const paginatedStories = filteredStories.slice(0, endIdx); | |
renderStories(paginatedStories); | |
isLoading = false; | |
// Show/hide load more button | |
if (endIdx >= filteredStories.length) { | |
loadMoreBtn.style.display = 'none'; | |
} else { | |
loadMoreBtn.style.display = 'inline-block'; | |
} | |
}, 800); | |
} | |
// Back to news view | |
backBtn.addEventListener('click', function() { | |
commentsView.classList.add('hidden'); | |
newsView.classList.remove('hidden'); | |
commentInput.value = ''; | |
commentInput.placeholder = 'Add your comment...'; | |
}); | |
// Submit comment | |
submitComment.addEventListener('click', function() { | |
const commentText = commentInput.value.trim(); | |
if (!commentText) return; | |
// Create new comment | |
const newComment = { | |
id: Math.floor(Math.random() * 10000), | |
by: 'currentuser', | |
time: Date.now(), | |
text: commentText, | |
score: 1, | |
kids: [] | |
}; | |
// In a real app, you would send this to your backend | |
if (!mockComments[currentStoryId]) { | |
mockComments[currentStoryId] = []; | |
} | |
mockComments[currentStoryId].unshift(newComment); | |
// Update the story's comment count | |
const story = mockStories.find(s => s.id === currentStoryId); | |
if (story) { | |
story.descendants += 1; | |
} | |
// Clear input | |
commentInput.value = ''; | |
// Re-render comments | |
showComments(currentStoryId); | |
// Scroll to new comment | |
setTimeout(() => { | |
const newCommentElement = document.querySelector(`[data-comment-id="${newComment.id}"]`); | |
if (newCommentElement) { | |
newCommentElement.scrollIntoView({ behavior: 'smooth' }); | |
} | |
}, 100); | |
}); | |
// Initial load | |
loadStories(currentPage, currentFilter); | |
// Load more stories | |
loadMoreBtn.addEventListener('click', function() { | |
if (isLoading) return; | |
currentPage++; | |
loadStories(currentPage, currentFilter); | |
}); | |
// Refresh stories | |
refreshBtn.addEventListener('click', function() { | |
if (isLoading) return; | |
currentPage = 1; | |
loadStories(currentPage, currentFilter); | |
// Add rotation animation to refresh button | |
refreshBtn.querySelector('i').classList.add('fa-spin'); | |
setTimeout(() => { | |
refreshBtn.querySelector('i').classList.remove('fa-spin'); | |
}, 800); | |
}); | |
// Filter stories | |
filterSelect.addEventListener('change', function() { | |
currentFilter = this.value; | |
currentPage = 1; | |
loadStories(currentPage, currentFilter); | |
}); | |
// Keyboard shortcuts | |
document.addEventListener('keydown', function(e) { | |
// Press 'r' to refresh | |
if (e.key === 'r' && !e.ctrlKey && !e.metaKey && !e.altKey) { | |
e.preventDefault(); | |
refreshBtn.click(); | |
} | |
// Press Escape to clear reply | |
if (e.key === 'Escape') { | |
commentInput.placeholder = 'Add your comment...'; | |
} | |
}); | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=sexyfrad/hackernews" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> | |
</html> |