diff options
Diffstat (limited to 'examples/hackernews/src')
-rw-r--r-- | examples/hackernews/src/components/Comment.astro | 59 | ||||
-rw-r--r-- | examples/hackernews/src/components/For.astro | 23 | ||||
-rw-r--r-- | examples/hackernews/src/components/Nav.astro | 99 | ||||
-rw-r--r-- | examples/hackernews/src/components/Show.astro | 9 | ||||
-rw-r--r-- | examples/hackernews/src/components/Story.astro | 77 | ||||
-rw-r--r-- | examples/hackernews/src/components/Toggle.astro | 78 | ||||
-rw-r--r-- | examples/hackernews/src/layouts/Layout.astro | 35 | ||||
-rw-r--r-- | examples/hackernews/src/lib/api.ts | 24 | ||||
-rw-r--r-- | examples/hackernews/src/pages/[...stories].astro | 105 | ||||
-rw-r--r-- | examples/hackernews/src/pages/stories/[id].astro | 96 | ||||
-rw-r--r-- | examples/hackernews/src/pages/users/[id].astro | 69 | ||||
-rw-r--r-- | examples/hackernews/src/types.ts | 27 |
12 files changed, 701 insertions, 0 deletions
diff --git a/examples/hackernews/src/components/Comment.astro b/examples/hackernews/src/components/Comment.astro new file mode 100644 index 000000000..07e55d19b --- /dev/null +++ b/examples/hackernews/src/components/Comment.astro @@ -0,0 +1,59 @@ +--- +import type { IComment } from '../types.js'; +import For from './For.astro'; +import Show from './Show.astro'; +import Toggle from './Toggle.astro'; + +interface Props { + comment: IComment; +} + +const { comment } = Astro.props; +--- + +<li> + <div class="by"> + <a href={`/users/${comment.user}`}>{comment.user}</a>{' '} + {comment.time_ago} + </div> + <div class="text" set:html={comment.content} /> + <Show when={comment.comments.length}> + <Toggle open> + <For each={comment.comments}>{(comment: IComment) => <Astro.self comment={comment} />}</For> + </Toggle> + </Show> +</li> + +<style> + li { + border-top: 1px solid #eee; + position: relative; + } + + .by, + .text { + font-size: 0.9em; + margin: 1em 0; + } + + .by { + color: rgb(51 65 85); + } + + .by a { + color: rgb(51 65 85); + text-decoration: underline; + } + + .text { + overflow-wrap: break-word; + } + + .text :global(a:hover) { + color: #335d92; + } + + .text :global(pre) { + white-space: pre-wrap; + } +</style> diff --git a/examples/hackernews/src/components/For.astro b/examples/hackernews/src/components/For.astro new file mode 100644 index 000000000..6eae88e27 --- /dev/null +++ b/examples/hackernews/src/components/For.astro @@ -0,0 +1,23 @@ +--- +import Show from './Show.astro'; + +interface Props<T> { + each: Iterable<T>; +} + +const { each } = Astro.props; +--- + +{ + (async function* () { + for await (const value of each) { + let html = await Astro.slots.render('default', [value]); + yield <Fragment set:html={html} />; + yield '\n'; + } + })() +} + +<Show when={!each.length}> + <slot name="fallback" /> +</Show> diff --git a/examples/hackernews/src/components/Nav.astro b/examples/hackernews/src/components/Nav.astro new file mode 100644 index 000000000..7eeba2865 --- /dev/null +++ b/examples/hackernews/src/components/Nav.astro @@ -0,0 +1,99 @@ +--- +interface Link { + href: string; + text: string; +} + +const links: Link[] = [ + { href: '/', text: 'HN' }, + { href: '/new', text: 'New' }, + { href: '/show', text: 'Show' }, + { href: '/ask', text: 'Ask' }, + { href: '/job', text: 'Jobs' }, +]; +--- + +<header> + <nav aria-label="Main menu"> + { + links.map(({ href, text }) => ( + <a href={href} aria-current={href === Astro.url.pathname ? 'page' : undefined}> + <strong>{text}</strong> + </a> + )) + } + <a class="github" href="http://github.com/withastro/astro" target="_blank" rel="noreferrer"> + Built with Astro + </a> + </nav> +</header> + +<style> + header { + background-color: rgb(107 33 168); + position: fixed; + z-index: 999; + height: 55px; + top: 0; + left: 0; + right: 0; + } + + nav { + max-width: 800px; + box-sizing: border-box; + margin: 0 auto; + padding: 15px 5px; + } + + nav a { + color: rgba(248, 250, 252, 0.8); + line-height: 24px; + transition: color 0.15s ease; + display: inline-block; + vertical-align: middle; + font-weight: 300; + letter-spacing: 0.075em; + margin-right: 1.8em; + } + + nav a:hover { + color: rgb(248 250 252); + } + + nav [aria-current='page'] { + color: rgb(248 250 252); + font-weight: 400; + } + + nav a:last-of-type { + margin-right: 0; + } + + .github { + color: rgb(248 250 252); + font-size: 0.9em; + margin: 0; + float: right; + } + + @media (max-width: 860px) { + nav { + padding: 15px 30px; + } + } + + @media (max-width: 600px) { + nav { + padding: 15px; + } + + a { + margin-right: 1em; + } + + .github { + display: none; + } + } +</style> diff --git a/examples/hackernews/src/components/Show.astro b/examples/hackernews/src/components/Show.astro new file mode 100644 index 000000000..ccb642fd7 --- /dev/null +++ b/examples/hackernews/src/components/Show.astro @@ -0,0 +1,9 @@ +--- +interface Props<T> { + when: T | number | boolean | undefined | null; +} + +const { when } = Astro.props; +--- + +{!!when ? <slot /> : <slot name="fallback" />} diff --git a/examples/hackernews/src/components/Story.astro b/examples/hackernews/src/components/Story.astro new file mode 100644 index 000000000..e91748a30 --- /dev/null +++ b/examples/hackernews/src/components/Story.astro @@ -0,0 +1,77 @@ +--- +import type { IStory } from '../types.js'; +import Show from './Show.astro'; + +interface Props { + story: IStory; +} + +const { story } = Astro.props; +--- + +<li> + <span class="score">{story.points}</span> + <span class="title"> + <Show when={story.url}> + <a href={story.url} target="_blank" rel="noreferrer"> + {story.title} + </a> + <span class="host"> ({story.domain})</span> + <a slot="fallback" href={`/item/${story.id}`}>{story.title}</a> + </Show> + </span> + <br /> + <span class="meta"> + <Show when={story.type !== 'job'}> + by <a href={`/users/${story.user}`}>{story.user}</a>{' '} + {story.time_ago}{' '}|{' '} + <a href={`/stories/${story.id}`}> + {story.comments_count ? `${story.comments_count} comments` : 'discuss'} + </a> + <a slot="fallback" href={`/stories/${story.id}`}>{story.time_ago}</a> + </Show> + </span> + <Show when={story.type !== 'link'}> + + <span class="label">{story.type}</span> + </Show> +</li> + +<style> + li { + background-color: rgb(248 250 252); + padding: 20px 30px 20px 80px; + border-bottom: 1px solid #eee; + position: relative; + line-height: 20px; + } + + .score { + color: rgb(88 28 135); + font-size: 1.1em; + font-weight: 700; + position: absolute; + top: 50%; + left: 0; + width: 80px; + text-align: center; + margin-top: -10px; + } + + .host, + .meta { + font-size: 0.85em; + color: rgb(51 65 85); + } + + .host a, + .meta a { + color: rgb(51 65 85); + text-decoration: underline; + } + + .host a:hover, + .meta a:hover { + color: #335d92; + } +</style> diff --git a/examples/hackernews/src/components/Toggle.astro b/examples/hackernews/src/components/Toggle.astro new file mode 100644 index 000000000..799fca08c --- /dev/null +++ b/examples/hackernews/src/components/Toggle.astro @@ -0,0 +1,78 @@ +--- +interface Props { + open?: boolean; +} + +const { open = false } = Astro.props; +--- + +<hn-toggle open={open ? '' : undefined}> + <div class="toggle"> + <a>{open ? '[-]' : '[+] comments collapsed'}</a> + </div> + <ul class="comment-children"> + <slot /> + </ul> +</hn-toggle> + +<style> + hn-toggle[open] > .toggle { + padding: 0; + background-color: transparent; + margin-bottom: -0.5em; + } + + hn-toggle:not([open]) > .toggle { + background-color: rgb(255 247 237); + } + hn-toggle:not([open]) ul { + display: none; + } + + .toggle { + font-size: 0.9em; + margin: 1em 0; + padding: 0.3em 0.5em; + border-radius: 4px; + } + + a { + color: rgb(51 65 85); + cursor: pointer; + } +</style> + +<script> + class HnToggle extends HTMLElement { + #btn = this.querySelector<HTMLAnchorElement>('a')!; + #toggleOpen = this.toggleOpen.bind(this); + + connectedCallback() { + this.#btn.addEventListener('click', this.#toggleOpen, false); + } + + disconnectedCallback() { + this.#btn.addEventListener('click', this.#toggleOpen); + } + + get open() { + return this.hasAttribute('open'); + } + + set open(value: boolean) { + if (value) { + this.setAttribute('open', ''); + this.#btn.textContent = '[-]'; + } else { + this.removeAttribute('open'); + this.#btn.textContent = '[+] comments collapsed'; + } + } + + toggleOpen() { + this.open = !this.open; + } + } + + customElements.define('hn-toggle', HnToggle); +</script> diff --git a/examples/hackernews/src/layouts/Layout.astro b/examples/hackernews/src/layouts/Layout.astro new file mode 100644 index 000000000..b1630da8a --- /dev/null +++ b/examples/hackernews/src/layouts/Layout.astro @@ -0,0 +1,35 @@ +--- +import Nav from '../components/Nav.astro'; +--- + +<html lang="en"> + <head> + <meta charset="utf-8" /> + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="generator" content={Astro.generator} /> + <title>Astro - Hacker News</title> + <meta name="description" content="Hacker News Clone built with Astro" /> + </head> + <body> + <Nav /> + <slot /> + <style is:global> + body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, + Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + font-size: 15px; + background-color: rgb(226 232 240); + margin: 0; + padding-top: 55px; + color: rgb(15 23 42); + overflow-y: scroll; + } + + a { + color: rgb(15 23 42); + text-decoration: none; + } + </style> + </body> +</html> diff --git a/examples/hackernews/src/lib/api.ts b/examples/hackernews/src/lib/api.ts new file mode 100644 index 000000000..49fd0c333 --- /dev/null +++ b/examples/hackernews/src/lib/api.ts @@ -0,0 +1,24 @@ +const story = (path: string) => `https://node-hnapi.herokuapp.com/${path}`; +const user = (path: string) => `https://hacker-news.firebaseio.com/v0/${path}.json`; + +export default async function fetchAPI(path: string) { + const url = path.startsWith('user') ? user(path) : story(path); + const headers = { 'User-Agent': 'chrome' }; + + try { + let response = await fetch(url, { headers }); + let text = await response.text(); + try { + if (text === null) { + return { error: 'Not found' }; + } + return JSON.parse(text); + } catch (e) { + console.error(`Received from API: ${text}`); + console.error(e); + return { error: e }; + } + } catch (error) { + return { error }; + } +} diff --git a/examples/hackernews/src/pages/[...stories].astro b/examples/hackernews/src/pages/[...stories].astro new file mode 100644 index 000000000..fa227e0c1 --- /dev/null +++ b/examples/hackernews/src/pages/[...stories].astro @@ -0,0 +1,105 @@ +--- +import For from '../components/For.astro'; +import Show from '../components/Show.astro'; +import Story from '../components/Story.astro'; +import Layout from '../layouts/Layout.astro'; +import fetchAPI from '../lib/api'; +import type { IStory } from '../types.js'; + +const mapStories = { + top: 'news', + new: 'newest', + show: 'show', + ask: 'ask', + job: 'jobs', +}; + +function safeParseInt(value: any, fallback: number) { + try { + return parseInt(value) || fallback; + } catch { + return fallback; + } +} + +const page = safeParseInt(Astro.url.searchParams.get('page'), 1); +const type = + Astro.params.stories && Astro.params.stories in mapStories + ? (Astro.params.stories.toString() as keyof typeof mapStories) + : 'top'; + +const stories = (await fetchAPI(`${mapStories[type]}?page=${page}`)) as IStory[]; +--- + +<Layout> + <section> + <nav aria-labelledby="current-page"> + <Show when={page > 1}> + <a href={`/${type}?page=${page - 1}`} aria-label="Previous Page"> < prev</a> + <span slot="fallback" aria-disabled="true"> < prev</span> + </Show> + <span id="current-page">page {page}</span> + <Show when={stories?.length >= 29}> + <a href={`/${type}?page=${page + 1}`} aria-label="Next Page">more ></a> + <span slot="fallback" aria-disabled="true"> more ></span> + </Show> + </nav> + <main> + <Show when={stories}> + <ul> + <For each={stories}>{(story: IStory) => <Story story={story} />}</For> + </ul> + </Show> + </main> + </section> +</Layout> + +<style> + section { + padding-top: 45px; + } + + nav, + main { + background-color: rgb(248 250 252); + border-radius: 2px; + } + + nav { + padding: 15px 30px; + position: fixed; + text-align: center; + top: 55px; + left: 0; + right: 0; + z-index: 998; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + } + + nav a { + margin: 0 1em; + } + + [aria-disabled='true'] { + color: rgb(71 85 105); + margin: 0 1em; + } + + main { + position: absolute; + margin: 30px 0; + width: 100%; + } + + ul { + list-style-type: none; + padding: 0; + margin: 0; + } + + @media (max-width: 600px) { + main { + margin: 10px 0; + } + } +</style> diff --git a/examples/hackernews/src/pages/stories/[id].astro b/examples/hackernews/src/pages/stories/[id].astro new file mode 100644 index 000000000..84383aa9e --- /dev/null +++ b/examples/hackernews/src/pages/stories/[id].astro @@ -0,0 +1,96 @@ +--- +import Comment from '../../components/Comment.astro'; +import For from '../../components/For.astro'; +import Show from '../../components/Show.astro'; +import Layout from '../../layouts/Layout.astro'; +import fetchAPI from '../../lib/api'; +import type { IComment, IStory } from '../../types.js'; + +const { id } = Astro.params as { id: string }; + +const story = (await fetchAPI(`item/${id}`)) as IStory; +--- + +<Layout> + <div> + <header> + <a href={story.url} target="_blank"> + <h1>{story.title}</h1> + </a> + <Show when={story.domain}> + <span class="host">({story.domain})</span> + </Show> + <p class="meta"> + {story.points} points | by + <a href={`/users/${story.user}`}> + {story.user} + </a> + {story.time_ago} + </p> + </header> + <main> + <p> + {story.comments_count ? story.comments_count + ' comments' : 'No comments yet.'} + </p> + <ul class="comment-children"> + <For each={story.comments}> + {(comment: IComment) => <Comment comment={comment} />} + </For> + </ul> + </main> + </div> +</Layout> + +<style> + header { + background-color: rgb(248 250 252); + padding: 1.8em 2em 1em; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + } + + h1 { + display: inline; + font-size: 1.5em; + margin: 0; + margin-right: 0.5em; + } + + .host, + .meta, + .host a { + color: rgb(51 65 85); + } + + .meta a { + text-decoration: underline; + } + + main { + background-color: rgb(248 250 252); + margin-top: 10px; + padding: 0 2em 0.5em; + } + + main p { + margin: 0; + font-size: 1.1em; + padding: 1em 0; + position: relative; + } + + main :global(ul) { + list-style-type: none; + padding: 0; + margin: 0; + } + + @media (max-width: 600px) { + h1 { + font-size: 1.25em; + } + } + + ul :global(ul) { + margin-left: 1.5em; + } +</style> diff --git a/examples/hackernews/src/pages/users/[id].astro b/examples/hackernews/src/pages/users/[id].astro new file mode 100644 index 000000000..e56085992 --- /dev/null +++ b/examples/hackernews/src/pages/users/[id].astro @@ -0,0 +1,69 @@ +--- +import Show from '../../components/Show.astro'; +import Layout from '../../layouts/Layout.astro'; +import fetchAPI from '../../lib/api'; +import type { IUser } from '../../types.js'; + +const { id } = Astro.params as { id: string }; + +const user = (await fetchAPI(`user/${id}`)) as IUser; +--- + +<Layout> + <main> + <Show when={user}> + <Show when={!user.error}> + <h1 slot="fallback">User not found.</h1> + <h1>User : {user.id}</h1> + <ul class="meta"> + <li> + <span class="label">Created:</span> + {user.created} + </li> + <li> + <span class="label">Karma:</span> + {user.karma} + </li> + <Show when={user.about}> + <li set:html={user.about} class="about" />{' '} + </Show> + </ul> + <p> + <a href={`https://news.ycombinator.com/submitted?id=${user.id}`}>submissions</a> |{' '} + <a href={`https://news.ycombinator.com/threads?id=${user.id}`}>comments</a> + </p> + </Show> + </Show> + </main> +</Layout> + +<style> + main { + background-color: rgb(248 250 252); + box-sizing: border-box; + padding: 2em 3em; + } + + h1 { + margin: 0; + font-size: 1.5em; + } + + .meta { + list-style-type: none; + padding: 0; + } + + .label { + display: inline-block; + min-width: 4em; + } + + .about { + margin: 1em 0; + } + + p a { + text-decoration: underline; + } +</style> diff --git a/examples/hackernews/src/types.ts b/examples/hackernews/src/types.ts new file mode 100644 index 000000000..e27ee85e4 --- /dev/null +++ b/examples/hackernews/src/types.ts @@ -0,0 +1,27 @@ +export interface IComment { + user: string; + time_ago: string; + content: string; + comments: IComment[]; +} + +export interface IStory { + id: string; + points: string; + url: string; + title: string; + domain: string; + type: string; + time_ago: string; + user: string; + comments_count: number; + comments: IComment[]; +} + +export interface IUser { + error: string; + id: string; + created: string; + karma: number; + about: string; +} |