├── .firebaserc ├── .gitignore ├── LICENSE ├── README.md ├── firebase.json ├── package-lock.json ├── package.json ├── src ├── assets │ └── icon.png ├── components.d.ts ├── components │ ├── ask-page │ │ ├── ask-page.scss │ │ └── ask-page.tsx │ ├── comments-list │ │ ├── comments-list.scss │ │ └── comments-list.tsx │ ├── comments-page │ │ ├── comments-page.scss │ │ └── comments-page.tsx │ ├── ionic-hn │ │ ├── ionic-hn.scss │ │ └── ionic-hn.tsx │ ├── jobs-page │ │ ├── jobs-page.scss │ │ └── jobs-page.tsx │ ├── list-container │ │ ├── list-container.scss │ │ └── list-container.tsx │ ├── nav-header │ │ ├── nav-header.scss │ │ └── nav-header.tsx │ ├── news-page │ │ ├── news-page.scss │ │ └── news-page.tsx │ ├── show-page │ │ ├── show-page.scss │ │ └── show-page.tsx │ └── story-list │ │ ├── story-list.scss │ │ └── story-list.tsx ├── global │ └── variables.css ├── index.html └── manifest.json ├── stencil.config.js └── tsconfig.json /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "sw-test-site" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | www/ 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | *.log 7 | *.lock 8 | *.tmp 9 | *.tmp.* 10 | log.txt 11 | *.sublime-project 12 | *.sublime-workspace 13 | 14 | .idea/ 15 | .vscode/ 16 | .sass-cache/ 17 | .versions/ 18 | node_modules/ 19 | $RECYCLE.BIN/ 20 | 21 | .DS_Store 22 | Thumbs.db 23 | UserInterfaceState.xcuserstate 24 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ionic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ionic-Stencil HN app 2 | 3 | [Stencil](https://github.com/ionic-team/stencil) is a compiler for building fast web apps and components using Web Components. 4 | 5 | Stencil combines the best concepts of the most popular frontend frameworks into a compile-time rather than run-time tool. Stencil takes TypeScript, JSX, a tiny virtual DOM layer, efficient one-way data binding, an asynchronous rendering pipeline (similar to React Fiber), and lazy-loading out of the box, and generates 100% standards-based Web Components that run in any browser supporting the Custom Elements v1 spec. 6 | 7 | Stencil components are just Web Components, so they work in any major framework or with no framework at all. In many cases, Stencil can be used as a drop in replacement for traditional frontend frameworks given the capabilities now available in the browser, though using it as such is certainly not required. 8 | 9 | Stencil also enables a number of key capabilities on top of Web Components, in particular Server Side Rendering (SSR) without the need to run a headless browser, pre-rendering, and objects-as-properties (instead of just strings). 10 | 11 | This PWA is a Hacker News demo built with Stencil and our Ionic core components that are also built with Stencil. 12 | 13 | Want to try it live? Check it out [here](https://corehacker-10883.firebaseapp.com/). 14 | 15 | ## Performance 16 | We use WebPageTest to keep track of the loading performance of this demo on real, low end devices and an emerging markets 3G network. [Here](https://www.webpagetest.org/lighthouse.php?test=170623_YE_1C1R&run=2) is the latest lighthouse from our latest WebPageTest. This is with a Moto G with an emerging markets 3G network. 17 | 18 | ## Getting Started 19 | 20 | To start devving on this project, clone this repo and run: 21 | 22 | ```bash 23 | npm install 24 | ``` 25 | 26 | then run: 27 | 28 | ```bash 29 | npm run dev 30 | ``` 31 | to get a live reload server that watches for changes 32 | 33 | 34 | To build the app for production, run: 35 | 36 | ```bash 37 | npm run build 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "www", 4 | "rewrites": [ 5 | { 6 | "source": "**", 7 | "destination": "/index.html" 8 | } 9 | ], 10 | "headers": [ 11 | { 12 | "source": "sw.js", 13 | "headers" : [ { 14 | "key" : "Cache-Control", 15 | "value" : "no-cache" 16 | } ] 17 | }, 18 | { 19 | "source": "/", 20 | "headers": [ 21 | { 22 | "key": "Link", 23 | "value": ";rel=preload;as=script,;rel=preload;as=script,;rel=preload;as=script,;rel=preload;as=script,;rel=preload;as=script,;rel=preload;as=script,;rel=preload;as=script,;rel=preload;as=script,;rel=preload;as=script,;rel=preload;as=script,;rel=preload;as=script,;rel=preload;as=script" 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stencil/starter", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "Stencil Starter App", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "build": "stencil build --prerender", 11 | "dev": "sd concurrent \"stencil build --dev --watch\" \"stencil-dev-server\" ", 12 | "serve": "stencil-dev-server", 13 | "start": "npm run dev", 14 | "devTest": "sd concurrent \"stencil build --dev --watch\" \"http-server www/\" ", 15 | "deploy": "npm run build && firebase deploy" 16 | }, 17 | "dependencies": { 18 | "@ionic/core": "0.1.5-1", 19 | "@stencil/core": "0.7.6", 20 | "@stencil/router": "0.0.28" 21 | }, 22 | "devDependencies": { 23 | "@stencil/dev-server": "latest", 24 | "@stencil/sass": "0.0.3", 25 | "@stencil/utils": "latest" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/ionic-team/stencil-starter.git" 30 | }, 31 | "author": "Ionic Team", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/ionic-team/stencil" 35 | }, 36 | "homepage": "https://github.com/ionic-team/stencil" 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ionic-team/ionic-stencil-hn-app/9b8d2c6927c9b99b285fa2aa0599016eac9f9604/src/assets/icon.png -------------------------------------------------------------------------------- /src/components.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an autogenerated file created by the Stencil build process. 3 | * It contains typing information for all components that exist in this project 4 | * and imports for stencil collections that might be configured in your stencil.config.js file 5 | */ 6 | declare global { 7 | namespace JSX { 8 | interface Element {} 9 | export interface IntrinsicElements {} 10 | } 11 | namespace JSXElements {} 12 | 13 | interface HTMLStencilElement extends HTMLElement { 14 | componentOnReady(): Promise; 15 | componentOnReady(done: (ele?: this) => void): void; 16 | 17 | forceUpdate(): void; 18 | } 19 | 20 | interface HTMLAttributes {} 21 | } 22 | 23 | import '@stencil/router'; 24 | import 'ionicons'; 25 | import '@ionic/core'; 26 | 27 | import { 28 | RouterHistory, 29 | } from '@stencil/router'; 30 | 31 | import { 32 | askPage as AskPage 33 | } from './components/ask-page/ask-page'; 34 | 35 | declare global { 36 | interface HTMLAskPageElement extends AskPage, HTMLStencilElement { 37 | } 38 | var HTMLAskPageElement: { 39 | prototype: HTMLAskPageElement; 40 | new (): HTMLAskPageElement; 41 | }; 42 | interface HTMLElementTagNameMap { 43 | 'ask-page': HTMLAskPageElement; 44 | } 45 | interface ElementTagNameMap { 46 | 'ask-page': HTMLAskPageElement; 47 | } 48 | namespace JSX { 49 | interface IntrinsicElements { 50 | 'ask-page': JSXElements.AskPageAttributes; 51 | } 52 | } 53 | namespace JSXElements { 54 | export interface AskPageAttributes extends HTMLAttributes { 55 | 'history'?: RouterHistory; 56 | 'match'?: any; 57 | 58 | } 59 | } 60 | } 61 | 62 | 63 | import { 64 | CommentsList as CommentsList 65 | } from './components/comments-list/comments-list'; 66 | 67 | declare global { 68 | interface HTMLCommentsListElement extends CommentsList, HTMLStencilElement { 69 | } 70 | var HTMLCommentsListElement: { 71 | prototype: HTMLCommentsListElement; 72 | new (): HTMLCommentsListElement; 73 | }; 74 | interface HTMLElementTagNameMap { 75 | 'comments-list': HTMLCommentsListElement; 76 | } 77 | interface ElementTagNameMap { 78 | 'comments-list': HTMLCommentsListElement; 79 | } 80 | namespace JSX { 81 | interface IntrinsicElements { 82 | 'comments-list': JSXElements.CommentsListAttributes; 83 | } 84 | } 85 | namespace JSXElements { 86 | export interface CommentsListAttributes extends HTMLAttributes { 87 | 'commentList'?: any; 88 | 89 | } 90 | } 91 | } 92 | 93 | 94 | import { 95 | CommentsPage as CommentsPage 96 | } from './components/comments-page/comments-page'; 97 | 98 | declare global { 99 | interface HTMLCommentsPageElement extends CommentsPage, HTMLStencilElement { 100 | } 101 | var HTMLCommentsPageElement: { 102 | prototype: HTMLCommentsPageElement; 103 | new (): HTMLCommentsPageElement; 104 | }; 105 | interface HTMLElementTagNameMap { 106 | 'comments-page': HTMLCommentsPageElement; 107 | } 108 | interface ElementTagNameMap { 109 | 'comments-page': HTMLCommentsPageElement; 110 | } 111 | namespace JSX { 112 | interface IntrinsicElements { 113 | 'comments-page': JSXElements.CommentsPageAttributes; 114 | } 115 | } 116 | namespace JSXElements { 117 | export interface CommentsPageAttributes extends HTMLAttributes { 118 | 'history'?: RouterHistory; 119 | 'match'?: any; 120 | 121 | } 122 | } 123 | } 124 | 125 | 126 | import { 127 | IonicHn as IonicHn 128 | } from './components/ionic-hn/ionic-hn'; 129 | 130 | declare global { 131 | interface HTMLIonicHnElement extends IonicHn, HTMLStencilElement { 132 | } 133 | var HTMLIonicHnElement: { 134 | prototype: HTMLIonicHnElement; 135 | new (): HTMLIonicHnElement; 136 | }; 137 | interface HTMLElementTagNameMap { 138 | 'ionic-hn': HTMLIonicHnElement; 139 | } 140 | interface ElementTagNameMap { 141 | 'ionic-hn': HTMLIonicHnElement; 142 | } 143 | namespace JSX { 144 | interface IntrinsicElements { 145 | 'ionic-hn': JSXElements.IonicHnAttributes; 146 | } 147 | } 148 | namespace JSXElements { 149 | export interface IonicHnAttributes extends HTMLAttributes { 150 | 151 | 152 | } 153 | } 154 | } 155 | 156 | 157 | import { 158 | jobsPage as JobsPage 159 | } from './components/jobs-page/jobs-page'; 160 | 161 | declare global { 162 | interface HTMLJobsPageElement extends JobsPage, HTMLStencilElement { 163 | } 164 | var HTMLJobsPageElement: { 165 | prototype: HTMLJobsPageElement; 166 | new (): HTMLJobsPageElement; 167 | }; 168 | interface HTMLElementTagNameMap { 169 | 'jobs-page': HTMLJobsPageElement; 170 | } 171 | interface ElementTagNameMap { 172 | 'jobs-page': HTMLJobsPageElement; 173 | } 174 | namespace JSX { 175 | interface IntrinsicElements { 176 | 'jobs-page': JSXElements.JobsPageAttributes; 177 | } 178 | } 179 | namespace JSXElements { 180 | export interface JobsPageAttributes extends HTMLAttributes { 181 | 'history'?: RouterHistory; 182 | 'match'?: any; 183 | 184 | } 185 | } 186 | } 187 | 188 | 189 | import { 190 | NewsPage as ListContainer 191 | } from './components/list-container/list-container'; 192 | 193 | declare global { 194 | interface HTMLListContainerElement extends ListContainer, HTMLStencilElement { 195 | } 196 | var HTMLListContainerElement: { 197 | prototype: HTMLListContainerElement; 198 | new (): HTMLListContainerElement; 199 | }; 200 | interface HTMLElementTagNameMap { 201 | 'list-container': HTMLListContainerElement; 202 | } 203 | interface ElementTagNameMap { 204 | 'list-container': HTMLListContainerElement; 205 | } 206 | namespace JSX { 207 | interface IntrinsicElements { 208 | 'list-container': JSXElements.ListContainerAttributes; 209 | } 210 | } 211 | namespace JSXElements { 212 | export interface ListContainerAttributes extends HTMLAttributes { 213 | 'pageNum'?: number; 214 | 'type'?: string; 215 | 216 | } 217 | } 218 | } 219 | 220 | 221 | import { 222 | Navheader as NavHeader 223 | } from './components/nav-header/nav-header'; 224 | 225 | declare global { 226 | interface HTMLNavHeaderElement extends NavHeader, HTMLStencilElement { 227 | } 228 | var HTMLNavHeaderElement: { 229 | prototype: HTMLNavHeaderElement; 230 | new (): HTMLNavHeaderElement; 231 | }; 232 | interface HTMLElementTagNameMap { 233 | 'nav-header': HTMLNavHeaderElement; 234 | } 235 | interface ElementTagNameMap { 236 | 'nav-header': HTMLNavHeaderElement; 237 | } 238 | namespace JSX { 239 | interface IntrinsicElements { 240 | 'nav-header': JSXElements.NavHeaderAttributes; 241 | } 242 | } 243 | namespace JSXElements { 244 | export interface NavHeaderAttributes extends HTMLAttributes { 245 | 246 | 247 | } 248 | } 249 | } 250 | 251 | 252 | import { 253 | NewsPage as NewsPage 254 | } from './components/news-page/news-page'; 255 | 256 | declare global { 257 | interface HTMLNewsPageElement extends NewsPage, HTMLStencilElement { 258 | } 259 | var HTMLNewsPageElement: { 260 | prototype: HTMLNewsPageElement; 261 | new (): HTMLNewsPageElement; 262 | }; 263 | interface HTMLElementTagNameMap { 264 | 'news-page': HTMLNewsPageElement; 265 | } 266 | interface ElementTagNameMap { 267 | 'news-page': HTMLNewsPageElement; 268 | } 269 | namespace JSX { 270 | interface IntrinsicElements { 271 | 'news-page': JSXElements.NewsPageAttributes; 272 | } 273 | } 274 | namespace JSXElements { 275 | export interface NewsPageAttributes extends HTMLAttributes { 276 | 'history'?: RouterHistory; 277 | 'match'?: any; 278 | 279 | } 280 | } 281 | } 282 | 283 | 284 | import { 285 | ShowPage as ShowPage 286 | } from './components/show-page/show-page'; 287 | 288 | declare global { 289 | interface HTMLShowPageElement extends ShowPage, HTMLStencilElement { 290 | } 291 | var HTMLShowPageElement: { 292 | prototype: HTMLShowPageElement; 293 | new (): HTMLShowPageElement; 294 | }; 295 | interface HTMLElementTagNameMap { 296 | 'show-page': HTMLShowPageElement; 297 | } 298 | interface ElementTagNameMap { 299 | 'show-page': HTMLShowPageElement; 300 | } 301 | namespace JSX { 302 | interface IntrinsicElements { 303 | 'show-page': JSXElements.ShowPageAttributes; 304 | } 305 | } 306 | namespace JSXElements { 307 | export interface ShowPageAttributes extends HTMLAttributes { 308 | 'history'?: RouterHistory; 309 | 'match'?: any; 310 | 311 | } 312 | } 313 | } 314 | 315 | 316 | import { 317 | StoryList as StoryList 318 | } from './components/story-list/story-list'; 319 | 320 | declare global { 321 | interface HTMLStoryListElement extends StoryList, HTMLStencilElement { 322 | } 323 | var HTMLStoryListElement: { 324 | prototype: HTMLStoryListElement; 325 | new (): HTMLStoryListElement; 326 | }; 327 | interface HTMLElementTagNameMap { 328 | 'story-list': HTMLStoryListElement; 329 | } 330 | interface ElementTagNameMap { 331 | 'story-list': HTMLStoryListElement; 332 | } 333 | namespace JSX { 334 | interface IntrinsicElements { 335 | 'story-list': JSXElements.StoryListAttributes; 336 | } 337 | } 338 | namespace JSXElements { 339 | export interface StoryListAttributes extends HTMLAttributes { 340 | 'stories'?: any; 341 | 342 | } 343 | } 344 | } 345 | 346 | declare global { namespace JSX { interface StencilJSX {} } } 347 | -------------------------------------------------------------------------------- /src/components/ask-page/ask-page.scss: -------------------------------------------------------------------------------- 1 | ask-page { 2 | ion-scroll { 3 | margin-top: 55px; 4 | } 5 | 6 | .page-number { 7 | line-height: 3; 8 | text-transform: uppercase; 9 | font-size: 92%; 10 | } 11 | 12 | ion-footer .toolbar-content { 13 | text-align: center; 14 | } 15 | 16 | .no-back button { 17 | opacity: 0.3; 18 | } 19 | 20 | .yes-back button { 21 | opacity: 1; 22 | color: #327eff !important; 23 | } 24 | } -------------------------------------------------------------------------------- /src/components/ask-page/ask-page.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop, State } from '@stencil/core'; 2 | import { RouterHistory } from '@stencil/router'; 3 | 4 | 5 | @Component({ 6 | tag: 'ask-page', 7 | styleUrl: 'ask-page.scss' 8 | }) 9 | export class askPage { 10 | 11 | @Prop() match: any; 12 | @Prop() history: RouterHistory; 13 | 14 | @State() page: number; 15 | 16 | componentWillLoad() { 17 | if (this.match && this.match.params.pageNum) { 18 | this.page = parseInt(this.match.params.pageNum); 19 | } else { 20 | this.page = 1; 21 | } 22 | } 23 | 24 | forward() { 25 | this.page = this.page + 1; 26 | this.history.push(`/ask/${this.page}`, {}); 27 | } 28 | 29 | back() { 30 | if (this.page !== 1) { 31 | this.page = this.page - 1; 32 | this.history.push(`/ask/${this.page}`, {}); 33 | } 34 | } 35 | 36 | hostData() { 37 | return { 38 | class: {'ion-page': true} 39 | }; 40 | } 41 | 42 | render() { 43 | return [ 44 | , 45 | 46 | 47 | , 48 | 49 | 50 | 51 | this.back()} 53 | fill='clear' 54 | color='primary' 55 | class={{ 'no-back': this.page === 1, 'yes-back': this.page > 1 }}> 56 | Prev 57 | 58 | 59 | 60 | 61 | page {this.page} 62 | 63 | 64 | 65 | this.forward()} fill='clear' color='primary'> 66 | Next 67 | 68 | 69 | 70 | 71 | ]; 72 | } 73 | } -------------------------------------------------------------------------------- /src/components/comments-list/comments-list.scss: -------------------------------------------------------------------------------- 1 | comments-list { 2 | ion-label { 3 | white-space: normal; 4 | } 5 | 6 | ion-list { 7 | margin-top: 10px; 8 | } 9 | 10 | .nested { 11 | margin-left: 45px; 12 | } 13 | 14 | .list-ios { 15 | margin-top: 15px !important; 16 | } 17 | } -------------------------------------------------------------------------------- /src/components/comments-list/comments-list.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop } from '@stencil/core'; 2 | 3 | @Component({ 4 | tag: 'comments-list', 5 | styleUrl: 'comments-list.scss' 6 | }) 7 | export class CommentsList { 8 | 9 | @Prop() commentList: any; 10 | 11 | render() { 12 | if (this.commentList) { 13 | const items = this.commentList.map((comment) => { 14 | return [ 15 | 16 | 17 |

18 | {`Posted by ${comment.user} ${comment.time_ago}`} 19 |

20 |
21 |
22 |
, 23 | 24 | comment.comments.map((comment) => { 25 | return ( 26 | 27 | 28 |

29 | {`Posted by ${comment.user} ${comment.time_ago}`} 30 |

31 |
32 |
33 |
34 | ) 35 | }) 36 | ]; 37 | }); 38 | 39 | return ( 40 | 41 | {items} 42 | 43 | ); 44 | } else { 45 | return ( 46 | 47 | {Array.from(Array(10)).map(() => 48 | 49 | 50 |

51 | 52 |

53 |
54 | 55 |
56 |
57 |
58 | )} 59 |
60 | ) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/comments-page/comments-page.scss: -------------------------------------------------------------------------------- 1 | comments-page { 2 | .close-button ion-icon { 3 | color: white; 4 | } 5 | 6 | #no-comments { 7 | text-align: center; 8 | } 9 | 10 | ion-buttons[slot="end"] { 11 | order: 6!important; 12 | } 13 | 14 | ion-buttons[slot="end"] button { 15 | height: 1.8em; 16 | } 17 | } -------------------------------------------------------------------------------- /src/components/comments-page/comments-page.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop, State } from '@stencil/core'; 2 | import { RouterHistory } from '@stencil/router'; 3 | import { LoadingController } from '@ionic/core'; 4 | 5 | @Component({ 6 | tag: 'comments-page', 7 | styleUrl: 'comments-page.scss' 8 | }) 9 | export class CommentsPage { 10 | 11 | @Prop() match: any; 12 | @Prop() history: RouterHistory; 13 | @Prop({ connect: 'ion-loading-controller' }) loadingCtrl: LoadingController; 14 | 15 | @State() comments: any[] = []; 16 | 17 | apiRootUrl: string = 'https://hnpwa.com/api/v0'; 18 | 19 | componentWillLoad() { 20 | if (this.match && this.match.params.id) { 21 | fetch(`${this.apiRootUrl}/item/${this.match.params.id}.json`).then((response: any) => { 22 | return response.json() 23 | }).then((data) => { 24 | this.comments = data.comments; 25 | }); 26 | } 27 | } 28 | 29 | close() { 30 | this.history.goBack(); 31 | } 32 | 33 | hostData() { 34 | return { 35 | class: {'ion-page': true} 36 | }; 37 | } 38 | 39 | render() { 40 | return [ 41 | 42 | 43 | 44 | Comments 45 | 46 | 47 | 48 | this.close()}> 49 | 50 | 51 | 52 | 53 | , 54 | 55 | 56 | 57 | 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/ionic-hn/ionic-hn.scss: -------------------------------------------------------------------------------- 1 | ionic-hn { 2 | ion-footer ion-toolbar div.toolbar-background { 3 | background: white; 4 | } 5 | 6 | ion-footer { 7 | height: 48px; 8 | } 9 | 10 | ion-footer ion-toolbar { 11 | min-height: 48px !important; 12 | } 13 | 14 | @media(min-width: 900px) { 15 | ion-footer ion-buttons[slot=end] { 16 | margin-right: 18em; 17 | } 18 | 19 | ion-footer ion-buttons[slot=start] { 20 | margin-left: 18em; 21 | } 22 | 23 | ion-header ion-buttons[slot=start] { 24 | margin-left: 18.3em; 25 | } 26 | 27 | .ion-page { 28 | padding-left: 18em; 29 | padding-right: 18em; 30 | background: #f3f3f3; 31 | } 32 | } 33 | 34 | @media(max-width: 368px) { 35 | ion-icon { 36 | display: none; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/ionic-hn/ionic-hn.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from '@stencil/core'; 2 | 3 | 4 | @Component({ 5 | tag: 'ionic-hn', 6 | styleUrl: 'ionic-hn.scss' 7 | }) 8 | export class IonicHn { 9 | 10 | render() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | } -------------------------------------------------------------------------------- /src/components/jobs-page/jobs-page.scss: -------------------------------------------------------------------------------- 1 | jobs-page { 2 | ion-scroll { 3 | margin-top: 55px; 4 | } 5 | 6 | .page-number { 7 | line-height: 3; 8 | text-transform: uppercase; 9 | font-size: 92%; 10 | } 11 | 12 | ion-footer .toolbar-content { 13 | text-align: center; 14 | } 15 | 16 | .no-back button { 17 | opacity: 0.3; 18 | } 19 | 20 | .yes-back button { 21 | opacity: 1; 22 | color: #327eff !important; 23 | } 24 | } -------------------------------------------------------------------------------- /src/components/jobs-page/jobs-page.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop, State } from '@stencil/core'; 2 | import { RouterHistory } from '@stencil/router'; 3 | 4 | 5 | @Component({ 6 | tag: 'jobs-page', 7 | styleUrl: 'jobs-page.scss' 8 | }) 9 | export class jobsPage { 10 | 11 | @Prop() match: any; 12 | @Prop() history: RouterHistory; 13 | 14 | @State() page: number; 15 | 16 | componentWillLoad() { 17 | if (this.match && this.match.params.pageNum) { 18 | this.page = parseInt(this.match.params.pageNum); 19 | } else { 20 | this.page = 1; 21 | } 22 | } 23 | 24 | forward() { 25 | this.page = this.page + 1; 26 | this.history.push(`/jobs/${this.page}`, {}); 27 | } 28 | 29 | back() { 30 | if (this.page !== 1) { 31 | this.page = this.page - 1; 32 | this.history.push(`/jobs/${this.page}`, {}); 33 | } 34 | } 35 | 36 | hostData() { 37 | return { 38 | class: {'ion-page': true} 39 | }; 40 | } 41 | 42 | render() { 43 | return [ 44 | , 45 | 46 | 47 | , 48 | 49 | 50 | 51 | this.back()} 53 | fill='clear' 54 | color='primary' 55 | class={{ 'no-back': this.page === 1, 'yes-back': this.page > 1 }} 56 | > 57 | Prev 58 | 59 | 60 | 61 | 62 | page {this.page} 63 | 64 | 65 | 66 | this.forward()} fill='clear' color='primary'> 67 | Next 68 | 69 | 70 | 71 | 72 | ]; 73 | } 74 | } -------------------------------------------------------------------------------- /src/components/list-container/list-container.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ionic-team/ionic-stencil-hn-app/9b8d2c6927c9b99b285fa2aa0599016eac9f9604/src/components/list-container/list-container.scss -------------------------------------------------------------------------------- /src/components/list-container/list-container.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop, Watch, State } from '@stencil/core'; 2 | import { LoadingController } from '@ionic/core'; 3 | 4 | 5 | @Component({ 6 | tag: 'list-container', 7 | styleUrl: 'list-container.scss' 8 | }) 9 | export class NewsPage { 10 | 11 | @Prop() pageNum: number; 12 | @Prop() type: string; 13 | @Prop({ connect: 'ion-loading-controller' }) loadingCtrl: LoadingController; 14 | @Prop({ context: 'isServer' }) private isServer: boolean; 15 | 16 | @State() stories: any[]; 17 | 18 | apiRootUrl: string = 'https://node-hnapi.herokuapp.com'; 19 | 20 | componentWillLoad() { 21 | // fetch without loading for the first run 22 | // so we dont pull in the loading controller 23 | // for the first view 24 | if (!this.isServer) { 25 | fetch(`${this.apiRootUrl}/${this.type}?page=${this.pageNum}`).then(rsp => { 26 | return rsp.json(); 27 | 28 | }).then(data => { 29 | this.stories = data; 30 | 31 | }).catch((err) => { 32 | console.error('Could not load data', err); 33 | }); 34 | } 35 | } 36 | 37 | @Watch('pageNum') 38 | fetchNew() { 39 | fetch(`${this.apiRootUrl}/${this.type}?page=${this.pageNum}`).then(rsp => { 40 | return rsp.json(); 41 | }).then(data => { 42 | if (data.length !== 0) { 43 | this.stories = data; 44 | } 45 | 46 | }).catch((err) => { 47 | console.error('Could not load data', err); 48 | }); 49 | } 50 | 51 | render() { 52 | return [ 53 | 54 | ]; 55 | } 56 | } -------------------------------------------------------------------------------- /src/components/nav-header/nav-header.scss: -------------------------------------------------------------------------------- 1 | nav-header { 2 | #ionic-icon { 3 | fill: #fff; 4 | margin-top: 5px; 5 | width: 3.5em; 6 | } 7 | 8 | #ionic-icon svg { 9 | fill: #fff; 10 | width: 4.4em; 11 | height: 2.5em; 12 | } 13 | 14 | .tabs-bar { 15 | order: 2; 16 | } 17 | 18 | ion-toolbar ion-button.header-button button { 19 | color: white !important; 20 | } 21 | 22 | a { 23 | opacity: 1 !important; 24 | } 25 | 26 | .header-button { 27 | opacity: 0.6; 28 | width: 5rem; 29 | } 30 | 31 | .active .header-button { 32 | opacity: 1 !important; 33 | } 34 | 35 | .active button { 36 | font-weight: bold !important; 37 | } 38 | } -------------------------------------------------------------------------------- /src/components/nav-header/nav-header.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from '@stencil/core'; 2 | 3 | 4 | @Component({ 5 | tag: 'nav-header', 6 | styleUrl: 'nav-header.scss' 7 | }) 8 | export class Navheader { 9 | 10 | render() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 27 | News 28 | 29 | 30 | 31 | 32 | 37 | Show 38 | 39 | 40 | 41 | 42 | 47 | Jobs 48 | 49 | 50 | 51 | 52 | 57 | Ask 58 | 59 | 60 |
61 |
62 |
63 | ); 64 | } 65 | } -------------------------------------------------------------------------------- /src/components/news-page/news-page.scss: -------------------------------------------------------------------------------- 1 | news-page { 2 | ion-scroll { 3 | margin-top: 55px; 4 | } 5 | 6 | .page-number { 7 | line-height: 3; 8 | text-transform: uppercase; 9 | font-size: 92%; 10 | } 11 | 12 | ion-footer .toolbar-content { 13 | text-align: center; 14 | } 15 | 16 | .no-back button { 17 | opacity: 0.3; 18 | } 19 | 20 | .yes-back button { 21 | opacity: 1; 22 | color: #327eff !important; 23 | } 24 | } -------------------------------------------------------------------------------- /src/components/news-page/news-page.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop, State } from '@stencil/core'; 2 | import { RouterHistory } from '@stencil/router'; 3 | 4 | 5 | @Component({ 6 | tag: 'news-page', 7 | styleUrl: 'news-page.scss' 8 | }) 9 | export class NewsPage { 10 | 11 | @Prop() match: any; 12 | @Prop() history: RouterHistory; 13 | 14 | @State() page: number; 15 | 16 | componentWillLoad() { 17 | 18 | if (this.match && this.match.params.pageNum) { 19 | this.page = parseInt(this.match.params.pageNum); 20 | } else { 21 | this.page = 1; 22 | } 23 | } 24 | 25 | forward() { 26 | this.page = this.page + 1; 27 | this.history.push(`/news/${this.page}`, {}); 28 | } 29 | 30 | back() { 31 | if (this.page !== 1) { 32 | this.page = this.page - 1; 33 | this.history.push(`/news/${this.page}`, {}); 34 | } 35 | } 36 | 37 | hostData() { 38 | return { 39 | class: {'ion-page': true} 40 | }; 41 | } 42 | 43 | render() { 44 | return [ 45 | , 46 | 47 | 48 | , 49 | 50 | 51 | 52 | this.back()} 54 | fill='clear' 55 | color='primary' 56 | class={{ 'no-back': this.page === 1, 'yes-back': this.page > 1 }}> 57 | Prev 58 | 59 | 60 | 61 | 62 | page {this.page} 63 | 64 | 65 | 66 | this.forward()} fill='clear' color='primary'> 67 | Next 68 | 69 | 70 | 71 | 72 | ]; 73 | } 74 | } -------------------------------------------------------------------------------- /src/components/show-page/show-page.scss: -------------------------------------------------------------------------------- 1 | show-page { 2 | ion-scroll { 3 | margin-top: 55px; 4 | } 5 | 6 | .page-number { 7 | line-height: 3; 8 | text-transform: uppercase; 9 | font-size: 92%; 10 | } 11 | 12 | ion-footer .toolbar-content { 13 | text-align: center; 14 | } 15 | 16 | .no-back button { 17 | opacity: 0.3; 18 | } 19 | 20 | .yes-back button { 21 | opacity: 1; 22 | color: #327eff !important; 23 | } 24 | } -------------------------------------------------------------------------------- /src/components/show-page/show-page.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop, State } from '@stencil/core'; 2 | import { RouterHistory } from '@stencil/router'; 3 | 4 | 5 | @Component({ 6 | tag: 'show-page', 7 | styleUrl: 'show-page.scss' 8 | }) 9 | export class ShowPage { 10 | 11 | @Prop() match: any; 12 | @Prop() history: RouterHistory; 13 | 14 | @State() page: number; 15 | 16 | componentWillLoad() { 17 | if (this.match && this.match.params.pageNum) { 18 | this.page = parseInt(this.match.params.pageNum); 19 | } else { 20 | this.page = 1; 21 | } 22 | } 23 | 24 | forward() { 25 | this.page = this.page + 1; 26 | this.history.push(`/show/${this.page}`, {}); 27 | } 28 | 29 | back() { 30 | if (this.page !== 1) { 31 | this.page = this.page - 1; 32 | this.history.push(`/show/${this.page}`, {}); 33 | } 34 | } 35 | 36 | hostData() { 37 | return { 38 | class: {'ion-page': true} 39 | }; 40 | } 41 | 42 | render() { 43 | return [ 44 | , 45 | 46 | 47 | , 48 | 49 | 50 | 51 | this.back()} 53 | fill='clear' 54 | color='primary' 55 | class={{ 'no-back': this.page === 1, 'yes-back': this.page > 1 }}> 56 | Prev 57 | 58 | 59 | 60 | 61 | page {this.page} 62 | 63 | 64 | 65 | this.forward()} fill='clear' color='primary'> 66 | Next 67 | 68 | 69 | 70 | 71 | ]; 72 | } 73 | } -------------------------------------------------------------------------------- /src/components/story-list/story-list.scss: -------------------------------------------------------------------------------- 1 | story-list { 2 | ion-list .item-inner { 3 | padding-bottom: 10px; 4 | padding-top: 7px; 5 | border-bottom-color: #e4e7ec !important; 6 | } 7 | 8 | .points { 9 | color: #488aff; 10 | font-weight: bold; 11 | width: 55px; 12 | padding-left: 10px; 13 | } 14 | 15 | .comments-text { 16 | border: none; 17 | background: transparent; 18 | font-size: 14px; 19 | padding: 0; 20 | margin: 0; 21 | color: #717883; 22 | text-align: start; 23 | } 24 | 25 | .list-header a { 26 | color: #202939; 27 | text-decoration: none; 28 | } 29 | 30 | .item-content { 31 | padding-top: 5px; 32 | padding-bottom: 5px; 33 | } 34 | 35 | .comments-title { 36 | line-height: 2.1; 37 | } 38 | 39 | ion-label { 40 | white-space: normal !important; 41 | } 42 | 43 | ion-skeleton-text { 44 | display: inline-block; 45 | width: 100%; 46 | pointer-events: none; 47 | user-select: none; 48 | } 49 | 50 | ion-skeleton-text span { 51 | display: inline-block; 52 | font-size: 0.8rem; 53 | background-color: #ececec; 54 | } 55 | } -------------------------------------------------------------------------------- /src/components/story-list/story-list.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Prop } from '@stencil/core'; 2 | 3 | 4 | @Component({ 5 | tag: 'story-list', 6 | styleUrl: 'story-list.scss' 7 | }) 8 | export class StoryList { 9 | 10 | @Prop() stories: any; 11 | 12 | preLoadData: any[] = [{ "id": 15796943, "title": "Show HN: Golang DNS server, including DNSSEC and DNS-over-TLS", "points": 181, "user": "tenta", "time": 1511875266, "time_ago": "5 hours ago", "comments_count": 50, "type": "link", "url": "https://github.com/tenta-browser/tenta-dns", "domain": "github.com" }, { "id": 15799034, "title": "Lessons we learned while bootstrapping", "points": 26, "user": "brianjackson", "time": 1511888711, "time_ago": "an hour ago", "comments_count": 3, "type": "link", "url": "https://kinsta.com/blog/bootstrapping-startup/", "domain": "kinsta.com" }, { "id": 15797045, "title": "The Poor Man's 3D Camera", "points": 128, "user": "et1337", "time": 1511876037, "time_ago": "4 hours ago", "comments_count": 8, "type": "link", "url": "http://etodd.io/2017/11/28/poor-mans-3d-camera/", "domain": "etodd.io" }, { "id": 15798849, "title": "The Joy of Erlang; Or, How to Ride a Toruk (2011)", "points": 20, "user": "Tomte", "time": 1511887605, "time_ago": "an hour ago", "comments_count": 11, "type": "link", "url": "http://www.evanmiller.org/joy-of-erlang.html", "domain": "evanmiller.org" }, { "id": 15796039, "title": "A Library of Parallel Algorithms", "points": 47, "user": "federicoponzi", "time": 1511866031, "time_ago": "7 hours ago", "comments_count": 0, "type": "link", "url": "https://www.cs.cmu.edu/~scandal/nesl/algorithms.html", "domain": "cs.cmu.edu" }, { "id": 15798238, "title": "Tencent’s Just Getting Started on Online Advertising", "points": 30, "user": "JumpCrisscross", "time": 1511884005, "time_ago": "2 hours ago", "comments_count": 4, "type": "link", "url": "https://www.bloomberg.com/news/articles/2017-11-27/tencent-s-just-getting-started-on-online-advertising", "domain": "bloomberg.com" }, { "id": 15797429, "title": "HP installs system-slowing spyware on its PCs", "points": 253, "user": "artsandsci", "time": 1511878405, "time_ago": "4 hours ago", "comments_count": 152, "type": "link", "url": "https://www.engadget.com/2017/11/28/hp-quietly-installs-system-slowing-spyware-on-its-pcs/", "domain": "engadget.com" }, { "id": 15796769, "title": "Artificial muscle lifts objects 1000x its own weight", "points": 65, "user": "lithander", "time": 1511873750, "time_ago": "5 hours ago", "comments_count": 14, "type": "link", "url": "https://wyss.harvard.edu/artificial-muscles-give-soft-robots-superpowers/", "domain": "wyss.harvard.edu" }, { "id": 15797323, "title": "How Cybercriminals Can Abuse Chat Platform APIs as C&C Infrastructures [pdf]", "points": 51, "user": "lainon", "time": 1511877796, "time_ago": "4 hours ago", "comments_count": 19, "type": "link", "url": "https://documents.trendmicro.com/assets/wp/wp-how-cybercriminals-can-abuse-chat-platform-apis-as-cnc-infrastructures.pdf", "domain": "documents.trendmicro.com" }, { "id": 15797079, "title": "Meshkit", "points": 44, "user": "jdowner", "time": 1511876284, "time_ago": "4 hours ago", "comments_count": 12, "type": "link", "url": "https://www.opengarden.com/meshkit.html", "domain": "opengarden.com" }, { "id": 15798205, "title": "DASH playback of AV1 video in Firefox", "points": 14, "user": "slederer", "time": 1511883813, "time_ago": "2 hours ago", "comments_count": 2, "type": "link", "url": "https://hacks.mozilla.org/2017/11/dash-playback-of-av1-video/", "domain": "hacks.mozilla.org" }, { "id": 15797613, "title": "Brown University raising $120M to eliminate all student loans", "points": 142, "user": "champagnepapi", "time": 1511879987, "time_ago": "3 hours ago", "comments_count": 89, "type": "link", "url": "https://www.cnbc.com/2017/09/25/brown-university-raising-120-million-to-eliminate-all-student-loans.html", "domain": "cnbc.com" }, { "id": 15797184, "title": "A Conversation with John Knoll (1998)", "points": 33, "user": "wallflower", "time": 1511876875, "time_ago": "4 hours ago", "comments_count": 2, "type": "link", "url": "http://www.drdobbs.com/a-conversation-with-john-knoll/184410606", "domain": "drdobbs.com" }, { "id": 15795337, "title": "Disable transparent hugepages", "points": 148, "user": "wheresvic3", "time": 1511857155, "time_ago": "10 hours ago", "comments_count": 47, "type": "link", "url": "https://blog.nelhage.com/post/transparent-hugepages/", "domain": "blog.nelhage.com" }, { "id": 15798424, "title": "Kotlin 1.2 Released: Sharing Code Between Platforms", "points": 70, "user": "dayanruben", "time": 1511885126, "time_ago": "2 hours ago", "comments_count": 14, "type": "link", "url": "https://blog.jetbrains.com/kotlin/2017/11/kotlin-1-2-released/", "domain": "blog.jetbrains.com" }, { "id": 15798960, "title": "Ajit Pai is right", "points": 112, "user": "OberstKrueger", "time": 1511888259, "time_ago": "an hour ago", "comments_count": 98, "type": "link", "url": "https://stratechery.com/2017/why-ajit-pai-is-right/", "domain": "stratechery.com" }, { "id": 15795808, "title": "DRM’s Dead Canary: How We Lost the Web, What We Learned, and What to Do Next", "points": 486, "user": "mimi89999", "time": 1511862904, "time_ago": "8 hours ago", "comments_count": 168, "type": "link", "url": "https://www.eff.org/deeplinks/2017/10/drms-dead-canary-how-we-just-lost-web-what-we-learned-it-and-what-we-need-do-next", "domain": "eff.org" }, { "id": 15795281, "title": "Netherlands fishmongers accuse herring-tasters of erring", "points": 93, "user": "lnguyen", "time": 1511856208, "time_ago": "10 hours ago", "comments_count": 46, "type": "link", "url": "http://www.economist.com/news/europe/21731656-can-dutch-still-trust-their-herring-tasters-netherlands-fishmongers-accuse-herring-tasters", "domain": "economist.com" }, { "id": 15795724, "title": "Firefox Debugger", "points": 183, "user": "Vinnl", "time": 1511861858, "time_ago": "8 hours ago", "comments_count": 58, "type": "link", "url": "https://mozilladevelopers.github.io/playground/debugger", "domain": "mozilladevelopers.github.io" }]; 13 | 14 | render() { 15 | if (this.stories) { 16 | const stories = this.stories.map((story) => { 17 | return ( 18 | 19 |
20 | {story.points || 0} 21 |
22 | 23 |

24 | {story.title} 25 |

26 | 27 | 30 | 31 |
32 |
33 | ) 34 | }); 35 | 36 | return ( 37 | 38 | {stories} 39 | 40 | ) 41 | } else { 42 | return ( 43 | 44 | {this.preLoadData.map((story) => 45 | 46 |
47 | {story.points || 0} 48 |
49 | 50 |

51 | {story.title} 52 |

53 | 54 | 57 | 58 |
59 |
60 | )} 61 |
62 | ) 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/global/variables.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ionic-team/ionic-stencil-hn-app/9b8d2c6927c9b99b285fa2aa0599016eac9f9604/src/global/variables.css -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | IonicHacker 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "IonicHacker", 3 | "short_name": "IonicHacker", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "icons": [{ 7 | "src": "assets/icon.png", 8 | "sizes": "512x512", 9 | "type": "image/png" 10 | }], 11 | "background_color": "#488aff", 12 | "theme_color": "#488aff" 13 | } -------------------------------------------------------------------------------- /stencil.config.js: -------------------------------------------------------------------------------- 1 | const sass = require('@stencil/sass'); 2 | 3 | exports.config = { 4 | plugins: [ 5 | sass() 6 | ], 7 | globalStyle: 'src/global/variables.css', 8 | outputTargets: [ 9 | { 10 | type: "www", 11 | serviceWorker: { 12 | globPatterns: [ 13 | '**/*.{js,css,json,html,ico,png}' 14 | ], 15 | globIgnores: [ 16 | 'build/app/svg/*.js' 17 | ] 18 | } 19 | } 20 | ] 21 | }; 22 | 23 | exports.devServer = { 24 | root: 'www', 25 | watchGlob: '**/**' 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "allowUnreachableCode": false, 5 | "declaration": false, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "dom", 9 | "es2015" 10 | ], 11 | "moduleResolution": "node", 12 | "module": "es2015", 13 | "target": "es2015", 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "jsx": "react", 17 | "jsxFactory": "h" 18 | }, 19 | "include": [ 20 | "src", 21 | "types/jsx.d.ts" 22 | ], 23 | "exclude": [ 24 | "node_modules" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------