├── .gitignore
├── README.md
├── api
└── index.js
├── app
├── components
│ ├── footer.js
│ ├── header.js
│ ├── item.js
│ ├── item.sc.js
│ ├── logo.js
│ ├── meta.js
│ ├── nav.js
│ ├── page.js
│ ├── server-info.server.js
│ ├── spinner.js
│ ├── stories.js
│ └── story.js
├── entry.client.jsx
├── entry.server.jsx
├── lib
│ ├── fetch-data.js
│ ├── get-item.js
│ ├── get-story-ids.js
│ ├── time-ago.js
│ └── use-data.js
├── pages
│ ├── _app.js
│ ├── _document.js
│ ├── csr.js
│ ├── index.js
│ ├── rsc.server.js
│ ├── slow.server.js
│ └── ssr.js
└── root.jsx
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
└── favicon.ico
├── remix.config.js
└── vercel.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | .cache
4 | .vercel
5 | .output
6 |
7 | public/build
8 | api/build
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to Remix!
2 |
3 | - [Remix Docs](https://remix.run/docs)
4 |
5 | ## Deployment
6 |
7 | After having run the `create-remix` command and selected "Vercel" as a deployment target, you only need to [import your Git repository](https://vercel.com/new) into Vercel, and it will be deployed.
8 |
9 | If you'd like to avoid using a Git repository, you can also deploy the directory by running [Vercel CLI](https://vercel.com/cli):
10 |
11 | ```sh
12 | npm i -g vercel
13 | vercel
14 | ```
15 |
16 | It is generally recommended to use a Git repository, because future commits will then automatically be deployed by Vercel, through its [Git Integration](https://vercel.com/docs/concepts/git).
17 |
18 | ## Development
19 |
20 | To run your Remix app locally, make sure your project's local dependencies are installed:
21 |
22 | ```sh
23 | npm install
24 | ```
25 |
26 | Afterwards, start the Remix development server like so:
27 |
28 | ```sh
29 | npm run dev
30 | ```
31 |
32 | Open up [http://localhost:3000](http://localhost:3000) and you should be ready to go!
33 |
34 | If you're used to using the `vercel dev` command provided by [Vercel CLI](https://vercel.com/cli) instead, you can also use that, but it's not needed.
35 |
--------------------------------------------------------------------------------
/api/index.js:
--------------------------------------------------------------------------------
1 | const { createRequestHandler } = require("@remix-run/vercel");
2 |
3 | module.exports = createRequestHandler({
4 | build: require("./build")
5 | });
6 |
--------------------------------------------------------------------------------
/app/components/footer.js:
--------------------------------------------------------------------------------
1 | export default function Footer() {
2 | return (
3 |
4 |
5 | Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC |
6 | Contact
7 |
8 |
9 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/app/components/header.js:
--------------------------------------------------------------------------------
1 | import Nav from "./nav";
2 | import Logo from "./logo";
3 |
4 | export default function Header() {
5 | return (
6 | <>
7 |
65 |
83 | >
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/app/components/item.js:
--------------------------------------------------------------------------------
1 | import Story from "./story";
2 | import Comment from "./comment";
3 | import CommentForm from "./comment-form";
4 |
5 | export default function Item({ story, comments = null }) {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {comments ? (
16 | comments.map((comment) =>
)
17 | ) : (
18 |
Loading…
19 | )}
20 |
21 |
22 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/app/components/item.sc.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from "react";
2 | import { useRouter } from "next/router";
3 |
4 | import Page from "./page";
5 | import Item from "./item";
6 |
7 | import getItem from "../lib/get-item";
8 | import getComments from "../app/lib/get-comments";
9 |
10 | let commentsData = {};
11 | let storyData = {};
12 | let fetchDataPromise = {};
13 |
14 | function ItemPageWithData({ id }) {
15 | if (!commentsData[id]) {
16 | if (!fetchDataPromise[id]) {
17 | fetchDataPromise[id] = getItem(id)
18 | .then((story) => {
19 | storyData[id] = story;
20 | return getComments(story.comments);
21 | })
22 | .then((c) => (commentsData[id] = c));
23 | }
24 | throw fetchDataPromise[id];
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 | );
32 | }
33 |
34 | export default function ItemPage() {
35 | const { id } = useRouter().query;
36 |
37 | if (!id) return null;
38 |
39 | return (
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/app/components/logo.js:
--------------------------------------------------------------------------------
1 | export default function Logo() {
2 | return (
3 | <>
4 | N
5 |
19 | >
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/app/components/meta.js:
--------------------------------------------------------------------------------
1 | export default function Meta() {
2 | return (
3 | <>
4 |
5 |
6 |
54 |
55 | );
56 |
--------------------------------------------------------------------------------
/app/components/story.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | import timeAgo from "../lib/time-ago";
4 |
5 | export default function Story({
6 | id,
7 | title,
8 | date,
9 | url,
10 | user,
11 | score,
12 | commentsCount,
13 | }) {
14 | const { host } = url ? new URL(url) : { host: "#" };
15 | const [voted, setVoted] = useState(false);
16 |
17 | return (
18 |
50 | );
51 | }
52 |
53 | const plural = (n, s) => s + (n === 0 || n > 1 ? "s" : "");
54 |
--------------------------------------------------------------------------------
/app/entry.client.jsx:
--------------------------------------------------------------------------------
1 | import { hydrate } from "react-dom";
2 | import { RemixBrowser } from "remix";
3 |
4 | hydrate( , document);
5 |
--------------------------------------------------------------------------------
/app/entry.server.jsx:
--------------------------------------------------------------------------------
1 | import { renderToString } from "react-dom/server";
2 | import { RemixServer } from "remix";
3 |
4 | export default function handleRequest(
5 | request,
6 | responseStatusCode,
7 | responseHeaders,
8 | remixContext
9 | ) {
10 | let markup = renderToString(
11 |
12 | );
13 |
14 | responseHeaders.set("Content-Type", "text/html");
15 | responseHeaders.set("Cache-Control", "s-maxage=5, stale-while-revalidate=5");
16 |
17 | return new Response("" + markup, {
18 | status: responseStatusCode,
19 | headers: responseHeaders,
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/app/lib/fetch-data.js:
--------------------------------------------------------------------------------
1 | export default async function fetchData(type, delay = 0) {
2 | const [res] = await Promise.all([
3 | fetch(`https://hacker-news.firebaseio.com/v0/${type}.json`),
4 | new Promise((res) => setTimeout(res, Math.random() * delay)),
5 | ]);
6 | if (res.status !== 200) {
7 | throw new Error(`Status ${res.status}`);
8 | }
9 | return res.json();
10 | }
11 |
--------------------------------------------------------------------------------
/app/lib/get-item.js:
--------------------------------------------------------------------------------
1 | import fetchData from './fetch-data'
2 |
3 | export default async function (id) {
4 | const val = await fetchData(`item/${id}`)
5 | if (val) {
6 | return transform(val)
7 | } else {
8 | return null
9 | }
10 | }
11 |
12 | export function transform(val) {
13 | return {
14 | id: val.id,
15 | url: val.url || '',
16 | user: val.by,
17 | // time is seconds since epoch, not ms
18 | date: new Date(val.time * 1000).getTime() || 0,
19 | // sometimes `kids` is `undefined`
20 | comments: val.kids || [],
21 | commentsCount: val.descendants || 0,
22 | score: val.score,
23 | title: val.title,
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/lib/get-story-ids.js:
--------------------------------------------------------------------------------
1 | import fetchData from './fetch-data'
2 |
3 | export default async function (
4 | type = 'topstories',
5 | { page = 1, max = 30 } = {}
6 | ) {
7 | const start = max * (page - 1)
8 | const end = start + max
9 | const ids = await fetchData(type)
10 | return ids.slice(start, end)
11 | }
12 |
--------------------------------------------------------------------------------
/app/lib/time-ago.js:
--------------------------------------------------------------------------------
1 | import ms from 'ms'
2 |
3 | const map = {
4 | s: 'seconds',
5 | ms: 'milliseconds',
6 | m: 'minutes',
7 | h: 'hours',
8 | d: 'days',
9 | }
10 |
11 | export default (date) =>
12 | date ? ms(new Date() - date).replace(/[a-z]+/, (str) => ' ' + map[str]) : ''
13 |
--------------------------------------------------------------------------------
/app/lib/use-data.js:
--------------------------------------------------------------------------------
1 | const cache = {}
2 |
3 | export default function useData(key, fetcher) {
4 | if (!cache[key]) {
5 | let data
6 | let promise
7 | cache[key] = () => {
8 | if (data !== undefined) return data
9 | if (!promise) promise = fetcher().then((r) => (data = r))
10 | throw promise
11 | }
12 | }
13 | return cache[key]()
14 | }
15 |
--------------------------------------------------------------------------------
/app/pages/_app.js:
--------------------------------------------------------------------------------
1 | function MyApp({ Component, pageProps }) {
2 | return
3 | }
4 |
5 | export default MyApp
6 |
--------------------------------------------------------------------------------
/app/pages/_document.js:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/app/pages/csr.js:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 |
3 | // Shared Components
4 | import Spinner from "../components/spinner";
5 |
6 | // Client Components
7 | import Page from "../components/page";
8 | import Story from "../components/story";
9 |
10 | // Utils
11 | import fetchData from "../lib/fetch-data";
12 | import { transform } from "../lib/get-item";
13 | import useData from "../lib/use-data";
14 |
15 | function StoryWithData({ id }) {
16 | const data = useData(`s-${id}`, () =>
17 | fetchData(`item/${id}`).then(transform)
18 | );
19 | return ;
20 | }
21 |
22 | function NewsWithData() {
23 | const storyIds = useData("top", () => fetchData("topstories"));
24 | return (
25 | <>
26 | {storyIds.slice(0, 30).map((id) => {
27 | return (
28 | }>
29 |
30 |
31 | );
32 | })}
33 | >
34 | );
35 | }
36 |
37 | export default function News() {
38 | return (
39 |
40 | {typeof window === "undefined" ? (
41 |
42 | ) : (
43 | }>
44 |
45 |
46 | )}
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/app/pages/index.js:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return (
3 |
4 |
React Server Components in Next.js
5 |
Without Streaming
6 |
11 |
16 |
React Server Components with Streaming
17 |
22 |
27 |
28 |
48 |
93 |
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/app/pages/rsc.server.js:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 |
3 | // Shared Components
4 | import Spinner from "../components/spinner";
5 |
6 | // Server Components
7 | import SystemInfo from "../components/server-info.server";
8 |
9 | // Client Components
10 | import Page from "../components/page";
11 | import Story from "../components/story";
12 | import Footer from "../components/footer";
13 |
14 | // Utils
15 | import fetchData from "../lib/fetch-data";
16 | import { transform } from "../lib/get-item";
17 | import useData from "../lib/use-data";
18 |
19 | function StoryWithData({ id }) {
20 | const data = useData(`s-${id}`, () =>
21 | fetchData(`item/${id}`).then(transform)
22 | );
23 | return ;
24 | }
25 |
26 | function NewsWithData() {
27 | const storyIds = useData("top", () => fetchData("topstories"));
28 | return (
29 | <>
30 | {storyIds.slice(0, 30).map((id) => {
31 | return (
32 | } key={id}>
33 |
34 |
35 | );
36 | })}
37 | >
38 | );
39 | }
40 |
41 | export default function News() {
42 | return (
43 |
44 | }>
45 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/app/pages/slow.server.js:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 |
3 | // Shared Components
4 | import Spinner from "../components/spinner";
5 |
6 | // Server Components
7 | import SystemInfo from "../components/server-info.server";
8 |
9 | // Client Components
10 | import Page from "../components/page";
11 | import Story from "../components/story";
12 | import Footer from "../components/footer";
13 |
14 | // Utils
15 | import fetchData from "../lib/fetch-data";
16 | import { transform } from "../lib/get-item";
17 | import useData from "../lib/use-data";
18 |
19 | function StoryWithData({ id }) {
20 | const data = useData(`s-${id}`, () =>
21 | fetchData(`item/${id}`).then(transform)
22 | );
23 | return ;
24 | }
25 |
26 | function NewsWithData() {
27 | const storyIds = useData("top", () => fetchData("topstories", 500));
28 | return (
29 | <>
30 | {storyIds.slice(0, 30).map((id) => {
31 | return (
32 | }>
33 |
34 |
35 | );
36 | })}
37 | >
38 | );
39 | }
40 |
41 | export default function News() {
42 | return (
43 |
44 | }>
45 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/app/pages/ssr.js:
--------------------------------------------------------------------------------
1 | import Page from "../components/page";
2 | import Story from "../components/story";
3 | import Footer from "../components/footer";
4 |
5 | // Utils
6 | import fetchData from "../lib/fetch-data";
7 | import { transform } from "../lib/get-item";
8 |
9 | export async function loader() {
10 | const storyIds = await fetchData("topstories");
11 | const data = await Promise.all(
12 | storyIds.slice(0, 30).map((id) => fetchData(`item/${id}`).then(transform))
13 | );
14 | return data;
15 | }
16 |
17 | export default function News({ data }) {
18 | return (
19 |
20 | {data.map((item, i) => {
21 | return ;
22 | })}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/root.jsx:
--------------------------------------------------------------------------------
1 | import { Links, LiveReload, Meta, Scripts, useLoaderData } from "remix";
2 | import { default as News, loader } from "./pages/ssr";
3 | import ExtraMeta from "./components/meta";
4 |
5 | export { loader };
6 |
7 | export default function Document() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {process.env.NODE_ENV === "development" && }
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "~/*": ["./app/*"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "remix-app-template-js",
4 | "description": "",
5 | "license": "",
6 | "scripts": {
7 | "build": "remix build",
8 | "dev": "remix dev",
9 | "postinstall": "remix setup node",
10 | "start": "remix-serve build"
11 | },
12 | "dependencies": {
13 | "@remix-run/react": "^1.0.6",
14 | "@remix-run/serve": "^1.0.6",
15 | "@remix-run/vercel": "^1.0.6",
16 | "ms": "^2.1.3",
17 | "react": "^17.0.2",
18 | "react-dom": "^17.0.2",
19 | "remix": "^1.0.6"
20 | },
21 | "devDependencies": {
22 | "@remix-run/dev": "^1.0.6"
23 | },
24 | "engines": {
25 | "node": ">=14"
26 | },
27 | "sideEffects": false
28 | }
29 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryanflorence/remix-hn/23a3b467fad4c13ca2ed9e38225d390df30716ba/public/favicon.ico
--------------------------------------------------------------------------------
/remix.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('@remix-run/dev/config').AppConfig}
3 | */
4 | module.exports = {
5 | appDirectory: "app",
6 | browserBuildDirectory: "public/build",
7 | publicPath: "/build/",
8 | serverBuildDirectory: "api/build",
9 | };
10 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "build": {
3 | "env": {
4 | "ENABLE_FILE_SYSTEM_API": "1"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------