From 51a35cbadeb927b524c55d59d90f6545f5b0205f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:38:21 +0000 Subject: [PATCH 1/2] feature: add React models and API foundations Co-Authored-By: Paul Abboud --- src/react/api/hackernews.ts | 46 ++++++++++++++++++++++++++++++ src/react/models/comment.ts | 10 +++++++ src/react/models/feed-type.type.ts | 1 + src/react/models/poll-result.ts | 4 +++ src/react/models/settings.ts | 7 +++++ src/react/models/story.ts | 21 ++++++++++++++ src/react/models/user.ts | 8 ++++++ src/react/utils/comments.ts | 8 ++++++ tsconfig.react.json | 5 ++-- 9 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 src/react/api/hackernews.ts create mode 100644 src/react/models/comment.ts create mode 100644 src/react/models/feed-type.type.ts create mode 100644 src/react/models/poll-result.ts create mode 100644 src/react/models/settings.ts create mode 100644 src/react/models/story.ts create mode 100644 src/react/models/user.ts create mode 100644 src/react/utils/comments.ts diff --git a/src/react/api/hackernews.ts b/src/react/api/hackernews.ts new file mode 100644 index 000000000..654e0cea4 --- /dev/null +++ b/src/react/api/hackernews.ts @@ -0,0 +1,46 @@ +import { FeedType } from '../models/feed-type.type'; +import { PollResult } from '../models/poll-result'; +import { Story } from '../models/story'; +import { User } from '../models/user'; + +const BASE_URL = 'https://node-hnapi.herokuapp.com'; + +async function getJson(path: string): Promise { + const response = await fetch(`${BASE_URL}${path}`); + + if (!response.ok) { + throw new Error(`Request failed with status ${response.status}`); + } + + return response.json() as Promise; +} + +export function fetchFeed(feedType: FeedType, page: number): Promise { + return getJson(`/${feedType}?page=${page}`); +} + +export async function fetchItemContent(id: number): Promise { + const story = await getJson(`/item/${id}`); + + if (story.type === 'poll') { + story.poll_votes_count = 0; + const pollResults = await Promise.all( + story.poll.map((_, index) => fetchPollContent(story.id + index + 1)) + ); + + pollResults.forEach((pollResult, index) => { + story.poll[index] = pollResult; + story.poll_votes_count += pollResult.points; + }); + } + + return story; +} + +export function fetchPollContent(id: number): Promise { + return getJson(`/item/${id}`); +} + +export function fetchUser(id: string): Promise { + return getJson(`/user/${id}`); +} diff --git a/src/react/models/comment.ts b/src/react/models/comment.ts new file mode 100644 index 000000000..afc289a2d --- /dev/null +++ b/src/react/models/comment.ts @@ -0,0 +1,10 @@ +export class Comment { + id: number; + level: number; + user: string; + time: number; + time_ago: string; + content: string; + deleted: boolean; + comments: Comment[]; +} diff --git a/src/react/models/feed-type.type.ts b/src/react/models/feed-type.type.ts new file mode 100644 index 000000000..5d3c15ba1 --- /dev/null +++ b/src/react/models/feed-type.type.ts @@ -0,0 +1 @@ +export type FeedType = 'poll' | 'story' | 'job'; diff --git a/src/react/models/poll-result.ts b/src/react/models/poll-result.ts new file mode 100644 index 000000000..bd8977d46 --- /dev/null +++ b/src/react/models/poll-result.ts @@ -0,0 +1,4 @@ +export class PollResult { + points: number; + content: string; +} diff --git a/src/react/models/settings.ts b/src/react/models/settings.ts new file mode 100644 index 000000000..cf08c432b --- /dev/null +++ b/src/react/models/settings.ts @@ -0,0 +1,7 @@ +export interface Settings { + showSettings: boolean; + openLinkInNewTab: boolean; + theme: string; + titleFontSize: string; + listSpacing: string; +} diff --git a/src/react/models/story.ts b/src/react/models/story.ts new file mode 100644 index 000000000..dab22e181 --- /dev/null +++ b/src/react/models/story.ts @@ -0,0 +1,21 @@ +import { Comment } from './comment'; +import { FeedType } from './feed-type.type'; +import { PollResult } from './poll-result'; + +export class Story { + id: number; + title: string; + points: number; + user: string; + time: number; + time_ago: number; + type: FeedType; + url: string; + domain: string; + comments: Comment[]; + comments_count: number; + poll: PollResult[]; + poll_votes_count: number; + deleted: boolean; + dead: boolean; +} diff --git a/src/react/models/user.ts b/src/react/models/user.ts new file mode 100644 index 000000000..33f0a7aa9 --- /dev/null +++ b/src/react/models/user.ts @@ -0,0 +1,8 @@ +export class User { + id: string; + crated_time: number; + created: string; + karma: number; + avg: number; + about: string; +} diff --git a/src/react/utils/comments.ts b/src/react/utils/comments.ts new file mode 100644 index 000000000..b2ed1f5f9 --- /dev/null +++ b/src/react/utils/comments.ts @@ -0,0 +1,8 @@ +export function formatComments(count?: number): string { + if (count !== undefined && count > 0) { + const label = count === 1 ? 'comment' : 'comments'; + return `${count} ${label}`; + } + + return 'discuss'; +} diff --git a/tsconfig.react.json b/tsconfig.react.json index 0435e7ec7..53b11cb2f 100644 --- a/tsconfig.react.json +++ b/tsconfig.react.json @@ -14,8 +14,9 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "types": ["react", "react-dom"] + "types": ["react", "react-dom"], + "strictPropertyInitialization": false }, - "include": ["src/main.tsx", "src/react/**/*.tsx"], + "include": ["src/main.tsx", "src/react/**/*.ts", "src/react/**/*.tsx"], "exclude": ["node_modules"] } From 3cecd694c0583e6b77cb9da48658967779814cff Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:39:24 +0000 Subject: [PATCH 2/2] feature: type feed routes separately from item types Co-Authored-By: Paul Abboud --- src/react/api/hackernews.ts | 6 +++--- src/react/models/feed-name.type.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 src/react/models/feed-name.type.ts diff --git a/src/react/api/hackernews.ts b/src/react/api/hackernews.ts index 654e0cea4..e0d7900c5 100644 --- a/src/react/api/hackernews.ts +++ b/src/react/api/hackernews.ts @@ -1,4 +1,4 @@ -import { FeedType } from '../models/feed-type.type'; +import { FeedName } from '../models/feed-name.type'; import { PollResult } from '../models/poll-result'; import { Story } from '../models/story'; import { User } from '../models/user'; @@ -15,8 +15,8 @@ async function getJson(path: string): Promise { return response.json() as Promise; } -export function fetchFeed(feedType: FeedType, page: number): Promise { - return getJson(`/${feedType}?page=${page}`); +export function fetchFeed(feedName: FeedName, page: number): Promise { + return getJson(`/${feedName}?page=${page}`); } export async function fetchItemContent(id: number): Promise { diff --git a/src/react/models/feed-name.type.ts b/src/react/models/feed-name.type.ts new file mode 100644 index 000000000..a87b6b76b --- /dev/null +++ b/src/react/models/feed-name.type.ts @@ -0,0 +1 @@ +export type FeedName = 'news' | 'newest' | 'show' | 'ask' | 'jobs';