hackernews / index.html
sexyfrad's picture
Add 2 files
812e4f9 verified
<!DOCTYPE html>
<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>