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> |