diff --git a/src/components/Item.scss b/src/components/Item.scss
new file mode 100644
index 00000000..1e7cd4ad
--- /dev/null
+++ b/src/components/Item.scss
@@ -0,0 +1,69 @@
+@use "../app/shared/scss/media" as *;
+
+p {
+ margin: 2px 0;
+
+ @media #{$mobile-only} {
+ margin-bottom: 5px;
+ margin-top: 0;
+ }
+}
+
+a {
+ cursor: pointer;
+ text-decoration: none;
+}
+
+.title {
+ font-size: 16px;
+ font-family: Verdana, Geneva, sans-serif;
+}
+
+.subtext-laptop {
+ font-size: 12px;
+ font-weight: bold;
+ letter-spacing: 0.5px;
+
+ a {
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ @media #{$mobile-only} {
+ display: none;
+ }
+}
+
+.subtext-palm {
+ font-size: 13px;
+ font-weight: bold;
+ letter-spacing: 0.5px;
+
+ a {
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .details {
+ margin-top: 5px;
+
+ .right {
+ float: right;
+ }
+ }
+
+ @media #{$laptop-only} {
+ display: none;
+ }
+}
+
+.domain {
+ color: #696969;
+ letter-spacing: 0.5px;
+}
+
+.item-details {
+ padding: 10px;
+}
diff --git a/src/components/Item.tsx b/src/components/Item.tsx
new file mode 100644
index 00000000..6dff707a
--- /dev/null
+++ b/src/components/Item.tsx
@@ -0,0 +1,83 @@
+import { Link } from 'react-router-dom';
+import type { Story } from '../types/Story';
+import { useSettings } from '../hooks/useSettings';
+import { formatCommentCount } from '../utils/formatCommentCount';
+import './Item.scss';
+
+interface ItemProps {
+ item: Story;
+ index: number;
+}
+
+export function Item({ item }: ItemProps) {
+ const { settings } = useSettings();
+ const hasUrl = item.url?.indexOf('http') === 0;
+
+ return (
+
+ {hasUrl ? (
+
+
+ {item.title}
+
+ {item.domain && ({item.domain})}
+
+ ) : (
+
+
+ {item.title}
+
+
+ )}
+
+
+ {item.type !== 'job' && (
+
+
+ {item.user}
+
+ {item.points} ★
+
+ )}
+
+ {item.time_ago}
+ {item.type !== 'job' && (
+
+ {' '}• {formatCommentCount(item.comments_count)}
+
+ )}
+
+
+
+
+ {item.type !== 'job' && (
+
+ {item.points} points by{' '}
+ {item.user}
+
+ )}
+
+ {item.time_ago}
+ {item.type !== 'job' && (
+
+ {' '}|{' '}
+
+ {formatCommentCount(item.comments_count)}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/pages/Feed.scss b/src/pages/Feed.scss
new file mode 100644
index 00000000..248e396d
--- /dev/null
+++ b/src/pages/Feed.scss
@@ -0,0 +1,104 @@
+@use "../app/shared/scss/media" as *;
+
+a {
+ text-decoration: none;
+ font-weight: bold;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+ol {
+ padding: 0 40px;
+ margin: 0;
+
+ @media #{$mobile-only} {
+ box-sizing: border-box;
+ list-style: none;
+ padding: 0 10px;
+ }
+
+ li {
+ position: relative;
+ transition: background-color 0.2s ease;
+ }
+}
+
+.list-margin {
+ @media #{$mobile-only} {
+ margin-top: 55px;
+ }
+}
+
+.main-content {
+ position: relative;
+ width: 100%;
+ min-height: 100vh;
+ transition: opacity 0.2s ease;
+ box-sizing: border-box;
+ padding: 8px 0;
+ z-index: 0;
+}
+
+.post {
+ padding: 10px 0 10px 5px;
+ transition: background-color 0.2s ease;
+ border-bottom: 1px solid #cececb;
+
+ .itemNum {
+ color: #696969;
+ position: absolute;
+ width: 30px;
+ text-align: right;
+ left: 0;
+ top: 4px;
+ }
+}
+
+.item-block {
+ display: block;
+}
+
+.nav {
+ padding: 10px 40px;
+ margin-top: 10px;
+ font-size: 17px;
+
+ a {
+ @media #{$mobile-only} {
+ text-decoration: none;
+ }
+ }
+
+ @media #{$mobile-only} {
+ margin: 20px 0;
+ text-align: center;
+ padding: 10px 80px;
+ height: 20px;
+ }
+
+ .prev {
+ padding-right: 20px;
+
+ @media #{$mobile-only} {
+ float: left;
+ padding-right: 0;
+ }
+ }
+
+ .more {
+ @media #{$mobile-only} {
+ float: right;
+ }
+ }
+}
+
+.job-header {
+ font-size: 15px;
+ padding: 0 40px 10px;
+
+ @media #{$mobile-only} {
+ padding: 60px 15px 25px 15px;
+ }
+}
diff --git a/src/pages/Feed.tsx b/src/pages/Feed.tsx
new file mode 100644
index 00000000..90defbc5
--- /dev/null
+++ b/src/pages/Feed.tsx
@@ -0,0 +1,113 @@
+import { useEffect, useReducer } from 'react';
+import { useParams, Link } from 'react-router-dom';
+import { fetchFeed } from '../api/hackerNewsApi';
+import type { Story } from '../types/Story';
+import { Item } from '../components/Item';
+import { Loader } from '../components/Loader';
+import { ErrorMessage } from '../components/ErrorMessage';
+import './Feed.scss';
+
+interface FeedProps {
+ feedType: string;
+}
+
+interface FeedState {
+ items: Story[] | null;
+ errorMessage: string;
+}
+
+type FeedAction =
+ | { type: 'loading' }
+ | { type: 'loaded'; items: Story[] }
+ | { type: 'error'; message: string };
+
+function feedReducer(_state: FeedState, action: FeedAction): FeedState {
+ switch (action.type) {
+ case 'loading':
+ return { items: null, errorMessage: '' };
+ case 'loaded':
+ return { items: action.items, errorMessage: '' };
+ case 'error':
+ return { items: null, errorMessage: action.message };
+ }
+}
+
+export function Feed({ feedType }: FeedProps) {
+ const { page } = useParams<{ page: string }>();
+ const pageNum = page ? parseInt(page, 10) : 1;
+ const [state, dispatch] = useReducer(feedReducer, {
+ items: null,
+ errorMessage: '',
+ });
+
+ useEffect(() => {
+ let cancelled = false;
+ dispatch({ type: 'loading' });
+
+ fetchFeed(feedType, pageNum)
+ .then((data) => {
+ if (!cancelled) {
+ dispatch({ type: 'loaded', items: data });
+ window.scrollTo(0, 0);
+ }
+ })
+ .catch(() => {
+ if (!cancelled) {
+ dispatch({
+ type: 'error',
+ message: `Could not load ${feedType} stories.`,
+ });
+ }
+ });
+
+ return () => { cancelled = true; };
+ }, [feedType, pageNum]);
+
+ const { items, errorMessage } = state;
+ const listStart = (pageNum - 1) * 30 + 1;
+
+ return (
+
+ {!items && !errorMessage &&
}
+ {!items && errorMessage &&
}
+
+ {items && (
+
+ {feedType === 'jobs' && (
+
+ These are jobs at startups that were funded by Y Combinator. You
+ can also get a job at a YC startup through{' '}
+ Triplebyte.
+
+ )}
+
+ {feedType !== 'new' && (
+
+ {items.map((item, index) => (
+ -
+
+
+ ))}
+
+ )}
+
+
+ {listStart !== 1 && (
+
+ ‹ Prev
+
+ )}
+ {items.length === 30 && (
+
+ More ›
+
+ )}
+
+
+ )}
+
+ );
+}