1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* This Hacker News clone uses Lucia for user authentication and SQLite for data storage.
* It provides a simple interface for viewing, submitting, and upvoting stories, mimicking the HN style.
* Users can upvote each post exactly once and comment on stories in an infinitely nested style.
*/
/** @jsxImportSource https://esm.sh/react */
import React, { useState, useEffect } from "https://esm.sh/react";
import { createRoot } from "https://esm.sh/react-dom/client";
import { luciaMiddleware } from "https://esm.town/v/yawnxyz/lucia_middleware_safe";
function App() {
const [stories, setStories] = useState([]);
const [user, setUser] = useState(null);
const [currentStory, setCurrentStory] = useState(null);
const [comments, setComments] = useState([]);
useEffect(() => {
fetchStories();
fetchUser();
}, []);
const fetchStories = async () => {
const response = await fetch('/stories');
const data = await response.json();
setStories(data);
};
const fetchUser = async () => {
const response = await fetch('/user');
const data = await response.json();
setUser(data.username);
};
const fetchComments = async (storyId) => {
const response = await fetch(`/comments/${storyId}`);
const data = await response.json();
setComments(data);
};
const submitStory = async (event) => {
event.preventDefault();
const title = event.target.title.value;
const url = event.target.url.value;
await fetch('/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, url }),
});
fetchStories();
event.target.reset();
};
const upvoteStory = async (storyId) => {
if (!user) return;
await fetch('/upvote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ storyId }),
});
fetchStories();
};
const submitComment = async (event, parentId = null) => {
event.preventDefault();
const content = event.target.content.value;
await fetch('/comment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ storyId: currentStory.id, content, parentId }),
});
fetchComments(currentStory.id);
event.target.reset();
};
const CommentComponent = ({ comment }) => (
<div className="comment">
<div>{comment.content}</div>
<div className="comment-details">
by {comment.username} | {comment.timestamp}
</div>
{user && (
<form onSubmit={(e) => submitComment(e, comment.id)}>
<textarea name="content" required></textarea>
<button type="submit">Reply</button>
</form>
)}
{comment.replies && comment.replies.map(reply => (
<CommentComponent key={reply.id} comment={reply} />
))}
</div>
);
return (
<html>
<head>
<title>Hacker News Clone</title>
<style dangerouslySetInnerHTML={{ __html: css }} />
</head>
<body>
<div className="header">