├── public └── favicon.ico ├── .gitignore ├── app ├── components │ ├── spinner.js │ ├── server-info.server.js │ ├── logo.js │ ├── footer.js │ ├── page.js │ ├── nav.js │ ├── item.js │ ├── item.sc.js │ ├── stories.js │ ├── story.js │ ├── header.js │ └── meta.js ├── pages │ ├── _app.js │ ├── _document.js │ ├── ssr.js │ ├── csr.js │ ├── rsc.server.js │ ├── slow.server.js │ └── index.js ├── entry.client.jsx ├── lib │ ├── time-ago.js │ ├── get-story-ids.js │ ├── use-data.js │ ├── fetch-data.js │ └── get-item.js ├── entry.server.jsx └── root.jsx ├── vercel.json ├── jsconfig.json ├── api └── index.js ├── remix.config.js ├── package.json └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanflorence/remix-hn/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .cache 4 | .vercel 5 | .output 6 | 7 | public/build 8 | api/build 9 | -------------------------------------------------------------------------------- /app/components/spinner.js: -------------------------------------------------------------------------------- 1 | export default function Spinner() { 2 | return
; 3 | } 4 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "env": { 4 | "ENABLE_FILE_SYSTEM_API": "1" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/pages/_app.js: -------------------------------------------------------------------------------- 1 | function MyApp({ Component, pageProps }) { 2 | return 3 | } 4 | 5 | export default MyApp 6 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~/*": ["./app/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/entry.client.jsx: -------------------------------------------------------------------------------- 1 | import { hydrate } from "react-dom"; 2 | import { RemixBrowser } from "remix"; 3 | 4 | hydrate(, document); 5 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | const { createRequestHandler } = require("@remix-run/vercel"); 2 | 3 | module.exports = createRequestHandler({ 4 | build: require("./build") 5 | }); 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/components/server-info.server.js: -------------------------------------------------------------------------------- 1 | export default function ServerInfo() { 2 | return ( 3 |
4 | Rendered at {new Date().toTimeString()} with Edge Middleware. 5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /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/pages/_document.js: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /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/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/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/components/logo.js: -------------------------------------------------------------------------------- 1 | export default function Logo() { 2 | return ( 3 | <> 4 | N 5 | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 |