├── .gitignore ├── .npmrc ├── .stackblitzrc ├── README.md ├── astro.config.mjs ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── sandbox.config.json ├── src ├── components │ ├── Comment.astro │ ├── Layout.astro │ ├── Story.astro │ └── Toggle.jsx ├── lib │ └── api.js ├── pages │ ├── [...stories].astro │ ├── stories │ │ └── [id].astro │ └── users │ │ └── [id].astro └── styles │ └── global.css └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | .output/ 4 | 5 | # dependencies 6 | node_modules/ 7 | 8 | # logs 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | 14 | 15 | # environment variables 16 | .env 17 | .env.production 18 | 19 | # macOS-specific files 20 | .DS_Store 21 | 22 | # Local Netlify folder 23 | .netlify 24 | netlify 25 | .vscode 26 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Expose Astro dependencies for `pnpm` users 2 | shamefully-hoist=true 3 | -------------------------------------------------------------------------------- /.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "startCommand": "npm start", 3 | "env": { 4 | "ENABLE_CJS_IMPORTS": true 5 | } 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Astro-Solid Hacker News 2 | 3 | This repos is a simple demo of using Astro + SolidJS integration to do a Hackernews Clone with Partial Hydration. It is setup with the Netlify Adapter and you can see it deployed [here](https://astro-solid-hn.netlify.app). 4 | 5 | ## 🧞 Commands 6 | 7 | All commands are run from the root of the project, from a terminal: 8 | 9 | | Command | Action | 10 | |:---------------- |:-------------------------------------------- | 11 | | `npm install` | Installs dependencies | 12 | | `npm run dev` | Starts local dev server at `localhost:3000` | 13 | | `npm run build` | Build your production site to `./dist/` | 14 | | `npm run preview` | Preview your build locally, before deploying | 15 | 16 | ## 👀 Want to learn more? 17 | 18 | Feel free to check [our documentation](https://github.com/withastro/astro) or jump into our [Discord server](https://astro.build/chat). 19 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import netlify from "@astrojs/netlify"; 3 | 4 | import solid from "@astrojs/solid-js"; 5 | 6 | // https://astro.build/config 7 | export default defineConfig({ 8 | adapter: netlify(), 9 | integrations: [solid()] 10 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-solid-hn", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview" 10 | }, 11 | "devDependencies": { 12 | "@astrojs/solid-js": "^0.1.2", 13 | "astro": "^1.0.0-beta.20", 14 | "solid-js": "^1.3.14" 15 | }, 16 | "dependencies": { 17 | "@astrojs/netlify": "^0.3.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryansolid/astro-solid-hackernews/abbc08b8c5e53fdd018e39d77c5e14479299b66e/public/favicon.ico -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "infiniteLoopProtection": true, 3 | "hardReloadOnChange": false, 4 | "view": "browser", 5 | "template": "node", 6 | "container": { 7 | "port": 3000, 8 | "startScript": "start", 9 | "node": "14" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Comment.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Toggle from "./Toggle"; 3 | const { comment } = Astro.props; 4 | --- 5 |
  • 6 |
    7 | ${comment.user} 8 | {` ${comment.time_ago}`} 9 |
    10 |
    11 | { 12 | comment.comments.length ? 13 | { 14 | comment.comments.map(nested => ) 15 | } : "" 16 | } 17 |
  • -------------------------------------------------------------------------------- /src/components/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '../styles/global.css'; 3 | --- 4 | 5 | 6 | 7 | 8 | 9 | 10 | Astro/Solid - Hacker News 11 | 12 | 13 |
    14 | 34 |
    35 | 36 | 37 | -------------------------------------------------------------------------------- /src/components/Story.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { story } = Astro.props; 3 | --- 4 | 5 |
  • 6 | {story.points} 7 | 8 | {story.url && !story.url.startsWith("item?id=") ? ( 9 | <> 10 | 11 | {story.title} 12 | 13 | ({story.domain}) 14 | 15 | ) : ( 16 | {story.title} 17 | )} 18 | 19 |
    20 | 21 | {story.type !== "job" ? ( 22 | <> 23 | by {story.user}{" "} 24 | {story.time_ago} |{" "} 25 | 26 | {story.comments_count 27 | ? `${story.comments_count} comments` 28 | : "discuss"} 29 | 30 | 31 | ) : ( 32 | {story.time_ago} 33 | )} 34 | 35 | {story.type !== "link" && ( 36 | {story.type} 37 | )} 38 |
  • -------------------------------------------------------------------------------- /src/components/Toggle.jsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from "solid-js"; 2 | 3 | export default function Toggle(props) { 4 | const [open, setOpen] = createSignal(true); 5 | return ( 6 | <> 7 |
    8 | setOpen((o) => !o)}> 9 | {open() ? "[-]" : "[+] comments collapsed"} 10 | 11 |
    12 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/api.js: -------------------------------------------------------------------------------- 1 | const mapStories = { 2 | top: "news", 3 | new: "newest", 4 | show: "show", 5 | ask: "ask", 6 | job: "jobs", 7 | }; 8 | 9 | const get = (path) => 10 | fetch(path, { 11 | headers: { "User-Agent": "chrome" }, 12 | }).then((r) => r.json()); 13 | 14 | export function getStory(id) { 15 | return get(`https://node-hnapi.herokuapp.com/item/${id}`); 16 | } 17 | export function getUser(id) { 18 | return get(`https://hacker-news.firebaseio.com/v0/user/${id}.json`); 19 | } 20 | export function getStories(type, page) { 21 | const l = mapStories[type]; 22 | if (!l) return []; 23 | return get(`https://node-hnapi.herokuapp.com/${l}?page=${page}`); 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/[...stories].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../components/Layout.astro"; 3 | import Story from "../components/Story.astro"; 4 | 5 | import { getStories } from "../lib/api"; 6 | 7 | const type = Astro.params.stories || "top"; 8 | const url = new URL(Astro.request.url); 9 | const page = +(url.searchParams.get("page") || 1); 10 | const stories = await getStories(type, page); 11 | --- 12 | 13 |
    14 |
    15 | { page > 1 ? 16 | 20 | {"<"} prev 21 | 22 | : 23 | 24 | } 25 | page {page} 26 | 27 | { stories.length >= 28 ? 28 | 32 | more {">"} 33 | 34 | : 35 | 36 | } 37 |
    38 |
    39 |
      { 40 | stories.map(story => ) 41 | } 42 |
    43 |
    44 |
    45 |
    -------------------------------------------------------------------------------- /src/pages/stories/[id].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../components/Layout.astro"; 3 | import Comment from "../../components/Comment.astro"; 4 | import { getStory } from "../../lib/api"; 5 | const story = await getStory(Astro.params.id); 6 | --- 7 | 8 | {story && ( 9 |
    10 |
    11 | 12 |

    {story.title}

    13 |
    14 | {story.domain && ({story.domain})} 15 |

    16 | {story.points} points | by{" "} 17 | {story.user}{" "} 18 | {story.time_ago} ago 19 |

    20 |
    21 |
    22 |

    23 | {story.comments_count 24 | ? story.comments_count + " comments" 25 | : "No comments yet."} 26 |

    27 |
      28 | {story.comments.map((comment) => ( 29 | 30 | ))} 31 |
    32 |
    33 |
    34 | )} 35 |
    -------------------------------------------------------------------------------- /src/pages/users/[id].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../../components/Layout.astro" 3 | import { getUser } from "../../lib/api"; 4 | const user = await getUser(Astro.params.id); 5 | --- 6 | 7 |
    8 | {user && user.error ? ( 9 |

    User not found.

    10 | ) : ( 11 | <> 12 |

    User : {user.id}

    13 | 27 | 36 | 37 | )} 38 |
    39 |
    -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, 3 | Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 4 | font-size: 15px; 5 | background-color: #f2f3f5; 6 | margin: 0; 7 | padding-top: 55px; 8 | color: #34495e; 9 | overflow-y: scroll; 10 | } 11 | a { 12 | color: #34495e; 13 | text-decoration: none; 14 | } 15 | .header { 16 | background-color: #4b158a; 17 | position: fixed; 18 | z-index: 999; 19 | height: 55px; 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | } 24 | .header .inner { 25 | max-width: 800px; 26 | box-sizing: border-box; 27 | margin: 0 auto; 28 | padding: 15px 5px; 29 | } 30 | .header a { 31 | color: rgba(255, 255, 255, 0.8); 32 | line-height: 24px; 33 | transition: color 0.15s ease; 34 | display: inline-block; 35 | vertical-align: middle; 36 | font-weight: 300; 37 | letter-spacing: 0.075em; 38 | margin-right: 1.8em; 39 | } 40 | .header a:hover { 41 | color: #fff; 42 | } 43 | .header a.active { 44 | color: #fff; 45 | font-weight: 400; 46 | } 47 | .header a:nth-child(6) { 48 | margin-right: 0; 49 | } 50 | .header .github { 51 | color: #fff; 52 | font-size: 0.9em; 53 | margin: 0; 54 | float: right; 55 | } 56 | .logo { 57 | width: 24px; 58 | margin-right: 10px; 59 | display: inline-block; 60 | vertical-align: middle; 61 | } 62 | .view { 63 | max-width: 800px; 64 | margin: 0 auto; 65 | position: relative; 66 | } 67 | @media (max-width: 860px) { 68 | .header .inner { 69 | padding: 15px 30px; 70 | } 71 | } 72 | @media (max-width: 600px) { 73 | .header .inner { 74 | padding: 15px; 75 | } 76 | .header a { 77 | margin-right: 1em; 78 | } 79 | .header .github { 80 | display: none; 81 | } 82 | } 83 | .news-view { 84 | padding-top: 45px; 85 | } 86 | .news-list, 87 | .news-list-nav { 88 | background-color: #fff; 89 | border-radius: 2px; 90 | } 91 | .news-list-nav { 92 | padding: 15px 30px; 93 | position: fixed; 94 | text-align: center; 95 | top: 55px; 96 | left: 0; 97 | right: 0; 98 | z-index: 998; 99 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); 100 | } 101 | .news-list-nav .page-link { 102 | margin: 0 1em; 103 | } 104 | .news-list-nav .disabled { 105 | color: #aaa; 106 | } 107 | .news-list { 108 | position: absolute; 109 | margin: 30px 0; 110 | width: 100%; 111 | } 112 | .news-list ul { 113 | list-style-type: none; 114 | padding: 0; 115 | margin: 0; 116 | } 117 | .slide-left-enter, 118 | .slide-right-exit-to { 119 | opacity: 0; 120 | transform: translate(30px, 0); 121 | } 122 | .slide-left-exit-to, 123 | .slide-right-enter { 124 | opacity: 0; 125 | transform: translate(-30px, 0); 126 | } 127 | @media (max-width: 600px) { 128 | .news-list { 129 | margin: 10px 0; 130 | } 131 | } 132 | .news-item { 133 | background-color: #fff; 134 | padding: 20px 30px 20px 80px; 135 | border-bottom: 1px solid #eee; 136 | position: relative; 137 | line-height: 20px; 138 | } 139 | .news-item .score { 140 | color: #4b158a; 141 | font-size: 1.1em; 142 | font-weight: 700; 143 | position: absolute; 144 | top: 50%; 145 | left: 0; 146 | width: 80px; 147 | text-align: center; 148 | margin-top: -10px; 149 | } 150 | .news-item .host, 151 | .news-item .meta { 152 | font-size: 0.85em; 153 | color: #626262; 154 | } 155 | .news-item .host a, 156 | .news-item .meta a { 157 | color: #626262; 158 | text-decoration: underline; 159 | } 160 | .news-item .host a:hover, 161 | .news-item .meta a:hover { 162 | color: #4b158a; 163 | } 164 | .item-view-header { 165 | background-color: #fff; 166 | padding: 1.8em 2em 1em; 167 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); 168 | } 169 | .item-view-header h1 { 170 | display: inline; 171 | font-size: 1.5em; 172 | margin: 0; 173 | margin-right: 0.5em; 174 | } 175 | .item-view-header .host, 176 | .item-view-header .meta, 177 | .item-view-header .meta a { 178 | color: #626262; 179 | } 180 | .item-view-header .meta a { 181 | text-decoration: underline; 182 | } 183 | .item-view-comments { 184 | background-color: #fff; 185 | margin-top: 10px; 186 | padding: 0 2em 0.5em; 187 | } 188 | .item-view-comments-header { 189 | margin: 0; 190 | font-size: 1.1em; 191 | padding: 1em 0; 192 | position: relative; 193 | } 194 | .item-view-comments-header .spinner { 195 | display: inline-block; 196 | margin: -15px 0; 197 | } 198 | .comment-children { 199 | list-style-type: none; 200 | padding: 0; 201 | margin: 0; 202 | } 203 | @media (max-width: 600px) { 204 | .item-view-header h1 { 205 | font-size: 1.25em; 206 | } 207 | } 208 | .comment-children .comment-children { 209 | margin-left: 1.5em; 210 | } 211 | .comment { 212 | border-top: 1px solid #eee; 213 | position: relative; 214 | } 215 | .comment .by, 216 | .comment .text, 217 | .comment .toggle { 218 | font-size: 0.9em; 219 | margin: 1em 0; 220 | } 221 | .comment .by { 222 | color: #626262; 223 | } 224 | .comment .by a { 225 | color: #626262; 226 | text-decoration: underline; 227 | } 228 | .comment .text { 229 | overflow-wrap: break-word; 230 | } 231 | .comment .text a:hover { 232 | color: #5f3392; 233 | } 234 | .comment .text pre { 235 | white-space: pre-wrap; 236 | } 237 | .comment .toggle { 238 | background-color: #fffbf2; 239 | padding: 0.3em 0.5em; 240 | border-radius: 4px; 241 | } 242 | .comment .toggle a { 243 | color: #626262; 244 | cursor: pointer; 245 | } 246 | .comment .toggle.open { 247 | padding: 0; 248 | background-color: transparent; 249 | margin-bottom: -0.5em; 250 | } 251 | .user-view { 252 | background-color: #fff; 253 | box-sizing: border-box; 254 | padding: 2em 3em; 255 | } 256 | .user-view h1 { 257 | margin: 0; 258 | font-size: 1.5em; 259 | } 260 | .user-view .meta { 261 | list-style-type: none; 262 | padding: 0; 263 | } 264 | .user-view .label { 265 | display: inline-block; 266 | min-width: 4em; 267 | } 268 | .user-view .about { 269 | margin: 1em 0; 270 | } 271 | .user-view .links a { 272 | text-decoration: underline; 273 | } 274 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node" 4 | } 5 | } 6 | --------------------------------------------------------------------------------