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
/** @jsxImportSource https://esm.sh/react */
import React, { useCallback, useEffect, useReducer, useRef, useState } from "https://esm.sh/react";
import { hydrateRoot } from "https://esm.sh/react-dom/client";
import { renderToString } from "https://esm.sh/react-dom/server";
import { hnSearch } from "https://esm.town/v/stevekrouse/hnSearch";
import About from "https://esm.town/v/vawogbemi/whoIsHiringAbout";
function App() {
const tabs = { "/": "Home", "/about": "About" };
const [activeTab, setActiveTab] = useState("/");
const [showScrollTop, setShowScrollTop] = useState(false);
const [selectedComment, setSelectedComment] = useState(null);
const reducer = (state: {
query: string;
story: string;
page: number;
loading: boolean;
hasMore: boolean;
}, action: { type: string; value: string | boolean | number }) => {
switch (action.type) {
case "query":
return { ...state, query: action.value as string, page: 1 };
case "story":
return { ...state, story: action.value as string, page: 1 };
case "page":
return { ...state, page: action.value as number };
case "loading":
return { ...state, loading: action.value as boolean };
case "hasMore":
return { ...state, hasMore: action.value as boolean };
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, {
query: "",
story: "",
page: 1,
loading: true,
hasMore: true,
});
const [stories, setStories] = useState<{ date: string; objectID: string }[]>([]);
const [comments, setComments] = useState<{ author: string; comment_text: string; objectID: string }[]>([]);
const observer = useRef<IntersectionObserver>();
const lastCommentElementRef = useCallback((node: HTMLDivElement) => {
if (state.loading) return;
if (observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && state.hasMore) {
dispatch({ type: "page", value: state.page + 1 });
}
});
if (node) observer.current.observe(node);
}, [state.loading, state.hasMore]);
const fetchStories = async () => {
try {
const response = await fetch("/api/stories");
if (!response.ok) throw new Error("Failed to fetch dates");
const data = await response.json();
setStories(data);
dispatch({ type: "story", value: data[0].objectID });
}
catch (err) {
console.error("Error fetching stories:", err);
}
};
const fetchComments = async (query: string, story: string, page: number) => {
try {
dispatch({ type: "loading", value: true });
const response = await fetch(`/api/comments?query=${encodeURIComponent(query)}&story=${story}&page=${page}`);
if (!response.ok) throw new Error("Failed to fetch comments");
const data = await response.json();
setComments(prevComments => page === 1 ? data.hits : [...prevComments, ...data.hits]);
dispatch({ type: "hasMore", value: data.hits.length > 0 });
dispatch({ type: "loading", value: false });
} catch (err) {
console.error("Error fetching comments:", err);
dispatch({ type: "loading", value: false });
}
};
const handleSearch = (newQuery = state.query) => {
setComments([]);
dispatch({ type: "page", value: 1 });
dispatch({ type: "loading", value: true });
dispatch({ type: "hasMore", value: true });
dispatch({ type: "query", value: newQuery });
fetchComments(newQuery, state.story, 1);
};
useEffect(() => {
fetchStories();